@rhinostone/swig-core 2.0.0-alpha.4 → 2.0.0-alpha.5
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/lib/backend.js +69 -21
- package/lib/ir.js +35 -3
- package/lib/utils.js +36 -0
- package/package.json +1 -1
package/lib/backend.js
CHANGED
|
@@ -503,6 +503,7 @@ function emitExpr(node, d) {
|
|
|
503
503
|
switch (node.type) {
|
|
504
504
|
case 'Literal': return emitLiteral(node, d);
|
|
505
505
|
case 'VarRef': return emitVarRef(node, d);
|
|
506
|
+
case 'VarRefExists': return emitVarRefExists(node, d);
|
|
506
507
|
case 'Access': return emitAccess(node, d);
|
|
507
508
|
case 'BinaryOp': return emitBinaryOp(node, d);
|
|
508
509
|
case 'UnaryOp': return emitUnaryOp(node, d);
|
|
@@ -559,6 +560,55 @@ function emitVarRef(node, d) {
|
|
|
559
560
|
return checkMatchExpr(node.path);
|
|
560
561
|
}
|
|
561
562
|
|
|
563
|
+
/*!
|
|
564
|
+
* Emit an existence-only check for a dot-path variable. Result is a JS
|
|
565
|
+
* boolean expression — truthy when every segment of `node.path` resolves
|
|
566
|
+
* defined and non-null in either `_ctx` or the surrounding closure
|
|
567
|
+
* scope, falsy otherwise. Distinct from {@link emitVarRef}, which
|
|
568
|
+
* coerces a missing or null result to `""` and so loses the
|
|
569
|
+
* defined/undefined signal that Twig's `is defined` test and `??`
|
|
570
|
+
* undefined-fallback need to preserve. @private
|
|
571
|
+
*/
|
|
572
|
+
function emitVarRefExists(node, d) {
|
|
573
|
+
if (!utils.isArray(node.path) || node.path.length === 0) {
|
|
574
|
+
d.throwError('emitVarRefExists: path must be a non-empty array');
|
|
575
|
+
}
|
|
576
|
+
utils.each(node.path, function (segment) {
|
|
577
|
+
checkDangerousSegment(segment, d, node);
|
|
578
|
+
});
|
|
579
|
+
return '(' + checkDotExpr(node.path, '_ctx.') + ' || ' + checkDotExpr(node.path, '') + ')';
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
/*!
|
|
583
|
+
* Build a `(typeof <head> !== "undefined" && <head> !== null && ...)`
|
|
584
|
+
* expression that is truthy when every segment of `path` is defined and
|
|
585
|
+
* non-null under the given lookup prefix (`'_ctx.'` for the dotted-ctx
|
|
586
|
+
* walk, `''` for the bare-closure walk).
|
|
587
|
+
*
|
|
588
|
+
* Hoisted out of {@link checkMatchExpr}'s inline `checkDot` closure so
|
|
589
|
+
* {@link emitVarRefExists} can reuse the same shape for Twig's
|
|
590
|
+
* `is defined` / `is null` tests and `??` undefined-fallback. The output
|
|
591
|
+
* MUST stay byte-identical to the pre-extraction inline form, since
|
|
592
|
+
* {@link checkMatchExpr}'s downstream concatenation is what every
|
|
593
|
+
* compiled VarRef body relies on. @private
|
|
594
|
+
*/
|
|
595
|
+
function checkDotExpr(path, ctxPrefix) {
|
|
596
|
+
var c = ctxPrefix + path[0],
|
|
597
|
+
build = '';
|
|
598
|
+
|
|
599
|
+
build = '(typeof ' + c + ' !== "undefined" && ' + c + ' !== null';
|
|
600
|
+
utils.each(path, function (v, i) {
|
|
601
|
+
if (i === 0) {
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
build += ' && ' + c + '.' + v + ' !== undefined && ' + c + '.' + v + ' !== null';
|
|
605
|
+
c += '.' + v;
|
|
606
|
+
});
|
|
607
|
+
build += ')';
|
|
608
|
+
|
|
609
|
+
return build;
|
|
610
|
+
}
|
|
611
|
+
|
|
562
612
|
/*!
|
|
563
613
|
* Replica of `TokenParser.prototype.checkMatch`. Kept as a local private
|
|
564
614
|
* helper rather than imported from tokenparser.js because (a) it is a
|
|
@@ -567,30 +617,12 @@ function emitVarRef(node, d) {
|
|
|
567
617
|
* frontend concern, not a shared-backend one). @private
|
|
568
618
|
*/
|
|
569
619
|
function checkMatchExpr(match) {
|
|
570
|
-
var
|
|
571
|
-
|
|
572
|
-
function checkDot(ctx) {
|
|
573
|
-
var c = ctx + temp,
|
|
574
|
-
m = match,
|
|
575
|
-
build = '';
|
|
576
|
-
|
|
577
|
-
build = '(typeof ' + c + ' !== "undefined" && ' + c + ' !== null';
|
|
578
|
-
utils.each(m, function (v, i) {
|
|
579
|
-
if (i === 0) {
|
|
580
|
-
return;
|
|
581
|
-
}
|
|
582
|
-
build += ' && ' + c + '.' + v + ' !== undefined && ' + c + '.' + v + ' !== null';
|
|
583
|
-
c += '.' + v;
|
|
584
|
-
});
|
|
585
|
-
build += ')';
|
|
586
|
-
|
|
587
|
-
return build;
|
|
588
|
-
}
|
|
620
|
+
var result;
|
|
589
621
|
|
|
590
622
|
function buildDot(ctx) {
|
|
591
|
-
return '(' +
|
|
623
|
+
return '(' + checkDotExpr(match, ctx) + ' ? ' + ctx + match.join('.') + ' : "")';
|
|
592
624
|
}
|
|
593
|
-
result = '(' +
|
|
625
|
+
result = '(' + checkDotExpr(match, '_ctx.') + ' ? ' + buildDot('_ctx.') + ' : ' + buildDot('') + ')';
|
|
594
626
|
return '(' + result + ' !== null ? ' + result + ' : ' + '"" )';
|
|
595
627
|
}
|
|
596
628
|
|
|
@@ -625,6 +657,22 @@ function emitBinaryOp(node, d) {
|
|
|
625
657
|
if (node.op === 'in') {
|
|
626
658
|
return left + ' in ' + right;
|
|
627
659
|
}
|
|
660
|
+
// Twig/Jinja2 `~` is explicit string-concat: both sides coerce to
|
|
661
|
+
// string before `+` runs. A bare `<left>~<right>` emission would be
|
|
662
|
+
// JS unary bitwise-NOT and SyntaxError.
|
|
663
|
+
if (node.op === '~') {
|
|
664
|
+
return '(String(' + left + ') + String(' + right + '))';
|
|
665
|
+
}
|
|
666
|
+
// Twig `??` undefined-fallback: when LHS is a VarRef, route through
|
|
667
|
+
// IRVarRefExists to preserve the defined/undefined signal. emitVarRef
|
|
668
|
+
// coerces missing/null lookups to "", and "" is defined — a bare
|
|
669
|
+
// `<left>??<right>` emission would never take the fallback branch.
|
|
670
|
+
// Non-VarRef LHS (FnCall, FilterCall, Literal) doesn't coerce that
|
|
671
|
+
// way, so falling through to bare `left??right` is correct there.
|
|
672
|
+
if (node.op === '??' && node.left && node.left.type === 'VarRef') {
|
|
673
|
+
var existsNode = ir.varRefExists(node.left.path, node.left.loc);
|
|
674
|
+
return '(' + emitVarRefExists(existsNode, d) + ' ? ' + left + ' : ' + right + ')';
|
|
675
|
+
}
|
|
628
676
|
return left + node.op + right;
|
|
629
677
|
}
|
|
630
678
|
|
package/lib/ir.js
CHANGED
|
@@ -309,6 +309,27 @@
|
|
|
309
309
|
* @property {IRLoc} [loc]
|
|
310
310
|
*/
|
|
311
311
|
|
|
312
|
+
/**
|
|
313
|
+
* Existence check for a dot-path variable. Emits an expression that
|
|
314
|
+
* evaluates truthy when every segment of the path is defined and non-null
|
|
315
|
+
* (in either `_ctx` or the surrounding closure scope), false otherwise.
|
|
316
|
+
*
|
|
317
|
+
* Distinct from {@link IRVarRef}: VarRef coerces a missing or null result
|
|
318
|
+
* to the empty string for safe interpolation, which loses the
|
|
319
|
+
* defined/undefined signal that backends like Twig's `is defined` test
|
|
320
|
+
* and `??` undefined-fallback need. IRVarRefExists preserves that signal
|
|
321
|
+
* by returning the raw boolean disjunction of the dot-walks rather than
|
|
322
|
+
* the value itself.
|
|
323
|
+
*
|
|
324
|
+
* Every path segment MUST pass the dangerousProps guard at backend emit
|
|
325
|
+
* time, same rule as IRVarRef.
|
|
326
|
+
*
|
|
327
|
+
* @typedef {Object} IRVarRefExists
|
|
328
|
+
* @property {'VarRefExists'} type
|
|
329
|
+
* @property {string[]} path
|
|
330
|
+
* @property {IRLoc} [loc]
|
|
331
|
+
*/
|
|
332
|
+
|
|
312
333
|
/**
|
|
313
334
|
* Dynamic (bracket) property access: `obj[key]`. `key` is any expression.
|
|
314
335
|
* When `key` is an {@link IRLiteral} of kind `'string'`, the backend
|
|
@@ -407,9 +428,9 @@
|
|
|
407
428
|
* Any expression-position IR node.
|
|
408
429
|
*
|
|
409
430
|
* @typedef {(
|
|
410
|
-
* IRLiteral | IRVarRef |
|
|
411
|
-
* IRConditional | IRArrayLiteral | IRObjectLiteral |
|
|
412
|
-
* IRFilterCallExpr
|
|
431
|
+
* IRLiteral | IRVarRef | IRVarRefExists | IRAccess | IRBinaryOp |
|
|
432
|
+
* IRUnaryOp | IRConditional | IRArrayLiteral | IRObjectLiteral |
|
|
433
|
+
* IRFnCall | IRFilterCallExpr
|
|
413
434
|
* )} IRExpr
|
|
414
435
|
*/
|
|
415
436
|
|
|
@@ -762,6 +783,17 @@ exports.varRef = function (path, loc) {
|
|
|
762
783
|
return withLoc({ type: 'VarRef', path: path }, loc);
|
|
763
784
|
};
|
|
764
785
|
|
|
786
|
+
/**
|
|
787
|
+
* Build an {@link IRVarRefExists} existence check. Every path segment
|
|
788
|
+
* MUST pass the dangerousProps guard at backend emit time.
|
|
789
|
+
* @param {string[]} path
|
|
790
|
+
* @param {IRLoc} [loc]
|
|
791
|
+
* @return {IRVarRefExists}
|
|
792
|
+
*/
|
|
793
|
+
exports.varRefExists = function (path, loc) {
|
|
794
|
+
return withLoc({ type: 'VarRefExists', path: path }, loc);
|
|
795
|
+
};
|
|
796
|
+
|
|
765
797
|
/**
|
|
766
798
|
* Build an {@link IRAccess} dynamic-bracket property access.
|
|
767
799
|
* @param {IRExpr} object
|
package/lib/utils.js
CHANGED
|
@@ -182,3 +182,39 @@ exports.throwError = function (message, line, file) {
|
|
|
182
182
|
}
|
|
183
183
|
throw new Error(message + '.');
|
|
184
184
|
};
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Inclusive range generator. Mirrors Twig's `..` operator semantics and
|
|
188
|
+
* PHP's range(): numeric bounds produce [from, from±1, ..., to]; single-
|
|
189
|
+
* character string bounds produce a char array across the charCode span.
|
|
190
|
+
* Descending when `from > to`. Mismatched or unsupported argument shapes
|
|
191
|
+
* return an empty array so compiled templates degrade silently rather
|
|
192
|
+
* than throwing at render time.
|
|
193
|
+
*
|
|
194
|
+
* @param {number|string} from
|
|
195
|
+
* @param {number|string} to
|
|
196
|
+
* @return {Array}
|
|
197
|
+
*/
|
|
198
|
+
exports.range = function (from, to) {
|
|
199
|
+
var out = [], i, fc, tc;
|
|
200
|
+
if (typeof from === 'number' && typeof to === 'number') {
|
|
201
|
+
if (from <= to) {
|
|
202
|
+
for (i = from; i <= to; i += 1) { out.push(i); }
|
|
203
|
+
} else {
|
|
204
|
+
for (i = from; i >= to; i -= 1) { out.push(i); }
|
|
205
|
+
}
|
|
206
|
+
return out;
|
|
207
|
+
}
|
|
208
|
+
if (typeof from === 'string' && typeof to === 'string' &&
|
|
209
|
+
from.length === 1 && to.length === 1) {
|
|
210
|
+
fc = from.charCodeAt(0);
|
|
211
|
+
tc = to.charCodeAt(0);
|
|
212
|
+
if (fc <= tc) {
|
|
213
|
+
for (i = fc; i <= tc; i += 1) { out.push(String.fromCharCode(i)); }
|
|
214
|
+
} else {
|
|
215
|
+
for (i = fc; i >= tc; i -= 1) { out.push(String.fromCharCode(i)); }
|
|
216
|
+
}
|
|
217
|
+
return out;
|
|
218
|
+
}
|
|
219
|
+
return out;
|
|
220
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rhinostone/swig-core",
|
|
3
|
-
"version": "2.0.0-alpha.
|
|
3
|
+
"version": "2.0.0-alpha.5",
|
|
4
4
|
"description": "Shared IR, backend, and runtime for the @rhinostone/swig family of template engines. First publish at 2.0.0-alpha.3 — see @rhinostone/swig #T14 (Phase 1 carve).",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"template",
|