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:
N. Taylor Mullen 2014-08-28 23:11:57 -07:00
parent 83da8e257d
commit 3cba84104d
11 changed files with 743 additions and 14 deletions

View File

@ -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)
{
}
}
}

View File

@ -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);
}
}

View File

@ -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),
};
}

View File

@ -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

View File

@ -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;
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}
}

View File

@ -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);
}
}
}

View File

@ -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);

View File

@ -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>

View File

@ -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);
}
}
}