NURLNURL registrynurl-lang.org →

← mermaid-server

mermaid-server 0.1.1 API

service.nu

mermaid-server/src/service.nu — the render pipeline and the two faces that expose it: an HTTP API and an MCP server, in one process.

The pipeline is the whole program in four calls:

parse → layout → render → SVG

with the template chosen per request. Everything the HTTP routes and the MCP tools do is a wrapper around mmd_render_source.

Threading: the server runs a worker POOL, so several requests render at once, each on its own OS thread. The only state shared between them is the loaded template set, which is built once before the listener opens and is read-only from then on — no lock is needed, and none is taken on the hot path. It lives behind a global pointer rather than a captured closure environment so that every worker sees the same set.

API

: ~ i g_mmd_ts 0

@ mmd_state_init MmdTemplateSet ts → v

@ mmd_state → MmdTemplateSet

@ mmd_state_free → v

: MmdRenderRes

: MmdRenderRes {
    b ok
    String svg  // the SVG on success, the error message otherwise
    i width
    i height
    i nodes
    i edges
    i line  // parse-error position, 0 when there is none
    i col
    ( Vec String ) warnings
}

@ mmd_render_res_free MmdRenderRes r → v

@ mmd_render_source s src s tmpl → MmdRenderRes

Parse, lay out and render src with the named template (empty = the default one). The returned struct owns everything in it.

@ mmd_templates_json → Json

@ mmd_tools_list → ( Vec Json )

@ mmd_dispatch_tool s name Json args → Json

@ mmd_mcp_dispatch Json req → ?Json

One JSON-RPC request in, the reply (or None for a notification) out. The same function backs both the HTTP transport and --stdio.

@ mmd_build_app i workers b quiet → *HttpApp

Build the router for the whole service. Exposed separately from mmd_serve so the tests can drive it without opening a socket.


parse.nu

mermaid-server/src/parse.nu — the mermaid flowchart parser.

Accepts the graph / flowchart dialect: a direction header, node declarations with the thirteen mermaid shapes, and link chains with the solid / dotted / thick line styles, arrow / circle / cross heads, bidirectional links, & node groups, and both label spellings (-->|text| and -- text -->).

Everything is byte-oriented and single-pass. The cursor and the error slot live behind *MmdParser — a heap pointer, because NURL structs are passed BY VALUE and every helper here has to advance the shared cursor.

Statements the flowchart grammar has but this parser does not model (classDef, class, style, linkStyle, click, accessibility statements) are skipped with a warning that travels out on the graph, so a caller can surface "rendered, but I ignored line 7". subgraph is a hard error rather than a warning: dropping a subgraph silently would change the diagram's meaning, not just its styling.

API

: MmdParser

: MmdParser {
    s src
    i n
    i pos
    i stop  // end of the statement currently being parsed
    i line
    i line_start
    b failed
    String err
    i err_line
    i err_col
}

: MmdShapeRes

: MmdShapeRes {
    b ok
    i shape
    i from
    i to
    i pos
}

: MmdLinkRes

: MmdLinkRes {
    b ok
    i line
    i head
    i tail
    i lab_from
    i lab_to
    i pos
}

: MmdParseResult

: MmdParseResult {
    b ok
    MmdGraph graph
    String message
    i line
    i col
}

@ mmd_parse_result_free MmdParseResult r → v

@ mmd_parse s src → MmdParseResult


render.nu

mermaid-server/src/render.nu — SVG emission, entirely template-driven.

Nothing here decides what a diagram looks like. Every colour, width, radius and font comes out of the MmdTheme the caller passes in, so a new look is a new TOML file and not a new branch in this file. Two mechanisms carry the template through:

from mmd_theme_var_str, which resolves node.<shape>.<key> before node.<key>; and

canvas.css. CSS outranks presentation attributes, so a template can restyle anything the key set does not cover yet.

Arrow heads are drawn as real geometry rather than SVG markers: markers inherit neither stroke nor size cleanly across renderers, and the endpoint direction is already known here.

API

@ mmd_render_svg MmdGraph g MmdLayout l MmdTheme t → String


graph.nu

mermaid-server/src/graph.nu — the parsed diagram IR.

One flowchart is a MmdGraph: a direction, a node table, an edge table and a list of parse warnings. Nodes are addressed by INDEX everywhere downstream (layout, render) — mmd_node_index interns an id on first mention, so a forward reference (A --> B before B[Label]) resolves to the same slot the later declaration fills in.

Shapes and link kinds are integer constants rather than enum variants: they are keys into the theme (node.diamond.fill, …) and into the renderer's shape dispatch, and an integer is what both want.

