Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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