Upstream xoofx/markdig state of the touched files before 67a2e4948f

This commit is contained in:
upstream committed 2026-09-04 03:24:13 +02:00
1 parent 0e598ccf6f
commit 93406f115e
4 files changed
+745

No files matched your search

+24
View File
@@ -0,0 +1,24 @@
using Markdig.Extensions.AutoLinks;
namespace Markdig.Tests;
[TestFixture]
public class TestAutoLinks
{
[Test]
[TestCase("https://localhost", "<p><a href=\"https://localhost\">https://localhost</a></p>")]
[TestCase("http://localhost", "<p><a href=\"http://localhost\">http://localhost</a></p>")]
[TestCase("https://l", "<p><a href=\"https://l\">https://l</a></p>")]
[TestCase("www.l", "<p><a href=\"http://www.l\">www.l</a></p>")]
[TestCase("https://localhost:5000", "<p><a href=\"https://localhost:5000\">https://localhost:5000</a></p>")]
[TestCase("www.l:5000", "<p><a href=\"http://www.l:5000\">www.l:5000</a></p>")]
public void TestLinksWithAllowDomainWithoutPeriod(string markdown, string expected)
{
var pipeline = new MarkdownPipelineBuilder()
.UseAutoLinks(new AutoLinkOptions { AllowDomainWithoutPeriod = true })
.Build();
var html = Markdown.ToHtml(markdown, pipeline);
Assert.That(html, Is.EqualTo(expected).IgnoreWhiteSpace);
}
}
@@ -0,0 +1,27 @@
// Copyright (c) Alexandre Mutel. All rights reserved.
// This file is licensed under the BSD-Clause 2 license.
// See the license.txt file in the project root for more information.
using Markdig.Syntax;
namespace Markdig.Extensions.AutoIdentifiers;
/// <summary>
/// A link reference definition to a <see cref="HeadingBlock"/> stored at the <see cref="MarkdownDocument"/> level.
/// </summary>
/// <seealso cref="LinkReferenceDefinition" />
public class HeadingLinkReferenceDefinition : LinkReferenceDefinition
{
/// <summary>
/// Initializes a new instance of the HeadingLinkReferenceDefinition class.
/// </summary>
public HeadingLinkReferenceDefinition(HeadingBlock headling)
{
Heading = headling;
}
/// <summary>
/// Gets or sets the heading related to this link reference definition.
/// </summary>
public HeadingBlock Heading { get; set; }
}
@@ -0,0 +1,450 @@
// Copyright (c) Alexandre Mutel. All rights reserved.
// This file is licensed under the BSD-Clause 2 license.
// See the license.txt file in the project root for more information.
using Markdig.Helpers;
using Markdig.Renderers.Html;
using Markdig.Syntax;
using Markdig.Syntax.Inlines;
namespace Markdig.Parsers.Inlines;
/// <summary>
/// An inline parser for <see cref="LinkInline"/>.
/// </summary>
/// <seealso cref="InlineParser" />
public class LinkInlineParser : InlineParser
{
/// <summary>
/// Initializes a new instance of the <see cref="LinkInlineParser"/> class.
/// </summary>
public LinkInlineParser() : this(new LinkOptions())
{
}
/// <summary>
/// Initializes a new instance of the <see cref="LinkInlineParser"/> class.
/// </summary>
public LinkInlineParser(LinkOptions options)
{
Options = options ?? throw new ArgumentNullException(nameof(options));
OpeningCharacters = ['[', ']', '!'];
}
/// <summary>
/// Gets or sets the options.
/// </summary>
public readonly LinkOptions Options;
/// <summary>
/// Attempts to match the parser at the current position.
/// </summary>
public override bool Match(InlineProcessor processor, ref StringSlice slice)
{
// The following methods are inspired by the "An algorithm for parsing nested emphasis and links"
// at the end of the CommonMark specs.
var c = slice.CurrentChar;
var startPosition = processor.GetSourcePosition(slice.Start, out int line, out int column);
bool isImage = false;
if (c == '!')
{
isImage = true;
c = slice.NextChar();
if (c != '[')
{
return false;
}
}
string? label;
SourceSpan labelWithTriviaSpan = SourceSpan.Empty;
switch (c)
{
case '[':
// If this is not an image, we may have a reference link shortcut
// so we try to resolve it here
var saved = slice;
SourceSpan labelSpan;
// If the label is followed by either a ( or a [, this is not a shortcut
if (processor.TrackTrivia)
{
if (LinkHelper.TryParseLabelTrivia(ref slice, out label, out labelSpan))
{
labelWithTriviaSpan.Start = labelSpan.Start; // skip opening [
labelWithTriviaSpan.End = labelSpan.End; // skip closing ]
if (!processor.Document.ContainsLinkReferenceDefinition(label))
{
label = null;
}
}
}
else
{
if (LinkHelper.TryParseLabel(ref slice, out label, out labelSpan))
{
if (!processor.Document.ContainsLinkReferenceDefinition(label))
{
label = null;
}
}
}
slice = saved;
// Else we insert a LinkDelimiter
slice.SkipChar();
var linkDelimiter = new LinkDelimiterInline(this)
{
Type = DelimiterType.Open,
Label = label,
LabelSpan = processor.GetSourcePositionFromLocalSpan(labelSpan),
IsImage = isImage,
Span = new SourceSpan(startPosition, processor.GetSourcePosition(slice.Start - 1)),
Line = line,
Column = column
};
if (processor.TrackTrivia)
{
linkDelimiter.LabelWithTrivia = new StringSlice(slice.Text, labelWithTriviaSpan.Start, labelWithTriviaSpan.End);
}
processor.Inline = linkDelimiter;
return true;
case ']':
slice.SkipChar();
if (processor.Inline != null)
{
if (TryProcessLinkOrImage(processor, ref slice))
{
return true;
}
}
// If we don’t find one, we return a literal slice node ].
// (Done after by the LiteralInline parser)
return false;
}
// We don't have an emphasis
return false;
}
private bool ProcessLinkReference(
InlineProcessor state,
StringSlice text,
string label,
SourceSpan labelWithriviaSpan,
bool isShortcut,
SourceSpan labelSpan,
LinkDelimiterInline parent,
int endPosition,
LocalLabel localLabel)
{
if (!state.Document.TryGetLinkReferenceDefinition(label, out LinkReferenceDefinition? linkRef))
{
return false;
}
Inline? link = null;
// Try to use a callback directly defined on the LinkReferenceDefinition
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
if (link is null)
{
// Inline Link
var linkInline = new LinkInline()
{
Url = HtmlHelper.Unescape(linkRef.Url, removeBackSlash: false),
Title = HtmlHelper.Unescape(linkRef.Title, removeBackSlash: false),
Label = label,
LabelSpan = labelSpan,
UrlSpan = linkRef.UrlSpan,
IsImage = parent.IsImage,
IsShortcut = isShortcut,
Reference = linkRef,
Span = new SourceSpan(parent.Span.Start, endPosition),
Line = parent.Line,
Column = parent.Column,
};
if (state.TrackTrivia)
{
linkInline.LabelWithTrivia = new StringSlice(text.Text, labelWithriviaSpan.Start, labelWithriviaSpan.End);
linkInline.LinkRefDefLabel = linkRef.Label;
linkInline.LinkRefDefLabelWithTrivia = linkRef.LabelWithTrivia;
linkInline.LocalLabel = localLabel;
}
if (Options.OpenInNewWindow)
{
linkInline.GetAttributes().AddPropertyIfNotExist("target", "_blank");
}
link = linkInline;
}
if (link is ContainerInline containerLink)
{
var child = parent.FirstChild;
if (child is null)
{
child = new LiteralInline()
{
Content = StringSlice.Empty,
IsClosed = true,
// Not exact but we leave it like this
Span = parent.Span,
Line = parent.Line,
Column = parent.Column,
};
containerLink.AppendChild(child);
}
else
{
// Insert all child into the link
while (child != null)
{
var next = child.NextSibling;
child.Remove();
containerLink.AppendChild(child);
child = next;
}
}
}
link.IsClosed = true;
// Process emphasis delimiters
state.PostProcessInlines(0, link, null, false);
state.Inline = link;
return true;
}
private bool TryProcessLinkOrImage(InlineProcessor inlineState, ref StringSlice text)
{
LinkDelimiterInline? openParent = inlineState.Inline!.FirstParentOfType<LinkDelimiterInline>();
if (openParent is null)
{
return false;
}
// If we do find one, but it’s not active,
// we remove the inactive delimiter from the stack,
// and return a literal text node ].
if (!openParent.IsActive)
{
inlineState.Inline = new LiteralInline()
{
Content = new StringSlice("["),
Span = openParent.Span,
Line = openParent.Line,
Column = openParent.Column,
};
openParent.ReplaceBy(inlineState.Inline);
return false;
}
// If we find one and it’s active,
// then we parse ahead to see if we have
// an inline link/image, reference link/image,
// compact reference link/image,
// or shortcut reference link/image
var parentDelimiter = openParent.Parent;
var savedText = text;
if (text.CurrentChar == '(')
{
LinkInline? link = null;
if (inlineState.TrackTrivia)
{
link = TryParseInlineLinkTrivia(ref text, inlineState, openParent);
}
else
{
if (LinkHelper.TryParseInlineLink(ref text, out string? url, out string? title, out SourceSpan linkSpan, out SourceSpan titleSpan))
{
// Inline Link
link = new LinkInline()
{
Url = HtmlHelper.Unescape(url, removeBackSlash: false),
Title = title is null ? null : HtmlHelper.Unescape(title, removeBackSlash: false),
IsImage = openParent.IsImage,
LabelSpan = openParent.LabelSpan,
UrlSpan = inlineState.GetSourcePositionFromLocalSpan(linkSpan),
TitleSpan = inlineState.GetSourcePositionFromLocalSpan(titleSpan),
Span = new SourceSpan(openParent.Span.Start, inlineState.GetSourcePosition(text.Start - 1)),
Line = openParent.Line,
Column = openParent.Column,
};
}
}
if (link is not null)
{
openParent.ReplaceBy(link);
// Notifies processor as we are creating an inline locally
inlineState.Inline = link;
// Process emphasis delimiters
inlineState.PostProcessInlines(0, link, null, false);
// If we have a link (and not an image),
// we also set all [ delimiters before the opening delimiter to inactive.
// (This will prevent us from getting links within links.)
if (!openParent.IsImage)
{
MarkParentAsInactive(parentDelimiter);
}
link.IsClosed = true;
return true;
}
text = savedText;
}
var labelSpan = SourceSpan.Empty;
string? label = null;
bool isLabelSpanLocal = true;
bool isShortcut = false;
LocalLabel localLabel = LocalLabel.Local;
// Handle Collapsed links
if (text.CurrentChar == '[')
{
if (text.PeekChar() == ']')
{
label = openParent.Label;
labelSpan = openParent.LabelSpan;
isLabelSpanLocal = false;
localLabel = LocalLabel.Empty;
text.SkipChar(); // Skip [
text.SkipChar(); // Skip ]
}
}
else
{
localLabel = LocalLabel.None;
label = openParent.Label;
isShortcut = true;
}
if (label != null || LinkHelper.TryParseLabelTrivia(ref text, true, out label, out labelSpan))
{
var labelWithTrivia = new SourceSpan(labelSpan.Start, labelSpan.End);
if (isLabelSpanLocal)
{
labelSpan = inlineState.GetSourcePositionFromLocalSpan(labelSpan);
}
if (ProcessLinkReference(inlineState, text, label!, labelWithTrivia, isShortcut, labelSpan, openParent, inlineState.GetSourcePosition(text.Start - 1), localLabel))
{
// Remove the open parent
openParent.Remove();
if (!openParent.IsImage)
{
MarkParentAsInactive(parentDelimiter);
}
return true;
}
else if (text.CurrentChar != ']' && text.CurrentChar != '[')
{
return false;
}
}
// We have a nested [ ]
// firstParent.Remove();
// The opening [ will be transformed to a literal followed by all the children of the [
var literal = new LiteralInline()
{
Span = openParent.Span,
Content = new StringSlice(openParent.IsImage ? "![" : "[")
};
inlineState.Inline = openParent.ReplaceBy(literal);
return false;
static LinkInline? TryParseInlineLinkTrivia(ref StringSlice text, InlineProcessor inlineState, LinkDelimiterInline openParent)
{
if (LinkHelper.TryParseInlineLinkTrivia(
ref text,
out string? url,
out SourceSpan unescapedUrlSpan,
out string? title,
out SourceSpan unescapedTitleSpan,
out char titleEnclosingCharacter,
out SourceSpan linkSpan,
out SourceSpan titleSpan,
out SourceSpan triviaBeforeLink,
out SourceSpan triviaAfterLink,
out SourceSpan triviaAfterTitle,
out bool urlHasPointyBrackets))
{
var wsBeforeLink = new StringSlice(text.Text, triviaBeforeLink.Start, triviaBeforeLink.End);
var wsAfterLink = new StringSlice(text.Text, triviaAfterLink.Start, triviaAfterLink.End);
var wsAfterTitle = new StringSlice(text.Text, triviaAfterTitle.Start, triviaAfterTitle.End);
var unescapedUrl = new StringSlice(text.Text, unescapedUrlSpan.Start, unescapedUrlSpan.End);
var unescapedTitle = new StringSlice(text.Text, unescapedTitleSpan.Start, unescapedTitleSpan.End);
return new LinkInline()
{
TriviaBeforeUrl = wsBeforeLink,
Url = HtmlHelper.Unescape(url, removeBackSlash: false),
UnescapedUrl = unescapedUrl,
UrlHasPointyBrackets = urlHasPointyBrackets,
TriviaAfterUrl = wsAfterLink,
Title = HtmlHelper.Unescape(title, removeBackSlash: false),
UnescapedTitle = unescapedTitle,
TitleEnclosingCharacter = titleEnclosingCharacter,
TriviaAfterTitle = wsAfterTitle,
IsImage = openParent.IsImage,
LabelSpan = openParent.LabelSpan,
UrlSpan = inlineState.GetSourcePositionFromLocalSpan(linkSpan),
TitleSpan = inlineState.GetSourcePositionFromLocalSpan(titleSpan),
Span = new SourceSpan(openParent.Span.Start, inlineState.GetSourcePosition(text.Start - 1)),
Line = openParent.Line,
Column = openParent.Column,
};
}
return null;
}
}
private static void MarkParentAsInactive(Inline? inline)
{
while (inline != null)
{
if (inline is LinkDelimiterInline linkInline)
{
if (linkInline.IsImage)
{
break;
}
linkInline.IsActive = false;
}
inline = inline.Parent;
}
}
}
@@ -0,0 +1,244 @@
// Copyright (c) Alexandre Mutel. All rights reserved.
// This file is licensed under the BSD-Clause 2 license.
// See the license.txt file in the project root for more information.
using System.Diagnostics.CodeAnalysis;
using Markdig.Helpers;
using Markdig.Parsers;
using Markdig.Syntax.Inlines;
namespace Markdig.Syntax;
/// <summary>
/// A link reference definition (Section 4.7 CommonMark specs)
/// </summary>
/// <seealso cref="LeafBlock" />
public class LinkReferenceDefinition : LeafBlock
{
private TriviaProperties? _trivia => TryGetDerivedTrivia<TriviaProperties>();
private TriviaProperties Trivia => GetOrSetDerivedTrivia<TriviaProperties>();
/// <summary>
/// Creates an inline link for the specified <see cref="LinkReferenceDefinition"/>.
/// </summary>
/// <param name="inlineState">State of the inline.</param>
/// <param name="linkRef">The link reference.</param>
/// <param name="child">The child.</param>
/// <returns>An inline link or null to use the default implementation</returns>
public delegate Inline CreateLinkInlineDelegate(InlineProcessor inlineState, LinkReferenceDefinition linkRef, Inline? child = null);
/// <summary>
/// Initializes a new instance of the <see cref="LinkReferenceDefinition"/> class.
/// </summary>
public LinkReferenceDefinition() : base(null)
{
IsOpen = false;
}
/// <summary>
/// Initializes a new instance of the <see cref="LinkReferenceDefinition"/> class.
/// </summary>
/// <param name="label">The label.</param>
/// <param name="url">The URL.</param>
/// <param name="title">The title.</param>
public LinkReferenceDefinition(string? label, string? url, string? title) : this()
{
Label = label;
Url = url;
Title = title;
}
/// <summary>
/// Gets or sets the label. Text is normalized according to spec.
/// </summary>
/// https://spec.commonmark.org/0.29/#matches
public string? Label { get; set; }
/// <summary>
/// The label span
/// </summary>
public SourceSpan LabelSpan;
/// <summary>
/// Non-normalized Label (includes trivia)
/// Trivia: only parsed when <see cref="MarkdownPipeline.TrackTrivia"/> is enabled, otherwise
/// <see cref="StringSlice.Empty"/>.
/// </summary>
public StringSlice LabelWithTrivia { get => _trivia?.LabelWithTrivia ?? StringSlice.Empty; set => Trivia.LabelWithTrivia = value; }
/// <summary>
/// Whitespace before the <see cref="Url"/>.
/// Trivia: only parsed when <see cref="MarkdownPipeline.TrackTrivia"/> is enabled, otherwise
/// <see cref="StringSlice.Empty"/>.
/// </summary>
public StringSlice TriviaBeforeUrl { get => _trivia?.TriviaBeforeUrl ?? StringSlice.Empty; set => Trivia.TriviaBeforeUrl = value; }
/// <summary>
/// Gets or sets the URL.
/// </summary>
public string? Url { get; set; }
/// <summary>
/// The URL span
/// </summary>
public SourceSpan UrlSpan;
/// <summary>
/// Non-normalized <see cref="Url"/>.
/// Trivia: only parsed when <see cref="MarkdownPipeline.TrackTrivia"/> is enabled, otherwise
/// <see cref="StringSlice.Empty"/>.
/// </summary>
public StringSlice UnescapedUrl { get => _trivia?.UnescapedUrl ?? StringSlice.Empty; set => Trivia.UnescapedUrl = value; }
/// <summary>
/// True when the <see cref="Url"/> is enclosed in point brackets in the source document.
/// Trivia: only parsed when <see cref="MarkdownPipeline.TrackTrivia"/> is enabled, otherwise
/// false.
/// </summary>
public bool UrlHasPointyBrackets { get => _trivia?.UrlHasPointyBrackets ?? false; set => Trivia.UrlHasPointyBrackets = value; }
/// <summary>
/// gets or sets the whitespace before a <see cref="Title"/>.
/// Trivia: only parsed when <see cref="MarkdownPipeline.TrackTrivia"/> is enabled, otherwise
/// <see cref="StringSlice.Empty"/>.
/// </summary>
public StringSlice TriviaBeforeTitle { get => _trivia?.TriviaBeforeTitle ?? StringSlice.Empty; set => Trivia.TriviaBeforeTitle = value; }
/// <summary>
/// Gets or sets the title.
/// </summary>
public string? Title { get; set; }
/// <summary>
/// The title span
/// </summary>
public SourceSpan TitleSpan;
/// <summary>
/// Non-normalized <see cref="Title"/>.
/// Trivia: only parsed when <see cref="MarkdownPipeline.TrackTrivia"/> is enabled, otherwise
/// <see cref="StringSlice.Empty"/>.
/// </summary>
public StringSlice UnescapedTitle { get => _trivia?.UnescapedTitle ?? StringSlice.Empty; set => Trivia.UnescapedTitle = value; }
/// <summary>
/// Gets or sets the character the <see cref="Title"/> is enclosed in.
/// Trivia: only parsed when <see cref="MarkdownPipeline.TrackTrivia"/> is enabled, otherwise \0.
/// </summary>
public char TitleEnclosingCharacter { get => _trivia?.TitleEnclosingCharacter ?? default; set => Trivia.TitleEnclosingCharacter = value; }
/// <summary>
/// Gets or sets the create link inline callback for this instance.
/// </summary>
/// <remarks>
/// This callback is called when an inline link is matching this reference definition.
/// </remarks>
public CreateLinkInlineDelegate? CreateLinkInline { get; set; }
/// <summary>
/// Tries to the parse the specified text into a definition.
/// </summary>
/// <typeparam name="T">Type of the text</typeparam>
/// <param name="text">The text.</param>
/// <param name="block">The block.</param>
/// <returns><c>true</c> if parsing is successful; <c>false</c> otherwise</returns>
public static bool TryParse<T>(ref T text, [NotNullWhen(true)] out LinkReferenceDefinition? block) where T : ICharIterator
{
block = null;
var startSpan = text.Start;
if (!LinkHelper.TryParseLinkReferenceDefinition(ref text, out string? label, out string? url, out string? title, out SourceSpan labelSpan, out SourceSpan urlSpan, out SourceSpan titleSpan))
{
return false;
}
block = new LinkReferenceDefinition(label, url, title)
{
LabelSpan = labelSpan,
UrlSpan = urlSpan,
TitleSpan = titleSpan,
Span = new SourceSpan(startSpan, titleSpan.End > 0 ? titleSpan.End: urlSpan.End)
};
return true;
}
/// <summary>
/// Tries to the parse the specified text into a definition.
/// </summary>
/// <typeparam name="T">Type of the text</typeparam>
/// <param name="text">The text.</param>
/// <param name="block">The block.</param>
/// <param name="triviaBeforeLabel"></param>
/// <param name="labelWithTrivia"></param>
/// <param name="triviaBeforeUrl"></param>
/// <param name="unescapedUrl"></param>
/// <param name="triviaBeforeTitle"></param>
/// <param name="unescapedTitle"></param>
/// <param name="triviaAfterTitle"></param>
/// <returns><c>true</c> if parsing is successful; <c>false</c> otherwise</returns>
public static bool TryParseTrivia<T>(
ref T text,
[NotNullWhen(true)] out LinkReferenceDefinition? block,
out SourceSpan triviaBeforeLabel,
out SourceSpan labelWithTrivia,
out SourceSpan triviaBeforeUrl,
out SourceSpan unescapedUrl,
out SourceSpan triviaBeforeTitle,
out SourceSpan unescapedTitle,
out SourceSpan triviaAfterTitle) where T : ICharIterator
{
block = null;
var startSpan = text.Start;
if (!LinkHelper.TryParseLinkReferenceDefinitionTrivia(
ref text,
out triviaBeforeLabel,
out string? label,
out labelWithTrivia,
out triviaBeforeUrl,
out string? url,
out unescapedUrl,
out bool urlHasPointyBrackets,
out triviaBeforeTitle,
out string? title,
out unescapedTitle,
out char titleEnclosingCharacter,
out NewLine newLine,
out triviaAfterTitle,
out SourceSpan labelSpan,
out SourceSpan urlSpan,
out SourceSpan titleSpan))
{
return false;
}
block = new LinkReferenceDefinition(label, url, title)
{
UrlHasPointyBrackets = urlHasPointyBrackets,
TitleEnclosingCharacter = titleEnclosingCharacter,
//LabelWithWhitespace = labelWithWhitespace,
LabelSpan = labelSpan,
UrlSpan = urlSpan,
//UnescapedUrl = unescapedUrl,
//UnescapedTitle = unescapedTitle,
TitleSpan = titleSpan,
Span = new SourceSpan(startSpan, titleSpan.End > 0 ? titleSpan.End : urlSpan.End),
NewLine = newLine,
};
return true;
}
private sealed class TriviaProperties
{
public StringSlice LabelWithTrivia;
public StringSlice TriviaBeforeUrl;
public StringSlice UnescapedUrl;
public bool UrlHasPointyBrackets;
public StringSlice TriviaBeforeTitle;
public StringSlice UnescapedTitle;
public char TitleEnclosingCharacter;
}
}