Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 | 329x 329x 329x 329x 329x 329x 329x 329x 329x 329x 329x 329x 329x 329x 329x 329x 329x 329x 329x 329x 329x 329x 329x 329x 329x 329x 329x 329x 329x 329x 329x 329x 329x 329x 329x 329x 329x 329x 329x 329x 329x 329x 329x 329x 329x 329x 329x 329x 329x 329x 329x 329x 329x 329x 329x 329x 329x 329x 329x 329x 329x 329x 329x 329x 329x 329x 329x 329x 329x 329x 329x 329x 329x 329x 329x 329x 329x 329x 329x 329x 329x 329x 329x 329x 329x 329x 329x 329x 329x 329x 329x 329x 329x 329x 329x 329x 329x 329x 329x 329x 329x 329x 329x 329x 329x 329x 329x 329x 329x 329x 329x 329x 329x 329x 329x 329x 329x 329x 329x 329x 329x 329x 329x 329x 329x 329x 329x 329x 329x 329x 329x 329x 329x 48x 48x 48x 48x 48x 329x 329x 329x 329x 329x 329x 329x 329x 616x 616x 329x 329x 329x 329x 329x 329x 329x 1784x 1784x 329x 329x 329x 329x 329x 329x 329x 329x 576x 576x 329x 329x 329x 329x 329x 329x 329x 1x 1x 329x 329x 329x 329x 329x 329x 329x 329x 329x 329x 329x 329x 329x 329x 1411x 1411x 1411x 1411x 1411x 1411x 1411x 1411x 1411x 1411x 1411x 329x 329x 329x 329x 329x 329x 329x 329x 329x 329x 329x 17x 17x 329x 329x 329x 329x 329x 329x 329x 329x 329x 329x 329x 329x 329x 329x 329x 329x 329x 329x 53x 53x 53x 53x 53x 53x 53x 53x 53x 53x 329x 329x 329x 329x 329x 329x 329x 329x 329x 329x 133x 133x 133x 133x 133x 133x 133x 329x 329x 329x 329x 329x 329x 329x 329x 9x 9x 329x 329x 329x 329x 329x 329x 329x 329x 329x 329x 12x 12x 329x 329x 329x 329x 329x 329x 329x 329x 329x 329x 329x 329x 329x 329x 329x 329x 329x 329x 50x 50x 50x 50x 30x 30x 50x 3x 3x 50x 5x 5x 5x 50x 1x 1x 50x 1x 1x 1x 50x | /**
* @file nodes.js
* @description The template intermediate representation, and the vocabulary the
* front and back halves of the compiler agree on.
*
* ## Why an IR exists
*
* Avenx used to compile a template by rewriting it into different markup.
* `<@for item in items>` became `<template data-ax-for="items"
* data-ax-as="item">`, `<@suspense>` became `<div data-ax-suspense>`, and the
* runtime rediscovered what each of them meant by reading those attributes back
* off the live DOM with `querySelectorAll`.
*
* That is a compiler that throws its own analysis away. Every consequence
* followed from it: the render-program backend could not compile a list,
* because by the time it ran the list was an anonymous `<template>` element
* carrying strings; the runtime had to ship a second renderer to interpret
* those attributes; and a `>` inside a header expression broke the rewrite
* because a regex was the only thing left that could read it.
*
* The IR is where a construct's meaning is written down instead. A `<@for>`
* becomes a {@link ForNode} with a list expression, a binding name, an optional
* key and two child fragments. Nothing downstream has to guess, and nothing has
* to parse markup a second time.
*
* ## The shape, and why fragments are explicit
*
* A {@link FragmentNode} is a compile boundary: each one becomes its own
* skeleton and its own op list in the emitted program, and its own DOM range at
* run time. Control flow owns fragments rather than containing raw children,
* because "the body of this loop" is exactly the unit that gets cloned per item
* and torn down per removal. Making that unit implicit is how a list renderer
* ends up re-deriving its own boundaries.
*
* ## Extension
*
* Adding a construct means adding a kind here, a builder case in
* {@link module:lib/compiler/ir/build}, and a lowering case in
* {@link module:lib/compiler/ir/lower}. A construct with no lowering case is
* refused with its own reason rather than silently mis-emitted, which is the
* same compile-or-refuse rule the render program has always followed -- moved
* one layer earlier, where the reason is still specific enough to be useful.
* @module lib/compiler/ir/nodes
*/
/**
* IR node kinds.
* @enum {string}
*/
export const IRKind = {
/** An ordered list of children forming one compile and DOM boundary. */
FRAGMENT: 'fragment',
/** A literal element. */
ELEMENT: 'element',
/** Literal character data. */
TEXT: 'text',
/** A `{{ }}` or `{{{ }}}` interpolation. */
INTERPOLATION: 'interpolation',
/** An HTML comment preserved in the output. */
COMMENT: 'comment',
/** Conditional rendering: `<@if>` / `<@elseif>` / `<@else>`. */
IF: 'if',
/** Iteration: `<@for>` / `<@empty>`. */
FOR: 'for',
/** A child component instantiation. */
COMPONENT: 'component',
/** A `<slot>` outlet. */
SLOT: 'slot',
/** Content rendered once a trigger fires: `<@defer>`. */
DEFER: 'defer',
};
/**
* Kinds of value binding an element can carry.
* @enum {string}
*/
export const BindingKind = {
/** `attr="{{ expr }}"` -- the whole value is one expression. */
ATTR: 'attr',
/** `attr="a {{ b }} c"` -- literal and expression parts. */
ATTR_PARTS: 'attrParts',
/** A boolean attribute driven by an expression's truthiness. */
BOOL: 'bool',
/** `data-ax-show` -- toggles `display`. */
SHOW: 'show',
/** `data-ax-class` -- string or object class binding. */
CLASS: 'class',
/** `data-ax-html` -- replaces inner HTML. */
HTML: 'html',
/** `data-ax-style` -- string or object inline style binding. */
STYLE: 'style',
/** A prop passed to a child component. */
PROP: 'prop',
};
/**
* Why a template, or part of one, could not be represented in the IR.
*
* These are the constructs the IR does not model yet. They are enumerated
* rather than free text so a build can group them, and so a reader can tell
* "not implemented" from "gave up".
* @enum {string}
*/
export const RefusalReason = {
SUSPENSE: 'a <@suspense> boundary',
ERROR_BOUNDARY: 'an <@errorBoundary>',
DEADLOCK: 'a <@deadlock> boundary',
DEFER: 'a <@defer> block',
TRANSITION: 'a transition',
RESOURCE: 'a <resource> declaration in the template',
DYNAMIC_COMPONENT: 'a dynamic component tag',
DYNAMIC_ATTR: 'a dynamic attribute name',
ROUTER_VIEW: 'a router view',
VALIDATION: 'declarative form validation',
REF: 'a template ref',
VIRTUAL_LIST: 'a virtualised list',
MALFORMED: 'a malformed template',
UNKNOWN_DIRECTIVE: 'an unrecognised directive',
};
/**
* A construct the IR builder declined to represent.
*
* Thrown rather than returned because a refusal aborts the whole template --
* there is no partial IR. A half-built IR would describe a template that does
* not exist.
*/
export class IRRefusal extends Error {
/**
* @param {string} reason - A {@link RefusalReason}.
* @param {string} detail - What in the template caused it.
*/
constructor(reason, detail) {
super(`${reason}: ${detail}`);
this.name = 'IRRefusal';
this.reason = reason;
this.detail = detail;
}
}
/**
* Creates a fragment.
* @param {object[]} [children] - Child IR nodes.
* @returns {object} The fragment node.
*/
export function fragment(children = []) {
return { kind: IRKind.FRAGMENT, children };
}
/**
* Creates a literal text node.
* @param {string} value - The character data.
* @returns {object} The text node.
*/
export function text(value) {
return { kind: IRKind.TEXT, value };
}
/**
* Creates an interpolation node.
* @param {string} expr - The expression source.
* @param {boolean} [raw] - True for `{{{ }}}`, which inserts markup.
* @returns {object} The interpolation node.
*/
export function interpolation(expr, raw = false) {
return { kind: IRKind.INTERPOLATION, expr, raw };
}
/**
* Creates a comment node.
* @param {string} value - The comment body.
* @returns {object} The comment node.
*/
export function comment(value) {
return { kind: IRKind.COMMENT, value };
}
/**
* Creates an element node.
* @param {string} tag - The tag name.
* @param {object} [options] - Element parts.
* @param {Object<string,string>} [options.attrs] - Static attributes.
* @param {object[]} [options.bindings] - Value bindings.
* @param {object[]} [options.events] - Event bindings.
* @param {object[]} [options.children] - Child nodes.
* @param {boolean} [options.selfClosing] - Whether the tag is void or self-closed.
* @param {boolean} [options.isStatic] - Whether the subtree provably never changes.
* @returns {object} The element node.
*/
export function element(tag, options = {}) {
return {
kind: IRKind.ELEMENT,
tag,
attrs: options.attrs || {},
bindings: options.bindings || [],
events: options.events || [],
children: options.children || [],
selfClosing: options.selfClosing === true,
isStatic: options.isStatic === true,
};
}
/**
* Creates a conditional node.
*
* Branches are ordered and the first whose test is truthy renders. A branch
* with a null test is the `<@else>` and may only appear last; the builder
* enforces that, so nothing downstream has to re-check it.
* @param {Array<{test: string|null, body: object}>} branches - Ordered branches.
* @returns {object} The conditional node.
*/
export function conditional(branches) {
return { kind: IRKind.IF, branches };
}
/**
* Creates an iteration node.
*
* Exactly one of `item` and `destructure` is set: the first for `x in xs`, the
* second for `[a, b] in pairs`, which destructures each element rather than
* binding an index. The index is bound implicitly under the name `index` by
* the runtime and is therefore not part of the node.
* @param {object} parts - Loop parts.
* @param {string} parts.list - The list expression source.
* @param {string|null} parts.item - The name bound to each element.
* @param {string[]|null} parts.destructure - Names each element is destructured into.
* @param {string|null} parts.key - The key expression source, when declared.
* @param {object} parts.body - The per-item fragment.
* @param {object|null} parts.empty - The fragment rendered for an empty list.
* @returns {object} The iteration node.
*/
export function iteration(parts) {
return {
kind: IRKind.FOR,
list: parts.list,
item: parts.item || null,
destructure: parts.destructure || null,
key: parts.key || null,
body: parts.body,
empty: parts.empty || null,
};
}
/**
* Creates a child-component node.
* @param {string} name - The component's registered PascalCase name.
* @param {object} [options] - Component parts.
* @param {object[]} [options.props] - Static and bound props.
* @param {object[]} [options.children] - Transcluded content.
* @returns {object} The component node.
*/
export function component(name, options = {}) {
return {
kind: IRKind.COMPONENT,
name,
props: options.props || [],
children: options.children || [],
};
}
/**
* Creates a slot outlet.
* @param {string} name - The slot name; `default` when unnamed.
* @param {object} [fallbackFragment] - Content rendered when nothing is transcluded.
* @returns {object} The slot node.
*/
export function slot(name, fallbackFragment = null) {
return { kind: IRKind.SLOT, name, fallback: fallbackFragment };
}
/**
* Creates a deferred block.
* @param {string} when - The trigger: `idle`, `visible`, `hover`, `click`,
* `interaction`, a duration such as `2s`, or an expression.
* @param {object} body - The fragment rendered once the trigger fires.
* @param {object|null} [placeholder] - The fragment rendered until it does.
* @returns {object} The defer node.
*/
export function deferred(when, body, placeholder = null) {
return { kind: IRKind.DEFER, when, body, placeholder };
}
/**
* Whether a node introduces its own compile and DOM boundary.
*
* Control flow does; an element does not. Used by the lowering pass to decide
* where one skeleton ends and the next begins.
* @param {object} node - An IR node.
* @returns {boolean} True when the node owns fragments.
*/
export function isBlockNode(node) {
return node && (node.kind === IRKind.IF || node.kind === IRKind.FOR || node.kind === IRKind.SLOT || node.kind === IRKind.DEFER);
}
/**
* Walks every node in an IR tree, depth first, including fragment bodies.
* @param {object} node - The root node.
* @param {function(object, (object|null)): void} visit - Called with each node and its parent.
* @param {object|null} [parent] - The parent, for recursive calls.
*/
export function walkIR(node, visit, parent = null) {
if (!node || typeof node !== 'object') return;
visit(node, parent);
if (Array.isArray(node.children)) {
for (const child of node.children) walkIR(child, visit, node);
}
if (node.kind === IRKind.IF) {
for (const branch of node.branches) walkIR(branch.body, visit, node);
}
if (node.kind === IRKind.FOR) {
walkIR(node.body, visit, node);
if (node.empty) walkIR(node.empty, visit, node);
}
if (node.kind === IRKind.SLOT && node.fallback) {
walkIR(node.fallback, visit, node);
}
if (node.kind === IRKind.DEFER) {
walkIR(node.body, visit, node);
if (node.placeholder) walkIR(node.placeholder, visit, node);
}
}
|