/** * BarefootJS Jinja2 Template Adapter * * Generates Jinja2 template files (.jinja) from BarefootJS IR. * * Near-mechanical port of the Text::Xslate (Kolon) adapter * (packages/adapter-xslate/src/adapter/xslate-adapter.ts), itself a * near-mechanical port of the Mojolicious EP adapter, from Kolon syntax to * Jinja2 syntax. The expression-lowering PIPELINE (JS → scalar values / * `bf.helper(...)` calls) is shared in spirit with the Xslate adapter; the * surrounding template syntax differs: * * Kolon `<: EXPR :>` → Jinja `{{ EXPR }}` (HTML-escaped) * Kolon `<: EXPR | mark_raw :>` → Jinja `{{ EXPR | safe }}` (raw) * Kolon `$bf.method(args)` → Jinja `bf.method(args)` * Kolon `$name` → Jinja `name` * Kolon `: if (C) { A : } else { B : }` → Jinja `{% if C %}A{% else %}B{% endif %}` (`elsif` → `{% elif %}`) * Kolon `: for $arr -> $item { … : }` → Jinja `{% for item in arr %}…{% endfor %}` * Kolon `: my $x = e;` → Jinja `{% set x = e %}` * Kolon `{ k => v }` hashref → Jinja `{'k': v}` dict literal (ALWAYS quoted key — see `lib/jinja-naming.ts`) * Kolon `~` concat → Jinja `~` concat * Kolon `//` defined-or → `(l if (l is defined and l is not none) else r)` inline (Jinja has no `//`; the `is defined` guard also treats a context var that was never seeded — Jinja's `ChainableUndefined` — as nullish, matching JS `??`'s null-OR-undefined check — see `expr/emitters.ts`'s `logical` for the full rationale) * Kolon macro children capture → Jinja `{% set NAME %}…{% endset %}` set-block (see "Children capture" below) * * The render `jinja2.Environment` this adapter's output assumes: * `autoescape=True`, `undefined=ChainableUndefined`, `FileSystemLoader`, * PLUS **`trim_blocks=True, lstrip_blocks=True`** — required because this * adapter places `{% … %}` control tags on their own source line (mirroring * Kolon's line-statement `:` mode, which consumes its own line for free); * without `trim_blocks`/`lstrip_blocks` every such line would leak a stray * newline/indentation into the rendered HTML. Templates are named * `.jinja` (same convention as Xslate's `.tx` files). * * Divergences beyond the syntax table above (all uniform, not per-fixture — * see the individual definition sites for the full rationale): * * 1. **JS truthiness** (`@barefootjs/jsx`'s `adapters/boolean-result.ts`, `expr/emitters.ts`'s * `truthyTest`, this file's `convertConditionToJinja`). Python's `[]` / * `{}` are falsy; JS's are truthy. Perl doesn't have this problem (a * Perl reference is always true), so Kolon needed no truthy-routing * layer. Every condition-TEST position (an `{% if %}` / `{% elif %}` * test, a ternary test, the left operand of `&&`/`||`) routes through * `bf.truthy(...)` unless it is structurally already boolean-shaped. * 2. **Stringification** (`bf.string`, applied at every text/attribute * interpolation position). Perl's default scalar stringification is * close enough to JS's `String(x)` that Kolon only special-cases * explicit `String()` calls and boolean-typed values (routed to * `bf.bool_str`). Python's default `str()` diverges further — * `str(True)` == `"True"`, `str(1.0)` == `"1.0"`, `str(None)` == * `"None"`, and Jinja's `~` concatenation operator calls `str()` on * each operand internally — so this port explicitly routes EVERY * text/attribute-position value (not already boolean-routed) through * `bf.string(...)` before it reaches Jinja's own escaping/concat * machinery. Verified empirically (`'a' ~ true ~ none` → `"aTrueNone"` * under plain Jinja) — the reason this wrapping is mandatory, not * cosmetic. * 3. **No Jinja lambda** (`expr/emitters.ts` header, divergence 2). Kolon's * `-> $x { … }` lambda — the Xslate top-level emitter's fallback when a * predicate callback can't be serialized to the runtime evaluator's * JSON form — has no Jinja equivalent. This adapter uses ONE mechanism * for every higher-order callback (the evaluator-JSON `*_eval` * payload); an unserializable predicate surfaces `BF101` instead of a * lambda fallback. `.sort`'s non-lambda STRUCTURED fallback * (`bf.sort` with a `{keys: […]}` descriptor) is unaffected and ports * unchanged. * 4. **Children/fallback capture via `{% set %}...{% endset %}`, never a * macro.** Every Kolon macro-capture site in the ported adapter * (`renderComponent`'s children forward, `renderAsync`'s fallback) is * invoked immediately, in place, with zero arguments — never reused * elsewhere or invoked lazily with different arguments. Jinja's * set-block (`{% set NAME %}…{% endset %}`) captures exactly that * shape as a `Markup` string (under `autoescape=True`) with no macro * indirection needed; the captured name is then referenced bare * (`NAME`, not `NAME()`) everywhere the Kolon port called `NAME()`. * 5. **Reserved-word identifier mangling** (`lib/jinja-naming.ts`). Every * bare Jinja variable reference / `{% set %}` target is passed through * `jinjaIdent()`; the Python runtime must apply the IDENTICAL mangling * when it builds the per-render context dict (so a prop literally * named e.g. `class` is threaded through as context key `'class_'` on * both sides). Dict-LITERAL keys are a separate, unconditional * concern — see `jinjaHashKey`'s docstring for why they are always * quoted (unlike Kolon's bareword-key sugar). * 6. **In-template signal/memo self-reference seeding is NOT skipped** * (`memo/seed.ts`'s file header) — Jinja's `{% set x = x + 1 %}` safely * resolves the right-hand `x` from the enclosing scope (verified * empirically), unlike Kolon's `my`-shadowing hazard, so a same-name * prop-derived signal/memo IS seeded in-template here (Xslate skips * it). Strictly more correct, not merely a port artifact. */ import type { ComponentIR, IRNode, IRElement, IRText, IRExpression, IRConditional, IRLoop, IRComponent, IRFragment, IRSlot, IRIfStatement, IRProvider, IRAsync, TemplatePrimitiveRegistry } from '@barefootjs/jsx'; import { DslTemplateAdapter, type IRNodeEmitter, type EmitIRNode } from '@barefootjs/jsx'; import type { JinjaRenderCtx } from './lib/types.ts'; import { jinjaIdent } from './lib/jinja-naming.ts'; export type { JinjaAdapterOptions } from './lib/types.ts'; import type { JinjaAdapterOptions } from './lib/types.ts'; /** * The `JinjaAdapter` template adapter. Pass an instance as the `adapter` option of `@barefootjs/vite`. * * @since 0.18.0 * @stability alpha */ export declare class JinjaAdapter extends DslTemplateAdapter implements IRNodeEmitter { name: string; extension: string; templatesPerComponent: boolean; /** * Identifier-path callees the Jinja runtime can render in template scope. * The relocate pass consults this map to mark matching calls as * template-safe; the SSR template emitter substitutes the JS call with the * registered `bf.NAME(...)` helper invocation. */ templatePrimitives: TemplatePrimitiveRegistry; private options; constructor(options?: JinjaAdapterOptions); protected readonly loopChildDiagnosticName = "Jinja"; protected readonly templateIdentifierSpelling: { ident: typeof jinjaIdent; label: string; }; /** * Names (signal getters + props) whose value is a string. Carried for * parity with the Perl-family adapters (Mojo needs it for `eq`/`ne` * selection); the Jinja emitters don't consume it — Jinja's `==`/`!=` * compare strings and numbers correctly. */ protected stringValueNamesFor(ir: ComponentIR): Set; protected contextConsumerSeed(ir: ComponentIR): string; protected derivedMemoSeed(ir: ComponentIR): string; protected generateScriptRegistrations(ir: ComponentIR, scriptBaseName?: string, scriptAssets?: string[], preloadAssets?: string[]): string; /** * Public entry point for node rendering. Delegates to the shared * `IRNodeEmitter` dispatcher; per-kind logic lives in the `IRNodeEmitter` * methods below. */ renderNode(node: IRNode): string; emitElement(node: IRElement, _ctx: JinjaRenderCtx, _emit: EmitIRNode): string; emitText(node: IRText): string; emitExpression(node: IRExpression): string; emitConditional(node: IRConditional, _ctx: JinjaRenderCtx, _emit: EmitIRNode): string; emitLoop(node: IRLoop, _ctx: JinjaRenderCtx, _emit: EmitIRNode): string; emitComponent(node: IRComponent, _ctx: JinjaRenderCtx, _emit: EmitIRNode): string; emitFragment(node: IRFragment, _ctx: JinjaRenderCtx, _emit: EmitIRNode): string; emitSlot(node: IRSlot): string; emitIfStatement(node: IRIfStatement, _ctx: JinjaRenderCtx, _emit: EmitIRNode): string; emitProvider(node: IRProvider, _ctx: JinjaRenderCtx, _emit: EmitIRNode): string; /** Lower a `` value prop to a Jinja expression. */ private providerValueJinja; /** * Lower an object-literal provider value (`value={{ open: () => props.open * ?? false, onOpenChange: … }}`) to a Jinja dict literal (#1897). The * SSR lowering is a per-member snapshot of what a consumer would READ * during the same render: * * - zero-param expression-body arrows are getters — lower the body (the * value is fixed for the render, so the call-time indirection drops out) * - `on[A-Z]`-named members and function-shaped values are client-only * behavior SSR never invokes — lower to `none` * - anything else lowers through the normal expression pipeline (so an * unsupported getter body still refuses loudly with BF101) * * Keys keep their JS names verbatim so a consumer-side `ctx.open` access * maps onto the same dict key. Returns `null` when the expression is not a * plain object literal (spread / computed key) — the caller falls back to * the whole-expression path, which refuses those shapes with BF101. */ private providerObjectLiteralJinja; emitAsync(node: IRAsync, _ctx: JinjaRenderCtx, _emit: EmitIRNode): string; renderElement(element: IRElement): string; /** * Route an `ssrPortalOwnerScope`-flagged element's ALREADY-RENDERED * markup (the `bf-po` attribute already stamped on its own tag) into * `bf.register_portal_element` instead of leaving it at its source * position (#3119). * * Jinja's native `{% set NAME %}…{% endset %}` set-block already * captures a nested render into a variable — the same mechanism * `renderComponent` uses for forwarded JSX children (see this file's * "Children capture" header section) — so no ERB/Go-style output-buffer * slicing or re-templating trick is needed: the element renders inline * as normal Jinja (its own `{{ }}` interpolations evaluate exactly as * they would anywhere else), the set-block captures the fully-evaluated * result into a local, and a SEPARATE `{% set _ = … %}` statement (never * `{{ }}`, so nothing prints at the source position) hands that string to * the portal collector — mirroring how `register_script` calls are * already threaded through `{% set %}` purely for their side effect * (`generateHead`/`generateRuntimeScripts` above). */ private wrapSsrPortalElement; renderExpression(expr: IRExpression): string; renderConditional(cond: IRConditional): string; /** * Add bf-c attribute to the first HTML element in a branch. * If no element found, wrap with comment markers. */ private addCondMarkerToFirstElement; renderLoop(loop: IRLoop): string; /** * AttrValue lowering for component invocation props (Jinja dict-entry * form). Jinja CANNOT splat a dict into positional args, so every prop is * emitted as a `'key': value` entry that the caller collects into ONE dict * literal passed to `bf.render_child(name, { ... })`. * * `jsx-children` returns empty — children are captured via a Jinja * set-block below, not threaded through the dict entry list. */ private readonly componentPropEmitter; /** * Fold ordered prop segments into a single Jinja expression via nested * `dict(base, **top)` calls — CPython's `dict()` builtin raises * `TemplateSyntaxError` on MORE THAN ONE `**` per call, so a flat * `dict(**a, **b, **c)` is not an option; each successive segment wraps * the accumulator as the positional `base` with the new segment * `**`-unpacked on top, later argument wins on key conflict — exactly * like `{...a, ...b}`. A spread segment's expression is wrapped * `(EXPR or {})` before unpacking: Jinja's `ChainableUndefined` lets a * missing bag (e.g. `children.props` when `children` was never passed) * chain through member access without raising, but `**`-unpacking an * `Undefined`/`None` value still fails, so the `or {}` guard normalises * it to an empty dict first (verified against real jinja2 3.1.6). * Empty `'entries'` segments are dropped so a leading/trailing spread * doesn't drag in a needless `dict({}, **...)`. Returns `'{}'` when * every segment is empty (no props at all). */ private combineComponentPropSegments; renderComponent(comp: IRComponent): string; /** Uniquifies the `presenceOrUndefined` temp binding (`bf_puN`) so two * presence-folded attrs in one template don't collide. */ private presenceVarCounter; protected renderIfStatement(ifStmt: IRIfStatement): string; /** * Wrap already-rendered output in the comment-based scope marker pair. * Shared by a `needsScopeComment` fragment root and a `needsScopeComment` * component root (#3141) — both are "this scope has no DOM element of its * own to carry bf-s/bf-p", so both lean on the same runtime helper pair. */ private wrapComponentRootScopeComment; private renderSlot; renderAsync(node: IRAsync): string; /** * AttrValue lowering for intrinsic-element attributes (Jinja). */ private readonly elementAttrEmitter; /** How this adapter spells the parts of an element `renderElementShell` writes. */ private readonly elementShell; /** * Lower a `style={{ … }}` object literal to a `bf.style_object(...)` call, * e.g. `{ backgroundColor: color }` → `{{ bf.style_object("background-color", * color) }}`. `style_object` is the single oracle-matching sanitizer (ported * from Hono's `hasUnsafeStyleValue`): it drops any key:value pair whose * value could break out of a CSS declaration and HTML-escapes the rest, * returning a `Markup` so Jinja's autoescape doesn't double-escape it (#2261). * Returns null when the shape is unsupported or any value can't be lowered * (caller falls through to BF101). (#1322) */ private tryLowerStyleObject; renderScopeMarker(_instanceIdExpr: string): string; renderSlotMarker(slotId: string): string; renderCondMarker(condId: string): string; /** * Convert a ParsedExpr AST to a Jinja expression string for filter * predicates. Wraps the shared ParsedExpr dispatcher with a * `JinjaFilterEmitter` carrying the predicate's loop param and any * block-body local var aliases. */ private renderJinjaFilterExpr; private convertTemplateLiteralPartsToJinja; /** * Build the EmitContext seam the top-level `ParsedExpr` emitter depends on. * Built as a private object (the adapter does NOT `implements JinjaEmitContext`) * so the wrapped bookkeeping — `_searchParamsLocals`, the const/record * resolvers, BF101 recording, the filter-predicate entry — stays private and * off the exported adapter's public type, matching the Go adapter's * `emitCtx` and the `spreadCtx` / `memoCtx` seams below. */ private get emitCtx(); /** * Build the narrow context the extracted spread lowering depends on. Passing * a purpose-built object (rather than `this`) keeps the adapter's bookkeeping * members private — they stay internal implementation detail, not part of the * exported class's public surface. */ private get spreadCtx(); /** Build the narrow context the extracted memo seeding depends on. */ private get memoCtx(); private convertExpressionToJinja; /** * Convert a JS condition (an `if` / ternary / loop-filter test) to a Jinja * boolean expression, routing through `bf.truthy(...)` unless the * expression is structurally already boolean-shaped. See the file header, * divergence 1. */ private convertConditionToJinja; /** * Shared helper: given the ORIGINAL JS expression (or its already-parsed * tree) and its ALREADY-RENDERED Jinja text, wrap the rendered text with * `bf.truthy(...)` unless the expression is structurally boolean-shaped. * Split from `convertConditionToJinja` so a caller that already lowered the * expression for another purpose (e.g. the `presenceOrUndefined` temp bind) * doesn't lower it twice. */ private wrapConditionExpr; /** * Render a full ParsedExpr tree to Jinja for top-level (non-filter) * expressions where identifiers are signals / template vars. */ private renderParsedExprToJinja; /** Whether `name` (a signal getter or prop) holds a string value. Carried * for parity with the Perl-family adapters; the Jinja emitters don't * consume it (Jinja's `==`/`!=` compare strings and numbers correctly). */ private _isStringValueName; /** * Parse `cond ? value : undefined` (or `: null`), returning the * condition/consequent source spans, else `null`. Used for the * attribute-omission rule (#1897). * * `testParsed`/`consequentParsed` (#2843 review) are the ACTUAL parsed * sub-trees, for the caller to pass through as `preParsed` — `condition`/ * `consequent` are `exprToString` output, a DEBUG formatter that renders * any nested `object-literal` as the non-reparseable `[UNSUPPORTED: …]` * placeholder (unlike `stringifyParsedExpr`, meant for round-tripping). * Re-parsing THAT string for a consequent like `queryHref(base, { tag })` * would corrupt the call's args before the lowering registry — now * consulted at any nesting depth (#2843) — ever sees them. Kept for any * caller that only needs the string form; the parsed trees are the * correct input wherever the caller can accept `preParsed`. */ /** Position-accurate loop-bound-name check — see `this.scope`'s docstring. */ private isLoopBoundName; /** * Whether an attribute-value expression should route through * `bf.bool_str` (vs. plain `bf.string`) at its interpolation site. * `isExplicitStringCall` is checked FIRST and short-circuits the other * three: an explicit `String(x)` call already lowers to `bf.string(x)`, * which — unlike Kolon's Perl port — correctly stringifies a real * boolean on its own (see `runtime.js_string`'s bool branch), so * layering `bf.bool_str` on top would run Python truthiness over the * ALREADY-STRINGIFIED text instead of the original boolean. See * `isExplicitStringCall`'s docstring in `@barefootjs/jsx`'s `adapters/boolean-result.ts` for the full * double-wrap failure mode this guards against. */ private shouldBoolStr; /** * Inline a const (any scope) whose initializer is a pure numeric or * single-quoted string literal (`const totalPages = 5`, #1897 * pagination) — function-scope consts never reach the per-render * context, so a bare reference would resolve to Undefined. * * The lookup is a flat name match with no notion of AST scope, so a * name bound by the CURRENTLY ENCLOSING loop's item/index/destructure/ * preamble param never inlines (#2221) — the occurrence may be the * loop's own (shadowing) binding, and substituting the outer const's * value there renders every iteration with the same hard-coded literal. * Position-accurate via the threaded `this.scope` (#2482 Stage 2) — a * same-named const elsewhere in the component, outside any loop that * shadows it, still inlines. */ private _resolveLiteralConst; /** * Resolve `IDENT.key` where `IDENT` is a module-scope object-literal const * (`variantClasses.ghost`, #1896/#1897) to the looked-up scalar. * * The lookup is a flat name match on `objectName` with no notion of AST * scope, so an enclosing loop callback's own param of the same name * (`.map((cfg) =>
  • {cfg.x}
  • )` shadowing a module `const cfg = {…}`) * still resolved to the OUTER const's member value at every iteration * (#2237) — the sibling hazard to #2221's `_resolveLiteralConst`. * Position-accurate via the threaded `this.scope` (#2482 Stage 2), passed * as `lookupStaticRecordLiteral`'s required guard: a name the CURRENTLY * ENCLOSING loop binds never inlines, falling back to the bare * `cfg['x']` member expression (which a Jinja for-loop binds correctly * at the shadowed occurrence); a same-named const elsewhere, outside any * loop that shadows it, still inlines. */ private _resolveStaticRecordLiteral; private _resolveModuleStringConst; private _recordExprBF101; /** Internal hook for higher-order: predicate body re-uses the filter emitter. */ private _renderJinjaFilterExprPublic; } export declare const jinjaAdapter: JinjaAdapter; //# sourceMappingURL=jinja-adapter.d.ts.map