Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -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
4 changes: 2 additions & 2 deletions source/Handlebars.Test/BasicIntegrationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down Expand Up @@ -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);
}

Expand Down
12 changes: 8 additions & 4 deletions source/Handlebars.Test/ComplexIntegrationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
70 changes: 69 additions & 1 deletion source/Handlebars.Test/IssueTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,9 @@

var transformed = navTemplate(context).Trim();

Assert.Equal("<div>\n <div>Menu Item: Getting Started</div>\n</div>", transformed);
Assert.Equal(@"<div>
<div>Menu Item: Getting Started</div>
</div>", transformed);
}

// issue: https://github.com/Handlebars-Net/Handlebars.Net/issues/394
Expand Down Expand Up @@ -1270,6 +1272,72 @@
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]));

Check warning on line 1288 in source/Handlebars.Test/IssueTests.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Converting null literal or possible null value to non-nullable type.

See more on https://sonarcloud.io/project/issues?id=Handlebars-Net_Handlebars.Net&issues=AZ_Zq4VIsEtYd74_TnW-&open=AZ_Zq4VIsEtYd74_TnW-&pullRequest=663

Check warning on line 1288 in source/Handlebars.Test/IssueTests.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Converting null literal or possible null value to non-nullable type.

See more on https://sonarcloud.io/project/issues?id=Handlebars-Net_Handlebars.Net&issues=AZ_Zq4VIsEtYd74_TnXA&open=AZ_Zq4VIsEtYd74_TnXA&pullRequest=663

Check warning on line 1288 in source/Handlebars.Test/IssueTests.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Dereference of a possibly null reference.

See more on https://sonarcloud.io/project/issues?id=Handlebars-Net_Handlebars.Net&issues=AZ_Zq4VIsEtYd74_TnXB&open=AZ_Zq4VIsEtYd74_TnXB&pullRequest=663

Check warning on line 1288 in source/Handlebars.Test/IssueTests.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Dereference of a possibly null reference.

See more on https://sonarcloud.io/project/issues?id=Handlebars-Net_Handlebars.Net&issues=AZ_Zq4VIsEtYd74_TnW_&open=AZ_Zq4VIsEtYd74_TnW_&pullRequest=663

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" });

Check warning on line 1305 in source/Handlebars.Test/IssueTests.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer 'static readonly' fields over constant array arguments if the called method is called repeatedly and is not mutating the passed array

See more on https://sonarcloud.io/project/issues?id=Handlebars-Net_Handlebars.Net&issues=AZ_Zq4VIsEtYd74_TnXC&open=AZ_Zq4VIsEtYd74_TnXC&pullRequest=663

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
Expand Down
6 changes: 3 additions & 3 deletions source/Handlebars.Test/PartialTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -950,7 +950,7 @@ public void PartialIndentationIsAppliedInsideBlock()
}

[Fact]
public void PartialWithCrLfLineEndingsNormalisedToLf()
public void PartialWithCrLfLineEndingsPreservedVerbatim()
{
var handlebars = Handlebars.Create();
var source = " {{> p}}";
Expand All @@ -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]
Expand Down
8 changes: 4 additions & 4 deletions source/Handlebars.Test/ReadmeTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
);
}
Expand Down
58 changes: 29 additions & 29 deletions source/Handlebars.Test/ViewEngine/ViewEngineTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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"}
};
Expand All @@ -28,15 +28,15 @@ 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()
{
//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"}
};
Expand All @@ -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]
Expand All @@ -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"}
};
Expand All @@ -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]
Expand All @@ -81,9 +81,9 @@ public void CanRenderInlineBlocks()
var files = new FakeFileSystem()
{
//Given a layout in a subfolder
{ "partials/layout.hbs", "<div class=\"nav\">\n{{> nav}}\n</div>\n<div class=\"content\">\n{{> content}}\n</div>"},
{ "partials/layout.hbs", "<div class=\"nav\">\r\n{{> nav}}\r\n</div>\r\n<div class=\"content\">\r\n{{> content}}\r\n</div>"},

{ "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
Expand All @@ -95,7 +95,7 @@ public void CanRenderInlineBlocks()
});

//Then the correct output should be rendered
Assert.Equal("<div class=\"nav\">\n&lt;My Nav&gt;\n</div>\n<div class=\"content\">\nMy Content\n</div>", output);
Assert.Equal("<div class=\"nav\">\r\n&lt;My Nav&gt;\r\n</div>\r\n<div class=\"content\">\r\nMy Content\r\n</div>", output);
}

