diff --git a/source/Handlebars.Test/BasicIntegrationTests.cs b/source/Handlebars.Test/BasicIntegrationTests.cs
index e6b9b659..bb2882c2 100644
--- a/source/Handlebars.Test/BasicIntegrationTests.cs
+++ b/source/Handlebars.Test/BasicIntegrationTests.cs
@@ -437,7 +437,7 @@ public void PathRelativeBinding(IHandlebars handlebars)
};
var result = handlebarsTemplate(data);
- var actual = string.Join(" ", result.Split(new[] { "\r\n" }, StringSplitOptions.RemoveEmptyEntries).Select(o => o.Trim(' ')));
+ var actual = string.Join(" ", result.Split(new[] { "\n" }, StringSplitOptions.RemoveEmptyEntries).Select(o => o.Trim()));
Assert.Equal("Garry Finch gazraa Karen Finch photobasics", actual);
}
@@ -501,7 +501,7 @@ public void PathRelativeBinding_WithDefaultValue(IHandlebars handlebars)
};
var result = handlebarsTemplate(data);
- var actual = string.Join(" ", result.Split(new[] { "\r\n" }, StringSplitOptions.RemoveEmptyEntries).Select(o => o.Trim(' ')));
+ var actual = string.Join(" ", result.Split(new[] { "\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 2f365b86..bed68944 100644
--- a/source/Handlebars.Test/ComplexIntegrationTests.cs
+++ b/source/Handlebars.Test/ComplexIntegrationTests.cs
@@ -56,14 +56,10 @@ public void DeepIf()
var resultTrueFalse = template(trueFalse);
var resultFalseTrue = template(falseTrue);
var resultFalseFalse = template(falseFalse);
- Assert.Equal(@"a is true
-", resultTrueTrue);
- Assert.Equal(@"a is false
-", resultTrueFalse);
- Assert.Equal(@"b is true
-", resultFalseTrue);
- Assert.Equal(@"b is false
-", resultFalseFalse);
+ 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);
}
[Fact]
diff --git a/source/Handlebars.Test/IssueTests.cs b/source/Handlebars.Test/IssueTests.cs
index 911dfec9..ae57657c 100644
--- a/source/Handlebars.Test/IssueTests.cs
+++ b/source/Handlebars.Test/IssueTests.cs
@@ -147,17 +147,11 @@ End outer partial block
var callback = handlebars.Compile(view);
string result = callback(new object());
- const string expected = @"Begin outer partial
- Begin outer partial block
-
- Begin inner partial
- Begin inner partial block
- View
- End inner partial block
- End inner partial
- End outer partial block
- End outer partial";
-
+ // Issue #614: partial indentation is now preserved (Handlebars.js behaviour).
+ // Each standalone {{>@partial-block}} applies its own leading whitespace as indent
+ // to every line of the rendered block content.
+ const string expected = "Begin outer partial \n Begin outer partial block\n \n Begin inner partial \n Begin inner partial block \n View \n End inner partial block \n End inner partial \n End outer partial block \n End outer partial";
+
Assert.Equal(expected, result);
}
@@ -231,9 +225,7 @@ public void RenderingWithUnusedPartial()
var transformed = navTemplate(context).Trim();
- Assert.Equal(@"
-
Menu Item: Getting Started
-
", transformed);
+ Assert.Equal("
\n
Menu Item: Getting Started
\n
", transformed);
}
// issue: https://github.com/Handlebars-Net/Handlebars.Net/issues/394
diff --git a/source/Handlebars.Test/Issues/Issue614Tests.cs b/source/Handlebars.Test/Issues/Issue614Tests.cs
new file mode 100644
index 00000000..ed5a98f1
--- /dev/null
+++ b/source/Handlebars.Test/Issues/Issue614Tests.cs
@@ -0,0 +1,190 @@
+using System.IO;
+using Xunit;
+
+namespace HandlebarsDotNet.Test
+{
+ ///
+ /// Tests for issue #614 — Partial indentation not preserved.
+ /// When {{> partial}} is indented with spaces/tabs on its own line, every line of the
+ /// rendered partial should be prefixed with that same indentation, matching Handlebars.js behavior.
+ ///
+ public class Issue614Tests
+ {
+ private readonly IHandlebars _handlebars;
+
+ public Issue614Tests()
+ {
+ _handlebars = Handlebars.Create();
+ }
+
+ ///
+ /// Spec section 20.12: Template " {{> p}}" + Partial "line1\nline2" => " line1\n line2"
+ /// The two leading spaces become the indentation for every line of the partial output.
+ ///
+ [Fact]
+ public void InlinePartialSpecExample()
+ {
+ var source = " {{> p}}";
+ var partialSource = "line1\nline2";
+
+ using (var reader = new StringReader(partialSource))
+ {
+ _handlebars.RegisterTemplate("p", _handlebars.Compile(reader));
+ }
+
+ var result = _handlebars.Compile(source)(new { });
+
+ Assert.Equal(" line1\n line2", result);
+ }
+
+ ///
+ /// The two-space indent before {{> user}} is applied to each line of the partial output.
+ /// The trailing newline after the standalone partial invocation is stripped (standalone behaviour).
+ ///
+ [Fact]
+ public void PartialIndentationWithMultiLinePartial()
+ {
+ var source = "Start\n {{> content}}\nEnd";
+ var partialSource = "line1\nline2\nline3";
+
+ using (var reader = new StringReader(partialSource))
+ {
+ _handlebars.RegisterTemplate("content", _handlebars.Compile(reader));
+ }
+
+ var result = _handlebars.Compile(source)(new { });
+
+ // TrimAfter strips the \n between the partial tag and "End", so the output is:
+ // "Start\n" + " line1\n line2\n line3" + "End"
+ Assert.Equal("Start\n line1\n line2\n line3End", result);
+ }
+
+ ///
+ /// A tab character before the partial invocation is used as the indentation.
+ ///
+ [Fact]
+ public void PartialIndentationWithTabCharacter()
+ {
+ var source = "Start\n\t{{> content}}\nEnd";
+ var partialSource = "line1\nline2";
+
+ using (var reader = new StringReader(partialSource))
+ {
+ _handlebars.RegisterTemplate("content", _handlebars.Compile(reader));
+ }
+
+ var result = _handlebars.Compile(source)(new { });
+
+ Assert.Equal("Start\n\tline1\n\tline2End", result);
+ }
+
+ ///
+ /// A partial with no preceding whitespace receives no indentation.
+ /// The newline that follows the standalone partial tag is stripped.
+ ///
+ [Fact]
+ public void PartialWithNoIndentationUnchanged()
+ {
+ var source = "Hello\n{{> greeting}}\nBye";
+ var partialSource = "World";
+
+ using (var reader = new StringReader(partialSource))
+ {
+ _handlebars.RegisterTemplate("greeting", _handlebars.Compile(reader));
+ }
+
+ var result = _handlebars.Compile(source)(new { });
+
+ // Standalone with no indent: TrimAfter strips \nBye → Bye, no indent added.
+ Assert.Equal("Hello\nWorldBye", result);
+ }
+
+ ///
+ /// The indentation is applied inside a block helper iteration.
+ /// A single-line partial produces indented output per iteration;
+ /// iterations are not separated because the newline after {{> user}} is stripped.
+ ///
+ [Fact]
+ public void PartialIndentationIsAppliedInsideBlock()
+ {
+ var source = "
Names
\n{{#names}}\n {{> user}}\n{{/names}}";
+ var partialSource = "{{name}}";
+
+ using (var reader = new StringReader(partialSource))
+ {
+ _handlebars.RegisterTemplate("user", _handlebars.Compile(reader));
+ }
+
+ var template = _handlebars.Compile(source);
+ var data = new
+ {
+ names = new[]
+ {
+ new { name = "Karen" },
+ new { name = "Jon" }
+ }
+ };
+
+ var result = template(data);
+
+ // The standalone \n after {{> user}} is stripped; iterations are concatenated directly.
+ // Each partial invocation outputs " Name" (indent applied).
+ Assert.Equal("
Names
\n KarenJon", result);
+ }
+
+ ///
+ /// A partial whose source uses Windows-style \r\n line endings (e.g. checked out on Windows
+ /// with git autocrlf=true, or produced by a StringWriter whose NewLine is \r\n) is normalised
+ /// to \n in the indented output. The library always emits \n as the line separator so that
+ /// rendered output is identical across platforms.
+ ///
+ [Fact]
+ public void PartialWithCrLfLineEndingsNormalisedToLf()
+ {
+ var source = " {{> p}}";
+ var partialSource = "line1\r\nline2\r\nline3"; // Windows-style \r\n
+
+ using (var reader = new StringReader(partialSource))
+ {
+ _handlebars.RegisterTemplate("p", _handlebars.Compile(reader));
+ }
+
+ 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);
+ }
+
+ ///
+ /// A multi-line partial called inside an iteration — each line of each iteration is indented.
+ ///
+ [Fact]
+ public void MultiLinePartialIndentationInsideBlock()
+ {
+ var source = "{{#items}}\n {{> row}}\n{{/items}}";
+ var partialSource = "- {{name}}\n ({{desc}})";
+
+ using (var reader = new StringReader(partialSource))
+ {
+ _handlebars.RegisterTemplate("row", _handlebars.Compile(reader));
+ }
+
+ var template = _handlebars.Compile(source);
+ var data = new
+ {
+ items = new[]
+ {
+ new { name = "A", desc = "alpha" },
+ new { name = "B", desc = "beta" }
+ }
+ };
+
+ var result = template(data);
+
+ // Each 2-line partial gets " " prepended to both lines.
+ // The newline between the partial tag and the next iteration/closing tag is stripped.
+ Assert.Equal(" - A\n (alpha) - B\n (beta)", result);
+ }
+
+ }
+}
diff --git a/source/Handlebars.Test/ReadmeTests.cs b/source/Handlebars.Test/ReadmeTests.cs
index 3b71e080..c41d9a3c 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}}.\r\n{{/each}}";
+ var template = "{{#each this}}The animal, {{@key}}, {{#StringEqualityBlockHelper @value 'dog'}}is a dog{{else}}is not a dog{{/StringEqualityBlockHelper}}.\n{{/each}}";
var compiledTemplate = handlebars.Compile(template);
string templateOutput = compiledTemplate(animals);
Assert.Equal(
- "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",
+ "The animal, Fluffy, is not a dog.\n" +
+ "The animal, Fido, is a dog.\n" +
+ "The animal, Chewy, is not a dog.\n",
templateOutput
);
}
diff --git a/source/Handlebars.Test/ViewEngine/ViewEngineTests.cs b/source/Handlebars.Test/ViewEngine/ViewEngineTests.cs
index 40182e12..a7bdebc1 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\r\n{{{body}}}\r\nlayout end"},
+ {"views\\somelayout.hbs", "layout start\n{{{body}}}\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\r\nThis is the body\r\nlayout end", output);
+ Assert.Equal("layout start\nThis is the body\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\r\n{{{body}}}\r\nlayout end"},
+ {"views\\somelayout.hbs", "layout start\n{{{body}}}\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\r\nThis is the body\r\nlayout end", output);
+ Assert.Equal("layout start\nThis is the body\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\r\n{{{body}}}\r\nlayout end"},
+ {"somelayout.hbs", "layout start\n{{{body}}}\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\r\nThis is the body\r\nlayout end", output);
+ Assert.Equal("layout start\nThis is the body\nlayout end", output);
}
[Fact]
@@ -81,9 +81,9 @@ public void CanRenderInlineBlocks()
var files = new FakeFileSystem()
{
//Given a layout in a subfolder
- { "partials/layout.hbs", "
\r\n{{> nav}}\r\n
\r\n
\r\n{{> content}}\r\n
"},
+ { "partials/layout.hbs", "
\n{{> nav}}\n
\n
\n{{> content}}\n
"},
- { "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}}"}
+ { "template.hbs", "{{#> layout}}\n{{#*inline \"nav\"}}\n{{Text}}\n{{/inline}}\n{{#*inline \"content\"}}\nMy Content\n{{/inline}}\n{{/layout}}"}
};
//When a viewengine renders that view
@@ -95,7 +95,7 @@ public void CanRenderInlineBlocks()
});
//Then the correct output should be rendered
- Assert.Equal("
\r\n<My Nav>\r\n
\r\n
\r\nMy Content\r\n
", output);
+ Assert.Equal("
\n<My Nav>\n
\n
\nMy Content\n
", output);
}
[Fact]
@@ -104,7 +104,7 @@ public void CanLoadAViewWithALayoutWithAVariable()
//Given a layout in the root
var files = new FakeFileSystem()
{
- {"somelayout.hbs", "{{var1}} start\r\n{{{body}}}\r\n{{var1}} end"},
+ {"somelayout.hbs", "{{var1}} start\n{{{body}}}\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\r\nThis is the body\r\nlayout end", output);
+ Assert.Equal("layout start\nThis is the body\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\r\n{{{body}}}\r\n{{var1}} end"},
+ {"somelayout.hbs", "{{var1}} start\n{{{body}}}\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\r\nThis is the body\r\nlayout end", output);
+ Assert.Equal("layout start\nThis is the body\nlayout end", output);
}
[Fact]
@@ -162,8 +162,8 @@ public void CanIgnoreCommentsContainingHtml()
{
var files = new FakeFileSystem()
{
- { "views\\layout.hbs", "Start\r\n{{{body}}}\r\nEnd" },
- { "views\\someview.hbs", "{{!< layout}}\r\n\r\nTemplate\r\n{{!--\r\n