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:
(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:
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 via importlib.resources (not a filesystem
path — the file may be inside a wheel/zip at runtime, so a plain
open(path) would break for some install methods). One package can
register more than one extension: call register_interpreter() more
than once inside the same register() function.
- Install it locally and verify. From the package's own directory
(wherever
pyproject.tomlis):
Expect your package listed and marked loaded, not failed. Then prove it actually resolves a file:
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 in st.plugins() marked as failed, with
the error, rather than silently vanishing. python -m structile
--plugins prints the same thing from the command line, and
STRUCTILE_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/token
For 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 that pip install put it on
sys.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
.props file 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.
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:
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.