lini 0.1.0

A small, human-readable language for plain-text diagrams that compiles to clean SVG
Documentation

Lini

A small language for plain-text diagrams. You place the boxes — Lini routes the wires — and out comes clean, themeable SVG, in milliseconds.

crates.io docs.rs CI license: MIT

cat -> dog -> bird

That one line is a complete diagram: three boxes, two arrows, sensible spacing. No coordinates, no XML, no mouse.

…and the diagram above? Also plain text — about twenty readable lines (samples/full_example.lini). You wrote the structure; Lini handled the geometry, the orthogonal wire routing, and the styling.


Why Lini

Most diagram tools make you pick a side: draw by hand (precise, but tedious and unversionable) or auto-layout everything (fast, but you get whatever the algorithm decides). Lini splits the difference:

  • You arrange, it routes. Lay nodes out with flex, grid, or anchors — the parts you have an opinion about. Lini routes the connectors between them: orthogonal, clearance-respecting, deterministic. The thing you don't want to do by hand.
  • Genuinely small syntax. Five sigils, sensible defaults. cat -> dog is a valid diagram. You can learn the whole thing in a coffee break.
  • Any shape you need. 13 primitives and 7 templates out of the box — and a raw path primitive that accepts any SVG path string. If SVG can draw it, you can place it and wire to it.
  • Fast, and a single file. A 1.5 MB native binary with one runtime dependency. No Node, no JVM, no headless browser. Typical diagrams compile in ~2 ms — process startup included.
  • Output you can trust. Compilation is byte-identical across runs, so renders diff cleanly in review and never churn in CI. 147 tests back it, including property tests that assert the router's laws on every sample.
  • Themeable like a web page. Colors and fonts ship as CSS variables inside an @layer, so a host page restyles a diagram without recompiling — or bake everything to a self-contained file for email and raster renderers.

Install

cargo install lini
lini diagram.lini -o diagram.svg     # compile to SVG
lini serve diagram.lini              # live-reloading preview in your browser
lini fmt diagram.lini                # canonical formatting (--check for CI)
echo "a -> b -> c" | lini -          # read stdin, write stdout

Building from a clone instead? cargo install --path .


A 60-second tour

Start with nothing. Undeclared names become boxes; -> connects them. Mix line styles and fan-outs freely:

cat -> dog -> bird     // a chain: three boxes, two arrows
fox & owl -> mouse     // fan-in
frog ~> pond           // wavy
fish --> bowl          // dashed

Add shape, labels, and a touch of style. A { defs } block at the top sets defaults and defines reusable styles and shapes:

{
  |wire| stroke:#444 clearance:10
  .loud  stroke:red thickness:2
  |db:cyl| fill:lightyellow      // a new shape, based on the cylinder primitive
}

api   |rect| "API"
queue |rect| "Queue" radius:8
store |db|   "Postgres"

api   -> queue  "enqueue"
queue -> store  "persist" .loud
store -.-> api  "ack"          // dotted, with an arrow

Lay things out. Containers pick a layout mode; children flow, grid, or anchor:

services |group| "Services" layout:row gap:24 {
  api  |rect| "API"
  auth |rect| "Auth"
}

layout:row · layout:column · layout:(cols, rows) for a grid, with cell:, span:, at:, and 9 inner + 8 outer anchors when you want precise placement.


A whole vocabulary of shapes

|hex|     "hex"   size:(82, 72)
|cyl|     "db"    size:(78, 78)
|poly|    "poly"  points:[(0, -34), (32, 11), (20, 34), (-20, 34), (-32, 11)]
|path|    "path"  d:"M -34 6 C -34 -34 34 -34 34 6 C 20 34 -20 34 -34 6 Z"

Rect, oval, hex, slant, cylinder, diamond, cloud, polygon, line, text, icon (Material Symbols), image — plus path for anything else. Templates (group, card, badge, button, note, table, cell) bundle the common patterns, and you can define your own shapes by extending any base: |callout:card| stroke:--accent.


Wires that route themselves

