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

Allow markup in @functions #317

Closed
wants to merge 1 commit into from
Closed
Changes from all commits
Commits
File filter...
Filter file types
Jump to…
Jump to file or symbol
Failed to load files and symbols.
+1,874 −4
Diff settings

Always

Just for now

@@ -19,7 +19,16 @@ protected override void ExecuteCore(RazorCodeDocument codeDocument, DocumentInte
{
for (var i = 0; i < functions.Node.Children.Count; i++)
{
@class.Children.Add(functions.Node.Children[i]);
var child = functions.Node.Children[i];
if (child is TemplateIntermediateNode)
{
// If there's a template at the top level in the functions block we want to just 'inline'
// its content. In this context a template acts like a transition to markup.
@class.Children.AddRange(child.Children);
continue;
}

@class.Children.Add(child);
}
}
}
@@ -59,6 +59,32 @@ public void Add(IntermediateNode item)
_inner.Add(item);
}

public void AddRange(IEnumerable<IntermediateNode> items)
{
if (items == null)
{
throw new ArgumentNullException(nameof(items));
}

foreach (var item in items)
{
Add(item);
}
}

public void AddRange(IntermediateNodeCollection items)
{
if (items == null)
{
throw new ArgumentNullException(nameof(items));
}

for (var i = 0; i < items._inner.Count; i++)
{
Add(items._inner[i]);
}
}

