diff --git a/src/Mvc/test/WebSites/BasicWebSite/RazorComponents/FetchData.razor b/src/Mvc/test/WebSites/BasicWebSite/RazorComponents/FetchData.razor
index d2d7e9ea6629..03c3c8a957b3 100644
--- a/src/Mvc/test/WebSites/BasicWebSite/RazorComponents/FetchData.razor
+++ b/src/Mvc/test/WebSites/BasicWebSite/RazorComponents/FetchData.razor
@@ -1,6 +1,6 @@
@using BasicWebSite.Services
@inject WeatherForecastService ForecastService
-
+@preservewhitespace true
Weather forecast
This component demonstrates fetching data from the server.
@@ -34,7 +34,6 @@ else
}
-
@code {
[Parameter] public DateTime StartDate { get; set; }
diff --git a/src/Razor/Microsoft.AspNetCore.Mvc.Razor.Extensions/test/TestFiles/IntegrationTests/CodeGenerationIntegrationTest/BasicComponent_Runtime.codegen.cs b/src/Razor/Microsoft.AspNetCore.Mvc.Razor.Extensions/test/TestFiles/IntegrationTests/CodeGenerationIntegrationTest/BasicComponent_Runtime.codegen.cs
index f1f8cd28b96c..c3df65cf639e 100644
--- a/src/Razor/Microsoft.AspNetCore.Mvc.Razor.Extensions/test/TestFiles/IntegrationTests/CodeGenerationIntegrationTest/BasicComponent_Runtime.codegen.cs
+++ b/src/Razor/Microsoft.AspNetCore.Mvc.Razor.Extensions/test/TestFiles/IntegrationTests/CodeGenerationIntegrationTest/BasicComponent_Runtime.codegen.cs
@@ -34,7 +34,6 @@ protected override void BuildRenderTree(Microsoft.AspNetCore.Components.Renderin
#line hidden
#nullable disable
);
- __builder.AddMarkupContent(4, "\r\n");
__builder.CloseElement();
}
#pragma warning restore 1998
diff --git a/src/Razor/Microsoft.AspNetCore.Mvc.Razor.Extensions/test/TestFiles/IntegrationTests/CodeGenerationIntegrationTest/BasicComponent_Runtime.ir.txt b/src/Razor/Microsoft.AspNetCore.Mvc.Razor.Extensions/test/TestFiles/IntegrationTests/CodeGenerationIntegrationTest/BasicComponent_Runtime.ir.txt
index 46b8dd5f0645..a1f78b09a342 100644
--- a/src/Razor/Microsoft.AspNetCore.Mvc.Razor.Extensions/test/TestFiles/IntegrationTests/CodeGenerationIntegrationTest/BasicComponent_Runtime.ir.txt
+++ b/src/Razor/Microsoft.AspNetCore.Mvc.Razor.Extensions/test/TestFiles/IntegrationTests/CodeGenerationIntegrationTest/BasicComponent_Runtime.ir.txt
@@ -16,7 +16,5 @@ Document -
LazyIntermediateToken - (74:3,0 [4] BasicComponent.cshtml) - Html -
CSharpExpression - (79:3,5 [29] BasicComponent.cshtml)
LazyIntermediateToken - (79:3,5 [29] BasicComponent.cshtml) - CSharp - string.Format("{0}", "Hello")
- HtmlContent - (108:3,34 [2] BasicComponent.cshtml)
- LazyIntermediateToken - (108:3,34 [2] BasicComponent.cshtml) - Html - \n
CSharpCode - (132:6,12 [37] BasicComponent.cshtml)
LazyIntermediateToken - (132:6,12 [37] BasicComponent.cshtml) - CSharp - \n void IDisposable.Dispose(){ }\n
diff --git a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/ComponentResources.resx b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/ComponentResources.resx
index b457a1f9f7f0..cbadc7ee10de 100644
--- a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/ComponentResources.resx
+++ b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/ComponentResources.resx
@@ -201,6 +201,15 @@
route template
+
+ True if whitespace should be preserved, otherwise false.
+
+
+ Preserve
+
+
+ Specifies whether or not whitespace should be preserved exactly. Defaults to false for better performance.
+
Populates the specified field or property with a reference to the element or component.
diff --git a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/ComponentPreserveWhitespaceDirective.cs b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/ComponentPreserveWhitespaceDirective.cs
new file mode 100644
index 000000000000..72f657aac77a
--- /dev/null
+++ b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/ComponentPreserveWhitespaceDirective.cs
@@ -0,0 +1,30 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+
+namespace Microsoft.AspNetCore.Razor.Language.Components
+{
+ internal static class ComponentPreserveWhitespaceDirective
+ {
+ public static readonly DirectiveDescriptor Directive = DirectiveDescriptor.CreateDirective(
+ "preservewhitespace",
+ DirectiveKind.SingleLine,
+ builder =>
+ {
+ builder.AddBooleanToken(ComponentResources.PreserveWhitespaceDirective_BooleanToken_Name, ComponentResources.PreserveWhitespaceDirective_BooleanToken_Description);
+ builder.Usage = DirectiveUsage.FileScopedMultipleOccurring;
+ builder.Description = ComponentResources.PreserveWhitespaceDirective_Description;
+ });
+
+ public static void Register(RazorProjectEngineBuilder builder)
+ {
+ if (builder == null)
+ {
+ throw new ArgumentNullException(nameof(builder));
+ }
+
+ builder.AddDirective(Directive, FileKinds.Component, FileKinds.ComponentImport);
+ }
+ }
+}
diff --git a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/ComponentWhitespacePass.cs b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/ComponentWhitespacePass.cs
index 08eec3ae8025..761e3eb8f4d3 100644
--- a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/ComponentWhitespacePass.cs
+++ b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/ComponentWhitespacePass.cs
@@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
+using System.Linq;
using Microsoft.AspNetCore.Razor.Language.Intermediate;
namespace Microsoft.AspNetCore.Razor.Language.Components
@@ -38,17 +39,45 @@ protected override void ExecuteCore(RazorCodeDocument codeDocument, DocumentInte
return;
}
+ // Respect @preservewhitespace directives
+ if (PreserveWhitespaceIsEnabled(documentNode))
+ {
+ return;
+ }
+
var method = documentNode.FindPrimaryMethod();
if (method != null)
{
RemoveContiguousWhitespace(method.Children, TraversalDirection.Forwards);
RemoveContiguousWhitespace(method.Children, TraversalDirection.Backwards);
+
+ var visitor = new Visitor();
+ visitor.Visit(method);
+ }
+ }
+
+ private static bool PreserveWhitespaceIsEnabled(DocumentIntermediateNode documentNode)
+ {
+ // If there's no @preservewhitespace attribute, the default is that we *don't* preserve whitespace
+ var shouldPreserveWhitespace = false;
+
+ foreach (var preserveWhitespaceDirective in documentNode.FindDirectiveReferences(ComponentPreserveWhitespaceDirective.Directive))
+ {
+ var token = ((DirectiveIntermediateNode)preserveWhitespaceDirective.Node).Tokens.FirstOrDefault();
+ var shouldPreserveWhitespaceContent = token?.Content;
+ if (shouldPreserveWhitespaceContent != null)
+ {
+ shouldPreserveWhitespace = string.Equals(shouldPreserveWhitespaceContent, "true", StringComparison.Ordinal);
+ }
}
+
+ return shouldPreserveWhitespace;
}
- private static void RemoveContiguousWhitespace(IntermediateNodeCollection nodes, TraversalDirection direction)
+ private static int RemoveContiguousWhitespace(IntermediateNodeCollection nodes, TraversalDirection direction, int? startIndex = null)
{
- var position = direction == TraversalDirection.Forwards ? 0 : nodes.Count - 1;
+ var position = startIndex.GetValueOrDefault(direction == TraversalDirection.Forwards ? 0 : nodes.Count - 1);
+ var countRemoved = 0;
while (position >= 0 && position < nodes.Count)
{
var node = nodes[position];
@@ -76,7 +105,7 @@ private static void RemoveContiguousWhitespace(IntermediateNodeCollection nodes,
shouldContinueIteration = false;
break;
- case CSharpCodeIntermediateNode codeIntermediateNode:
+ case CSharpCodeIntermediateNode _:
shouldRemoveNode = false;
shouldContinueIteration = false;
break;
@@ -90,6 +119,7 @@ private static void RemoveContiguousWhitespace(IntermediateNodeCollection nodes,
if (shouldRemoveNode)
{
nodes.RemoveAt(position);
+ countRemoved++;
if (direction == TraversalDirection.Forwards)
{
position--;
@@ -103,6 +133,8 @@ private static void RemoveContiguousWhitespace(IntermediateNodeCollection nodes,
break;
}
}
+
+ return countRemoved;
}
enum TraversalDirection
@@ -110,5 +142,39 @@ enum TraversalDirection
Forwards,
Backwards
}
+
+ class Visitor : IntermediateNodeWalker
+ {
+ public override void VisitMarkupElement(MarkupElementIntermediateNode node)
+ {
+ RemoveContiguousWhitespace(node.Children, TraversalDirection.Forwards);
+ RemoveContiguousWhitespace(node.Children, TraversalDirection.Backwards);
+ VisitDefault(node);
+ }
+
+ public override void VisitTagHelperBody(TagHelperBodyIntermediateNode node)
+ {
+ // The goal here is to remove leading/trailing whitespace inside component child content. However,
+ // at the time this whitespace pass runs, ComponentChildContent is still TagHelperBody in the tree.
+ RemoveContiguousWhitespace(node.Children, TraversalDirection.Forwards);
+ RemoveContiguousWhitespace(node.Children, TraversalDirection.Backwards);
+ VisitDefault(node);
+ }
+
+ public override void VisitDefault(IntermediateNode node)
+ {
+ // For any CSharpCodeIntermediateNode children, remove their preceding and trailing whitespace
+ for (var childIndex = 0; childIndex < node.Children.Count; childIndex++)
+ {
+ if (node.Children[childIndex] is CSharpCodeIntermediateNode)
+ {
+ childIndex -= RemoveContiguousWhitespace(node.Children, TraversalDirection.Backwards, childIndex - 1);
+ RemoveContiguousWhitespace(node.Children, TraversalDirection.Forwards, childIndex + 1);
+ }
+ }
+
+ base.VisitDefault(node);
+ }
+ }
}
}
diff --git a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/DirectiveDescriptorBuilderExtensions.cs b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/DirectiveDescriptorBuilderExtensions.cs
index e8257bba7fff..3e9c14b46f99 100644
--- a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/DirectiveDescriptorBuilderExtensions.cs
+++ b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/DirectiveDescriptorBuilderExtensions.cs
@@ -117,6 +117,28 @@ public static IDirectiveDescriptorBuilder AddAttributeToken(this IDirectiveDescr
return builder;
}
+ public static IDirectiveDescriptorBuilder AddBooleanToken(this IDirectiveDescriptorBuilder builder)
+ {
+ return AddBooleanToken(builder, name: null, description: null);
+ }
+
+ public static IDirectiveDescriptorBuilder AddBooleanToken(this IDirectiveDescriptorBuilder builder, string name, string description)
+ {
+ if (builder == null)
+ {
+ throw new ArgumentNullException(nameof(builder));
+ }
+
+ builder.Tokens.Add(
+ DirectiveTokenDescriptor.CreateToken(
+ DirectiveTokenKind.Boolean,
+ optional: false,
+ name: name,
+ description: description));
+
+ return builder;
+ }
+
public static IDirectiveDescriptorBuilder AddOptionalMemberToken(this IDirectiveDescriptorBuilder builder)
{
return AddOptionalMemberToken(builder, name: null, description: null);
diff --git a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/DirectiveTokenKind.cs b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/DirectiveTokenKind.cs
index 916a57c3cae4..88b214071c9e 100644
--- a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/DirectiveTokenKind.cs
+++ b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/DirectiveTokenKind.cs
@@ -10,5 +10,6 @@ public enum DirectiveTokenKind
Member,
String,
Attribute,
+ Boolean,
}
}
\ No newline at end of file
diff --git a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Legacy/CSharpCodeParser.cs b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Legacy/CSharpCodeParser.cs
index 9daadf371417..fd94358c157e 100644
--- a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Legacy/CSharpCodeParser.cs
+++ b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Legacy/CSharpCodeParser.cs
@@ -1322,7 +1322,8 @@ private void ParseExtensibleDirective(in SyntaxListBuilder buil
if (tokenDescriptor.Kind == DirectiveTokenKind.Member ||
tokenDescriptor.Kind == DirectiveTokenKind.Namespace ||
tokenDescriptor.Kind == DirectiveTokenKind.Type ||
- tokenDescriptor.Kind == DirectiveTokenKind.Attribute)
+ tokenDescriptor.Kind == DirectiveTokenKind.Attribute ||
+ tokenDescriptor.Kind == DirectiveTokenKind.Boolean)
{
SpanContext.ChunkGenerator = SpanChunkGenerator.Null;
SpanContext.EditHandler.AcceptedCharacters = AcceptedCharactersInternal.Whitespace;
@@ -1417,6 +1418,22 @@ private void ParseExtensibleDirective(in SyntaxListBuilder buil
return;
}
break;
+
+ case DirectiveTokenKind.Boolean:
+ if (AtBooleanLiteral() && !CurrentToken.ContainsDiagnostics)
+ {
+ AcceptAndMoveNext();
+ }
+ else
+ {
+ Context.ErrorSink.OnError(
+ RazorDiagnosticFactory.CreateParsing_DirectiveExpectsBooleanLiteral(
+ new SourceSpan(CurrentStart, CurrentToken.Content.Length), descriptor.Directive));
+ builder.Add(BuildDirective());
+ return;
+ }
+ break;
+
case DirectiveTokenKind.Attribute:
if (At(SyntaxKind.LeftBracket))
{
@@ -1699,6 +1716,12 @@ private bool TryParseKeyword(in SyntaxListBuilder builder, IRea
return false;
}
+ private bool AtBooleanLiteral()
+ {
+ var result = CSharpTokenizer.GetTokenKeyword(CurrentToken);
+ return result.HasValue && (result.Value == CSharpKeyword.True || result.Value == CSharpKeyword.False);
+ }
+
private void SetupExpressionParsers()
{
MapExpressionKeyword(ParseAwaitExpression, CSharpKeyword.Await);
diff --git a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/RazorDiagnosticFactory.cs b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/RazorDiagnosticFactory.cs
index 1617fc417c67..3d1fbb234a4e 100644
--- a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/RazorDiagnosticFactory.cs
+++ b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/RazorDiagnosticFactory.cs
@@ -426,6 +426,16 @@ public static RazorDiagnostic CreateParsing_DirectiveExpectsCSharpAttribute(Sour
{
return RazorDiagnostic.Create(Parsing_DirectiveExpectsCSharpAttribute, location, directiveName);
}
+
+ internal static readonly RazorDiagnosticDescriptor Parsing_DirectiveExpectsBooleanLiteral =
+ new RazorDiagnosticDescriptor(
+ $"{DiagnosticPrefix}1038",
+ () => Resources.DirectiveExpectsBooleanLiteral,
+ RazorDiagnosticSeverity.Error);
+ public static RazorDiagnostic CreateParsing_DirectiveExpectsBooleanLiteral(SourceSpan location, string directiveName)
+ {
+ return RazorDiagnostic.Create(Parsing_DirectiveExpectsBooleanLiteral, location, directiveName);
+ }
#endregion
#region Semantic Errors
diff --git a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/RazorProjectEngine.cs b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/RazorProjectEngine.cs
index 07729dcd2548..ac75eac6d17f 100644
--- a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/RazorProjectEngine.cs
+++ b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/RazorProjectEngine.cs
@@ -120,7 +120,7 @@ public static RazorProjectEngine Create(
NamespaceDirective.Register(builder);
AttributeDirective.Register(builder);
- AddComponentFeatures(builder);
+ AddComponentFeatures(builder, configuration.LanguageVersion);
}
LoadExtensions(builder, configuration.Extensions);
@@ -206,7 +206,7 @@ private static void AddDefaultFeatures(ICollection features)
});
}
- private static void AddComponentFeatures(RazorProjectEngineBuilder builder)
+ private static void AddComponentFeatures(RazorProjectEngineBuilder builder, RazorLanguageVersion razorLanguageVersion)
{
// Project Engine Features
builder.Features.Add(new ComponentImportProjectFeature());
@@ -218,6 +218,11 @@ private static void AddComponentFeatures(RazorProjectEngineBuilder builder)
ComponentPageDirective.Register(builder);
ComponentTypeParamDirective.Register(builder);
+ if (razorLanguageVersion.CompareTo(RazorLanguageVersion.Version_5_0) >= 0)
+ {
+ ComponentPreserveWhitespaceDirective.Register(builder);
+ }
+
// Document Classifier
builder.Features.Add(new ComponentDocumentClassifierPass());
diff --git a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Resources.resx b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Resources.resx
index 33c63d93c25f..65fdf317bb17 100644
--- a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Resources.resx
+++ b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Resources.resx
@@ -559,4 +559,7 @@
Invalid tag helper required directive attribute '{0}'. The directive attribute '{1}' should start with a '@' character.
+
+ The '{0}' directive expects a boolean literal.
+
\ No newline at end of file
diff --git a/src/Razor/Microsoft.AspNetCore.Razor.Language/test/Components/ComponentMarkupBlockPassTest.cs b/src/Razor/Microsoft.AspNetCore.Razor.Language/test/Components/ComponentMarkupBlockPassTest.cs
index 365b7909f924..6c00e4f32227 100644
--- a/src/Razor/Microsoft.AspNetCore.Razor.Language/test/Components/ComponentMarkupBlockPassTest.cs
+++ b/src/Razor/Microsoft.AspNetCore.Razor.Language/test/Components/ComponentMarkupBlockPassTest.cs
@@ -47,11 +47,9 @@ public void Execute_RewritesHtml_Basic()