Skip to content
supsupPublic

About

Charts Y'all

Resources

Stars

1 star

Watchers

0 watching

Forks

Repository files navigation

Sirentide

Living, narratable diagrams — baked to static SVG, no runtime JS.

Sirentide is a clean-room, pure-Java, zero-dependency renderer that turns a small diagram DSL into inert static SVG at build time — designed to carry a native effect/narrative layer: diagrams you'll be able to play through, effects bound to the diagram's own meaning. The foundation for that layer already ships — stable semantic anchors baked into the SVG plus a baked-frame renderFrames play-through API — alongside real LaTeX math baked into labels (via its sibling, LatteX).

It is not a Mermaid clone. Mermaid is a design reference; Sirentide takes the idea, drops the browser, and aims at the thing no browser-JS diagram tool can put in a CSP-clean static bake: a controlled, sanitizer-safe effect layer that will give a diagram presence.

Why it exists

  • Static-site / docs safe. Output is inert svg/g/path/rect/line geometry, plus title/desc for accessibility (labels baked to <path> glyphs) — no <script>, no runtime JS, no external fonts. It survives a strict HTML sanitizer untouched.
  • Hermetic build. Pure JVM, zero runtime deps (no headless browser at bake). The one build-time asset is a bundled OFL/Apache font, rendered to paths.
  • Native effects (by design). Sirentide already emits the SVG with its own stable semantic anchors, so the coming effect layer ("glow the critical path", "pulse the active step", "play the flow in order") will attach natively — not bolted onto a drifting class-soup.
  • Math composition. A node label, axis tick, or bar can be a real LaTeX formula — its LatteX sibling renders it at bake, and the two minimal-alphabet emitters compose for free.

Status

Live and shipping. The render pipeline (DSL → IR → layout → SVG), the clean-room font-metrics oracle, and twenty-three diagram types are built today — each baked to that same inert alphabet — plus LaTeX math in labels (the LatteX bridge), semantic anchors (data-sirentide-role/id/seq), the baked-frame play-through API (renderFrames, with a renderFramesWithDiagnostics twin that adds a why-did-it-degrade channel without touching the never-throw bake), and a live /docs integration: a ```sirentide fenced block in a docs page bakes to a sanitized inline diagram. See examples/showcase.html — every type + the one-bake-any-theme demo, all live renderer output. Every shipped sealed-IR type also has a browser-audited BrewShot reference in the gallery, enforced by a headless type-to-capture coverage test.

The six flagship types, in detail:

Type One-liner Sample
pie proportional wedges; optional legend (alias key), per-item #hex colours (3- or 6-digit); a thin slice's outside label is shortened with an ellipsis to fit, or dropped when not even the ellipsis fits (the drop is named as a caveat) pie legend
"Reviews" : 40
"Docs" : 30 #22c55e
xychart signed bars on a fractional-clean axis, or line / scatter; several series with a legend; a categorical x axis, or numeric for a real one; a dropped or shortened label is named as a caveat xychart
"Mon" : 5
"Tue" : -3
timeline events placed proportionally in time; a value is a bare number (such as a year, shown as a number) or an ISO date (YYYY-MM-DD / YYYY-MM, shown as written) — use one form per diagram, because the two are not placed on a common scale; labels ellipsized/clamped timeline
"Founded" : 2000
"Launch" : 2020
gantt tasks on a shared, min-normalized time axis; degenerate domains still draw markers gantt
"Design" : 0-3
"Build" : 3-8
flowchart TD/LR directed graph, A[rect]/A{diamond} nodes, `--> label
sequence actors + time-ordered messages: ->> calls, -->> replies, self-messages sequence
Client ->> Auth : login
Auth -->> Client : ok

The flowchart sample is written -->\|yes\| in this file's source, because a Markdown table needs the pipe escaped. Copy it from the rendered page or from the block below. Pasted with the backslashes it still renders and reports no caveat, but the yes edge label is lost.

flowchart TD
A[Open PR] --> B{Approve?}
B -->|yes| C[Merge]

Plus seventeen more: state (rides the flowchart engine), quadrant, classDiagram (all five UML relationship markers), erDiagram (crow-foot cardinalities), gitGraph, journey, mindmap, sankey, mathblock (standalone display LaTeX), matrix (comparison / verdict grid), heatmap (continuous 0..1 cells on a sequential ramp, with a legend; or categorical cells with palette:, plus a custom ramp: and bins:, hide: for headers and ! to outline a cell), snake (continued-fraction snake graph), tensornetwork (Penrose MPS/MPO), young (integer-partition boxes), dynkin (semisimple Lie-algebra classification), rootsystem (deterministic finite-root-system Coxeter-plane projections), and knot (classical knots — trefoil/unknot/figure-eight). graph is accepted as a synonym for flowchart, and xychart-beta for xychart. The flowchart carries eight of mermaid's node shapes — rect, rounded, stadium, subroutine, cylinder, circle, diamond, hexagon — plus edge styles and nested subgraphs. Among the forms not supported are the parallelogram, trapezoid, asymmetric and double-circle — named rather than counted, because an exclusion list that reads as exhaustive is the same overclaim one level down. The sequence diagram carries alt/loop/par frames and activation bars. Activation is implicit: a call ->> activates its callee and a reply -->> ends it, with nested activations stacked side by side. The mermaid sigils (->>+ / -->>-) are accepted and change nothing, so a diagram written with them renders the same bars rather than minting an actor named + B.

