Skip to content

Commit 5678df9

Browse files
committed
First commit
1 parent 408c009 commit 5678df9

File tree

8 files changed

+415
-0
lines changed

8 files changed

+415
-0
lines changed

docs/syntax/diagram.md

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
# Diagram Directive
2+
3+
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.
4+
5+
## Basic Usage
6+
7+
The basic syntax for the diagram directive is:
8+
9+
```markdown
10+
::::{diagram} [diagram-type]
11+
<diagram content>
12+
::::
13+
```
14+
15+
If no diagram type is specified, it defaults to `mermaid`.
16+
17+
## Supported Diagram Types
18+
19+
The diagram directive supports the following diagram types:
20+
21+
- `mermaid` - Mermaid diagrams (default)
22+
- `d2` - D2 diagrams
23+
- `graphviz` - Graphviz/DOT diagrams
24+
- `plantuml` - PlantUML diagrams
25+
- `ditaa` - Ditaa diagrams
26+
- `erd` - Entity Relationship diagrams
27+
- `excalidraw` - Excalidraw diagrams
28+
- `nomnoml` - Nomnoml diagrams
29+
- `pikchr` - Pikchr diagrams
30+
- `structurizr` - Structurizr diagrams
31+
- `svgbob` - Svgbob diagrams
32+
- `vega` - Vega diagrams
33+
- `vegalite` - Vega-Lite diagrams
34+
- `wavedrom` - WaveDrom diagrams
35+
36+
## Examples
37+
38+
### Mermaid Flowchart (Default)
39+
40+
::::{diagram}
41+
flowchart LR
42+
A[Start] --> B{Decision}
43+
B -->|Yes| C[Action 1]
44+
B -->|No| D[Action 2]
45+
C --> E[End]
46+
D --> E
47+
::::
48+
49+
### Mermaid Sequence Diagram
50+
51+
::::{diagram} mermaid
52+
sequenceDiagram
53+
participant A as Alice
54+
participant B as Bob
55+
A->>B: Hello Bob, how are you?
56+
B-->>A: Great!
57+
::::
58+
59+
### D2 Diagram
60+
61+
::::{diagram} d2
62+
x -> y: hello world
63+
y -> z: nice to meet you
64+
::::
65+
66+
### Graphviz Diagram
67+
68+
::::{diagram} graphviz
69+
digraph G {
70+
rankdir=LR;
71+
A -> B -> C;
72+
A -> C;
73+
}
74+
::::
75+
76+
## How It Works
77+
78+
The diagram directive:
79+
80+
1. **Parses** the diagram type from the directive argument
81+
2. **Extracts** the diagram content from the directive body
82+
3. **Encodes** the content using zlib compression and Base64URL encoding
83+
4. **Generates** a Kroki URL in the format: `https://kroki.io/{type}/svg/{encoded-content}`
84+
5. **Renders** an HTML `<img>` tag that loads the diagram from Kroki
85+
86+
## Error Handling
87+
88+
If the diagram content is empty or the encoding fails, an error message will be displayed instead of the diagram.
89+
90+
## Implementation Details
91+
92+
The diagram directive is implemented using:
93+
94+
- **DiagramBlock**: Parses the directive and extracts content
95+
- **DiagramEncoder**: Handles compression and encoding using the same algorithm as the Kroki documentation
96+
- **DiagramView**: Renders the final HTML with the Kroki URL
97+
- **Kroki Service**: External service that generates SVG diagrams from encoded content
98+
99+
The encoding follows the Kroki specification exactly, ensuring compatibility with all supported diagram types.
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
// Licensed to Elasticsearch B.V under one or more agreements.
2+
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
3+
// See the LICENSE file in the project root for more information
4+
5+
using Elastic.Markdown.Diagnostics;
6+
7+
namespace Elastic.Markdown.Myst.Directives.Diagram;
8+
9+
public class DiagramBlock(DirectiveBlockParser parser, ParserContext context) : DirectiveBlock(parser, context)
10+
{
11+
public override string Directive => "diagram";
12+
13+
/// <summary>
14+
/// The diagram type (e.g., "mermaid", "d2", "graphviz", "plantuml")
15+
/// </summary>
16+
public string? DiagramType { get; private set; }
17+
18+
/// <summary>
19+
/// The raw diagram content
20+
/// </summary>
21+
public string? Content { get; private set; }
22+
23+
/// <summary>
24+
/// The encoded diagram URL for Kroki service
25+
/// </summary>
26+
public string? EncodedUrl { get; private set; }
27+
28+
public override void FinalizeAndValidate(ParserContext context)
29+
{
30+
// Extract diagram type from arguments or default to "mermaid"
31+
DiagramType = !string.IsNullOrWhiteSpace(Arguments) ? Arguments.ToLowerInvariant() : "mermaid";
32+
33+
// Extract content from the directive body
34+
Content = ExtractContent();
35+
36+
if (string.IsNullOrWhiteSpace(Content))
37+
{
38+
this.EmitError("Diagram directive requires content.");
39+
return;
40+
}
41+
42+
// Generate the encoded URL for Kroki
43+
try
44+
{
45+
EncodedUrl = DiagramEncoder.GenerateKrokiUrl(DiagramType, Content);
46+
}
47+
catch (Exception ex)
48+
{
49+
this.EmitError($"Failed to encode diagram: {ex.Message}", ex);
50+
}
51+
}
52+
53+
private string? ExtractContent()
54+
{
55+
if (!this.Any())
56+
return null;
57+
58+
var lines = new List<string>();
59+
foreach (var block in this)
60+
{
61+
if (block is Markdig.Syntax.LeafBlock leafBlock)
62+
{
63+
var content = leafBlock.Lines.ToString();
64+
if (!string.IsNullOrWhiteSpace(content))
65+
lines.Add(content);
66+
}
67+
}
68+
69+
return lines.Count > 0 ? string.Join("\n", lines) : null;
70+
}
71+
}
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
// Licensed to Elasticsearch B.V under one or more agreements.
2+
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
3+
// See the LICENSE file in the project root for more information
4+
5+
using System.IO.Compression;
6+
using System.Text;
7+
8+
namespace Elastic.Markdown.Myst.Directives.Diagram;
9+
10+
/// <summary>
11+
/// Utility class for encoding diagrams for use with Kroki service
12+
/// </summary>
13+
public static class DiagramEncoder
14+
{
15+
/// <summary>
16+
/// Supported diagram types for Kroki service
17+
/// </summary>
18+
private static readonly HashSet<string> SupportedTypes = new(StringComparer.OrdinalIgnoreCase)
19+
{
20+
"mermaid", "d2", "graphviz", "plantuml", "ditaa", "erd", "excalidraw",
21+
"nomnoml", "pikchr", "structurizr", "svgbob", "vega", "vegalite", "wavedrom"
22+
};
23+
24+
/// <summary>
25+
/// Generates a Kroki URL for the given diagram type and content
26+
/// </summary>
27+
/// <param name="diagramType">The type of diagram (e.g., "mermaid", "d2")</param>
28+
/// <param name="content">The diagram content</param>
29+
/// <returns>The complete Kroki URL for rendering the diagram as SVG</returns>
30+
/// <exception cref="ArgumentException">Thrown when diagram type is not supported</exception>
31+
/// <exception cref="ArgumentNullException">Thrown when content is null or empty</exception>
32+
public static string GenerateKrokiUrl(string diagramType, string content)
33+
{
34+
if (string.IsNullOrWhiteSpace(diagramType))
35+
throw new ArgumentException("Diagram type cannot be null or empty", nameof(diagramType));
36+
37+
if (string.IsNullOrWhiteSpace(content))
38+
throw new ArgumentException("Diagram content cannot be null or empty", nameof(content));
39+
40+
var normalizedType = diagramType.ToLowerInvariant();
41+
if (!SupportedTypes.Contains(normalizedType))
42+
throw new ArgumentException($"Unsupported diagram type: {diagramType}. Supported types: {string.Join(", ", SupportedTypes)}", nameof(diagramType));
43+
44+
var compressedBytes = Deflate(Encoding.UTF8.GetBytes(content));
45+
var encodedOutput = EncodeBase64Url(compressedBytes);
46+
47+
return $"https://kroki.io/{normalizedType}/svg/{encodedOutput}";
48+
}
49+
50+
/// <summary>
51+
/// Compresses data using Deflate compression with zlib headers
52+
/// </summary>
53+
/// <param name="data">The data to compress</param>
54+
/// <param name="level">The compression level</param>
55+
/// <returns>The compressed data with zlib headers</returns>
56+
private static byte[] Deflate(byte[] data, CompressionLevel? level = null)
57+
{
58+
using var memStream = new MemoryStream();
59+
60+
#if NET6_0_OR_GREATER
61+
using (var zlibStream = level.HasValue ? new ZLibStream(memStream, level.Value, true) : new ZLibStream(memStream, CompressionMode.Compress, true))
62+
{
63+
zlibStream.Write(data);
64+
}
65+
#else
66+
// Reference: https://yal.cc/cs-deflatestream-zlib/#code
67+
68+
// write header:
69+
memStream.WriteByte(0x78);
70+
memStream.WriteByte(level switch
71+
{
72+
CompressionLevel.NoCompression or CompressionLevel.Fastest => 0x01,
73+
CompressionLevel.Optimal => 0x0A,
74+
_ => 0x9C,
75+
});
76+
77+
// write compressed data (with Deflate headers):
78+
using (var dflStream = level.HasValue ? new DeflateStream(memStream, level.Value, true) : new DeflateStream(memStream, CompressionMode.Compress, true))
79+
{
80+
dflStream.Write(data, 0, data.Length);
81+
}
82+
83+
// compute Adler-32:
84+
uint a1 = 1, a2 = 0;
85+
foreach (byte b in data)
86+
{
87+
a1 = (a1 + b) % 65521;
88+
a2 = (a2 + a1) % 65521;
89+
}
90+
91+
memStream.WriteByte((byte)(a2 >> 8));
92+
memStream.WriteByte((byte)a2);
93+
memStream.WriteByte((byte)(a1 >> 8));
94+
memStream.WriteByte((byte)a1);
95+
#endif
96+
97+
return memStream.ToArray();
98+
}
99+
100+
/// <summary>
101+
/// Encodes bytes to Base64URL format
102+
/// </summary>
103+
/// <param name="bytes">The bytes to encode</param>
104+
/// <returns>The Base64URL encoded string</returns>
105+
private static string EncodeBase64Url(byte[] bytes)
106+
{
107+
#if NET9_0_OR_GREATER
108+
// You can use this in previous version of .NET with Microsoft.Bcl.Memory package
109+
return System.Buffers.Text.Base64Url.EncodeToString(bytes);
110+
#else
111+
return Convert.ToBase64String(bytes).Replace('+', '-').Replace('/', '_');
112+
#endif
113+
}
114+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
@inherits RazorSlice<Elastic.Markdown.Myst.Directives.Diagram.DiagramViewModel>
2+
@{
3+
var diagram = Model.DiagramBlock;
4+
if (diagram?.EncodedUrl != null)
5+
{
6+
<div class="diagram" data-diagram-type="@diagram.DiagramType">
7+
<img src="@diagram.EncodedUrl" alt="@diagram.DiagramType diagram" loading="lazy" />
8+
</div>
9+
}
10+
else
11+
{
12+
<div class="diagram-error">
13+
<p>Failed to render diagram</p>
14+
</div>
15+
}
16+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// Licensed to Elasticsearch B.V under one or more agreements.
2+
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
3+
// See the LICENSE file in the project root for more information
4+
5+
namespace Elastic.Markdown.Myst.Directives.Diagram;
6+
7+
public class DiagramViewModel : DirectiveViewModel
8+
{
9+
/// <summary>
10+
/// The diagram block containing the encoded URL and metadata
11+
/// </summary>
12+
public DiagramBlock? DiagramBlock { get; set; }
13+
}

src/Elastic.Markdown/Myst/Directives/DirectiveBlockParser.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
using System.Collections.Frozen;
66
using Elastic.Markdown.Myst.Directives.Admonition;
7+
using Elastic.Markdown.Myst.Directives.Diagram;
78
using Elastic.Markdown.Myst.Directives.Image;
89
using Elastic.Markdown.Myst.Directives.Include;
910
using Elastic.Markdown.Myst.Directives.Mermaid;
@@ -109,6 +110,9 @@ protected override DirectiveBlock CreateFencedBlock(BlockProcessor processor)
109110
if (info.IndexOf("{mermaid}") > 0)
110111
return new MermaidBlock(this, context);
111112

113+
if (info.IndexOf("{diagram}") > 0)
114+
return new DiagramBlock(this, context);
115+
112116
if (info.IndexOf("{include}") > 0)
113117
return new IncludeBlock(this, context);
114118

src/Elastic.Markdown/Myst/Directives/DirectiveHtmlRenderer.cs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using Elastic.Markdown.Diagnostics;
77
using Elastic.Markdown.Myst.CodeBlocks;
88
using Elastic.Markdown.Myst.Directives.Admonition;
9+
using Elastic.Markdown.Myst.Directives.Diagram;
910
using Elastic.Markdown.Myst.Directives.Dropdown;
1011
using Elastic.Markdown.Myst.Directives.Image;
1112
using Elastic.Markdown.Myst.Directives.Include;
@@ -38,6 +39,9 @@ protected override void Write(HtmlRenderer renderer, DirectiveBlock directiveBlo
3839

3940
switch (directiveBlock)
4041
{
42+
case DiagramBlock diagramBlock:
43+
WriteDiagram(renderer, diagramBlock);
44+
return;
4145
case MermaidBlock mermaidBlock:
4246
WriteMermaid(renderer, mermaidBlock);
4347
return;
@@ -243,6 +247,16 @@ private static void WriteTabItem(HtmlRenderer renderer, TabItemBlock block)
243247
RenderRazorSlice(slice, renderer);
244248
}
245249

250+
private static void WriteDiagram(HtmlRenderer renderer, DiagramBlock block)
251+
{
252+
var slice = DiagramView.Create(new DiagramViewModel
253+
{
254+
DirectiveBlock = block,
255+
DiagramBlock = block
256+
});
257+
RenderRazorSlice(slice, renderer);
258+
}
259+
246260
private static void WriteMermaid(HtmlRenderer renderer, MermaidBlock block)
247261
{
248262
var slice = MermaidView.Create(new MermaidViewModel { DirectiveBlock = block });

0 commit comments

Comments
 (0)