using Markdig.Helpers; using Markdig.Parsers; namespace OliverBooth.Markdown.Timestamp; /// /// Represents a Markdown inline parser that matches Discord-style timestamps. /// public sealed class TimestampInlineParser : InlineParser { /// /// Initializes a new instance of the class. /// public TimestampInlineParser() { OpeningCharacters = new[] { '<' }; } /// public override bool Match(InlineProcessor processor, ref StringSlice slice) { // Previous char must be a space if (!slice.PeekCharExtra(-1).IsWhiteSpaceOrZero()) { return false; } ReadOnlySpan span = slice.Text.AsSpan(slice.Start, slice.Length); if (!TryConsumeTimestamp(span, out ReadOnlySpan rawTimestamp, out char format)) { return false; } if (!long.TryParse(rawTimestamp, out long timestamp)) { return false; } bool hasFormat = format != '\0'; processor.Inline = new TimestampInline { Format = (TimestampFormat)format, Timestamp = DateTimeOffset.FromUnixTimeSeconds(timestamp) }; int paddingCount = hasFormat ? 6 : 4; // or optionally slice.Start += rawTimestamp.Length + paddingCount; return true; } private bool TryConsumeTimestamp(ReadOnlySpan source, out ReadOnlySpan timestamp, out char format) { timestamp = default; format = default; if (!source.StartsWith("') == -1) { timestamp = default; return false; } int delimiterIndex = timestamp.IndexOf(':'); if (delimiterIndex == 0) { // invalid format timestamp = default; return false; } if (delimiterIndex == -1) { // no format, default to relative format = 'R'; timestamp = timestamp[..^1]; // trim > } else { // use specified format format = timestamp[^2]; timestamp = timestamp[..^3]; } return true; } }