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
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,9 +5,12 @@ using System;
|
|||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNet.Razor.Parser.SyntaxTree;
|
||||
using Microsoft.AspNet.Razor.Parser.TagHelpers.Internal;
|
||||
using Microsoft.AspNet.Razor.TagHelpers;
|
||||
using Microsoft.AspNet.Razor.Text;
|
||||
|
||||
namespace Microsoft.AspNet.Razor.Parser
|
||||
|
|
@ -28,12 +31,22 @@ namespace Microsoft.AspNet.Razor.Parser
|
|||
MarkupParser = markupParser;
|
||||
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>()
|
||||
{
|
||||
// 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
|
||||
new WhiteSpaceRewriter(MarkupParser.BuildSpan),
|
||||
// Collapse conditional attributes where the entire value is literal
|
||||
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 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);
|
||||
}
|
||||
Type = source.Type.Value;
|
||||
Children = source.Children;
|
||||
Name = source.Name;
|
||||
CodeGenerator = source.CodeGenerator;
|
||||
source.Reset();
|
||||
|
||||
Type = type.Value;
|
||||
Children = contents;
|
||||
CodeGenerator = generator;
|
||||
|
||||
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)
|
||||
{
|
||||
Type = type;
|
||||
|
|
@ -42,7 +48,7 @@ namespace Microsoft.AspNet.Razor.Parser.SyntaxTree
|
|||
public BlockType Type { get; private set; }
|
||||
|
||||
public IEnumerable<SyntaxTreeNode> Children { get; private set; }
|
||||
public string Name { get; private set; }
|
||||
|
||||
public IBlockCodeGenerator CodeGenerator { get; private set; }
|
||||
|
||||
public override bool IsBlock
|
||||
|
|
|
|||
|
|
@ -18,26 +18,22 @@ namespace Microsoft.AspNet.Razor.Parser.SyntaxTree
|
|||
{
|
||||
Type = original.Type;
|
||||
Children = new List<SyntaxTreeNode>(original.Children);
|
||||
Name = original.Name;
|
||||
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")]
|
||||
public BlockType? Type { get; set; }
|
||||
|
||||
public IList<SyntaxTreeNode> Children { get; private set; }
|
||||
public string Name { get; set; }
|
||||
public IBlockCodeGenerator CodeGenerator { get; set; }
|
||||
|
||||
public Block Build()
|
||||
public virtual Block Build()
|
||||
{
|
||||
return new Block(this);
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
public virtual void Reset()
|
||||
{
|
||||
Type = null;
|
||||
Name = null;
|
||||
Children = new List<SyntaxTreeNode>();
|
||||
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);
|
||||
}
|
||||
|
||||
/// <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)
|
||||
{
|
||||
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">
|
||||
<value>Section blocks ("{0}") cannot be nested. Only one level of section blocks are allowed.</value>
|
||||
</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>
|
||||
|
|
@ -39,5 +39,17 @@ namespace Microsoft.AspNet.Razor.Tokenizer.Symbols
|
|||
{
|
||||
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