diff --git a/OliverBooth.Extensions.Markdig/Markdown/MarkdownExtensions.cs b/OliverBooth.Extensions.Markdig/Markdown/MarkdownExtensions.cs deleted file mode 100644 index cd54de4..0000000 --- a/OliverBooth.Extensions.Markdig/Markdown/MarkdownExtensions.cs +++ /dev/null @@ -1,21 +0,0 @@ -using Markdig; -using OliverBooth.Extensions.Markdig.Markdown.Callout; - -namespace OliverBooth.Extensions.Markdig.Markdown; - -/// -/// Extension methods for . -/// -internal static class MarkdownExtensions -{ - /// - /// Uses this extension to enable Obsidian-style callouts. - /// - /// The pipeline. - /// The modified pipeline. - public static MarkdownPipelineBuilder UseCallouts(this MarkdownPipelineBuilder pipeline) - { - pipeline.Extensions.AddIfNotAlready(); - return pipeline; - } -} diff --git a/OliverBooth.Extensions.Markdig/MarkdownPipelineExtensions.cs b/OliverBooth.Extensions.Markdig/MarkdownPipelineExtensions.cs index 20ea173..5426937 100644 --- a/OliverBooth.Extensions.Markdig/MarkdownPipelineExtensions.cs +++ b/OliverBooth.Extensions.Markdig/MarkdownPipelineExtensions.cs @@ -1,4 +1,5 @@ using Markdig; +using OliverBooth.Extensions.Markdig.Markdown.Callout; using OliverBooth.Extensions.Markdig.Markdown.Template; using OliverBooth.Extensions.Markdig.Services; @@ -9,6 +10,23 @@ namespace OliverBooth.Extensions.Markdig; /// public static class MarkdownPipelineExtensions { + /// + /// Enables the use of Obsidian-style callouts in this pipeline. + /// + /// The Markdig markdown pipeline builder. + /// The modified Markdig markdown pipeline builder. + /// is . + public static MarkdownPipelineBuilder UseCallouts(this MarkdownPipelineBuilder builder) + { + if (builder is null) + { + throw new ArgumentNullException(nameof(builder)); + } + + builder.Extensions.AddIfNotAlready(); + return builder; + } + /// /// Enables the use of Wiki-style templates in this pipeline. /// diff --git a/OliverBooth/Markdown/Callout/CalloutBlock.cs b/OliverBooth/Markdown/Callout/CalloutBlock.cs deleted file mode 100644 index 5c01675..0000000 --- a/OliverBooth/Markdown/Callout/CalloutBlock.cs +++ /dev/null @@ -1,43 +0,0 @@ -using Markdig.Helpers; -using Markdig.Syntax; - -namespace OliverBooth.Markdown.Callout; - -/// -/// Represents a callout block. -/// -internal sealed class CalloutBlock : QuoteBlock -{ - /// - /// Initializes a new instance of the class. - /// - /// The type of the callout. - public CalloutBlock(StringSlice type) : base(null) - { - Type = type; - } - - /// - /// Gets or sets a value indicating whether this callout is foldable. - /// - /// if this callout is foldable; otherwise, . - public bool Foldable { get; set; } - - /// - /// Gets or sets the title of the callout. - /// - /// The title of the callout. - public StringSlice Title { get; set; } - - /// - /// Gets or sets the trailing whitespace trivia. - /// - /// The trailing whitespace trivia. - public StringSlice TrailingWhitespaceTrivia { get; set; } - - /// - /// Gets or sets the type of the callout. - /// - /// The type of the callout. - public StringSlice Type { get; set; } -} diff --git a/OliverBooth/Markdown/Callout/CalloutExtension.cs b/OliverBooth/Markdown/Callout/CalloutExtension.cs deleted file mode 100644 index c9731a2..0000000 --- a/OliverBooth/Markdown/Callout/CalloutExtension.cs +++ /dev/null @@ -1,32 +0,0 @@ -using Markdig; -using Markdig.Parsers.Inlines; -using Markdig.Renderers; -using Markdig.Renderers.Html; - -namespace OliverBooth.Markdown.Callout; - -/// -/// Extension for adding Obsidian-style callouts to a Markdown pipeline. -/// -internal sealed class CalloutExtension : IMarkdownExtension -{ - /// - public void Setup(MarkdownPipelineBuilder pipeline) - { - var parser = pipeline.InlineParsers.Find(); - if (parser is null) - { - pipeline.InlineParsers.InsertBefore(new CalloutInlineParser()); - } - } - - /// - public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer) - { - var blockRenderer = renderer.ObjectRenderers.FindExact(); - if (blockRenderer is null) - { - renderer.ObjectRenderers.InsertBefore(new CalloutRenderer(pipeline)); - } - } -} diff --git a/OliverBooth/Markdown/Callout/CalloutInlineParser.cs b/OliverBooth/Markdown/Callout/CalloutInlineParser.cs deleted file mode 100644 index ce7ddc5..0000000 --- a/OliverBooth/Markdown/Callout/CalloutInlineParser.cs +++ /dev/null @@ -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; - -/// -/// An inline parser for Obsidian-style callouts ([!NOTE] etc.) -/// -internal sealed class CalloutInlineParser : InlineParser -{ - // ugly hack to access internal method - private static readonly MethodInfo ReplaceParentContainerMethod = - typeof(InlineProcessor).GetMethod("ReplaceParentContainer", BindingFlags.Instance | BindingFlags.NonPublic)!; - - /// - /// Initializes a new instance of the class. - /// - public CalloutInlineParser() - { - OpeningCharacters = ['[']; - } - - /// - 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); - } -} diff --git a/OliverBooth/Markdown/Callout/CalloutRenderer.cs b/OliverBooth/Markdown/Callout/CalloutRenderer.cs deleted file mode 100644 index f2e4f77..0000000 --- a/OliverBooth/Markdown/Callout/CalloutRenderer.cs +++ /dev/null @@ -1,119 +0,0 @@ -using HtmlAgilityPack; -using Humanizer; -using Markdig; -using Markdig.Renderers; -using Markdig.Renderers.Html; - -namespace OliverBooth.Markdown.Callout; - -/// -/// Represents an HTML renderer which renders a . -/// -internal sealed class CalloutRenderer : HtmlObjectRenderer -{ - private readonly MarkdownPipeline _pipeline; - - private static readonly Dictionary 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; - } - - /// - 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 type = block.Type.AsSpan(); - Span 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($"
'); - renderer.Write("
"); - - string calloutTitle = title.Length == 0 ? typeString.Humanize(LetterCasing.Sentence) : title; - WriteTitle(renderer, pipeline, calloutTitle); - - if (block.Foldable) - { - renderer.Write(""); - } - - renderer.WriteLine("
"); - renderer.Write("
"); - renderer.WriteChildren(block); - renderer.WriteLine("
"); - renderer.WriteLine("
"); - 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

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 type = block.Type.AsSpan(); - renderer.WriteLine(title.Length == 0 ? type.ToString().ToUpperInvariant() : title.ToUpperInvariant()); - renderer.WriteChildren(block); - renderer.EnsureLine(); - } -} diff --git a/OliverBooth/Markdown/MarkdownExtensions.cs b/OliverBooth/Markdown/MarkdownExtensions.cs deleted file mode 100644 index d63d869..0000000 --- a/OliverBooth/Markdown/MarkdownExtensions.cs +++ /dev/null @@ -1,21 +0,0 @@ -using Markdig; -using OliverBooth.Markdown.Callout; - -namespace OliverBooth.Markdown; - -///

