Files
markdig/src/Textamina.Markdig/Syntax/HtmlBlock.cs

309 lines
11 KiB
C#
Raw Normal View History

2016-02-16 22:42:40 +09:00
using System;
using Textamina.Markdig.Helpers;
2016-02-16 22:42:40 +09:00
using Textamina.Markdig.Parsing;
namespace Textamina.Markdig.Syntax
{
public class HtmlBlock : LeafBlock
{
public static readonly BlockParser Parser = new ParserInternal();
public HtmlBlock(BlockParser parser) : base(parser)
2016-02-17 20:45:04 +09:00
{
// We don't process inline of an html block, as we will copy the content as-is
NoInline = true;
}
2016-02-16 22:42:40 +09:00
public HtmlBlockType Type { get; set; }
private class ParserInternal : BlockParser
{
private static readonly string[] HtmlTags =
{
"address", // 0
"article", // 1
"aside", // 2
"base", // 3
"basefont", // 4
"blockquote", // 5
"body", // 6
"caption", // 7
"center", // 8
"col", // 9
"colgroup", // 10
"dd", // 11
"details", // 12
"dialog", // 13
"dir", // 14
"div", // 15
"dl", // 16
"dt", // 17
"fieldset", // 18
"figcaption", // 19
"figure", // 20
"footer", // 21
"form", // 22
"frame", // 23
"frameset", // 24
"h1", // 25
"head", // 26
"header", // 27
"hr", // 28
"html", // 29
"iframe", // 30
"legend", // 31
"li", // 32
"link", // 33
"main", // 34
"menu", // 35
"menuitem", // 36
"meta", // 37
"nav", // 38
"noframes", // 39
"ol", // 40
"optgroup", // 41
"option", // 42
"p", // 43
"param", // 44
"pre", // 45 <- special group 1
"script", // 46 <- special group 1
"section", // 47
"source", // 48
"style", // 49 <- special group 1
"summary", // 50
"table", // 51
"tbody", // 52
"td", // 53
"tfoot", // 54
"th", // 55
"thead", // 56
"title", // 57
"tr", // 58
"track", // 59
"ul", // 60
};
public override MatchLineResult Match(BlockParserState state)
2016-02-16 22:42:40 +09:00
{
var htmlBlock = state.Pending as HtmlBlock;
2016-02-16 22:42:40 +09:00
if (htmlBlock == null)
{
2016-02-22 20:40:34 +09:00
var result = MatchStart(state);
// An end-tag can occur on the same line
if (result == MatchLineResult.Continue)
{
return MatchEnd(state, (HtmlBlock) state.NewBlocks.Peek());
}
return result;
2016-02-16 22:42:40 +09:00
}
2016-02-22 20:40:34 +09:00
return MatchEnd(state, htmlBlock);
2016-02-16 22:42:40 +09:00
}
private MatchLineResult MatchStart(BlockParserState state)
2016-02-16 22:42:40 +09:00
{
int index = 0;
for (int i = 0; i < 3; i++)
{
2016-02-25 20:30:37 +09:00
if (!state.Line.PeekChar(index).IsSpace())
2016-02-16 22:42:40 +09:00
{
break;
}
index++;
}
// Early exit if it is not starting by an HTML tag
var column = index;
2016-02-25 20:30:37 +09:00
var c = state.Line.PeekChar(index++);
2016-02-16 22:42:40 +09:00
if (c != '<')
{
return MatchLineResult.None;
}
2016-02-25 20:30:37 +09:00
var result = TryParseTagType16(state, ref state.Line, index, column);
2016-02-22 20:40:34 +09:00
// HTML blocks of type 7 cannot interrupt a paragraph:
if (result == MatchLineResult.None && !(state.LastBlock is ParagraphBlock))
{
2016-02-25 20:30:37 +09:00
result = TryParseTagType7(state, ref state.Line, index, column);
2016-02-22 20:40:34 +09:00
}
return result;
}
2016-02-25 20:30:37 +09:00
private MatchLineResult TryParseTagType7(BlockParserState state, ref StringSlice liner, int index, int startColumn)
2016-02-22 20:40:34 +09:00
{
var builder = StringBuilderCache.Local();
2016-02-25 20:30:37 +09:00
liner.Start = index;
var c = liner.CurrentChar;
2016-02-22 20:40:34 +09:00
var result = MatchLineResult.None;
2016-02-25 20:30:37 +09:00
if ((c == '/' && HtmlHelper.TryParseHtmlCloseTag(liner, builder)) || HtmlHelper.TryParseHtmlTagOpenTag(liner, builder))
2016-02-22 20:40:34 +09:00
{
// Must be followed by whitespace only
bool hasOnlySpaces = true;
2016-02-25 20:30:37 +09:00
c = liner.CurrentChar;
2016-02-22 20:40:34 +09:00
while (true)
{
if (c == '\0')
{
break;
}
if (!c.IsWhitespace())
{
hasOnlySpaces = false;
break;
}
2016-02-25 20:30:37 +09:00
c = liner.NextChar();
2016-02-22 20:40:34 +09:00
}
if (hasOnlySpaces)
{
result = CreateHtmlBlock(state, HtmlBlockType.NonInterruptingBlock, startColumn);
2016-02-22 20:40:34 +09:00
}
}
builder.Clear();
return result;
}
2016-02-25 20:30:37 +09:00
private MatchLineResult TryParseTagType16(BlockParserState state, ref StringSlice liner, int index, int startColumn)
2016-02-22 20:40:34 +09:00
{
char c;
2016-02-16 22:42:40 +09:00
c = liner.PeekChar(index);
if (c == '!')
{
c = liner.PeekChar(index + 1);
if (c == '-' && liner.PeekChar(index + 2) == '-')
{
return CreateHtmlBlock(state, HtmlBlockType.Comment, startColumn); // group 2
2016-02-16 22:42:40 +09:00
}
2016-02-22 20:40:34 +09:00
if (c.IsAlphaUpper())
2016-02-16 22:42:40 +09:00
{
return CreateHtmlBlock(state, HtmlBlockType.DocumentType, startColumn); // group 4
2016-02-16 22:42:40 +09:00
}
if (c == '[' && liner.Match("CDATA[", 3))
{
return CreateHtmlBlock(state, HtmlBlockType.CData, startColumn); // group 5
2016-02-16 22:42:40 +09:00
}
return MatchLineResult.None;
}
if (c == '?')
{
return CreateHtmlBlock(state, HtmlBlockType.ProcessingInstruction, startColumn); // group 3
2016-02-16 22:42:40 +09:00
}
var hasLeadingClose = c == '/';
if (hasLeadingClose)
{
index++;
}
var tag = new char[10];
var count = 0;
for (; count < tag.Length; index++, count++)
{
c = liner.PeekChar(index);
2016-02-22 20:40:34 +09:00
if (!c.IsAlphaNumeric())
2016-02-16 22:42:40 +09:00
{
break;
}
tag[count] = char.ToLowerInvariant(c);
}
2016-02-22 20:40:34 +09:00
if (
!(c == '>' || (!hasLeadingClose && c == '/' && liner.PeekChar(index + 1) == '>') || c.IsWhitespace() ||
c == '\0'))
2016-02-16 22:42:40 +09:00
{
return MatchLineResult.None;
}
if (count == 0)
{
return MatchLineResult.None;
}
var tagName = new string(tag, 0, count);
var tagIndex = Array.BinarySearch(HtmlTags, tagName, StringComparer.Ordinal);
if (tagIndex < 0)
{
return MatchLineResult.None;
}
// Cannot start with </script </pre or </style
if ((tagIndex == 45 || tagIndex == 46 || tagIndex == 49))
{
if (c == '/' || hasLeadingClose)
{
return MatchLineResult.None;
}
return CreateHtmlBlock(state, HtmlBlockType.ScriptPreOrStyle, startColumn);
2016-02-16 22:42:40 +09:00
}
return CreateHtmlBlock(state, HtmlBlockType.InterruptingBlock, startColumn);
2016-02-16 22:42:40 +09:00
}
private MatchLineResult MatchEnd(BlockParserState state, HtmlBlock htmlBlock)
2016-02-16 22:42:40 +09:00
{
// Early exit if it is not starting by an HTML tag
2016-02-25 20:30:37 +09:00
var c = state.Line.CurrentChar;
2016-02-16 22:42:40 +09:00
switch (htmlBlock.Type)
{
case HtmlBlockType.Comment:
2016-02-25 20:30:37 +09:00
if (state.Line.Search("-->"))
2016-02-16 22:42:40 +09:00
{
return MatchLineResult.Last;
}
break;
case HtmlBlockType.CData:
2016-02-25 20:30:37 +09:00
if (state.Line.Search("]]>"))
2016-02-16 22:42:40 +09:00
{
return MatchLineResult.Last;
}
break;
case HtmlBlockType.ProcessingInstruction:
2016-02-25 20:30:37 +09:00
if (state.Line.Search("?>"))
2016-02-16 22:42:40 +09:00
{
return MatchLineResult.Last;
}
break;
case HtmlBlockType.DocumentType:
2016-02-25 20:30:37 +09:00
if (state.Line.Search(">"))
2016-02-16 22:42:40 +09:00
{
return MatchLineResult.Last;
}
break;
case HtmlBlockType.ScriptPreOrStyle:
// TODO: could be optimized with a dedicated parser
2016-02-25 20:30:37 +09:00
if (state.Line.SearchLowercase("</script>") || state.Line.SearchLowercase("</pre>") || state.Line.SearchLowercase("</style>"))
2016-02-16 22:42:40 +09:00
{
return MatchLineResult.Last;
2016-02-16 22:42:40 +09:00
}
break;
case HtmlBlockType.InterruptingBlock:
2016-02-25 20:30:37 +09:00
if (state.IsBlankLine)
2016-02-16 22:42:40 +09:00
{
return MatchLineResult.LastDiscard;
}
break;
case HtmlBlockType.NonInterruptingBlock:
2016-02-25 20:30:37 +09:00
if (state.IsBlankLine)
2016-02-22 20:40:34 +09:00
{
return MatchLineResult.LastDiscard;
}
2016-02-16 22:42:40 +09:00
break;
}
return MatchLineResult.Continue;
}
private MatchLineResult CreateHtmlBlock(BlockParserState state, HtmlBlockType type, int startColumn)
2016-02-16 22:42:40 +09:00
{
state.NewBlocks.Push(new HtmlBlock(this) {Column = startColumn, Type = type});
2016-02-16 22:42:40 +09:00
return MatchLineResult.Continue;
}
}
}
}