public void Clear()
{
_inner.Clear();
@@ -1409,7 +1409,15 @@ private void ParseExtensibleDirective(in SyntaxListBuilder<RazorSyntaxNode> buil
ParseDirectiveBlock(directiveBuilder, descriptor, parseChildren: (childBuilder, startingBraceLocation) =>
{
NextToken();
Balance(childBuilder, BalancingModes.NoErrorOnFailure, SyntaxKind.LeftBrace, SyntaxKind.RightBrace, startingBraceLocation);

var balancingMode = BalancingModes.NoErrorOnFailure;
if (Context.FeatureFlags.AllowRazorInCodeBlockDirectives)
{
// We want to allow templates inside code blocks (like @functions)
balancingMode |= BalancingModes.AllowCommentsAndTemplates;
}

Balance(childBuilder, balancingMode, SyntaxKind.LeftBrace, SyntaxKind.RightBrace, startingBraceLocation);
SpanContext.ChunkGenerator = new StatementChunkGenerator();
var existingEditHandler = SpanContext.EditHandler;
SpanContext.EditHandler = new CodeBlockEditHandler(Language.TokenizeString);
@@ -76,7 +76,7 @@ internal static bool ModifiesInvalidContent(SyntaxNode target, SourceChange chan
{
var relativePosition = change.Span.AbsoluteIndex - target.Position;

if (target.GetContent().IndexOfAny(new[] { '{', '}' }, relativePosition, change.Span.Length) >= 0)
if (target.GetContent().IndexOfAny(new[] { '{', '}', '@', '<', '*', }, relativePosition, change.Span.Length) >= 0)
{
return true;
}
@@ -103,7 +103,7 @@ internal static bool IsAcceptableInsertion(SourceChange change)
// Internal for testing
internal static bool ContainsInvalidContent(SourceChange change)
{
if (change.NewText.IndexOfAny(new[] { '{', '}' }) >= 0)
if (change.NewText.IndexOfAny(new[] { '{', '}', '@', '<', '*', }) >= 0)

This comment has been minimized.

Copy link
@rynowak

rynowak Mar 8, 2019

Author Member

@NTaylorMullen - looking for your advice on this, plus any ideas you have if I'm missing tests. I had to add all of these various characters to make partial-parsing work but other than that it was straightforward.

I considered making these conditional on the version, but functions doesn't get much usage outside of 3.0 scenarios anyway....

This comment has been minimized.

Copy link
@NTaylorMullen

NTaylorMullen Mar 8, 2019

Member

Played around with this a bit and it looks like it doesn't work exactly how you'd expect. For instance if you have

@functions  {
    
    |

    @<p></p>
}

Typing anything at the pipe wont end up hitting the codeblock edit handler. Seems as though bits don't get classified properly when the functions block already has a template in it.

{
return true;
}
@@ -10,6 +10,7 @@ public static RazorParserFeatureFlags Create(RazorLanguageVersion version)
var allowMinimizedBooleanTagHelperAttributes = false;
var allowHtmlCommentsInTagHelpers = false;
var allowComponentFileKind = false;
var allowRazorInCodeBlockDirectives = false;
var experimental_AllowConditionalDataDashAttributes = false;

if (version.CompareTo(RazorLanguageVersion.Version_2_1) >= 0)
@@ -23,6 +24,7 @@ public static RazorParserFeatureFlags Create(RazorLanguageVersion version)
{
// Added in 3.0
allowComponentFileKind = true;
allowRazorInCodeBlockDirectives = true;
}

if (version.CompareTo(RazorLanguageVersion.Experimental) >= 0)
@@ -34,6 +36,7 @@ public static RazorParserFeatureFlags Create(RazorLanguageVersion version)
allowMinimizedBooleanTagHelperAttributes,
allowHtmlCommentsInTagHelpers,
allowComponentFileKind,
allowRazorInCodeBlockDirectives,
experimental_AllowConditionalDataDashAttributes);
}

@@ -43,6 +46,8 @@ public static RazorParserFeatureFlags Create(RazorLanguageVersion version)

public abstract bool AllowComponentFileKind { get; }

public abstract bool AllowRazorInCodeBlockDirectives { get; }

public abstract bool EXPERIMENTAL_AllowConditionalDataDashAttributes { get; }

private class DefaultRazorParserFeatureFlags : RazorParserFeatureFlags
@@ -51,11 +56,13 @@ private class DefaultRazorParserFeatureFlags : RazorParserFeatureFlags
bool allowMinimizedBooleanTagHelperAttributes,
bool allowHtmlCommentsInTagHelpers,
bool allowComponentFileKind,
bool allowRazorInCodeBlockDirectives,
bool experimental_AllowConditionalDataDashAttributes)
{
AllowMinimizedBooleanTagHelperAttributes = allowMinimizedBooleanTagHelperAttributes;
AllowHtmlCommentsInTagHelpers = allowHtmlCommentsInTagHelpers;
AllowComponentFileKind = allowComponentFileKind;
AllowRazorInCodeBlockDirectives = allowRazorInCodeBlockDirectives;
EXPERIMENTAL_AllowConditionalDataDashAttributes = experimental_AllowConditionalDataDashAttributes;
}

@@ -65,6 +72,8 @@ private class DefaultRazorParserFeatureFlags : RazorParserFeatureFlags

public override bool AllowComponentFileKind { get; }

public override bool AllowRazorInCodeBlockDirectives { get; }

public override bool EXPERIMENTAL_AllowConditionalDataDashAttributes { get; }
}
}
@@ -46,6 +46,18 @@ public void Templates_Runtime()
RunTimeTest();
}

[Fact]
public void Templates_InFunctionsBlock_Runtime()
{
RunTimeTest();
}

[Fact]
public void Templates_InFunctionsBlockWithTagHelper_Runtime()
{
RunRuntimeTagHelpersTest(TestTagHelperDescriptors.SimpleTagHelperDescriptors);
}

[Fact]
public void StringLiterals_Runtime()
{
@@ -503,6 +515,18 @@ public void Templates_DesignTime()
DesignTimeTest();
}

[Fact]
public void Templates_InFunctionsBlock_DesignTime()
{
DesignTimeTest();
}

[Fact]
public void Templates_InFunctionsBlockWithTagHelper_DesignTime()
{
RunDesignTimeTagHelpersTest(TestTagHelperDescriptors.SimpleTagHelperDescriptors);
}

[Fact]
public void StringLiterals_DesignTime()
{
@@ -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 Microsoft.AspNetCore.Razor.Language.Extensions;
using Xunit;

namespace Microsoft.AspNetCore.Razor.Language.Legacy
@@ -88,5 +89,112 @@ public void _WithDoubleTransition_DoesNotThrow()
{
ParseDocumentTest("@{ var foo = bar; Html.ExecuteTemplate(foo, @<p foo='@@'>Foo #@item</p>); }");
}

[Fact]
public void TemplateInFunctionsBlock_DoesNotParseWhenNotSupported()
{
ParseDocumentTest(
RazorLanguageVersion.Version_2_1,
@"
@functions {
void Announcment(string message)
{
@<h3>@message</h3>
}
}
", new[] { FunctionsDirective.Directive, }, designTime: false);
}

[Fact]
public void TemplateInFunctionsBlock_ParsesTemplateInsideMethod()
{
ParseDocumentTest(
RazorLanguageVersion.Version_3_0,
@"
@functions {
void Announcment(string message)
{
@<h3>@message</h3>
}
}
", new[] { FunctionsDirective.Directive, }, designTime: false);
}

// This will parse correctly in Razor, but will generate invalid C#.
[Fact]
public void TemplateInFunctionsBlock_ParsesTemplateWithExpressionsMethod()
{
ParseDocumentTest(
RazorLanguageVersion.Version_3_0,
@"
@functions {
void Announcment(string message) => @<h3>@message</h3>
}
", new[] { FunctionsDirective.Directive, }, designTime: false);
}

[Fact]
public void TemplateInFunctionsBlock_DoesNotParseTemplateInString()
{
ParseDocumentTest(
RazorLanguageVersion.Version_3_0,
@"
@functions {
void Announcment(string message) => ""@<h3>@message</h3>"";
}
", new[] { FunctionsDirective.Directive, }, designTime: false);
}

[Fact]
public void TemplateInFunctionsBlock_DoesNotParseTemplateInVerbatimString()
{
ParseDocumentTest(
RazorLanguageVersion.Version_3_0,
@"
@functions {
void Announcment(string message) => @""@<h3>@message</h3>"";
}
", new[] { FunctionsDirective.Directive, }, designTime: false);
}

[Fact]
public void TemplateInFunctionsBlock_TemplateCanContainCurlyBraces()
{
ParseDocumentTest(
RazorLanguageVersion.Version_3_0,
@"
@functions {
void Announcment(string message)
{
@<div>
@if (message.Length > 0)
{
<p>@message.Length</p>
}
</div>
}
}
", new[] { FunctionsDirective.Directive, }, designTime: false);
}

[Fact]
public void TemplateInFunctionsBlock_TemplateCanContainTemplate()
{
ParseDocumentTest(
RazorLanguageVersion.Version_3_0,
@"
@functions {
void Announcment(string message)
{
@<div>
@if (message.Length > 0)
{
Repeat(@<p>@message.Length</p>);
}
</div>
}
}
", new[] { FunctionsDirective.Directive, }, designTime: false);
}
}
}
@@ -24,6 +24,20 @@ public void IsAcceptableReplacement_AcceptableReplacement_ReturnsTrue()
Assert.True(result);
}

[Fact]
public void IsAcceptableReplacement_AcceptableReplacement_WithTemplate_ReturnsTrue()
{
// Arrange
var span = GetSpan(SourceLocation.Zero, "Hello @<div>@world</div>.");
var change = new SourceChange(new SourceSpan(0, 5), "H3ll0");

// Act
var result = CodeBlockEditHandler.IsAcceptableReplacement(span, change);

// Assert
Assert.True(result);
}

[Fact]
public void IsAcceptableReplacement_ChangeModifiesInvalidContent_ReturnsFalse()
{
@@ -38,6 +52,34 @@ public void IsAcceptableReplacement_ChangeModifiesInvalidContent_ReturnsFalse()
Assert.False(result);
}

[Fact]
public void IsAcceptableReplacement_ChangeAddsTemplate_ReturnsFalse()
{
// Arrange
var span = GetSpan(SourceLocation.Zero, "Hello <div></div>.");
var change = new SourceChange(new SourceSpan(6, 1), "@<");

// Act
var result = CodeBlockEditHandler.IsAcceptableReplacement(span, change);

// Assert
Assert.False(result);
}

[Fact]
public void IsAcceptableReplacement_ChangeToComment_ReturnsFalse()
{
// Arrange
var span = GetSpan(SourceLocation.Zero, "Hello @");
var change = new SourceChange(new SourceSpan(6, 1), "@*");

// Act
var result = CodeBlockEditHandler.IsAcceptableReplacement(span, change);

// Assert
Assert.False(result);
}

[Fact]
public void IsAcceptableReplacement_ChangeContainsInvalidContent_ReturnsFalse()
{
@@ -162,6 +204,32 @@ public void IsAcceptableInsertion_InvalidChange_ReturnsFalse()
Assert.False(result);
}

[Fact]
public void IsAcceptableInsertion_InvalidChange_Transition_ReturnsFalse()
{
// Arrange
var change = new SourceChange(new SourceSpan(0, 0), "@");

// Act
var result = CodeBlockEditHandler.IsAcceptableInsertion(change);

// Assert
Assert.False(result);
}

[Fact]
public void IsAcceptableInsertion_InvalidChange_TemplateTransition_ReturnsFalse()
{
// Arrange
var change = new SourceChange(new SourceSpan(0, 0), "<");

// Act
var result = CodeBlockEditHandler.IsAcceptableInsertion(change);

// Assert
Assert.False(result);
}

[Fact]
public void IsAcceptableInsertion_NotInsert_ReturnsFalse()
{
@@ -179,6 +247,9 @@ public void IsAcceptableInsertion_NotInsert_ReturnsFalse()
[InlineData("{")]
[InlineData("}")]
[InlineData("if (true) { }")]
[InlineData("@<div></div>")]
[InlineData("<div></div>")]
[InlineData("*")]
public void ContainsInvalidContent_InvalidContent_ReturnsTrue(string content)
{
// Arrange
Oops, something went wrong.
ProTip! Use n and p to navigate between commits in a pull request.
You can’t perform that action at this time.