diff --git a/src/Markdig.Tests/TestLinkHelper.cs b/src/Markdig.Tests/TestLinkHelper.cs index 75acbb19..d46c50ab 100644 --- a/src/Markdig.Tests/TestLinkHelper.cs +++ b/src/Markdig.Tests/TestLinkHelper.cs @@ -89,6 +89,14 @@ public void TestTitleMultiline() Assert.AreEqual("this\ris\r\na\ntitle", title); } + [Test] + public void TestTitleMultilineWithSpaceAndBackslash() + { + var text = new StringSlice("'a\n\\ \\\ntitle'"); + Assert.True(LinkHelper.TryParseTitle(ref text, out string title, out _)); + Assert.AreEqual("a\n\\ \\\ntitle", title); + } + [Test] public void TestUrlAndTitle() { @@ -238,6 +246,13 @@ public void TestlLinkReferenceDefinitionSimple() } + [Test] + public void TestlLinkReferenceDefinitionInvalid() + { + var text = new StringSlice("[foo]: /url (title) x\n"); + Assert.False(LinkHelper.TryParseLinkReferenceDefinition(ref text, out _, out _, out _, out _, out _, out _)); + } + [Test] public void TestAutoLinkUrlSimple() { diff --git a/src/Markdig.Tests/TestPlayParser.cs b/src/Markdig.Tests/TestPlayParser.cs index 53420b1d..18942411 100644 --- a/src/Markdig.Tests/TestPlayParser.cs +++ b/src/Markdig.Tests/TestPlayParser.cs @@ -46,6 +46,14 @@ public void TestLink() Assert.AreEqual("/yoyo", link?.Url); } + [Test] + public void TestLinkWithMultipleBackslashesInTitle() + { + var doc = Markdown.Parse(@"[link](/uri '\\\\127.0.0.1')"); + var link = doc.Descendants().FirstOrDefault(); + Assert.AreEqual(@"\\127.0.0.1", link?.Title); + } + [Test] public void TestListBug2() { diff --git a/src/Markdig/Helpers/LinkHelper.cs b/src/Markdig/Helpers/LinkHelper.cs index 48581e1e..ac47b294 100644 --- a/src/Markdig/Helpers/LinkHelper.cs +++ b/src/Markdig/Helpers/LinkHelper.cs @@ -545,88 +545,70 @@ public static bool TryParseTitle(ref T text, out string? title, out char encl enclosingCharacter = c; var closingQuote = c == '(' ? ')' : c; bool hasEscape = false; - // -1: undefined - // 0: has only spaces - // 1: has other characters - int hasOnlyWhiteSpacesSinceLastLine = -1; - while (true) + bool isLineBlank = false; // the first line is never blank + while ((c = text.NextChar()) != '\0') { - c = text.NextChar(); - if (c == '\r' || c == '\n') { - if (hasOnlyWhiteSpacesSinceLastLine >= 0) + if (isLineBlank) { - if (hasOnlyWhiteSpacesSinceLastLine == 1) - { - break; - } - hasOnlyWhiteSpacesSinceLastLine = -1; + break; + } + + if (hasEscape) + { + hasEscape = false; + buffer.Append('\\'); } + buffer.Append(c); + if (c == '\r' && text.PeekChar() == '\n') { buffer.Append('\n'); text.SkipChar(); } - continue; - } - if (c == '\0') - { - break; + isLineBlank = true; } - - if (c == closingQuote) + else if (hasEscape) { - if (hasEscape) + hasEscape = false; + + if (!c.IsAsciiPunctuation()) { - buffer.Append(closingQuote); - hasEscape = false; - continue; + buffer.Append('\\'); } - // Skip last quote - text.SkipChar(); - goto ReturnValid; + buffer.Append(c); } - - if (hasEscape && !c.IsAsciiPunctuation()) + else if (c == closingQuote) { - buffer.Append('\\'); + // Skip last quote + text.SkipChar(); + title = buffer.ToString(); + return true; } - - if (c == '\\') + else if (c == '\\') { hasEscape = true; - continue; + isLineBlank = false; } - - hasEscape = false; - - if (c.IsSpaceOrTab()) + else { - if (hasOnlyWhiteSpacesSinceLastLine < 0) + if (isLineBlank && !c.IsSpaceOrTab()) { - hasOnlyWhiteSpacesSinceLastLine = 1; + isLineBlank = false; } - } - else if (c != '\n' && c != '\r' && text.PeekChar() != '\n') - { - hasOnlyWhiteSpacesSinceLastLine = 0; - } - buffer.Append(c); + buffer.Append(c); + } } } buffer.Dispose(); title = null; return false; - - ReturnValid: - title = buffer.ToString(); - return true; } public static bool TryParseTitleTrivia(ref T text, out string? title, out char enclosingCharacter) where T : ICharIterator @@ -642,88 +624,70 @@ public static bool TryParseTitleTrivia(ref T text, out string? title, out cha enclosingCharacter = c; var closingQuote = c == '(' ? ')' : c; bool hasEscape = false; - // -1: undefined - // 0: has only spaces - // 1: has other characters - int hasOnlyWhiteSpacesSinceLastLine = -1; - while (true) + bool isLineBlank = false; // the first line is never blank + while ((c = text.NextChar()) != '\0') { - c = text.NextChar(); - if (c == '\r' || c == '\n') { - if (hasOnlyWhiteSpacesSinceLastLine >= 0) + if (isLineBlank) { - if (hasOnlyWhiteSpacesSinceLastLine == 1) - { - break; - } - hasOnlyWhiteSpacesSinceLastLine = -1; + break; } + + if (hasEscape) + { + hasEscape = false; + buffer.Append('\\'); + } + buffer.Append(c); + if (c == '\r' && text.PeekChar() == '\n') { buffer.Append('\n'); text.SkipChar(); } - continue; - } - if (c == '\0') - { - break; + isLineBlank = true; } - - if (c == closingQuote) + else if (hasEscape) { - if (hasEscape) + hasEscape = false; + + if (!c.IsAsciiPunctuation()) { - buffer.Append(closingQuote); - hasEscape = false; - continue; + buffer.Append('\\'); } - // Skip last quote - text.SkipChar(); - goto ReturnValid; + buffer.Append(c); } - - if (hasEscape && !c.IsAsciiPunctuation()) + else if (c == closingQuote) { - buffer.Append('\\'); + // Skip last quote + text.SkipChar(); + title = buffer.ToString(); + return true; } - - if (c == '\\') + else if (c == '\\') { hasEscape = true; - continue; + isLineBlank = false; } - - hasEscape = false; - - if (c.IsSpaceOrTab()) + else { - if (hasOnlyWhiteSpacesSinceLastLine < 0) + if (isLineBlank && !c.IsSpaceOrTab()) { - hasOnlyWhiteSpacesSinceLastLine = 1; + isLineBlank = false; } - } - else if (c != '\n' && c != '\r' && text.PeekChar() != '\n') - { - hasOnlyWhiteSpacesSinceLastLine = 0; - } - buffer.Append(c); + buffer.Append(c); + } } } buffer.Dispose(); title = null; return false; - - ReturnValid: - title = buffer.ToString(); - return true; } public static bool TryParseUrl(T text, [NotNullWhen(true)] out string? link) where T : ICharIterator @@ -760,12 +724,15 @@ public static bool TryParseUrl(ref T text, [NotNullWhen(true)] out string? li break; } - if (hasEscape && !c.IsAsciiPunctuation()) + if (hasEscape) { - buffer.Append('\\'); + hasEscape = false; + if (!c.IsAsciiPunctuation()) + { + buffer.Append('\\'); + } } - - if (c == '\\') + else if (c == '\\') { hasEscape = true; continue; @@ -776,8 +743,6 @@ public static bool TryParseUrl(ref T text, [NotNullWhen(true)] out string? li break; } - hasEscape = false; - buffer.Append(c); } while (c != '\0'); @@ -816,20 +781,21 @@ public static bool TryParseUrl(ref T text, [NotNullWhen(true)] out string? li if (!isAutoLink) { - if (hasEscape && !c.IsAsciiPunctuation()) + if (hasEscape) { - buffer.Append('\\'); + hasEscape = false; + if (!c.IsAsciiPunctuation()) + { + buffer.Append('\\'); + } } - // If we have an escape - if (c == '\\') + else if (c == '\\') { hasEscape = true; c = text.NextChar(); continue; } - - hasEscape = false; } if (IsEndOfUri(c, isAutoLink)) @@ -907,12 +873,15 @@ public static bool TryParseUrlTrivia(ref T text, out string? link, out bool h break; } - if (hasEscape && !c.IsAsciiPunctuation()) + if (hasEscape) { - buffer.Append('\\'); + hasEscape = false; + if (!c.IsAsciiPunctuation()) + { + buffer.Append('\\'); + } } - - if (c == '\\') + else if (c == '\\') { hasEscape = true; continue; @@ -923,8 +892,6 @@ public static bool TryParseUrlTrivia(ref T text, out string? link, out bool h break; } - hasEscape = false; - buffer.Append(c); } while (c != '\0'); @@ -963,20 +930,21 @@ public static bool TryParseUrlTrivia(ref T text, out string? link, out bool h if (!isAutoLink) { - if (hasEscape && !c.IsAsciiPunctuation()) + if (hasEscape) { - buffer.Append('\\'); + hasEscape = false; + if (!c.IsAsciiPunctuation()) + { + buffer.Append('\\'); + } } - // If we have an escape - if (c == '\\') + else if (c == '\\') { hasEscape = true; c = text.NextChar(); continue; } - - hasEscape = false; } if (IsEndOfUri(c, isAutoLink)) @@ -1161,7 +1129,7 @@ public static bool TryParseLinkReferenceDefinition(ref T text, c = text.NextChar(); } - if (c != '\0' && c != '\n' && c != '\r' && text.PeekChar() != '\n') + if (c != '\0' && c != '\n' && c != '\r') { // If we were able to parse the url but the title doesn't end with space, // we are still returning a valid definition @@ -1301,7 +1269,7 @@ public static bool TryParseLinkReferenceDefinitionTrivia( c = text.NextChar(); } - if (c != '\0' && c != '\n' && c != '\r' && text.PeekChar() != '\n') + if (c != '\0' && c != '\n' && c != '\r') { // If we were able to parse the url but the title doesn't end with space, // we are still returning a valid definition diff --git a/src/Markdig/Parsers/Inlines/LinkInlineParser.cs b/src/Markdig/Parsers/Inlines/LinkInlineParser.cs index 0caa1ef4..eee94ec7 100644 --- a/src/Markdig/Parsers/Inlines/LinkInlineParser.cs +++ b/src/Markdig/Parsers/Inlines/LinkInlineParser.cs @@ -137,6 +137,9 @@ private bool ProcessLinkReference( if (linkRef.CreateLinkInline != null) { link = linkRef.CreateLinkInline(state, linkRef, parent.FirstChild); + link.Span = new SourceSpan(parent.Span.Start, endPosition); + link.Line = parent.Line; + link.Column = parent.Column; } // Create a default link if the callback was not found @@ -145,8 +148,8 @@ private bool ProcessLinkReference( // Inline Link var linkInline = new LinkInline() { - Url = HtmlHelper.Unescape(linkRef.Url), - Title = HtmlHelper.Unescape(linkRef.Title), + Url = HtmlHelper.Unescape(linkRef.Url, removeBackSlash: false), + Title = HtmlHelper.Unescape(linkRef.Title, removeBackSlash: false), Label = label, LabelSpan = labelSpan, UrlSpan = linkRef.UrlSpan, @@ -256,8 +259,8 @@ private bool TryProcessLinkOrImage(InlineProcessor inlineState, ref StringSlice // Inline Link link = new LinkInline() { - Url = HtmlHelper.Unescape(url), - Title = HtmlHelper.Unescape(title), + Url = HtmlHelper.Unescape(url, removeBackSlash: false), + Title = HtmlHelper.Unescape(title, removeBackSlash: false), IsImage = openParent.IsImage, LabelSpan = openParent.LabelSpan, UrlSpan = inlineState.GetSourcePositionFromLocalSpan(linkSpan), @@ -382,11 +385,11 @@ private bool TryProcessLinkOrImage(InlineProcessor inlineState, ref StringSlice return new LinkInline() { TriviaBeforeUrl = wsBeforeLink, - Url = HtmlHelper.Unescape(url), + Url = HtmlHelper.Unescape(url, removeBackSlash: false), UnescapedUrl = unescapedUrl, UrlHasPointyBrackets = urlHasPointyBrackets, TriviaAfterUrl = wsAfterLink, - Title = HtmlHelper.Unescape(title), + Title = HtmlHelper.Unescape(title, removeBackSlash: false), UnescapedTitle = unescapedTitle, TitleEnclosingCharacter = titleEnclosingCharacter, TriviaAfterTitle = wsAfterTitle,