A small sample for more of the seventeen (the gallery has a rendered case for every type, including the ones not listed here):

Type Sample
state stateDiagram-v2
[*] --> Draft
Draft : Draft (editable)
Draft --> Review : submit
Review --> Draft : changes requested
Review --> Published #22c55e : approve
Published --> [*]
classDiagram classDiagram
class Customer {
+String email
}
class Order {
+total() Money
}
Customer "1" --> "0..*" Order : places
Order "1" *-- "1..*" LineItem : contains
matrix matrix
cols: Chrome, Firefox, Safari
"SVG filters" : yes, yes, partial
"Container queries" : yes, yes, 16.0+:yes
"View transitions" : yes, behind flag:partial, no
"WebGPU" : yes, -, no
heatmap heatmap
cols: unit, integration, e2e
scale: "untested" --> "covered"
"parser" : 0.95, 0.80, 0.40
"layout" : 0.70, 0.55!, -
"cli" : 90%, 85%, 60%
tensornetwork tensornetwork
mps A B C D
young young
rows: 5, 3, 1
dynkin dynkin
type: E8
rootsystem rootsystem
type: A2
edges: minimal
knot knot
type: trefoil

An erDiagram relation is written with pipe characters, so its sample is a block rather than a table cell:

erDiagram
AUTHOR ||--o{ BOOK : writes
BOOK }o..o{ TAG : tagged
BOOK {
int id PK
int author_id FK
string isbn UK
}

Cross-cutting: a color= header modifier for off-slice text, currentColor theme-adaptive labels, per-diagram themes, and hard input caps so a malformed or oversized source degrades to an inert shell — the bake never throws. A $…$ fragment inside a label of a pie, xychart, timeline, gantt, flowchart, sequence, state, quadrant, classDiagram or erDiagram (in a pie only a comfortable slice's inside label, not thin-slice leader labels or legend rows; in an xychart only the category labels on the x axis) typesets as real math via the LatteX bridge; labels in the other types bake it as plain text.

Still ahead (the remaining thesis work): the native effect layer — data-sirentide-fx, the security-gated Part 2 the anchors were built to carry — see SLOWSTART.md.

Docker

Build the Java 25 image from the repository root. The build argument is required, not optional — the jar stamps Sirentide-Source-Revision into its manifest and refuses to build without an exact 40-hex commit, so a bare docker build -t sirentide . fails with Sirentide-Source-Revision requires one exact lowercase 40-hex git commit; got '':

docker build --build-arg SIRENTIDE_SOURCE_REVISION="$(git rev-parse HEAD)" -t sirentide .

That stamp is worth understanding rather than pasting past, because it makes a Sirentide image self-describing: the artifact carries the commit it was built from, so "which Sirentide is this?" is answerable from the image alone — no build logs, no tag archaeology. Read it back at any time:

docker run --rm --entrypoint sh sirentide -c \
  'unzip -p /opt/sirentide/sirentide.jar META-INF/MANIFEST.MF' \
  | grep -E 'Implementation-Version|Sirentide-Source-Revision'

The same two facts are also image labels, so docker inspect answers without running the image. The build checks both before labelling: the revision is the one the jar stamps, and SIRENTIDE_VERSION (default: the build version) must equal the jar's Implementation-Version or the build refuses.

docker inspect --format '{{ index .Config.Labels "org.opencontainers.image.revision" }} {{ index .Config.Labels "org.opencontainers.image.version" }}' sirentide

The image keeps its application artifacts under the immutable /opt/sirentide tree and runs as the non-root 10001:10001 user by default. The existing one-shot CLI remains the default entry point, so the original stdin-to-stdout flow is unchanged:

printf '%s\n' 'pie' '"Reviews" : 40' '"Docs" : 60' \
  | docker run --rm -i sirentide > diagram.svg

cli is an optional explicit spelling for scripts that want to distinguish one-shot work from the folder worker. File input can be mounted read-only while the output mount stays writable:

mkdir -p Input Output
# the page to render: any Markdown file holding a sirentide fence
printf '%s\n' '# Demo' '' '```sirentide' 'pie legend' '"Reviews" : 40' '"Docs" : 30 #22c55e' '```' \
  > Input/diagram.md
docker run --rm \
  --user "$(id -u):$(id -g)" \
  --mount type=bind,src="$PWD/Input",dst=/sirentide/input,readonly \
  --mount type=bind,src="$PWD/Output",dst=/sirentide/output \
  sirentide cli render /sirentide/input/diagram.md \
    -o /sirentide/output/diagram.svg

Watched input and output folders

A longer walkthrough of the image, from build to a running worker, is in docs/dockerDogFoodflow.md.

Run a long-lived worker with writable input and output mounts:

mkdir -p Input Output
docker run -d --name sirentide-worker --restart unless-stopped \
  --user "$(id -u):$(id -g)" \
  --mount type=bind,src="$PWD/Input",dst=/sirentide/input \
  --mount type=bind,src="$PWD/Output",dst=/sirentide/output \
  sirentide watch

The examples map the process to the current host UID/GID so ordinary bind folders stay writable on Linux. If your container runtime already translates ownership, omit --user and retain the image's non-root 10001:10001 default. Prefer either approach over world-writable permissions. Watch mode accepts complete direct children of /sirentide/input with these extensions:

  • .md and .markdown — render the first ```sirentide fence, using the same fence extraction, source cap, and render outcome as the CLI.
  • .sirentide — render a raw Sirentide DSL source.

Hidden names, temporary uploads, symlinks, other extensions, and the worker's state directories are ignored. Producers should write a hidden sibling and rename it into place only when complete, for example:

cp diagram.md Input/.diagram.md.tmp
mv Input/.diagram.md.tmp Input/diagram.md

For diagram.md, a successful job creates Output/diagram.md.svg and moves the source to Input/finished/diagram.md. The full lifecycle is:

Input/diagram.md
  -> Input/processing/<job-id>/diagram.md
  -> Input/finished/diagram.md

A failed render instead writes a bounded, content-free Output/diagram.md.error.txt and moves the source to Input/failed/diagram.md. Existing outputs and archived inputs are never overwritten; conflicting archives are retained below a job-id directory in finished/collisions or failed/collisions. A file left in processing by a stopped container is recovered on restart. Multiple workers may share the same mounts: atomic claims plus idempotent publication ensure one final state even on bind-mount drivers that do not coordinate advisory file locks.

An eligible file that the non-root worker cannot open follows the same failed lifecycle instead of stopping the watcher: its inode is moved without reading or copying the source bytes, one bounded diagnostic is emitted, and later jobs continue normally. When a same-name failed archive already exists, the unreadable source remains below its job-id directory in failed/collisions. If diagnostic publication is interrupted, the inode remains durably recoverable below failed/pending/<job-id>; the next watcher publishes the same idempotent diagnostic before completing the final failed disposition. A temporary output mount fault therefore cannot leave an archived unreadable source without its promised diagnostic. Shared watchers snapshot that unreadable claim's filesystem identity; a losing worker accepts a pending, collision, or direct failed path only when it is the same inode and size. An unrelated same-name archive is never treated as proof of completion, and a completed race does not stop either watcher.

The worker never lengthens the original name inside a path component. If the source name fits the mounted filesystem but adding .svg or .error.txt would not, the corresponding output uses a bounded job-<job-id> filename instead; the source is still archived under its unchanged original name. A diagnostic collision uses a bounded job-and-attempt filename and never overwrites the existing file.

Set SIRENTIDE_WATCH_POLL_MS to an integer from 10 through 60000 to change the scan interval (default 500). SIRENTIDE_INPUT_DIR and SIRENTIDE_OUTPUT_DIR move the watched folders away from /sirentide/input and /sirentide/output (a blank value keeps the default). The watched input mount must be writable so the worker can move jobs between state folders; the one-shot CLI input mount can remain read-only.

Command line

Outside Docker, ./gradlew jar builds build/libs/sirentide-<version>.jar, a runnable jar: java -jar build/libs/sirentide-<version>.jar render page.md -o out.svg. It renders raw DSL from stdin, the first fence of a Markdown page, or many sources in one JVM with --batch, and can typeset math (--math), screenshot to PNG (--png) and fail on any caveat (--strict). Every form, flag and exit code is in QUICKSTART.md section 3.

Docs

Release provenance

An immutable release carries three jars — executable, sources, and Javadoc — plus a .sha256 sidecar for each. The executable jar's manifest binds Implementation-Version to the release number and Sirentide-Source-Revision to the exact lowercase 40-hex git commit used to build it. After downloading a release, verify the sidecars from the directory containing the artifacts (the example names 0.5.0; use the version you downloaded):

shasum -a 256 -c sirentide-0.5.0.jar.sha256
shasum -a 256 -c sirentide-0.5.0-sources.jar.sha256
shasum -a 256 -c sirentide-0.5.0-javadoc.jar.sha256
unzip -p sirentide-0.5.0.jar META-INF/MANIFEST.MF

For maintainers, SIRENTIDE_REQUIRE_CHROME=1 ./gradlew releaseBuild is the release-only gate. It requires a clean, resolvable git commit and finalized release notes, runs the clean full build with the browser suite unable to skip green, then checks all three jars, the paired frame-deck assets, manifest identity, and checksum sidecars before a tag or GitHub release is created.

Family

  • LatteX — the clean-room LaTeX→SVG math sibling Sirentide depends on (render-only) and composes with.

License

Apache-2.0.

Disclaimer

This project is provided under the Apache License 2.0 on an "AS IS" basis, without warranties or conditions of any kind. See the LICENSE file for details.

About

Charts Y'all

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages