From 5678df92545ca4e144ca6121b995b55dab5d35d3 Mon Sep 17 00:00:00 2001 From: Fabrizio Ferri Benedetti Date: Tue, 22 Jul 2025 10:40:58 +0200 Subject: [PATCH 1/8] First commit --- docs/syntax/diagram.md | 99 +++++++++++++++ .../Myst/Directives/Diagram/DiagramBlock.cs | 71 +++++++++++ .../Myst/Directives/Diagram/DiagramEncoder.cs | 114 ++++++++++++++++++ .../Directives/Diagram/DiagramView.cshtml | 16 +++ .../Directives/Diagram/DiagramViewModel.cs | 13 ++ .../Myst/Directives/DirectiveBlockParser.cs | 4 + .../Myst/Directives/DirectiveHtmlRenderer.cs | 14 +++ .../Directives/DiagramTests.cs | 84 +++++++++++++ 8 files changed, 415 insertions(+) create mode 100644 docs/syntax/diagram.md create mode 100644 src/Elastic.Markdown/Myst/Directives/Diagram/DiagramBlock.cs create mode 100644 src/Elastic.Markdown/Myst/Directives/Diagram/DiagramEncoder.cs create mode 100644 src/Elastic.Markdown/Myst/Directives/Diagram/DiagramView.cshtml create mode 100644 src/Elastic.Markdown/Myst/Directives/Diagram/DiagramViewModel.cs create mode 100644 tests/Elastic.Markdown.Tests/Directives/DiagramTests.cs diff --git a/docs/syntax/diagram.md b/docs/syntax/diagram.md new file mode 100644 index 000000000..2fb8095c4 --- /dev/null +++ b/docs/syntax/diagram.md @@ -0,0 +1,99 @@ +# Diagram Directive + +The `diagram` directive allows you to render various types of diagrams using the [Kroki](https://kroki.io/) service. Kroki supports many diagram types including Mermaid, D2, Graphviz, PlantUML, and more. + +## Basic Usage + +The basic syntax for the diagram directive is: + +```markdown +::::{diagram} [diagram-type] + +:::: +``` + +If no diagram type is specified, it defaults to `mermaid`. + +## Supported Diagram Types + +The diagram directive supports the following diagram types: + +- `mermaid` - Mermaid diagrams (default) +- `d2` - D2 diagrams +- `graphviz` - Graphviz/DOT diagrams +- `plantuml` - PlantUML diagrams +- `ditaa` - Ditaa diagrams +- `erd` - Entity Relationship diagrams +- `excalidraw` - Excalidraw diagrams +- `nomnoml` - Nomnoml diagrams +- `pikchr` - Pikchr diagrams +- `structurizr` - Structurizr diagrams +- `svgbob` - Svgbob diagrams +- `vega` - Vega diagrams +- `vegalite` - Vega-Lite diagrams +- `wavedrom` - WaveDrom diagrams + +## Examples + +### Mermaid Flowchart (Default) + +::::{diagram} +flowchart LR + A[Start] --> B{Decision} + B -->|Yes| C[Action 1] + B -->|No| D[Action 2] + C --> E[End] + D --> E +:::: + +### Mermaid Sequence Diagram + +::::{diagram} mermaid +sequenceDiagram + participant A as Alice + participant B as Bob + A->>B: Hello Bob, how are you? + B-->>A: Great! +:::: + +### D2 Diagram + +::::{diagram} d2 +x -> y: hello world +y -> z: nice to meet you +:::: + +### Graphviz Diagram + +::::{diagram} graphviz +digraph G { + rankdir=LR; + A -> B -> C; + A -> C; +} +:::: + +## How It Works + +The diagram directive: + +1. **Parses** the diagram type from the directive argument +2. **Extracts** the diagram content from the directive body +3. **Encodes** the content using zlib compression and Base64URL encoding +4. **Generates** a Kroki URL in the format: `https://kroki.io/{type}/svg/{encoded-content}` +5. **Renders** an HTML `` tag that loads the diagram from Kroki + +## Error Handling + +If the diagram content is empty or the encoding fails, an error message will be displayed instead of the diagram. + +## Implementation Details + +The diagram directive is implemented using: + +- **DiagramBlock**: Parses the directive and extracts content +- **DiagramEncoder**: Handles compression and encoding using the same algorithm as the Kroki documentation +- **DiagramView**: Renders the final HTML with the Kroki URL +- **Kroki Service**: External service that generates SVG diagrams from encoded content + +The encoding follows the Kroki specification exactly, ensuring compatibility with all supported diagram types. diff --git a/src/Elastic.Markdown/Myst/Directives/Diagram/DiagramBlock.cs b/src/Elastic.Markdown/Myst/Directives/Diagram/DiagramBlock.cs new file mode 100644 index 000000000..f9a7c75fb --- /dev/null +++ b/src/Elastic.Markdown/Myst/Directives/Diagram/DiagramBlock.cs @@ -0,0 +1,71 @@ +// Licensed to Elasticsearch B.V under one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using Elastic.Markdown.Diagnostics; + +namespace Elastic.Markdown.Myst.Directives.Diagram; + +public class DiagramBlock(DirectiveBlockParser parser, ParserContext context) : DirectiveBlock(parser, context) +{ + public override string Directive => "diagram"; + + /// + /// The diagram type (e.g., "mermaid", "d2", "graphviz", "plantuml") + /// + public string? DiagramType { get; private set; } + + /// + /// The raw diagram content + /// + public string? Content { get; private set; } + + /// + /// The encoded diagram URL for Kroki service + /// + public string? EncodedUrl { get; private set; } + + public override void FinalizeAndValidate(ParserContext context) + { + // Extract diagram type from arguments or default to "mermaid" + DiagramType = !string.IsNullOrWhiteSpace(Arguments) ? Arguments.ToLowerInvariant() : "mermaid"; + + // Extract content from the directive body + Content = ExtractContent(); + + if (string.IsNullOrWhiteSpace(Content)) + { + this.EmitError("Diagram directive requires content."); + return; + } + + // Generate the encoded URL for Kroki + try + { + EncodedUrl = DiagramEncoder.GenerateKrokiUrl(DiagramType, Content); + } + catch (Exception ex) + { + this.EmitError($"Failed to encode diagram: {ex.Message}", ex); + } + } + + private string? ExtractContent() + { + if (!this.Any()) + return null; + + var lines = new List(); + foreach (var block in this) + { + if (block is Markdig.Syntax.LeafBlock leafBlock) + { + var content = leafBlock.Lines.ToString(); + if (!string.IsNullOrWhiteSpace(content)) + lines.Add(content); + } + } + + return lines.Count > 0 ? string.Join("\n", lines) : null; + } +} diff --git a/src/Elastic.Markdown/Myst/Directives/Diagram/DiagramEncoder.cs b/src/Elastic.Markdown/Myst/Directives/Diagram/DiagramEncoder.cs new file mode 100644 index 000000000..2ef7dea1b --- /dev/null +++ b/src/Elastic.Markdown/Myst/Directives/Diagram/DiagramEncoder.cs @@ -0,0 +1,114 @@ +// Licensed to Elasticsearch B.V under one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using System.IO.Compression; +using System.Text; + +namespace Elastic.Markdown.Myst.Directives.Diagram; + +/// +/// Utility class for encoding diagrams for use with Kroki service +/// +public static class DiagramEncoder +{ + /// + /// Supported diagram types for Kroki service + /// + private static readonly HashSet SupportedTypes = new(StringComparer.OrdinalIgnoreCase) + { + "mermaid", "d2", "graphviz", "plantuml", "ditaa", "erd", "excalidraw", + "nomnoml", "pikchr", "structurizr", "svgbob", "vega", "vegalite", "wavedrom" + }; + + /// + /// Generates a Kroki URL for the given diagram type and content + /// + /// The type of diagram (e.g., "mermaid", "d2") + /// The diagram content + /// The complete Kroki URL for rendering the diagram as SVG + /// Thrown when diagram type is not supported + /// Thrown when content is null or empty + public static string GenerateKrokiUrl(string diagramType, string content) + { + if (string.IsNullOrWhiteSpace(diagramType)) + throw new ArgumentException("Diagram type cannot be null or empty", nameof(diagramType)); + + if (string.IsNullOrWhiteSpace(content)) + throw new ArgumentException("Diagram content cannot be null or empty", nameof(content)); + + var normalizedType = diagramType.ToLowerInvariant(); + if (!SupportedTypes.Contains(normalizedType)) + throw new ArgumentException($"Unsupported diagram type: {diagramType}. Supported types: {string.Join(", ", SupportedTypes)}", nameof(diagramType)); + + var compressedBytes = Deflate(Encoding.UTF8.GetBytes(content)); + var encodedOutput = EncodeBase64Url(compressedBytes); + + return $"https://kroki.io/{normalizedType}/svg/{encodedOutput}"; + } + + /// + /// Compresses data using Deflate compression with zlib headers + /// + /// The data to compress + /// The compression level + /// The compressed data with zlib headers + private static byte[] Deflate(byte[] data, CompressionLevel? level = null) + { + using var memStream = new MemoryStream(); + +#if NET6_0_OR_GREATER + using (var zlibStream = level.HasValue ? new ZLibStream(memStream, level.Value, true) : new ZLibStream(memStream, CompressionMode.Compress, true)) + { + zlibStream.Write(data); + } +#else + // Reference: https://yal.cc/cs-deflatestream-zlib/#code + + // write header: + memStream.WriteByte(0x78); + memStream.WriteByte(level switch + { + CompressionLevel.NoCompression or CompressionLevel.Fastest => 0x01, + CompressionLevel.Optimal => 0x0A, + _ => 0x9C, + }); + + // write compressed data (with Deflate headers): + using (var dflStream = level.HasValue ? new DeflateStream(memStream, level.Value, true) : new DeflateStream(memStream, CompressionMode.Compress, true)) + { + dflStream.Write(data, 0, data.Length); + } + + // compute Adler-32: + uint a1 = 1, a2 = 0; + foreach (byte b in data) + { + a1 = (a1 + b) % 65521; + a2 = (a2 + a1) % 65521; + } + + memStream.WriteByte((byte)(a2 >> 8)); + memStream.WriteByte((byte)a2); + memStream.WriteByte((byte)(a1 >> 8)); + memStream.WriteByte((byte)a1); +#endif + + return memStream.ToArray(); + } + + /// + /// Encodes bytes to Base64URL format + /// + /// The bytes to encode + /// The Base64URL encoded string + private static string EncodeBase64Url(byte[] bytes) + { +#if NET9_0_OR_GREATER + // You can use this in previous version of .NET with Microsoft.Bcl.Memory package + return System.Buffers.Text.Base64Url.EncodeToString(bytes); +#else + return Convert.ToBase64String(bytes).Replace('+', '-').Replace('/', '_'); +#endif + } +} diff --git a/src/Elastic.Markdown/Myst/Directives/Diagram/DiagramView.cshtml b/src/Elastic.Markdown/Myst/Directives/Diagram/DiagramView.cshtml new file mode 100644 index 000000000..40c13b2c3 --- /dev/null +++ b/src/Elastic.Markdown/Myst/Directives/Diagram/DiagramView.cshtml @@ -0,0 +1,16 @@ +@inherits RazorSlice +@{ + var diagram = Model.DiagramBlock; + if (diagram?.EncodedUrl != null) + { +
+ @diagram.DiagramType diagram +
+ } + else + { +
+

Failed to render diagram

+
+ } +} diff --git a/src/Elastic.Markdown/Myst/Directives/Diagram/DiagramViewModel.cs b/src/Elastic.Markdown/Myst/Directives/Diagram/DiagramViewModel.cs new file mode 100644 index 000000000..bfee262d7 --- /dev/null +++ b/src/Elastic.Markdown/Myst/Directives/Diagram/DiagramViewModel.cs @@ -0,0 +1,13 @@ +// Licensed to Elasticsearch B.V under one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +namespace Elastic.Markdown.Myst.Directives.Diagram; + +public class DiagramViewModel : DirectiveViewModel +{ + /// + /// The diagram block containing the encoded URL and metadata + /// + public DiagramBlock? DiagramBlock { get; set; } +} diff --git a/src/Elastic.Markdown/Myst/Directives/DirectiveBlockParser.cs b/src/Elastic.Markdown/Myst/Directives/DirectiveBlockParser.cs index 5666cfdc1..a52a17b47 100644 --- a/src/Elastic.Markdown/Myst/Directives/DirectiveBlockParser.cs +++ b/src/Elastic.Markdown/Myst/Directives/DirectiveBlockParser.cs @@ -4,6 +4,7 @@ using System.Collections.Frozen; using Elastic.Markdown.Myst.Directives.Admonition; +using Elastic.Markdown.Myst.Directives.Diagram; using Elastic.Markdown.Myst.Directives.Image; using Elastic.Markdown.Myst.Directives.Include; using Elastic.Markdown.Myst.Directives.Mermaid; @@ -109,6 +110,9 @@ protected override DirectiveBlock CreateFencedBlock(BlockProcessor processor) if (info.IndexOf("{mermaid}") > 0) return new MermaidBlock(this, context); + if (info.IndexOf("{diagram}") > 0) + return new DiagramBlock(this, context); + if (info.IndexOf("{include}") > 0) return new IncludeBlock(this, context); diff --git a/src/Elastic.Markdown/Myst/Directives/DirectiveHtmlRenderer.cs b/src/Elastic.Markdown/Myst/Directives/DirectiveHtmlRenderer.cs index fe949f05c..4c8a6e2d6 100644 --- a/src/Elastic.Markdown/Myst/Directives/DirectiveHtmlRenderer.cs +++ b/src/Elastic.Markdown/Myst/Directives/DirectiveHtmlRenderer.cs @@ -6,6 +6,7 @@ using Elastic.Markdown.Diagnostics; using Elastic.Markdown.Myst.CodeBlocks; using Elastic.Markdown.Myst.Directives.Admonition; +using Elastic.Markdown.Myst.Directives.Diagram; using Elastic.Markdown.Myst.Directives.Dropdown; using Elastic.Markdown.Myst.Directives.Image; using Elastic.Markdown.Myst.Directives.Include; @@ -38,6 +39,9 @@ protected override void Write(HtmlRenderer renderer, DirectiveBlock directiveBlo switch (directiveBlock) { + case DiagramBlock diagramBlock: + WriteDiagram(renderer, diagramBlock); + return; case MermaidBlock mermaidBlock: WriteMermaid(renderer, mermaidBlock); return; @@ -243,6 +247,16 @@ private static void WriteTabItem(HtmlRenderer renderer, TabItemBlock block) RenderRazorSlice(slice, renderer); } + private static void WriteDiagram(HtmlRenderer renderer, DiagramBlock block) + { + var slice = DiagramView.Create(new DiagramViewModel + { + DirectiveBlock = block, + DiagramBlock = block + }); + RenderRazorSlice(slice, renderer); + } + private static void WriteMermaid(HtmlRenderer renderer, MermaidBlock block) { var slice = MermaidView.Create(new MermaidViewModel { DirectiveBlock = block }); diff --git a/tests/Elastic.Markdown.Tests/Directives/DiagramTests.cs b/tests/Elastic.Markdown.Tests/Directives/DiagramTests.cs new file mode 100644 index 000000000..b55d5eeec --- /dev/null +++ b/tests/Elastic.Markdown.Tests/Directives/DiagramTests.cs @@ -0,0 +1,84 @@ +// Licensed to Elasticsearch B.V under one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using Elastic.Markdown.Myst.Directives.Diagram; +using FluentAssertions; + +namespace Elastic.Markdown.Tests.Directives; + +public class DiagramBlockTests(ITestOutputHelper output) : DirectiveTest(output, +""" +::::{diagram} mermaid +flowchart LR + A[Start] --> B[Process] + B --> C[End] +:::: +""" +) +{ + [Fact] + public void ParsesBlock() => Block.Should().NotBeNull(); + + [Fact] + public void ExtractsDiagramType() => Block!.DiagramType.Should().Be("mermaid"); + + [Fact] + public void ExtractsContent() => Block!.Content.Should().Contain("flowchart LR"); + + [Fact] + public void GeneratesEncodedUrl() => Block!.EncodedUrl.Should().StartWith("https://kroki.io/mermaid/svg/"); + + [Fact] + public void RendersImageTag() => Html.Should().Contain("(output, +""" +::::{diagram} d2 +x -> y +y -> z +:::: +""" +) +{ + [Fact] + public void ParsesD2Block() => Block.Should().NotBeNull(); + + [Fact] + public void ExtractsD2Type() => Block!.DiagramType.Should().Be("d2"); + + [Fact] + public void ExtractsD2Content() => Block!.Content.Should().Contain("x -> y"); + + [Fact] + public void GeneratesD2EncodedUrl() => Block!.EncodedUrl.Should().StartWith("https://kroki.io/d2/svg/"); +} + +public class DiagramBlockDefaultTests(ITestOutputHelper output) : DirectiveTest(output, +""" +::::{diagram} +graph TD + A --> B +:::: +""" +) +{ + [Fact] + public void DefaultsToMermaid() => Block!.DiagramType.Should().Be("mermaid"); + + [Fact] + public void ExtractsContentWithoutType() => Block!.Content.Should().Contain("graph TD"); +} + +public class DiagramBlockEmptyTests(ITestOutputHelper output) : DirectiveTest(output, +""" +::::{diagram} +:::: +""" +) +{ + [Fact] + public void EmptyContentGeneratesError() => + Collector.Diagnostics.Should().ContainSingle(d => d.Message.Contains("Diagram directive requires content")); +} From a8a075a0b886132168e4536adaa78a3227461ddc Mon Sep 17 00:00:00 2001 From: Fabrizio Ferri Benedetti Date: Tue, 22 Jul 2025 11:00:44 +0200 Subject: [PATCH 2/8] Update docs --- docs/_docset.yml | 1 + docs/syntax/{diagram.md => diagrams.md} | 99 ++++++++++++++++++++----- 2 files changed, 80 insertions(+), 20 deletions(-) rename docs/syntax/{diagram.md => diagrams.md} (64%) diff --git a/docs/_docset.yml b/docs/_docset.yml index 96aac9673..dd3fb3441 100644 --- a/docs/_docset.yml +++ b/docs/_docset.yml @@ -86,6 +86,7 @@ toc: - file: code.md - file: comments.md - file: conditionals.md + - file: diagrams.md - file: dropdowns.md - file: definition-lists.md - file: example_blocks.md diff --git a/docs/syntax/diagram.md b/docs/syntax/diagrams.md similarity index 64% rename from docs/syntax/diagram.md rename to docs/syntax/diagrams.md index 2fb8095c4..71954896f 100644 --- a/docs/syntax/diagram.md +++ b/docs/syntax/diagrams.md @@ -1,8 +1,8 @@ -# Diagram Directive +# Diagrams The `diagram` directive allows you to render various types of diagrams using the [Kroki](https://kroki.io/) service. Kroki supports many diagram types including Mermaid, D2, Graphviz, PlantUML, and more. -## Basic Usage +## Basic usage The basic syntax for the diagram directive is: @@ -14,7 +14,7 @@ The basic syntax for the diagram directive is: If no diagram type is specified, it defaults to `mermaid`. -## Supported Diagram Types +## Supported diagram types The diagram directive supports the following diagram types: @@ -35,8 +35,12 @@ The diagram directive supports the following diagram types: ## Examples -### Mermaid Flowchart (Default) +### Mermaid flowchart (default) +::::::{tab-set} + +:::::{tab-item} Source +```markdown ::::{diagram} flowchart LR A[Start] --> B{Decision} @@ -45,9 +49,39 @@ flowchart LR C --> E[End] D --> E :::: +``` +::::: + +:::::{tab-item} Rendered +::::{diagram} +flowchart LR + A[Start] --> B{Decision} + B -->|Yes| C[Action 1] + B -->|No| D[Action 2] + C --> E[End] + D --> E +:::: +::::: + +:::::: -### Mermaid Sequence Diagram +### Mermaid sequence diagram +::::::{tab-set} + +:::::{tab-item} Source +```markdown +::::{diagram} mermaid +sequenceDiagram + participant A as Alice + participant B as Bob + A->>B: Hello Bob, how are you? + B-->>A: Great! +:::: +``` +::::: + +:::::{tab-item} Rendered ::::{diagram} mermaid sequenceDiagram participant A as Alice @@ -55,16 +89,38 @@ sequenceDiagram A->>B: Hello Bob, how are you? B-->>A: Great! :::: +::::: + +:::::: -### D2 Diagram +### D2 diagram +::::::{tab-set} + +:::::{tab-item} Source +```markdown ::::{diagram} d2 x -> y: hello world y -> z: nice to meet you :::: +``` +::::: -### Graphviz Diagram +:::::{tab-item} Rendered +::::{diagram} d2 +x -> y: hello world +y -> z: nice to meet you +:::: +::::: +:::::: + +### Graphviz diagram + +::::::{tab-set} + +:::::{tab-item} Source +```markdown ::::{diagram} graphviz digraph G { rankdir=LR; @@ -72,8 +128,22 @@ digraph G { A -> C; } :::: +``` +::::: -## How It Works +:::::{tab-item} Rendered +::::{diagram} graphviz +digraph G { + rankdir=LR; + A -> B -> C; + A -> C; +} +:::: +::::: + +:::::: + +## How it works The diagram directive: @@ -83,17 +153,6 @@ The diagram directive: 4. **Generates** a Kroki URL in the format: `https://kroki.io/{type}/svg/{encoded-content}` 5. **Renders** an HTML `` tag that loads the diagram from Kroki -## Error Handling +## Error handling If the diagram content is empty or the encoding fails, an error message will be displayed instead of the diagram. - -## Implementation Details - -The diagram directive is implemented using: - -- **DiagramBlock**: Parses the directive and extracts content -- **DiagramEncoder**: Handles compression and encoding using the same algorithm as the Kroki documentation -- **DiagramView**: Renders the final HTML with the Kroki URL -- **Kroki Service**: External service that generates SVG diagrams from encoded content - -The encoding follows the Kroki specification exactly, ensuring compatibility with all supported diagram types. From ee1cf91b5c04da3e96804678bb318ffaa29967df Mon Sep 17 00:00:00 2001 From: Fabrizio Ferri Benedetti Date: Tue, 22 Jul 2025 11:10:21 +0200 Subject: [PATCH 3/8] Update docs --- docs/syntax/diagrams.md | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/docs/syntax/diagrams.md b/docs/syntax/diagrams.md index 71954896f..ce20375de 100644 --- a/docs/syntax/diagrams.md +++ b/docs/syntax/diagrams.md @@ -143,16 +143,6 @@ digraph G { :::::: -## How it works - -The diagram directive: - -1. **Parses** the diagram type from the directive argument -2. **Extracts** the diagram content from the directive body -3. **Encodes** the content using zlib compression and Base64URL encoding -4. **Generates** a Kroki URL in the format: `https://kroki.io/{type}/svg/{encoded-content}` -5. **Renders** an HTML `` tag that loads the diagram from Kroki - ## Error handling If the diagram content is empty or the encoding fails, an error message will be displayed instead of the diagram. From 4f8afb16883ea883c7289251301c25c3ae9d4cea Mon Sep 17 00:00:00 2001 From: Fabrizio Ferri Benedetti Date: Tue, 22 Jul 2025 11:18:41 +0200 Subject: [PATCH 4/8] Fix lint issue --- diagram-directive-pr-message.md | 0 .../Myst/Directives/Diagram/DiagramEncoder.cs | 8 +++----- 2 files changed, 3 insertions(+), 5 deletions(-) create mode 100644 diagram-directive-pr-message.md diff --git a/diagram-directive-pr-message.md b/diagram-directive-pr-message.md new file mode 100644 index 000000000..e69de29bb diff --git a/src/Elastic.Markdown/Myst/Directives/Diagram/DiagramEncoder.cs b/src/Elastic.Markdown/Myst/Directives/Diagram/DiagramEncoder.cs index 2ef7dea1b..94303faf8 100644 --- a/src/Elastic.Markdown/Myst/Directives/Diagram/DiagramEncoder.cs +++ b/src/Elastic.Markdown/Myst/Directives/Diagram/DiagramEncoder.cs @@ -102,13 +102,11 @@ private static byte[] Deflate(byte[] data, CompressionLevel? level = null) /// /// The bytes to encode /// The Base64URL encoded string - private static string EncodeBase64Url(byte[] bytes) - { + private static string EncodeBase64Url(byte[] bytes) => #if NET9_0_OR_GREATER // You can use this in previous version of .NET with Microsoft.Bcl.Memory package - return System.Buffers.Text.Base64Url.EncodeToString(bytes); + System.Buffers.Text.Base64Url.EncodeToString(bytes); #else - return Convert.ToBase64String(bytes).Replace('+', '-').Replace('/', '_'); + Convert.ToBase64String(bytes).Replace('+', '-').Replace('/', '_'); #endif - } } From ca78f2be98b0af006215c3c73ead405e189d423c Mon Sep 17 00:00:00 2001 From: Fabrizio Ferri Benedetti Date: Tue, 22 Jul 2025 11:24:47 +0200 Subject: [PATCH 5/8] Delete file --- diagram-directive-pr-message.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 diagram-directive-pr-message.md diff --git a/diagram-directive-pr-message.md b/diagram-directive-pr-message.md deleted file mode 100644 index e69de29bb..000000000 From 48d7319236724efc31f9b2917920cdd629bc41e8 Mon Sep 17 00:00:00 2001 From: Fabrizio Ferri Benedetti Date: Tue, 22 Jul 2025 11:42:14 +0200 Subject: [PATCH 6/8] Add LLM generation logic and tests --- .../LlmMarkdown/LlmBlockRenderers.cs | 25 +++++++++++++ .../LlmMarkdown/LlmMarkdownOutput.fs | 35 +++++++++++++++++++ 2 files changed, 60 insertions(+) diff --git a/src/Elastic.Markdown/Myst/Renderers/LlmMarkdown/LlmBlockRenderers.cs b/src/Elastic.Markdown/Myst/Renderers/LlmMarkdown/LlmBlockRenderers.cs index 516f33a81..6ab2fd1d6 100644 --- a/src/Elastic.Markdown/Myst/Renderers/LlmMarkdown/LlmBlockRenderers.cs +++ b/src/Elastic.Markdown/Myst/Renderers/LlmMarkdown/LlmBlockRenderers.cs @@ -6,6 +6,7 @@ using Elastic.Markdown.Myst.CodeBlocks; using Elastic.Markdown.Myst.Directives; using Elastic.Markdown.Myst.Directives.Admonition; +using Elastic.Markdown.Myst.Directives.Diagram; using Elastic.Markdown.Myst.Directives.Image; using Elastic.Markdown.Myst.Directives.Include; using Markdig.Extensions.DefinitionLists; @@ -372,6 +373,9 @@ protected override void Write(LlmMarkdownRenderer renderer, DirectiveBlock obj) case IncludeBlock includeBlock: WriteIncludeBlock(renderer, includeBlock); return; + case DiagramBlock diagramBlock: + WriteDiagramBlock(renderer, diagramBlock); + return; } // Ensure single empty line before directive @@ -417,6 +421,27 @@ private static void WriteImageBlock(LlmMarkdownRenderer renderer, ImageBlock ima renderer.EnsureLine(); } + private static void WriteDiagramBlock(LlmMarkdownRenderer renderer, DiagramBlock diagramBlock) + { + renderer.EnsureBlockSpacing(); + + // Render diagram as structured comment with type information + renderer.WriteLine($""); + + // Render the diagram content with indentation + if (!string.IsNullOrWhiteSpace(diagramBlock.Content)) + { + var lines = diagramBlock.Content.Split(['\n', '\r'], StringSplitOptions.RemoveEmptyEntries); + foreach (var line in lines) + { + renderer.WriteLine(line); + } + } + + renderer.WriteLine(""); + renderer.EnsureLine(); + } + private void WriteIncludeBlock(LlmMarkdownRenderer renderer, IncludeBlock block) { if (!block.Found || block.IncludePath is null) diff --git a/tests/authoring/LlmMarkdown/LlmMarkdownOutput.fs b/tests/authoring/LlmMarkdown/LlmMarkdownOutput.fs index 275def95d..2eb1945ce 100644 --- a/tests/authoring/LlmMarkdown/LlmMarkdownOutput.fs +++ b/tests/authoring/LlmMarkdown/LlmMarkdownOutput.fs @@ -488,3 +488,38 @@ Hello, this is a substitution: {{hello-world}} Hello, this is a substitution: Hello World! ``` """ + +type ``diagram directive`` () = + static let markdown = Setup.Document """ +::::{diagram} mermaid +flowchart LR + A[Start] --> B{Decision} + B -->|Yes| C[Action 1] + B -->|No| D[Action 2] + C --> E[End] + D --> E +:::: + +::::{diagram} d2 +x -> y: hello world +y -> z: nice to meet you +:::: +""" + + [] + let ``renders diagram with type information`` () = + markdown |> convertsToNewLLM """ + +flowchart LR + A[Start] --> B{Decision} + B -->|Yes| C[Action 1] + B -->|No| D[Action 2] + C --> E[End] + D --> E + + + +x -> y: hello world +y -> z: nice to meet you + +""" From c55b287a2c45b331a64819036eaebcae77571a53 Mon Sep 17 00:00:00 2001 From: Fabrizio Ferri Benedetti Date: Tue, 22 Jul 2025 11:53:32 +0200 Subject: [PATCH 7/8] Review edit --- .../Renderers/LlmMarkdown/LlmBlockRenderers.cs | 8 +++----- tests/authoring/LlmMarkdown/LlmMarkdownOutput.fs | 16 ++++++++-------- 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/src/Elastic.Markdown/Myst/Renderers/LlmMarkdown/LlmBlockRenderers.cs b/src/Elastic.Markdown/Myst/Renderers/LlmMarkdown/LlmBlockRenderers.cs index 6ab2fd1d6..15b03e983 100644 --- a/src/Elastic.Markdown/Myst/Renderers/LlmMarkdown/LlmBlockRenderers.cs +++ b/src/Elastic.Markdown/Myst/Renderers/LlmMarkdown/LlmBlockRenderers.cs @@ -431,11 +431,9 @@ private static void WriteDiagramBlock(LlmMarkdownRenderer renderer, DiagramBlock // Render the diagram content with indentation if (!string.IsNullOrWhiteSpace(diagramBlock.Content)) { - var lines = diagramBlock.Content.Split(['\n', '\r'], StringSplitOptions.RemoveEmptyEntries); - foreach (var line in lines) - { - renderer.WriteLine(line); - } + var reader = new StringReader(diagramBlock.Content); + while (reader.ReadLine() is { } line) + renderer.WriteLine(string.IsNullOrWhiteSpace(line) ? string.Empty : " " + line); } renderer.WriteLine("
"); diff --git a/tests/authoring/LlmMarkdown/LlmMarkdownOutput.fs b/tests/authoring/LlmMarkdown/LlmMarkdownOutput.fs index 2eb1945ce..a39dd9557 100644 --- a/tests/authoring/LlmMarkdown/LlmMarkdownOutput.fs +++ b/tests/authoring/LlmMarkdown/LlmMarkdownOutput.fs @@ -510,16 +510,16 @@ y -> z: nice to meet you let ``renders diagram with type information`` () = markdown |> convertsToNewLLM """ -flowchart LR - A[Start] --> B{Decision} - B -->|Yes| C[Action 1] - B -->|No| D[Action 2] - C --> E[End] - D --> E + flowchart LR + A[Start] --> B{Decision} + B -->|Yes| C[Action 1] + B -->|No| D[Action 2] + C --> E[End] + D --> E -x -> y: hello world -y -> z: nice to meet you + x -> y: hello world + y -> z: nice to meet you """ From 419d73999e0c4b7780ef5e2e2b6aaeb00eb0b8b5 Mon Sep 17 00:00:00 2001 From: Fabrizio Ferri Benedetti Date: Tue, 22 Jul 2025 12:14:59 +0200 Subject: [PATCH 8/8] Make diagrams doc hidden --- docs/_docset.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/_docset.yml b/docs/_docset.yml index dd3fb3441..58475f7ef 100644 --- a/docs/_docset.yml +++ b/docs/_docset.yml @@ -86,7 +86,7 @@ toc: - file: code.md - file: comments.md - file: conditionals.md - - file: diagrams.md + - hidden: diagrams.md - file: dropdowns.md - file: definition-lists.md - file: example_blocks.md