Offline copy, generated from danieltuzes.github.io/structile — some content there may have moved on since this was downloaded.

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:

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:

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:

  1. Rename the distribution and the importable module. The zip's pyproject.toml calls the distribution structile-demo-plugin; the folder src/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 of structile_demo_plugin/structile-demo-plugin below needs to become your own package's name (acme_structile_formats in this walkthrough).

  2. Replace the interpreter script. Swap src/structile_demo_plugin/interpreters/demo_keyed.js for your own .js file (the props-interpreter.js from the worked example above, or your own) — keep it inside an interpreters/ subfolder of the package so the package-data line below can find it unchanged.

  3. 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
    
  4. Edit src/acme_structile_formats/__init__.py (renamed from structile_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.

  5. Install it locally and verify. From the package's own directory (wherever pyproject.toml is):

    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 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.

  6. Build and distribute it, exactly like any other Python package — nothing about structile.interpreters entry 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:

  1. Replace interpreters/demo_keyed.js with your own interpreter script.

  2. 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"
      }
    }
    
  3. 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.

  4. Distribute it. Three options, not mutually exclusive:

    • Hand out the .vsix directly — 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:

{ ".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