dalila 1.8.1 → 1.8.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/runtime/bind.js +529 -19
- package/package.json +1 -1
package/dist/runtime/bind.js
CHANGED
|
@@ -81,6 +81,492 @@ function warn(message) {
|
|
|
81
81
|
console.warn(`[Dalila] ${message}`);
|
|
82
82
|
}
|
|
83
83
|
}
|
|
84
|
+
const expressionCache = new Map();
|
|
85
|
+
function isIdentStart(ch) {
|
|
86
|
+
return /[a-zA-Z_$]/.test(ch);
|
|
87
|
+
}
|
|
88
|
+
function isIdentPart(ch) {
|
|
89
|
+
return /[a-zA-Z0-9_$]/.test(ch);
|
|
90
|
+
}
|
|
91
|
+
function tokenizeExpression(input) {
|
|
92
|
+
const tokens = [];
|
|
93
|
+
let i = 0;
|
|
94
|
+
const pushOp = (op) => {
|
|
95
|
+
tokens.push({ type: 'operator', value: op });
|
|
96
|
+
i += op.length;
|
|
97
|
+
};
|
|
98
|
+
while (i < input.length) {
|
|
99
|
+
const ch = input[i];
|
|
100
|
+
if (/\s/.test(ch)) {
|
|
101
|
+
i++;
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
if (isIdentStart(ch)) {
|
|
105
|
+
const start = i;
|
|
106
|
+
i++;
|
|
107
|
+
while (i < input.length && isIdentPart(input[i]))
|
|
108
|
+
i++;
|
|
109
|
+
const ident = input.slice(start, i);
|
|
110
|
+
if (ident === 'true')
|
|
111
|
+
tokens.push({ type: 'literal', value: true });
|
|
112
|
+
else if (ident === 'false')
|
|
113
|
+
tokens.push({ type: 'literal', value: false });
|
|
114
|
+
else if (ident === 'null')
|
|
115
|
+
tokens.push({ type: 'literal', value: null });
|
|
116
|
+
else if (ident === 'undefined')
|
|
117
|
+
tokens.push({ type: 'literal', value: undefined });
|
|
118
|
+
else
|
|
119
|
+
tokens.push({ type: 'identifier', value: ident });
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
if (/[0-9]/.test(ch)) {
|
|
123
|
+
const start = i;
|
|
124
|
+
i++;
|
|
125
|
+
while (i < input.length && /[0-9]/.test(input[i]))
|
|
126
|
+
i++;
|
|
127
|
+
if (input[i] === '.') {
|
|
128
|
+
i++;
|
|
129
|
+
while (i < input.length && /[0-9]/.test(input[i]))
|
|
130
|
+
i++;
|
|
131
|
+
}
|
|
132
|
+
const num = Number(input.slice(start, i));
|
|
133
|
+
tokens.push({ type: 'number', value: num });
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
if (ch === '"' || ch === "'") {
|
|
137
|
+
const quote = ch;
|
|
138
|
+
i++;
|
|
139
|
+
let value = '';
|
|
140
|
+
let closed = false;
|
|
141
|
+
while (i < input.length) {
|
|
142
|
+
const c = input[i];
|
|
143
|
+
if (c === '\\') {
|
|
144
|
+
const next = input[i + 1];
|
|
145
|
+
if (next === undefined)
|
|
146
|
+
break;
|
|
147
|
+
value += next;
|
|
148
|
+
i += 2;
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
if (c === quote) {
|
|
152
|
+
closed = true;
|
|
153
|
+
i++;
|
|
154
|
+
break;
|
|
155
|
+
}
|
|
156
|
+
value += c;
|
|
157
|
+
i++;
|
|
158
|
+
}
|
|
159
|
+
if (!closed) {
|
|
160
|
+
throw new Error('Unterminated string literal');
|
|
161
|
+
}
|
|
162
|
+
tokens.push({ type: 'string', value });
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
const three = input.slice(i, i + 3);
|
|
166
|
+
const two = input.slice(i, i + 2);
|
|
167
|
+
if (three === '===' || three === '!==') {
|
|
168
|
+
pushOp(three);
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
if (two === '&&' || two === '||' || two === '??' || two === '?.' || two === '==' || two === '!='
|
|
172
|
+
|| two === '>=' || two === '<=') {
|
|
173
|
+
pushOp(two);
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
if ('+-*/%!<>.?:'.includes(ch)) {
|
|
177
|
+
pushOp(ch);
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
if (ch === '(' || ch === ')') {
|
|
181
|
+
tokens.push({ type: 'paren', value: ch });
|
|
182
|
+
i++;
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
if (ch === '[' || ch === ']') {
|
|
186
|
+
tokens.push({ type: 'bracket', value: ch });
|
|
187
|
+
i++;
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
throw new Error(`Unexpected token "${ch}"`);
|
|
191
|
+
}
|
|
192
|
+
return tokens;
|
|
193
|
+
}
|
|
194
|
+
function parseExpression(input) {
|
|
195
|
+
const tokens = tokenizeExpression(input);
|
|
196
|
+
let index = 0;
|
|
197
|
+
const peek = () => tokens[index];
|
|
198
|
+
const next = () => tokens[index++];
|
|
199
|
+
const matchOperator = (...ops) => {
|
|
200
|
+
const token = peek();
|
|
201
|
+
if (token?.type === 'operator' && ops.includes(token.value)) {
|
|
202
|
+
index++;
|
|
203
|
+
return token.value;
|
|
204
|
+
}
|
|
205
|
+
return null;
|
|
206
|
+
};
|
|
207
|
+
const expectOperator = (value) => {
|
|
208
|
+
const token = next();
|
|
209
|
+
if (!token || token.type !== 'operator' || token.value !== value) {
|
|
210
|
+
throw new Error(`Expected "${value}"`);
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
const expectParen = (value) => {
|
|
214
|
+
const token = next();
|
|
215
|
+
if (!token || token.type !== 'paren' || token.value !== value) {
|
|
216
|
+
throw new Error(`Expected "${value}"`);
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
const expectBracket = (value) => {
|
|
220
|
+
const token = next();
|
|
221
|
+
if (!token || token.type !== 'bracket' || token.value !== value) {
|
|
222
|
+
throw new Error(`Expected "${value}"`);
|
|
223
|
+
}
|
|
224
|
+
};
|
|
225
|
+
const parsePrimary = () => {
|
|
226
|
+
const token = next();
|
|
227
|
+
if (!token)
|
|
228
|
+
throw new Error('Unexpected end of expression');
|
|
229
|
+
if (token.type === 'number')
|
|
230
|
+
return { type: 'literal', value: token.value };
|
|
231
|
+
if (token.type === 'string')
|
|
232
|
+
return { type: 'literal', value: token.value };
|
|
233
|
+
if (token.type === 'literal')
|
|
234
|
+
return { type: 'literal', value: token.value };
|
|
235
|
+
if (token.type === 'identifier')
|
|
236
|
+
return { type: 'identifier', name: token.value };
|
|
237
|
+
if (token.type === 'paren' && token.value === '(') {
|
|
238
|
+
const expr = parseConditional();
|
|
239
|
+
expectParen(')');
|
|
240
|
+
return expr;
|
|
241
|
+
}
|
|
242
|
+
throw new Error('Invalid expression');
|
|
243
|
+
};
|
|
244
|
+
const parseMember = () => {
|
|
245
|
+
let node = parsePrimary();
|
|
246
|
+
while (true) {
|
|
247
|
+
const token = peek();
|
|
248
|
+
if (token?.type === 'operator' && token.value === '.') {
|
|
249
|
+
next();
|
|
250
|
+
const prop = next();
|
|
251
|
+
if (!prop || prop.type !== 'identifier') {
|
|
252
|
+
throw new Error('Expected identifier after "."');
|
|
253
|
+
}
|
|
254
|
+
node = {
|
|
255
|
+
type: 'member',
|
|
256
|
+
object: node,
|
|
257
|
+
property: { type: 'literal', value: prop.value },
|
|
258
|
+
computed: false,
|
|
259
|
+
optional: false,
|
|
260
|
+
};
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
if (token?.type === 'operator' && token.value === '?.') {
|
|
264
|
+
next();
|
|
265
|
+
const nextToken = peek();
|
|
266
|
+
if (nextToken?.type === 'identifier') {
|
|
267
|
+
const prop = nextToken.value;
|
|
268
|
+
next();
|
|
269
|
+
node = {
|
|
270
|
+
type: 'member',
|
|
271
|
+
object: node,
|
|
272
|
+
property: { type: 'literal', value: prop },
|
|
273
|
+
computed: false,
|
|
274
|
+
optional: true,
|
|
275
|
+
};
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
if (nextToken?.type === 'bracket' && nextToken.value === '[') {
|
|
279
|
+
expectBracket('[');
|
|
280
|
+
const propertyExpr = parseConditional();
|
|
281
|
+
expectBracket(']');
|
|
282
|
+
node = {
|
|
283
|
+
type: 'member',
|
|
284
|
+
object: node,
|
|
285
|
+
property: propertyExpr,
|
|
286
|
+
computed: true,
|
|
287
|
+
optional: true,
|
|
288
|
+
};
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
throw new Error('Expected identifier or "[" after "?."');
|
|
292
|
+
}
|
|
293
|
+
if (token?.type === 'bracket' && token.value === '[') {
|
|
294
|
+
expectBracket('[');
|
|
295
|
+
const propertyExpr = parseConditional();
|
|
296
|
+
expectBracket(']');
|
|
297
|
+
node = {
|
|
298
|
+
type: 'member',
|
|
299
|
+
object: node,
|
|
300
|
+
property: propertyExpr,
|
|
301
|
+
computed: true,
|
|
302
|
+
optional: false,
|
|
303
|
+
};
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
break;
|
|
307
|
+
}
|
|
308
|
+
return node;
|
|
309
|
+
};
|
|
310
|
+
const parseUnary = () => {
|
|
311
|
+
const op = matchOperator('!', '+', '-');
|
|
312
|
+
if (op) {
|
|
313
|
+
return { type: 'unary', op, arg: parseUnary() };
|
|
314
|
+
}
|
|
315
|
+
return parseMember();
|
|
316
|
+
};
|
|
317
|
+
const parseMultiplicative = () => {
|
|
318
|
+
let node = parseUnary();
|
|
319
|
+
while (true) {
|
|
320
|
+
const op = matchOperator('*', '/', '%');
|
|
321
|
+
if (!op)
|
|
322
|
+
break;
|
|
323
|
+
node = { type: 'binary', op, left: node, right: parseUnary() };
|
|
324
|
+
}
|
|
325
|
+
return node;
|
|
326
|
+
};
|
|
327
|
+
const parseAdditive = () => {
|
|
328
|
+
let node = parseMultiplicative();
|
|
329
|
+
while (true) {
|
|
330
|
+
const op = matchOperator('+', '-');
|
|
331
|
+
if (!op)
|
|
332
|
+
break;
|
|
333
|
+
node = { type: 'binary', op, left: node, right: parseMultiplicative() };
|
|
334
|
+
}
|
|
335
|
+
return node;
|
|
336
|
+
};
|
|
337
|
+
const parseComparison = () => {
|
|
338
|
+
let node = parseAdditive();
|
|
339
|
+
while (true) {
|
|
340
|
+
const op = matchOperator('<', '>', '<=', '>=');
|
|
341
|
+
if (!op)
|
|
342
|
+
break;
|
|
343
|
+
node = { type: 'binary', op, left: node, right: parseAdditive() };
|
|
344
|
+
}
|
|
345
|
+
return node;
|
|
346
|
+
};
|
|
347
|
+
const parseEquality = () => {
|
|
348
|
+
let node = parseComparison();
|
|
349
|
+
while (true) {
|
|
350
|
+
const op = matchOperator('==', '!=', '===', '!==');
|
|
351
|
+
if (!op)
|
|
352
|
+
break;
|
|
353
|
+
node = { type: 'binary', op, left: node, right: parseComparison() };
|
|
354
|
+
}
|
|
355
|
+
return node;
|
|
356
|
+
};
|
|
357
|
+
const parseLogicalAnd = () => {
|
|
358
|
+
let node = parseEquality();
|
|
359
|
+
while (true) {
|
|
360
|
+
const op = matchOperator('&&');
|
|
361
|
+
if (!op)
|
|
362
|
+
break;
|
|
363
|
+
node = { type: 'binary', op, left: node, right: parseEquality() };
|
|
364
|
+
}
|
|
365
|
+
return node;
|
|
366
|
+
};
|
|
367
|
+
const parseLogicalOr = () => {
|
|
368
|
+
let node = parseLogicalAnd();
|
|
369
|
+
while (true) {
|
|
370
|
+
const op = matchOperator('||');
|
|
371
|
+
if (!op)
|
|
372
|
+
break;
|
|
373
|
+
node = { type: 'binary', op, left: node, right: parseLogicalAnd() };
|
|
374
|
+
}
|
|
375
|
+
return node;
|
|
376
|
+
};
|
|
377
|
+
const parseNullish = () => {
|
|
378
|
+
let node = parseLogicalOr();
|
|
379
|
+
while (true) {
|
|
380
|
+
const op = matchOperator('??');
|
|
381
|
+
if (!op)
|
|
382
|
+
break;
|
|
383
|
+
node = { type: 'binary', op, left: node, right: parseLogicalOr() };
|
|
384
|
+
}
|
|
385
|
+
return node;
|
|
386
|
+
};
|
|
387
|
+
const parseConditional = () => {
|
|
388
|
+
const condition = parseNullish();
|
|
389
|
+
if (!matchOperator('?'))
|
|
390
|
+
return condition;
|
|
391
|
+
const trueBranch = parseConditional();
|
|
392
|
+
expectOperator(':');
|
|
393
|
+
const falseBranch = parseConditional();
|
|
394
|
+
return { type: 'conditional', condition, trueBranch, falseBranch };
|
|
395
|
+
};
|
|
396
|
+
const root = parseConditional();
|
|
397
|
+
if (index < tokens.length) {
|
|
398
|
+
throw new Error('Unexpected token after end of expression');
|
|
399
|
+
}
|
|
400
|
+
return root;
|
|
401
|
+
}
|
|
402
|
+
function evalExpressionAst(node, ctx) {
|
|
403
|
+
const evalNode = (current) => {
|
|
404
|
+
if (current.type === 'literal')
|
|
405
|
+
return { ok: true, value: current.value };
|
|
406
|
+
if (current.type === 'identifier') {
|
|
407
|
+
if (!(current.name in ctx)) {
|
|
408
|
+
return {
|
|
409
|
+
ok: false,
|
|
410
|
+
reason: 'missing_identifier',
|
|
411
|
+
message: `Text interpolation: "${current.name}" not found in context`,
|
|
412
|
+
identifier: current.name,
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
return { ok: true, value: resolve(ctx[current.name]) };
|
|
416
|
+
}
|
|
417
|
+
if (current.type === 'member') {
|
|
418
|
+
const objectEval = evalNode(current.object);
|
|
419
|
+
if (!objectEval.ok)
|
|
420
|
+
return objectEval;
|
|
421
|
+
const obj = objectEval.value;
|
|
422
|
+
if (obj == null)
|
|
423
|
+
return { ok: true, value: undefined };
|
|
424
|
+
if (!current.computed) {
|
|
425
|
+
const key = current.property.value;
|
|
426
|
+
return { ok: true, value: resolve(obj[String(key)]) };
|
|
427
|
+
}
|
|
428
|
+
const propEval = evalNode(current.property);
|
|
429
|
+
if (!propEval.ok)
|
|
430
|
+
return propEval;
|
|
431
|
+
return { ok: true, value: resolve(obj[String(propEval.value)]) };
|
|
432
|
+
}
|
|
433
|
+
if (current.type === 'unary') {
|
|
434
|
+
const arg = evalNode(current.arg);
|
|
435
|
+
if (!arg.ok)
|
|
436
|
+
return arg;
|
|
437
|
+
if (current.op === '!')
|
|
438
|
+
return { ok: true, value: !arg.value };
|
|
439
|
+
if (current.op === '+')
|
|
440
|
+
return { ok: true, value: +arg.value };
|
|
441
|
+
return { ok: true, value: -arg.value };
|
|
442
|
+
}
|
|
443
|
+
if (current.type === 'conditional') {
|
|
444
|
+
const condition = evalNode(current.condition);
|
|
445
|
+
if (!condition.ok)
|
|
446
|
+
return condition;
|
|
447
|
+
return condition.value ? evalNode(current.trueBranch) : evalNode(current.falseBranch);
|
|
448
|
+
}
|
|
449
|
+
const left = evalNode(current.left);
|
|
450
|
+
if (!left.ok)
|
|
451
|
+
return left;
|
|
452
|
+
if (current.op === '&&') {
|
|
453
|
+
return left.value ? evalNode(current.right) : left;
|
|
454
|
+
}
|
|
455
|
+
if (current.op === '||') {
|
|
456
|
+
return left.value ? left : evalNode(current.right);
|
|
457
|
+
}
|
|
458
|
+
if (current.op === '??') {
|
|
459
|
+
return left.value == null ? evalNode(current.right) : left;
|
|
460
|
+
}
|
|
461
|
+
const right = evalNode(current.right);
|
|
462
|
+
if (!right.ok)
|
|
463
|
+
return right;
|
|
464
|
+
switch (current.op) {
|
|
465
|
+
case '+': return { ok: true, value: left.value + right.value };
|
|
466
|
+
case '-': return { ok: true, value: left.value - right.value };
|
|
467
|
+
case '*': return { ok: true, value: left.value * right.value };
|
|
468
|
+
case '/': return { ok: true, value: left.value / right.value };
|
|
469
|
+
case '%': return { ok: true, value: left.value % right.value };
|
|
470
|
+
case '<': return { ok: true, value: left.value < right.value };
|
|
471
|
+
case '>': return { ok: true, value: left.value > right.value };
|
|
472
|
+
case '<=': return { ok: true, value: left.value <= right.value };
|
|
473
|
+
case '>=': return { ok: true, value: left.value >= right.value };
|
|
474
|
+
case '==': return { ok: true, value: left.value == right.value };
|
|
475
|
+
case '!=': return { ok: true, value: left.value != right.value };
|
|
476
|
+
case '===': return { ok: true, value: left.value === right.value };
|
|
477
|
+
case '!==': return { ok: true, value: left.value !== right.value };
|
|
478
|
+
default:
|
|
479
|
+
return { ok: false, reason: 'parse', message: `Unsupported operator "${current.op}"` };
|
|
480
|
+
}
|
|
481
|
+
};
|
|
482
|
+
return evalNode(node);
|
|
483
|
+
}
|
|
484
|
+
function parseInterpolationExpression(expression) {
|
|
485
|
+
let ast = expressionCache.get(expression);
|
|
486
|
+
if (ast === undefined) {
|
|
487
|
+
try {
|
|
488
|
+
ast = parseExpression(expression);
|
|
489
|
+
expressionCache.set(expression, ast);
|
|
490
|
+
}
|
|
491
|
+
catch (err) {
|
|
492
|
+
expressionCache.set(expression, null);
|
|
493
|
+
return {
|
|
494
|
+
ok: false,
|
|
495
|
+
message: `Text interpolation parse error in "{${expression}}": ${err.message}`,
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
if (ast === null) {
|
|
500
|
+
return {
|
|
501
|
+
ok: false,
|
|
502
|
+
message: `Text interpolation parse error in "{${expression}}"`,
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
return { ok: true, ast };
|
|
506
|
+
}
|
|
507
|
+
function evaluateExpressionRaw(node, ctx) {
|
|
508
|
+
if (node.type === 'literal')
|
|
509
|
+
return { ok: true, value: node.value };
|
|
510
|
+
if (node.type === 'identifier') {
|
|
511
|
+
if (!(node.name in ctx)) {
|
|
512
|
+
return {
|
|
513
|
+
ok: false,
|
|
514
|
+
reason: 'missing_identifier',
|
|
515
|
+
message: `Text interpolation: "${node.name}" not found in context`,
|
|
516
|
+
identifier: node.name,
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
return { ok: true, value: ctx[node.name] };
|
|
520
|
+
}
|
|
521
|
+
if (node.type === 'member') {
|
|
522
|
+
const objectEval = evaluateExpressionRaw(node.object, ctx);
|
|
523
|
+
if (!objectEval.ok)
|
|
524
|
+
return objectEval;
|
|
525
|
+
const obj = objectEval.value;
|
|
526
|
+
if (obj == null)
|
|
527
|
+
return { ok: true, value: undefined };
|
|
528
|
+
if (!node.computed) {
|
|
529
|
+
const key = node.property.value;
|
|
530
|
+
return { ok: true, value: obj[String(key)] };
|
|
531
|
+
}
|
|
532
|
+
const propEval = evalExpressionAst(node.property, ctx);
|
|
533
|
+
if (!propEval.ok)
|
|
534
|
+
return propEval;
|
|
535
|
+
return { ok: true, value: obj[String(propEval.value)] };
|
|
536
|
+
}
|
|
537
|
+
// For non-member expressions, the regular evaluator is fine.
|
|
538
|
+
return evalExpressionAst(node, ctx);
|
|
539
|
+
}
|
|
540
|
+
function expressionDependsOnReactiveSource(node, ctx) {
|
|
541
|
+
if (node.type === 'identifier') {
|
|
542
|
+
const value = ctx[node.name];
|
|
543
|
+
return isSignal(value) || (typeof value === 'function' && value.length === 0);
|
|
544
|
+
}
|
|
545
|
+
if (node.type === 'literal')
|
|
546
|
+
return false;
|
|
547
|
+
if (node.type === 'unary')
|
|
548
|
+
return expressionDependsOnReactiveSource(node.arg, ctx);
|
|
549
|
+
if (node.type === 'binary') {
|
|
550
|
+
return expressionDependsOnReactiveSource(node.left, ctx) || expressionDependsOnReactiveSource(node.right, ctx);
|
|
551
|
+
}
|
|
552
|
+
if (node.type === 'conditional') {
|
|
553
|
+
return expressionDependsOnReactiveSource(node.condition, ctx)
|
|
554
|
+
|| expressionDependsOnReactiveSource(node.trueBranch, ctx)
|
|
555
|
+
|| expressionDependsOnReactiveSource(node.falseBranch, ctx);
|
|
556
|
+
}
|
|
557
|
+
if (node.type === 'member') {
|
|
558
|
+
if (expressionDependsOnReactiveSource(node.object, ctx)
|
|
559
|
+
|| (node.computed ? expressionDependsOnReactiveSource(node.property, ctx) : false)) {
|
|
560
|
+
return true;
|
|
561
|
+
}
|
|
562
|
+
const memberValue = evaluateExpressionRaw(node, ctx);
|
|
563
|
+
if (!memberValue.ok)
|
|
564
|
+
return false;
|
|
565
|
+
const value = memberValue.value;
|
|
566
|
+
return isSignal(value) || (typeof value === 'function' && value.length === 0);
|
|
567
|
+
}
|
|
568
|
+
return false;
|
|
569
|
+
}
|
|
84
570
|
// ============================================================================
|
|
85
571
|
// Default Options
|
|
86
572
|
// ============================================================================
|
|
@@ -94,7 +580,7 @@ const DEFAULT_RAW_TEXT_SELECTORS = 'pre, code';
|
|
|
94
580
|
*/
|
|
95
581
|
function bindTextNode(node, ctx, cleanups) {
|
|
96
582
|
const text = node.data;
|
|
97
|
-
const regex = /\{
|
|
583
|
+
const regex = /\{([^{}]+)\}/g;
|
|
98
584
|
// Check if there are any tokens
|
|
99
585
|
if (!regex.test(text))
|
|
100
586
|
return;
|
|
@@ -109,28 +595,52 @@ function bindTextNode(node, ctx, cleanups) {
|
|
|
109
595
|
if (before) {
|
|
110
596
|
frag.appendChild(document.createTextNode(before));
|
|
111
597
|
}
|
|
112
|
-
const
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
598
|
+
const rawToken = match[0];
|
|
599
|
+
const expression = match[1].trim();
|
|
600
|
+
const textNode = document.createTextNode('');
|
|
601
|
+
let warnedParse = false;
|
|
602
|
+
let warnedMissingIdentifier = false;
|
|
603
|
+
const parsed = parseInterpolationExpression(expression);
|
|
604
|
+
const applyResult = (result) => {
|
|
605
|
+
if (!result.ok) {
|
|
606
|
+
if (result.reason === 'parse') {
|
|
607
|
+
if (!warnedParse) {
|
|
608
|
+
warn(result.message);
|
|
609
|
+
warnedParse = true;
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
else if (!warnedMissingIdentifier) {
|
|
613
|
+
warn(result.message);
|
|
614
|
+
warnedMissingIdentifier = true;
|
|
615
|
+
}
|
|
616
|
+
// Backward compatibility for "{identifier}" missing from context:
|
|
617
|
+
// preserve the literal token exactly as before.
|
|
618
|
+
const simpleIdent = expression.match(/^[a-zA-Z_$][\w$]*$/);
|
|
619
|
+
if (result.reason === 'missing_identifier' && simpleIdent && result.identifier === simpleIdent[0]) {
|
|
620
|
+
textNode.data = rawToken;
|
|
621
|
+
}
|
|
622
|
+
else {
|
|
623
|
+
textNode.data = '';
|
|
624
|
+
}
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
627
|
+
textNode.data = result.value == null ? '' : String(result.value);
|
|
628
|
+
};
|
|
629
|
+
if (!parsed.ok) {
|
|
630
|
+
applyResult({ ok: false, reason: 'parse', message: parsed.message });
|
|
631
|
+
frag.appendChild(textNode);
|
|
632
|
+
cursor = match.index + match[0].length;
|
|
633
|
+
continue;
|
|
117
634
|
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
635
|
+
// First render is synchronous to avoid empty text until microtask flush.
|
|
636
|
+
applyResult(evalExpressionAst(parsed.ast, ctx));
|
|
637
|
+
// Only schedule reactive updates when expression depends on reactive sources.
|
|
638
|
+
if (expressionDependsOnReactiveSource(parsed.ast, ctx)) {
|
|
122
639
|
effect(() => {
|
|
123
|
-
|
|
124
|
-
textNode.data = v == null ? '' : String(v);
|
|
640
|
+
applyResult(evalExpressionAst(parsed.ast, ctx));
|
|
125
641
|
});
|
|
126
|
-
frag.appendChild(textNode);
|
|
127
|
-
}
|
|
128
|
-
else {
|
|
129
|
-
// Static value — or a function with params, in which case resolve()
|
|
130
|
-
// warns and returns undefined, normalised to empty string below.
|
|
131
|
-
const resolved = resolve(value);
|
|
132
|
-
frag.appendChild(document.createTextNode(resolved == null ? '' : String(resolved)));
|
|
133
642
|
}
|
|
643
|
+
frag.appendChild(textNode);
|
|
134
644
|
cursor = match.index + match[0].length;
|
|
135
645
|
}
|
|
136
646
|
// Add remaining text
|