API

: i MMD_SHAPE_RECT 0 // A[label]

: i MMD_SHAPE_ROUND 1 // A(label)

: i MMD_SHAPE_STADIUM 2 // A([label])

: i MMD_SHAPE_SUBROUTINE 3 // A[[label]]

: i MMD_SHAPE_CYLINDER 4 // A[(label)]

: i MMD_SHAPE_CIRCLE 5 // A((label))

: i MMD_SHAPE_DIAMOND 6 // A{label}

: i MMD_SHAPE_HEXAGON 7 // A{{label}}

: i MMD_SHAPE_PARALLELOGRAM 8 // A[/label/]

: i MMD_SHAPE_PARALLELOGRAM_ALT 9 // A[\label\]

: i MMD_SHAPE_TRAPEZOID 10 // A[/label\]

: i MMD_SHAPE_TRAPEZOID_ALT 11 // A[\label/]

: i MMD_SHAPE_FLAG 12 // A>label]

: i MMD_LINE_SOLID 0 // --- -->

Line styles.

: i MMD_LINE_DOTTED 1 // -.- -.->

: i MMD_LINE_THICK 2 // === ==>

: i MMD_ARROW_NONE 0

Arrow heads (head = the target end, tail = the source end).

: i MMD_ARROW_POINT 1 // >

: i MMD_ARROW_CIRCLE 2 // o

: i MMD_ARROW_CROSS 3 // x

: i MMD_DIR_TD 0 // graph TD / TB — top to bottom

Rank directions.

: i MMD_DIR_LR 1 // graph LR — left to right

: i MMD_DIR_BT 2 // graph BT — bottom to top

: i MMD_DIR_RL 3 // graph RL — right to left

@ mmd_shape_name i shape → s

@ mmd_line_name i line → s

@ mmd_dir_name i dir → s

@ mmd_dir_parse s raw → i

TD/TB/LR/BT/RL, case-insensitively. -1 when unrecognised.

: MmdNode

: MmdNode {
    String id
    String label
    i shape
    b declared
}

: MmdEdge

: MmdEdge {
    i from
    i to
    String label
    i line
    i head
    i tail
}

: MmdGraph

: MmdGraph {
    i dir
    ( Vec MmdNode ) nodes
    ( Vec MmdEdge ) edges
    ( Vec String ) warnings
}

@ mmd_graph_new → MmdGraph

@ mmd_graph_free MmdGraph g → v

@ mmd_node_count MmdGraph g → i

@ mmd_edge_count MmdGraph g → i

@ mmd_find_node MmdGraph g s id → i

Index of the node with this id, or -1.

@ mmd_node_index MmdGraph g s id → i

Intern id: return its index, appending an undeclared placeholder node (label = id, shape = rect) when this is the first mention.

@ mmd_node_declare MmdGraph g i idx String label i shape → v

Give node idx an explicit label + shape. CONSUMES label.

@ mmd_add_edge MmdGraph g i from i to String label i line i head i tail → v

CONSUMES label (pass an empty String for an unlabelled link).

@ mmd_warn MmdGraph g s text → v


main.nu

mermaid-server — render mermaid flowcharts to SVG over HTTP and MCP.

mermaid-server serve HTTP + MCP on 127.0.0.1:8808 mermaid-server --stdio serve MCP over stdio instead mermaid-server render diagram.mmd one-shot: SVG on stdout mermaid-server templates list the loaded templates

Templates are read from an external directory — never compiled in — so the set of looks a server offers is a deployment decision. See --templates and the resolution order in __mmdm_template_dir.

API

: s MMD_VERSION 0.1.0``

@ main → i


layout.nu

mermaid-server/src/layout.nu — layered (Sugiyama-style) flowchart layout.

Five passes, all integer arithmetic so the emitted SVG carries no float formatting:

  1. size — every node measured from its label and its shape, using

the template's font size, padding and glyph scale.

  1. rank — depth-first search marks the back edges that would make

the graph cyclic, then a longest-path pass over what is left assigns each node to a layer.

  1. split — an edge crossing more than one layer boundary is cut

into unit-length segments joined by DUMMY nodes, one per layer it passes through. Dummies are laid out like any other node, so a long edge reserves its own corridor instead of being drawn straight through whatever happens to sit between its endpoints, and the renderer gets the bend points as the edge's route.

  1. order — barycentre sweeps down and up the layers to pull linked

nodes towards each other, which is what removes most crossings. Dummies take part, so long edges bend towards their neighbours rather than cutting across them.

  1. place — cross-axis positions from the ordering, refined twice

