using Cysharp.Text; using Markdig.Helpers; using Markdig.Parsers; namespace OliverBooth.Markdown; /// /// Represents a Markdown inline parser that handles MediaWiki-style templates. /// public sealed class TemplateInlineParser : InlineParser { private static readonly IReadOnlyDictionary EmptyParams = new Dictionary().AsReadOnly(); /// public override bool Match(InlineProcessor processor, ref StringSlice slice) { ReadOnlySpan span = slice.Text.AsSpan()[slice.Start..]; if (!span.StartsWith("{{")) { return false; } ReadOnlySpan template = ReadUntilClosure(span); if (template.IsEmpty) { return false; } template = template[2..^2]; // trim {{ and }} ReadOnlySpan name = ReadTemplateName(template, out ReadOnlySpan argumentSpan); if (argumentSpan.IsEmpty) { processor.Inline = new TemplateInline { Name = name.ToString(), ArgumentString = string.Empty, ArgumentList = ArraySegment.Empty, Params = EmptyParams }; slice.End = slice.Start; slice.Start += template.Length + 4; return true; } var argumentList = new List(); var paramsList = new Dictionary(); ParseArguments(argumentSpan, argumentList, paramsList); processor.Inline = new TemplateInline { Name = name.ToString(), ArgumentString = argumentSpan.ToString(), ArgumentList = argumentList.AsReadOnly(), Params = paramsList.AsReadOnly() }; slice.End = slice.Start; slice.Start += template.Length + 4; return true; } private static void ParseArguments(ReadOnlySpan argumentSpan, IList argumentList, IDictionary paramsList) { using Utf8ValueStringBuilder buffer = ZString.CreateUtf8StringBuilder(); var isKey = true; for (var index = 0; index < argumentSpan.Length; index++) { if (isKey) { ReadOnlySpan result = ReadNext(argumentSpan, ref index, false, out bool hasValue); if (!hasValue) { var argument = result.ToString(); argumentList.Add(argument); } buffer.Append(result); isKey = false; } else { ReadOnlySpan result = ReadNext(argumentSpan, ref index, false, out bool hasValue); var key = buffer.ToString(); var value = result.ToString(); buffer.Clear(); isKey = true; paramsList.Add(key, value); argumentList.Add($"{key}={value}"); } } } private static ReadOnlySpan ReadNext(ReadOnlySpan argumentSpan, ref int index, bool consumeToken, out bool hasValue) { var isEscaped = false; for (; index < argumentSpan.Length; index++) { char currentChar = argumentSpan[index]; switch (currentChar) { case '\\' when isEscaped: isEscaped = false; break; case '\\': isEscaped = true; break; case '|' when !isEscaped: hasValue = false; return argumentSpan[..index]; case '=' when !isEscaped && !consumeToken: hasValue = true; return argumentSpan[..index]; } } hasValue = false; return argumentSpan[..index]; } private static ReadOnlySpan ReadUntilClosure(ReadOnlySpan input) { int endIndex = FindClosingBraceIndex(input); return endIndex != -1 ? input[..(endIndex + 1)] : ReadOnlySpan.Empty; } private static ReadOnlySpan ReadTemplateName(ReadOnlySpan input, out ReadOnlySpan argumentSpan) { int argumentStartIndex = input.IndexOf('|'); if (argumentStartIndex == -1) { argumentSpan = Span.Empty; return input; } argumentSpan = input[(argumentStartIndex + 1)..]; return input[..argumentStartIndex]; } private static int FindClosingBraceIndex(ReadOnlySpan input) { var openingBraces = 0; var closingBraces = 0; for (var index = 0; index < input.Length - 1; index++) { char currentChar = input[index]; char nextChar = index < input.Length - 2 ? input[index + 1] : '\0'; if (IsOpeningBraceSequence(currentChar, nextChar)) { openingBraces++; index++; } else if (IsClosingBraceSequence(currentChar, nextChar)) { closingBraces++; index++; } if (openingBraces == closingBraces && openingBraces > 0) { return index; } } return -1; } private static bool IsOpeningBraceSequence(char currentChar, char nextChar) { return currentChar == '{' && nextChar == '{'; } private static bool IsClosingBraceSequence(char currentChar, char nextChar) { return currentChar == '}' && nextChar == '}'; } }