diff --git a/src/Testamina.Markdig.Benchmarks/Program.cs b/src/Testamina.Markdig.Benchmarks/Program.cs index 67c4022b..2f214b37 100644 --- a/src/Testamina.Markdig.Benchmarks/Program.cs +++ b/src/Testamina.Markdig.Benchmarks/Program.cs @@ -40,7 +40,7 @@ namespace Testamina.Markdig.Benchmarks //CommonMark.CommonMarkConverter.Convert(new StringReader(text), new StringWriter()); //var reader = new StreamReader(File.Open("spec.md", FileMode.Open)); var reader = new StringReader(text); - CommonMark.CommonMarkConverter.Parse(reader); + CommonMark.CommonMarkConverter.ProcessStage1(reader); reader.Dispose(); } @@ -50,8 +50,8 @@ namespace Testamina.Markdig.Benchmarks var program = new Program(); for (int i = 0; i < 200; i++) { - program.TestMarkdig(); - //program.TestCommonMark(); + //program.TestMarkdig(); + program.TestCommonMark(); } Console.WriteLine($"time: {clock.ElapsedMilliseconds}ms"); DumpGC(); diff --git a/src/Textamina.Markdig.Tests/TestHtmlHelper.cs b/src/Textamina.Markdig.Tests/TestHtmlHelper.cs index 9afd812f..64fe50f9 100644 --- a/src/Textamina.Markdig.Tests/TestHtmlHelper.cs +++ b/src/Textamina.Markdig.Tests/TestHtmlHelper.cs @@ -14,7 +14,7 @@ namespace Textamina.Markdig.Tests public void TestParseHtmlTagSimple() { var inputTag = ""; - var text = new StringLineGroup(inputTag); + var text = new StringSlice(inputTag); string outputTag; Assert.True(HtmlHelper.TryParseHtmlTag(text, out outputTag)); Assert.AreEqual(inputTag, outputTag); @@ -24,7 +24,7 @@ namespace Textamina.Markdig.Tests public void TestParseHtmlTagSimpleWithAttribute() { var inputTag = ""; - var text = new StringLineGroup(inputTag); + var text = new StringSlice(inputTag); string outputTag; Assert.True(HtmlHelper.TryParseHtmlTag(text, out outputTag)); Assert.AreEqual(inputTag, outputTag); diff --git a/src/Textamina.Markdig.Tests/TestLinkHelper.cs b/src/Textamina.Markdig.Tests/TestLinkHelper.cs index 59008a3f..829db282 100644 --- a/src/Textamina.Markdig.Tests/TestLinkHelper.cs +++ b/src/Textamina.Markdig.Tests/TestLinkHelper.cs @@ -10,9 +10,9 @@ namespace Textamina.Markdig.Tests [Test] public void TestUrlSimple() { - var text = new StringLineGroup("toto tutu"); + var text = new StringSlice("toto tutu"); string link; - Assert.True(LinkHelper.TryParseUrl(text, out link)); + Assert.True(LinkHelper.TryParseUrl(ref text, out link)); Assert.AreEqual("toto", link); Assert.AreEqual(' ', text.CurrentChar); } @@ -20,9 +20,9 @@ namespace Textamina.Markdig.Tests [Test] public void TestUrlUrl() { - var text = new StringLineGroup("http://google.com)"); + var text = new StringSlice("http://google.com)"); string link; - Assert.True(LinkHelper.TryParseUrl(text, out link)); + Assert.True(LinkHelper.TryParseUrl(ref text, out link)); Assert.AreEqual("http://google.com", link); Assert.AreEqual(')', text.CurrentChar); } @@ -30,9 +30,9 @@ namespace Textamina.Markdig.Tests [Test] public void TestUrlNestedParenthesis() { - var text = new StringLineGroup("(toto)tutu(tata) nooo"); + var text = new StringSlice("(toto)tutu(tata) nooo"); string link; - Assert.True(LinkHelper.TryParseUrl(text, out link)); + Assert.True(LinkHelper.TryParseUrl(ref text, out link)); Assert.AreEqual("(toto)tutu(tata)", link); Assert.AreEqual(' ', text.CurrentChar); } @@ -40,9 +40,9 @@ namespace Textamina.Markdig.Tests [Test] public void TestUrlAlternate() { - var text = new StringLineGroup(" nooo"); + var text = new StringSlice(" nooo"); string link; - Assert.True(LinkHelper.TryParseUrl(text, out link)); + Assert.True(LinkHelper.TryParseUrl(ref text, out link)); Assert.AreEqual("toto_tata_tutu", link); Assert.AreEqual(' ', text.CurrentChar); } @@ -50,26 +50,26 @@ namespace Textamina.Markdig.Tests [Test] public void TestUrlAlternateInvalid() { - var text = new StringLineGroup(")A"); + var text = new StringSlice(@"(<>)A"); string link; string title; - Assert.True(LinkHelper.TryParseInlineLink(text, out link, out title)); + Assert.True(LinkHelper.TryParseInlineLink(ref text, out link, out title)); Assert.AreEqual(string.Empty, link); Assert.AreEqual(string.Empty, title); Assert.AreEqual('A', text.CurrentChar); @@ -101,10 +101,10 @@ namespace Textamina.Markdig.Tests [Test] public void TestUrlAndTitleEmpty2() { - var text = new StringLineGroup(@"( <> )A"); + var text = new StringSlice(@"( <> )A"); string link; string title; - Assert.True(LinkHelper.TryParseInlineLink(text, out link, out title)); + Assert.True(LinkHelper.TryParseInlineLink(ref text, out link, out title)); Assert.AreEqual(string.Empty, link); Assert.AreEqual(string.Empty, title); Assert.AreEqual('A', text.CurrentChar); @@ -114,10 +114,10 @@ namespace Textamina.Markdig.Tests [Test] public void TestUrlEmptyWithTitleWithMultipleSpaces() { - var text = new StringLineGroup(@"( <> 'toto' )A"); + var text = new StringSlice(@"( <> 'toto' )A"); string link; string title; - Assert.True(LinkHelper.TryParseInlineLink(text, out link, out title)); + Assert.True(LinkHelper.TryParseInlineLink(ref text, out link, out title)); Assert.AreEqual(string.Empty, link); Assert.AreEqual("toto", title); Assert.AreEqual('A', text.CurrentChar); @@ -126,10 +126,10 @@ namespace Textamina.Markdig.Tests [Test] public void TestUrlEmpty() { - var text = new StringLineGroup(@"()A"); + var text = new StringSlice(@"()A"); string link; string title; - Assert.True(LinkHelper.TryParseInlineLink(text, out link, out title)); + Assert.True(LinkHelper.TryParseInlineLink(ref text, out link, out title)); Assert.AreEqual(string.Empty, link); Assert.AreEqual(string.Empty, title); Assert.AreEqual('A', text.CurrentChar); @@ -138,45 +138,41 @@ namespace Textamina.Markdig.Tests [Test] public void TestMultipleLines() { - var text = new StringLineGroup() - { - new StringLine("("), - new StringLine(" "), - new StringLine(" 'toto' )A") - }; + var text = new StringSlice(@"( + + 'toto' )A"); string link; string title; - Assert.True(LinkHelper.TryParseInlineLink(text, out link, out title)); + Assert.True(LinkHelper.TryParseInlineLink(ref text, out link, out title)); Assert.AreEqual("http://google.com", link); Assert.AreEqual("toto", title); Assert.AreEqual('A', text.CurrentChar); } - [Test] public void TestLabelSimple() { - var text = new StringLineGroup("[foo]"); + var text = new StringSlice("[foo]"); string label; - Assert.True(LinkHelper.TryParseLabel(text, out label)); + Assert.True(LinkHelper.TryParseLabel(ref text, out label)); Assert.AreEqual("foo", label); } [Test] public void TestLabelEscape() { - var text = new StringLineGroup(@"[fo\[\]o]"); + var text = new StringSlice(@"[fo\[\]o]"); string label; - Assert.True(LinkHelper.TryParseLabel(text, out label)); + Assert.True(LinkHelper.TryParseLabel(ref text, out label)); Assert.AreEqual(@"fo[]o", label); } [Test] public void TestLabelEscape2() { - var text = new StringLineGroup(@"[\]]"); + var text = new StringSlice(@"[\]]"); string label; - Assert.True(LinkHelper.TryParseLabel(text, out label)); + Assert.True(LinkHelper.TryParseLabel(ref text, out label)); Assert.AreEqual(@"]", label); } @@ -184,31 +180,31 @@ namespace Textamina.Markdig.Tests public void TestLabelInvalids() { string label; - Assert.False(LinkHelper.TryParseLabel(new StringLineGroup(@"a"), out label)); - Assert.False(LinkHelper.TryParseLabel(new StringLineGroup(@"["), out label)); - Assert.False(LinkHelper.TryParseLabel(new StringLineGroup(@"[\x]"), out label)); - Assert.False(LinkHelper.TryParseLabel(new StringLineGroup(@"[[]"), out label)); - Assert.False(LinkHelper.TryParseLabel(new StringLineGroup(@"[ ]"), out label)); - Assert.False(LinkHelper.TryParseLabel(new StringLineGroup(@"[ \t \n ]"), out label)); + Assert.False(LinkHelper.TryParseLabel(new StringSlice(@"a"), out label)); + Assert.False(LinkHelper.TryParseLabel(new StringSlice(@"["), out label)); + Assert.False(LinkHelper.TryParseLabel(new StringSlice(@"[\x]"), out label)); + Assert.False(LinkHelper.TryParseLabel(new StringSlice(@"[[]"), out label)); + Assert.False(LinkHelper.TryParseLabel(new StringSlice(@"[ ]"), out label)); + Assert.False(LinkHelper.TryParseLabel(new StringSlice(@"[ \t \n ]"), out label)); } [Test] public void TestLabelWhitespaceCollapsedAndTrim() { - var text = new StringLineGroup(@"[ fo o z ]"); + var text = new StringSlice(@"[ fo o z ]"); string label; - Assert.True(LinkHelper.TryParseLabel(text, out label)); + Assert.True(LinkHelper.TryParseLabel(ref text, out label)); Assert.AreEqual(@"fo o z", label); } [Test] public void TestlLinkReferenceDefinitionSimple() { - var text = new StringLineGroup(@"[foo]: /toto 'title'"); + var text = new StringSlice(@"[foo]: /toto 'title'"); string label; string url; string title; - Assert.True(LinkHelper.TryParseLinkReferenceDefinition(text, out label, out url, out title)); + Assert.True(LinkHelper.TryParseLinkReferenceDefinition(ref text, out label, out url, out title)); Assert.AreEqual(@"foo", label); Assert.AreEqual(@"/toto", url); Assert.AreEqual(@"title", title); @@ -217,10 +213,10 @@ namespace Textamina.Markdig.Tests [Test] public void TestAutoLinkUrlSimple() { - var text = new StringLineGroup(@""); + var text = new StringSlice(@""); string url; bool isEmail; - Assert.True(LinkHelper.TryParseAutolink(text, out url, out isEmail)); + Assert.True(LinkHelper.TryParseAutolink(ref text, out url, out isEmail)); Assert.False(isEmail); Assert.AreEqual("http://google.com", url); } @@ -228,10 +224,10 @@ namespace Textamina.Markdig.Tests [Test] public void TestAutoLinkEmailSimple() { - var text = new StringLineGroup(@""); + var text = new StringSlice(@""); string email; bool isEmail; - Assert.True(LinkHelper.TryParseAutolink(text, out email, out isEmail)); + Assert.True(LinkHelper.TryParseAutolink(ref text, out email, out isEmail)); Assert.True(isEmail); Assert.AreEqual("user@host.com", email); } @@ -241,10 +237,10 @@ namespace Textamina.Markdig.Tests { string text; bool isEmail; - Assert.False(LinkHelper.TryParseAutolink(new StringLineGroup(@""), out text, out isEmail)); - Assert.False(LinkHelper.TryParseAutolink(new StringLineGroup(@"<"), out text, out isEmail)); - Assert.False(LinkHelper.TryParseAutolink(new StringLineGroup(@""), out text, out isEmail)); + Assert.False(LinkHelper.TryParseAutolink(new StringSlice(@""), out text, out isEmail)); + Assert.False(LinkHelper.TryParseAutolink(new StringSlice(@"<"), out text, out isEmail)); + Assert.False(LinkHelper.TryParseAutolink(new StringSlice(@""), out text, out isEmail)); } } } \ No newline at end of file diff --git a/src/Textamina.Markdig.Tests/TestParser.cs b/src/Textamina.Markdig.Tests/TestParser.cs index 0f05d94e..7380cee8 100644 --- a/src/Textamina.Markdig.Tests/TestParser.cs +++ b/src/Textamina.Markdig.Tests/TestParser.cs @@ -24,7 +24,7 @@ namespace Textamina.Markdig.Tests [Test] public void TestSimple() { - var reader = new StringReader(@"####### foo"); + var reader = new StringReader(@"foo"); // var reader = new StringReader(@"> > toto tata //> titi toto //"); diff --git a/src/Textamina.Markdig.Tests/TestStringLine.cs b/src/Textamina.Markdig.Tests/TestStringLine.cs index c3f96452..4331da5a 100644 --- a/src/Textamina.Markdig.Tests/TestStringLine.cs +++ b/src/Textamina.Markdig.Tests/TestStringLine.cs @@ -8,24 +8,25 @@ namespace Textamina.Markdig.Tests [TestFixture] public class TestStringLine { - // TODO: Add tests for StringLine + /* + // TODO: Add tests for StringSlice // TODO: Add more tests for StringLineGroup [Test] public void TestStringLineGroupSimple() { - var text = new StringLineGroup() + var text = new StringSliceList() { - new StringLine("ABC"), - new StringLine("E"), - new StringLine("F") + new StringSlice("ABC"), + new StringSlice("E"), + new StringSlice("F") }; var chars = ToString(text); TextAssert.AreEqual("ABC\nE\nF", chars.ToString()); } - private static string ToString(StringLineGroup text) + private static string ToString(StringSliceList text) { var chars = new StringBuilder(); while (text.CurrentChar != '\0') @@ -39,10 +40,10 @@ namespace Textamina.Markdig.Tests [Test] public void TestStringLineGroupSaveAndRestore() { - var text = new StringLineGroup() + var text = new StringSliceList() { - new StringLine("ABCD"), - new StringLine("EF"), + new StringSlice("ABCD"), + new StringSlice("EF"), }; text.NextChar(); // B @@ -66,7 +67,7 @@ namespace Textamina.Markdig.Tests [Test] public void TestSkipWhitespaces() { - var text = new StringLineGroup(" ABC"); + var text = new StringSliceList(" ABC"); Assert.True(text.SkipWhiteSpaces()); Assert.False(text.SkipWhiteSpaces()); Assert.AreEqual('A', text.CurrentChar); @@ -75,14 +76,14 @@ namespace Textamina.Markdig.Tests [Test] public void TestStringLineGroupWithModifiedStart() { - var line1 = new StringLine(" ABC"); + var line1 = new StringSlice(" ABC"); line1.NextChar(); line1.NextChar(); - var line2 = new StringLine(" DEF "); + var line2 = new StringSlice(" DEF "); line2.Trim(); - var text = new StringLineGroup() {line1, line2}; + var text = new StringSliceList() {line1, line2}; var result = ToString(text); TextAssert.AreEqual("ABC\nDEF", result); @@ -92,19 +93,19 @@ namespace Textamina.Markdig.Tests [Test] public void TestStringLineGroupWithTrim() { - var line1 = new StringLine(" ABC "); + var line1 = new StringSlice(" ABC "); line1.NextChar(); line1.NextChar(); - var line2 = new StringLine(" DEF "); + var line2 = new StringSlice(" DEF "); - var text = new StringLineGroup() { line1, line2 }; + var text = new StringSliceList() { line1, line2 }; text.Trim(); var result = ToString(text); TextAssert.AreEqual("ABC \n DEF", result); } - + */ } } \ No newline at end of file diff --git a/src/Textamina.Markdig/Formatters/HtmlFormatter.cs b/src/Textamina.Markdig/Formatters/HtmlFormatter.cs index 64b98a73..88d7c916 100644 --- a/src/Textamina.Markdig/Formatters/HtmlFormatter.cs +++ b/src/Textamina.Markdig/Formatters/HtmlFormatter.cs @@ -26,7 +26,7 @@ namespace Textamina.Markdig.Formatters [typeof(FencedCodeBlock)] = o => Write((FencedCodeBlock)o), [typeof(CodeBlock)] = o => Write((CodeBlock)o), [typeof(HeadingBlock)] = o => Write((HeadingBlock)o), - [typeof(BreakBlock)] = o => Write((BreakBlock)o), + [typeof(ThematicBreakBlock)] = o => Write((ThematicBreakBlock)o), [typeof(QuoteBlock)] = o => Write((QuoteBlock)o), [typeof(ParagraphBlock)] = o => Write((ParagraphBlock)o), [typeof(HtmlBlock)] = o => Write((HtmlBlock)o), @@ -135,7 +135,7 @@ namespace Textamina.Markdig.Formatters writer.WriteLineConstant(">"); } - protected void Write(BreakBlock breakBlock) + protected void Write(ThematicBreakBlock breakBlock) { writer.WriteLineConstant("
"); } @@ -282,7 +282,7 @@ namespace Textamina.Markdig.Formatters { writer.WriteLine(); } - var line = lines[i]; + var line = lines.Slices[i]; if (escape) { HtmlHelper.EscapeHtml(line.ToString(), writer); diff --git a/src/Textamina.Markdig/Helpers/HtmlHelper.cs b/src/Textamina.Markdig/Helpers/HtmlHelper.cs index e9cf58f9..84d079e9 100644 --- a/src/Textamina.Markdig/Helpers/HtmlHelper.cs +++ b/src/Textamina.Markdig/Helpers/HtmlHelper.cs @@ -55,16 +55,22 @@ namespace Textamina.Markdig.Helpers "XMLRPC.BEEP", "XMLRPC.BEEPS", "XMPP", "XRI", "YMSGR", "Z39.50R", "Z39.50S" }; - public static bool TryParseHtmlTag(StringLineGroup text, out string htmlTag) + + public static bool TryParseHtmlTag(StringSlice text, out string htmlTag) + { + return TryParseHtmlTag(ref text, out htmlTag); + } + + public static bool TryParseHtmlTag(ref StringSlice text, out string htmlTag) { var builder = StringBuilderCache.Local(); - var result = TryParseHtmlTag(text, builder); + var result = TryParseHtmlTag(ref text, builder); htmlTag = builder.ToString(); builder.Clear(); return result; } - public static bool TryParseHtmlTag(StringLineGroup text, StringBuilder builder) + public static bool TryParseHtmlTag(ref StringSlice text, StringBuilder builder) { if (builder == null) throw new ArgumentNullException(nameof(builder)); var c = text.CurrentChar; @@ -101,7 +107,7 @@ namespace Textamina.Markdig.Helpers return TryParseHtmlTagOpenTag(text, builder); } - internal static bool TryParseHtmlTagOpenTag(StringLineGroup text, StringBuilder builder) + internal static bool TryParseHtmlTagOpenTag(StringSlice text, StringBuilder builder) { var c = text.CurrentChar; @@ -256,7 +262,7 @@ namespace Textamina.Markdig.Helpers } } - private static bool TryParseHtmlTagDeclaration(StringLineGroup text, StringBuilder builder) + private static bool TryParseHtmlTagDeclaration(StringSlice text, StringBuilder builder) { var c = text.CurrentChar; bool hasAlpha = false; @@ -291,7 +297,7 @@ namespace Textamina.Markdig.Helpers } } - private static bool TryParseHtmlTagCData(StringLineGroup text, StringBuilder builder) + private static bool TryParseHtmlTagCData(StringSlice text, StringBuilder builder) { builder.Append('['); var c = text.NextChar(); @@ -334,7 +340,7 @@ namespace Textamina.Markdig.Helpers return false; } - internal static bool TryParseHtmlCloseTag(StringLineGroup text, StringBuilder builder) + internal static bool TryParseHtmlCloseTag(StringSlice text, StringBuilder builder) { // builder.Append('/'); @@ -379,7 +385,7 @@ namespace Textamina.Markdig.Helpers } - private static bool TryParseHtmlTagHtmlComment(StringLineGroup text, StringBuilder builder) + private static bool TryParseHtmlTagHtmlComment(StringSlice text, StringBuilder builder) { var c = text.NextChar(); if (c != '-') @@ -388,7 +394,7 @@ namespace Textamina.Markdig.Helpers } builder.Append('-'); builder.Append('-'); - if (text.PeekCharOnSameLine() == '>') + if (text.PeekChar(1) == '>') { return false; } @@ -417,7 +423,7 @@ namespace Textamina.Markdig.Helpers } } - private static bool TryParseHtmlTagProcessingInstruction(StringLineGroup text, StringBuilder builder) + private static bool TryParseHtmlTagProcessingInstruction(StringSlice text, StringBuilder builder) { builder.Append('?'); var prevChar = '\0'; diff --git a/src/Textamina.Markdig/Helpers/LinkHelper.cs b/src/Textamina.Markdig/Helpers/LinkHelper.cs index a5017bb0..c879b772 100644 --- a/src/Textamina.Markdig/Helpers/LinkHelper.cs +++ b/src/Textamina.Markdig/Helpers/LinkHelper.cs @@ -8,7 +8,12 @@ namespace Textamina.Markdig.Helpers { public static class LinkHelper { - public static bool TryParseAutolink(StringLineGroup text, out string link, out bool isEmail) + public static bool TryParseAutolink(StringSlice text, out string link, out bool isEmail) + { + return TryParseAutolink(ref text, out link, out isEmail); + } + + public static bool TryParseAutolink(ref StringSlice text, out string link, out bool isEmail) { link = null; isEmail = false; @@ -205,10 +210,13 @@ namespace Textamina.Markdig.Helpers return false; } - public static bool TryParseInlineLink(StringLineGroup text, out string link, out string title) + public static bool TryParseInlineLink(StringSlice text, out string link, out string title) { - if (text == null) throw new ArgumentNullException(nameof(text)); + return TryParseInlineLink(ref text, out link, out title); + } + public static bool TryParseInlineLink(ref StringSlice text, out string link, out string title) + { // 1. An inline link consists of a link text followed immediately by a left parenthesis (, // 2. optional whitespace, TODO: specs: is it whitespace or multiple whitespaces? // 3. an optional link destination, @@ -224,11 +232,11 @@ namespace Textamina.Markdig.Helpers if (c == '(') { text.NextChar(); - text.SkipWhiteSpaces(); + text.TrimStart(); - if (TryParseUrl(text, out link)) + if (TryParseUrl(ref text, out link)) { - var hasWhiteSpaces = text.SkipWhiteSpaces(); + var hasWhiteSpaces = text.TrimStart(); c = text.CurrentChar; if (c == ')') @@ -242,9 +250,9 @@ namespace Textamina.Markdig.Helpers { isValid = true; } - else if (TryParseTitle(text, out title)) + else if (TryParseTitle(ref text, out title)) { - text.SkipWhiteSpaces(); + text.TrimStart(); c = text.CurrentChar; if (c == ')') @@ -266,10 +274,13 @@ namespace Textamina.Markdig.Helpers return isValid; } - public static bool TryParseTitle(StringLineGroup text, out string title) + public static bool TryParseTitle(StringSlice text, out string title) { - if (text == null) throw new ArgumentNullException(nameof(text)); + return TryParseTitle(ref text, out title); + } + public static bool TryParseTitle(ref StringSlice text, out string title) + { bool isValid = false; var buffer = StringBuilderCache.Local(); buffer.Clear(); @@ -286,7 +297,7 @@ namespace Textamina.Markdig.Helpers { c = text.NextChar(); - if (text.Current != null && text.Current.IsBlankLine()) + if (c == '\n') // TODO? { break; } @@ -333,10 +344,13 @@ namespace Textamina.Markdig.Helpers return isValid; } - public static bool TryParseUrl(StringLineGroup text, out string link) + public static bool TryParseUrl(StringSlice text, out string link) { - if (text == null) throw new ArgumentNullException(nameof(text)); + return TryParseUrl(ref text, out link); + } + public static bool TryParseUrl(ref StringSlice text, out string link) + { bool isValid = false; var buffer = StringBuilderCache.Local(); buffer.Clear(); @@ -453,12 +467,18 @@ namespace Textamina.Markdig.Helpers return isValid; } - public static bool TryParseLinkReferenceDefinition(StringLineGroup text, out string label, out string url, + public static bool TryParseLinkReferenceDefinition(StringSlice text, out string label, out string url, + out string title) + { + return TryParseLinkReferenceDefinition(ref text, out label, out url, out title); + } + + public static bool TryParseLinkReferenceDefinition(ref StringSlice text, out string label, out string url, out string title) { url = null; title = null; - if (!TryParseLabel(text, out label)) + if (!TryParseLabel(ref text, out label)) { return false; } @@ -471,19 +491,19 @@ namespace Textamina.Markdig.Helpers text.NextChar(); // Skip ':' // Skip any whitespaces before the url - text.SkipWhiteSpaces(); - if (!TryParseUrl(text, out url) || string.IsNullOrEmpty(url)) + text.TrimStart(); + if (!TryParseUrl(ref text, out url) || string.IsNullOrEmpty(url)) { return false; } - var saved = text.Save(); + var saved = text; int newLineCount; - var hasWhiteSpaces = text.SkipWhiteSpaces(out newLineCount); + var hasWhiteSpaces = text.TrimStart(out newLineCount); var c = text.CurrentChar; if (c == '\'' || c == '"' || c == '(') { - if (TryParseTitle(text, out title)) + if (TryParseTitle(ref text, out title)) { // If we have a title, it requires a whitespace after the url if (!hasWhiteSpaces) @@ -517,7 +537,7 @@ namespace Textamina.Markdig.Helpers // we are still returning a valid definition if (newLineCount > 0 && title != null) { - text.Restore(ref saved); + text = saved; title = null; return true; } @@ -531,13 +551,17 @@ namespace Textamina.Markdig.Helpers return true; } - public static bool TryParseLabel(StringLineGroup lines, out string label) + public static bool TryParseLabel(StringSlice lines, out string label) { - return TryParseLabel(lines, false, out label); + return TryParseLabel(ref lines, false, out label); } + public static bool TryParseLabel(ref StringSlice lines, out string label) + { + return TryParseLabel(ref lines, false, out label); + } - public static bool TryParseLabel(StringLineGroup lines, bool allowEmpty, out string label) + public static bool TryParseLabel(ref StringSlice lines, bool allowEmpty, out string label) { label = null; char c = lines.CurrentChar; diff --git a/src/Textamina.Markdig/Parsing/BlockParserState.cs b/src/Textamina.Markdig/Parsing/BlockParserState.cs index 65df64c4..ebf445f2 100644 --- a/src/Textamina.Markdig/Parsing/BlockParserState.cs +++ b/src/Textamina.Markdig/Parsing/BlockParserState.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Runtime.CompilerServices; using Textamina.Markdig.Helpers; using Textamina.Markdig.Syntax; @@ -20,7 +21,32 @@ namespace Textamina.Markdig.Parsing Add(root); } - public StringLine Line; + + public StringSlice Line; + + public int LineIndex; + + public bool IsBlankLine => CurrentChar == '\0'; + + public bool IsEndOfLine => Line.IsEndOfSlice; + + public char CurrentChar => Line.CurrentChar; + + public char NextChar() => Line.NextChar(); + + public char CharAt(int index) => Line[index]; + + public int Start => Line.Start; + + public int EndOffset => Line.End; + + public int Indent => Column - ColumnBegin; + + public bool IsCodeIndent => Indent >= 4; + + public int ColumnBegin { get; private set; } + + public int Column { get; set; } public Block Pending { get; set; } @@ -36,6 +62,40 @@ namespace Textamina.Markdig.Parsing public StringBuilderCache StringBuilders { get; } + public char PeekChar(int offset) + { + return Line.PeekChar(offset); + } + + [MethodImpl(MethodImplOptionPortable.AggressiveInlining)] + public void SetCurrentLine(ref StringSlice line) + { + Line = line; + EatSpaces(); + } + + public void EatSpaces() + { + var c = CurrentChar; + ColumnBegin = Column; + while (c !='\0') + { + if (c == ' ') + { + Column++; + } + else if (c == '\t') + { + Column = ((Column + 3) >> 2) << 2; + } + else + { + break; + } + c = NextChar(); + } + } + public Block NextPending { get { return PendingIndex + 1 < Count ? this[PendingIndex + 1] : null; } @@ -112,11 +172,5 @@ namespace Textamina.Markdig.Parsing Close(i); } } - - internal void Reset(StringLine line) - { - Line = line; - Pending = null; - } } } \ No newline at end of file diff --git a/src/Textamina.Markdig/Parsing/InlineParserState.cs b/src/Textamina.Markdig/Parsing/InlineParserState.cs index 7b2d71c3..d04d382e 100644 --- a/src/Textamina.Markdig/Parsing/InlineParserState.cs +++ b/src/Textamina.Markdig/Parsing/InlineParserState.cs @@ -16,7 +16,7 @@ namespace Textamina.Markdig.Parsing public LeafBlock Block { get; internal set; } - public StringLineGroup Lines; + public StringSlice Text; public Inline Inline { get; set; } diff --git a/src/Textamina.Markdig/Parsing/MarkdownParser.cs b/src/Textamina.Markdig/Parsing/MarkdownParser.cs index 6df13b23..98737213 100644 --- a/src/Textamina.Markdig/Parsing/MarkdownParser.cs +++ b/src/Textamina.Markdig/Parsing/MarkdownParser.cs @@ -2,9 +2,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.IO; -using System.Text; using System.Threading.Tasks; -using System.Xml.Serialization; using Textamina.Markdig.Helpers; using Textamina.Markdig.Syntax; @@ -13,9 +11,6 @@ namespace Textamina.Markdig.Parsing public class MarkdownParser { public static TextWriter Log; - - private StringLine line; - private readonly List blockParsers; private readonly List inlineParsers; private readonly List regularInlineParsers; @@ -36,7 +31,7 @@ namespace Textamina.Markdig.Parsing blockParserState = new BlockParserState(stringBuilderCache, document); blockParsers = new List() { - BreakBlock.Parser, + ThematicBreakBlock.Parser, HeadingBlock.Parser, QuoteBlock.Parser, ListBlock.Parser, @@ -112,13 +107,12 @@ namespace Textamina.Markdig.Parsing public Document Parse() { ParseLines(); - ProcessInlines(document); + //ProcessInlines(document); return document; } private void ParseLines() { - int lineIndex = 0; while (true) { var lineText = Reader.ReadLine(); @@ -128,13 +122,15 @@ namespace Textamina.Markdig.Parsing { break; } - line = new StringLine(lineText, lineIndex++); + var line = new StringSlice(lineText); + blockParserState.SetCurrentLine(ref line); + blockParserState.LineIndex++; bool continueProcessLiner = ProcessPendingBlocks(); // If we have already reached eol and the last block was a paragraph // we close it - if (line.IsEol) + if (blockParserState.Line.IsEndOfSlice) { int index = blockParserState.Count - 1; if (blockParserState[index] is ParagraphBlock) @@ -203,7 +199,7 @@ namespace Textamina.Markdig.Parsing } // Create the line state that will be used by all parser - blockParserState.Reset(line); + blockParserState.Pending = null; // Process any current block potentially opened for (int i = 1; i < blockParserState.Count; i++) @@ -220,7 +216,7 @@ namespace Textamina.Markdig.Parsing break; } - var saveLiner = line.Save(); + var saveLiner = blockParserState.Line; // If we have a discard, we can remove it from the current state blockParserState.CurrentContainer = LastContainer; @@ -235,7 +231,7 @@ namespace Textamina.Markdig.Parsing if (result == MatchLineResult.None) { // Restore the Line where it was - line.Restore(ref saveLiner); + blockParserState.SetCurrentLine(ref saveLiner); break; } @@ -258,7 +254,7 @@ namespace Textamina.Markdig.Parsing processLiner = false; if (result != MatchLineResult.LastDiscard && result != MatchLineResult.ContinueDiscard) { - leaf.Append(line); + leaf.Lines.Append(ref blockParserState.Line); } if (blockParserState.NewBlocks.Count > 0) @@ -294,12 +290,12 @@ namespace Textamina.Markdig.Parsing private void ParseNewBlocks(ref bool continueProcessLiner) { - blockParserState.Reset(line); + blockParserState.Pending = null; for (int j = 0; j < blockParsers.Count; j++) { var blockParser = blockParsers[j]; - if (line.IsEol) + if (blockParserState.Line.IsEndOfSlice) { continueProcessLiner = false; break; @@ -319,19 +315,19 @@ namespace Textamina.Markdig.Parsing blockParserState.CurrentContainer = LastContainer; blockParserState.LastBlock = lastBlock; - var saveLiner = line.Save(); + var saveLiner = blockParserState.Line; var result = blockParser.Match(blockParserState); if (result == MatchLineResult.None) { // If we have reached a blank line after trying to parse a paragraph // we can ignore it - if (isParsingParagraph && line.IsBlankLine()) + if (isParsingParagraph && blockParserState.IsBlankLine) { continueProcessLiner = false; break; } - line.Restore(ref saveLiner); + blockParserState.SetCurrentLine(ref saveLiner); continue; } @@ -342,7 +338,7 @@ namespace Textamina.Markdig.Parsing Debug.Assert(blockParserState.NewBlocks.Count == 0); continueProcessLiner = false; - paragraph.Append(line); + paragraph.Lines.Append(ref blockParserState.Line); // We have just found a lazy continuation for a paragraph, early exit // Mark all block opened after a lazy continuation @@ -383,7 +379,7 @@ namespace Textamina.Markdig.Parsing { var block = newBlocks.Pop(); - block.Line = line.LineIndex; + block.Line = blockParserState.LineIndex; // If we have a leaf block var leaf = block as LeafBlock; @@ -391,7 +387,7 @@ namespace Textamina.Markdig.Parsing { if (result != MatchLineResult.LastDiscard && result != MatchLineResult.ContinueDiscard) { - leaf.Append(line); + leaf.Lines.Append(ref blockParserState.Line); } if (newBlocks.Count > 0) @@ -433,28 +429,25 @@ namespace Textamina.Markdig.Parsing var lines = leafBlock.Lines; leafBlock.Inline = new ContainerInline() {IsClosed = false}; - var inlineState = new InlineParserState(stringBuilderCache, document); - - inlineState.Lines = lines; - inlineState.Inline = leafBlock.Inline; - inlineState.Block = leafBlock; - - var saveLines = new StringLineGroup.State(); - - while (!lines.IsEndOfLines) + var inlineState = new InlineParserState(stringBuilderCache, document) { - lines.Save(ref saveLines); + Text = new StringSlice(leafBlock.Lines.ToString()), + Inline = leafBlock.Inline, + Block = leafBlock + }; - var c = lines.CurrentChar; + while (!inlineState.Text.IsEndOfSlice) + { + var saveLine = inlineState.Text; + + var c = saveLine.CurrentChar; var inlineParser = c < 128 ? inlineWithFirstCharParsers[c] : null; if (inlineParser == null || !inlineParser.Match(inlineState)) { for (int i = 0; i < regularInlineParsers.Count; i++) { - lines.Restore(ref saveLines); - + inlineState.Text = saveLine; inlineParser = regularInlineParsers[i]; - if (inlineParser.Match(inlineState)) { break; @@ -465,7 +458,7 @@ namespace Textamina.Markdig.Parsing if (inlineParser == null) { - lines.Restore(ref saveLines); + inlineState.Text = saveLine; } } diff --git a/src/Textamina.Markdig/Syntax/CodeBlock.cs b/src/Textamina.Markdig/Syntax/CodeBlock.cs index 6fe403a4..3cac7a8b 100644 --- a/src/Textamina.Markdig/Syntax/CodeBlock.cs +++ b/src/Textamina.Markdig/Syntax/CodeBlock.cs @@ -1,5 +1,3 @@ -using System; -using Textamina.Markdig.Helpers; using Textamina.Markdig.Parsing; namespace Textamina.Markdig.Syntax @@ -28,56 +26,15 @@ namespace Textamina.Markdig.Syntax public override MatchLineResult Match(BlockParserState state) { - var liner = state.Line; - int position = liner.Start; - liner.SkipLeadingSpaces3(); - - // 4.4 Indented code blocks - var c = liner.Current; - var isTab = c.IsTab(); - var isSpace = c.IsSpace(); - var isBlankLine = liner.IsBlankLine(); - var codeBlock = state.Pending as CodeBlock; - // && !isBlankLine) || (isBlankLine && codeBlock != null && !codeBlock.Lines[codeBlock.Lines.Count - 1].IsBlankLine()) - if ((codeBlock != null && isBlankLine) || (isTab || (isSpace && (liner.Start - position) == 3))) - //if (((isTab || (isSpace && (liner.Start - position) == 3)) && !isBlankLine)) + if (!state.IsCodeIndent || state.IsBlankLine) { - liner.NextChar(); - if (state.Pending == null) - { - if (isBlankLine) - { - return MatchLineResult.None; - } - - state.NewBlocks.Push(new CodeBlock(this) { Column = position }); - } - return MatchLineResult.Continue; + return state.IsBlankLine && state.Pending != null ? MatchLineResult.LastDiscard : MatchLineResult.None; } - - return MatchLineResult.None; - } - - public override void Close(BlockParserState state) - { - var codeBlock = state.Pending as CodeBlock; - if (codeBlock != null) + if (state.Pending == null) { - var lines = codeBlock.Lines; - // Remove trailing blankline - for (int i = lines.Count - 1; i >= 0; i--) - { - if (lines[i].IsBlankLine()) - { - lines.RemoveAt(i); - } - else - { - break; - } - } + state.NewBlocks.Push(new CodeBlock(this) { Column = state.Column }); } - + return MatchLineResult.Continue; } } } diff --git a/src/Textamina.Markdig/Syntax/FencedCodeBlock.cs b/src/Textamina.Markdig/Syntax/FencedCodeBlock.cs index 3b59790d..1e7c3100 100644 --- a/src/Textamina.Markdig/Syntax/FencedCodeBlock.cs +++ b/src/Textamina.Markdig/Syntax/FencedCodeBlock.cs @@ -34,149 +34,148 @@ namespace Textamina.Markdig.Syntax { public override MatchLineResult Match(BlockParserState state) { - var liner = state.Line; + int count; + char matchChar; + char c = state.CurrentChar; + int offset = 0; - var fenced = state.Pending as FencedCodeBlock; - if (fenced != null) + var currentFenced = state.Pending as FencedCodeBlock; + if (currentFenced != null) { - var saveLiner = liner.Save(); - liner.SkipLeadingSpaces3(); - - var c = liner.Current; - int count = fenced.fencedCharCount; - var matchChar = fenced.fencedChar; - while (!liner.IsEol) + count = currentFenced.fencedCharCount; + matchChar = currentFenced.fencedChar; + while (c == matchChar) { - if (c != matchChar) - { - break; - } - c = liner.NextChar(); - count--; + offset++; + c = state.Line.PeekChar(offset); } - if (count <= 0) + if (offset >= count) { - int endPosition = liner.Start; - liner.TrimEnd(true); - if (liner.IsEol || liner.End == endPosition) + state.Line.TrimEnd(true); + if (state.CurrentChar == matchChar) { return MatchLineResult.LastDiscard; } } - liner.Restore(ref saveLiner); - // TODO: It is unclear how to handle this correctly // Break only if Eof return MatchLineResult.Continue; } - else - { - // TODO: We need to count the number of leading space to remove them on each line - var start = liner.Start; - liner.SkipLeadingSpaces3(); - var column = liner.Start; - var indentCount = liner.Start - start; - var c = liner.Current; - int count = 0; - var matchChar = (char)0; - while (!liner.IsEol) + // Else if the we have an indent, it is not valid + if (state.IsCodeIndent) + { + return MatchLineResult.None; + } + + count = 0; + matchChar = (char) 0; + while (c != '\0') + { + if (count == 0 && (c == '`' || c == '~')) { - if (count == 0 && (c == '`' || c == '~')) - { - matchChar = c; - } - else if (c != matchChar) - { - break; - } - c = liner.NextChar(); - count++; + matchChar = c; + } + else if (c != matchChar) + { + break; + } + count++; + c = state.PeekChar(count); + } + + if (count >= 3) + { + return MatchLineResult.None; + } + + // TODO: We need to count the number of leading space to remove them on each line + var column = state.Column; + + // specs spaces: Is space and tabs? or only spaces? Use space and tab for this case + while (c.IsSpaceOrTab()) + { + offset++; + c = state.PeekChar(offset); + } + var start = state.Start + count + offset; + + string infoString; + string argString = null; + + // An info string cannot contain any backsticks + int firstSpace = -1; + for (int i = start; i <= state.EndOffset; i++) + { + c = state.Line[i]; + if (c == '`') + { + return MatchLineResult.None; } - if (count >= 3) + if (firstSpace < 0 && c.IsSpaceOrTab()) { - // specs spaces: Is space and tabs? or only spaces? Use space and tab for this case - liner.TrimStart(true); - liner.TrimEnd(true); + firstSpace = i; + } + } - string infoString = null; - string argString = null; + if (firstSpace > 0) + { + infoString = state.Line.Text.Substring(start, firstSpace - start); - // An info string cannot contain any backsticks - int firstSpace = -1; - for (int i = liner.Start; i <= liner.End; i++) + // Skip any spaces after info string + firstSpace++; + while (true) + { + c = state.Line[firstSpace]; + if (c.IsSpaceOrTab()) { - c = liner[i]; - if (c == '`') - { - return MatchLineResult.None; - } - else if (firstSpace < 0 && c.IsSpaceOrTab()) - { - firstSpace = i; - } - } - - if (firstSpace > 0) - { - infoString = liner.Text.Substring(liner.Start, firstSpace - liner.Start); - - // Skip any spaces after info string firstSpace++; - while (true) - { - c = liner[firstSpace]; - if (c.IsSpaceOrTab()) - { - firstSpace++; - } - else - { - break; - } - } - - argString = liner.Text.Substring(firstSpace, liner.End - firstSpace + 1); } else { - infoString = liner.ToString(); + break; } - - // Store the number of matched string into the context - state.NewBlocks.Push(new FencedCodeBlock(this) - { - Column = column, - fencedChar = matchChar, - fencedCharCount = count, - indentCount = indentCount, - Language = HtmlHelper.Unescape(infoString), - Arguments = HtmlHelper.Unescape(argString), - }); - return MatchLineResult.ContinueDiscard; } - return MatchLineResult.None; + argString = state.Line.Text.Substring(firstSpace, state.Line.End - firstSpace + 1); } + else + { + infoString = state.Line.Text.Substring(start, state.EndOffset - start + 1); + } + + // Store the number of matched string into the context + state.NewBlocks.Push(new FencedCodeBlock(this) + { + Column = column, + fencedChar = matchChar, + fencedCharCount = count, + indentCount = state.Indent, + Language = HtmlHelper.Unescape(infoString), + Arguments = HtmlHelper.Unescape(argString), + }); + + // Discard the current line + return MatchLineResult.ContinueDiscard; } public override void Close(BlockParserState state) { - var fenced = (FencedCodeBlock) state.Pending; - for (int i = 0; i < fenced.Lines.Count; i++) + var fenced = ((FencedCodeBlock) state.Pending); + var lines = fenced.Lines; + for (int i = 0; i < lines.Count; i++) { - var line = fenced.Lines[i]; - // Fences can be indented. If the opening fence is indented, // content lines will have equivalent opening indentation removed, if present: for (int j = 0; j < fenced.indentCount; j++) { - if (line.Start < line.End && line[line.Start].IsSpace()) + var start = lines.Slices[i].Start; + if (start < lines.Slices[i].End && lines.Slices[i][start].IsSpace()) { - line.Start++; + lines.Slices[i].Start++; } else { diff --git a/src/Textamina.Markdig/Syntax/HeadingBlock.cs b/src/Textamina.Markdig/Syntax/HeadingBlock.cs index e8d60bea..f046d711 100644 --- a/src/Textamina.Markdig/Syntax/HeadingBlock.cs +++ b/src/Textamina.Markdig/Syntax/HeadingBlock.cs @@ -20,10 +20,10 @@ namespace Textamina.Markdig.Syntax { public override MatchLineResult Match(BlockParserState state) { - var liner = state.Line; - liner.SkipLeadingSpaces3(); - - var column = liner.Start; + if (state.IsCodeIndent) + { + return MatchLineResult.None; + } // 4.2 ATX headings // An ATX heading consists of a string of characters, parsed as inline content, @@ -35,37 +35,37 @@ namespace Textamina.Markdig.Syntax // the heading are stripped of leading and trailing spaces before being parsed as // inline content. The heading level is equal to the number of # characters in the // opening sequence. - var c = liner.Current; + var column = state.Column; + var c = state.CurrentChar; int leadingCount = 0; - for (; !liner.IsEol && leadingCount <= 6; leadingCount++) + for (; !state.Line.IsEndOfSlice && leadingCount <= 6; leadingCount++) { if (c != '#') { break; } - c = liner.NextChar(); + c = state.PeekChar(leadingCount); } // closing # will be handled later, because anyway we have matched // A space is required after leading # - if (leadingCount > 0 && leadingCount <=6 && (c.IsSpace() || liner.IsEol)) + if (leadingCount > 0 && leadingCount <=6 && (c.IsSpace() || state.Line.IsEndOfSlice)) { - liner.NextChar(); + state.Line.Start = leadingCount + 1; state.NewBlocks.Push(new HeadingBlock(this) {Level = leadingCount, Column = column }); - // The optional closing sequence of #s must be preceded by a space and may be followed by spaces only. int endState = 0; int countClosingTags = 0; - for (int i = liner.End; i >= liner.Start - 1; i--) // Go up to Start - 1 in order to match the space after the first ### + for (int i = state.Line.End; i >= state.Line.Start - 1; i--) // Go up to Start - 1 in order to match the space after the first ### { - c = liner[i]; + c = state.Line[i]; if (endState == 0) { - if (CharHelper.IsSpace(c)) // TODO: Not clear if it is a space or space+tab in the specs + if (c.IsSpace()) // TODO: Not clear if it is a space or space+tab in the specs { continue; } @@ -81,9 +81,9 @@ namespace Textamina.Markdig.Syntax if (countClosingTags > 0) { - if (CharHelper.IsSpace(c)) + if (c.IsSpace()) { - liner.End = i - 1; + state.Line.End = i - 1; } break; } diff --git a/src/Textamina.Markdig/Syntax/HtmlBlock.cs b/src/Textamina.Markdig/Syntax/HtmlBlock.cs index d68fceed..6becd456 100644 --- a/src/Textamina.Markdig/Syntax/HtmlBlock.cs +++ b/src/Textamina.Markdig/Syntax/HtmlBlock.cs @@ -16,10 +16,6 @@ namespace Textamina.Markdig.Syntax public HtmlBlockType Type { get; set; } - [ThreadStatic] - private static readonly StringLineGroup HtmlLineGroup = new StringLineGroup(); - - private class ParserInternal : BlockParser { private static readonly string[] HtmlTags = @@ -106,12 +102,11 @@ namespace Textamina.Markdig.Syntax private MatchLineResult MatchStart(BlockParserState state) { - var liner = state.Line; int index = 0; for (int i = 0; i < 3; i++) { - if (!liner.PeekChar(index).IsSpace()) + if (!state.Line.PeekChar(index).IsSpace()) { break; } @@ -120,36 +115,34 @@ namespace Textamina.Markdig.Syntax // Early exit if it is not starting by an HTML tag var column = index; - var c = liner.PeekChar(index++); + var c = state.Line.PeekChar(index++); if (c != '<') { return MatchLineResult.None; } - var result = TryParseTagType16(state, liner, index, column); + var result = TryParseTagType16(state, ref state.Line, index, column); // HTML blocks of type 7 cannot interrupt a paragraph: if (result == MatchLineResult.None && !(state.LastBlock is ParagraphBlock)) { - result = TryParseTagType7(state, liner, index, column); + result = TryParseTagType7(state, ref state.Line, index, column); } return result; } - private MatchLineResult TryParseTagType7(BlockParserState state, StringLine liner, int index, int startColumn) + private MatchLineResult TryParseTagType7(BlockParserState state, ref StringSlice liner, int index, int startColumn) { var builder = StringBuilderCache.Local(); - var text = HtmlLineGroup; - text.Add(liner); - text.SetColumn(index); - var c = text.CurrentChar; + liner.Start = index; + var c = liner.CurrentChar; var result = MatchLineResult.None; - if ((c == '/' && HtmlHelper.TryParseHtmlCloseTag(text, builder)) || HtmlHelper.TryParseHtmlTagOpenTag(text, builder)) + if ((c == '/' && HtmlHelper.TryParseHtmlCloseTag(liner, builder)) || HtmlHelper.TryParseHtmlTagOpenTag(liner, builder)) { // Must be followed by whitespace only bool hasOnlySpaces = true; - c = text.CurrentChar; + c = liner.CurrentChar; while (true) { if (c == '\0') @@ -161,7 +154,7 @@ namespace Textamina.Markdig.Syntax hasOnlySpaces = false; break; } - c = text.NextChar(); + c = liner.NextChar(); } if (hasOnlySpaces) @@ -170,12 +163,11 @@ namespace Textamina.Markdig.Syntax } } - text.Clear(); builder.Clear(); return result; } - private MatchLineResult TryParseTagType16(BlockParserState state, StringLine liner, int index, int startColumn) + private MatchLineResult TryParseTagType16(BlockParserState state, ref StringSlice liner, int index, int startColumn) { char c; c = liner.PeekChar(index); @@ -255,51 +247,49 @@ namespace Textamina.Markdig.Syntax private MatchLineResult MatchEnd(BlockParserState state, HtmlBlock htmlBlock) { - var liner = state.Line; - // Early exit if it is not starting by an HTML tag - var c = liner.Current; + var c = state.Line.CurrentChar; switch (htmlBlock.Type) { case HtmlBlockType.Comment: - if (liner.Search("-->")) + if (state.Line.Search("-->")) { return MatchLineResult.Last; } break; case HtmlBlockType.CData: - if (liner.Search("]]>")) + if (state.Line.Search("]]>")) { return MatchLineResult.Last; } break; case HtmlBlockType.ProcessingInstruction: - if (liner.Search("?>")) + if (state.Line.Search("?>")) { return MatchLineResult.Last; } break; case HtmlBlockType.DocumentType: - if (liner.Search(">")) + if (state.Line.Search(">")) { return MatchLineResult.Last; } break; case HtmlBlockType.ScriptPreOrStyle: // TODO: could be optimized with a dedicated parser - if (liner.SearchLowercase("") || liner.SearchLowercase("") || liner.SearchLowercase("")) + if (state.Line.SearchLowercase("") || state.Line.SearchLowercase("") || state.Line.SearchLowercase("")) { return MatchLineResult.Last; } break; case HtmlBlockType.InterruptingBlock: - if (liner.IsBlankLine()) + if (state.IsBlankLine) { return MatchLineResult.LastDiscard; } break; case HtmlBlockType.NonInterruptingBlock: - if (liner.IsBlankLine()) + if (state.IsBlankLine) { return MatchLineResult.LastDiscard; } diff --git a/src/Textamina.Markdig/Syntax/Inlines/AutoLinkInline.cs b/src/Textamina.Markdig/Syntax/Inlines/AutoLinkInline.cs index 3dd5037d..784acf5a 100644 --- a/src/Textamina.Markdig/Syntax/Inlines/AutoLinkInline.cs +++ b/src/Textamina.Markdig/Syntax/Inlines/AutoLinkInline.cs @@ -20,20 +20,18 @@ namespace Textamina.Markdig.Syntax public override bool Match(InlineParserState state) { - var text = state.Lines; - string link; bool isEmail; - var saved = text.Save(); - if (LinkHelper.TryParseAutolink(text, out link, out isEmail)) + var saved = state.Text; + if (LinkHelper.TryParseAutolink(ref state.Text, out link, out isEmail)) { state.Inline = new AutolinkInline() {IsEmail = isEmail, Url = link}; } else { - text.Restore(ref saved); + state.Text = saved; string htmlTag; - if (!HtmlHelper.TryParseHtmlTag(text, out htmlTag)) + if (!HtmlHelper.TryParseHtmlTag(ref state.Text, out htmlTag)) { return false; } diff --git a/src/Textamina.Markdig/Syntax/Inlines/CodeInline.cs b/src/Textamina.Markdig/Syntax/Inlines/CodeInline.cs index 84cf0416..d05cd209 100644 --- a/src/Textamina.Markdig/Syntax/Inlines/CodeInline.cs +++ b/src/Textamina.Markdig/Syntax/Inlines/CodeInline.cs @@ -18,10 +18,10 @@ namespace Textamina.Markdig.Syntax public override bool Match(InlineParserState state) { - var text = state.Lines; + var text = state.Text; int openSticks = 0; - if (text.PreviousChar1 == '`') + if (text.PeekChar(-1) == '`') { return false; } diff --git a/src/Textamina.Markdig/Syntax/Inlines/EmphasisInline.cs b/src/Textamina.Markdig/Syntax/Inlines/EmphasisInline.cs index 374a0b00..063e604b 100644 --- a/src/Textamina.Markdig/Syntax/Inlines/EmphasisInline.cs +++ b/src/Textamina.Markdig/Syntax/Inlines/EmphasisInline.cs @@ -215,15 +215,13 @@ namespace Textamina.Markdig.Syntax public override bool Match(InlineParserState state) { - var lines = state.Lines; - // First, some definitions. A delimiter run is either a sequence of one or more * characters that // is not preceded or followed by a * character, or a sequence of one or more _ characters that // is not preceded or followed by a _ character. - var delimiterChar = lines.CurrentChar; - var pc = lines.PreviousChar1; - if (delimiterChar == pc && lines.PreviousChar2 != '\\') + var delimiterChar = state.Text.CurrentChar; + var pc = state.Text.PeekChar(-1); + if (delimiterChar == pc && state.Text.PeekChar(-2) != '\\') { return false; } @@ -233,7 +231,7 @@ namespace Textamina.Markdig.Syntax do { delimiterCount++; - c = lines.NextChar(); + c = state.Text.NextChar(); } while (c == delimiterChar); diff --git a/src/Textamina.Markdig/Syntax/Inlines/EscapeInline.cs b/src/Textamina.Markdig/Syntax/Inlines/EscapeInline.cs index 644a2718..0869d214 100644 --- a/src/Textamina.Markdig/Syntax/Inlines/EscapeInline.cs +++ b/src/Textamina.Markdig/Syntax/Inlines/EscapeInline.cs @@ -20,7 +20,7 @@ namespace Textamina.Markdig.Syntax public override bool Match(InlineParserState state) { - var lines = state.Lines; + var lines = state.Text; // Go to escape character lines.NextChar(); diff --git a/src/Textamina.Markdig/Syntax/Inlines/HardlineBreakInline.cs b/src/Textamina.Markdig/Syntax/Inlines/HardlineBreakInline.cs index 6dcca1af..322605fc 100644 --- a/src/Textamina.Markdig/Syntax/Inlines/HardlineBreakInline.cs +++ b/src/Textamina.Markdig/Syntax/Inlines/HardlineBreakInline.cs @@ -17,8 +17,7 @@ namespace Textamina.Markdig.Syntax public override bool Match(InlineParserState state) { // Hard line breaks are for separating inline content within a block. Neither syntax for hard line breaks works at the end of a paragraph or other block element: - var text = state.Lines; - if (!(state.Block is ParagraphBlock) || text.ColumnPosition == 0 || !text.PreviousChar1.IsSpace() || !text.PreviousChar2.IsSpace()) + if (!(state.Block is ParagraphBlock) || state.Text.Column == 0 || !state.Text.PeekChar(-1).IsSpace() || !state.Text.PeekChar(-2).IsSpace()) { return false; } diff --git a/src/Textamina.Markdig/Syntax/Inlines/LinkInline.cs b/src/Textamina.Markdig/Syntax/Inlines/LinkInline.cs index 2d9320cb..8a001cc4 100644 --- a/src/Textamina.Markdig/Syntax/Inlines/LinkInline.cs +++ b/src/Textamina.Markdig/Syntax/Inlines/LinkInline.cs @@ -30,15 +30,13 @@ namespace Textamina.Markdig.Syntax public override bool Match(InlineParserState state) { - var text = state.Lines; - - var c = text.CurrentChar; + var c = state.Text.CurrentChar; bool isImage = false; if (c == '!') { isImage = true; - c = text.NextChar(); + c = state.Text.NextChar(); if (c != '[') { return false; @@ -50,21 +48,21 @@ namespace Textamina.Markdig.Syntax case '[': // If this is not an image, we may have a reference link shortcut // so we try to resolve it here - var saved = text.Save(); + var saved = state.Text; string label; // If the label is followed by either a ( or a [, this is not a shortcut - if (LinkHelper.TryParseLabel(text, out label)) + if (LinkHelper.TryParseLabel(ref state.Text, out label)) { if (!state.Document.LinkReferenceDefinitions.ContainsKey(label)) { label = null; } } - text.Restore(ref saved); + state.Text = saved; // Else we insert a LinkDelimiter - text.NextChar(); + state.Text.NextChar(); state.Inline = new LinkDelimiterInline(this) { Type = DelimiterType.Open, @@ -74,10 +72,10 @@ namespace Textamina.Markdig.Syntax return true; case ']': - text.NextChar(); + state.Text.NextChar(); if (state.Inline != null) { - if (TryProcessLinkOrImage(state, text)) + if (TryProcessLinkOrImage(state, ref state.Text)) { return true; } @@ -146,7 +144,7 @@ namespace Textamina.Markdig.Syntax return isValidLink; } - private bool TryProcessLinkOrImage(InlineParserState inlineState, StringLineGroup text) + private bool TryProcessLinkOrImage(InlineParserState inlineState, ref StringSlice text) { LinkDelimiterInline openParent = null; foreach (var parent in inlineState.Inline.FindParentOfType()) @@ -164,7 +162,7 @@ namespace Textamina.Markdig.Syntax case '(': string url; string title; - if (LinkHelper.TryParseInlineLink(text, out url, out title)) + if (LinkHelper.TryParseInlineLink(ref text, out url, out title)) { // Inline Link var link = new LinkInline() @@ -192,7 +190,7 @@ namespace Textamina.Markdig.Syntax // Handle Collapsed links if (text.CurrentChar == '[') { - if (text.PeekCharOnSameLine() == ']') + if (text.PeekChar(1) == ']') { label = openParent.Label; text.NextChar(); // Skip [ @@ -204,7 +202,7 @@ namespace Textamina.Markdig.Syntax label = openParent.Label; } - if (label != null || LinkHelper.TryParseLabel(text, true, out label)) + if (label != null || LinkHelper.TryParseLabel(ref text, true, out label)) { if (ProcessLinkReference(inlineState, label, openParent.IsImage, openParent.FirstChild)) diff --git a/src/Textamina.Markdig/Syntax/Inlines/LiteralInline.cs b/src/Textamina.Markdig/Syntax/Inlines/LiteralInline.cs index e0e7a5d7..fbd63cb6 100644 --- a/src/Textamina.Markdig/Syntax/Inlines/LiteralInline.cs +++ b/src/Textamina.Markdig/Syntax/Inlines/LiteralInline.cs @@ -41,7 +41,7 @@ namespace Textamina.Markdig.Syntax builder = literal.ContentBuilder; } - var text = state.Lines; + var text = state.Text; builder.Append(text.CurrentChar); text.NextChar(); return true; diff --git a/src/Textamina.Markdig/Syntax/LeafBlock.cs b/src/Textamina.Markdig/Syntax/LeafBlock.cs index bcea30ad..1d6cb9b7 100644 --- a/src/Textamina.Markdig/Syntax/LeafBlock.cs +++ b/src/Textamina.Markdig/Syntax/LeafBlock.cs @@ -6,18 +6,13 @@ namespace Textamina.Markdig.Syntax { protected LeafBlock(BlockParser parser) : base(parser) { - Lines = new StringLineGroup(); + Lines = new StringSliceList(); } - public StringLineGroup Lines { get; set; } + public StringSliceList Lines { get; set; } public Inline Inline { get; set; } public bool NoInline { get; set; } - - internal void Append(StringLine line) - { - Lines.Add(line); - } } } \ No newline at end of file diff --git a/src/Textamina.Markdig/Syntax/LinkReferenceDefinitionBlock.cs b/src/Textamina.Markdig/Syntax/LinkReferenceDefinitionBlock.cs index fa8892af..bba37aae 100644 --- a/src/Textamina.Markdig/Syntax/LinkReferenceDefinitionBlock.cs +++ b/src/Textamina.Markdig/Syntax/LinkReferenceDefinitionBlock.cs @@ -23,14 +23,14 @@ namespace Textamina.Markdig.Syntax public string Title { get; set; } - public static bool TryParse(StringLineGroup lines, out LinkReferenceDefinitionBlock block) + public static bool TryParse(ref StringSlice text, out LinkReferenceDefinitionBlock block) { block = null; string label; string url; string title; - if (!LinkHelper.TryParseLinkReferenceDefinition(lines, out label, out url, out title)) + if (!LinkHelper.TryParseLinkReferenceDefinition(ref text, out label, out url, out title)) { return false; } diff --git a/src/Textamina.Markdig/Syntax/ListBlock.cs b/src/Textamina.Markdig/Syntax/ListBlock.cs index 554591f3..6037e20b 100644 --- a/src/Textamina.Markdig/Syntax/ListBlock.cs +++ b/src/Textamina.Markdig/Syntax/ListBlock.cs @@ -38,26 +38,24 @@ namespace Textamina.Markdig.Syntax return MatchLineResult.Skip; } - var liner = state.Line; - // When both a thematic break and a list item are possible // interpretations of a line, the thematic break takes precedence - var save = liner.Save(); - if (BreakBlock.Parser.Match(state) == MatchLineResult.Last) + var save = state.Line; + if (ThematicBreakBlock.Parser.Match(state) == MatchLineResult.Last) { - // Remove the BreakBlock as we will let the BreakBlock to catch it later + // Remove the ThematicBreakBlock as we will let the ThematicBreakBlock to catch it later state.NewBlocks.Pop(); return MatchLineResult.None; } - liner.Restore(ref save); + state.SetCurrentLine(ref save); // 5.2 List items // TODO: Check with specs, it is not clear that list marker or bullet marker must be followed by at least 1 space int preIndent = 0; - for (int i = liner.Start - 1; i >= 0; i--) + for (int i = state.Line.Start - 1; i >= 0; i--) { - if (liner[i].IsSpaceOrTab()) + if (state.Line[i].IsSpaceOrTab()) { preIndent++; } @@ -67,7 +65,7 @@ namespace Textamina.Markdig.Syntax } } - var saveLiner = liner.Save(); + var saveLiner = state.Line; // If we have already a ListItemBlock, we are going to try to append to it var listItem = state.Pending as ListItemBlock; @@ -78,7 +76,7 @@ namespace Textamina.Markdig.Syntax // Allow all blanks lines if the last block is a fenced code block // Allow 1 blank line inside a list // If > 1 blank line, terminate this list - var isBlankLine = liner.IsBlankLine(); + var isBlankLine = state.IsBlankLine; //if (isBlankLine && !(state.LastBlock is FencedCodeBlock)) // TODO: Handle this case var isInFencedBlock = state.LastBlock is FencedCodeBlock; if (isBlankLine) @@ -114,26 +112,26 @@ namespace Textamina.Markdig.Syntax list.CountBlankLinesReset = 0; - var c = liner.Current; - var startPosition = liner.Column; + var c = state.Line.CurrentChar; + var startPosition = state.Line.Start; // List Item starting with a blank line (-1) if (listItem.NumberOfSpaces < 0) { int expectedCount = -listItem.NumberOfSpaces; int countSpaces = 0; - var saved = new StringLine.State(); + var saved = new StringSlice(); while (c.IsSpaceOrTab()) { - c = liner.NextChar(); - countSpaces = preIndent + liner.Column - startPosition; + c = state.Line.NextChar(); + countSpaces = preIndent + state.Line.Column - startPosition; if (countSpaces == expectedCount) { - saved = liner.Save(); + saved = state.Line; } else if (countSpaces >= 4) { - liner.Restore(ref saved); + state.SetCurrentLine(ref saved); countSpaces = expectedCount; break; } @@ -149,15 +147,15 @@ namespace Textamina.Markdig.Syntax { while (c.IsSpaceOrTab()) { - c = liner.NextChar(); - var countSpaces = preIndent + liner.Column - startPosition; + c = state.Line.NextChar(); + var countSpaces = preIndent + state.Line.Column - startPosition; if (countSpaces >= listItem.NumberOfSpaces) { return MatchLineResult.Continue; } } } - liner.Restore(ref saveLiner); + state.SetCurrentLine(ref saveLiner); } return TryParseListItem(ref state, preIndent); @@ -165,33 +163,32 @@ namespace Textamina.Markdig.Syntax private MatchLineResult TryParseListItem(ref BlockParserState state, int preIndent) { - var liner = state.Line; - var isInList = state.Pending is ListItemBlock; - var preStartPosition = liner.Start; + var preStartPosition = state.Line.Start; - var c = liner.Current; + var c = state.Line.CurrentChar; if (isInList) { while (c.IsSpaceOrTab()) { - c = liner.NextChar(); + c = state.Line.NextChar(); } } else { - liner.SkipLeadingSpaces3(); - c = liner.Current; + // TODO + //state.Line.SkipLeadingSpaces3(); + c = state.Line.CurrentChar; } - preIndent = preIndent + liner.Start - preStartPosition; + preIndent = preIndent + state.Line.Start - preStartPosition; var isOrdered = false; var bulletChar = (char) 0; int orderedStart = 0; var orderedDelimiter = (char) 0; - var column = liner.Start; + var column = state.Line.Start; if (c.IsBulletListMarker()) { @@ -204,7 +201,7 @@ namespace Textamina.Markdig.Syntax while (c.IsDigit()) { orderedStart = orderedStart*10 + c - '0'; - c = liner.NextChar(); + c = state.Line.NextChar(); preIndent++; countDigit++; } @@ -230,11 +227,11 @@ namespace Textamina.Markdig.Syntax } // Skip Bullet or '.' - liner.NextChar(); + state.Line.NextChar(); // Item starting with a blank line int numberOfSpaces; - if (liner.IsBlankLine()) + if (state.IsBlankLine) { // Use a negative number to store the number of expected chars numberOfSpaces = -(preIndent + 1); @@ -243,34 +240,34 @@ namespace Textamina.Markdig.Syntax { var startPosition = -1; int countSpaceAfterBullet = 0; - var saved = new StringLine.State(); + var saved = new StringSlice(); for (int i = 0; i <= 4; i++) { - c = liner.Current; + c = state.Line.CurrentChar; if (!c.IsSpaceOrTab()) { break; } if (i == 0) { - startPosition = liner.Column; + startPosition = state.Line.Column; } - var endPosition = liner.Column; + var endPosition = state.Line.Column; countSpaceAfterBullet = endPosition - startPosition; if (countSpaceAfterBullet == 1) { - saved = liner.Save(); + saved = state.Line; } else if (countSpaceAfterBullet >= 4) { - liner.SpaceHeaderCount = countSpaceAfterBullet - 4; + //state.Line.SpaceHeaderCount = countSpaceAfterBullet - 4; countSpaceAfterBullet = 0; - liner.Restore(ref saved); + state.SetCurrentLine(ref saved); break; } - liner.NextChar(); + state.Line.NextChar(); } // If we haven't matched any spaces, early exit diff --git a/src/Textamina.Markdig/Syntax/ParagraphBlock.cs b/src/Textamina.Markdig/Syntax/ParagraphBlock.cs index 0cc50af8..fdc57cd9 100644 --- a/src/Textamina.Markdig/Syntax/ParagraphBlock.cs +++ b/src/Textamina.Markdig/Syntax/ParagraphBlock.cs @@ -25,13 +25,15 @@ namespace Textamina.Markdig.Syntax { public override MatchLineResult Match(BlockParserState state) { - var liner = state.Line; - liner.SkipLeadingSpaces3(); + if (state.IsCodeIndent) + { + return MatchLineResult.None; + } - var column = liner.Start; + var column = state.Column; // Else it is a continue, we don't break on blank lines - var isBlankLine = liner.IsBlankLine(); + var isBlankLine = state.IsBlankLine; var paragraph = state.Pending as ParagraphBlock; @@ -59,9 +61,9 @@ namespace Textamina.Markdig.Syntax { var headingChar = (char) 0; bool checkForSpaces = false; - for (int i = liner.Start; i <= liner.End; i++) + for (int i = state.Start; i <= state.EndOffset; i++) { - var c = liner[i]; + var c = state.Line[i]; if (headingChar == 0) { if (c == '=' || c == '-') @@ -112,10 +114,7 @@ namespace Textamina.Markdig.Syntax Level = level, Lines = paragraph.Lines, }; - foreach (var line in heading.Lines) - { - line.Trim(); - } + heading.Lines.Trim(); // Remove the paragraph as a pending block state.NewBlocks.Push(heading); @@ -129,46 +128,46 @@ namespace Textamina.Markdig.Syntax return result; } - private bool TryMatchLinkReferenceDefinition(StringLineGroup localLineGroup, BlockParserState state) + private bool TryMatchLinkReferenceDefinition(StringSliceList localLineGroup, BlockParserState state) { bool atLeastOneFound = false; - var saved = new StringLineGroup.State(); - while (true) - { - // If we have found a LinkReferenceDefinition, we can discard the previous paragraph - localLineGroup.Save(ref saved); - LinkReferenceDefinitionBlock linkReferenceDefinition; - if (LinkReferenceDefinitionBlock.TryParse(localLineGroup, out linkReferenceDefinition)) - { - if (!state.Root.LinkReferenceDefinitions.ContainsKey(linkReferenceDefinition.Label)) - { - state.Root.LinkReferenceDefinitions[linkReferenceDefinition.Label] = linkReferenceDefinition; - } - atLeastOneFound = true; + //var saved = new StringSliceList.State(); + //while (true) + //{ + // // If we have found a LinkReferenceDefinition, we can discard the previous paragraph + // localLineGroup.Save(ref saved); + // LinkReferenceDefinitionBlock linkReferenceDefinition; + // if (LinkReferenceDefinitionBlock.TryParse(localLineGroup, out linkReferenceDefinition)) + // { + // if (!state.Root.LinkReferenceDefinitions.ContainsKey(linkReferenceDefinition.Label)) + // { + // state.Root.LinkReferenceDefinitions[linkReferenceDefinition.Label] = linkReferenceDefinition; + // } + // atLeastOneFound = true; - // Remove lines that have been matched - if (localLineGroup.LinePosition == localLineGroup.Count) - { - localLineGroup.Clear(); - } - else - { - for (int i = localLineGroup.LinePosition - 1; i >= 0; i--) - { - localLineGroup.RemoveAt(i); - } - } - } - else - { - if (!atLeastOneFound) - { - localLineGroup.Restore(ref saved); - } - break; - } - } + // // Remove lines that have been matched + // if (localLineGroup.LinePosition == localLineGroup.Count) + // { + // localLineGroup.Clear(); + // } + // else + // { + // for (int i = localLineGroup.LinePosition - 1; i >= 0; i--) + // { + // localLineGroup.RemoveAt(i); + // } + // } + // } + // else + // { + // if (!atLeastOneFound) + // { + // localLineGroup.Restore(ref saved); + // } + // break; + // } + //} return atLeastOneFound; } @@ -193,7 +192,7 @@ namespace Textamina.Markdig.Syntax var lineCount = lines.Count; for (int i = 0; i < lineCount; i++) { - var line = lines[i]; + var line = lines.Slices[i]; line.Trim(); } } diff --git a/src/Textamina.Markdig/Syntax/QuoteBlock.cs b/src/Textamina.Markdig/Syntax/QuoteBlock.cs index 0f5262f9..57ecd06f 100644 --- a/src/Textamina.Markdig/Syntax/QuoteBlock.cs +++ b/src/Textamina.Markdig/Syntax/QuoteBlock.cs @@ -15,17 +15,18 @@ namespace Textamina.Markdig.Syntax { public override MatchLineResult Match(BlockParserState state) { - var liner = state.Line; - - liner.SkipLeadingSpaces3(); + if (state.IsCodeIndent) + { + return MatchLineResult.None; + } // 5.1 Block quotes // A block quote marker consists of 0-3 spaces of initial indent, plus (a) the character > together with a following space, or (b) a single character > not followed by a space. - var c = liner.Current; - var column = liner.Start; + var c = state.CurrentChar; + var column = state.Column; if (c != '>') { - if (state.Pending != null && liner.IsBlankLine()) + if (state.Pending != null && state.IsBlankLine) { state.Close(state.Pending); return MatchLineResult.None; @@ -33,10 +34,10 @@ namespace Textamina.Markdig.Syntax return MatchLineResult.None; } - c = liner.NextChar(); + c = state.NextChar(); if (c.IsSpace()) { - liner.NextChar(); + state.NextChar(); } if (state.Pending == null) diff --git a/src/Textamina.Markdig/Syntax/StringLine.cs b/src/Textamina.Markdig/Syntax/StringLine.cs deleted file mode 100644 index c8c7a08d..00000000 --- a/src/Textamina.Markdig/Syntax/StringLine.cs +++ /dev/null @@ -1,251 +0,0 @@ -using System; -using System.Runtime.CompilerServices; -using Textamina.Markdig.Helpers; -using Textamina.Markdig.Parsing; - -namespace Textamina.Markdig.Syntax -{ - public sealed class StringLine - { - public StringLine(string text, int lineIndex = 0) - { - if (text == null) throw new ArgumentNullException(nameof(text)); - LineIndex = lineIndex; - Text = text; - Start = 0; - End = Text.Length - 1; - Column = 0; - SpaceHeaderCount = 0; - Current = End >= 0 ? Text[0] : (char)0; - } - - public int LineIndex { get; } - - public string Text { get; private set; } - - public int Start { get; set; } - - public int End { get; set; } - - public int SpaceHeaderCount { get; set; } - - public int Column { get; private set; } - - public char Current { get; private set; } - - public bool IsEol => Start > End; - - private bool isBlankLine; - - public char this[int index] => Text[index]; - - [MethodImpl(MethodImplOptionPortable.AggressiveInlining)] - public State Save() - { - return new State(Start, End, Column, SpaceHeaderCount, Current); - } - - [MethodImpl(MethodImplOptionPortable.AggressiveInlining)] - public void Restore(State state) - { - Restore(ref state); - } - - [MethodImpl(MethodImplOptionPortable.AggressiveInlining)] - public void Restore(ref State state) - { - Start = state.Start; - End = state.End; - Column = state.Column; - SpaceHeaderCount = state.SpaceHeaderCount; - Current = state.Current; - } - - public char NextChar() - { - Start++; - if (Start <= End) - { - // If previous character was a tab make the Column += 4 - Column++; - if (Current.IsTab()) - { - // Align the tab on a column - Column = ((Column + 3) / 4) * 4; - } - Current = Text[Start].EscapeInsecure(); - } - else - { - Start = End + 1; - Current = (char) 0; - } - return Current; - } - - [MethodImpl(MethodImplOptionPortable.AggressiveInlining)] - public char PeekChar(int offset) - { - var index = Start + offset; - return index <= End ? Text[index].EscapeInsecure() : (char) 0; - } - - public bool Match(string text, int offset = 0) - { - return Match(text, End, offset); - } - - public bool Match(string text, int end, int offset) - { - var index = Start + offset; - int i = 0; - for (; index <= end && i < text.Length; i++, index++) - { - if (text[i] != Text[index]) - { - return false; - } - } - - return i == text.Length; - } - - public bool MatchLowercase(string text, int end, int offset) - { - var index = Start + offset; - int i = 0; - for (; index <= end && i < text.Length; i++, index++) - { - if (text[i] != char.ToLowerInvariant(Text[index])) - { - return false; - } - } - - return i == text.Length; - } - - public bool Search(string text, int offset = 0) - { - var end = End - text.Length + 1; - for (int i = Start; i <= end; i++) - { - if (Match(text, End, i)) - { - return true; - } - } - return false; - } - - public bool SearchLowercase(string text, int offset = 0) - { - var end = End - text.Length + 1; - for (int i = Start; i <= end; i++) - { - if (MatchLowercase(text, End, i)) - { - return true; - } - } - return false; - } - - public bool IsBlankLine() - { - if (isBlankLine) - { - return true; - } - - for (int i = Start; i <= End; i++) - { - if (!Text[i].IsSpace()) - { - return false; - } - } - isBlankLine = true; - return true; - } - - [MethodImpl(MethodImplOptionPortable.AggressiveInlining)] - public void SkipLeadingSpaces3() - { - var c = Current; - if (c.IsSpace()) - { - c = NextChar(); - if (c.IsSpace()) - { - c = NextChar(); - if (c.IsSpace()) - { - NextChar(); - } - } - } - } - - public StringSlice ToSlice() - { - return new StringSlice(Text, Start, End); - } - - public void TrimStart(bool includeTabs = false) - { - // Strip leading spaces - var c = Current; - while (includeTabs ? c.IsSpaceOrTab() :c.IsSpace()) - { - c = NextChar(); - } - } - - public void TrimEnd(bool includeTabs = false) - { - for (int i = End; i >= Start; i--) - { - End = i; - var c = this[i]; - if (!(includeTabs ? c.IsSpaceOrTab() : c.IsSpace())) - { - break; - } - } - } - - public void Trim() - { - TrimStart(); - TrimEnd(); - } - - public override string ToString() - { - return Start <= End ? Text.Substring(Start, End - Start + 1) : string.Empty; - } - - public struct State - { - public State(int start, int end, int column, int spaceHeaderCount, char current) - { - Start = start; - End = end; - Column = column; - SpaceHeaderCount = spaceHeaderCount; - Current = current; - } - - public readonly int Start; - - public readonly int End; - - public readonly int Column; - - public readonly int SpaceHeaderCount; - - public readonly char Current; - } - } -} \ No newline at end of file diff --git a/src/Textamina.Markdig/Syntax/StringLineGroup.cs b/src/Textamina.Markdig/Syntax/StringLineGroup.cs deleted file mode 100644 index 0d766987..00000000 --- a/src/Textamina.Markdig/Syntax/StringLineGroup.cs +++ /dev/null @@ -1,265 +0,0 @@ -using System; -using System.Collections.ObjectModel; -using System.Text; -using Textamina.Markdig.Helpers; -using Textamina.Markdig.Parsing; - -namespace Textamina.Markdig.Syntax -{ - public class StringLineGroup : Collection - { - private StringLine currentLine; - - public StringLineGroup() - { - } - - public StringLineGroup(string text) - { - if (text == null) throw new ArgumentNullException(nameof(text)); - Add(new StringLine(text)); - } - - public int LinePosition { get; private set; } - - public int ColumnPosition { get; private set; } - - public StringLine Current => currentLine; - - public char CurrentChar { get; private set; } - - public char PreviousChar1 { get; private set; } - - public char PreviousChar2 { get; private set; } - - public void Reset() - { - ColumnPosition = -1; - LinePosition = 0; - currentLine = Count > 0 ? this[0] : null; - if (currentLine != null) - { - ColumnPosition = currentLine.Start - 1; - } - NextChar(); - } - - public void SetColumn(int index) - { - if (Current != null && index <= Current.End) - { - CurrentChar = Current[index]; - ColumnPosition = index; - // Don't set previous char - PreviousChar1 = '\0'; - PreviousChar2 = '\0'; - } - } - - public char PeekCharOnSameLine() - { - if (Current != null && (ColumnPosition+1) <= Current.End) - { - return Current[ColumnPosition + 1]; - } - return (char)0; - } - - protected override void ClearItems() - { - base.ClearItems(); - Reset(); - } - - protected override void InsertItem(int index, StringLine item) - { - base.InsertItem(index, item); - if (index == 0) - { - Reset(); - } - } - - protected override void SetItem(int index, StringLine item) - { - base.SetItem(index, item); - if (index == 0) - { - Reset(); - } - } - - protected override void RemoveItem(int index) - { - base.RemoveItem(index); - if (index == 0) - { - Reset(); - } - } - - public bool IsEndOfLines => CurrentChar == '\0'; - - public State Save() - { - return new State(currentLine, LinePosition, ColumnPosition, CurrentChar, PreviousChar1, PreviousChar2); - } - - public void Save(ref State state) - { - state.CurrentLine = currentLine; - state.LinePosition = LinePosition; - state.ColumnPosition = ColumnPosition; - state.Current = CurrentChar; - state.PreviousChar1 = PreviousChar1; - state.PreviousChar2 = PreviousChar2; - } - - public void Restore(ref State state) - { - currentLine = state.CurrentLine; - LinePosition = state.LinePosition; - ColumnPosition = state.ColumnPosition; - CurrentChar = state.Current; - PreviousChar1 = state.PreviousChar1; - PreviousChar2 = state.PreviousChar2; - } - - public char NextChar() - { - PreviousChar2 = PreviousChar1; - PreviousChar1 = CurrentChar; - if (currentLine != null) - { - ColumnPosition++; - if (ColumnPosition <= currentLine.End) - { - CurrentChar = currentLine[ColumnPosition].EscapeInsecure(); - } - else - { - LinePosition++; - if (LinePosition < Count) - { - currentLine = this[LinePosition]; - } - else - { - currentLine = null; - LinePosition = Count; - } - - ColumnPosition = -1; - if (currentLine != null) - { - ColumnPosition = currentLine.Start - 1; - } - CurrentChar = currentLine != null ? '\n' : '\0'; - } - } - else - { - CurrentChar = '\0'; - } - - return CurrentChar; - } - - public bool SkipWhiteSpaces() - { - bool hasWhitespaces = false; - while (CurrentChar.IsWhitespace()) - { - NextChar(); - hasWhitespaces = true; - } - return hasWhitespaces; - } - - public bool SkipWhiteSpaces(out int newLineCount) - { - bool hasWhitespaces = false; - newLineCount = 0; - while (CurrentChar.IsWhitespace()) - { - if (CurrentChar == '\n') - { - newLineCount++; - } - NextChar(); - hasWhitespaces = true; - } - return hasWhitespaces; - } - - public void TrimStart() - { - if (Count == 0) - { - return; - } - - // Strip leading and - this[0].TrimStart(); - Reset(); - } - - public void TrimEnd() - { - if (Count == 0) - { - return; - } - - this[Count - 1].TrimEnd(); - Reset(); - } - - public void Trim() - { - TrimStart(); - TrimEnd(); - } - - public override string ToString() - { - var stringBuilder = new StringBuilder(); - bool firstLine = true; - foreach (var line in this) - { - if (!firstLine) - { - stringBuilder.Append('\n'); - } - stringBuilder.Append(line); - firstLine = false; - } - return stringBuilder.ToString(); - } - - public struct State - { - public State(StringLine currentLine, int linePosition, int columnPosition, char current, char previousChar1, char previousChar2) - { - CurrentLine = currentLine; - LinePosition = linePosition; - ColumnPosition = columnPosition; - Current = current; - PreviousChar1 = previousChar1; - PreviousChar2 = previousChar2; - } - - public StringLine CurrentLine; - - public int LinePosition; - - public int ColumnPosition; - - public char Current; - - public char PreviousChar1; - - public char PreviousChar2; - } - } -} \ No newline at end of file diff --git a/src/Textamina.Markdig/Syntax/StringSlice.cs b/src/Textamina.Markdig/Syntax/StringSlice.cs index a9d48a67..5658d86a 100644 --- a/src/Textamina.Markdig/Syntax/StringSlice.cs +++ b/src/Textamina.Markdig/Syntax/StringSlice.cs @@ -1,25 +1,171 @@ -namespace Textamina.Markdig.Syntax +using System; +using System.Runtime.CompilerServices; +using Textamina.Markdig.Helpers; + +namespace Textamina.Markdig.Syntax { public struct StringSlice { - public StringSlice(string text, int start, int end) + public StringSlice(string text) { + if (text == null) throw new ArgumentNullException(nameof(text)); Text = text; - Start = start; - End = end; + Start = 0; + End = Text.Length - 1; } - public string Text; + public readonly string Text; public int Start; public int End; - public int Length - { - get { return End - Start; } + public int Column => Start; - set { End = Start + value; } + public char CurrentChar => Start <= End ? this[Start] : '\0'; + + public bool IsEndOfSlice => Start > End; + + public char this[int index] => Text[index].EscapeInsecure(); + + [MethodImpl(MethodImplOptionPortable.AggressiveInlining)] + public char NextChar() + { + Start++; + if (Start > End) + { + Start = End + 1; + } + return CurrentChar; + } + + [MethodImpl(MethodImplOptionPortable.AggressiveInlining)] + public char PeekChar(int offset) + { + var index = Start + offset; + return index >= Start && index <= End ? Text[index].EscapeInsecure() : (char) 0; + } + + [MethodImpl(MethodImplOptionPortable.AggressiveInlining)] + public char PeekCharExtra(int offset) + { + var index = Start + offset; + return index >= 0 && index < Text.Length ? Text[index].EscapeInsecure() : (char)0; + } + + public bool Match(string text, int offset = 0) + { + return Match(text, End, offset); + } + + public bool Match(string text, int end, int offset) + { + var index = Start + offset; + int i = 0; + for (; index <= end && i < text.Length; i++, index++) + { + if (text[i] != Text[index]) + { + return false; + } + } + + return i == text.Length; + } + + public bool MatchLowercase(string text, int end, int offset) + { + var index = Start + offset; + int i = 0; + for (; index <= end && i < text.Length; i++, index++) + { + if (text[i] != char.ToLowerInvariant(Text[index])) + { + return false; + } + } + + return i == text.Length; + } + + public bool Search(string text, int offset = 0) + { + var end = End - text.Length + 1; + for (int i = Start; i <= end; i++) + { + if (Match(text, End, i)) + { + return true; + } + } + return false; + } + + public bool SearchLowercase(string text, int offset = 0) + { + var end = End - text.Length + 1; + for (int i = Start; i <= end; i++) + { + if (MatchLowercase(text, End, i)) + { + return true; + } + } + return false; + } + + public bool TrimStart() + { + // Strip leading spaces + var c = CurrentChar; + var hasWhitespaces = false; + while (c.IsWhitespace()) + { + c = NextChar(); + hasWhitespaces = true; + } + return hasWhitespaces; + } + + public bool TrimStart(out int newLineCount) + { + bool hasWhitespaces = false; + newLineCount = 0; + var c = CurrentChar; + while (c.IsWhitespace()) + { + if (c == '\n') + { + newLineCount++; + } + c = NextChar(); + hasWhitespaces = true; + } + return hasWhitespaces; + } + + public void TrimEnd(bool includeTabs = false) + { + for (int i = End; i >= Start; i--) + { + End = i; + var c = this[i]; + if (!(includeTabs ? c.IsSpaceOrTab() : c.IsSpace())) + { + break; + } + } + } + + public void Trim() + { + TrimStart(); + TrimEnd(); + } + + public override string ToString() + { + return Start <= End ? Text.Substring(Start, End - Start + 1) : string.Empty; } } } \ No newline at end of file diff --git a/src/Textamina.Markdig/Syntax/StringSliceList.cs b/src/Textamina.Markdig/Syntax/StringSliceList.cs new file mode 100644 index 00000000..3a1b54b3 --- /dev/null +++ b/src/Textamina.Markdig/Syntax/StringSliceList.cs @@ -0,0 +1,67 @@ +using System; +using System.Text; +using Textamina.Markdig.Helpers; + +namespace Textamina.Markdig.Syntax +{ + public class StringSliceList + { + private static readonly StringSlice[] Empty = new StringSlice[0]; + + private StringSlice currentLine; + + public StringSliceList() + { + Slices = Empty; + } + + public StringSliceList(string text) + { + if (text == null) throw new ArgumentNullException(nameof(text)); + Append(new StringSlice(text)); + } + + public StringSlice[] Slices; + + public int Count; + + public void Append(ref StringSlice slice) + { + if (Count == Slices.Length) IncreaseCapacity(); + Slices[Count++] = slice; + } + + public void Append(StringSlice slice) + { + if (Count == Slices.Length) IncreaseCapacity(); + Slices[Count++] = slice; + } + + private void IncreaseCapacity() + { + int newCapacity = Slices.Length == 0 ? 4 : Slices.Length * 2; + var newItems = new StringSlice[newCapacity]; + if (Count > 0) + { + Array.Copy(Slices, 0, newItems, 0, Count); + } + Slices = newItems; + } + + public override string ToString() + { + var stringBuilder = StringBuilderCache.Local(); + for(int i = 0; i < Count; i++) + { + if (i > 0) + { + stringBuilder.Append('\n'); + } + stringBuilder.Append(Slices[i]); + } + var str = stringBuilder.ToString(); + stringBuilder.Clear(); + return str; + } + } +} \ No newline at end of file diff --git a/src/Textamina.Markdig/Syntax/StringSliceListExtensions.cs b/src/Textamina.Markdig/Syntax/StringSliceListExtensions.cs new file mode 100644 index 00000000..a59a695e --- /dev/null +++ b/src/Textamina.Markdig/Syntax/StringSliceListExtensions.cs @@ -0,0 +1,14 @@ +namespace Textamina.Markdig.Syntax +{ + public static class StringSliceListExtensions + { + public static void Trim(this StringSliceList slices) + { + for (int i = 0; i < slices.Count; i++) + { + slices.Slices[i].Trim(); + } + } + + } +} \ No newline at end of file diff --git a/src/Textamina.Markdig/Syntax/BreakBlock.cs b/src/Textamina.Markdig/Syntax/ThematicBreakBlock.cs similarity index 80% rename from src/Textamina.Markdig/Syntax/BreakBlock.cs rename to src/Textamina.Markdig/Syntax/ThematicBreakBlock.cs index 8371f068..4ca96645 100644 --- a/src/Textamina.Markdig/Syntax/BreakBlock.cs +++ b/src/Textamina.Markdig/Syntax/ThematicBreakBlock.cs @@ -6,11 +6,11 @@ namespace Textamina.Markdig.Syntax /// /// Repressents a thematic break. /// - public class BreakBlock : LeafBlock + public class ThematicBreakBlock : LeafBlock { public new static readonly BlockParser Parser = new ParserInternal(); - public BreakBlock(BlockParser parser) : base(parser) + public ThematicBreakBlock(BlockParser parser) : base(parser) { NoInline = true; } @@ -19,20 +19,23 @@ namespace Textamina.Markdig.Syntax { public override MatchLineResult Match(BlockParserState state) { - var liner = state.Line; - liner.SkipLeadingSpaces3(); + if (state.IsCodeIndent) + { + return MatchLineResult.None; + } - var column = liner.Start; + var column = state.Column; // 4.1 Thematic breaks // A line consisting of 0-3 spaces of indentation, followed by a sequence of three or more matching -, _, or * characters, each followed optionally by any number of spaces - var c = liner.Current; + var c = state.CurrentChar; int count = 0; var matchChar = (char)0; bool hasSpacesSinceLastMatch = false; bool hasInnerSpaces = false; - while (!liner.IsEol) + int offset = 0; + while (c != '\0') { if (count == 0 && (c == '-' || c == '_' || c == '*')) { @@ -56,7 +59,9 @@ namespace Textamina.Markdig.Syntax { hasSpacesSinceLastMatch = true; } - c = liner.NextChar(); + + offset++; + c = state.PeekChar(offset); } // If it as less than 3 chars or it is a setex heading and we are already in a paragraph, let the paragraph handle it @@ -77,8 +82,8 @@ namespace Textamina.Markdig.Syntax return MatchLineResult.None; } - state.NewBlocks.Push(new BreakBlock(this) { Column = column }); - return MatchLineResult.Last; + state.NewBlocks.Push(new ThematicBreakBlock(this) { Column = column }); + return MatchLineResult.LastDiscard; } } } diff --git a/src/Textamina.Markdig/Textamina.Markdig.csproj b/src/Textamina.Markdig/Textamina.Markdig.csproj index 8d305c87..42455093 100644 --- a/src/Textamina.Markdig/Textamina.Markdig.csproj +++ b/src/Textamina.Markdig/Textamina.Markdig.csproj @@ -63,7 +63,6 @@ - @@ -75,7 +74,8 @@ - + + @@ -86,8 +86,8 @@ - - + +