Writing and distributing interpreters
Structile understands JSON and Python-repr natively. Everything else — XML, HTML, INI, TOML, YAML, CSV, or any in-house format — goes through a small interpreter: a JavaScript file that says how to turn that format's text into a plain value, and (optionally) back again. This page covers writing one, trying it out, and then the four ways to get it to whoever else needs it — from "just send the file" up to "install a package and it's automatic."
This one page applies to all three ways of running Structile — the standalone viewer, the Python library, and the VS Code extension — since all three run the exact same interpreter script the exact same way.
The contract
An interpreter defines one or two functions, depending on the kind of data:
- Any text format that isn't markup (INI, TOML, YAML, CSV, a custom
DSL, …):
interpretText(text), given the raw file text, returning a plain JS value. Optionally alsoserializeText(value), the inverse — when present, Save writes edits back to this exact text shape instead of falling back to a different format. - Markup (XML, or HTML — HTML is just XML-like markup with a looser
parsing mode):
interpretXML(xmlDocument), given a browser-parsed DOMDocument, returning a plain JS value. Optionally alsoserializeXML(value).
Only interpretText/interpretXML is required — read-only support (view
but not edit-and-save) is a perfectly reasonable place to stop.
One rule matters more than the shape of the code: fail loudly. If the
text doesn't actually look like your format, throw — don't guess and
return something meaningless. This is what makes it safe to hand
Structile a list of candidate interpreters for the same file: it tries
each in order and uses the first one that doesn't throw, so two
mutually-incompatible schemas can coexist without anyone having to say up
front which file is which.
A worked example
A minimal "flat properties" format — one key = value pair per line,
# for comments:
// props-interpreter.js — one key=value pair per line.
//
// interpretText(text) is the required half: raw text in, a plain JS
// value out. serializeText(value) is optional: the inverse, needed only
// if you want Save to write back to this exact shape.
function interpretText(text) {
const result = {};
const lines = text.split(/\r\n|\r|\n/);
for (let i = 0; i < lines.length; i++) {
const line = lines[i].trim();
if (line === "" || line.startsWith("#")) continue;
const eq = line.indexOf("=");
if (eq === -1) {
// Fail loudly: this line isn't "key = value", so this text
// probably isn't a props file at all.
throw new Error(`props interpreter: line ${i + 1} (${JSON.stringify(line)}) has no "="`);
}
result[line.slice(0, eq).trim()] = line.slice(eq + 1).trim();
}
return result;
}
function serializeText(value) {
if (value === null || typeof value !== "object" || Array.isArray(value)) {
throw new Error("props interpreter: can only serialize a flat object of key/value pairs");
}
return Object.keys(value).map((key) => `${key} = ${value[key]}`).join("\n") + "\n";
}
A markup interpreter has the same shape, just walking a DOM instead of splitting lines:
function interpretXML(xmlDocument) {
const root = xmlDocument.documentElement;
// ... read root's tag/attributes/children, return a plain JS value ...
// throw if root isn't the shape you expect.
}
function serializeXML(value) {
// ... the inverse: build an XML string from a plain JS value ...
}
Trying it out
Standalone viewer: drop (or paste) the data file and the interpreter
.js file onto the page together — either order. See the
standalone viewer guide.
Python:
import structile as st
st.open("settings.props", interpreter="props-interpreter.js", format="ini")
(format= picks the text-vs-markup contract; for a file whose extension
you've registered — see below — it's inferred automatically.)
VS Code: name it settings.props.interpreter.js, right next to
settings.props — a sibling file always wins with no configuration at
all. See the VS Code extension guide.
Distributing it
Just send the file
The simplest option, and often the right one: share the .js file
however you'd share any other file (chat, email, a shared drive, a repo
alongside the data it describes). Every environment can point at a bare
file path with no packaging step:
- Standalone viewer: drop it alongside the data, as above.
- Python:
interpreter="path/to/props-interpreter.js", or register it once so nobody has to remember the path again —st.register_interpreter(".props", "path/to/props-interpreter.js"). - VS Code: a sibling
<name>.interpreter.jsfile, or thestructile.interpretersetting.
This stops scaling once more than a couple of people need the same format — everyone has to independently know the file exists and keep their own copy in sync. The three options below solve that.
Python: an installable plugin
Distribute it as an ordinary pip install-able package, via a standard
entry point
in the group structile.interpreters — the same mechanism pytest and
Sphinx use for their own plugins. Once installed, it just works — no
import, no registration call, no wrapper API, for anyone who installs it:
import structile as st
st.open("settings.props") # correct interpreter chosen automatically
Fastest path: download a working copy and rename it.
structile-demo-plugin.zip is a
complete, real package — the exact three files below, already wired up
and already proven to install and register correctly (it's a fixture the
structile test suite itself installs and exercises, not just a
made-up sample). Unzip it, then:
-
Rename the distribution and the importable module. The zip's
pyproject.tomlcalls the distributionstructile-demo-plugin; the foldersrc/structile_demo_plugin/is the actual Python package (importable names use underscores, distribution names on PyPI conventionally use hyphens — that's normal, not a typo). Rename the folder and update every place its old name appears — every occurrence ofstructile_demo_plugin/structile-demo-pluginbelow needs to become your own package's name (acme_structile_formatsin this walkthrough). -
Replace the interpreter script. Swap
src/structile_demo_plugin/interpreters/demo_keyed.jsfor your own.jsfile (theprops-interpreter.jsfrom the worked example above, or your own) — keep it inside aninterpreters/subfolder of the package so thepackage-dataline below can find it unchanged. -
Edit
pyproject.toml— the full file, annotated:[build-system] requires = ["setuptools>=64"] build-backend = "setuptools.build_meta" [project] name = "acme-structile-formats" # <- your distribution name (PyPI-style, hyphens) version = "1.0.0" description = "Acme's structile interpreter(s) — .props file support." dependencies = ["structile"] # <- REQUIRED for a real plugin. # The downloaded template deliberately OMITS this line (see its own # comment) so it stays installable offline inside structile's own test # suite. Copying the template without adding this line back is the # single most common mistake here: your package will still install # fine, but `from structile import InterpreterSource` in __init__.py # will only work if something else already installed structile first. [project.entry-points."structile.interpreters"] # left of "=" is just a label (shown nowhere, can be anything unique in # this table); right of "=" is "<your_package>:<entry point function>" props = "acme_structile_formats:register" [tool.setuptools] package-dir = {"" = "src"} [tool.setuptools.packages.find] where = ["src"] [tool.setuptools.package-data] acme_structile_formats = ["interpreters/*.js"] # <- must match your renamed package -
Edit
src/acme_structile_formats/__init__.py(renamed fromstructile_demo_plugin) — the full file, annotated:import importlib.resources from structile import InterpreterSource def register(registry) -> None: """The plugin entry point — must be named exactly what pyproject.toml's entry-points table points at ("register" above). `registry` is a small facade exposing only register_interpreter().""" js_path = importlib.resources.files("acme_structile_formats") / "interpreters" / "props-interpreter.js" text = js_path.read_text(encoding="utf-8") # First argument: the file extension this interpreter owns, leading dot. # name= is a label only, shown in logs/errors and st.plugins() below. registry.register_interpreter(".props", InterpreterSource(text, name="acme_structile_formats"))InterpreterSource(text, name=)wraps JS source loaded directly from the installed package viaimportlib.resources(not a filesystem path — the file may be inside a wheel/zip at runtime, so a plainopen(path)would break for some install methods). One package can register more than one extension: callregister_interpreter()more than once inside the sameregister()function. -
Install it locally and verify. From the package's own directory (wherever
pyproject.tomlis):pip install -e . python -c "import structile as st; print(st.plugins())"Expect your package listed and marked loaded, not failed. Then prove it actually resolves a file:
python -c "import structile as st; st.open('example.props', renderer='text')"A plugin that fails to load or raises during registration is logged as a warning and never applied, but never breaks anyone else's
open()call — and it still shows up inst.plugins()marked as failed, with the error, rather than silently vanishing.python -m structile --pluginsprints the same thing from the command line, andSTRUCTILE_DISABLE_PLUGINS(any truthy value) skips discovery entirely, for isolating "is my plugin the problem?" from everything else. -
Build and distribute it, exactly like any other Python package — nothing about
structile.interpretersentry points changes this part:pip install build twine python -m build # writes dist/*.whl and dist/*.tar.gz twine upload dist/* # to PyPI — needs a PyPI account/tokenFor an internal-only format that shouldn't be public, upload to your organisation's own package index instead (Artifactory, devpi, a simple authenticated HTTP index, AWS CodeArtifact, …) and have colleagues
pip install --index-url https://your-internal-index/simple/ acme-structile-formats— the entry-point mechanism itself doesn't care where the package came from, only thatpip installput it onsys.path.
Discovery is lazy (triggered by the first open()/diff()/convert()
call, never by import structile) and runs at most once per process.
Distributing to a VS Code team
Two ways to ship a schema to a whole team at once, mirroring the Python plugin above.
1. A VS Code extension contribution (the primary mechanism). Any
installed extension — including one that does nothing else — can declare
one or more interpreters in its own package.json. This extension never
has to activate at all: VS Code reads contributes from every installed
extension's manifest with no activation-order dependency, and installing/
uninstalling it takes effect immediately, with no window reload.
Fastest path: download a working copy and rename it.
structile-demo-companion-extension.zip
is a complete, minimal, real VS Code extension that does nothing except
contribute one interpreter — no compile step, no main entry point, just
package.json + one .js file, on purpose. Unzip it, then:
-
Replace
interpreters/demo_keyed.jswith your own interpreter script. -
Edit
package.json— the full file, annotated:{ "name": "acme-structile-formats-vscode", // <- rename "displayName": "Acme Structile Formats", // <- rename "description": "Ships Acme's .props interpreter for the Structile extension.", "version": "1.0.0", "publisher": "acme", // <- your Marketplace publisher id, if publishing "license": "Apache-2.0", "engines": { "vscode": "^1.85.0" }, "categories": ["Other"], "contributes": { "structileInterpreters": [ { "extensions": [".props"], // <- your extension(s), leading dot "path": "./interpreters/props-interpreter.js", // <- relative to this file "name": "Acme props format", // <- shown in the two debug commands below "format": "text", // "text" (interpretText/serializeText) | "xml" (interpretXML/serializeXML) "priority": 0 // only matters if another installed extension also claims .props — higher wins } ] }, "scripts": { "package": "vsce package --allow-missing-repository --no-rewrite-relative-links" }, "devDependencies": { "@vscode/vsce": "^3.9.2" } } -
Package and install it locally to verify:
npm install npm run package # writes acme-structile-formats-vscode-1.0.0.vsix in this folder code --install-extension acme-structile-formats-vscode-1.0.0.vsix(Or, without a command line: VS Code -> Ctrl+Shift+P -> Extensions: Install from VSIX... -> pick the
.vsix.) Then open a.propsfile and confirm: Structile: List Contributed Interpreters should list your extension; Structile: Show Interpreter Resolution on the open file should show it as used. -
Distribute it. Three options, not mutually exclusive:
- Hand out the
.vsixdirectly — exactly what this docs site does for the main extension itself (see the VS Code extension guide): host the file anywhere, colleagues run Install from VSIX.... No account, no review process, works today. - Publish to the VS Code Marketplace (
npx vsce publish— needs a Marketplace publisher account and a Personal Access Token) for discoverability via the Extensions view's search. - Publish to Open VSX (
npx ovsx publish) alongside or instead of the Marketplace, for VSCodium and other non-Microsoft-marketplace editors.
- Hand out the
2. Workspace-local interpreters, for a team that can't publish an
internal extension: drop .js files under .structile/interpreters/ in
the workspace root, mapped by .structile/interpreters.json:
{ ".props": "props-interpreter.js" }
Only active in a trusted workspace — loading arbitrary JS out of a cloned repository into a webview is exactly the threat Workspace Trust exists for.
Resolution order
When more than one of the above applies to the same file, the most specific one wins — how narrowly a declaration targets this exact file, not which mechanism supplied it:
Python, most to least direct:
| Rung | Source |
|---|---|
| 1 | An explicit interpreter= argument |
| 2 | A direct register_interpreter() call |
| 3 | A plugin registration (entry points) |
| 4 | Nothing — existing fallback, unchanged |
VS Code, most to least specific:
| Rung | Source |
|---|---|
| 1 | Sibling <name>.interpreter.js |
| 2 | structile.interpreter setting (workspace, then user scope) |
| 3 | Workspace .structile/interpreters/ mapping (trusted workspaces only) |
| 4 | Interpreters contributed by installed extensions (highest priority first) |
| 5 | Built-in .xml/.html fallback — opens with a warning and waits |
Within any rung that produces more than one candidate, the same "fail loudly, first non-throwing one wins" trial from above applies.
Debugging what's actually loaded
- Python:
st.plugins()(orpython -m structile --pluginsfrom the command line) lists every discovered plugin, valid or failed — a failed one shows its error rather than silently disappearing. SetSTRUCTILE_DISABLE_PLUGINSto any truthy value to skip discovery entirely (explicitregister_interpreter()calls are unaffected). - VS Code: Structile: Show Interpreter Resolution shows every candidate considered for the active file, each marked used, rejected, or not tried. Structile: List Contributed Interpreters lists every contribution from every installed extension. Structile: Reload Contributed Interpreters re-scans installed extensions after you edit a contributed interpreter's own content.