Add TagHelper parse level opt-out character '!'.
- Added the ability to opt-out of TagHelper parsing by adding a '!' to the beginning of a tag name. - Modified parsing logic to allow bangs in tags. - Bangs in tags are removed from output always and are handled as meta code. #187
This commit is contained in:
parent
32f0858e8f
commit
94230a5a14
|
|
@ -75,19 +75,19 @@ namespace Microsoft.AspNet.Razor.Runtime
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parameter {0} must not contain null tag names.
|
||||
/// Tag name cannot be null or whitespace.
|
||||
/// </summary>
|
||||
internal static string HtmlElementNameAttribute_AdditionalTagsCannotContainNull
|
||||
internal static string HtmlElementNameAttribute_ElementNameCannotBeNullOrWhitespace
|
||||
{
|
||||
get { return GetString("HtmlElementNameAttribute_AdditionalTagsCannotContainNull"); }
|
||||
get { return GetString("HtmlElementNameAttribute_ElementNameCannotBeNullOrWhitespace"); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parameter {0} must not contain null tag names.
|
||||
/// Tag name cannot be null or whitespace.
|
||||
/// </summary>
|
||||
internal static string FormatHtmlElementNameAttribute_AdditionalTagsCannotContainNull(object p0)
|
||||
internal static string FormatHtmlElementNameAttribute_ElementNameCannotBeNullOrWhitespace()
|
||||
{
|
||||
return string.Format(CultureInfo.CurrentCulture, GetString("HtmlElementNameAttribute_AdditionalTagsCannotContainNull"), p0);
|
||||
return GetString("HtmlElementNameAttribute_ElementNameCannotBeNullOrWhitespace");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -122,6 +122,22 @@ namespace Microsoft.AspNet.Razor.Runtime
|
|||
return string.Format(CultureInfo.CurrentCulture, GetString("TagHelperDescriptorResolver_EncounteredUnexpectedError"), p0, p1, p2);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tag helpers cannot target element name '{0}' because it contains a '{1}' character.
|
||||
/// </summary>
|
||||
internal static string HtmlElementNameAttribute_InvalidElementName
|
||||
{
|
||||
get { return GetString("HtmlElementNameAttribute_InvalidElementName"); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tag helpers cannot target element name '{0}' because it contains a '{1}' character.
|
||||
/// </summary>
|
||||
internal static string FormatHtmlElementNameAttribute_InvalidElementName(object p0, object p1)
|
||||
{
|
||||
return string.Format(CultureInfo.CurrentCulture, GetString("HtmlElementNameAttribute_InvalidElementName"), p0, p1);
|
||||
}
|
||||
|
||||
private static string GetString(string name, params string[] formatterNames)
|
||||
{
|
||||
var value = _resourceManager.GetString(name);
|
||||
|
|
|
|||
|
|
@ -1,17 +1,17 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
Version 2.0
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
associated with the data types.
|
||||
|
||||
|
||||
Example:
|
||||
|
||||
|
||||
... ado.net/XML headers & schema ...
|
||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||
<resheader name="version">2.0</resheader>
|
||||
|
|
@ -26,36 +26,36 @@
|
|||
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||
<comment>This is a comment</comment>
|
||||
</data>
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
name/value pairs.
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
mimetype set.
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
extensible. For a given mimetype the value must be set accordingly:
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
read any of the formats listed below.
|
||||
|
||||
|
||||
mimetype: application/x-microsoft.net.object.binary.base64
|
||||
value : The object must be serialized with
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
|
||||
mimetype: application/x-microsoft.net.object.soap.base64
|
||||
value : The object must be serialized with
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||
value : The object must be serialized into a byte array
|
||||
value : The object must be serialized into a byte array
|
||||
: using a System.ComponentModel.TypeConverter
|
||||
: and then encoded with base64 encoding.
|
||||
-->
|
||||
|
|
@ -129,8 +129,8 @@
|
|||
<data name="ScopeManager_EndCannotBeCalledWithoutACallToBegin" xml:space="preserve">
|
||||
<value>Must call '{2}.{1}' before calling '{2}.{0}'.</value>
|
||||
</data>
|
||||
<data name="HtmlElementNameAttribute_AdditionalTagsCannotContainNull" xml:space="preserve">
|
||||
<value>Parameter {0} must not contain null tag names.</value>
|
||||
<data name="HtmlElementNameAttribute_ElementNameCannotBeNullOrWhitespace" xml:space="preserve">
|
||||
<value>Tag name cannot be null or whitespace.</value>
|
||||
</data>
|
||||
<data name="ArgumentCannotBeNullOrEmpty" xml:space="preserve">
|
||||
<value>The value cannot be null or empty.</value>
|
||||
|
|
@ -138,4 +138,7 @@
|
|||
<data name="TagHelperDescriptorResolver_EncounteredUnexpectedError" xml:space="preserve">
|
||||
<value>Encountered an unexpected error when attempting to resolve tag helper directive '{0}' with value '{1}'. Error: {2}</value>
|
||||
</data>
|
||||
<data name="HtmlElementNameAttribute_InvalidElementName" xml:space="preserve">
|
||||
<value>Tag helpers cannot target element name '{0}' because it contains a '{1}' character.</value>
|
||||
</data>
|
||||
</root>
|
||||
|
|
@ -19,6 +19,8 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
|
|||
/// <param name="tag">The HTML tag name for the <see cref="TagHelper"/> to target.</param>
|
||||
public HtmlElementNameAttribute([NotNull] string tag)
|
||||
{
|
||||
ValidateTagName(tag, nameof(tag));
|
||||
|
||||
Tags = new[] { tag };
|
||||
}
|
||||
|
||||
|
|
@ -29,12 +31,12 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
|
|||
/// <param name="additionalTags">Additional HTML tag names for the <see cref="TagHelper"/> to target.</param>
|
||||
public HtmlElementNameAttribute([NotNull] string tag, [NotNull] params string[] additionalTags)
|
||||
{
|
||||
if (additionalTags.Contains(null))
|
||||
ValidateTagName(tag, nameof(tag));
|
||||
|
||||
foreach (var tagName in additionalTags)
|
||||
{
|
||||
throw new ArgumentNullException(
|
||||
nameof(additionalTags),
|
||||
Resources.FormatHtmlElementNameAttribute_AdditionalTagsCannotContainNull(nameof(additionalTags)));
|
||||
};
|
||||
ValidateTagName(tagName, nameof(additionalTags));
|
||||
}
|
||||
|
||||
var allTags = new List<string>(additionalTags);
|
||||
allTags.Add(tag);
|
||||
|
|
@ -45,6 +47,23 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
|
|||
/// <summary>
|
||||
/// An <see cref="IEnumerable{string}"/> of tag names for the <see cref="TagHelper"/> to target.
|
||||
/// </summary>
|
||||
public IEnumerable<string> Tags { get; private set; }
|
||||
public IEnumerable<string> Tags { get; }
|
||||
|
||||
private static void ValidateTagName(string tagName, string parameterName)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(tagName))
|
||||
{
|
||||
throw new ArgumentException(
|
||||
Resources.HtmlElementNameAttribute_ElementNameCannotBeNullOrWhitespace,
|
||||
parameterName);
|
||||
}
|
||||
|
||||
if (tagName.Contains('!'))
|
||||
{
|
||||
throw new ArgumentException(
|
||||
Resources.FormatHtmlElementNameAttribute_InvalidElementName(tagName, '!'),
|
||||
parameterName);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -133,7 +133,9 @@ namespace Microsoft.AspNet.Razor.Parser
|
|||
IDisposable tagBlockWrapper = null;
|
||||
try
|
||||
{
|
||||
if (!EndOfFile && !AtSpecialTag)
|
||||
var atSpecialTag = AtSpecialTag;
|
||||
|
||||
if (!EndOfFile && !atSpecialTag)
|
||||
{
|
||||
// Start a Block tag. This is used to wrap things like <p> or <a class="btn"> etc.
|
||||
tagBlockWrapper = Context.StartBlock(BlockType.Tag);
|
||||
|
|
@ -157,7 +159,7 @@ namespace Microsoft.AspNet.Razor.Parser
|
|||
}
|
||||
else
|
||||
{
|
||||
complete = AfterTagStart(tagStart, tags, tagBlockWrapper);
|
||||
complete = AfterTagStart(tagStart, tags, atSpecialTag, tagBlockWrapper);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -187,6 +189,7 @@ namespace Microsoft.AspNet.Razor.Parser
|
|||
|
||||
private bool AfterTagStart(SourceLocation tagStart,
|
||||
Stack<Tuple<HtmlSymbol, SourceLocation>> tags,
|
||||
bool atSpecialTag,
|
||||
IDisposable tagBlockWrapper)
|
||||
{
|
||||
if (!EndOfFile)
|
||||
|
|
@ -197,9 +200,16 @@ namespace Microsoft.AspNet.Razor.Parser
|
|||
// End Tag
|
||||
return EndTag(tagStart, tags, tagBlockWrapper);
|
||||
case HtmlSymbolType.Bang:
|
||||
// Comment
|
||||
Accept(_bufferedOpenAngle);
|
||||
return BangTag();
|
||||
// Comment, CDATA, DOCTYPE, or a parser-escaped HTML tag.
|
||||
if (atSpecialTag)
|
||||
{
|
||||
Accept(_bufferedOpenAngle);
|
||||
return BangTag();
|
||||
}
|
||||
else
|
||||
{
|
||||
goto default;
|
||||
}
|
||||
case HtmlSymbolType.QuestionMark:
|
||||
// XML PI
|
||||
Accept(_bufferedOpenAngle);
|
||||
|
|
@ -275,30 +285,48 @@ namespace Microsoft.AspNet.Razor.Parser
|
|||
{
|
||||
// Accept "/" and move next
|
||||
Assert(HtmlSymbolType.ForwardSlash);
|
||||
var solidus = CurrentSymbol;
|
||||
var forwardSlash = CurrentSymbol;
|
||||
if (!NextToken())
|
||||
{
|
||||
Accept(_bufferedOpenAngle);
|
||||
Accept(solidus);
|
||||
Accept(forwardSlash);
|
||||
return false;
|
||||
}
|
||||
else
|
||||
{
|
||||
var tagName = String.Empty;
|
||||
if (At(HtmlSymbolType.Text))
|
||||
var tagName = string.Empty;
|
||||
HtmlSymbol bangSymbol = null;
|
||||
|
||||
if (At(HtmlSymbolType.Bang))
|
||||
{
|
||||
bangSymbol = CurrentSymbol;
|
||||
|
||||
var nextSymbol = Lookahead(count: 1);
|
||||
|
||||
if (nextSymbol != null && nextSymbol.Type == HtmlSymbolType.Text)
|
||||
{
|
||||
tagName = "!" + nextSymbol.Content;
|
||||
}
|
||||
}
|
||||
else if (At(HtmlSymbolType.Text))
|
||||
{
|
||||
tagName = CurrentSymbol.Content;
|
||||
}
|
||||
|
||||
var matched = RemoveTag(tags, tagName, tagStart);
|
||||
|
||||
if (tags.Count == 0 &&
|
||||
String.Equals(tagName, SyntaxConstants.TextTagName, StringComparison.OrdinalIgnoreCase) &&
|
||||
// Note tagName may contain a '!' escape character. This ensures </!text> doesn't match here.
|
||||
// </!text> tags are treated like any other escaped HTML end tag.
|
||||
string.Equals(tagName, SyntaxConstants.TextTagName, StringComparison.OrdinalIgnoreCase) &&
|
||||
matched)
|
||||
{
|
||||
return EndTextTag(solidus, tagBlockWrapper);
|
||||
return EndTextTag(forwardSlash, tagBlockWrapper);
|
||||
}
|
||||
Accept(_bufferedOpenAngle);
|
||||
Accept(solidus);
|
||||
Accept(forwardSlash);
|
||||
|
||||
OptionalBangEscape();
|
||||
|
||||
AcceptUntil(HtmlSymbolType.CloseAngle);
|
||||
|
||||
|
|
@ -347,14 +375,22 @@ namespace Microsoft.AspNet.Razor.Parser
|
|||
return seenCloseAngle;
|
||||
}
|
||||
|
||||
// Special tags include <! and <? tags
|
||||
// Special tags include <!--, <!DOCTYPE, <![CDATA and <? tags
|
||||
private bool AtSpecialTag
|
||||
{
|
||||
get
|
||||
{
|
||||
return (At(HtmlSymbolType.OpenAngle) &&
|
||||
(NextIs(HtmlSymbolType.Bang) ||
|
||||
NextIs(HtmlSymbolType.QuestionMark)));
|
||||
if (At(HtmlSymbolType.OpenAngle))
|
||||
{
|
||||
if (NextIs(HtmlSymbolType.Bang))
|
||||
{
|
||||
return !IsBangEscape(lookahead: 1);
|
||||
}
|
||||
|
||||
return NextIs(HtmlSymbolType.QuestionMark);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -651,20 +687,41 @@ namespace Microsoft.AspNet.Razor.Parser
|
|||
|
||||
private bool StartTag(Stack<Tuple<HtmlSymbol, SourceLocation>> tags, IDisposable tagBlockWrapper)
|
||||
{
|
||||
// If we're at text, it's the name, otherwise the name is ""
|
||||
HtmlSymbol tagName;
|
||||
if (At(HtmlSymbolType.Text))
|
||||
HtmlSymbol bangSymbol = null;
|
||||
HtmlSymbol potentialTagNameSymbol;
|
||||
|
||||
if (At(HtmlSymbolType.Bang))
|
||||
{
|
||||
tagName = CurrentSymbol;
|
||||
bangSymbol = CurrentSymbol;
|
||||
|
||||
potentialTagNameSymbol = Lookahead(count: 1);
|
||||
}
|
||||
else
|
||||
{
|
||||
tagName = new HtmlSymbol(CurrentLocation, String.Empty, HtmlSymbolType.Unknown);
|
||||
potentialTagNameSymbol = CurrentSymbol;
|
||||
}
|
||||
|
||||
HtmlSymbol tagName;
|
||||
|
||||
if (potentialTagNameSymbol == null || potentialTagNameSymbol.Type != HtmlSymbolType.Text)
|
||||
{
|
||||
tagName = new HtmlSymbol(potentialTagNameSymbol.Start, string.Empty, HtmlSymbolType.Unknown);
|
||||
}
|
||||
else if (bangSymbol != null)
|
||||
{
|
||||
tagName = new HtmlSymbol(bangSymbol.Start, "!" + potentialTagNameSymbol.Content, HtmlSymbolType.Text);
|
||||
}
|
||||
else
|
||||
{
|
||||
tagName = potentialTagNameSymbol;
|
||||
}
|
||||
|
||||
Tuple<HtmlSymbol, SourceLocation> tag = Tuple.Create(tagName, _lastTagStart);
|
||||
|
||||
if (tags.Count == 0 && String.Equals(tag.Item1.Content, SyntaxConstants.TextTagName, StringComparison.OrdinalIgnoreCase))
|
||||
if (tags.Count == 0 &&
|
||||
// Note tagName may contain a '!' escape character. This ensures <!text> doesn't match here.
|
||||
// <!text> tags are treated like any other escaped HTML start tag.
|
||||
string.Equals(tag.Item1.Content, SyntaxConstants.TextTagName, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
Output(SpanKind.Markup);
|
||||
Span.CodeGenerator = SpanCodeGenerator.Null;
|
||||
|
|
@ -709,7 +766,9 @@ namespace Microsoft.AspNet.Razor.Parser
|
|||
|
||||
return true;
|
||||
}
|
||||
|
||||
Accept(_bufferedOpenAngle);
|
||||
OptionalBangEscape();
|
||||
Optional(HtmlSymbolType.Text);
|
||||
return RestOfTag(tag, tags, tagBlockWrapper);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using Microsoft.AspNet.Razor.Generator;
|
||||
using Microsoft.AspNet.Razor.Parser.SyntaxTree;
|
||||
using Microsoft.AspNet.Razor.Tokenizer.Symbols;
|
||||
|
||||
|
|
@ -42,58 +43,69 @@ namespace Microsoft.AspNet.Razor.Parser
|
|||
{
|
||||
if (NextIs(HtmlSymbolType.Bang))
|
||||
{
|
||||
AcceptAndMoveNext(); // Accept '<'
|
||||
BangTag();
|
||||
// Checking to see if we meet the conditions of a special '!' tag: <!DOCTYPE, <![CDATA[, <!--.
|
||||
if (!IsBangEscape(lookahead: 1))
|
||||
{
|
||||
AcceptAndMoveNext(); // Accept '<'
|
||||
BangTag();
|
||||
return;
|
||||
}
|
||||
|
||||
// We should behave like a normal tag that has a parser escape, fall through to the normal
|
||||
// tag logic.
|
||||
}
|
||||
else if (NextIs(HtmlSymbolType.QuestionMark))
|
||||
{
|
||||
AcceptAndMoveNext(); // Accept '<'
|
||||
XmlPI();
|
||||
return;
|
||||
}
|
||||
|
||||
Output(SpanKind.Markup);
|
||||
|
||||
// Start tag block
|
||||
var tagBlock = Context.StartBlock(BlockType.Tag);
|
||||
|
||||
AcceptAndMoveNext(); // Accept '<'
|
||||
|
||||
if (!At(HtmlSymbolType.ForwardSlash))
|
||||
{
|
||||
OptionalBangEscape();
|
||||
|
||||
// Parsing a start tag
|
||||
var scriptTag = At(HtmlSymbolType.Text) &&
|
||||
string.Equals(CurrentSymbol.Content, "script", StringComparison.OrdinalIgnoreCase);
|
||||
Optional(HtmlSymbolType.Text);
|
||||
TagContent(); // Parse the tag, don't care about the content
|
||||
Optional(HtmlSymbolType.ForwardSlash);
|
||||
Optional(HtmlSymbolType.CloseAngle);
|
||||
|
||||
if (scriptTag)
|
||||
{
|
||||
Output(SpanKind.Markup);
|
||||
tagBlock.Dispose();
|
||||
|
||||
SkipToEndScriptAndParseCode();
|
||||
return;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Output(SpanKind.Markup);
|
||||
// Parsing an end tag
|
||||
// This section can accept things like: '</p >' or '</p>' etc.
|
||||
Optional(HtmlSymbolType.ForwardSlash);
|
||||
|
||||
// Start tag block
|
||||
var tagBlock = Context.StartBlock(BlockType.Tag);
|
||||
|
||||
AcceptAndMoveNext(); // Accept '<'
|
||||
|
||||
if (!At(HtmlSymbolType.ForwardSlash))
|
||||
{
|
||||
// Parsing a start tag
|
||||
var scriptTag = At(HtmlSymbolType.Text) &&
|
||||
string.Equals(CurrentSymbol.Content, "script", StringComparison.OrdinalIgnoreCase);
|
||||
Optional(HtmlSymbolType.Text);
|
||||
TagContent(); // Parse the tag, don't care about the content
|
||||
Optional(HtmlSymbolType.ForwardSlash);
|
||||
Optional(HtmlSymbolType.CloseAngle);
|
||||
|
||||
if (scriptTag)
|
||||
{
|
||||
Output(SpanKind.Markup);
|
||||
tagBlock.Dispose();
|
||||
|
||||
SkipToEndScriptAndParseCode();
|
||||
return;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Parsing an end tag
|
||||
// This section can accept things like: '</p >' or '</p>' etc.
|
||||
Optional(HtmlSymbolType.ForwardSlash);
|
||||
// Whitespace here is invalid (according to the spec)
|
||||
Optional(HtmlSymbolType.Text);
|
||||
AcceptAll(HtmlSymbolType.WhiteSpace);
|
||||
Optional(HtmlSymbolType.CloseAngle);
|
||||
}
|
||||
|
||||
Output(SpanKind.Markup);
|
||||
|
||||
// End tag block
|
||||
tagBlock.Dispose();
|
||||
// Whitespace here is invalid (according to the spec)
|
||||
OptionalBangEscape();
|
||||
Optional(HtmlSymbolType.Text);
|
||||
AcceptAll(HtmlSymbolType.WhiteSpace);
|
||||
Optional(HtmlSymbolType.CloseAngle);
|
||||
}
|
||||
|
||||
Output(SpanKind.Markup);
|
||||
|
||||
// End tag block
|
||||
tagBlock.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -184,5 +184,38 @@ namespace Microsoft.AspNet.Razor.Parser
|
|||
Initialize(Span);
|
||||
NextToken();
|
||||
}
|
||||
|
||||
private bool IsBangEscape(int lookahead)
|
||||
{
|
||||
var potentialBang = Lookahead(lookahead);
|
||||
|
||||
if (potentialBang != null &&
|
||||
potentialBang.Type == HtmlSymbolType.Bang)
|
||||
{
|
||||
var afterBang = Lookahead(lookahead + 1);
|
||||
|
||||
return afterBang != null &&
|
||||
afterBang.Type == HtmlSymbolType.Text &&
|
||||
!string.Equals(afterBang.Content, "DOCTYPE", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void OptionalBangEscape()
|
||||
{
|
||||
if (IsBangEscape(lookahead: 0))
|
||||
{
|
||||
Output(SpanKind.Markup);
|
||||
|
||||
// Accept the parser escape character '!'.
|
||||
Assert(HtmlSymbolType.Bang);
|
||||
AcceptAndMoveNext();
|
||||
|
||||
// Setup the metacode span that we will be outputing.
|
||||
Span.CodeGenerator = SpanCodeGenerator.Null;
|
||||
Output(SpanKind.MetaCode, AcceptedCharacters.None);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Microsoft.AspNet.Razor.Parser.SyntaxTree;
|
||||
using Microsoft.AspNet.Razor.Text;
|
||||
|
|
@ -75,6 +76,43 @@ namespace Microsoft.AspNet.Razor.Parser
|
|||
}
|
||||
}
|
||||
|
||||
protected TSymbol Lookahead(int count)
|
||||
{
|
||||
if (count < 0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(count));
|
||||
}
|
||||
else if (count == 0)
|
||||
{
|
||||
return CurrentSymbol;
|
||||
}
|
||||
|
||||
// We add 1 in order to store the current symbol.
|
||||
var symbols = new TSymbol[count + 1];
|
||||
var currentSymbol = CurrentSymbol;
|
||||
|
||||
symbols[0] = currentSymbol;
|
||||
|
||||
// We need to look forward "count" many times.
|
||||
for (var i = 1; i <= count; i++)
|
||||
{
|
||||
NextToken();
|
||||
symbols[i] = CurrentSymbol;
|
||||
}
|
||||
|
||||
// Restore Tokenizer's location to where it was pointing before the look-ahead.
|
||||
for (var i = count; i >= 0; i--)
|
||||
{
|
||||
PutBack(symbols[i]);
|
||||
}
|
||||
|
||||
// The PutBacks above will set CurrentSymbol to null. EnsureCurrent will set our CurrentSymbol to the
|
||||
// next symbol.
|
||||
EnsureCurrent();
|
||||
|
||||
return symbols[count];
|
||||
}
|
||||
|
||||
protected internal bool NextToken()
|
||||
{
|
||||
PreviousSymbol = CurrentSymbol;
|
||||
|
|
|
|||
Loading…
Reference in New Issue