uml

    Download zip
    Version
    0.4.4
    License
    Apache-2.0
    Last updated
    yesterday
    Downloads
    13K

    #uml

    A MoonBit library that converts PlantUML source text to SVG, aiming to align with PlantUML's behavior, including its layout.

    Gallery examples below are executable MoonBit tests. Their SVG snapshots in __snapshot__/ also provide the images in this document. Run moon test --update from this directory to refresh them.

    #Quick start

    moon add moonbit-community/uml

    The facade package is moonbit-community/uml/api. Rendering requires a host-provided measurer and graphviz_context; parsing uses neither. The core depends only on their contracts, never on a platform provider. Two optional packages are provided in the same module:

    • metrics/heuristic.measurer() provides deterministic, portable estimates for CLI use and tests.
    • metrics/canvas.measurer() uses a browser Canvas 2D context (JS only). Reuse the service across renders to reuse its cache. Load any required web fonts before rendering; font-loading events invalidate its cached measurements, and a new render rebuilds the resolved font baselines.

    Importing only the Canvas implementation does not pull in UML's heuristic font data. There is no default backend, global switch, or automatic fallback. The dispatcher creates a rendering context from the supplied service without modifying parsed styles, so a document can be reused with different services. Direct users of family renderers and layouts must provide a @style.RenderContext::new(measurer, graphviz_context~) as their first argument. Canvas measurements follow the browser's installed fonts; they do not guarantee identical Java PlantUML font measurements.

    Graphviz layouts, including raw DOT, use the supplied graphviz_context. Use graphviz/text/heuristic.context() for portable estimates or construct a graphviz/layout/text.Context from a graphviz/text/canvas.Canvas in a browser. Canvas contexts retain their measurer and cache across renders.

    Its entry points are:

    • @api.render_svg(source, measurer=text_measurer, graphviz_context~) — parse a PlantUML source and render it to SVG in one call.
    • @api.parse(source) — parse only; returns a Document whose kind() names the diagram family the source selected.
    • Document::render_svg(measurer=text_measurer, graphviz_context~) — render an already parsed document.

    ///|
    let source =
    #|@startuml
    #|participant Alice
    #|Alice -> Bob : hello
    #|@enduml

    ///|
    let text_measurer = @heuristic.measurer() // Import metrics/heuristic as heuristic.

    ///|
    let graphviz_context = @graphviz_heuristic.context() // Import graphviz/text/heuristic.

    ///|
    let svg = @api.render_svg(source, measurer=text_measurer, graphviz_context~)

    For browser font measurements, import moonbit-community/uml/metrics/canvas as canvas and the common package as metrics. Also import Graphviz's text/canvas as graphviz_canvas and layout/text as graphviz_text. Supply a Canvas 2D context as ctx and reuse both services:

    ///|
    let text_measurer : @metrics.Measurer = @canvas.measurer()

    ///|
    let graphviz_context = @graphviz_text.Context::new(
    @graphviz_canvas.Canvas::new(ctx),
    )

    ///|
    let svg = @api.render_svg(source, measurer=text_measurer, graphviz_context~)

    #Sequence diagram

    Participants and actors, activations, autonumber, alt/else groups, and notes:

    ///|
    let text_measurer : @metrics.Measurer = @heuristic.measurer()

    ///|
    let graphviz_context : @graphviz_text.Context = @graphviz_heuristic.context()

    ///|
    test "sequence diagram" (it : @test.Test) {
    let source =
    #|@startuml
    #|autonumber
    #|actor User
    #|participant "Web App" as App
    #|participant "Auth Service" as Auth
    #|User -> App : sign in
    #|App -> Auth : POST /token
    #|activate Auth
    #|Auth --> App : access token
    #|deactivate Auth
    #|alt token granted
    #|App --> User : welcome page
    #|else invalid credentials
    #|App --> User : error message
    #|end
    #|note right of Auth : stateless issuer
    #|@enduml

    let svg = @api.render_svg(source, measurer=text_measurer, graphviz_context~)
    it.write(svg)
    it.snapshot(filename="sequence.svg")
    }

    Sequence diagram

    #Class diagram

    Interfaces, abstract classes, visibility markers, and relations:

    ///|
    test "class diagram" (it : @test.Test) {
    let source =
    #|@startuml
    #|interface Shape {
    #| + area() : Double
    #|}
    #|abstract class Polygon {
    #| # vertices : Array[Point]
    #| + area() : Double
    #|}
    #|class Circle {
    #| - radius : Double
    #| + area() : Double
    #|}
    #|class Point {
    #| + x : Double
    #| + y : Double
    #|}
    #|Shape <|.. Polygon
    #|Shape <|.. Circle
    #|Polygon o-- Point
    #|@enduml

    let svg = @api.render_svg(source, measurer=text_measurer, graphviz_context~)
    it.write(svg)
    it.snapshot(filename="class.svg")
    }

    Class diagram

    #Object diagram

    Objects with slots, map tables, JSON trees, notes and relations; the title chrome renders above the content:

    ///|
    test "object diagram" (it : @test.Test) {
    let source =
    #|@startuml
    #|title
    #|Order snapshot
    #|end title
    #|object "Ada Lovelace" as ada <<customer>> #lightblue {
    #| + id = "customer-42"
    #| + active = true
    #|}
    #|map OrderIndex {
    #| orderId => "order-7"
    #|}
    #|json Profile {
    #| "name": "Ada",
    #| "preferences": {
    #| "locale": "en-GB",
    #| "template": "{customer}/{order}"
    #| }
    #|}
    #|ada --> Profile : serialized
    #|note right of ada
    #| Captured at checkout
    #|end note
    #|@enduml

    let svg = @api.render_svg(source, measurer=text_measurer, graphviz_context~)
    it.write(svg)
    it.snapshot(filename="object.svg")
    }

    Object diagram

    #Use case diagram

    Actors, use cases, and dotted relations:

    ///|
    test "usecase diagram" (it : @test.Test) {
    let source =
    #|@startuml
    #|:Customer: --> (Browse catalog)
    #|:Customer: --> (Place order)
    #|:Sales clerk: --> (Approve order)
    #|(Place order) ..> (Approve order) : include
    #|@enduml

    let svg = @api.render_svg(source, measurer=text_measurer, graphviz_context~)
    it.write(svg)
    it.snapshot(filename="usecase.svg")
    }

    Use case diagram

    #Mindmap

    * levels grow to the right, -- levels grow to the left:

    ///|
    test "mindmap diagram" (it : @test.Test) {
    let source =
    #|@startmindmap
    #|* uml
    #|** Diagrams
    #|*** Sequence
    #|*** Class
    #|*** Use case
    #|** Formats
    #|*** JSON
    #|*** YAML
    #|*** TOML
    #|-- Backend
    #|--- SVG
    #|-- Tooling
    #|--- moon test
    #|@endmindmap

    let svg = @api.render_svg(source, measurer=text_measurer, graphviz_context~)
    it.write(svg)
    it.snapshot(filename="mindmap.svg")
    }

    Mindmap diagram

    #JSON data diagram

    ///|
    test "json diagram" (it : @test.Test) {
    let source =
    #|@startjson
    #|{
    #| "name": "moonbit-community/uml",
    #| "version": "0.1.2",
    #| "targets": ["wasm", "js", "native"],
    #| "diagrams": {
    #| "available": 7,
    #| "planned": 3
    #| }
    #|}
    #|@endjson

    let svg = @api.render_svg(source, measurer=text_measurer, graphviz_context~)
    it.write(svg)
    it.snapshot(filename="json.svg")
    }

    JSON diagram

    #YAML data diagram

    ///|
    test "yaml diagram" (it : @test.Test) {
    let source =
    #|@startyaml
    #|name: uml
    #|license: Apache-2.0
    #|diagrams:
    #| - sequence
    #| - class
    #| - mindmap
    #|render:
    #| backend: svg
    #| compatible: PlantUML
    #|@endyaml

    let svg = @api.render_svg(source, measurer=text_measurer, graphviz_context~)
    it.write(svg)
    it.snapshot(filename="yaml.svg")
    }

    YAML diagram

    #TOML data diagram

    ///|
    test "toml diagram" (it : @test.Test) {
    let source =
    #|@starttoml
    #|[package]
    #|name = "uml"
    #|version = "0.1.2"
    #|
    #|[render]
    #|backend = "svg"
    #|targets = ["wasm", "js"]
    #|@endtoml

    let svg = @api.render_svg(source, measurer=text_measurer, graphviz_context~)
    it.write(svg)
    it.snapshot(filename="toml.svg")
    }

    TOML diagram

    #DOT diagram

    @startdot forwards the DOT source to the shared graphviz engine, like PlantUML does with the system dot executable:

    ///|
    test "dot diagram" (it : @test.Test) {
    let source =
    #|@startdot
    #|digraph G {
    #| rankdir=LR;
    #| a -> b -> c;
    #| a -> c [label="direct"];
    #|}
    #|@enduml

    let svg = @api.render_svg(source, measurer=text_measurer, graphviz_context~)
    it.write(svg)
    it.snapshot(filename="dot.svg")
    }

    DOT diagram

    #Theming

    render_svg accepts a color_scheme. The two positional roles are the ink colors (text and line); every other role is optional and may be a literal color or a CSS variable such as var(--uml-text), so one scheme can target a specific light or dark page without touching each diagram. Document-level skinparam lines still win over the scheme.

    ///|
    test "sequence dark diagram" (it : @test.Test) {
    let dark = @style.ColorScheme::ColorScheme(
    "#e6edf3", // text
    "#8b949e", // line
    canvas="#0d1117",
    participant="#161b22",
    activation="#21262d",
    lifeline="#30363d",
    note="#2d2a1f",
    )

    let source =
    #|@startuml
    #|participant "Web App" as App
    #|participant "Auth Service" as Auth
    #|App -> Auth : POST /token
    #|activate Auth
    #|Auth --> App : access token
    #|deactivate Auth
    #|note right of Auth : stateless issuer
    #|@enduml

    let svg = @api.render_svg(
    source,
    color_scheme=dark,
    measurer=text_measurer,
    graphviz_context~,
    )
    it.write(svg)
    it.snapshot(filename="sequence_dark.svg")
    }

    Dark sequence diagram

    #Documents and diagram kinds

    parse chooses the diagram family with the same per-line heuristics PlantUML uses to pick a diagram factory, and Document::kind exposes the choice:

    ///|
    test "documents expose their detected diagram kind" {
    let source =
    #|@startmindmap
    #|* root
    #|@endmindmap
    let document = @api.parse(source)
    assert_true(document.kind() is Mindmap)
    }

    #Collapsible class members

    With class_member_collapsible=true, SVG member controls expose data-member-node, data-member-section, and aria-expanded. The host handles click or keyboard activation, updates member_states by qualified node code, and calls the renderer again. Each call recomputes the full layout. A standalone SVG shows its current state; interaction requires a host.

    ///|
    let source =
    #|@startuml
    #|class User {
    #|- secret
    #|}
    #|@enduml

    ///|
    let svg = @api.render_svg(
    source,
    class_member_collapsible=true,
    measurer=text_measurer,
    graphviz_context~,
    )

    Run from the uml/ directory:

    moon test # run tests, including gallery snapshots moon test -p moonbit-community/uml --update # update __snapshot__/*.svg from README examples

    The gallery tests render the examples directly, including theme settings, and write the images through MoonBit's snapshot mechanism. To update only the object example, add --filter 'object diagram' to the update command.

    #License

    Apache-2.0