Connect any two nodes by id; Lini finds an orthogonal path through the free space, keeps a configurable clearance from every node and every other wire, rounds the corners, and lands the arrowhead on the edge. One knob (clearance, default 16) governs spacing for the whole diagram.

The operator is the wire's look — [start][line][end], no spaces:

Line Markers
- solid -- dashed > arrow * dot
-.- dotted ~ wavy < crow <> diamond

So -> is a solid arrow, <-> is bidirectional, --* is a dashed line ending in a dot, ~> a wavy arrow. Endpoints support fan-out, fan-in, and cartesian fans with &, sides with a.right -> b.left, and dot-paths into nested containers (closet.outlet -> fridge.inlet). Labels ride their wire and slide to dodge nodes — the wire never moves for them.

The full routing contract — crossings, priority, self-loops, starvation — lives in WIRING.md.


Theming without recompiling

Visual defaults (colors, fonts, shadow) emit as live var(--lini-*) references wrapped in @layer lini.defaults, so unlayered host CSS wins automatically — no !important, no rebuild:

.lini { --lini-accent: #ff6600; }   /* recolor every diagram on the page */

Geometry is always baked into the SVG, so layout never depends on the host. For non-browser renderers (resvg, librsvg) and email, --bake-vars inlines every variable into a self-contained file that renders identically anywhere. Every lini-* class is a stable styling hook, too.


The CLI

lini [options] <input.lini>
lini fmt   [--check] [--stdout] <input.lini>
lini serve [--port N] [--bake-vars] <input.lini>
Flag Meaning
-o, --output FILE Output path (default: stdout).
--format svg|html Raw SVG (default), or wrapped in a minimal HTML page.
--bake-vars Inline var() references — for resvg, librsvg, raster, email.
--theme FILE A CSS file of --lini-* overrides.
--check Parse and validate only.
--watch Recompile on every change (with -o).
--no-warn / --strict Silence lint warnings / promote them to errors.

Errors are LSP-formatted (file:line:col: error: …) and suggest fixes — an unknown endpoint says did you mean kitchen.counter.bowl?. lini serve runs a live-reloading preview (default port 7700).


How fast?

Measured end-to-end on an Apple-silicon laptop, including process startup (--bake-vars, output discarded):

Diagram Time
One node ~1.6 ms
Realistic service diagram (9 nodes, 5 wires) ~2.2 ms
Dense scene (100 nodes, 90 routed wires) ~50 ms

A single-pass parser, bottom-up layout, and an orthogonal router — no browser to spin up, nothing to warm.


When to reach for Lini

Lini Mermaid Graphviz PlantUML
Runtime native binary Node / browser native binary JVM
Placement you control (flex/grid/anchors) automatic automatic automatic
Wire routing automatic, orthogonal automatic automatic (splines) automatic
Theming CSS variables + classes themes / CSS limited skins

Reach for Mermaid or Graphviz when you want a tool to lay everything out for you and don't care exactly where things land. Reach for Lini when you have a layout in mind — a grid, a top-to-bottom flow, framed groups — and you just don't want to draw the connectors by hand.


How it works

A clean pipeline, each stage independently testable:

lex → parse → resolve → layout → route → render

Parse is recursive-descent over an LL(1) grammar; resolve applies CSS-like specificity (inline beats class beats default) and expands user shapes; layout sizes bottom-up; the router solves wires orthogonally against a clearance contract; render emits semantic SVG. The full language is specified in SPEC.md — complete enough to build a conforming engine from scratch.


Status

v0.1. The language (spec v2) is stable, and the whole pipeline is implemented and tested — wires route and render, layout and theming are complete, and the formatter and dev server ship in the same binary.

Non-goals, by design: automatic node placement (you position; Lini routes), multi-file imports, animation, and manual wire waypoints. The syntax stays forward-compatible for all of these.

Development

cargo test                               # 147 tests
cargo run -- samples/hello.lini
cargo run -- serve samples/full_example.lini

samples/ holds one .lini per language feature; tests/conformance.rs snapshots their SVG with insta, and tests/wiring.rs asserts the router's laws on every scene.

License

MIT — see LICENSE.