Templating Engines
@telorun/templating is the shared core that powers expression evaluation and
per-property templating across Telo's host packages — kernel, analyzer, telo
editor, and the VS Code extension. It owns three things:
- CEL primitives — the CEL
Environmentbuilder, the${{ }}compile path, and the chain validator used by static analysis. - A pluggable engine registry — every templating engine that participates
in YAML tag dispatch (today
!celand!literal) is defined here. - The YAML
customTagsfactory — a single source of truth that everyparseAllDocumentscall site uses, so the parse-side configuration cannot drift between hosts.
Tag-based templating
By default, a string scalar in a manifest is interpreted as a CEL template:
any ${{ ... }} segments are interpolated, anything else is plain text.
greeting: "Hello ${{ variables.name }}!" # untagged — CEL interpolation
For per-property control, prefix the value with an explicit YAML tag:
greeting: !cel 'variables.name'
header: !literal '${{ this is not interpolated }}'
fallback: 'Hello ${{ variables.name }}!' # untagged
| Tag | Semantics |
|---|---|
!cel | The entire scalar is one CEL expression. No ${{ }} wrapping. |
!literal | The scalar is opaque text — no interpolation, no analysis. |
| untagged | ${{ }} segments interpolated; everything else literal. |
!literal is the escape hatch for fields that need to carry literal ${{ }}
text — JSON Schema const values, pattern strings, code samples, etc.
Built-in engines
cel
Treats the source as a single CEL expression. Compile produces a
CompiledValue that the kernel evaluates against an EvaluationContext at
runtime. Static analysis runs the same chain validator as the untagged
${{ }} path: parse → extract member-access chains → validate each chain
against the effective context schema.
The Monaco language id for the editor is cel.
literal
Returns the source string verbatim at compile time. Static analysis is a
no-op. Useful any time a value happens to contain ${{ }} for unrelated
reasons.
Adding a new engine
The package exports a single source of truth — builtinEngines and
createDefaultRegistry(). Per-host à-la-carte registration is forbidden:
that path lets a manifest validate clean in one host (e.g. cel only) and
crash in another (e.g. cel + literal). New engines are added by extending
builtinEngines here, then propagating to every host on the next install.
An engine implements:
interface TemplatingEngine {
/** Registry key matching the YAML tag name (without `!`). */
name: string;
/** Optional Monaco language id for editor syntax highlighting. */
language?: string;
/** Convert a tagged source string into a runtime value. Called once at
* precompile. Returns either a CompiledValue (engines that defer
* evaluation to a runtime EvalContext, like cel) or a plain value
* (engines that resolve fully at compile time, like literal). */
compile(source: string, env: CompileEnv): CompiledValue | unknown;
/** Static analysis hook. Engines that can't statically check (e.g.
* literal) return []. The walker accumulates diagnostics across all
* values. */
analyze(source: string, env: AnalyzeEnv): readonly EngineDiagnostic[];
}
Architecture notes
- Browser-safe. The package has no Node built-in dependencies. The analyzer (which must run in the browser per Telo's architecture) consumes it transitively without polyfills.
CompiledValueis unchanged. Engine identity is captured inside thecall(ctx)closure. The kernel's hot path stays a single dispatch throughisCompiledValue(v) && v.call(ctx); it does not learn about engines.- One registry, one parse config. The
defaultCustomTags()helper memoizes thecustomTagsarray built from the default registry. EveryparseAllDocumentscall site imports it.