src/Elastic.Markdown/Myst/InlineParsers/Substitution/SubstitutionParser.cs (92 lines of code) (raw):
// Licensed to Elasticsearch B.V under one or more agreements.
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
// See the LICENSE file in the project root for more information
using System.Buffers;
using System.Diagnostics;
using Elastic.Markdown.Diagnostics;
using Markdig.Helpers;
using Markdig.Parsers;
using Markdig.Renderers;
using Markdig.Renderers.Html;
using Markdig.Syntax;
using Markdig.Syntax.Inlines;
namespace Elastic.Markdown.Myst.InlineParsers.Substitution;
[DebuggerDisplay("{GetType().Name} Line: {Line}, Found: {Found}, Replacement: {Replacement}")]
public class SubstitutionLeaf(string content, bool found, string replacement) : CodeInline(content)
{
public bool Found { get; } = found;
public string Replacement { get; } = replacement;
}
public class SubstitutionRenderer : HtmlObjectRenderer<SubstitutionLeaf>
{
protected override void Write(HtmlRenderer renderer, SubstitutionLeaf obj) =>
renderer.Write(obj.Found ? obj.Replacement : obj.Content);
}
public class SubstitutionParser : InlineParser
{
public SubstitutionParser() => OpeningCharacters = ['{'];
private readonly SearchValues<char> _values = SearchValues.Create(['\r', '\n', ' ', '\t', '}']);
public override bool Match(InlineProcessor processor, ref StringSlice slice)
{
var match = slice.CurrentChar;
if (slice.PeekCharExtra(1) != match)
return false;
if (processor.Context is not ParserContext context)
return false;
Debug.Assert(match is not ('\r' or '\n'));
// Match the opened sticks
var openSticks = slice.CountAndSkipChar(match);
var span = slice.AsSpan();
var i = span.IndexOfAny(_values);
if ((uint)i >= (uint)span.Length)
{
// We got to the end of the input before seeing the match character.
return false;
}
var closeSticks = 0;
while ((uint)i < (uint)span.Length && span[i] == '}')
{
closeSticks++;
i++;
}
span = span[i..];
if (closeSticks != 2)
return false;
var rawContent = slice.AsSpan()[..(slice.Length - span.Length)];
var content = new LazySubstring(slice.Text, slice.Start, rawContent.Length);
var startPosition = slice.Start;
slice.Start = startPosition + rawContent.Length;
// We've already skipped the opening sticks. Account for that here.
startPosition -= openSticks;
startPosition = Math.Max(startPosition, 0);
var key = content.ToString().Trim(['{', '}']).ToLowerInvariant();
var found = false;
var replacement = string.Empty;
if (context.Substitutions.TryGetValue(key, out var value))
{
found = true;
replacement = value;
}
else if (context.ContextSubstitutions.TryGetValue(key, out value))
{
found = true;
replacement = value;
}
if (found)
context.Build.Collector.CollectUsedSubstitutionKey(key);
var start = processor.GetSourcePosition(startPosition, out var line, out var column);
var end = processor.GetSourcePosition(slice.Start);
var sourceSpan = new SourceSpan(start, end);
var substitutionLeaf = new SubstitutionLeaf(content.ToString(), found, replacement)
{
Delimiter = '{',
Span = sourceSpan,
Line = line,
Column = column,
DelimiterCount = openSticks
};
if (!found)
processor.EmitError(line + 1, column + 3, substitutionLeaf.Span.Length - 3, $"Substitution key {{{key}}} is undefined");
if (processor.TrackTrivia)
{
// startPosition and slice.Start include the opening/closing sticks.
substitutionLeaf.ContentWithTrivia =
new StringSlice(slice.Text, startPosition + openSticks, slice.Start - openSticks - 1);
}
processor.Inline = substitutionLeaf;
return true;
}
}