-/// Extension methods for . -/// -internal static class MarkdownExtensions -{ - /// - /// Uses this extension to enable Obsidian-style callouts. - /// - /// The pipeline. - /// The modified pipeline. - public static MarkdownPipelineBuilder UseCallouts(this MarkdownPipelineBuilder pipeline) - { - pipeline.Extensions.AddIfNotAlready(); - return pipeline; - } -} diff --git a/OliverBooth/Markdown/Timestamp/TimestampExtension.cs b/OliverBooth/Markdown/Timestamp/TimestampExtension.cs deleted file mode 100644 index 45ae2df..0000000 --- a/OliverBooth/Markdown/Timestamp/TimestampExtension.cs +++ /dev/null @@ -1,25 +0,0 @@ -using Markdig; -using Markdig.Renderers; - -namespace OliverBooth.Markdown.Timestamp; - -/// -/// Represents a Markdig extension that supports Discord-style timestamps. -/// -public class TimestampExtension : IMarkdownExtension -{ - /// - public void Setup(MarkdownPipelineBuilder pipeline) - { - pipeline.InlineParsers.AddIfNotAlready(); - } - - /// - public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer) - { - if (renderer is HtmlRenderer htmlRenderer) - { - htmlRenderer.ObjectRenderers.AddIfNotAlready(); - } - } -} diff --git a/OliverBooth/Markdown/Timestamp/TimestampFormat.cs b/OliverBooth/Markdown/Timestamp/TimestampFormat.cs deleted file mode 100644 index 29eca6b..0000000 --- a/OliverBooth/Markdown/Timestamp/TimestampFormat.cs +++ /dev/null @@ -1,42 +0,0 @@ -namespace OliverBooth.Markdown.Timestamp; - -/// -/// An enumeration of timestamp formats. -/// -public enum TimestampFormat -{ - /// - /// Short time format. Example: 12:00 - /// - ShortTime = 't', - - /// - /// Long time format. Example: 12:00:00 - /// - LongTime = 'T', - - /// - /// Short date format. Example: 1/1/2000 - /// - ShortDate = 'd', - - /// - /// Long date format. Example: 1 January 2000 - /// - LongDate = 'D', - - /// - /// Short date/time format. Example: 1 January 2000 at 12:00 - /// - LongDateShortTime = 'f', - - /// - /// Long date/time format. Example: Saturday, 1 January 2000 at 12:00 - /// - LongDateTime = 'F', - - /// - /// Relative date/time format. Example: 1 second ago - /// - Relative = 'R', -} diff --git a/OliverBooth/Markdown/Timestamp/TimestampInline.cs b/OliverBooth/Markdown/Timestamp/TimestampInline.cs deleted file mode 100644 index 615ee03..0000000 --- a/OliverBooth/Markdown/Timestamp/TimestampInline.cs +++ /dev/null @@ -1,21 +0,0 @@ -using Markdig.Syntax.Inlines; - -namespace OliverBooth.Markdown.Timestamp; - -/// -/// Represents a Markdown inline element that contains a timestamp. -/// -public sealed class TimestampInline : Inline -{ - /// - /// Gets or sets the format. - /// - /// The format. - public TimestampFormat Format { get; set; } - - /// - /// Gets or sets the timestamp. - /// - /// The timestamp. - public DateTimeOffset Timestamp { get; set; } -} diff --git a/OliverBooth/Markdown/Timestamp/TimestampInlineParser.cs b/OliverBooth/Markdown/Timestamp/TimestampInlineParser.cs deleted file mode 100644 index 414fc48..0000000 --- a/OliverBooth/Markdown/Timestamp/TimestampInlineParser.cs +++ /dev/null @@ -1,91 +0,0 @@ -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; - } -} diff --git a/OliverBooth/Markdown/Timestamp/TimestampRenderer.cs b/OliverBooth/Markdown/Timestamp/TimestampRenderer.cs deleted file mode 100644 index b2c7f3d..0000000 --- a/OliverBooth/Markdown/Timestamp/TimestampRenderer.cs +++ /dev/null @@ -1,55 +0,0 @@ -using System.ComponentModel; -using Humanizer; -using Markdig.Renderers; -using Markdig.Renderers.Html; - -namespace OliverBooth.Markdown.Timestamp; - -/// -/// Represents a Markdown object renderer that renders elements. -/// -public sealed class TimestampRenderer : HtmlObjectRenderer -{ - /// - protected override void Write(HtmlRenderer renderer, TimestampInline obj) - { - DateTimeOffset timestamp = obj.Timestamp; - TimestampFormat format = obj.Format; - - 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(""); - } -} diff --git a/OliverBooth/Program.cs b/OliverBooth/Program.cs index 006a3b7..84203ce 100644 --- a/OliverBooth/Program.cs +++ b/OliverBooth/Program.cs @@ -5,9 +5,8 @@ using OliverBooth.Data.Blog; using OliverBooth.Data.Web; using OliverBooth.Extensions; using OliverBooth.Extensions.Markdig; +using OliverBooth.Extensions.Markdig.Markdown.Timestamp; using OliverBooth.Extensions.Markdig.Services; -using OliverBooth.Markdown; -using OliverBooth.Markdown.Timestamp; using OliverBooth.Services; using Serilog;