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