Wit

Embed Wit in an app

Embedding Wit is the five-stage pipeline plus your own DataSource — no CLI, no config file, no subprocess. This page is the copy-paste starting point: wrap the stages in one function, catch the two error families, and choose fragment or document output.

The worked helper

Verified end to end. Drop it into a server, a build step, or a static-site generator:

import { parse, WitError } from '@witlang/parser';
import {
  loadExternalData, resolve, expand, scanCloseless, RuntimeError,
} from '@witlang/runtime';
import { renderHtml } from '@witlang/render-html';

export function compileWit(
  src: string,
  file: string,                      // absolute path: `reference` resolves against it
  data: Record<string, unknown> = {},
): string {
  try {
    const closeless = scanCloseless(file, src);              // names needing no close
    const prepare   = (d) => loadExternalData(d, data);      // dict DataSource
    const parsed    = parse(src, file, { closeless });
    const resolved  = resolve(prepare(parsed), { rootPath: file, closeless, prepare });
    const expanded  = expand(resolved);
    return renderHtml(expanded, { mode: 'fragment' });
  } catch (err) {
    if (err instanceof WitError || err instanceof RuntimeError) {
      const { line, col } = err.loc;
      throw new Error(`${file}:${line}:${col}: ${err.code}: ${err.message}`);
    }
    throw err;
  }
}

WitError covers parse; RuntimeError (and its subclasses ResolverError and ExpanderError) covers resolve and expand. Both carry .code and .loc, so one branch formats them all.

The embedded data path

The data argument is a DataSource: a plain dict for static data, or a function for dynamic. No config file, no subprocess:

compileWit(src, 'report.wit', {
  meta:  { version: '0.4.0' },
  sales: [ { site: 'Dunmore', lit: 'yes' } ],
});

The document names each alias it needs with a @load line; a key in the dict with no matching @load is never read:

@load meta load@
@load sales load@

Version @meta.version.

@ul
(each @sales as s)
(li @s.site — @s.lit li)
(end)
ul@

From there @meta.version and (each @sales as s) read the data exactly as if it had been written inline.

Cross-file documents

If your documents use reference, pass rootPath and a synchronous fileReader so the referenced files resolve against your real or virtual filesystem. Give the same reader to scanCloseless, so a chapter can use a closeless definition from a shared file:

const reader    = (abs) => readFileFromCacheSync(abs);   // must be synchronous
const closeless = scanCloseless('/content/main.wit', src, reader);
resolve(parse(src, '/content/main.wit', { closeless }), {
  rootPath: '/content/main.wit',
  fileReader: reader,
  closeless,                    // used when parsing each referenced file
  prepare,                      // optional: per-file hook, e.g. loadExternalData
});

prepare runs on each referenced file right after it is parsed; the CLI uses it so a chapter's own @load lines load too.

Output choice

  • renderHtml(expanded, { mode: 'fragment' }) — an <article> to inject into a page you already own.
  • renderHtml(expanded, { mode: 'document' }) — a standalone <!doctype html> file.
  • renderMarkdown(expanded) — for a text or Markdown pipeline.

Common mistakes

  • Forgetting rootPath — a document with a reference throws E_MISSING_REFERENCE_FILE without it.
  • Skipping scanCloseless for multi-file documents — a chapter's @sig defined in a shared file then raises E_UNCLOSED_NODE where it opens a line.
  • Using an async fileReader — it must be synchronous.
  • Catching only one error family — parse throws WitError, the runtime throws RuntimeError; catch both.
  • Rendering per request without caching.

See also

External data · Resolve & expand · Rendering · CLI reference (the host wiring to contrast).