Add TagHelper parse tree visitor.
- The visitor looks for TagBlock's that match registered TagHelpers and rebuilds them into TagHelperBlock's. - Added the code generator and corresponding chunk for a TagHelperBlock. - Added syntax tree specific objects & helper methods to create accurate tag helper structures. #71
This commit is contained in:
parent
83da8e257d
commit
3cba84104d
|
|
@ -0,0 +1,38 @@
|
||||||
|
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using Microsoft.AspNet.Razor.Parser.SyntaxTree;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNet.Razor.Generator
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A <see cref="BlockCodeGenerator"/> that is responsible for generating valid <see cref="TagHelperChunk"/>s.
|
||||||
|
/// </summary>
|
||||||
|
public class TagHelperCodeGenerator : BlockCodeGenerator
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Starts the generation of a <see cref="TagHelperChunk"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="target">
|
||||||
|
/// The <see cref="Block"/> responsible for this <see cref="TagHelperCodeGenerator"/>.
|
||||||
|
/// </param>
|
||||||
|
/// <param name="context">A <see cref="CodeGeneratorContext"/> instance that contains information about
|
||||||
|
/// the current code generation process.</param>
|
||||||
|
public override void GenerateStartBlockCode(Block target, CodeGeneratorContext context)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Ends the generation of a <see cref="TagHelperChunk"/> capturing all previously visited children
|
||||||
|
/// since <see cref="GenerateStartBlockCode"/> method was called.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="target">
|
||||||
|
/// The <see cref="Block"/> responsible for this <see cref="TagHelperCodeGenerator"/>.
|
||||||
|
/// </param>
|
||||||
|
/// <param name="context">A <see cref="CodeGeneratorContext"/> instance that contains information about
|
||||||
|
/// the current code generation process.</param>
|
||||||
|
public override void GenerateEndBlockCode(Block target, CodeGeneratorContext context)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -5,8 +5,20 @@ using Microsoft.AspNet.Razor.Parser.SyntaxTree;
|
||||||
|
|
||||||
namespace Microsoft.AspNet.Razor.Parser
|
namespace Microsoft.AspNet.Razor.Parser
|
||||||
{
|
{
|
||||||
internal interface ISyntaxTreeRewriter
|
/// <summary>
|
||||||
|
/// Defines the contract for rewriting a syntax tree.
|
||||||
|
/// </summary>
|
||||||
|
public interface ISyntaxTreeRewriter
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Rewrites the provided <paramref name="input"/> syntax tree.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="input">The current syntax tree.</param>
|
||||||
|
/// <returns>The <paramref name="input"/> syntax tree or a syntax tree to be used instead of the
|
||||||
|
/// <paramref name="input"/> tree.</returns>
|
||||||
|
/// <remarks>
|
||||||
|
/// If you choose not to modify the syntax tree you can always return <paramref name="input"/>.
|
||||||
|
/// </remarks>
|
||||||
Block Rewrite(Block input);
|
Block Rewrite(Block input);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,9 +5,12 @@ using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNet.Razor.Parser.SyntaxTree;
|
using Microsoft.AspNet.Razor.Parser.SyntaxTree;
|
||||||
|
using Microsoft.AspNet.Razor.Parser.TagHelpers.Internal;
|
||||||
|
using Microsoft.AspNet.Razor.TagHelpers;
|
||||||
using Microsoft.AspNet.Razor.Text;
|
using Microsoft.AspNet.Razor.Text;
|
||||||
|
|
||||||
namespace Microsoft.AspNet.Razor.Parser
|
namespace Microsoft.AspNet.Razor.Parser
|
||||||
|
|
@ -28,12 +31,22 @@ namespace Microsoft.AspNet.Razor.Parser
|
||||||
MarkupParser = markupParser;
|
MarkupParser = markupParser;
|
||||||
CodeParser = codeParser;
|
CodeParser = codeParser;
|
||||||
|
|
||||||
|
// TODO: As part of https://github.com/aspnet/Razor/issues/111 and
|
||||||
|
// https://github.com/aspnet/Razor/issues/112 pull the provider from some sort of tag helper locator
|
||||||
|
// object.
|
||||||
|
var provider = new TagHelperDescriptorProvider(Enumerable.Empty<TagHelperDescriptor>());
|
||||||
|
|
||||||
Optimizers = new List<ISyntaxTreeRewriter>()
|
Optimizers = new List<ISyntaxTreeRewriter>()
|
||||||
{
|
{
|
||||||
|
// TODO: Modify the below WhiteSpaceRewriter & ConditionalAttributeCollapser to handle
|
||||||
|
// TagHelperBlock's: https://github.com/aspnet/Razor/issues/117
|
||||||
|
|
||||||
// Move whitespace from start of expression block to markup
|
// Move whitespace from start of expression block to markup
|
||||||
new WhiteSpaceRewriter(MarkupParser.BuildSpan),
|
new WhiteSpaceRewriter(MarkupParser.BuildSpan),
|
||||||
// Collapse conditional attributes where the entire value is literal
|
// Collapse conditional attributes where the entire value is literal
|
||||||
new ConditionalAttributeCollapser(MarkupParser.BuildSpan),
|
new ConditionalAttributeCollapser(MarkupParser.BuildSpan),
|
||||||
|
// Enables tag helpers
|
||||||
|
new TagHelperParseTreeRewriter(provider),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,16 +14,21 @@ namespace Microsoft.AspNet.Razor.Parser.SyntaxTree
|
||||||
public class Block : SyntaxTreeNode
|
public class Block : SyntaxTreeNode
|
||||||
{
|
{
|
||||||
public Block(BlockBuilder source)
|
public Block(BlockBuilder source)
|
||||||
|
: this(source.Type, source.Children, source.CodeGenerator)
|
||||||
{
|
{
|
||||||
if (source.Type == null)
|
source.Reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected Block(BlockType? type, IEnumerable<SyntaxTreeNode> contents, IBlockCodeGenerator generator)
|
||||||
|
{
|
||||||
|
if (type == null)
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException(RazorResources.Block_Type_Not_Specified);
|
throw new InvalidOperationException(RazorResources.Block_Type_Not_Specified);
|
||||||
}
|
}
|
||||||
Type = source.Type.Value;
|
|
||||||
Children = source.Children;
|
Type = type.Value;
|
||||||
Name = source.Name;
|
Children = contents;
|
||||||
CodeGenerator = source.CodeGenerator;
|
CodeGenerator = generator;
|
||||||
source.Reset();
|
|
||||||
|
|
||||||
foreach (SyntaxTreeNode node in Children)
|
foreach (SyntaxTreeNode node in Children)
|
||||||
{
|
{
|
||||||
|
|
@ -31,6 +36,7 @@ namespace Microsoft.AspNet.Razor.Parser.SyntaxTree
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A Test constructor
|
||||||
internal Block(BlockType type, IEnumerable<SyntaxTreeNode> contents, IBlockCodeGenerator generator)
|
internal Block(BlockType type, IEnumerable<SyntaxTreeNode> contents, IBlockCodeGenerator generator)
|
||||||
{
|
{
|
||||||
Type = type;
|
Type = type;
|
||||||
|
|
@ -42,7 +48,7 @@ namespace Microsoft.AspNet.Razor.Parser.SyntaxTree
|
||||||
public BlockType Type { get; private set; }
|
public BlockType Type { get; private set; }
|
||||||
|
|
||||||
public IEnumerable<SyntaxTreeNode> Children { get; private set; }
|
public IEnumerable<SyntaxTreeNode> Children { get; private set; }
|
||||||
public string Name { get; private set; }
|
|
||||||
public IBlockCodeGenerator CodeGenerator { get; private set; }
|
public IBlockCodeGenerator CodeGenerator { get; private set; }
|
||||||
|
|
||||||
public override bool IsBlock
|
public override bool IsBlock
|
||||||
|
|
|
||||||
|
|
@ -18,26 +18,22 @@ namespace Microsoft.AspNet.Razor.Parser.SyntaxTree
|
||||||
{
|
{
|
||||||
Type = original.Type;
|
Type = original.Type;
|
||||||
Children = new List<SyntaxTreeNode>(original.Children);
|
Children = new List<SyntaxTreeNode>(original.Children);
|
||||||
Name = original.Name;
|
|
||||||
CodeGenerator = original.CodeGenerator;
|
CodeGenerator = original.CodeGenerator;
|
||||||
}
|
}
|
||||||
|
|
||||||
[SuppressMessage("Microsoft.Naming", "CA1721:PropertyNamesShouldNotMatchGetMethods", Justification = "Type is the most appropriate name for this property and there is little chance of confusion with GetType")]
|
[SuppressMessage("Microsoft.Naming", "CA1721:PropertyNamesShouldNotMatchGetMethods", Justification = "Type is the most appropriate name for this property and there is little chance of confusion with GetType")]
|
||||||
public BlockType? Type { get; set; }
|
public BlockType? Type { get; set; }
|
||||||
|
|
||||||
public IList<SyntaxTreeNode> Children { get; private set; }
|
public IList<SyntaxTreeNode> Children { get; private set; }
|
||||||
public string Name { get; set; }
|
|
||||||
public IBlockCodeGenerator CodeGenerator { get; set; }
|
public IBlockCodeGenerator CodeGenerator { get; set; }
|
||||||
|
|
||||||
public Block Build()
|
public virtual Block Build()
|
||||||
{
|
{
|
||||||
return new Block(this);
|
return new Block(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Reset()
|
public virtual void Reset()
|
||||||
{
|
{
|
||||||
Type = null;
|
Type = null;
|
||||||
Name = null;
|
|
||||||
Children = new List<SyntaxTreeNode>();
|
Children = new List<SyntaxTreeNode>();
|
||||||
CodeGenerator = BlockCodeGenerator.Null;
|
CodeGenerator = BlockCodeGenerator.Null;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,84 @@
|
||||||
|
// Copyright (c) Microsoft Open Technologies, Inc. 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;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Linq;
|
||||||
|
using Microsoft.AspNet.Razor.Parser.SyntaxTree;
|
||||||
|
using Microsoft.Internal.Web.Utils;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNet.Razor.Parser.TagHelpers
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A <see cref="Block"/> that reprents a special HTML element.
|
||||||
|
/// </summary>
|
||||||
|
public class TagHelperBlock : Block, IEquatable<TagHelperBlock>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Instantiates a new instance of a <see cref="TagHelperBlock"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="source">A <see cref="TagHelperBlockBuilder"/> used to construct a valid
|
||||||
|
/// <see cref="TagHelperBlock"/>.</param>
|
||||||
|
public TagHelperBlock(TagHelperBlockBuilder source)
|
||||||
|
: base(source.Type, source.Children, source.CodeGenerator)
|
||||||
|
{
|
||||||
|
TagName = source.TagName;
|
||||||
|
Attributes = new Dictionary<string, SyntaxTreeNode>(source.Attributes);
|
||||||
|
|
||||||
|
source.Reset();
|
||||||
|
|
||||||
|
foreach (var attributeChildren in Attributes.Values)
|
||||||
|
{
|
||||||
|
attributeChildren.Parent = this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The HTML attributes.
|
||||||
|
/// </summary>
|
||||||
|
public IDictionary<string, SyntaxTreeNode> Attributes { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The HTML tag name.
|
||||||
|
/// </summary>
|
||||||
|
public string TagName { get; private set; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
return string.Format(CultureInfo.CurrentCulture,
|
||||||
|
"'{0}' (Attrs: {1}) Tag Helper Block at {2}::{3} (Gen:{4})",
|
||||||
|
TagName, Attributes.Count, Start, Length, CodeGenerator);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Determines whether two <see cref="TagHelperBlock"/>s are equal by comparing the <see cref="TagName"/>,
|
||||||
|
/// <see cref="Attributes"/>, <see cref="Block.Type"/>, <see cref="Block.CodeGenerator"/> and
|
||||||
|
/// <see cref="Block.Children"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="other">The <see cref="TagHelperBlock"/> to check equality against.</param>
|
||||||
|
/// <returns>
|
||||||
|
/// <c>true</c> if the current <see cref="TagHelperBlock"/> is equivalent to the given
|
||||||
|
/// <paramref name="other"/>, <c>false</c> otherwise.
|
||||||
|
/// </returns>
|
||||||
|
public bool Equals(TagHelperBlock other)
|
||||||
|
{
|
||||||
|
return other != null &&
|
||||||
|
TagName == other.TagName &&
|
||||||
|
Attributes.SequenceEqual(other.Attributes) &&
|
||||||
|
base.Equals(other);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override int GetHashCode()
|
||||||
|
{
|
||||||
|
return HashCodeCombiner.Start()
|
||||||
|
.Add(TagName)
|
||||||
|
.Add(Attributes)
|
||||||
|
.Add(base.GetHashCode())
|
||||||
|
.CombinedHash;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,309 @@
|
||||||
|
// Copyright (c) Microsoft Open Technologies, Inc. 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;
|
||||||
|
using System.Linq;
|
||||||
|
using Microsoft.AspNet.Razor.Generator;
|
||||||
|
using Microsoft.AspNet.Razor.Parser.SyntaxTree;
|
||||||
|
using Microsoft.AspNet.Razor.Tokenizer.Symbols;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNet.Razor.Parser.TagHelpers
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A <see cref="BlockBuilder"/> used to create <see cref="TagHelperBlock"/>s.
|
||||||
|
/// </summary>
|
||||||
|
public class TagHelperBlockBuilder : BlockBuilder
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Instantiates a new <see cref="TagHelperBlockBuilder"/> instance based on given the
|
||||||
|
/// <paramref name="original"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="original">The original <see cref="TagHelperBlock"/> to copy data from.</param>
|
||||||
|
public TagHelperBlockBuilder(TagHelperBlock original)
|
||||||
|
: base(original)
|
||||||
|
{
|
||||||
|
TagName = original.TagName;
|
||||||
|
Attributes = new Dictionary<string, SyntaxTreeNode>(original.Attributes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Instantiates a new instance of the <see cref="TagHelperBlockBuilder"/> class
|
||||||
|
/// with the provided <paramref name="tagName"/> and derives its <see cref="Attributes"/>
|
||||||
|
/// and <see cref="BlockBuilder.Type"/> from the <paramref name="startTag"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="tagName">An HTML tag name.</param>
|
||||||
|
/// <param name="startTag">The <see cref="Block"/> that contains all information about the start
|
||||||
|
/// of the HTML element.</param>
|
||||||
|
public TagHelperBlockBuilder(string tagName, Block startTag)
|
||||||
|
{
|
||||||
|
TagName = tagName;
|
||||||
|
CodeGenerator = new TagHelperCodeGenerator();
|
||||||
|
Type = startTag.Type;
|
||||||
|
Attributes = GetTagAttributes(startTag);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Internal for testing
|
||||||
|
internal TagHelperBlockBuilder(string tagName,
|
||||||
|
IDictionary<string, SyntaxTreeNode> attributes,
|
||||||
|
IEnumerable<SyntaxTreeNode> children)
|
||||||
|
{
|
||||||
|
TagName = tagName;
|
||||||
|
Attributes = attributes;
|
||||||
|
Type = BlockType.Tag;
|
||||||
|
CodeGenerator = new TagHelperCodeGenerator();
|
||||||
|
|
||||||
|
// Children is IList, no AddRange
|
||||||
|
foreach (var child in children)
|
||||||
|
{
|
||||||
|
Children.Add(child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The HTML attributes.
|
||||||
|
/// </summary>
|
||||||
|
public IDictionary<string, SyntaxTreeNode> Attributes { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The HTML tag name.
|
||||||
|
/// </summary>
|
||||||
|
public string TagName { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Constructs a new <see cref="TagHelperBlock"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>A <see cref="TagHelperBlock"/>.</returns>
|
||||||
|
public override Block Build()
|
||||||
|
{
|
||||||
|
return new TagHelperBlock(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
/// <remarks>
|
||||||
|
/// Sets the <see cref="TagName"/> to <c>null</c> and clears the <see cref="Attributes"/>.
|
||||||
|
/// </remarks>
|
||||||
|
public override void Reset()
|
||||||
|
{
|
||||||
|
TagName = null;
|
||||||
|
|
||||||
|
if (Attributes != null)
|
||||||
|
{
|
||||||
|
Attributes.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
base.Reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IDictionary<string, SyntaxTreeNode> GetTagAttributes(Block tagBlock)
|
||||||
|
{
|
||||||
|
var attributes = new Dictionary<string, SyntaxTreeNode>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
// TODO: Handle malformed tags: https://github.com/aspnet/razor/issues/104
|
||||||
|
|
||||||
|
// We skip the first child "<tagname" and take everything up to the "ending" portion of the tag ">" or "/>".
|
||||||
|
// The -2 accounts for both the start and end tags.
|
||||||
|
var attributeChildren = tagBlock.Children.Skip(1).Take(tagBlock.Children.Count() - 2);
|
||||||
|
|
||||||
|
foreach (var child in attributeChildren)
|
||||||
|
{
|
||||||
|
KeyValuePair<string, SyntaxTreeNode> attribute;
|
||||||
|
|
||||||
|
if (child.IsBlock)
|
||||||
|
{
|
||||||
|
attribute = ParseBlock((Block)child);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
attribute = ParseSpan((Span)child);
|
||||||
|
}
|
||||||
|
|
||||||
|
attributes.Add(attribute.Key, attribute.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return attributes;
|
||||||
|
}
|
||||||
|
|
||||||
|
// This method handles cases when the attribute is a simple span attribute such as
|
||||||
|
// class="something moresomething". This does not handle complex attributes such as
|
||||||
|
// class="@myclass". Therefore the span.Content is equivalent to the entire attribute.
|
||||||
|
private static KeyValuePair<string, SyntaxTreeNode> ParseSpan(Span span)
|
||||||
|
{
|
||||||
|
var afterEquals = false;
|
||||||
|
var builder = new SpanBuilder
|
||||||
|
{
|
||||||
|
CodeGenerator = span.CodeGenerator,
|
||||||
|
EditHandler = span.EditHandler,
|
||||||
|
Kind = span.Kind
|
||||||
|
};
|
||||||
|
var htmlSymbols = span.Symbols.OfType<HtmlSymbol>().ToArray();
|
||||||
|
var symbolOffset = 1;
|
||||||
|
string name = null;
|
||||||
|
|
||||||
|
// Iterate down through the symbols to find the name and the start of the value.
|
||||||
|
// We subtract the symbolOffset so we don't accept an ending quote of a span.
|
||||||
|
for (var i = 0; i < htmlSymbols.Length - symbolOffset; i++)
|
||||||
|
{
|
||||||
|
var symbol = htmlSymbols[i];
|
||||||
|
|
||||||
|
if (name == null && symbol.Type == HtmlSymbolType.Text)
|
||||||
|
{
|
||||||
|
name = symbol.Content;
|
||||||
|
}
|
||||||
|
else if (symbol.Type == HtmlSymbolType.Equals)
|
||||||
|
{
|
||||||
|
// We've found an '=' symbol, this means that the coming symbols will either be a quote
|
||||||
|
// or value (in the case that the value is unquoted).
|
||||||
|
// Spaces after/before the equal symbol are not yet supported:
|
||||||
|
// https://github.com/aspnet/Razor/issues/123
|
||||||
|
|
||||||
|
// TODO: Handle malformed tags, if there's an '=' then there MUST be a value.
|
||||||
|
// https://github.com/aspnet/Razor/issues/104
|
||||||
|
|
||||||
|
// Check for attribute start values, aka single or double quote
|
||||||
|
if (IsQuote(htmlSymbols[i + 1]))
|
||||||
|
{
|
||||||
|
// Move past the attribute start so we can accept the true value.
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Set the symbol offset to 0 so we don't attempt to skip an end quote that doesn't exist.
|
||||||
|
symbolOffset = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
afterEquals = true;
|
||||||
|
}
|
||||||
|
else if (afterEquals)
|
||||||
|
{
|
||||||
|
builder.Accept(symbol);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new KeyValuePair<string, SyntaxTreeNode>(name, builder.Build());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static KeyValuePair<string, SyntaxTreeNode> ParseBlock(Block block)
|
||||||
|
{
|
||||||
|
// TODO: Accept more than just spans: https://github.com/aspnet/Razor/issues/96.
|
||||||
|
// The first child will only ever NOT be a Span if a user is doing something like:
|
||||||
|
// <input @checked />
|
||||||
|
|
||||||
|
var childSpan = block.Children.First() as Span;
|
||||||
|
|
||||||
|
if (childSpan == null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(RazorResources.TagHelpers_CannotHaveCSharpInTagDeclaration);
|
||||||
|
}
|
||||||
|
|
||||||
|
var builder = new BlockBuilder(block);
|
||||||
|
|
||||||
|
// If there's only 1 child it means that it's plain text inside of the attribute.
|
||||||
|
// i.e. <div class="plain text in attribute">
|
||||||
|
if (builder.Children.Count == 1)
|
||||||
|
{
|
||||||
|
return ParseSpan(childSpan);
|
||||||
|
}
|
||||||
|
|
||||||
|
var textSymbol = childSpan.Symbols.FirstHtmlSymbolAs(HtmlSymbolType.Text);
|
||||||
|
var name = textSymbol != null ? textSymbol.Content : null;
|
||||||
|
|
||||||
|
if (name == null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(RazorResources.TagHelpers_AttributesMustHaveAName);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove first child i.e. foo="
|
||||||
|
builder.Children.RemoveAt(0);
|
||||||
|
|
||||||
|
// Grabbing last child to check if the attribute value is quoted.
|
||||||
|
var endNode = block.Children.Last();
|
||||||
|
if (!endNode.IsBlock)
|
||||||
|
{
|
||||||
|
var endSpan = (Span)endNode;
|
||||||
|
var endSymbol = (HtmlSymbol)endSpan.Symbols.Last();
|
||||||
|
|
||||||
|
// Checking to see if it's a quoted attribute, if so we should remove end quote
|
||||||
|
if (IsQuote(endSymbol))
|
||||||
|
{
|
||||||
|
builder.Children.RemoveAt(builder.Children.Count - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// We need to rebuild the code generators of the builder and its children (this is needed to
|
||||||
|
// ensure we don't do special attribute code generation since this is a tag helper).
|
||||||
|
block = RebuildCodeGenerators(builder.Build());
|
||||||
|
|
||||||
|
return new KeyValuePair<string, SyntaxTreeNode>(name, block);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Block RebuildCodeGenerators(Block block)
|
||||||
|
{
|
||||||
|
var builder = new BlockBuilder(block);
|
||||||
|
|
||||||
|
var isDynamic = builder.CodeGenerator is DynamicAttributeBlockCodeGenerator;
|
||||||
|
|
||||||
|
// We don't want any attribute specific logic here, null out the block code generator.
|
||||||
|
if (isDynamic || builder.CodeGenerator is AttributeBlockCodeGenerator)
|
||||||
|
{
|
||||||
|
builder.CodeGenerator = BlockCodeGenerator.Null;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var i = 0; i < builder.Children.Count; i++)
|
||||||
|
{
|
||||||
|
var child = builder.Children[i];
|
||||||
|
|
||||||
|
if (child.IsBlock)
|
||||||
|
{
|
||||||
|
// The child is a block, recurse down into the block to rebuild its children
|
||||||
|
builder.Children[i] = RebuildCodeGenerators((Block)child);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var childSpan = (Span)child;
|
||||||
|
ISpanCodeGenerator newCodeGenerator = null;
|
||||||
|
var literalGenerator = childSpan.CodeGenerator as LiteralAttributeCodeGenerator;
|
||||||
|
|
||||||
|
if (literalGenerator != null)
|
||||||
|
{
|
||||||
|
if (literalGenerator.ValueGenerator == null || literalGenerator.ValueGenerator.Value == null)
|
||||||
|
{
|
||||||
|
newCodeGenerator = new MarkupCodeGenerator();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
newCodeGenerator = literalGenerator.ValueGenerator.Value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (isDynamic && childSpan.CodeGenerator == SpanCodeGenerator.Null)
|
||||||
|
{
|
||||||
|
// Usually the dynamic code generator handles rendering the null code generators underneath
|
||||||
|
// it. This doesn't make sense in terms of tag helpers though, we need to change null code
|
||||||
|
// generators to markup code generators.
|
||||||
|
|
||||||
|
newCodeGenerator = new MarkupCodeGenerator();
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we have a new code generator we'll need to re-build the child
|
||||||
|
if (newCodeGenerator != null)
|
||||||
|
{
|
||||||
|
var childSpanBuilder = new SpanBuilder(childSpan)
|
||||||
|
{
|
||||||
|
CodeGenerator = newCodeGenerator
|
||||||
|
};
|
||||||
|
|
||||||
|
builder.Children[i] = childSpanBuilder.Build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return builder.Build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsQuote(HtmlSymbol htmlSymbol)
|
||||||
|
{
|
||||||
|
return htmlSymbol.Type == HtmlSymbolType.DoubleQuote ||
|
||||||
|
htmlSymbol.Type == HtmlSymbolType.SingleQuote;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,221 @@
|
||||||
|
// Copyright (c) Microsoft Open Technologies, Inc. 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;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Linq;
|
||||||
|
using Microsoft.AspNet.Razor.Parser.SyntaxTree;
|
||||||
|
using Microsoft.AspNet.Razor.TagHelpers;
|
||||||
|
using Microsoft.AspNet.Razor.Tokenizer.Symbols;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNet.Razor.Parser.TagHelpers.Internal
|
||||||
|
{
|
||||||
|
public class TagHelperParseTreeRewriter : ISyntaxTreeRewriter
|
||||||
|
{
|
||||||
|
private TagHelperDescriptorProvider _provider;
|
||||||
|
private Stack<TagHelperBlockBuilder> _tagStack;
|
||||||
|
private Stack<BlockBuilder> _blockStack;
|
||||||
|
private BlockBuilder _currentBlock;
|
||||||
|
|
||||||
|
public TagHelperParseTreeRewriter(TagHelperDescriptorProvider provider)
|
||||||
|
{
|
||||||
|
_provider = provider;
|
||||||
|
_tagStack = new Stack<TagHelperBlockBuilder>();
|
||||||
|
_blockStack = new Stack<BlockBuilder>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Block Rewrite(Block input)
|
||||||
|
{
|
||||||
|
RewriteTags(input);
|
||||||
|
|
||||||
|
Debug.Assert(_blockStack.Count == 0);
|
||||||
|
|
||||||
|
return _currentBlock.Build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RewriteTags(Block input)
|
||||||
|
{
|
||||||
|
// We want to start a new block without the children from existing (we rebuild them).
|
||||||
|
TrackBlock(new BlockBuilder
|
||||||
|
{
|
||||||
|
Type = input.Type,
|
||||||
|
CodeGenerator = input.CodeGenerator
|
||||||
|
});
|
||||||
|
|
||||||
|
foreach (var child in input.Children)
|
||||||
|
{
|
||||||
|
if (child.IsBlock)
|
||||||
|
{
|
||||||
|
var childBlock = (Block)child;
|
||||||
|
|
||||||
|
if (childBlock.Type == BlockType.Tag)
|
||||||
|
{
|
||||||
|
// TODO: Fully handle malformed tags: https://github.com/aspnet/Razor/issues/104
|
||||||
|
|
||||||
|
// Get tag name of the current block (doesn't matter if it's an end or start tag)
|
||||||
|
var tagName = GetTagName(childBlock);
|
||||||
|
|
||||||
|
if (tagName == null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!IsEndTag(childBlock))
|
||||||
|
{
|
||||||
|
// We're in a begin tag block
|
||||||
|
|
||||||
|
if (IsPotentialTagHelper(tagName, childBlock) && IsRegisteredTagHelper(tagName))
|
||||||
|
{
|
||||||
|
// Found a new tag helper block
|
||||||
|
TrackTagHelperBlock(new TagHelperBlockBuilder(tagName, childBlock));
|
||||||
|
|
||||||
|
// If it's a self closing block then we don't have to worry about nested children
|
||||||
|
// within the tag... complete it.
|
||||||
|
if (IsSelfClosing(childBlock))
|
||||||
|
{
|
||||||
|
BuildCurrentlyTrackedTagHelperBlock();
|
||||||
|
}
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var currentTagHelper = _tagStack.Count > 0 ? _tagStack.Peek() : null;
|
||||||
|
|
||||||
|
// Check if it's an "end" tag helper that matches our current tag helper
|
||||||
|
if (currentTagHelper != null &&
|
||||||
|
string.Equals(currentTagHelper.TagName, tagName, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
BuildCurrentlyTrackedTagHelperBlock();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We're in an end tag, there won't be anymore tag helpers nested.
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we get to here it means that we're a normal html tag. No need to iterate
|
||||||
|
// any deeper into the children of it because they wont be tag helpers.
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// We're not an Html tag so iterate through children recursively.
|
||||||
|
RewriteTags(childBlock);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// At this point the child is a Span or Block with Type BlockType.Tag that doesn't happen to be a
|
||||||
|
// tag helper.
|
||||||
|
|
||||||
|
// Add the child to current block.
|
||||||
|
_currentBlock.Children.Add(child);
|
||||||
|
}
|
||||||
|
|
||||||
|
BuildCurrentlyTrackedBlock();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void BuildCurrentlyTrackedBlock()
|
||||||
|
{
|
||||||
|
// Going to remove the current BlockBuilder from the stack because it's complete.
|
||||||
|
var currentBlock = _blockStack.Pop();
|
||||||
|
|
||||||
|
// If there are block stacks left it means we're not at the root.
|
||||||
|
if (_blockStack.Count > 0)
|
||||||
|
{
|
||||||
|
// Grab the next block in line so we can continue managing its children (it's not done).
|
||||||
|
var previousBlock = _blockStack.Peek();
|
||||||
|
|
||||||
|
// We've finished the currentBlock so build it and add it to its parent.
|
||||||
|
previousBlock.Children.Add(currentBlock.Build());
|
||||||
|
|
||||||
|
// Update the _currentBlock to point at the last tracked block because it's not complete.
|
||||||
|
_currentBlock = previousBlock;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_currentBlock = currentBlock;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void BuildCurrentlyTrackedTagHelperBlock()
|
||||||
|
{
|
||||||
|
_tagStack.Pop();
|
||||||
|
|
||||||
|
BuildCurrentlyTrackedBlock();
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool IsPotentialTagHelper(string tagName, Block childBlock)
|
||||||
|
{
|
||||||
|
var child = childBlock.Children.FirstOrDefault();
|
||||||
|
Debug.Assert(child != null);
|
||||||
|
|
||||||
|
var childSpan = (Span)child;
|
||||||
|
|
||||||
|
// text tags that are labeled as transitions should be ignored aka they're not tag helpers.
|
||||||
|
return !string.Equals(tagName, SyntaxConstants.TextTagName, StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
childSpan.Kind != SpanKind.Transition;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool IsRegisteredTagHelper(string tagName)
|
||||||
|
{
|
||||||
|
return _provider.GetTagHelpers(tagName).Any();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void TrackBlock(BlockBuilder builder)
|
||||||
|
{
|
||||||
|
_currentBlock = builder;
|
||||||
|
|
||||||
|
_blockStack.Push(builder);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void TrackTagHelperBlock(TagHelperBlockBuilder builder)
|
||||||
|
{
|
||||||
|
_tagStack.Push(builder);
|
||||||
|
|
||||||
|
TrackBlock(builder);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetTagName(Block tagBlock)
|
||||||
|
{
|
||||||
|
var child = tagBlock.Children.First();
|
||||||
|
|
||||||
|
if (tagBlock.Type != BlockType.Tag || !tagBlock.Children.Any() || !(child is Span))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var childSpan = (Span)child;
|
||||||
|
var textSymbol = childSpan.Symbols.FirstHtmlSymbolAs(HtmlSymbolType.Text);
|
||||||
|
|
||||||
|
return textSymbol != null ? textSymbol.Content : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsSelfClosing(Block beginTagBlock)
|
||||||
|
{
|
||||||
|
EnsureTagBlock(beginTagBlock);
|
||||||
|
|
||||||
|
var childSpan = (Span)beginTagBlock.Children.Last();
|
||||||
|
|
||||||
|
return childSpan.Content.EndsWith("/>");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsEndTag(Block tagBlock)
|
||||||
|
{
|
||||||
|
EnsureTagBlock(tagBlock);
|
||||||
|
|
||||||
|
var childSpan = (Span)tagBlock.Children.First();
|
||||||
|
// We grab the symbol that could be forward slash
|
||||||
|
var relevantSymbol = (HtmlSymbol)childSpan.Symbols.Take(2).Last();
|
||||||
|
|
||||||
|
return relevantSymbol.Type == HtmlSymbolType.ForwardSlash;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void EnsureTagBlock(Block tagBlock)
|
||||||
|
{
|
||||||
|
Debug.Assert(tagBlock.Type == BlockType.Tag);
|
||||||
|
Debug.Assert(tagBlock.Children.First() is Span);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1430,6 +1430,38 @@ namespace Microsoft.AspNet.Razor
|
||||||
return string.Format(CultureInfo.CurrentCulture, GetString("ParseError_Sections_Cannot_Be_Nested"), p0);
|
return string.Format(CultureInfo.CurrentCulture, GetString("ParseError_Sections_Cannot_Be_Nested"), p0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tag Helper attributes must have a name.
|
||||||
|
/// </summary>
|
||||||
|
internal static string TagHelpers_AttributesMustHaveAName
|
||||||
|
{
|
||||||
|
get { return GetString("TagHelpers_AttributesMustHaveAName"); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tag Helper attributes must have a name.
|
||||||
|
/// </summary>
|
||||||
|
internal static string FormatTagHelpers_AttributesMustHaveAName()
|
||||||
|
{
|
||||||
|
return GetString("TagHelpers_AttributesMustHaveAName");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tag Helpers cannot have C# in an HTML tag element's attribute declaration area.
|
||||||
|
/// </summary>
|
||||||
|
internal static string TagHelpers_CannotHaveCSharpInTagDeclaration
|
||||||
|
{
|
||||||
|
get { return GetString("TagHelpers_CannotHaveCSharpInTagDeclaration"); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tag Helpers cannot have C# in an HTML tag element's attribute declaration area.
|
||||||
|
/// </summary>
|
||||||
|
internal static string FormatTagHelpers_CannotHaveCSharpInTagDeclaration()
|
||||||
|
{
|
||||||
|
return GetString("TagHelpers_CannotHaveCSharpInTagDeclaration");
|
||||||
|
}
|
||||||
|
|
||||||
private static string GetString(string name, params string[] formatterNames)
|
private static string GetString(string name, params string[] formatterNames)
|
||||||
{
|
{
|
||||||
var value = _resourceManager.GetString(name);
|
var value = _resourceManager.GetString(name);
|
||||||
|
|
|
||||||
|
|
@ -403,4 +403,10 @@ Instead, wrap the contents of the block in "{{}}":
|
||||||
<data name="ParseError_Sections_Cannot_Be_Nested" xml:space="preserve">
|
<data name="ParseError_Sections_Cannot_Be_Nested" xml:space="preserve">
|
||||||
<value>Section blocks ("{0}") cannot be nested. Only one level of section blocks are allowed.</value>
|
<value>Section blocks ("{0}") cannot be nested. Only one level of section blocks are allowed.</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="TagHelpers_AttributesMustHaveAName" xml:space="preserve">
|
||||||
|
<value>Tag Helper attributes must have a name.</value>
|
||||||
|
</data>
|
||||||
|
<data name="TagHelpers_CannotHaveCSharpInTagDeclaration" xml:space="preserve">
|
||||||
|
<value>Tag Helpers cannot have C# in an HTML tag element's attribute declaration area.</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
|
|
@ -39,5 +39,17 @@ namespace Microsoft.AspNet.Razor.Tokenizer.Symbols
|
||||||
{
|
{
|
||||||
return new LocationTagged<string>(symbol.Content, symbol.Start);
|
return new LocationTagged<string>(symbol.Content, symbol.Start);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Converts the generic <see cref="IEnumerable{ISymbol}"/> to a <see cref="IEnumerable{HtmlSymbol}"/> and
|
||||||
|
/// finds the first <see cref="HtmlSymbol"/> with type <paramref name="type"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="symbols">The <see cref="IEnumerable{ISymbol}"/> instance this method extends.</param>
|
||||||
|
/// <param name="type">The <see cref="HtmlSymbolType"/> to search for.</param>
|
||||||
|
/// <returns>The first <see cref="HtmlSymbol"/> of type <paramref name="type"/>.</returns>
|
||||||
|
public static HtmlSymbol FirstHtmlSymbolAs(this IEnumerable<ISymbol> symbols, HtmlSymbolType type)
|
||||||
|
{
|
||||||
|
return symbols.OfType<HtmlSymbol>().FirstOrDefault(sym => sym.Type == type);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue