diff --git a/ConsoleMarkdownRenderer.Example/data/example.md b/ConsoleMarkdownRenderer.Example/data/example.md index ad542d7..715c1c0 100644 --- a/ConsoleMarkdownRenderer.Example/data/example.md +++ b/ConsoleMarkdownRenderer.Example/data/example.md @@ -196,6 +196,16 @@ Inside code spans (`:smile:`) and code blocks, the original text is preserved: Some text after the block +## Mathematics (LaTeX source rendered verbatim) + +Markdig parses inline math like $E = mc^2$ and block math like + +$$ +\int_0^1 x^2 dx = \frac{1}{3} +$$ + +into AST nodes. Terminals cannot typeset LaTeX, so the raw source is rendered with a distinctive style. + ### TODOs - [x] Code Blocks @@ -217,6 +227,7 @@ Some text after the block - [x] Custom containers (admonitions) - [x] Abbreviations - [x] Figures +- [x] Mathematics (inline `$...$` and block `$$...$$` LaTeX source) - [ ] One to always leave unchecked And here is the end \ No newline at end of file diff --git a/ConsoleMarkdownRenderer.Tests/RendererTests.cs b/ConsoleMarkdownRenderer.Tests/RendererTests.cs index b8479d3..855b335 100644 --- a/ConsoleMarkdownRenderer.Tests/RendererTests.cs +++ b/ConsoleMarkdownRenderer.Tests/RendererTests.cs @@ -86,6 +86,22 @@ public void RendererTests_CodeInlineTest(bool useCrazy) AssertMarkdownYieldsFormat("codeInline", "in line code", new Style(foreground: Color.Yellow, background: Color.Blue), useCrazy); } + [TestMethod] + [DataRow(false)] + [DataRow(true)] + public void RendererTests_MathInlineTest(bool useCrazy) + { + AssertMarkdownYieldsFormat("mathInline", "E = mc^2", new Style(foreground: Color.Green, background: Color.Purple), useCrazy); + } + + [TestMethod] + [DataRow(false)] + [DataRow(true)] + public void RendererTests_MathBlockTest(bool useCrazy) + { + AssertMarkdownYieldsFormat("mathBlock", "\\int_0^1 x^2 dx = \\frac{1}{3}", new Style(foreground: Color.Green, background: Color.Purple), useCrazy); + } + [TestMethod] public void RendererTests_FencedCodeBlockInfoDisabledByDefault() { @@ -876,6 +892,10 @@ private static Dictionary Counts(string text) Inserted = c_crazyFormat, Italic = c_crazyFormat, Marked = c_crazyFormat, + MathBlock = c_crazyFormat, + MathBlockLabel = c_crazyFormat, + MathBlockLabelText = "math", + MathInline = c_crazyFormat, QuotedBlock = c_crazyFormat, ShowFencedCodeBlockInfo = true, Strikethrough = c_crazyFormat, diff --git a/ConsoleMarkdownRenderer.Tests/resources/bracketEscaping.md b/ConsoleMarkdownRenderer.Tests/resources/bracketEscaping.md index 0330cbb..5852340 100644 --- a/ConsoleMarkdownRenderer.Tests/resources/bracketEscaping.md +++ b/ConsoleMarkdownRenderer.Tests/resources/bracketEscaping.md @@ -47,3 +47,9 @@ Paragraph mentioning testAbbr abbreviation usage. ^^^ [test30] figure caption with **[test31] bold** content. ![[test32] alt](http://example.com/img.png) ^^^ + +Inline math $[test33] \frac{1}{2}$ embedded. + +$$ +[test34] \int_0^1 dx +$$ diff --git a/ConsoleMarkdownRenderer.Tests/resources/bracketEscaping.txt b/ConsoleMarkdownRenderer.Tests/resources/bracketEscaping.txt index 080ff0c..60b10f5 100644 --- a/ConsoleMarkdownRenderer.Tests/resources/bracketEscaping.txt +++ b/ConsoleMarkdownRenderer.Tests/resources/bracketEscaping.txt @@ -53,6 +53,12 @@ │ │ [test30] figure caption with [test31] bold content. │ │ │ │ ![[test32] alt](http://example.com/img.png) │ │ │ └─────────────────────────────────────────────────────┘ │ +│ Inline math [test33] \frac{1}{2} embedded. │ +│ ┌────────────────────────┐ │ +│ │ │ │ +│ │ [test34] \int_0^1 dx │ │ +│ │ │ │ +│ └────────────────────────┘ │ │ ┌──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ │ │ │ ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── │ │ │ │ ┌───────────────────────────────────────────────────────┐ │ │ diff --git a/ConsoleMarkdownRenderer.Tests/resources/mathBlock.md b/ConsoleMarkdownRenderer.Tests/resources/mathBlock.md new file mode 100644 index 0000000..1e8c5b8 --- /dev/null +++ b/ConsoleMarkdownRenderer.Tests/resources/mathBlock.md @@ -0,0 +1,3 @@ +$$ +\int_0^1 x^2 dx = \frac{1}{3} +$$ diff --git a/ConsoleMarkdownRenderer.Tests/resources/mathBlock.txt b/ConsoleMarkdownRenderer.Tests/resources/mathBlock.txt new file mode 100644 index 0000000..e13172f --- /dev/null +++ b/ConsoleMarkdownRenderer.Tests/resources/mathBlock.txt @@ -0,0 +1,7 @@ +┌─────────────────────────────────────┐ +│ ┌─────────────────────────────────┐ │ +│ │ │ │ +│ │ \int_0^1 x^2 dx = \frac{1}{3} │ │ +│ │ │ │ +│ └─────────────────────────────────┘ │ +└─────────────────────────────────────┘ diff --git a/ConsoleMarkdownRenderer.Tests/resources/mathInline.md b/ConsoleMarkdownRenderer.Tests/resources/mathInline.md new file mode 100644 index 0000000..53af194 --- /dev/null +++ b/ConsoleMarkdownRenderer.Tests/resources/mathInline.md @@ -0,0 +1 @@ +An inline math expression $E = mc^2$ inside a paragraph. diff --git a/ConsoleMarkdownRenderer.Tests/resources/mathInline.txt b/ConsoleMarkdownRenderer.Tests/resources/mathInline.txt new file mode 100644 index 0000000..60be942 --- /dev/null +++ b/ConsoleMarkdownRenderer.Tests/resources/mathInline.txt @@ -0,0 +1,3 @@ +┌────────────────────────────────────────────────────────┐ +│ An inline math expression E = mc^2 inside a paragraph. │ +└────────────────────────────────────────────────────────┘ diff --git a/DisplayOptions.cs b/DisplayOptions.cs index a8ced68..403bd6c 100644 --- a/DisplayOptions.cs +++ b/DisplayOptions.cs @@ -127,6 +127,34 @@ public sealed class DisplayOptions /// public TextStyle Marked { get; set; } = new(foreground: TextColor.Black, background: TextColor.Yellow); + /// + /// Style applied to the verbatim source of a + /// (display math delimited by $$ ... $$). Terminals cannot typeset LaTeX, so the raw + /// source is rendered with this style inside a fenced presentation similar to a code block. + /// + public TextStyle MathBlock { get; set; } = new(foreground: TextColor.Green, background: TextColor.Purple); + + /// + /// Style applied to the optional label emitted at the top of a + /// when is non-empty. + /// + public TextStyle MathBlockLabel { get; set; } = new(foreground: TextColor.Yellow, background: TextColor.Purple); + + /// + /// Text used for the optional label, rendered at the top + /// of each math block similar to how emits the language identifier for + /// a fenced code block. When or empty, no label is emitted (the default). + /// + public string MathBlockLabelText { get; set; } = string.Empty; + + /// + /// Style applied to the verbatim source of a + /// (inline math delimited by $ ... $). Rendered with a code-like style so callers can + /// distinguish it visually from prose; defaults differ from so math is + /// also distinguishable from code. + /// + public TextStyle MathInline { get; set; } = new(foreground: TextColor.Green, background: TextColor.Purple); + public TextStyle QuotedBlock { get; set; } = new(decoration: TextDecoration.Italic); /// @@ -196,6 +224,10 @@ public sealed class DisplayOptions Inserted = this.Inserted, Italic = this.Italic, Marked = this.Marked, + MathBlock = this.MathBlock, + MathBlockLabel = this.MathBlockLabel, + MathBlockLabelText = this.MathBlockLabelText, + MathInline = this.MathInline, QuotedBlock = this.QuotedBlock, ShowAbbreviationTitle = this.ShowAbbreviationTitle, ShowFencedCodeBlockInfo = this.ShowFencedCodeBlockInfo, diff --git a/ObjectRenderers/ConsoleMathBlockRenderer.cs b/ObjectRenderers/ConsoleMathBlockRenderer.cs new file mode 100644 index 0000000..eb822c9 --- /dev/null +++ b/ObjectRenderers/ConsoleMathBlockRenderer.cs @@ -0,0 +1,42 @@ +using BoxOfYellow.ConsoleMarkdownRenderer.Styling; +using Markdig.Extensions.Mathematics; + +namespace BoxOfYellow.ConsoleMarkdownRenderer.ObjectRenderers +{ + internal class ConsoleMathBlockRenderer : ConsoleObjectRenderer + { + protected override void Write(ConsoleRenderer renderer, MathBlock obj) + { + renderer + .NewFrame() + .PushStyle(renderer.Options.MathBlock.ToSpectreStyle()) + .StartInline() + .AddInLine(Environment.NewLine); + + if (!string.IsNullOrEmpty(renderer.Options.MathBlockLabelText)) + { + renderer + .AddInLine($"[{renderer.Options.MathBlockLabel.ToSpectreStyle().ToMarkup()}]") + .WriteEscape($" [{renderer.Options.MathBlockLabelText}]") + .AddInLine("[/]") + .AddInLine(Environment.NewLine); + } + + for (int i = 0; i < obj.Lines.Lines.Length; i++) + { + if (!string.IsNullOrEmpty(obj.Lines.Lines[i].Slice.Text)) + { + renderer + .AddInLine(" ") + .WriteEscape(ref obj.Lines.Lines[i].Slice) + .AddInLine(Environment.NewLine); + } + } + + renderer + .EndInline() + .PopStyle() + .CompleteFrame(); + } + } +} diff --git a/ObjectRenderers/ConsoleObjectRenderers.cs b/ObjectRenderers/ConsoleObjectRenderers.cs index 15b0f17..676a8cb 100644 --- a/ObjectRenderers/ConsoleObjectRenderers.cs +++ b/ObjectRenderers/ConsoleObjectRenderers.cs @@ -3,6 +3,7 @@ using Markdig.Extensions.DefinitionLists; using Markdig.Extensions.Figures; using Markdig.Extensions.Footnotes; +using Markdig.Extensions.Mathematics; using Markdig.Extensions.TaskLists; using Markdig.Renderers; using Markdig.Syntax; @@ -213,6 +214,15 @@ protected override void Write(ConsoleRenderer renderer, LiteralInline obj) => renderer.WriteEscape(ref obj.Content); } + internal class ConsoleMathInlineRenderer : ConsoleObjectRenderer + { + protected override void Write(ConsoleRenderer renderer, MathInline obj) + => renderer + .AddInLine($"[{renderer.Options.MathInline.ToSpectreStyle().ToMarkup()}]") + .WriteEscape(ref obj.Content) + .AddInLine("[/]"); + } + internal class ConsoleParagraphBlockRenderer : ConsoleObjectRenderer { protected override void Write(ConsoleRenderer renderer, ParagraphBlock obj) diff --git a/ObjectRenderers/ConsoleRenderer.cs b/ObjectRenderers/ConsoleRenderer.cs index 1b1e946..7d22c2a 100644 --- a/ObjectRenderers/ConsoleRenderer.cs +++ b/ObjectRenderers/ConsoleRenderer.cs @@ -18,6 +18,11 @@ internal ConsoleRenderer(DisplayOptions options, bool omitAutolinkInlineRenderer ObjectRenderers.AddRange([ new ConsoleAbbreviationInlineRenderer(), + // ConsoleMathBlockRenderer must precede ConsoleCodeBlockRenderer because + // Markdig's MathBlock extends FencedCodeBlock (which extends CodeBlock), + // and renderer dispatch uses type assignability — so the math-specific + // renderer has to win before the code-block renderer claims the type. + new ConsoleMathBlockRenderer(), new ConsoleCodeBlockRenderer(), new ConsoleCodeInlineRenderer(), new ConsoleCustomContainerInlineRenderer(), @@ -43,6 +48,7 @@ internal ConsoleRenderer(DisplayOptions options, bool omitAutolinkInlineRenderer new ConsoleListBlockRenderer(), new ConsoleListItemBlockRenderer(), new ConsoleLiteralInlineRenderer(), + new ConsoleMathInlineRenderer(), new ConsoleParagraphBlockRenderer(), new ConsoleQuoteBlockRenderer(), new ConsoleTableCellRenderer(), diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 1045250..949e277 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -49,6 +49,24 @@ - After Image - [#144](https://github.com/boxofyellow/ConsoleMarkdownRenderer): Expose Spectre.Console Rule style for thematic breaks +- [#145](https://github.com/boxofyellow/ConsoleMarkdownRenderer/pull/145): Render Markdig MathInline and MathBlock nodes + - ```markdown + Inline math $E = mc^2$ and block math: + + $$ + \int_0^1 x^2 dx = \frac{1}{3} + $$ + ``` + - Rendered + Inline math $E = mc^2$ and block math: + + $$ + \int_0^1 x^2 dx = \frac{1}{3} + $$ + - Before + Image + - After + Image ### :wrench: Internal Improvements :wrench: - [#129](https://github.com/boxofyellow/ConsoleMarkdownRenderer/pull/129): Use ConfigureAwait(false) on awaits in published library code