From e7f40affacebb3d819d8327c025a3428f8b8e1a5 Mon Sep 17 00:00:00 2001
From: Andreas Zerbst <73799582+andr317c@users.noreply.github.com>
Date: Tue, 19 Mar 2024 13:14:38 +0100
Subject: [PATCH] V14 Bugfix ensures correct line endings for partial view
snippets (#15906)
* Created extension class so we can ensure native line endings
* Added usage of extension method for ensuring native line endings
* Added tests, to see if the snippets return the correct content
* Removed space
---
.../Extensions/LineEndingsExtensions.cs | 50 +++++++++++++++++++
.../PartialViewSnippetCollectionBuilder.cs | 2 +
.../Settings/PartialView/PartialView.spec.ts | 19 +++++--
3 files changed, 67 insertions(+), 4 deletions(-)
create mode 100644 src/Umbraco.Core/Extensions/LineEndingsExtensions.cs
diff --git a/src/Umbraco.Core/Extensions/LineEndingsExtensions.cs b/src/Umbraco.Core/Extensions/LineEndingsExtensions.cs
new file mode 100644
index 0000000000..f074c27776
--- /dev/null
+++ b/src/Umbraco.Core/Extensions/LineEndingsExtensions.cs
@@ -0,0 +1,50 @@
+using System.Runtime.InteropServices;
+
+namespace Umbraco.Cms.Core.Extensions;
+
+public static class LineEndingsExtensions
+{
+ ///
+ /// Ensures Lf only everywhere.
+ ///
+ /// The text to filter.
+ /// The filtered text.
+ private static string Lf(string text)
+ {
+ if (string.IsNullOrEmpty(text))
+ {
+ return text;
+ }
+
+ text = text.Replace("\r", string.Empty); // remove CR
+ return text;
+ }
+
+ ///
+ /// Ensures CrLf everywhere.
+ ///
+ /// The text to filter.
+ /// The filtered text.
+ private static string CrLf(string text)
+ {
+ if (string.IsNullOrEmpty(text))
+ {
+ return text;
+ }
+
+ text = text.Replace("\r", string.Empty); // remove CR
+ text = text.Replace("\n", "\r\n"); // add CRLF everywhere
+ return text;
+ }
+
+ ///
+ /// Ensures native line endings.
+ ///
+ /// the text to ensure native line endings for.
+ /// the text with native line endings.
+ public static string EnsureNativeLineEndings(this string text)
+ {
+ var useCrLf = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
+ return useCrLf ? CrLf(text) : Lf(text);
+ }
+}
diff --git a/src/Umbraco.Core/Snippets/PartialViewSnippetCollectionBuilder.cs b/src/Umbraco.Core/Snippets/PartialViewSnippetCollectionBuilder.cs
index 31f33cf6f2..9e9746afe4 100644
--- a/src/Umbraco.Core/Snippets/PartialViewSnippetCollectionBuilder.cs
+++ b/src/Umbraco.Core/Snippets/PartialViewSnippetCollectionBuilder.cs
@@ -2,6 +2,7 @@ using System.Text.RegularExpressions;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.FileProviders;
using Umbraco.Cms.Core.Composing;
+using Umbraco.Cms.Core.Extensions;
using Umbraco.Cms.Core.Strings;
using Umbraco.Extensions;
@@ -38,6 +39,7 @@ public partial class PartialViewSnippetCollectionBuilder : LazyCollectionBuilder
private string CleanUpSnippetContent(string content)
{
const string partialViewHeader = "@inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage";
+ content = content.EnsureNativeLineEndings();
// Strip the @inherits if it's there
Regex headerMatch = HeaderRegex();
diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/PartialView/PartialView.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/PartialView/PartialView.spec.ts
index 74f539e2cf..18afe4842a 100644
--- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/PartialView/PartialView.spec.ts
+++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/PartialView/PartialView.spec.ts
@@ -33,9 +33,10 @@ test.describe('Partial View tests', () => {
await expect(umbracoUi.partialView.checkItemNameUnderPartialViewTree(partialViewFileName)).toBeVisible();
})
- test.skip('can create a partial view from snippet', async ({umbracoApi, umbracoUi}) => {
+ test('can create a partial view from snippet', async ({umbracoApi, umbracoUi}) => {
// Arrange
- const expectedPartialViewContent = '@inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage\n@using Umbraco.Cms.Core.Routing\n@using Umbraco.Extensions\n\n@inject IPublishedUrlProvider PublishedUrlProvider\n@*\n This snippet makes a breadcrumb of parents using an unordered HTML list.\n\n How it works:\n - It uses the Ancestors() method to get all parents and then generates links so the visitor can go back\n - Finally it outputs the name of the current page (without a link)\n*@\n\n@{ var selection = Model.Ancestors().ToArray(); }\n\n@if (selection?.Length > 0)\n{\n
\n @* For each page in the ancestors collection which have been ordered by Level (so we start with the highest top node first) *@\n @foreach (var item in selection.OrderBy(x => x.Level))\n {\n - @item.Name /
\n }\n\n @* Display the current page as the last item in the list *@\n - @Model.Name
\n
\n}';
+ const expectedPartialViewContentWindows = '@inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage\r\n@using Umbraco.Cms.Core.Routing\r\n@using Umbraco.Extensions\r\n\n@inject IPublishedUrlProvider PublishedUrlProvider\r\n@*\r\n This snippet makes a breadcrumb of parents using an unordered HTML list.\r\n\r\n How it works:\r\n - It uses the Ancestors() method to get all parents and then generates links so the visitor can go back\r\n - Finally it outputs the name of the current page (without a link)\r\n*@\r\n\r\n@{ var selection = Model.Ancestors().ToArray(); }\r\n\r\n@if (selection?.Length > 0)\r\n{\r\n \r\n @* For each page in the ancestors collection which have been ordered by Level (so we start with the highest top node first) *@\r\n @foreach (var item in selection.OrderBy(x => x.Level))\r\n {\r\n - @item.Name /
\r\n }\r\n\r\n @* Display the current page as the last item in the list *@\r\n - @Model.Name
\r\n
\r\n}';
+ const expectedPartialViewContentLinux = '@inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage\n@using Umbraco.Cms.Core.Routing\n@using Umbraco.Extensions\n\n@inject IPublishedUrlProvider PublishedUrlProvider\n@*\n This snippet makes a breadcrumb of parents using an unordered HTML list.\n\n How it works:\n - It uses the Ancestors() method to get all parents and then generates links so the visitor can go back\n - Finally it outputs the name of the current page (without a link)\n*@\n\n@{ var selection = Model.Ancestors().ToArray(); }\n\n@if (selection?.Length > 0)\n{\n \n @* For each page in the ancestors collection which have been ordered by Level (so we start with the highest top node first) *@\n @foreach (var item in selection.OrderBy(x => x.Level))\n {\n - @item.Name /
\n }\n\n @* Display the current page as the last item in the list *@\n - @Model.Name
\n
\n}';
// Act
await umbracoUi.partialView.clickActionsMenuAtRoot();
@@ -50,8 +51,18 @@ test.describe('Partial View tests', () => {
await umbracoUi.partialView.isSuccessNotificationVisible();
expect(await umbracoApi.partialView.doesExist(partialViewFileName)).toBeTruthy();
const partialViewData = await umbracoApi.partialView.getByName(partialViewFileName);
- expect(partialViewData.content.length).toBe(expectedPartialViewContent.length);
- expect(partialViewData.content).toBe(expectedPartialViewContent);
+
+ switch (process.platform) {
+ case 'win32':
+ expect(partialViewData.content).toBe(expectedPartialViewContentWindows);
+ break;
+ case 'linux':
+ expect(partialViewData.content).toBe(expectedPartialViewContentLinux);
+ break;
+ default:
+ throw new Error(`Untested platform: ${process.platform}`);
+ }
+
// Verify the new partial view is displayed under the Partial Views section
await umbracoUi.partialView.clickRootFolderCaretButton();
await expect(umbracoUi.partialView.checkItemNameUnderPartialViewTree(partialViewFileName)).toBeVisible();