diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 00000000..f28471b1
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,8 @@
+# Force LF line endings for all text files, regardless of the checking-out platform's git
+# config (e.g. Windows runners/clients that default core.autocrlf=true). Several tests embed
+# multi-line template literals (verbatim/raw C# strings) and compare rendered output against
+# an explicit \n-based expectation; without this, those literals check out as \r\n on Windows
+# and the comparison fails, since Handlebars.Net no longer rewrites line endings at render time
+# (see issue #661) — line endings in template source are the caller's content now, not something
+# the library silently normalizes.
+* text=auto eol=lf
diff --git a/source/Handlebars.Test/BasicIntegrationTests.cs b/source/Handlebars.Test/BasicIntegrationTests.cs
index 0c64e9b3..d5211f82 100644
--- a/source/Handlebars.Test/BasicIntegrationTests.cs
+++ b/source/Handlebars.Test/BasicIntegrationTests.cs
@@ -482,7 +482,7 @@ public void PathRelativeBinding(IHandlebars handlebars)
};
var result = handlebarsTemplate(data);
- var actual = string.Join(" ", result.Split(new[] { "\n" }, StringSplitOptions.RemoveEmptyEntries).Select(o => o.Trim()));
+ var actual = string.Join(" ", result.Split(new[] { "\r\n" }, StringSplitOptions.RemoveEmptyEntries).Select(o => o.Trim(' ')));
Assert.Equal("Garry Finch gazraa Karen Finch photobasics", actual);
}
@@ -546,7 +546,7 @@ public void PathRelativeBinding_WithDefaultValue(IHandlebars handlebars)
};
var result = handlebarsTemplate(data);
- var actual = string.Join(" ", result.Split(new[] { "\n" }, StringSplitOptions.RemoveEmptyEntries).Select(o => o.Trim()));
+ var actual = string.Join(" ", result.Split(new[] { "\r\n" }, StringSplitOptions.RemoveEmptyEntries).Select(o => o.Trim(' ')));
Assert.Equal("Garry Finch N/A Karen Finch photobasics", actual);
}
diff --git a/source/Handlebars.Test/ComplexIntegrationTests.cs b/source/Handlebars.Test/ComplexIntegrationTests.cs
index da1c26d0..0332c57a 100644
--- a/source/Handlebars.Test/ComplexIntegrationTests.cs
+++ b/source/Handlebars.Test/ComplexIntegrationTests.cs
@@ -56,10 +56,14 @@ public void DeepIf()
var resultTrueFalse = template(trueFalse);
var resultFalseTrue = template(falseTrue);
var resultFalseFalse = template(falseFalse);
- Assert.Equal("a is true\n", resultTrueTrue);
- Assert.Equal("a is false\n", resultTrueFalse);
- Assert.Equal("b is true\n", resultFalseTrue);
- Assert.Equal("b is false\n", resultFalseFalse);
+ Assert.Equal(@"a is true
+", resultTrueTrue);
+ Assert.Equal(@"a is false
+", resultTrueFalse);
+ Assert.Equal(@"b is true
+", resultFalseTrue);
+ Assert.Equal(@"b is false
+", resultFalseFalse);
}
[Fact]
diff --git a/source/Handlebars.Test/IssueTests.cs b/source/Handlebars.Test/IssueTests.cs
index 5396af96..130efdc0 100644
--- a/source/Handlebars.Test/IssueTests.cs
+++ b/source/Handlebars.Test/IssueTests.cs
@@ -225,7 +225,9 @@ public void RenderingWithUnusedPartial()
var transformed = navTemplate(context).Trim();
- Assert.Equal("
\n
Menu Item: Getting Started
\n
", transformed);
+ Assert.Equal(@"
+
Menu Item: Getting Started
+
", transformed);
}
// issue: https://github.com/Handlebars-Net/Handlebars.Net/issues/394
@@ -1270,6 +1272,72 @@ public void Issue601_DefaultInterfaceMemberPropertyIsEnumerated()
Assert.Contains($"OtherStr={data.OtherStr};", result);
}
+ // Issue: https://github.com/Handlebars-Net/Handlebars.Net/issues/661
+ // Static template text must round-trip verbatim: the compiler must not rewrite
+ // line endings the caller put in the template string.
+ //
+ // This mirrors the reporter's exact repro shape: #each over a helper subexpression
+ // (String.Split isn't in this repo, so a local "Split" helper stands in for it) and
+ // the @Key/@Index casing they used — confirmed case-insensitive via ChainSegment's
+ // OrdinalIgnoreCase lookup, so this is equivalent to @key/@index, not a looser stand-in.
+ [Fact]
+ public void Issue661_ExplicitCrLfInTemplateIsPreservedVerbatim()
+ {
+ var handlebars = Handlebars.Create();
+ handlebars.RegisterHelper("Split", (context, arguments) =>
+ ((string) arguments[0]).Split(((string) arguments[1])[0]));
+
+ var template = handlebars.Compile("{{#each (Split \"a;b;c\" ';')}}\r\n{{@Key}}:{{@Index}}:{{this}}\r\n{{/each}}");
+
+ var result = template(new { });
+
+ Assert.Equal("0:0:a\r\n1:1:b\r\n2:2:c\r\n", result);
+ }
+
+ // A bare \n in the template must not be turned into \r\n either — the fix must not
+ // trade under-preservation for over-preservation.
+ [Fact]
+ public void Issue661_ExplicitLfInTemplateIsPreservedVerbatim()
+ {
+ var handlebars = Handlebars.Create();
+ var template = handlebars.Compile("{{#each this}}\n{{@key}}:{{@index}}:{{this}}\n{{/each}}");
+
+ var result = template(new[] { "a", "b", "c" });
+
+ Assert.Equal("0:0:a\n1:1:b\n2:2:c\n", result);
+ }
+
+ // Whatever the platform's native newline is, it must pass through unchanged. On the
+ // CI matrix (macOS/Ubuntu/Windows) this exercises \n and \r\n as separate runs without
+ // any OS-conditional test code.
+ [Fact]
+ public void Issue661_PlatformNewlineInTemplateIsPreservedVerbatim()
+ {
+ var handlebars = Handlebars.Create();
+ var template = handlebars.Compile("line one" + Environment.NewLine + "{{value}}" + Environment.NewLine + "line three");
+
+ var result = template(new { value = "middle" });
+
+ Assert.Equal("line one" + Environment.NewLine + "middle" + Environment.NewLine + "line three", result);
+ }
+
+ // Indented partials must preserve \r\n in the partial's own content too — WriteWithIndent
+ // used to normalise to \n while everything else passed content through verbatim.
+ [Fact]
+ public void Issue661_IndentedPartialPreservesCrLf()
+ {
+ var handlebars = Handlebars.Create();
+ using (var reader = new StringReader("one\r\ntwo\r\nthree"))
+ {
+ handlebars.RegisterTemplate("myPartial", handlebars.Compile(reader));
+ }
+ var template = handlebars.Compile(" {{> myPartial}}");
+
+ var result = template(new { });
+
+ Assert.Equal(" one\r\n two\r\n three", result);
+ }
+
// Issue: https://github.com/Handlebars-Net/Handlebars.Net/issues/660
// A writer-based helper used as a subexpression must hand its captured output to the
// outer helper as a plain System.String, not an opaque internal wrapper type — otherwise
diff --git a/source/Handlebars.Test/PartialTests.cs b/source/Handlebars.Test/PartialTests.cs
index 00f7a868..41848ae6 100644
--- a/source/Handlebars.Test/PartialTests.cs
+++ b/source/Handlebars.Test/PartialTests.cs
@@ -950,7 +950,7 @@ public void PartialIndentationIsAppliedInsideBlock()
}
[Fact]
- public void PartialWithCrLfLineEndingsNormalisedToLf()
+ public void PartialWithCrLfLineEndingsPreservedVerbatim()
{
var handlebars = Handlebars.Create();
var source = " {{> p}}";
@@ -963,8 +963,8 @@ public void PartialWithCrLfLineEndingsNormalisedToLf()
var result = handlebars.Compile(source)(new { });
- // \r\n in the partial source is normalised to \n; every line gets the indent.
- Assert.Equal(" line1\n line2\n line3", result);
+ // \r\n in the partial source is preserved verbatim; every line gets the indent (issue #661).
+ Assert.Equal(" line1\r\n line2\r\n line3", result);
}
[Fact]
diff --git a/source/Handlebars.Test/ReadmeTests.cs b/source/Handlebars.Test/ReadmeTests.cs
index 8966be8e..52589baa 100644
--- a/source/Handlebars.Test/ReadmeTests.cs
+++ b/source/Handlebars.Test/ReadmeTests.cs
@@ -29,14 +29,14 @@ public void RegisterBlockHelper()
{"Chewy", "hamster" }
};
- var template = "{{#each this}}The animal, {{@key}}, {{#StringEqualityBlockHelper @value 'dog'}}is a dog{{else}}is not a dog{{/StringEqualityBlockHelper}}.\n{{/each}}";
+ var template = "{{#each this}}The animal, {{@key}}, {{#StringEqualityBlockHelper @value 'dog'}}is a dog{{else}}is not a dog{{/StringEqualityBlockHelper}}.\r\n{{/each}}";
var compiledTemplate = handlebars.Compile(template);
string templateOutput = compiledTemplate(animals);
Assert.Equal(
- "The animal, Fluffy, is not a dog.\n" +
- "The animal, Fido, is a dog.\n" +
- "The animal, Chewy, is not a dog.\n",
+ "The animal, Fluffy, is not a dog.\r\n" +
+ "The animal, Fido, is a dog.\r\n" +
+ "The animal, Chewy, is not a dog.\r\n",
templateOutput
);
}
diff --git a/source/Handlebars.Test/ViewEngine/ViewEngineTests.cs b/source/Handlebars.Test/ViewEngine/ViewEngineTests.cs
index fb3b21c0..71bfe339 100644
--- a/source/Handlebars.Test/ViewEngine/ViewEngineTests.cs
+++ b/source/Handlebars.Test/ViewEngine/ViewEngineTests.cs
@@ -17,7 +17,7 @@ public void CanLoadAViewWithALayout()
//Given a layout in a subfolder
var files = new FakeFileSystem()
{
- {"views\\somelayout.hbs", "layout start\n{{{body}}}\nlayout end"},
+ {"views\\somelayout.hbs", "layout start\r\n{{{body}}}\r\nlayout end"},
//And a view in the same folder which uses that layout
{ "views\\someview.hbs", "{{!< somelayout}}This is the body"}
};
@@ -28,7 +28,7 @@ public void CanLoadAViewWithALayout()
var output = renderView(null);
//Then the correct output should be rendered
- Assert.Equal("layout start\nThis is the body\nlayout end", output);
+ Assert.Equal("layout start\r\nThis is the body\r\nlayout end", output);
}
[Fact]
public void CanLoadAWriterViewWithALayout()
@@ -36,7 +36,7 @@ public void CanLoadAWriterViewWithALayout()
//Given a layout in a subfolder
var files = new FakeFileSystem()
{
- {"views\\somelayout.hbs", "layout start\n{{{body}}}\nlayout end"},
+ {"views\\somelayout.hbs", "layout start\r\n{{{body}}}\r\nlayout end"},
//And a view in the same folder which uses that layout
{ "views\\someview.hbs", "{{!< somelayout}}This is the body"}
};
@@ -50,7 +50,7 @@ public void CanLoadAWriterViewWithALayout()
var output = sb.ToString();
//Then the correct output should be rendered
- Assert.Equal("layout start\nThis is the body\nlayout end", output);
+ Assert.Equal("layout start\r\nThis is the body\r\nlayout end", output);
}
[Fact]
@@ -59,7 +59,7 @@ public void CanLoadAViewWithALayoutInTheRoot()
//Given a layout in the root
var files = new FakeFileSystem()
{
- {"somelayout.hbs", "layout start\n{{{body}}}\nlayout end"},
+ {"somelayout.hbs", "layout start\r\n{{{body}}}\r\nlayout end"},
//And a view in a subfolder folder which uses that layout
{ "views\\someview.hbs", "{{!< somelayout}}This is the body"}
};
@@ -70,7 +70,7 @@ public void CanLoadAViewWithALayoutInTheRoot()
var output = render(null);
//Then the correct output should be rendered
- Assert.Equal("layout start\nThis is the body\nlayout end", output);
+ Assert.Equal("layout start\r\nThis is the body\r\nlayout end", output);
}
[Fact]
@@ -81,9 +81,9 @@ public void CanRenderInlineBlocks()
var files = new FakeFileSystem()
{
//Given a layout in a subfolder
- { "partials/layout.hbs", "
\n{{> nav}}\n
\n
\n{{> content}}\n
"},
+ { "partials/layout.hbs", "
\r\n{{> nav}}\r\n
\r\n
\r\n{{> content}}\r\n
"},
- { "template.hbs", "{{#> layout}}\n{{#*inline \"nav\"}}\n{{Text}}\n{{/inline}}\n{{#*inline \"content\"}}\nMy Content\n{{/inline}}\n{{/layout}}"}
+ { "template.hbs", "{{#> layout}}\r\n{{#*inline \"nav\"}}\r\n{{Text}}\r\n{{/inline}}\r\n{{#*inline \"content\"}}\r\nMy Content\r\n{{/inline}}\r\n{{/layout}}"}
};
//When a viewengine renders that view
@@ -95,7 +95,7 @@ public void CanRenderInlineBlocks()
});
//Then the correct output should be rendered
- Assert.Equal("
\n<My Nav>\n
\n
\nMy Content\n
", output);
+ Assert.Equal("
\r\n<My Nav>\r\n
\r\n
\r\nMy Content\r\n
", output);
}
[Fact]
@@ -104,7 +104,7 @@ public void CanLoadAViewWithALayoutWithAVariable()
//Given a layout in the root
var files = new FakeFileSystem()
{
- {"somelayout.hbs", "{{var1}} start\n{{{body}}}\n{{var1}} end"},
+ {"somelayout.hbs", "{{var1}} start\r\n{{{body}}}\r\n{{var1}} end"},
//And a view in a subfolder folder which uses that layout
{ "views\\someview.hbs", "{{!< somelayout}}This is the {{var2}}"}
};
@@ -115,7 +115,7 @@ public void CanLoadAViewWithALayoutWithAVariable()
var output = renderView(new { var1 = "layout", var2 = "body" });
//Then the correct output should be rendered
- Assert.Equal("layout start\nThis is the body\nlayout end", output);
+ Assert.Equal("layout start\r\nThis is the body\r\nlayout end", output);
}
[Fact]
@@ -124,7 +124,7 @@ public void CanLoadAViewWithALayoutInTheRootWithAVariable()
//Given a layout in the root
var files = new FakeFileSystem()
{
- {"somelayout.hbs", "{{var1}} start\n{{{body}}}\n{{var1}} end"},
+ {"somelayout.hbs", "{{var1}} start\r\n{{{body}}}\r\n{{var1}} end"},
//And a view in a subfolder folder which uses that layout
{ "views\\someview.hbs", "{{!< somelayout}}This is the {{var2}}"}
};
@@ -135,7 +135,7 @@ public void CanLoadAViewWithALayoutInTheRootWithAVariable()
var output = render(new { var1 = "layout", var2 = "body" });
//Then the correct output should be rendered
- Assert.Equal("layout start\nThis is the body\nlayout end", output);
+ Assert.Equal("layout start\r\nThis is the body\r\nlayout end", output);
}
[Fact]
@@ -162,8 +162,8 @@ public void CanIgnoreCommentsContainingHtml()
{
var files = new FakeFileSystem()
{
- { "views\\layout.hbs", "Start\n{{{body}}}\nEnd" },
- { "views\\someview.hbs", "{{!< layout}}\n\nTemplate\n{{!--\n