@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 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 temp = match[0], result;
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 '(' + checkDot(ctx) + ' ? ' + ctx + match.join('.') + ' : "")';
623
+ return '(' + checkDotExpr(match, ctx) + ' ? ' + ctx + match.join('.') + ' : "")';
592
624
  }
593
- result = '(' + checkDot('_ctx.') + ' ? ' + buildDot('_ctx.') + ' : ' + buildDot('') + ')';
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 | IRAccess | IRBinaryOp | IRUnaryOp |
411
- * IRConditional | IRArrayLiteral | IRObjectLiteral | IRFnCall |
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.4",
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",