Upstream xoofx/markdig state of the touched files before 51b0655f4c

This commit is contained in:
upstream committed 2026-09-02 11:59:02 +02:00
commit 165ebe1cf1
1 file changed
+455
@@ -0,0 +1,455 @@
// 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;
using System.Runtime.CompilerServices;
using System.Text;
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="EmphasisInline"/>.
/// </summary>
/// <seealso cref="InlineParser" />
/// <seealso cref="IPostInlineProcessor" />
public class EmphasisInlineParser : InlineParser, IPostInlineProcessor
{
private CharacterMap<EmphasisDescriptor>? emphasisMap;
private readonly DelimitersObjectCache inlinesCache = new();
/// <summary>
/// Represents the EmphasisInline type.
/// </summary>
public delegate EmphasisInline? TryCreateEmphasisInlineDelegate(char emphasisChar, int delimiterCount);
/// <summary>
/// Initializes a new instance of the <see cref="EmphasisInlineParser"/> class.
/// </summary>
public EmphasisInlineParser()
{
EmphasisDescriptors =
[
new EmphasisDescriptor('*', 1, 2, true),
new EmphasisDescriptor('_', 1, 2, false)
];
}
/// <summary>
/// Gets the emphasis descriptors.
/// </summary>
public List<EmphasisDescriptor> EmphasisDescriptors { get; }
/// <summary>
/// Gets or toggles whether the emphasis parser should be CJK-friendly.
/// </summary>
/// <seealso href="https://github.com/tats-u/markdown-cjk-friendly"/>
public bool CjkFriendlyEmphasis { get; set; } = false;
/// <summary>
/// Determines whether this parser is using the specified character as an emphasis delimiter.
/// </summary>
/// <param name="c">The character to look for.</param>
/// <returns><c>true</c> if this parser is using the specified character as an emphasis delimiter; otherwise <c>false</c></returns>
public bool HasEmphasisChar(char c)
{
foreach (var emphasis in EmphasisDescriptors)
{
if (emphasis.Character == c)
{
return true;
}
}
return false;
}
/// <summary>
/// Gets or sets the try create emphasis inline list.
/// </summary>
public readonly List<TryCreateEmphasisInlineDelegate> TryCreateEmphasisInlineList = [];
/// <summary>
/// Performs the initialize operation.
/// </summary>
public override void Initialize()
{
OpeningCharacters = new char[EmphasisDescriptors.Count];
var tempMap = new List<KeyValuePair<char, EmphasisDescriptor>>();
for (int i = 0; i < EmphasisDescriptors.Count; i++)
{
var emphasis = EmphasisDescriptors[i];
if (Array.IndexOf(OpeningCharacters, emphasis.Character) >= 0)
{
ThrowHelper.InvalidOperationException(
$"The character `{emphasis.Character}` is already used by another emphasis descriptor");
}
OpeningCharacters[i] = emphasis.Character;
tempMap.Add(new KeyValuePair<char, EmphasisDescriptor>(emphasis.Character, emphasis));
}
emphasisMap = new CharacterMap<EmphasisDescriptor>(tempMap);
}
/// <summary>
/// Performs the post process operation.
/// </summary>
public bool PostProcess(InlineProcessor state, Inline? root, Inline? lastChild, int postInlineProcessorIndex, bool isFinalProcessing)
{
if (root is null || !root.IsContainerInline)
{
return true;
}
ContainerInline container = Unsafe.As<ContainerInline>(root);
List<EmphasisDelimiterInline>? delimiters = null;
if (container is EmphasisDelimiterInline emphasisDelimiter)
{
delimiters = inlinesCache.Get();
delimiters.Add(emphasisDelimiter);
}
// Collect all EmphasisDelimiterInline by searching from the root container
var child = container.FirstChild;
while (child != null)
{
// Stop the search on the delimitation child
if (child == lastChild)
{
break;
}
if (child.IsContainer && child is DelimiterInline delimiterInline)
{
// If we have a delimiter, we search into it as we should have a tree of EmphasisDelimiterInline
if (delimiterInline is EmphasisDelimiterInline delimiter)
{
delimiters ??= inlinesCache.Get();
delimiters.Add(delimiter);
}
// Follow DelimiterInline (EmphasisDelimiter, TableDelimiter...)
// If the delimiter has IsClosed=true (e.g., pipe table delimiter), it has no children
// In that case, continue to next sibling instead of stopping
var firstChild = delimiterInline.FirstChild;
child = firstChild ?? delimiterInline.NextSibling;
}
else
{
child = child.NextSibling;
}
}
if (delimiters != null)
{
ProcessEmphasis(state, delimiters);
inlinesCache.Release(delimiters);
}
return true;
}
/// <summary>
/// Attempts to match the parser at the current position.
/// </summary>
public override bool Match(InlineProcessor processor, ref StringSlice slice)
{
// First, some definitions.
// A delimiter run is a sequence of one or more delimiter characters that is not preceded or followed by the same delimiter character
// The amount of delimiter characters in the delimiter run may exceed emphasisDesc.MaximumCount, as that is handeled in `ProcessEmphasis`
var delimiterChar = slice.CurrentChar;
var emphasisDesc = emphasisMap![delimiterChar]!;
Rune pc = (Rune)0;
Rune twoPreviousChar = default;
if (processor.Inline is HtmlEntityInline htmlEntityInline)
{
if (htmlEntityInline.Transcoded.Length > 0)
{
var transcoded = htmlEntityInline.Transcoded.Text.AsSpan(htmlEntityInline.Transcoded.Start, htmlEntityInline.Transcoded.Length);
Rune.DecodeLastFromUtf16(transcoded, out pc, out int charsConsumed);
if (CjkFriendlyEmphasis && charsConsumed < transcoded.Length)
{
Rune.DecodeLastFromUtf16(transcoded.Slice(0, transcoded.Length - charsConsumed), out twoPreviousChar, out _);
}
}
}
if (pc.Value == 0)
{
pc = slice.PeekRuneExtra(-1);
if (CjkFriendlyEmphasis)
{
// This cannot be a delegate (Func<Rune>?) because slice is a reference
twoPreviousChar = slice.PeekRuneExtra(-1 - pc.Utf16SequenceLength);
}
// delimiterChar is BMP, so slice.PeekCharExtra(-2) is (a part of) the character two positions back.
if (pc == (Rune)delimiterChar && slice.PeekCharExtra(-2) != '\\')
{
// If we get here, we determined that either:
// a) there weren't enough delimiters in the delimiter run to satisfy the MinimumCount condition
// b) the previous character couldn't open/close
return false;
}
}
var startPosition = slice.Start;
int delimiterCount = slice.CountAndSkipChar(delimiterChar);
// If the emphasis doesn't have the minimum required character
if (delimiterCount < emphasisDesc.MinimumCount)
{
return false;
}
Rune c = slice.CurrentRune;
// The following character is actually an entity, we need to decode it
if (HtmlEntityParser.TryParse(ref slice, out string? htmlString, out int htmlLength))
{
// Note: c is U+FFFD when decode error
Rune.DecodeFromUtf16(htmlString, out c, out _);
}
bool canOpen;
bool canClose;
// Calculate Open-Close for current character
if (CjkFriendlyEmphasis)
{
CharHelper.CheckOpenCloseDelimiterCjkFriendly(pc, c, twoPreviousChar, emphasisDesc.EnableWithinWord, out canOpen, out canClose);
}
else
{
CharHelper.CheckOpenCloseDelimiter(pc, c, emphasisDesc.EnableWithinWord, out canOpen, out canClose);
}
// We have potentially an open or close emphasis
if (canOpen || canClose)
{
var delimiterType = DelimiterType.Undefined;
if (canOpen) delimiterType |= DelimiterType.Open;
if (canClose) delimiterType |= DelimiterType.Close;
var delimiter = new EmphasisDelimiterInline(this, emphasisDesc, new StringSlice(slice.Text, startPosition, slice.Start - 1))
{
DelimiterCount = delimiterCount,
Type = delimiterType,
Span = new SourceSpan(processor.GetSourcePosition(startPosition, out int line, out int column), processor.GetSourcePosition(slice.Start - 1)),
Column = column,
Line = line,
};
processor.Inline = delimiter;
return true;
}
// We don't have an emphasis
return false;
}
private void ProcessEmphasis(InlineProcessor processor, List<EmphasisDelimiterInline> delimiters)
{
// The following method is inspired by the "An algorithm for parsing nested emphasis and links"
// at the end of the CommonMark specs.
// TODO: Benchmark difference between using List and LinkedList here since there could be a few Remove calls
// Move current_position forward in the delimiter stack (if needed) until
// we find the first potential closer with delimiter * or _. (This will be the potential closer closest to the beginning of the input – the first one in parse order.)
for (int i = 0; i < delimiters.Count; i++)
{
var closeDelimiter = delimiters[i];
// Skip delimiters not supported by this instance
EmphasisDescriptor? emphasisDesc = emphasisMap![closeDelimiter.DelimiterChar];
if (emphasisDesc is null)
{
continue;
}
if ((closeDelimiter.Type & DelimiterType.Close) != 0)
{
while (closeDelimiter.DelimiterCount >= emphasisDesc.MinimumCount)
{
// Now, look back in the stack (staying above stack_bottom and the openers_bottom for this delimiter type)
// for the first matching potential opener (“matching” means same delimiter).
EmphasisDelimiterInline? openDelimiter = null;
int openDelimiterIndex = -1;
for (int j = i - 1; j >= 0; j--)
{
var previousOpenDelimiter = delimiters[j];
var isOddMatch = ((closeDelimiter.Type & DelimiterType.Open) != 0 || (previousOpenDelimiter.Type & DelimiterType.Close) != 0) &&
previousOpenDelimiter.DelimiterCount != closeDelimiter.DelimiterCount &&
(previousOpenDelimiter.DelimiterCount + closeDelimiter.DelimiterCount) % 3 == 0 &&
(previousOpenDelimiter.DelimiterCount % 3 != 0 || closeDelimiter.DelimiterCount % 3 != 0);
if (previousOpenDelimiter.DelimiterChar == closeDelimiter.DelimiterChar &&
(previousOpenDelimiter.Type & DelimiterType.Open) != 0 &&
previousOpenDelimiter.DelimiterCount >= emphasisDesc.MinimumCount && !isOddMatch)
{
openDelimiter = previousOpenDelimiter;
openDelimiterIndex = j;
break;
}
}
if (openDelimiter != null)
{
process_delims:
Debug.Assert(openDelimiter.DelimiterCount >= emphasisDesc.MinimumCount, "Extra emphasis should have been discarded by now");
Debug.Assert(closeDelimiter.DelimiterCount >= emphasisDesc.MinimumCount, "Extra emphasis should have been discarded by now");
int delimiterDelta = Math.Min(Math.Min(openDelimiter.DelimiterCount, closeDelimiter.DelimiterCount), emphasisDesc.MaximumCount);
// Insert an emph or strong emph node accordingly, after the text node corresponding to the opener.
EmphasisInline? emphasis = null;
// Go in backwards order to give priority to newer delegates
for (int delegateIndex = TryCreateEmphasisInlineList.Count - 1; delegateIndex >= 0; delegateIndex--)
{
emphasis = TryCreateEmphasisInlineList[delegateIndex].Invoke(closeDelimiter.DelimiterChar, delimiterDelta);
if (emphasis != null) break;
}
emphasis ??= new EmphasisInline
{
DelimiterChar = closeDelimiter.DelimiterChar,
DelimiterCount = delimiterDelta
};
// Update position for emphasis
var openDelimitercount = openDelimiter.DelimiterCount;
var closeDelimitercount = closeDelimiter.DelimiterCount;
emphasis!.Span.Start = openDelimiter.Span.Start + openDelimitercount - delimiterDelta;
emphasis.Line = openDelimiter.Line;
emphasis.Column = openDelimiter.Column + openDelimitercount - delimiterDelta;
emphasis.Span.End = closeDelimiter.Span.End - closeDelimitercount + delimiterDelta;
openDelimiter.Span.End -= delimiterDelta;
openDelimiter.Content.End -= delimiterDelta;
closeDelimiter.Content.Start += delimiterDelta;
closeDelimiter.Span.Start += delimiterDelta;
closeDelimiter.Column += delimiterDelta;
openDelimiter.DelimiterCount -= delimiterDelta;
closeDelimiter.DelimiterCount -= delimiterDelta;
var embracer = (ContainerInline)openDelimiter;
// Copy attributes attached to delimiter to the emphasis
var attributes = closeDelimiter.TryGetAttributes();
if (attributes != null)
{
emphasis.SetAttributes(attributes);
}
// Embrace all delimiters
embracer.EmbraceChildrenBy(emphasis);
// Remove any intermediate emphasis
for (int k = i - 1; k >= openDelimiterIndex + 1; k--)
{
var literalDelimiter = delimiters[k];
literalDelimiter.ReplaceBy(literalDelimiter.AsLiteralInline());
delimiters.RemoveAt(k);
i--;
}
if (closeDelimiter.DelimiterCount == 0)
{
var newParent = openDelimiter.DelimiterCount > 0 ? emphasis : emphasis.Parent!;
closeDelimiter.MoveChildrenAfter(newParent);
closeDelimiter.Remove();
delimiters.RemoveAt(i);
i--;
// Remove the open delimiter if it is also empty
if (openDelimiter.DelimiterCount == 0)
{
openDelimiter.MoveChildrenAfter(openDelimiter);
openDelimiter.Remove();
delimiters.RemoveAt(openDelimiterIndex);
i--;
}
break;
}
// The current delimiters are matching
if (openDelimiter.DelimiterCount >= emphasisDesc.MinimumCount &&
closeDelimiter.DelimiterCount >= emphasisDesc.MinimumCount)
{
goto process_delims;
}
else if (openDelimiter.DelimiterCount > 0)
{
// There are still delimiter characters left, there's just not enough of them
openDelimiter.ReplaceBy(openDelimiter.AsLiteralInline());
delimiters.RemoveAt(openDelimiterIndex);
i--;
}
else
{
// Remove the open delimiter if it is also empty
var firstChild = openDelimiter.FirstChild!;
firstChild.Remove();
openDelimiter.ReplaceBy(firstChild);
firstChild.IsClosed = true;
closeDelimiter.Remove();
firstChild.InsertAfter(closeDelimiter);
delimiters.RemoveAt(openDelimiterIndex);
i--;
}
}
else if ((closeDelimiter.Type & DelimiterType.Open) == 0)
{
closeDelimiter.ReplaceBy(closeDelimiter.AsLiteralInline());
delimiters.RemoveAt(i);
i--;
break;
}
else
{
break;
}
}
}
}
// Any delimiters left must be literal
for (int i = 0; i < delimiters.Count; i++)
{
var delimiter = delimiters[i];
delimiter.ReplaceBy(delimiter.AsLiteralInline());
}
delimiters.Clear();
}
/// <summary>
/// Represents the DelimitersObjectCache type.
/// </summary>
public class DelimitersObjectCache : ObjectCache<List<EmphasisDelimiterInline>>
{
/// <summary>
/// Performs the new instance operation.
/// </summary>
protected override List<EmphasisDelimiterInline> NewInstance()
{
return new List<EmphasisDelimiterInline>(4);
}
/// <summary>
/// Performs the reset operation.
/// </summary>
protected override void Reset(List<EmphasisDelimiterInline> instance)
{
instance.Clear();
}
}
}