2023-08-11 15:51:20 +01:00
|
|
|
using Cysharp.Text;
|
2023-08-08 21:03:41 +01:00
|
|
|
using Markdig.Helpers;
|
|
|
|
using Markdig.Parsers;
|
|
|
|
|
2023-08-13 17:33:54 +01:00
|
|
|
namespace OliverBooth.Markdown.Template;
|
2023-08-08 21:03:41 +01:00
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Represents a Markdown inline parser that handles MediaWiki-style templates.
|
|
|
|
/// </summary>
|
|
|
|
public sealed class TemplateInlineParser : InlineParser
|
|
|
|
{
|
2023-08-08 22:18:42 +01:00
|
|
|
private static readonly IReadOnlyDictionary<string, string> EmptyParams =
|
|
|
|
new Dictionary<string, string>().AsReadOnly();
|
|
|
|
|
2023-08-09 21:09:50 +01:00
|
|
|
/// <summary>
|
|
|
|
/// Initializes a new instance of the <see cref="TemplateInlineParser" /> class.
|
|
|
|
/// </summary>
|
|
|
|
public TemplateInlineParser()
|
|
|
|
{
|
|
|
|
OpeningCharacters = new[] { '{' };
|
|
|
|
}
|
|
|
|
|
2023-08-08 21:03:41 +01:00
|
|
|
/// <inheritdoc />
|
|
|
|
public override bool Match(InlineProcessor processor, ref StringSlice slice)
|
|
|
|
{
|
2023-08-08 22:18:42 +01:00
|
|
|
ReadOnlySpan<char> span = slice.Text.AsSpan()[slice.Start..];
|
|
|
|
if (!span.StartsWith("{{"))
|
2023-08-08 21:03:41 +01:00
|
|
|
{
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2023-08-08 22:18:42 +01:00
|
|
|
ReadOnlySpan<char> template = ReadUntilClosure(span);
|
|
|
|
if (template.IsEmpty)
|
2023-08-08 21:03:41 +01:00
|
|
|
{
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2023-08-08 22:18:42 +01:00
|
|
|
template = template[2..^2]; // trim {{ and }}
|
|
|
|
ReadOnlySpan<char> name = ReadTemplateName(template, out ReadOnlySpan<char> argumentSpan);
|
2023-08-15 17:04:43 +01:00
|
|
|
int variantIndex = name.IndexOf(':');
|
|
|
|
bool hasVariant = variantIndex > -1;
|
|
|
|
var variant = ReadOnlySpan<char>.Empty;
|
|
|
|
|
|
|
|
if (hasVariant)
|
|
|
|
{
|
|
|
|
variant = name[(variantIndex + 1)..];
|
|
|
|
name = name[..variantIndex];
|
|
|
|
}
|
|
|
|
|
2023-08-08 22:18:42 +01:00
|
|
|
if (argumentSpan.IsEmpty)
|
|
|
|
{
|
|
|
|
processor.Inline = new TemplateInline
|
|
|
|
{
|
|
|
|
Name = name.ToString(),
|
2023-08-15 17:04:43 +01:00
|
|
|
Variant = hasVariant ? variant.ToString() : string.Empty,
|
2023-08-08 22:18:42 +01:00
|
|
|
ArgumentString = string.Empty,
|
|
|
|
ArgumentList = ArraySegment<string>.Empty,
|
|
|
|
Params = EmptyParams
|
|
|
|
};
|
|
|
|
|
|
|
|
slice.End = slice.Start;
|
|
|
|
slice.Start += template.Length + 4;
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2023-08-08 21:03:41 +01:00
|
|
|
var argumentList = new List<string>();
|
2023-08-08 22:18:42 +01:00
|
|
|
var paramsList = new Dictionary<string, string>();
|
2023-08-08 21:03:41 +01:00
|
|
|
|
2023-08-08 22:18:42 +01:00
|
|
|
ParseArguments(argumentSpan, argumentList, paramsList);
|
2023-08-08 21:03:41 +01:00
|
|
|
|
2023-08-08 22:18:42 +01:00
|
|
|
processor.Inline = new TemplateInline
|
|
|
|
{
|
|
|
|
Name = name.ToString(),
|
2023-08-15 17:04:43 +01:00
|
|
|
Variant = hasVariant ? variant.ToString() : string.Empty,
|
2023-08-08 22:18:42 +01:00
|
|
|
ArgumentString = argumentSpan.ToString(),
|
|
|
|
ArgumentList = argumentList.AsReadOnly(),
|
|
|
|
Params = paramsList.AsReadOnly()
|
|
|
|
};
|
2023-08-08 21:03:41 +01:00
|
|
|
|
2023-08-08 22:18:42 +01:00
|
|
|
slice.Start += template.Length + 4;
|
|
|
|
return true;
|
|
|
|
}
|
2023-08-08 21:03:41 +01:00
|
|
|
|
2023-08-08 22:18:42 +01:00
|
|
|
private static void ParseArguments(ReadOnlySpan<char> argumentSpan,
|
|
|
|
IList<string> argumentList,
|
|
|
|
IDictionary<string, string> paramsList)
|
|
|
|
{
|
|
|
|
using Utf8ValueStringBuilder buffer = ZString.CreateUtf8StringBuilder();
|
|
|
|
var isKey = true;
|
2023-08-08 21:03:41 +01:00
|
|
|
|
2023-08-08 22:18:42 +01:00
|
|
|
for (var index = 0; index < argumentSpan.Length; index++)
|
|
|
|
{
|
|
|
|
if (isKey)
|
2023-08-08 21:03:41 +01:00
|
|
|
{
|
2023-08-08 22:18:42 +01:00
|
|
|
ReadOnlySpan<char> result = ReadNext(argumentSpan, ref index, false, out bool hasValue);
|
|
|
|
if (!hasValue)
|
2023-08-08 21:03:41 +01:00
|
|
|
{
|
2023-08-08 22:25:00 +01:00
|
|
|
argumentList.Add(result.ToString());
|
2023-08-08 22:20:58 +01:00
|
|
|
continue;
|
2023-08-08 21:03:41 +01:00
|
|
|
}
|
|
|
|
|
2023-08-08 22:18:42 +01:00
|
|
|
buffer.Append(result);
|
|
|
|
isKey = false;
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
2023-08-08 22:19:57 +01:00
|
|
|
ReadOnlySpan<char> result = ReadNext(argumentSpan, ref index, true, out bool _);
|
2023-08-08 22:18:42 +01:00
|
|
|
var key = buffer.ToString();
|
|
|
|
var value = result.ToString();
|
2023-08-08 21:03:41 +01:00
|
|
|
|
2023-08-08 22:18:42 +01:00
|
|
|
buffer.Clear();
|
|
|
|
isKey = true;
|
2023-08-08 21:03:41 +01:00
|
|
|
|
2023-08-08 22:18:42 +01:00
|
|
|
paramsList.Add(key, value);
|
|
|
|
argumentList.Add($"{key}={value}");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2023-08-08 21:03:41 +01:00
|
|
|
|
2023-08-08 22:18:42 +01:00
|
|
|
private static ReadOnlySpan<char> ReadNext(ReadOnlySpan<char> argumentSpan,
|
|
|
|
ref int index,
|
|
|
|
bool consumeToken,
|
|
|
|
out bool hasValue)
|
|
|
|
{
|
|
|
|
var isEscaped = false;
|
2023-08-11 21:33:14 +01:00
|
|
|
|
2023-08-08 22:25:23 +01:00
|
|
|
int startIndex = index;
|
2023-08-08 22:18:42 +01:00
|
|
|
for (; index < argumentSpan.Length; index++)
|
|
|
|
{
|
|
|
|
char currentChar = argumentSpan[index];
|
|
|
|
switch (currentChar)
|
|
|
|
{
|
|
|
|
case '\\' when isEscaped:
|
|
|
|
isEscaped = false;
|
|
|
|
break;
|
2023-08-08 21:03:41 +01:00
|
|
|
|
2023-08-08 22:18:42 +01:00
|
|
|
case '\\':
|
|
|
|
isEscaped = true;
|
|
|
|
break;
|
2023-08-08 21:03:41 +01:00
|
|
|
|
2023-08-08 22:18:42 +01:00
|
|
|
case '|' when !isEscaped:
|
|
|
|
hasValue = false;
|
2023-08-08 22:25:23 +01:00
|
|
|
return argumentSpan[startIndex..index];
|
2023-08-08 22:18:42 +01:00
|
|
|
|
|
|
|
case '=' when !isEscaped && !consumeToken:
|
|
|
|
hasValue = true;
|
2023-08-08 22:25:23 +01:00
|
|
|
return argumentSpan[startIndex..index];
|
2023-08-08 21:03:41 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-08-08 22:18:42 +01:00
|
|
|
hasValue = false;
|
2023-08-08 22:25:23 +01:00
|
|
|
return argumentSpan[startIndex..index];
|
2023-08-08 22:18:42 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
private static ReadOnlySpan<char> ReadUntilClosure(ReadOnlySpan<char> input)
|
|
|
|
{
|
|
|
|
int endIndex = FindClosingBraceIndex(input);
|
|
|
|
return endIndex != -1 ? input[..(endIndex + 1)] : ReadOnlySpan<char>.Empty;
|
|
|
|
}
|
|
|
|
|
|
|
|
private static ReadOnlySpan<char> ReadTemplateName(ReadOnlySpan<char> input, out ReadOnlySpan<char> argumentSpan)
|
|
|
|
{
|
|
|
|
int argumentStartIndex = input.IndexOf('|');
|
|
|
|
if (argumentStartIndex == -1)
|
2023-08-08 21:03:41 +01:00
|
|
|
{
|
2023-08-08 22:18:42 +01:00
|
|
|
argumentSpan = Span<char>.Empty;
|
|
|
|
return input;
|
|
|
|
}
|
2023-08-08 21:03:41 +01:00
|
|
|
|
2023-08-08 22:18:42 +01:00
|
|
|
argumentSpan = input[(argumentStartIndex + 1)..];
|
|
|
|
return input[..argumentStartIndex];
|
|
|
|
}
|
|
|
|
|
|
|
|
private static int FindClosingBraceIndex(ReadOnlySpan<char> 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 == '}';
|
2023-08-08 21:03:41 +01:00
|
|
|
}
|
|
|
|
}
|