Extensions — authoring
This page is the manifest-schema reference and worked-example tour. For a higher-level orientation, read Extensions overview first. The bundled plantuml, graphviz, and gherkin extensions inside Sigla.app/Contents/Resources/Extensions/ are the canonical worked examples — copy one as a starting point.
Where extensions live
| Source | Path | Loaded | Trust |
|---|---|---|---|
| User | ~/.config/sigla/extensions/<id>/ (or $XDG_CONFIG_HOME/sigla/extensions/<id>/) | At launch and on ⌘⇧R | HTML sanitized at render |
| Bundled | Sigla.app/Contents/Resources/Extensions/<id>/ | At launch | Manifest SHA-verified; HTML passes through |
The directory name must equal the manifest’s id — they’re cross-checked at load.
Manifest schema
Every extension declares itself in sigla.json at the root of its directory.
Minimum manifest
{
"id": "my-extension",
"displayName": "My Extension",
"minHostVersion": "0.4"
}
This loads as an inert asset-only extension (no fence labels, no renderer). Add fenceLabels + render to make it actually do something.
Full schema
| Field | Type | Required | Notes |
|---|---|---|---|
id | string | yes | Must match the directory name |
displayName | string | no | Falls back to id |
minHostVersion | string | no | Semver-like ("0.4"). Defaults to "0.0" (no requirement) |
fenceLabels | string[] | conditional | The fence tags this extension claims, e.g. ["plantuml"]. Must be present iff render is |
detectionClass | string | null | no | A CSS class added to rendered output; used by asset gating to inject scripts only when this extension’s output is actually present |
render | object | conditional | The render kind. Must be present iff fenceLabels is non-empty |
assets | object[] | no | Asset declarations (see below) |
render — template kind
"render": {
"kind": "template",
"html": "<pre class=\"my-class\" data-source=\"{{SOURCE_ATTR}}\"><code>{{SOURCE_BODY}}</code></pre>"
}
Placeholders Sigla substitutes:
| Placeholder | Substitution |
|---|---|
{{SOURCE_ATTR}} | Fence body with HTML attribute escaping (safe inside attr="…") |
{{SOURCE_BODY}} | Fence body with HTML body escaping (safe between tags) |
Limit: render.html ≤ 16 KB.
render — process kind
"render": {
"kind": "process",
"binary": {
"name": "plantuml",
"search": ["/opt/homebrew/bin/plantuml", "/usr/local/bin/plantuml"]
},
"invocation": {
"args": ["-tsvg", "-pipe"],
"stdin": true,
"stdoutAs": "svg",
"timeoutSeconds": 10,
"environment": []
},
"cache": { "enabled": true },
"isAsync": true,
"missing": {
"html": "<div class=\"plantuml-placeholder\">PlantUML not installed.<br>Run: <code>brew install plantuml</code></div>"
},
"error": {
"html": "<div class=\"plantuml-error\"><strong>PlantUML error.</strong><pre>{{STDERR}}</pre></div>"
}
}
render.binary
| Field | Type | Required | Notes |
|---|---|---|---|
name | string | yes | Display name surfaced in diagnostics |
search | string[] | yes | Ordered list of absolute paths. First executable wins. PATH lookup is not supported |
The SIGLA_BINARY_<ID_UPPER> environment variable (SIGLA_BINARY_PLANTUML, SIGLA_BINARY_GRAPHVIZ, etc.) is prepended to search at resolve time. Hyphens in the id become underscores: my-tool → SIGLA_BINARY_MY_TOOL. Useful for pointing the renderer at a custom install location without forking the manifest.
[!WARNING] Relative paths in
searchare rejected at load with a diagnostic. Use absolute paths only. Sigla re-resolves on every render sobrew upgrademid-session works without⌘⇧R.
render.invocation
| Field | Type | Required | Default | Notes |
|---|---|---|---|---|
args | string[] | no | [] | Literal arguments. No {{SOURCE}}-style templating into args (security) |
stdin | bool | no | true | Whether to pipe the fence body to the binary’s stdin |
stdoutAs | string | yes | — | "svg" | "html" | "text" |
timeoutSeconds | number | no | 10 | Process killed after this many seconds |
environment | string[] | no | [] | Names of additional env vars to inherit |
The base environment allowlist (LANG, LC_ALL, HOME, TZ) is always inherited. Your environment array adds to that base. Names must match ^[A-Z_][A-Z0-9_]*$.
render.cache
| Field | Type | Required | Default | Notes |
|---|---|---|---|---|
enabled | bool | no | true | When on, identical source + binary fingerprint serves from cache |
Cache fingerprint is source body + binary mtime + binary size. Edit the source or upgrade the binary → cache miss → re-render.
render.isAsync
When true, the first render emits a placeholder; the real output swaps in when the spawn completes. Use for slow binaries (JVM startup, expensive layouts). PlantUML and Graphviz both ship as async.
When false (the default), the render call blocks until the binary returns.
render.missing.html (required) and render.error.html (optional)
HTML shown when the binary can’t be found or when it exits non-zero. error.html receives {{STDERR}} substitution (HTML-escaped). Both limited to 16 KB.
Assets
"assets": [
{ "id": "gherkin/highlight", "kind": "inlineScript", "file": "highlight.js" },
{ "id": "gherkin/styles", "kind": "inlineStyle", "file": "styles.css" }
]
| Field | Type | Required | Notes |
|---|---|---|---|
id | string | yes | Unique within the extension |
kind | string | yes | "script" | "inlineScript" | "stylesheet" | "inlineStyle" |
file | string | yes | Path relative to the extension directory. No .., no leading /, no escape |
defer | bool | no | For "script" only — adds the defer attribute |
integrity | string | no | SRI hash. Optional |
override | bool | no | Reserved; currently unused at render time |
Limits per extension:
- 32 assets maximum
- 2 MB per asset file
- 50 MB total directory size
- 64 KB manifest file
Worked example — template kind (Gherkin)
{
"id": "gherkin",
"displayName": "Gherkin",
"minHostVersion": "0.4",
"fenceLabels": ["gherkin"],
"detectionClass": "sigla-gherkin",
"render": {
"kind": "template",
"html": "<pre class=\"sigla-gherkin\" data-source=\"{{SOURCE_ATTR}}\"><code>{{SOURCE_BODY}}</code></pre>"
},
"assets": [
{ "id": "gherkin/highlight", "kind": "inlineScript", "file": "highlight.js" },
{ "id": "gherkin/styles", "kind": "inlineStyle", "file": "styles.css" }
]
}
What this does:
- Claims
```gherkinfences. - Wraps the fence body in a
<pre class="sigla-gherkin">…<code>…</code></pre>shell. - Injects
highlight.jsandstyles.cssinline at render time — but only if the document contains at least one element withclass="sigla-gherkin"(thedetectionClassgate). Documents without any Gherkin pay no payload cost.
Worked example — process kind (Graphviz)
{
"id": "graphviz",
"displayName": "Graphviz",
"minHostVersion": "0.4",
"fenceLabels": ["dot", "graphviz"],
"detectionClass": null,
"render": {
"kind": "process",
"binary": {
"name": "dot",
"search": ["/opt/homebrew/bin/dot", "/usr/local/bin/dot", "/usr/bin/dot"]
},
"invocation": {
"args": ["-Tsvg"],
"stdin": true,
"stdoutAs": "svg",
"timeoutSeconds": 10,
"environment": []
},
"cache": { "enabled": true },
"isAsync": true,
"missing": {
"html": "<div class=\"sigla-process-missing\"><strong>Graphviz not installed.</strong><br>Run: <code>brew install graphviz</code></div>"
},
"error": {
"html": "<div class=\"sigla-process-error\"><strong>Graphviz error.</strong><pre>{{STDERR}}</pre></div>"
}
},
"assets": []
}
What this does:
- Claims
```dotand```graphvizfences. - For each fence: spawns
dot -Tsvg, pipes the body to stdin, parses stdout as SVG. - Async — first render shows a small placeholder; SVG swaps in when
dotreturns. - Caches on source + binary fingerprint.
- Falls back to a missing-binary message when
dotisn’t installed. - Falls back to an error message (with the underlying stderr inlined) when
dotexits non-zero.
HTML sanitizer allowlist (user extensions)
When source is .user, HTML in render.html, missing.html, and error.html is filtered through an explicit allowlist before injection.
Allowed tags
div, span, p, pre, code, strong, em, br, hr, ul, ol, li, a, img, h1–h6, table, thead, tbody, tr, th, td
Categorically dangerous, dropped with content
script, iframe, object, form, button, style — the open tag and everything up to the matching close tag are removed.
Dropped silently
link, meta, input, embed — the tag is removed but trailing siblings are preserved.
Allowed attributes
- Global, any tag:
class,style,title,id, plus anydata-*attribute. <a>:href<img>:src,alt,width,heighton*event handlers: always rejected.
Allowed URL schemes
href:http,https,mailto,sigla-user-configsrc:http,https,sigla-user-config, plusdata:image/*for<img>only
Bundled extensions bypass this — trust derives from the app’s code signature.
Hard limits
| Limit | Value | Field |
|---|---|---|
| Manifest size | 64 KB | sigla.json |
| Assets per extension | 32 | assets[] count |
| Asset file size | 2 MB | per assets[].file |
| Extension directory size | 50 MB | total aggregate |
| HTML slot size | 16 KB | render.html, missing.html, error.html |
| Process timeout | configurable | render.invocation.timeoutSeconds |
| Env var name pattern | ^[A-Z_][A-Z0-9_]*$ | render.invocation.environment[] |
Violations are reported in Settings → Renderers → Diagnostics. The offending extension is skipped but other extensions continue to load.
Reloading during development
⌘⇧R (View → Reload Extensions) re-runs the scan + validation, picks up edits, and re-renders the active document. No app restart needed.
For the load order, override semantics, and diagnostic messages, see Settings — Renderers.