refactor: amend 6ec4103a3a
Remove unused Markdown classes from within core web app project
This commit is contained in:
parent
6ec4103a3a
commit
dec9307f1d
@ -1,21 +0,0 @@
|
|||||||
using Markdig;
|
|
||||||
using OliverBooth.Extensions.Markdig.Markdown.Callout;
|
|
||||||
|
|
||||||
namespace OliverBooth.Extensions.Markdig.Markdown;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Extension methods for <see cref="MarkdownPipelineBuilder" />.
|
|
||||||
/// </summary>
|
|
||||||
internal static class MarkdownExtensions
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Uses this extension to enable Obsidian-style callouts.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="pipeline">The pipeline.</param>
|
|
||||||
/// <returns>The modified pipeline.</returns>
|
|
||||||
public static MarkdownPipelineBuilder UseCallouts(this MarkdownPipelineBuilder pipeline)
|
|
||||||
{
|
|
||||||
pipeline.Extensions.AddIfNotAlready<CalloutExtension>();
|
|
||||||
return pipeline;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,4 +1,5 @@
|
|||||||
using Markdig;
|
using Markdig;
|
||||||
|
using OliverBooth.Extensions.Markdig.Markdown.Callout;
|
||||||
using OliverBooth.Extensions.Markdig.Markdown.Template;
|
using OliverBooth.Extensions.Markdig.Markdown.Template;
|
||||||
using OliverBooth.Extensions.Markdig.Services;
|
using OliverBooth.Extensions.Markdig.Services;
|
||||||
|
|
||||||
@ -9,6 +10,23 @@ namespace OliverBooth.Extensions.Markdig;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public static class MarkdownPipelineExtensions
|
public static class MarkdownPipelineExtensions
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Enables the use of Obsidian-style callouts in this pipeline.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="builder">The Markdig markdown pipeline builder.</param>
|
||||||
|
/// <returns>The modified Markdig markdown pipeline builder.</returns>
|
||||||
|
/// <exception cref="ArgumentNullException"><paramref name="builder" /> is <see langword="null" />.</exception>
|
||||||
|
public static MarkdownPipelineBuilder UseCallouts(this MarkdownPipelineBuilder builder)
|
||||||
|
{
|
||||||
|
if (builder is null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(builder));
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.Extensions.AddIfNotAlready<CalloutExtension>();
|
||||||
|
return builder;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Enables the use of Wiki-style templates in this pipeline.
|
/// Enables the use of Wiki-style templates in this pipeline.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -1,43 +0,0 @@
|
|||||||
using Markdig.Helpers;
|
|
||||||
using Markdig.Syntax;
|
|
||||||
|
|
||||||
namespace OliverBooth.Markdown.Callout;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Represents a callout block.
|
|
||||||
/// </summary>
|
|
||||||
internal sealed class CalloutBlock : QuoteBlock
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new instance of the <see cref="CalloutBlock" /> class.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="type">The type of the callout.</param>
|
|
||||||
public CalloutBlock(StringSlice type) : base(null)
|
|
||||||
{
|
|
||||||
Type = type;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets a value indicating whether this callout is foldable.
|
|
||||||
/// </summary>
|
|
||||||
/// <value><see langword="true" /> if this callout is foldable; otherwise, <see langword="false" />.</value>
|
|
||||||
public bool Foldable { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the title of the callout.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>The title of the callout.</value>
|
|
||||||
public StringSlice Title { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the trailing whitespace trivia.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>The trailing whitespace trivia.</value>
|
|
||||||
public StringSlice TrailingWhitespaceTrivia { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the type of the callout.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>The type of the callout.</value>
|
|
||||||
public StringSlice Type { get; set; }
|
|
||||||
}
|
|
@ -1,32 +0,0 @@
|
|||||||
using Markdig;
|
|
||||||
using Markdig.Parsers.Inlines;
|
|
||||||
using Markdig.Renderers;
|
|
||||||
using Markdig.Renderers.Html;
|
|
||||||
|
|
||||||
namespace OliverBooth.Markdown.Callout;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Extension for adding Obsidian-style callouts to a Markdown pipeline.
|
|
||||||
/// </summary>
|
|
||||||
internal sealed class CalloutExtension : IMarkdownExtension
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
public void Setup(MarkdownPipelineBuilder pipeline)
|
|
||||||
{
|
|
||||||
var parser = pipeline.InlineParsers.Find<CalloutInlineParser>();
|
|
||||||
if (parser is null)
|
|
||||||
{
|
|
||||||
pipeline.InlineParsers.InsertBefore<LinkInlineParser>(new CalloutInlineParser());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer)
|
|
||||||
{
|
|
||||||
var blockRenderer = renderer.ObjectRenderers.FindExact<CalloutRenderer>();
|
|
||||||
if (blockRenderer is null)
|
|
||||||
{
|
|
||||||
renderer.ObjectRenderers.InsertBefore<QuoteBlockRenderer>(new CalloutRenderer(pipeline));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,176 +0,0 @@
|
|||||||
using System.Reflection;
|
|
||||||
using Cysharp.Text;
|
|
||||||
using Markdig.Helpers;
|
|
||||||
using Markdig.Parsers;
|
|
||||||
using Markdig.Renderers.Html;
|
|
||||||
using Markdig.Syntax;
|
|
||||||
|
|
||||||
namespace OliverBooth.Markdown.Callout;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// An inline parser for Obsidian-style callouts (<c>[!NOTE]</c> etc.)
|
|
||||||
/// </summary>
|
|
||||||
internal sealed class CalloutInlineParser : InlineParser
|
|
||||||
{
|
|
||||||
// ugly hack to access internal method
|
|
||||||
private static readonly MethodInfo ReplaceParentContainerMethod =
|
|
||||||
typeof(InlineProcessor).GetMethod("ReplaceParentContainer", BindingFlags.Instance | BindingFlags.NonPublic)!;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new instance of the <see cref="CalloutInlineParser" /> class.
|
|
||||||
/// </summary>
|
|
||||||
public CalloutInlineParser()
|
|
||||||
{
|
|
||||||
OpeningCharacters = ['['];
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override bool Match(InlineProcessor processor, ref StringSlice slice)
|
|
||||||
{
|
|
||||||
// We expect the alert to be the first child of a quote block. Example:
|
|
||||||
// > [!NOTE]
|
|
||||||
// > This is a note
|
|
||||||
if (processor.Block is not ParagraphBlock { Parent: QuoteBlock quoteBlock } paragraphBlock ||
|
|
||||||
paragraphBlock.Inline?.FirstChild != null)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
StringSlice cache = slice;
|
|
||||||
char current = slice.NextChar();
|
|
||||||
|
|
||||||
if (current != '!')
|
|
||||||
{
|
|
||||||
slice = cache;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
current = slice.NextChar(); // skip !
|
|
||||||
|
|
||||||
int start = slice.Start;
|
|
||||||
int end = start;
|
|
||||||
|
|
||||||
while (current.IsAlphaUpper())
|
|
||||||
{
|
|
||||||
end = slice.Start;
|
|
||||||
current = slice.NextChar();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (current != ']' || start == end)
|
|
||||||
{
|
|
||||||
slice = cache;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var type = new StringSlice(slice.Text, start, end);
|
|
||||||
current = slice.NextChar(); // skip ]
|
|
||||||
start = slice.Start;
|
|
||||||
|
|
||||||
bool fold = false;
|
|
||||||
if (current == '-')
|
|
||||||
{
|
|
||||||
fold = true;
|
|
||||||
current = slice.NextChar(); // skip -
|
|
||||||
start = slice.Start;
|
|
||||||
}
|
|
||||||
|
|
||||||
ReadTitle(current, ref slice, out StringSlice title, out end);
|
|
||||||
|
|
||||||
var callout = new CalloutBlock(type)
|
|
||||||
{
|
|
||||||
Foldable = fold,
|
|
||||||
Span = quoteBlock.Span,
|
|
||||||
TrailingWhitespaceTrivia = new StringSlice(slice.Text, start, end),
|
|
||||||
Line = quoteBlock.Line,
|
|
||||||
Column = quoteBlock.Column,
|
|
||||||
Title = title
|
|
||||||
};
|
|
||||||
|
|
||||||
AddAttributes(callout, type);
|
|
||||||
ReplaceQuoteBlock(processor, quoteBlock, callout);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void ReadTitle(char startChar, ref StringSlice slice, out StringSlice title, out int end)
|
|
||||||
{
|
|
||||||
using Utf16ValueStringBuilder builder = ZString.CreateStringBuilder();
|
|
||||||
|
|
||||||
char current = startChar;
|
|
||||||
while (true)
|
|
||||||
{
|
|
||||||
if (current is not ('\0' or '\r' or '\n'))
|
|
||||||
{
|
|
||||||
builder.Append(current);
|
|
||||||
current = slice.NextChar();
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
end = slice.Start;
|
|
||||||
if (HandleCharacter(ref slice, ref end, ref current))
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
title = new StringSlice(builder.ToString(), 0, builder.Length);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool HandleCharacter(ref StringSlice slice, ref int end, ref char current)
|
|
||||||
{
|
|
||||||
switch (current)
|
|
||||||
{
|
|
||||||
case '\r':
|
|
||||||
current = slice.NextChar(); // skip \r
|
|
||||||
|
|
||||||
if (current is not ('\0' or '\n'))
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
end = slice.Start;
|
|
||||||
if (current == '\n')
|
|
||||||
{
|
|
||||||
slice.NextChar(); // skip \n
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
|
|
||||||
case '\n':
|
|
||||||
slice.NextChar(); // skip \n
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void AddAttributes(IMarkdownObject callout, StringSlice type)
|
|
||||||
{
|
|
||||||
HtmlAttributes attributes = callout.GetAttributes();
|
|
||||||
attributes.AddClass("callout");
|
|
||||||
attributes.AddProperty("data-callout", type.AsSpan().ToString().ToLowerInvariant());
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void ReplaceQuoteBlock(InlineProcessor processor, QuoteBlock quoteBlock, CalloutBlock callout)
|
|
||||||
{
|
|
||||||
ContainerBlock? parentQuoteBlock = quoteBlock.Parent;
|
|
||||||
if (parentQuoteBlock is null)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
int indexOfQuoteBlock = parentQuoteBlock.IndexOf(quoteBlock);
|
|
||||||
parentQuoteBlock[indexOfQuoteBlock] = callout;
|
|
||||||
|
|
||||||
while (quoteBlock.Count > 0)
|
|
||||||
{
|
|
||||||
var block = quoteBlock[0];
|
|
||||||
quoteBlock.RemoveAt(0);
|
|
||||||
callout.Add(block);
|
|
||||||
}
|
|
||||||
|
|
||||||
ReplaceParentContainerMethod.Invoke(processor, [quoteBlock, callout]);
|
|
||||||
// ReplaceParentContainer(processor, quoteBlock, callout);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,119 +0,0 @@
|
|||||||
using HtmlAgilityPack;
|
|
||||||
using Humanizer;
|
|
||||||
using Markdig;
|
|
||||||
using Markdig.Renderers;
|
|
||||||
using Markdig.Renderers.Html;
|
|
||||||
|
|
||||||
namespace OliverBooth.Markdown.Callout;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Represents an HTML renderer which renders a <see cref="CalloutBlock" />.
|
|
||||||
/// </summary>
|
|
||||||
internal sealed class CalloutRenderer : HtmlObjectRenderer<CalloutBlock>
|
|
||||||
{
|
|
||||||
private readonly MarkdownPipeline _pipeline;
|
|
||||||
|
|
||||||
private static readonly Dictionary<string, string> CalloutTypes = new()
|
|
||||||
{
|
|
||||||
["NOTE"] = "pencil",
|
|
||||||
["ABSTRACT"] = "clipboard-list",
|
|
||||||
["INFO"] = "info",
|
|
||||||
["TODO"] = "circle-check",
|
|
||||||
["TIP"] = "flame",
|
|
||||||
["IMPORTANT"] = "flame",
|
|
||||||
["SUCCESS"] = "check",
|
|
||||||
["QUESTION"] = "circle-help",
|
|
||||||
["WARNING"] = "triangle-alert",
|
|
||||||
["FAILURE"] = "x",
|
|
||||||
["DANGER"] = "zap",
|
|
||||||
["BUG"] = "bug",
|
|
||||||
["EXAMPLE"] = "list",
|
|
||||||
["CITE"] = "quote",
|
|
||||||
["UPDATE"] = "calendar-check",
|
|
||||||
};
|
|
||||||
|
|
||||||
public CalloutRenderer(MarkdownPipeline pipeline)
|
|
||||||
{
|
|
||||||
_pipeline = pipeline;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Write(HtmlRenderer renderer, CalloutBlock block)
|
|
||||||
{
|
|
||||||
renderer.EnsureLine();
|
|
||||||
if (renderer.EnableHtmlForBlock)
|
|
||||||
{
|
|
||||||
RenderAsHtml(renderer, block, _pipeline);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
RenderAsText(renderer, block);
|
|
||||||
}
|
|
||||||
|
|
||||||
renderer.EnsureLine();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void RenderAsHtml(HtmlRenderer renderer, CalloutBlock block, MarkdownPipeline pipeline)
|
|
||||||
{
|
|
||||||
string title = block.Title.Text;
|
|
||||||
ReadOnlySpan<char> type = block.Type.AsSpan();
|
|
||||||
Span<char> upperType = stackalloc char[type.Length];
|
|
||||||
type.ToUpperInvariant(upperType);
|
|
||||||
|
|
||||||
if (!CalloutTypes.TryGetValue(upperType.ToString(), out string? lucideClass))
|
|
||||||
{
|
|
||||||
lucideClass = "pencil";
|
|
||||||
}
|
|
||||||
|
|
||||||
var typeString = type.ToString().ToLowerInvariant();
|
|
||||||
|
|
||||||
renderer.Write($"<div class=\"callout\" data-callout=\"{typeString}\"");
|
|
||||||
if (block.Foldable)
|
|
||||||
{
|
|
||||||
renderer.Write(" data-callout-fold=\"true\"");
|
|
||||||
}
|
|
||||||
|
|
||||||
renderer.Write('>');
|
|
||||||
renderer.Write("<div class=\"callout-title\"><i data-lucide=\"");
|
|
||||||
renderer.Write(lucideClass);
|
|
||||||
renderer.Write("\"></i> ");
|
|
||||||
|
|
||||||
string calloutTitle = title.Length == 0 ? typeString.Humanize(LetterCasing.Sentence) : title;
|
|
||||||
WriteTitle(renderer, pipeline, calloutTitle);
|
|
||||||
|
|
||||||
if (block.Foldable)
|
|
||||||
{
|
|
||||||
renderer.Write("<span class=\"callout-fold\"><i data-lucide=\"chevron-down\"></i></span>");
|
|
||||||
}
|
|
||||||
|
|
||||||
renderer.WriteLine("</div>");
|
|
||||||
renderer.Write("<div class=\"callout-content\">");
|
|
||||||
renderer.WriteChildren(block);
|
|
||||||
renderer.WriteLine("</div>");
|
|
||||||
renderer.WriteLine("</div>");
|
|
||||||
renderer.EnsureLine();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void WriteTitle(TextRendererBase renderer, MarkdownPipeline pipeline, string calloutTitle)
|
|
||||||
{
|
|
||||||
string html = Markdig.Markdown.ToHtml(calloutTitle, pipeline);
|
|
||||||
var document = new HtmlDocument();
|
|
||||||
document.LoadHtml(html);
|
|
||||||
if (document.DocumentNode.FirstChild is { Name: "p" } child)
|
|
||||||
{
|
|
||||||
// ugly hack to remove <p> tag generated by Markdig
|
|
||||||
document.DocumentNode.InnerHtml = child.InnerHtml;
|
|
||||||
}
|
|
||||||
|
|
||||||
document.Save(renderer.Writer);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void RenderAsText(HtmlRenderer renderer, CalloutBlock block)
|
|
||||||
{
|
|
||||||
string title = block.Title.Text;
|
|
||||||
ReadOnlySpan<char> type = block.Type.AsSpan();
|
|
||||||
renderer.WriteLine(title.Length == 0 ? type.ToString().ToUpperInvariant() : title.ToUpperInvariant());
|
|
||||||
renderer.WriteChildren(block);
|
|
||||||
renderer.EnsureLine();
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,21 +0,0 @@
|
|||||||
using Markdig;
|
|
||||||
using OliverBooth.Markdown.Callout;
|
|
||||||
|
|
||||||
namespace OliverBooth.Markdown;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Extension methods for <see cref="MarkdownPipelineBuilder" />.
|
|
||||||
/// </summary>
|
|
||||||
internal static class MarkdownExtensions
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Uses this extension to enable Obsidian-style callouts.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="pipeline">The pipeline.</param>
|
|
||||||
/// <returns>The modified pipeline.</returns>
|
|
||||||
public static MarkdownPipelineBuilder UseCallouts(this MarkdownPipelineBuilder pipeline)
|
|
||||||
{
|
|
||||||
pipeline.Extensions.AddIfNotAlready<CalloutExtension>();
|
|
||||||
return pipeline;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,25 +0,0 @@
|
|||||||
using Markdig;
|
|
||||||
using Markdig.Renderers;
|
|
||||||
|
|
||||||
namespace OliverBooth.Markdown.Timestamp;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Represents a Markdig extension that supports Discord-style timestamps.
|
|
||||||
/// </summary>
|
|
||||||
public class TimestampExtension : IMarkdownExtension
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
public void Setup(MarkdownPipelineBuilder pipeline)
|
|
||||||
{
|
|
||||||
pipeline.InlineParsers.AddIfNotAlready<TimestampInlineParser>();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer)
|
|
||||||
{
|
|
||||||
if (renderer is HtmlRenderer htmlRenderer)
|
|
||||||
{
|
|
||||||
htmlRenderer.ObjectRenderers.AddIfNotAlready<TimestampRenderer>();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,42 +0,0 @@
|
|||||||
namespace OliverBooth.Markdown.Timestamp;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// An enumeration of timestamp formats.
|
|
||||||
/// </summary>
|
|
||||||
public enum TimestampFormat
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Short time format. Example: 12:00
|
|
||||||
/// </summary>
|
|
||||||
ShortTime = 't',
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Long time format. Example: 12:00:00
|
|
||||||
/// </summary>
|
|
||||||
LongTime = 'T',
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Short date format. Example: 1/1/2000
|
|
||||||
/// </summary>
|
|
||||||
ShortDate = 'd',
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Long date format. Example: 1 January 2000
|
|
||||||
/// </summary>
|
|
||||||
LongDate = 'D',
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Short date/time format. Example: 1 January 2000 at 12:00
|
|
||||||
/// </summary>
|
|
||||||
LongDateShortTime = 'f',
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Long date/time format. Example: Saturday, 1 January 2000 at 12:00
|
|
||||||
/// </summary>
|
|
||||||
LongDateTime = 'F',
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Relative date/time format. Example: 1 second ago
|
|
||||||
/// </summary>
|
|
||||||
Relative = 'R',
|
|
||||||
}
|
|
@ -1,21 +0,0 @@
|
|||||||
using Markdig.Syntax.Inlines;
|
|
||||||
|
|
||||||
namespace OliverBooth.Markdown.Timestamp;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Represents a Markdown inline element that contains a timestamp.
|
|
||||||
/// </summary>
|
|
||||||
public sealed class TimestampInline : Inline
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the format.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>The format.</value>
|
|
||||||
public TimestampFormat Format { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the timestamp.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>The timestamp.</value>
|
|
||||||
public DateTimeOffset Timestamp { get; set; }
|
|
||||||
}
|
|
@ -1,91 +0,0 @@
|
|||||||
using Markdig.Helpers;
|
|
||||||
using Markdig.Parsers;
|
|
||||||
|
|
||||||
namespace OliverBooth.Markdown.Timestamp;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Represents a Markdown inline parser that matches Discord-style timestamps.
|
|
||||||
/// </summary>
|
|
||||||
public sealed class TimestampInlineParser : InlineParser
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new instance of the <see cref="TimestampInlineParser" /> class.
|
|
||||||
/// </summary>
|
|
||||||
public TimestampInlineParser()
|
|
||||||
{
|
|
||||||
OpeningCharacters = new[] { '<' };
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override bool Match(InlineProcessor processor, ref StringSlice slice)
|
|
||||||
{
|
|
||||||
// Previous char must be a space
|
|
||||||
if (!slice.PeekCharExtra(-1).IsWhiteSpaceOrZero())
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
ReadOnlySpan<char> span = slice.Text.AsSpan(slice.Start, slice.Length);
|
|
||||||
|
|
||||||
if (!TryConsumeTimestamp(span, out ReadOnlySpan<char> 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; // <t:*> or optionally <t:*:*>
|
|
||||||
slice.Start += rawTimestamp.Length + paddingCount;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool TryConsumeTimestamp(ReadOnlySpan<char> source,
|
|
||||||
out ReadOnlySpan<char> timestamp,
|
|
||||||
out char format)
|
|
||||||
{
|
|
||||||
timestamp = default;
|
|
||||||
format = default;
|
|
||||||
|
|
||||||
if (!source.StartsWith("<t:")) return false;
|
|
||||||
timestamp = source[3..];
|
|
||||||
|
|
||||||
if (timestamp.IndexOf('>') == -1)
|
|
||||||
{
|
|
||||||
timestamp = default;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
int delimiterIndex = timestamp.IndexOf(':');
|
|
||||||
if (delimiterIndex == 0)
|
|
||||||
{
|
|
||||||
// invalid format <t::*>
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,55 +0,0 @@
|
|||||||
using System.ComponentModel;
|
|
||||||
using Humanizer;
|
|
||||||
using Markdig.Renderers;
|
|
||||||
using Markdig.Renderers.Html;
|
|
||||||
|
|
||||||
namespace OliverBooth.Markdown.Timestamp;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Represents a Markdown object renderer that renders <see cref="TimestampInline" /> elements.
|
|
||||||
/// </summary>
|
|
||||||
public sealed class TimestampRenderer : HtmlObjectRenderer<TimestampInline>
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Write(HtmlRenderer renderer, TimestampInline obj)
|
|
||||||
{
|
|
||||||
DateTimeOffset timestamp = obj.Timestamp;
|
|
||||||
TimestampFormat format = obj.Format;
|
|
||||||
|
|
||||||
renderer.Write("<span class=\"timestamp\" data-timestamp=\"");
|
|
||||||
renderer.Write(timestamp.ToUnixTimeSeconds().ToString());
|
|
||||||
renderer.Write("\" data-format=\"");
|
|
||||||
renderer.Write(((char)format).ToString());
|
|
||||||
renderer.Write("\" title=\"");
|
|
||||||
renderer.WriteEscape(timestamp.ToString("dddd, d MMMM yyyy HH:mm"));
|
|
||||||
renderer.Write("\">");
|
|
||||||
|
|
||||||
switch (format)
|
|
||||||
{
|
|
||||||
case TimestampFormat.LongDate:
|
|
||||||
renderer.Write(timestamp.ToString("d MMMM yyyy"));
|
|
||||||
break;
|
|
||||||
|
|
||||||
case TimestampFormat.LongDateShortTime:
|
|
||||||
renderer.Write(timestamp.ToString(@"d MMMM yyyy \a\t HH:mm"));
|
|
||||||
break;
|
|
||||||
|
|
||||||
case TimestampFormat.LongDateTime:
|
|
||||||
renderer.Write(timestamp.ToString(@"dddd, d MMMM yyyy \a\t HH:mm"));
|
|
||||||
break;
|
|
||||||
|
|
||||||
case TimestampFormat.Relative:
|
|
||||||
renderer.Write(timestamp.Humanize());
|
|
||||||
break;
|
|
||||||
|
|
||||||
case var _ when !Enum.IsDefined(format):
|
|
||||||
throw new InvalidEnumArgumentException(nameof(format), (int)format, typeof(TimestampFormat));
|
|
||||||
|
|
||||||
default:
|
|
||||||
renderer.Write(timestamp.ToString(((char)format).ToString()));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
renderer.Write("</span>");
|
|
||||||
}
|
|
||||||
}
|
|
@ -5,9 +5,8 @@ using OliverBooth.Data.Blog;
|
|||||||
using OliverBooth.Data.Web;
|
using OliverBooth.Data.Web;
|
||||||
using OliverBooth.Extensions;
|
using OliverBooth.Extensions;
|
||||||
using OliverBooth.Extensions.Markdig;
|
using OliverBooth.Extensions.Markdig;
|
||||||
|
using OliverBooth.Extensions.Markdig.Markdown.Timestamp;
|
||||||
using OliverBooth.Extensions.Markdig.Services;
|
using OliverBooth.Extensions.Markdig.Services;
|
||||||
using OliverBooth.Markdown;
|
|
||||||
using OliverBooth.Markdown.Timestamp;
|
|
||||||
using OliverBooth.Services;
|
using OliverBooth.Services;
|
||||||
using Serilog;
|
using Serilog;
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user