towards the mean of each node's neighbours and then de-overlapped; layer positions from the running maximum layer thickness.

The result is direction-agnostic until the last step: everything is computed on a (main, cross) axis pair and only then mapped to x/y for TD / LR / BT / RL.

API

: MmdPt

: MmdPt {
    i x
    i y
}

: MmdBox

: MmdBox {
    i x
    i y
    i w
    i h
    i rank
    i order
}

: MmdSize

: MmdSize {
    i w
    i h
}

A width/height pair.

: MmdLayout

: MmdLayout {
    ( Vec MmdBox ) boxes
    ( Vec MmdPt ) route  // every edge's bend points, concatenated
    ( Vec i ) route_start  // per edge: first bend point
    ( Vec i ) route_len  // per edge: how many
    i width
    i height
    i ranks
}

@ mmd_layout_free MmdLayout l → v

@ mmd_layout_box MmdLayout l i idx → MmdBox

@ mmd_layout_bends MmdLayout l i e → i

How many bend points edge e routes through (0 for a direct link).

@ mmd_layout_bend MmdLayout l i e i k → MmdPt

@ _mmdl_text_units s text → i

Width in percent-of-font-size units of the widest \n-separated line.

@ mmd_text_lines s text → i

@ mmd_layout MmdGraph g MmdTheme t → MmdLayout


theme.nu

mermaid-server/src/theme.nu — templates: the look, loaded from outside.

The renderer holds no colours, no fonts and no spacing of its own. Every value it draws with comes from a TEMPLATE — a TOML file read from an external source at startup — looked up by a dotted key:

canvas.background layout.rank_gap node.fill node.diamond.fill

A theme is therefore just a flat key → string store: the TOML tree is flattened on load, and the renderer asks for node.<shape>.<key> and falls back to node.<key>. Adding a look means dropping a file in the template directory; adding a knob means one mmd_theme_* call in the renderer plus a line of documentation. Neither needs a new type.

Values are stored as strings because that is what SVG consumes. TOML has no float in this stdlib, so a non-integer (stroke_width = "1.5") is written quoted and travels through verbatim; the integer accessor parses on demand for the values layout needs as numbers.

MmdTemplateSet.kind is the source discriminator. Only MMD_TSRC_DIR (a filesystem directory) exists today; a registry- or HTTP-backed source becomes a second kind and a second branch in mmd_templates_load, with nothing else in the program changing.

API

: i MMD_TSRC_DIR 0

: MmdKV

: MmdKV {
    String key
    String val
}

: MmdTheme

: MmdTheme {
    String name
    String desc
    ( Vec MmdKV ) kv
}

@ mmd_theme_new s name → MmdTheme

@ mmd_theme_free MmdTheme t → v

@ mmd_theme_set MmdTheme t s key s val → v

Last write wins, so a [node.diamond] table may restate a key set in [node].

@ mmd_theme_str MmdTheme t s key s deflt → s

The stored value, or deflt. BORROWS: the returned pointer is valid while the theme is.

@ mmd_theme_has MmdTheme t s key → b

@ mmd_theme_int MmdTheme t s key i deflt → i

@ mmd_theme_var_str MmdTheme t s prefix s variant s key s deflt → s

<prefix>.<variant>.<key> if present, else <prefix>.<key>, else deflt. This two-step is what makes a template able to restyle one shape (or one line style) without restating the rest.

@ mmd_theme_var_int MmdTheme t s prefix s variant s key i deflt → i

: MmdThemeRes

: MmdThemeRes {
    b ok
    MmdTheme theme
    String message
}

@ mmd_theme_parse s name s src → MmdThemeRes

Parse one template's TOML text. name is the fallback display name (the file stem) when the file omits name = "...".

: MmdTemplate

: MmdTemplate {
    String name
    String path
    MmdTheme theme
}

: MmdTemplateSet

: MmdTemplateSet {
    i kind
    String root
    ( Vec MmdTemplate ) items
    String default_name
}

@ mmd_templates_free MmdTemplateSet ts → v

@ mmd_templates_count MmdTemplateSet ts → i

@ mmd_templates_find MmdTemplateSet ts s name → i

Index of the template with this name, or -1. An empty name means "the default one".

@ mmd_templates_theme MmdTemplateSet ts i idx → MmdTheme

: MmdTemplatesRes

: MmdTemplatesRes {
    b ok
    MmdTemplateSet set
    String message
}

@ mmd_templates_load i kind s root → MmdTemplatesRes

The one entry point a caller needs: load a template set from root using source kind.