Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/_docset.yml
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ toc:
- file: code.md
- file: comments.md
- file: conditionals.md
- hidden: diagrams.md
- file: dropdowns.md
- file: definition-lists.md
- file: example_blocks.md
Expand Down
148 changes: 148 additions & 0 deletions docs/syntax/diagrams.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
# 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

The basic syntax for the diagram directive is:

```markdown
::::{diagram} [diagram-type]
<diagram content>
::::
```

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)

::::::{tab-set}

:::::{tab-item} Source
```markdown
::::{diagram}
flowchart LR
A[Start] --> B{Decision}
B -->|Yes| C[Action 1]
B -->|No| D[Action 2]
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

::::::{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
participant B as Bob
A->>B: Hello Bob, how are you?
B-->>A: Great!
::::
:::::

::::::

### D2 diagram

::::::{tab-set}

:::::{tab-item} Source
```markdown
::::{diagram} d2
x -> y: hello world
y -> z: nice to meet you
::::
```
:::::

:::::{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;
A -> B -> C;
A -> C;
}
::::
```
:::::

:::::{tab-item} Rendered
::::{diagram} graphviz
digraph G {
rankdir=LR;
A -> B -> C;
A -> C;
}
::::
:::::

::::::

## Error handling

If the diagram content is empty or the encoding fails, an error message will be displayed instead of the diagram.
71 changes: 71 additions & 0 deletions src/Elastic.Markdown/Myst/Directives/Diagram/DiagramBlock.cs
Original file line number Diff line number Diff line change
@@ -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";

/// <summary>
/// The diagram type (e.g., "mermaid", "d2", "graphviz", "plantuml")
/// </summary>
public string? DiagramType { get; private set; }

/// <summary>
/// The raw diagram content
/// </summary>
public string? Content { get; private set; }

/// <summary>
/// The encoded diagram URL for Kroki service
/// </summary>
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<string>();
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;
}
}
112 changes: 112 additions & 0 deletions src/Elastic.Markdown/Myst/Directives/Diagram/DiagramEncoder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
// 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;

/// <summary>
/// Utility class for encoding diagrams for use with Kroki service
/// </summary>
public static class DiagramEncoder
{
/// <summary>
/// Supported diagram types for Kroki service
/// </summary>
private static readonly HashSet<string> SupportedTypes = new(StringComparer.OrdinalIgnoreCase)
{
"mermaid", "d2", "graphviz", "plantuml", "ditaa", "erd", "excalidraw",
"nomnoml", "pikchr", "structurizr", "svgbob", "vega", "vegalite", "wavedrom"
};

/// <summary>
/// Generates a Kroki URL for the given diagram type and content
/// </summary>
/// <param name="diagramType">The type of diagram (e.g., "mermaid", "d2")</param>
/// <param name="content">The diagram content</param>
/// <returns>The complete Kroki URL for rendering the diagram as SVG</returns>
/// <exception cref="ArgumentException">Thrown when diagram type is not supported</exception>
/// <exception cref="ArgumentNullException">Thrown when content is null or empty</exception>
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}";
}

/// <summary>
/// Compresses data using Deflate compression with zlib headers
/// </summary>
/// <param name="data">The data to compress</param>
/// <param name="level">The compression level</param>
/// <returns>The compressed data with zlib headers</returns>
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();
}

/// <summary>
/// Encodes bytes to Base64URL format
/// </summary>
/// <param name="bytes">The bytes to encode</param>
/// <returns>The Base64URL encoded string</returns>
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
System.Buffers.Text.Base64Url.EncodeToString(bytes);
#else
Convert.ToBase64String(bytes).Replace('+', '-').Replace('/', '_');
#endif
}
16 changes: 16 additions & 0 deletions src/Elastic.Markdown/Myst/Directives/Diagram/DiagramView.cshtml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
@inherits RazorSlice<Elastic.Markdown.Myst.Directives.Diagram.DiagramViewModel>
@{
var diagram = Model.DiagramBlock;
if (diagram?.EncodedUrl != null)
{
<div class="diagram" data-diagram-type="@diagram.DiagramType">
<img src="@diagram.EncodedUrl" alt="@diagram.DiagramType diagram" loading="lazy" />
</div>
}
else
{
<div class="diagram-error">
<p>Failed to render diagram</p>
</div>
}
}
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// The diagram block containing the encoded URL and metadata
/// </summary>
public DiagramBlock? DiagramBlock { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);

Expand Down
Loading
Loading