Sign in

    diagrams-plantuml

    PlantUML for diagrams.mbt: native activity, regex, EBNF, JSON and YAML readers, writers and conversion to diagram models

    Download zip
    Author
    Version
    0.3.0
    License
    Apache-2.0
    Last updated
    2 hours ago
    Downloads
    4

    #moonrockz/diagrams-plantuml

    PlantUML for diagrams.mbt. This module reads native PlantUML regex, EBNF, JSON and YAML blocks into lossless syntax trees and typed ASTs, writes canonical PlantUML, and converts the blocks to format-independent models in moonrockz/diagrams. render_svg composes reading, conversion and the moonrockz/diagrams-svg renderer.

    The module follows PlantUML 1.2026.9 (commit 18d0aae311a4f1eb3723bb4b0ad939185e22f166). It has no Java, Graphviz, server or network dependency, and it runs on wasm, wasm-gc, js and native. A pinned JAR is test-only reference tooling; see the corpus notice.

    BlockModelNotes
    @startregex@regex.RegexOne pattern line. Structure only; no input matching.
    @startebnf@ebnf.GrammarOrdered rules and notes. References stay symbolic.
    @startjson@data_tree.DataTreeExact number lexemes, ordered and repeated members.
    @startyaml@data_tree.DataTreeThe native PlantUML YAML subset. Every scalar is text.

    #Read, print and convert

    read accepts exactly one native block. read_body accepts a raw body when the caller gives its kind. The tree text is always equal to the source, also when the source has errors. A document is present only when there is no error.

    ///|
    test {
    let source = "@startregex\ntitle Year\n(?<year>\\d{4})-\\d{2}\n@endregex\n"
    let read = @plantuml.read(source)
    assert_eq(read.tree.text(), source)
    assert_eq(read.diagnostics.length(), 0)
    let document = read.document.unwrap()
    assert_eq(
    @plantuml.print(document).output,
    Some("@startregex\ntitle Year\n(?<year>\\d{4})-\\d{2}\n@endregex\n"),
    )
    guard @plantuml.to_diagram(document).diagram is Some(Regex(model)) else {
    fail("regex")
    }
    assert_eq(model.title, Some("Year"))
    }

    Canonical writing derives the text from typed values. Reading the output gives an equal logical AST: source locations, comments and statement position are not compared. A value that the native syntax cannot express gives a diagnostic and no text. The writer never copies stale source text.

    ///|
    test {
    let body = @plantuml.read_body(
    Ebnf,
    "expr = term, { (\"+\" | \"-\"), term };",
    )
    assert_eq(body.diagnostics.length(), 0)
    let printed = @plantuml.print_body(body.document.unwrap())
    assert_eq(printed.output, Some("expr = term, { (\"+\" | \"-\"), term };\n"))
    }

    #Errors instead of partial results

    Each block kind has a stable inventory of diagnostic codes in diagnostic_codes. Diagnostics use half-open UTF-16 spans. These inputs give errors:

    InputCode
    Two blocks, or text before or after the blockplantuml/multiple-blocks, plantuml/text-outside-block
    A wrong end marker, such as @endyaml for @startjsonplantuml/wrong-end-marker
    !include, !define, !theme and other preprocessor directivesplantuml/unsupported-directive
    A preprocessor call such as %date()plantuml/unsupported-preprocessing
    A regex with more than one pattern lineregex/multiple-patterns
    An EBNF rule that PlantUML draws as "Syntax error!"ebnf/syntax
    Malformed JSONjson/...
    A YAML form that the pinned parser refuses, such as a folded > valueyaml/...

    ///|
    test {
    let read = @plantuml.read(
    "@startregex\n!include other.puml\nabc\n@endregex\n",
    )
    assert_true(read.document is None)
    assert_eq(read.diagnostics[0].code, "plantuml/unsupported-directive")
    }

    title is the diagram title. caption, header, footer, legend, skinparam, <style>, skin, scale, hide, !pragma and !option stay in the AST as inert statements, with an plantuml/unapplied-presentation warning. Library themes and render options paint the diagram.

    #Regex profile

    The reader follows the token order of the pinned PlantUML reader. It reads literals and escaped characters, ., letter escapes such as \d and \s, \p{...}, \x{...}, \uXXXX, \0ooo, POSIX classes [[:name:]], sets, anchors ^ $ \A \Z \z \G \b \B, numbered backreferences, \Q...\E, capturing, non-capturing and named groups, lookahead and lookbehind assertions, alternatives and the quantifiers * + ? {n} {n,} {,m} {n,m}, also reluctant. Repetition bounds stay symbolic: a{1000000} is one model node.

    Adjacent characters of a sequence become one literal term, as PlantUML draws them. Capturing and non-capturing groups stay in the model and the SVG shows them as frames. Lookaround assertions are constraints, not consuming branches.

    The library refuses \x without braces, \k, \P{...}, inline flags such as (?i), atomic groups, unclosed sets, quantifiers with two commas and bounds larger than a 32-bit integer. PlantUML 1.2026.9 refuses lazy brace quantifiers such as {2,4}?, empty alternatives and empty groups; so does this library.

    #EBNF profile

    Statements dispatch in the pinned order: whole-line notes (* ... *), multi-line notes (* ... *) and (** ... **), single-line rules and multi-line rules that end with ;. Rules keep their order and Unicode names. Expressions have terminals in either quote, nonterminal references, special sequences ? ... ?, concatenation, alternatives, grouping, [ ], { }, { }-, decimal counts n * x and exclusions x - y. A rule without a body is an empty rule. Notes on terms and the note below a rule name are kept.

    An exclusion takes the rest of its group, as in the pinned reader: a - b | c is a - (b | c). The model keeps the exclusion as a constraint on its left term; it never becomes an alternative. A reference to a missing or repeated rule name gives a warning and stays a visible nonterminal. A recursive reference stays one reference and is never expanded.

    #JSON and YAML profiles

    JSON keeps all member occurrences in order, including repeated names, and the exact spelling of numbers such as 12345678901234567890 and -0.0. A scalar root stays a scalar model; PlantUML draws it in a one-element array.

    YAML follows the pinned PlantUML line parser, not a general YAML library. Every scalar is text: enabled: true gives the string true. Indentation, - items, lists of maps, flow lists [a, "b,c"], quotes, # comments, | block text and key-only lines with following text are read. Anchors, aliases and tags such as &a, *a and !!str stay literal text. A repeated key keeps every line in the AST; the model takes the last value at the first position, with a yaml/duplicate-key warning, as PlantUML does.

    #highlight selectors resolve to member and value identities. A quoted segment keeps a / inside it. * and ** are wildcards. A name that matches more than one member highlights all of them with a warning; "name"#n selects one occurrence. An unresolved selector gives a warning and selects nothing. A <<stereotype>> is inert metadata and never becomes CSS.

    ///|
    test {
    let source = "@startjson\n{\"id\": 12345678901234567890, \"id\": -0.0}\n@endjson\n"
    let document = @plantuml.read(source).document.unwrap()
    guard @plantuml.to_diagram(document).diagram is Some(DataTree(tree)) else {
    fail("json")
    }
    let numbers = tree.nodes
    .iter()
    .filter_map(n => {
    match n.value {
    Number(lexeme) => Some(lexeme)
    _ => None
    }
    })
    .collect()
    assert_eq(numbers, ["12345678901234567890", "-0.0"])
    }

    #Render

    render_svg reads, converts and renders one block. Diagnostics from every stage accumulate, and an error gives no output.

    ///|
    test {
    let rendered = @plantuml.render_svg(
    "@startyaml\nname: test\nlist:\n - a\n - b\n@endyaml\n",
    renderer=@svg.SvgRenderer::new(mode=Fixed(Light)),
    )
    assert_true(rendered.output.unwrap().contains("data-diagram=\"data-tree\""))
    }

    #External trace providers

    The library bundles no regex matcher, grammar parser or parser generator. An external engine implements the public RailroadTraceProvider trait from moonrockz/diagrams/railroad-trace. A request has a typed regex or EBNF subject, the input, the view identity, explicit limits and, for EBNF, the start rule. A result is Matched, NotMatched, Unsupported, Exhausted or ProviderError. A provider that supports one subject kind returns Unsupported for the other.

    render_traced_svg is the only PlantUML entry point that calls a provider. It validates the result and renders it with independent highlight channels. Ordinary rendering, scenes, playback and export never call a provider.

    ///|
    /// A scripted stub: it supports regex requests and reports a failure at
    /// the first term. It is an example of the contract, not a matcher.
    struct FirstTermFails {}

    ///|
    impl @trace.RailroadTraceProvider for FirstTermFails with fn trace(_, request) {
    guard request.subject is Regex(_) else { return Unsupported("regex only") }
    let topology = request.topology().topology.unwrap()
    let track = topology.tracks[0]
    let first = topology.terms[0]
    NotMatched(
    @trace.Trace::new(request, "first-term-fails", [
    @trace.Attempt::new(
    "a0",
    NotMatched,
    [Traverse(topology.connector_of(track.id, Start).unwrap().id, false)],
    failure=@trace.FailurePoint::new(Terminal(first.id), position=0),
    ),
    ]),
    )
    }

    ///|
    test {
    let result = @plantuml.render_traced_svg(
    "@startregex\nab|cd\n@endregex\n",
    FirstTermFails::{ },
    "x",
    )
    assert_true(result.run.result is Some(NotMatched(_)))
    assert_true(result.rendered.output.unwrap().contains("rr-trace-failure"))
    let grammar = @plantuml.render_traced_svg(
    "@startebnf\nr = \"x\";\n@endebnf\n",
    FirstTermFails::{ },
    "x",
    start_rule="r",
    )
    assert_true(grammar.run.result is Some(Unsupported(_)))
    assert_true(grammar.rendered.output is None)
    }

    To draw evidence that a host adapter already has, pass a TraceHighlight to SvgRenderer::new(trace=...) and render normally. The SVG module guide describes the channels.

    #Modern and legacy activity flowcharts

    @startuml activity blocks use the shared flowchart model, SVG renderer and offline player. The reader keeps the activity dialect, typed control statements and lossless source. Other UML families remain explicit unsupported inputs. Both modern and legacy syntax use the pinned PlantUML 1.2026.9 profile.

    ///|
    test "activity source prints and converts to a flowchart" {
    let source = "@startuml\nstart\n:Read;\n:Read;\nstop\n@enduml\n"
    let result = @plantuml.read(source)
    assert_eq(result.tree.text(), source)
    assert_eq(result.diagnostics, [])
    let document = result.document.unwrap()
    guard document is Activity(activity) else { fail("Expected activity") }
    assert_eq(activity.dialect, Modern)
    let printed = @plantuml.print(document)
    assert_eq(printed.diagnostics, [])
    let model = @plantuml.to_diagram(document)
    assert_eq(model.diagnostics, [])
    guard model.diagram is Some(Flowchart(flowchart)) else {
    fail("Expected flowchart")
    }
    assert_eq(flowchart.nodes.length(), 4)
    assert_true(flowchart.nodes[1].id != flowchart.nodes[2].id)
    }

    An explicit activity body needs a dialect. This keeps generic UML block selection separate from body parsing.

    ///|
    test "legacy activity body requires an explicit dialect" {
    let result = @plantuml.read_body(
    Activity,
    "(*) --> \"Read\" as R\nR --> (*)\n",
    dialect=Legacy,
    )
    assert_eq(result.diagnostics, [])
    let model = @plantuml.to_diagram(result.document.unwrap())
    assert_true(model.diagram is Some(Flowchart(_)))
    }

    See the PlantUML flowchart guide for control syntax, ownership, notes, finite structural playback and independent compatibility evidence. Conditions and join specifications remain descriptive. The library does not execute workflows or load author-requested resources.

    Conversion

    pub struct Conversion {
    diagram :
    Diagram
    ?
    diagnostics : ReadOnlyArray[
    Diagnostic
    ]
    }

    PrintResult

    pub struct PrintResult {
    output : String?
    diagnostics : ReadOnlyArray[
    Diagnostic
    ]
    } derive(Eq,
    Debug
    )

    PrintResult::equal

    fn PrintResult::equal(PrintResult, PrintResult) -> Bool

    PrintResult::not_equal

    fn PrintResult::not_equal(x : PrintResult, y : PrintResult) -> Bool

    ReadResult

    pub struct ReadResult {
    tree :
    Node

    document : Document?
    diagnostics : ReadOnlyArray[
    Diagnostic
    ]
    }

    The result of reading. tree reproduces every input code unit. Activity recovery can retain a partial document; diagnostics determine validity. The other native readers return no document after an error.

    diagnostic_codes

    let diagnostic_codes : ReadOnlyArray[String]

    The stable diagnostic codes of the PlantUML module.

    plantuml_commit

    let plantuml_commit : String

    The source commit of the pinned PlantUML version.

    plantuml_version

    let plantuml_version : String

    The pinned PlantUML version whose native behavior this library follows.

    print

    Write a complete native block: start marker, retained statements, the body and the end marker. The output limit applies to the whole text.

    Write only the body of a document, from its typed values.

    read

    fn read(source : String, limits? :
    Limits
    ) -> ReadResult

    Read one regex, EBNF, JSON, YAML or modern/legacy activity block with its matching end marker. Activities use @startuml and @enduml. Other UML kinds, extra blocks and text outside the block are errors.

    read_body

    Read a raw body of an explicit kind, without block markers.

    render_svg

    Read, convert and render one native block as standalone SVG with the options of renderer, including any supplied trace highlights. Diagnostics accumulate; any error gives no output.

    render_traced_svg

    Read and convert one regex or EBNF block, invoke provider once for input and render the validated trace. start_rule names an EBNF start rule by its rule name. This is the only PlantUML entry point that calls a provider.

    to_diagram

    fn to_diagram(document : Document, id? : String, limits? :
    Limits
    ) -> Conversion

    Convert a document to its format-independent model.

    Source Files

    Powered by MoonBit

    Site sourceReport issuePackagesBuild queueSkillsStatistics

    © 2026 mooncakes.io