diff --git a/src/Microsoft.AspNet.Razor/Generator/TagHelperCodeGenerator.cs b/src/Microsoft.AspNet.Razor/Generator/TagHelperCodeGenerator.cs new file mode 100644 index 0000000000..d44101b208 --- /dev/null +++ b/src/Microsoft.AspNet.Razor/Generator/TagHelperCodeGenerator.cs @@ -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 +{ + /// + /// A that is responsible for generating valid s. + /// + public class TagHelperCodeGenerator : BlockCodeGenerator + { + /// + /// Starts the generation of a . + /// + /// + /// The responsible for this . + /// + /// A instance that contains information about + /// the current code generation process. + public override void GenerateStartBlockCode(Block target, CodeGeneratorContext context) + { + } + + /// + /// Ends the generation of a capturing all previously visited children + /// since method was called. + /// + /// + /// The responsible for this . + /// + /// A instance that contains information about + /// the current code generation process. + public override void GenerateEndBlockCode(Block target, CodeGeneratorContext context) + { + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Razor/Parser/ISyntaxTreeRewriter.cs b/src/Microsoft.AspNet.Razor/Parser/ISyntaxTreeRewriter.cs index 0a050cb84c..b080ca237f 100644 --- a/src/Microsoft.AspNet.Razor/Parser/ISyntaxTreeRewriter.cs +++ b/src/Microsoft.AspNet.Razor/Parser/ISyntaxTreeRewriter.cs @@ -5,8 +5,20 @@ using Microsoft.AspNet.Razor.Parser.SyntaxTree; namespace Microsoft.AspNet.Razor.Parser { - internal interface ISyntaxTreeRewriter + /// + /// Defines the contract for rewriting a syntax tree. + /// + public interface ISyntaxTreeRewriter { + /// + /// Rewrites the provided syntax tree. + /// + /// The current syntax tree. + /// The syntax tree or a syntax tree to be used instead of the + /// tree. + /// + /// If you choose not to modify the syntax tree you can always return . + /// Block Rewrite(Block input); } } diff --git a/src/Microsoft.AspNet.Razor/Parser/RazorParser.cs b/src/Microsoft.AspNet.Razor/Parser/RazorParser.cs index 7ef89705b9..8d1a42d583 100644 --- a/src/Microsoft.AspNet.Razor/Parser/RazorParser.cs +++ b/src/Microsoft.AspNet.Razor/Parser/RazorParser.cs @@ -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()); + Optimizers = new List() { + // 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), }; } diff --git a/src/Microsoft.AspNet.Razor/Parser/SyntaxTree/Block.cs b/src/Microsoft.AspNet.Razor/Parser/SyntaxTree/Block.cs index 565d09f259..b86e797c76 100644 --- a/src/Microsoft.AspNet.Razor/Parser/SyntaxTree/Block.cs +++ b/src/Microsoft.AspNet.Razor/Parser/SyntaxTree/Block.cs @@ -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 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 contents, IBlockCodeGenerator generator) { Type = type; @@ -42,7 +48,7 @@ namespace Microsoft.AspNet.Razor.Parser.SyntaxTree public BlockType Type { get; private set; } public IEnumerable Children { get; private set; } - public string Name { get; private set; } + public IBlockCodeGenerator CodeGenerator { get; private set; } public override bool IsBlock diff --git a/src/Microsoft.AspNet.Razor/Parser/SyntaxTree/BlockBuilder.cs b/src/Microsoft.AspNet.Razor/Parser/SyntaxTree/BlockBuilder.cs index 5ac144adec..7ce05f8b7e 100644 --- a/src/Microsoft.AspNet.Razor/Parser/SyntaxTree/BlockBuilder.cs +++ b/src/Microsoft.AspNet.Razor/Parser/SyntaxTree/BlockBuilder.cs @@ -18,26 +18,22 @@ namespace Microsoft.AspNet.Razor.Parser.SyntaxTree { Type = original.Type; Children = new List(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 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(); CodeGenerator = BlockCodeGenerator.Null; } diff --git a/src/Microsoft.AspNet.Razor/Parser/TagHelpers/TagHelperBlock.cs b/src/Microsoft.AspNet.Razor/Parser/TagHelpers/TagHelperBlock.cs new file mode 100644 index 0000000000..66cd083982 --- /dev/null +++ b/src/Microsoft.AspNet.Razor/Parser/TagHelpers/TagHelperBlock.cs @@ -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 +{ + /// + /// A that reprents a special HTML element. + /// + public class TagHelperBlock : Block, IEquatable + { + /// + /// Instantiates a new instance of a . + /// + /// A used to construct a valid + /// . + public TagHelperBlock(TagHelperBlockBuilder source) + : base(source.Type, source.Children, source.CodeGenerator) + { + TagName = source.TagName; + Attributes = new Dictionary(source.Attributes); + + source.Reset(); + + foreach (var attributeChildren in Attributes.Values) + { + attributeChildren.Parent = this; + } + } + + /// + /// The HTML attributes. + /// + public IDictionary Attributes { get; private set; } + + /// + /// The HTML tag name. + /// + public string TagName { get; private set; } + + /// + 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); + } + + /// + /// Determines whether two s are equal by comparing the , + /// , , and + /// . + /// + /// The to check equality against. + /// + /// true if the current is equivalent to the given + /// , false otherwise. + /// + public bool Equals(TagHelperBlock other) + { + return other != null && + TagName == other.TagName && + Attributes.SequenceEqual(other.Attributes) && + base.Equals(other); + } + + /// + public override int GetHashCode() + { + return HashCodeCombiner.Start() + .Add(TagName) + .Add(Attributes) + .Add(base.GetHashCode()) + .CombinedHash; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Razor/Parser/TagHelpers/TagHelperBlockBuilder.cs b/src/Microsoft.AspNet.Razor/Parser/TagHelpers/TagHelperBlockBuilder.cs new file mode 100644 index 0000000000..9e44c2deb1 --- /dev/null +++ b/src/Microsoft.AspNet.Razor/Parser/TagHelpers/TagHelperBlockBuilder.cs @@ -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 +{ + /// + /// A used to create s. + /// + public class TagHelperBlockBuilder : BlockBuilder + { + /// + /// Instantiates a new instance based on given the + /// . + /// + /// The original to copy data from. + public TagHelperBlockBuilder(TagHelperBlock original) + : base(original) + { + TagName = original.TagName; + Attributes = new Dictionary(original.Attributes); + } + + /// + /// Instantiates a new instance of the class + /// with the provided and derives its + /// and from the . + /// + /// An HTML tag name. + /// The that contains all information about the start + /// of the HTML element. + 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 attributes, + IEnumerable children) + { + TagName = tagName; + Attributes = attributes; + Type = BlockType.Tag; + CodeGenerator = new TagHelperCodeGenerator(); + + // Children is IList, no AddRange + foreach (var child in children) + { + Children.Add(child); + } + } + + /// + /// The HTML attributes. + /// + public IDictionary Attributes { get; private set; } + + /// + /// The HTML tag name. + /// + public string TagName { get; set; } + + /// + /// Constructs a new . + /// + /// A . + public override Block Build() + { + return new TagHelperBlock(this); + } + + /// + /// + /// Sets the to null and clears the . + /// + public override void Reset() + { + TagName = null; + + if (Attributes != null) + { + Attributes.Clear(); + } + + base.Reset(); + } + + private static IDictionary GetTagAttributes(Block tagBlock) + { + var attributes = new Dictionary(StringComparer.OrdinalIgnoreCase); + + // TODO: Handle malformed tags: https://github.com/aspnet/razor/issues/104 + + // We skip the first child "" 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 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 ParseSpan(Span span) + { + var afterEquals = false; + var builder = new SpanBuilder + { + CodeGenerator = span.CodeGenerator, + EditHandler = span.EditHandler, + Kind = span.Kind + }; + var htmlSymbols = span.Symbols.OfType().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(name, builder.Build()); + } + + private static KeyValuePair 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: + // + + 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.
+ 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(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; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Razor/Parser/TagHelpers/TagHelperParseTreeRewriter.cs b/src/Microsoft.AspNet.Razor/Parser/TagHelpers/TagHelperParseTreeRewriter.cs new file mode 100644 index 0000000000..2e536f0b05 --- /dev/null +++ b/src/Microsoft.AspNet.Razor/Parser/TagHelpers/TagHelperParseTreeRewriter.cs @@ -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 _tagStack; + private Stack _blockStack; + private BlockBuilder _currentBlock; + + public TagHelperParseTreeRewriter(TagHelperDescriptorProvider provider) + { + _provider = provider; + _tagStack = new Stack(); + _blockStack = new Stack(); + } + + 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); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Razor/Properties/RazorResources.Designer.cs b/src/Microsoft.AspNet.Razor/Properties/RazorResources.Designer.cs index b95d8d986b..bf85a90990 100644 --- a/src/Microsoft.AspNet.Razor/Properties/RazorResources.Designer.cs +++ b/src/Microsoft.AspNet.Razor/Properties/RazorResources.Designer.cs @@ -1430,6 +1430,38 @@ namespace Microsoft.AspNet.Razor return string.Format(CultureInfo.CurrentCulture, GetString("ParseError_Sections_Cannot_Be_Nested"), p0); } + /// + /// Tag Helper attributes must have a name. + /// + internal static string TagHelpers_AttributesMustHaveAName + { + get { return GetString("TagHelpers_AttributesMustHaveAName"); } + } + + /// + /// Tag Helper attributes must have a name. + /// + internal static string FormatTagHelpers_AttributesMustHaveAName() + { + return GetString("TagHelpers_AttributesMustHaveAName"); + } + + /// + /// Tag Helpers cannot have C# in an HTML tag element's attribute declaration area. + /// + internal static string TagHelpers_CannotHaveCSharpInTagDeclaration + { + get { return GetString("TagHelpers_CannotHaveCSharpInTagDeclaration"); } + } + + /// + /// Tag Helpers cannot have C# in an HTML tag element's attribute declaration area. + /// + internal static string FormatTagHelpers_CannotHaveCSharpInTagDeclaration() + { + return GetString("TagHelpers_CannotHaveCSharpInTagDeclaration"); + } + private static string GetString(string name, params string[] formatterNames) { var value = _resourceManager.GetString(name); diff --git a/src/Microsoft.AspNet.Razor/RazorResources.resx b/src/Microsoft.AspNet.Razor/RazorResources.resx index 8a717cea4f..9ea5df2833 100644 --- a/src/Microsoft.AspNet.Razor/RazorResources.resx +++ b/src/Microsoft.AspNet.Razor/RazorResources.resx @@ -403,4 +403,10 @@ Instead, wrap the contents of the block in "{{}}": Section blocks ("{0}") cannot be nested. Only one level of section blocks are allowed. + + Tag Helper attributes must have a name. + + + Tag Helpers cannot have C# in an HTML tag element's attribute declaration area. + \ No newline at end of file diff --git a/src/Microsoft.AspNet.Razor/Tokenizer/Symbols/SymbolExtensions.cs b/src/Microsoft.AspNet.Razor/Tokenizer/Symbols/SymbolExtensions.cs index ea7dbc529f..c72862656d 100644 --- a/src/Microsoft.AspNet.Razor/Tokenizer/Symbols/SymbolExtensions.cs +++ b/src/Microsoft.AspNet.Razor/Tokenizer/Symbols/SymbolExtensions.cs @@ -39,5 +39,17 @@ namespace Microsoft.AspNet.Razor.Tokenizer.Symbols { return new LocationTagged(symbol.Content, symbol.Start); } + + /// + /// Converts the generic to a and + /// finds the first with type . + /// + /// The instance this method extends. + /// The to search for. + /// The first of type . + public static HtmlSymbol FirstHtmlSymbolAs(this IEnumerable symbols, HtmlSymbolType type) + { + return symbols.OfType().FirstOrDefault(sym => sym.Type == type); + } } }