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 Node — local 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​