Skip to main content

Loadable generators

Everything else in these docs is an element the compiler ships with. <x:import> adds elements it doesn't: it binds an XML namespace to a compiled WASM module — a geometry generator — and every element in that namespace is lowered by the module into the same plain-SVG subset as a built-in.

<xsvg viewBox="0 0 420 260"
xmlns="http://www.w3.org/2000/svg"
xmlns:x="https://xsvg.visioncortex.org"
xmlns:clip="https://xsvg.visioncortex.org/ext/clipart">

<x:import ns="https://xsvg.visioncortex.org/ext/clipart" src="clipart.wasm"/>

<clip:arrow x1="30" y1="60" x2="240" y2="60" thickness="22" head-width="56"/>
</xsvg>
Compiling…
Elements the compiler has never heard ofOpen in viewer ↗Edit in Playground →
<clip:arrow> is a leaf generator — parameters in, geometry out; <clip:callout> is a container, sized to the textbox it wraps. Both come from clipart.wasm.

This is the part of xsvg that isn't ours to finish. Anyone can write a module — the SDK is one trait and one macro — and the official clipart extension goes through exactly the same public seam as yours would, with no compiled-in privilege. If a shape you draw over and over could be described by a handful of numbers, it can be an element.

Two ways to think about it

An XML transformer, in the XSLT tradition. xsvg is XML in, XML out: a declarative document describing what it wants, lowered to plain SVG by a transformation you never write imperatively. <x:import> is that tradition's extension mechanism — XSLT binds extension elements by namespace and so does xsvg, except the implementation is a portable WASM module rather than a processor-specific Java or C hook, so the same document transforms the same way in every host. You aren't scripting the compiler; you're teaching it a new element.

A geometry shader. A module is a pure function from parameters to geometry: no document access, no host callbacks, no network, no state carried between runs. Give it the same attributes and it returns the same shape — in the browser, in the CLI, and in Node, from the same bytes. That's why a generated shape is as trustworthy an operand as a built-in one, and why nothing it emits can escape the static-SVG subset.

<x:import>

AttributeRequiredMeaning
nsyesthe namespace URI to bind
srcyesthe module, resolved like a cross-file <use href>
integrityrecommendedSRI-style sha256-…, checked before the module runs

Like <x:theme>, it is a definition: collected before compilation, emits nothing, and may appear anywhere in the document — forward references work. A namespace binds once (a conflicting second binding degrades with a marker, first wins), and the core x: namespace can never be bound, so a module cannot shadow a built-in.

Generators are ordinary elements

A generator element is not a second-class citizen bolted on at the end:

  • It composes. Its emitted geometry is harvested like any other, so a <clip:arrow> is a valid <x:boolean> operand, a <x:warp> target, or the in="#id" container for a textbox.
  • It takes geometry in. in="#id" resolves to a path and reaches the module as one.
  • It can wrap content. An element the module declares a container gets its children compiled and measured first, then handed over as markup plus boxes — which is how <clip:callout> sizes a bubble to whatever text it holds.
  • Themes apply. var() tokens are resolved before the module sees an attribute, so fill="var(accent)" works without the module knowing themes exist.
  • Reserved attributes stay reserved. id, transform, x:layer, x:order, x:hidden, and x:artboard are handled by the compiler — the element stays referenceable, placeable, and layerable. Everything else on the element is the module's parameter surface.
  • It recompiles incrementally. Editing the element re-emits it alone; editing an <x:import> re-emits every element in that namespace; editing an in="#id" target re-emits its dependents.

Nothing a module returns escapes the subset

The module's output is compiled by the core exactly like document content — it may contain x: elements, which lower normally (a generator can emit an <x:offset> and let the compiler do the work), and it passes the same subset checks: <script> and <animate*> dropped, on* attributes stripped, foreign namespaces dropped, nested generator elements refused. The guarantee is therefore narrow and firm: a module can put nothing in the output that a hand-written document couldn't. It is not a claim about cost — the native hosts bound a runaway module with wasmi fuel, but a browser cannot interrupt a synchronous call on its main thread.

Totality holds too. A module that fails to load, fails its integrity check, declines an element, or returns malformed markup degrades that element with a marker naming the reason — never an error, never a lost document. An element in a namespace nothing has bound degrades the same way, which is what makes unknown markup an extension point rather than noise.

The three tiers

Dispatch is by namespace URI, so the tiers are namespaces:

TierNamespace URIShips from
Corehttps://xsvg.visioncortex.orgcompiled into the compiler; normative and permanent
Official extensionshttps://xsvg.visioncortex.org/ext/<module>this project, as versioned WASM modules — experimental, may change or graduate to core
Third-partyany other URI you controlanyone

Official extensions load through the same public seam as anyone else's — same explicit <x:import>, same sandbox, no compiled-in privilege. clipart (<arrow>, <callout>) is the first of them. Prefixes are convention: bind the namespace to whatever prefix you like.

Across hosts

The same module bytes run in the browser, the CLI, and Node, so a generator's output is identical by construction rather than by keeping two implementations in step.

  • Browser — fetches src same-origin by default; a cross-origin src needs integrity.
  • CLI and Nodelocal paths only. A src with a scheme degrades rather than being fetched: a command-line compile reaching the network would be a surprise. Vendor the module next to the document. The native hosts additionally bound a runaway generator with wasmi fuel.
  • Every host checks integrity (one padded-base64 spelling works everywhere) and isolates modules from each other and from successive compiles.
  • Opt out entirely with xsvg --no-plugins or compile(src, { plugins: false }).

Supplying the bytes yourself

A host that can't fetch — a VS Code webview under a strict CSP reaches neither file:// nor an arbitrary origin — supplies a PluginLoader and the bytes arrive over whatever channel it already has. Every browser surface takes it, mirroring the loader hook for cross-file deps:

import { compileXsvg, createPreview, type PluginLoader } from "@visioncortex/xsvg-viewer";

const plugins: PluginLoader = {
key: (base, src) => new URL(src, base).href, // canonical cache key
fetch: async (key) => rpc.readFile(key), // → Promise<ArrayBuffer>
};

await compileXsvg(source, { plugins }); // or `plugins: false` to load none
createPreview(host, { plugins });
<xsvg-view src="diagram.xsvg"></xsvg-view>
<script>document.querySelector("xsvg-view").plugins = plugins;</script>

<xsvg-view-interactive> takes the same property. Unset, the default same-origin fetch applies.

See also