[Fact]
Expand All @@ -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}}"}
};
Expand All @@ -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]
Expand All @@ -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}}"}
};
Expand All @@ -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]
Expand All @@ -162,25 +162,25 @@ public void CanIgnoreCommentsContainingHtml()
{
var files = new FakeFileSystem()
{
{ "views\\layout.hbs", "Start\n{{{body}}}\nEnd" },
{ "views\\someview.hbs", "{{!< layout}}\n\nTemplate\n{{!--\n<div>Commented out HTML</div>\n--}}" },
{ "views\\layout.hbs", "Start\r\n{{{body}}}\r\nEnd" },
{ "views\\someview.hbs", "{{!< layout}}\r\n\r\nTemplate\r\n{{!--\r\n<div>Commented out HTML</div>\r\n--}}" },
};

var handlebarsConfiguration = new HandlebarsConfiguration() { FileSystem = files };
var handlebars = Handlebars.Create(handlebarsConfiguration);
var render = handlebars.CompileView("views\\someview.hbs");
var output = render(null);

Assert.Equal("Start\n\nTemplate\n\nEnd", output);
Assert.Equal("Start\r\n\r\nTemplate\r\n\r\nEnd", output);
}

[Fact]
public void CanUseDictionaryModelInLayout()
{
var files = new FakeFileSystem
{
{ "views\\layout.hbs", "Layout: {{property}}\n{{{body}}}" },
{ "views\\someview.hbs", "{{!< layout}}\n\nBody: {{property}}" },
{ "views\\layout.hbs", "Layout: {{property}}\r\n{{{body}}}" },
{ "views\\someview.hbs", "{{!< layout}}\r\n\r\nBody: {{property}}" },
};

var handlebarsConfiguration = new HandlebarsConfiguration { FileSystem = files };
Expand All @@ -193,16 +193,16 @@ public void CanUseDictionaryModelInLayout()
}
);

Assert.Equal("Layout: Foo\n\nBody: Foo", output);
Assert.Equal("Layout: Foo\r\n\r\nBody: Foo", output);
}

[Fact]
public void CanUseDynamicModelInLayout()
{
var files = new FakeFileSystem
{
{ "views\\layout.hbs", "Layout: {{property}}\n{{{body}}}" },
{ "views\\someview.hbs", "{{!< layout}}\n\nBody: {{property}}" },
{ "views\\layout.hbs", "Layout: {{property}}\r\n{{{body}}}" },
{ "views\\someview.hbs", "{{!< layout}}\r\n\r\nBody: {{property}}" },
};

dynamic model = new MyDynamicModel();
Expand All @@ -211,17 +211,17 @@ public void CanUseDynamicModelInLayout()
var render = handlebars.CompileView("views\\someview.hbs");
var output = render(model);

Assert.Equal("Layout: Foo\n\nBody: Foo", output);
Assert.Equal("Layout: Foo\r\n\r\nBody: Foo", output);
}

[Fact]
public void CanBindToModelInNestedLayout()
{
var files = new FakeFileSystem
{
{ "views\\parent_layout.hbs", "Parent layout: {{property}}\n{{{body}}}" },
{ "views\\layout.hbs", "{{!< parent_layout}}\nLayout: {{property}}\n{{{body}}}" },
{ "views\\someview.hbs", "{{!< layout}}\n\nBody: {{property}}" },
{ "views\\parent_layout.hbs", "Parent layout: {{property}}\r\n{{{body}}}" },
{ "views\\layout.hbs", "{{!< parent_layout}}\r\nLayout: {{property}}\r\n{{{body}}}" },
{ "views\\someview.hbs", "{{!< layout}}\r\n\r\nBody: {{property}}" },
};

var handlebarsConfiguration = new HandlebarsConfiguration { FileSystem = files };
Expand All @@ -234,24 +234,24 @@ public void CanBindToModelInNestedLayout()
}
);

Assert.Equal("Parent layout: Foo\nLayout: Foo\n\nBody: Foo", output);
Assert.Equal("Parent layout: Foo\r\nLayout: Foo\r\n\r\nBody: Foo", output);
}

[Fact]
public void CanUseNullModelInLayout()
{
var files = new FakeFileSystem
{
{ "views\\layout.hbs", "Layout: {{property}}\n{{{body}}}" },
{ "views\\someview.hbs", "{{!< layout}}\n\nBody: {{property}}" },
{ "views\\layout.hbs", "Layout: {{property}}\r\n{{{body}}}" },
{ "views\\someview.hbs", "{{!< layout}}\r\n\r\nBody: {{property}}" },
};

var handlebarsConfiguration = new HandlebarsConfiguration { FileSystem = files };
var handlebars = Handlebars.Create(handlebarsConfiguration);
var render = handlebars.CompileView("views\\someview.hbs");
var output = render(null);

Assert.Equal("Layout: \n\nBody: ", output);
Assert.Equal("Layout: \r\n\r\nBody: ", output);
}

[Fact]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,7 @@ public override IEnumerable<object> ConvertTokens(IEnumerable<object> sequence)

if (staticToken.Value != string.Empty)
{
// Normalize to \n so output is platform-independent regardless of source file line endings.
var value = staticToken.Value.IndexOf('\r') >= 0
? staticToken.Value.Replace("\r\n", "\n").Replace("\r", "\n")
: staticToken.Value;
yield return HandlebarsExpression.Static(value);
yield return HandlebarsExpression.Static(staticToken.Value);
}
}
}
Expand Down
Loading
Loading