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

SourcePathLoadedTrust
User~/.config/sigla/extensions/<id>/ (or $XDG_CONFIG_HOME/sigla/extensions/<id>/)At launch and on ⌘⇧RHTML sanitized at render
BundledSigla.app/Contents/Resources/Extensions/<id>/At launchManifest 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

FieldTypeRequiredNotes
idstringyesMust match the directory name
displayNamestringnoFalls back to id
minHostVersionstringnoSemver-like ("0.4"). Defaults to "0.0" (no requirement)
fenceLabelsstring[]conditionalThe fence tags this extension claims, e.g. ["plantuml"]. Must be present iff render is
detectionClassstring | nullnoA CSS class added to rendered output; used by asset gating to inject scripts only when this extension’s output is actually present
renderobjectconditionalThe render kind. Must be present iff fenceLabels is non-empty
assetsobject[]noAsset 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:

PlaceholderSubstitution
{{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

FieldTypeRequiredNotes
namestringyesDisplay name surfaced in diagnostics
searchstring[]yesOrdered 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-toolSIGLA_BINARY_MY_TOOL. Useful for pointing the renderer at a custom install location without forking the manifest.

[!WARNING] Relative paths in search are rejected at load with a diagnostic. Use absolute paths only. Sigla re-resolves on every render so brew upgrade mid-session works without ⌘⇧R.

render.invocation

FieldTypeRequiredDefaultNotes
argsstring[]no[]Literal arguments. No {{SOURCE}}-style templating into args (security)
stdinboolnotrueWhether to pipe the fence body to the binary’s stdin
stdoutAsstringyes"svg" | "html" | "text"
timeoutSecondsnumberno10Process killed after this many seconds
environmentstring[]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

FieldTypeRequiredDefaultNotes
enabledboolnotrueWhen 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" }
]
FieldTypeRequiredNotes
idstringyesUnique within the extension
kindstringyes"script" | "inlineScript" | "stylesheet" | "inlineStyle"
filestringyesPath relative to the extension directory. No .., no leading /, no escape
deferboolnoFor "script" only — adds the defer attribute
integritystringnoSRI hash. Optional
overrideboolnoReserved; 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:

  1. Claims ```gherkin fences.
  2. Wraps the fence body in a <pre class="sigla-gherkin">…<code>…</code></pre> shell.
  3. Injects highlight.js and styles.css inline at render time — but only if the document contains at least one element with class="sigla-gherkin" (the detectionClass gate). 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:

  1. Claims ```dot and ```graphviz fences.
  2. For each fence: spawns dot -Tsvg, pipes the body to stdin, parses stdout as SVG.
  3. Async — first render shows a small placeholder; SVG swaps in when dot returns.
  4. Caches on source + binary fingerprint.
  5. Falls back to a missing-binary message when dot isn’t installed.
  6. Falls back to an error message (with the underlying stderr inlined) when dot exits 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, h1h6, 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 any data-* attribute.
  • <a>: href
  • <img>: src, alt, width, height
  • on* event handlers: always rejected.

Allowed URL schemes

  • href: http, https, mailto, sigla-user-config
  • src: http, https, sigla-user-config, plus data:image/* for <img> only

Bundled extensions bypass this — trust derives from the app’s code signature.

Hard limits

LimitValueField
Manifest size64 KBsigla.json
Assets per extension32assets[] count
Asset file size2 MBper assets[].file
Extension directory size50 MBtotal aggregate
HTML slot size16 KBrender.html, missing.html, error.html
Process timeoutconfigurablerender.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.