Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Generate pure HTML document as part of Razor code generation #1624

Merged
merged 2 commits into from
Feb 25, 2020
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Collections.Generic;
using Microsoft.AspNetCore.Razor.Language.CodeGeneration;
using Microsoft.AspNetCore.Razor.Language.Intermediate;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// 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
{
internal class DefaultRazorHtmlDocument : RazorHtmlDocument
{
private readonly string _generatedHtml;
private readonly RazorCodeGenerationOptions _options;

public DefaultRazorHtmlDocument(
string generatedHtml,
RazorCodeGenerationOptions options)
{
if (generatedHtml == null)
{
throw new ArgumentNullException(nameof(generatedHtml));
}

if (options == null)
{
throw new ArgumentNullException(nameof(options));
}

_generatedHtml = generatedHtml;
_options = options;
}

public override string GeneratedHtml => _generatedHtml;

public override RazorCodeGenerationOptions Options => _options;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -102,8 +102,6 @@ private static bool MatchesDirective(TagHelperDescriptor descriptor, string type
return string.Equals(descriptor.Name, typePattern, StringComparison.Ordinal);
}



internal abstract class DirectiveVisitor : SyntaxWalker
{
public abstract HashSet<TagHelperDescriptor> Matches { get; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,6 @@

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
using Microsoft.AspNetCore.Razor.Language.Extensions;
using Microsoft.AspNetCore.Razor.Language.Intermediate;
Expand Down Expand Up @@ -138,6 +135,26 @@ public static void SetCSharpDocument(this RazorCodeDocument document, RazorCShar
document.Items[typeof(RazorCSharpDocument)] = csharp;
}

internal static RazorHtmlDocument GetHtmlDocument(this RazorCodeDocument document)
{
if (document == null)
{
throw new ArgumentNullException(nameof(document));
}

return (RazorHtmlDocument)document.Items[typeof(RazorHtmlDocument)];
}

internal static void SetHtmlDocument(this RazorCodeDocument document, RazorHtmlDocument html)
{
if (document == null)
{
throw new ArgumentNullException(nameof(document));
}

document.Items[typeof(RazorHtmlDocument)] = html;
}

public static RazorParserOptions GetParserOptions(this RazorCodeDocument document)
{
if (document == null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ public abstract class RazorEngine
{
#pragma warning disable CS0618 // Type or member is obsolete
private static RazorEngine CreateCore(RazorConfiguration configuration, bool designTime, Action<IRazorEngineBuilder> configure)
#pragma warning disable CS0618 // Type or member is obsolete
#pragma warning restore CS0618 // Type or member is obsolete
{
if (configuration == null)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
using System.Collections.Generic;

namespace Microsoft.AspNetCore.Razor.Language
{
internal abstract class RazorHtmlDocument
{
public abstract string GeneratedHtml { get; }

public abstract RazorCodeGenerationOptions Options { get; }

public static RazorHtmlDocument Create(string generatedHtml, RazorCodeGenerationOptions options, IEnumerable<RazorDiagnostic> diagnostics)
ajaybhargavb marked this conversation as resolved.
Show resolved Hide resolved
{
if (generatedHtml == null)
{
throw new ArgumentNullException(nameof(generatedHtml));
}

if (options == null)
{
throw new ArgumentNullException(nameof(options));
}

return new DefaultRazorHtmlDocument(generatedHtml, options);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
// 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;
using System.Diagnostics;
using System.Text;
using Microsoft.AspNetCore.Razor.Language.Intermediate;
using Microsoft.AspNetCore.Razor.Language.Syntax;

namespace Microsoft.AspNetCore.Razor.Language
{
internal class RazorHtmlLoweringPass : IntermediateNodePassBase, IRazorDocumentClassifierPass
{
protected override void ExecuteCore(RazorCodeDocument codeDocument, DocumentIntermediateNode documentNode)
ajaybhargavb marked this conversation as resolved.
Show resolved Hide resolved
{
if (!documentNode.Options.DesignTime)
{
// Not needed in run time. This pass generates the backing HTML document that is used to provide HTML intellisense.
return;
}

var generatedHtml = HtmlWriter.GetGeneratedHtml(codeDocument);
var razorHtmlDocument = new DefaultRazorHtmlDocument(generatedHtml, documentNode.Options);
codeDocument.SetHtmlDocument(razorHtmlDocument);
}

// We want to generate a HTML document that contains only pure HTML.
// So we want replace all non-HTML content with whitespace.
// Ideally we should just use ClassifiedSpans to generate this document but
// not all characters in the document are included in the ClassifiedSpans.
internal class HtmlWriter : SyntaxWalker
{
private bool _isHtml;

private HtmlWriter(RazorSourceDocument source)
{
if (source is null)
{
throw new ArgumentNullException(nameof(source));
}

Source = source;
Builder = new StringBuilder(Source.Length);
_isHtml = true;
}

public RazorSourceDocument Source { get; }

public StringBuilder Builder { get; }

public static string GetGeneratedHtml(RazorCodeDocument codeDocument)
{
var writer = new HtmlWriter(codeDocument.Source);
var syntaxTree = codeDocument.GetSyntaxTree();

writer.Visit(syntaxTree.Root);

var generatedHtml = writer.Builder.ToString();
Debug.Assert(
writer.Source.Length == writer.Builder.Length,
$"The backing HTML document should be the same length as the original document. Expected: {writer.Source.Length} Actual: {writer.Builder.Length}");

return generatedHtml;
}

public override void VisitRazorCommentBlock(RazorCommentBlockSyntax node)
{
WriteNode(node, isHtml: false, base.VisitRazorCommentBlock);
}

public override void VisitRazorMetaCode(RazorMetaCodeSyntax node)
{
WriteNode(node, isHtml: false, base.VisitRazorMetaCode);
}

public override void VisitMarkupTransition(MarkupTransitionSyntax node)
{
WriteNode(node, isHtml: false, base.VisitMarkupTransition);
}

public override void VisitCSharpTransition(CSharpTransitionSyntax node)
{
WriteNode(node, isHtml: false, base.VisitCSharpTransition);
}

public override void VisitCSharpEphemeralTextLiteral(CSharpEphemeralTextLiteralSyntax node)
{
WriteNode(node, isHtml: false, base.VisitCSharpEphemeralTextLiteral);
}

public override void VisitCSharpExpressionLiteral(CSharpExpressionLiteralSyntax node)
{
WriteNode(node, isHtml: false, base.VisitCSharpExpressionLiteral);
}

public override void VisitCSharpStatementLiteral(CSharpStatementLiteralSyntax node)
{
WriteNode(node, isHtml: false, base.VisitCSharpStatementLiteral);
}

public override void VisitMarkupStartTag(MarkupStartTagSyntax node)
{
WriteNode(node, isHtml: true, base.VisitMarkupStartTag);
}

public override void VisitMarkupEndTag(MarkupEndTagSyntax node)
{
WriteNode(node, isHtml: true, base.VisitMarkupEndTag);
}

public override void VisitMarkupTagHelperStartTag(MarkupTagHelperStartTagSyntax node)
{
WriteNode(node, isHtml: true, base.VisitMarkupTagHelperStartTag);
}

public override void VisitMarkupTagHelperEndTag(MarkupTagHelperEndTagSyntax node)
{
WriteNode(node, isHtml: true, base.VisitMarkupTagHelperEndTag);
}

public override void VisitMarkupEphemeralTextLiteral(MarkupEphemeralTextLiteralSyntax node)
{
WriteNode(node, isHtml: true, base.VisitMarkupEphemeralTextLiteral);
}

public override void VisitMarkupTextLiteral(MarkupTextLiteralSyntax node)
{
WriteNode(node, isHtml: true, base.VisitMarkupTextLiteral);
}

public override void VisitUnclassifiedTextLiteral(UnclassifiedTextLiteralSyntax node)
{
WriteNode(node, isHtml: true, base.VisitUnclassifiedTextLiteral);
}

public override void VisitToken(SyntaxToken token)
{
base.VisitToken(token);
WriteToken(token);
}

private void WriteToken(SyntaxToken token)
ajaybhargavb marked this conversation as resolved.
Show resolved Hide resolved
{
var content = token.Content;
if (_isHtml)
{
// If we're in HTML context, append the content directly.
Builder.Append(content);
return;
}

// We're in non-HTML context. Let's replace all non-whitespace chars with a space.
foreach (var c in content)
{
if (char.IsWhiteSpace(c))
ajaybhargavb marked this conversation as resolved.
Show resolved Hide resolved
{
Builder.Append(c);
}
else
{
Builder.Append(' ');
}
}
}

private void WriteNode<TNode>(TNode node, bool isHtml, Action<TNode> handler) where TNode : SyntaxNode
{
var old = _isHtml;
_isHtml = isHtml;
handler(node);
_isHtml = old;
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,9 @@ private static void AddDefaultFeatures(ICollection<IRazorFeature> features)
features.Add(new DefaultDirectiveSyntaxTreePass());
features.Add(new HtmlNodeOptimizationPass());

// This works over the syntax tree but needs to happen after the tag helper binder phase.
features.Add(new RazorHtmlLoweringPass());

// Intermediate Node Passes
features.Add(new DefaultDocumentClassifierPass());
features.Add(new MetadataAttributePass());
Expand Down
Loading