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 | 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 23496x 23496x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 1183x 45x 45x 1183x 483x 159x 483x 55x 55x 483x 55x 55x 55x 45x 1183x 505x 505x 505x 505x 505x 505x 505x 505x 110x 110x 110x 110x 110x 505x 505x 505x 505x 505x 505x 505x 505x 55x 55x 55x 55x 55x 55x 66x 66x 55x 55x 55x 55x 55x 55x 55x 55x 55x 55x 49x 49x 49x 17x 17x 17x 7x 17x 10x 10x 10x 17x 17x 17x 17x 17x 12x 12x 12x 12x 12x 12x 12x 12x 12x 12x 12x 12x 5x 5x 5x 5x 5x 5x 5x 49x 38x 55x 55x 55x 55x 55x 55x 55x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 101x 101x 101x 101x 101x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 124x 124x 124x 124x 124x | import { tokenizeMarkup, applyEdits, quoteAttributeValue } from './markupLexer.js';
/**
* Creates a fresh regex matching template interpolations — both the raw
* triple-brace form and the escaped double-brace form.
*
* Expressions may span multiple lines (a long ternary or object literal wraps
* naturally, and formatters will wrap them), so the pattern must not stop at a
* newline. The compiler and the runtime share this single definition: when the
* two sides used different patterns, a wrapped expression passed compile-time
* validation and then rendered as literal braces at runtime with no diagnostic.
*
* A new instance is returned on every call because the regex is global and
* callers rely on their own `lastIndex`.
* @returns {RegExp} A fresh global interpolation regex.
*/
export function createInterpolationRegex() {
return /\{\{\{\s*([\s\S]*?)\s*\}\}\}|\{\{\s*([\s\S]*?)\s*\}\}/g;
}
/**
* Expands `data-ax-bind` on `<input>`, `<textarea>` and `<select>` elements.
*
* `data-ax-bind="expr"` becomes a value binding plus a write-back handler:
*
* - text inputs, textareas: `value="{{ expr }}" @input="expr = event.target.value"`
* - selects: `value="{{ expr }}" @change="expr = event.target.value"`
* - checkboxes: `checked` bound to the value (or membership, for an array) and
* an `@change` that writes or toggles membership
* - radios: `checked` bound to equality with the radio's value, and `@change`
*
* The template is read with the shared markup lexer, so an attribute value
* containing `>` no longer ends the tag early, and only the tag being expanded
* changes. Attributes are rejoined with single spaces, as before, but a value's
* own whitespace is kept. Generated attributes use whichever quote the
* expression does not contain, so `data-ax-bind='user["name"]'` expands to a
* binding the expression compiler can read.
* @param {string} template - The template string.
* @returns {string} The processed template.
* @throws {Error} When a bound expression contains both quote characters and
* cannot be written into an attribute verbatim.
*/
export function processBindDirectives(template) {
if (typeof template !== 'string' || !/data-ax-bind/i.test(template)) return template;
const edits = [];
for (const token of tokenizeMarkup(template)) {
if (token.type !== 'open' || token.unterminated || token.directive) continue;
const tagName = token.name.toLowerCase();
if (tagName !== 'input' && tagName !== 'textarea' && tagName !== 'select') continue;
const bind = token.attrs.find((attr) => attr.name.toLowerCase() === 'data-ax-bind' && attr.value !== null);
if (!bind) continue;
edits.push({ start: token.start, end: token.end, text: expandBoundTag(template, token, bind) });
}
return applyEdits(template, edits);
}
/**
* Writes one generated attribute, choosing a quote the value does not contain.
* @param {string} name - The attribute name.
* @param {string} value - The attribute value.
* @returns {string} `name="value"` or `name='value'`.
* @throws {Error} When the value contains both quote characters.
*/
function generatedAttribute(name, value) {
const quoted = quoteAttributeValue(value);
if (quoted === null) {
throw new Error(
`data-ax-bind cannot write ${name} for an expression containing both " and ': ${value}\n` +
'Move the expression into a computed value or an action and bind that name instead.',
);
}
return `${name}=${quoted}`;
}
/**
* Rebuilds one bound tag.
* @param {string} template - The template source.
* @param {object} token - The open-tag token.
* @param {object} bind - The `data-ax-bind` attribute token.
* @returns {string} The expanded tag.
*/
function expandBoundTag(template, token, bind) {
const tagName = token.name;
const lowerTag = tagName.toLowerCase();
const bindExpr = bind.value.trim();
const suffix = token.selfClosing ? ' />' : '>';
const attrValue = (name) => {
const found = token.attrs.find((attr) => attr.name.toLowerCase() === name);
return found ? found.value : undefined;
};
const kept = (exclude) =>
token.attrs
.filter((attr) => attr !== bind && !exclude(attr))
.map((attr) => template.slice(attr.start, attr.end));
const assemble = (attrs) => `<${[tagName, ...attrs].join(' ')}${suffix}`;
if (lowerTag === 'input') {
const type = (attrValue('type') || 'text').toLowerCase();
if (type === 'checkbox' || type === 'radio') {
const rawValue = attrValue('value');
let jsValue;
if (rawValue === undefined || rawValue === null) {
jsValue = "'on'";
} else if (rawValue.trim().includes('{{')) {
jsValue = rawValue.trim().replace(/\{\{\s*|\s*\}\}/g, '');
} else {
jsValue = `'${rawValue.trim().replace(/'/g, "\\'")}'`;
}
// `checked` is owned by the binding; an authored one would fight it.
const others = kept((attr) => attr.name.toLowerCase() === 'checked');
if (type === 'checkbox') {
return assemble([
...others,
generatedAttribute(
'checked',
`{{ Array.isArray(${bindExpr}) ? (${bindExpr}).includes(${jsValue}) : !!(${bindExpr}) }}`,
),
generatedAttribute(
'@change',
`Array.isArray(${bindExpr}) ? (event.target.checked ? (!(${bindExpr}).includes(${jsValue}) ? (${bindExpr}).push(${jsValue}) : null) : ((${bindExpr}).includes(${jsValue}) ? (${bindExpr}).splice((${bindExpr}).indexOf(${jsValue}), 1) : null)) : (${bindExpr} = event.target.checked)`,
),
]);
}
return assemble([
...others,
generatedAttribute('checked', `{{ (${bindExpr}) === ${jsValue} }}`),
generatedAttribute('@change', `${bindExpr} = event.target.value`),
]);
}
}
const eventName = lowerTag === 'select' ? 'change' : 'input';
return assemble([
...kept(() => false),
generatedAttribute('value', `{{ ${bindExpr} }}`),
generatedAttribute(`@${eventName}`, `${bindExpr} = event.target.value`),
]);
}
/**
* Hides one level of interpolation markers from the current render pass.
*
* A `<@for>` body is rendered once per item, so its `{{ }}` must survive the
* component's own render untouched and be resolved later, per row. The compiler
* therefore rewrites the body's markers to `{% %}` and the list manager restores
* them when it renders each row.
*
* The depth matters. A list nested inside a list is rendered twice, once as
* part of the outer row and once per inner item, so its markers have to survive
* two passes. Escaping deepens an existing marker rather than leaving it alone,
* so each nesting level adds one `%` and each render removes one. Before this,
* the inner body was escaped once and unescaped by the outer row, which meant
* `{{ row.id }}` was evaluated in the group's scope, where `row` does not
* exist, and every nested list rendered empty.
* @param {string} text - Template text to escape one level.
* @returns {string} The escaped text.
*/
export function escapeTemplateMarkers(text) {
if (typeof text !== 'string') return text;
return text
.replace(/\{\{|\{(%+)/g, (match, percents) => (percents ? `{${percents}%` : '{%'))
.replace(/\}\}|(%+)\}/g, (match, percents) => (percents ? `${percents}%}` : '%}'));
}
/**
* Restores one level of interpolation markers before rendering.
*
* The inverse of {@link escapeTemplateMarkers}: `{%` becomes `{{` and `{%%`
* becomes `{%`, so a nested body keeps exactly the levels its own nesting
* requires.
* @param {string} text - Template text to unescape one level.
* @returns {string} The unescaped text.
*/
export function unescapeTemplateMarkers(text) {
if (typeof text !== 'string') return text;
return text
.replace(/\{(%+)/g, (match, percents) => (percents.length === 1 ? '{{' : `{${percents.slice(1)}`))
.replace(/(%+)\}/g, (match, percents) => (percents.length === 1 ? '}}' : `${percents.slice(1)}}`));
}
|