@rhinostone/swig-core 2.2.0 → 2.4.0

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/ir.js CHANGED
@@ -1,14 +1,14 @@
1
1
  /**
2
2
  * Swig IR — intermediate representation for the shared backend.
3
3
  *
4
- * Phase 1: typedef stubs only. No runtime code here Phase 1 keeps the
5
- * native frontend emitting JS source directly. Phase 2 ports the native
6
- * frontend to emit IR, at which point `@rhinostone/swig-core/lib/backend.js`
7
- * walks an IRTemplate and produces the compiled `new Function(...)` body.
4
+ * Two halves: the `@typedef` schema for every IR node shape, and the
5
+ * runtime factories (further down) that build them. Every frontend
6
+ * (native Swig, Twig, and future flavors) lowers its parse tree into
7
+ * these shapes; `@rhinostone/swig-core/lib/backend.js` then walks an
8
+ * IRTemplate and produces the compiled `new Function(...)` body.
8
9
  *
9
- * Every frontend (native Swig, Twig, Jinja2, Django) must lower its
10
- * parse tree into these shapes. Constructs that cannot lower cleanly
11
- * must throw at parse time — no silent partial behavior.
10
+ * Constructs that cannot lower cleanly must throw at parse time — no
11
+ * silent partial behavior.
12
12
  */
13
13
 
14
14
  /**
@@ -103,11 +103,17 @@
103
103
  /**
104
104
  * For-loop. `emptyBody` supports Twig/Django `{% for … %}{% else %}`.
105
105
  *
106
+ * `iterable` is typed `IRExpr | string` for Phase 2 — the native
107
+ * frontend still hands in a raw JS-source string (its `for` tag has
108
+ * not migrated to expression-level IR), while the Twig frontend lowers
109
+ * to a real {@link IRExpr}. Backends MUST tolerate both shapes — same
110
+ * transitional widening as {@link IRSet}'s `target`.
111
+ *
106
112
  * @typedef {Object} IRFor
107
113
  * @property {'For'} type
108
114
  * @property {string} [key] Loop key var (second binding).
109
115
  * @property {string} value Loop value var (first binding).
110
- * @property {IRExpr} iterable
116
+ * @property {IRExpr|string} iterable Transitional — see note above.
111
117
  * @property {IRStatement[]} body
112
118
  * @property {IRStatement[]} [emptyBody]
113
119
  * @property {IRLoc} [loc]
@@ -279,12 +285,13 @@
279
285
  * In sync codegen mode the parser pre-resolves `extends` / `include` /
280
286
  * `import` / Twig `from` paths via `swig.parseFile(...)` and inlines
281
287
  * the resolved tokens at parse-finalization time. That model can't run
282
- * against an async-only loader (S3 / Redis / fetch-backed), and it
288
+ * against an async-only loader (S3 / Redis / network-backed), and it
283
289
  * can't handle dynamic paths (`{% extends parent_var %}`) since the
284
290
  * value isn't known until render.
285
291
  *
286
292
  * The deferred shapes carry the unresolved path expression to render
287
- * time. The async backend (`compileAsync`) emits cb-shaped or
293
+ * time. In async codegen mode (`backend.compile` with
294
+ * `options.codegenMode === 'async'`) the backend emits cb-shaped or
288
295
  * AsyncFunction-shaped JS that hits a runtime `_swig.getTemplate(...)`
289
296
  * call to resolve and apply the parent / included / imported template.
290
297
  *
@@ -511,7 +518,7 @@
511
518
  */
512
519
 
513
520
  /* ------------------------------------------------------------------ *
514
- * Runtime node factories — Phase 2 scaffold (Session 7, 2026-04-14).
521
+ * Runtime node factories.
515
522
  *
516
523
  * Each factory returns a plain JSON-serialisable object matching one
517
524
  * of the typedefs above. `loc` is always optional; when omitted it is
@@ -519,9 +526,8 @@
519
526
  * `'loc' in node`). All other parameters are required unless documented
520
527
  * otherwise on the corresponding typedef.
521
528
  *
522
- * No consumers yet this commit introduces the schema surface only.
523
- * Subsequent sessions will migrate the native frontend's token-tree
524
- * production over to these shapes.
529
+ * Consumed by every frontend's tag handlers and `tokenparser.js` (which
530
+ * build the IR) and by `backend.js` (which walks it).
525
531
  * ------------------------------------------------------------------ */
526
532
 
527
533
  /*!
@@ -310,12 +310,39 @@ TokenParser.prototype = {
310
310
  self.filterApplyIdx.push(self.out.length - 1);
311
311
  break;
312
312
 
313
+ case _t.QMARK:
314
+ // Ternary `?` — emit a flat operator and mark the state stack so
315
+ // the matching `:` is recognised as the alternative-branch
316
+ // separator (not an object-literal colon). The lowerExpr ->
317
+ // parseExpr IR path does the real ternary codegen for built-in
318
+ // tags; emitting `?`/`:` flat here just keeps the legacy tag-arg
319
+ // walk from aborting so `args` is built for every tag.
320
+ self.out.push(' ? ');
321
+ self.state.push(token.type);
322
+ self.filterApplyIdx.pop();
323
+ break;
324
+
313
325
  case _t.COLON:
314
- if (lastState !== _t.CURLYOPEN) {
326
+ if (lastState === _t.CURLYOPEN) {
327
+ self.state.push(token.type);
328
+ self.out.push(':');
329
+ } else if (lastState === _t.QMARK) {
330
+ // Ternary alternative branch. Pop the QMARK marker the QMARK
331
+ // case pushed. A full ternary (`a ? b : c`) has a non-empty
332
+ // then-branch, so the colon emits as a flat operator. The Elvis
333
+ // shorthand (`a ?: b`) has an empty then-branch — the `?` we
334
+ // emitted is still the last fragment — so rewrite it to `||`,
335
+ // the single-evaluation equivalent of `a ? a : b`. A bare
336
+ // `a ? : b` would be invalid JS.
337
+ self.state.pop();
338
+ if (self.out[self.out.length - 1] === ' ? ') {
339
+ self.out[self.out.length - 1] = ' || ';
340
+ } else {
341
+ self.out.push(' : ');
342
+ }
343
+ } else {
315
344
  utils.throwError('Unexpected colon', self.line, self.filename);
316
345
  }
317
- self.state.push(token.type);
318
- self.out.push(':');
319
346
  self.filterApplyIdx.pop();
320
347
  break;
321
348
 
@@ -647,6 +674,37 @@ TokenParser.prototype = {
647
674
  var right = parseExpression(info.prec + 1);
648
675
  left = ir.binaryOp(info.op, left, right);
649
676
  }
677
+ // Ternary + Elvis — binds looser than every binary op, so it is only
678
+ // handled at the top-level minPrec === 0 entry. Recursive calls for a
679
+ // binary op's RHS run at prec + 1 >= 1 and skip this branch, which is
680
+ // what lets `a + b ? c : d` parse as `(a + b) ? c : d`. Recursive
681
+ // parseExpression(0) calls (arg-list elements, object-literal values,
682
+ // grouped sub-expressions) still get ternary via their own entry.
683
+ //
684
+ // Elvis shorthand `a ?: b` lowers to Conditional(a, a, b). The `a`
685
+ // subexpression is evaluated twice by downstream emitters — a
686
+ // documented consequence of the transliteration.
687
+ if (minPrec === 0) {
688
+ var qtok = peek();
689
+ if (qtok && qtok.type === _t.QMARK) {
690
+ consume();
691
+ var afterQ = peek();
692
+ var elseBranch;
693
+ if (afterQ && afterQ.type === _t.COLON) {
694
+ consume();
695
+ elseBranch = parseExpression(0);
696
+ left = ir.conditional(left, left, elseBranch);
697
+ } else {
698
+ var thenBranch = parseExpression(0);
699
+ var colon = consume();
700
+ if (!colon || colon.type !== _t.COLON) {
701
+ bail('Expected colon in ternary expression');
702
+ }
703
+ elseBranch = parseExpression(0);
704
+ left = ir.conditional(left, thenBranch, elseBranch);
705
+ }
706
+ }
707
+ }
650
708
  return left;
651
709
  }
652
710
 
@@ -706,6 +764,7 @@ TokenParser.prototype = {
706
764
  var depth = 0,
707
765
  hasTopOp = false,
708
766
  hasTopFilter = false,
767
+ hasTopTernary = false,
709
768
  firstTopFilterIdx = -1;
710
769
  for (i = 0; i < tokens.length; i += 1) {
711
770
  t = tokens[i];
@@ -718,6 +777,9 @@ TokenParser.prototype = {
718
777
  hasTopFilter = true;
719
778
  if (firstTopFilterIdx < 0) { firstTopFilterIdx = i; }
720
779
  }
780
+ if (t.type === _t.QMARK) {
781
+ hasTopTernary = true;
782
+ }
721
783
  }
722
784
  if (t.type === _t.PARENOPEN || t.type === _t.FUNCTION ||
723
785
  t.type === _t.BRACKETOPEN || t.type === _t.CURLYOPEN ||
@@ -779,7 +841,13 @@ TokenParser.prototype = {
779
841
  // trailing FILTER / FILTEREMPTY and wraps the atom in an
780
842
  // IRFilterCallExpr at the right tree depth. Autoescape remains
781
843
  // a top-level wrap (single `e` filterCall appended).
782
- if (hasTopOp && hasTopFilter) {
844
+ //
845
+ // A top-level ternary (`{{ a ? b : c }}`) takes the same route:
846
+ // the prefix/filter-drain path below would slice the stream at
847
+ // the first top-level filter (e.g. a filter inside a branch,
848
+ // `{{ x ? a|upper : b }}`) and mis-parse it. Full-stream
849
+ // parseExpr consumes the whole ternary, branch filters included.
850
+ if (hasTopTernary || (hasTopOp && hasTopFilter)) {
783
851
  var exprPO = self.parseExpr(tokens);
784
852
  var fcallsPO = escape ? [ir.filterCall('e')] : [];
785
853
  return ir.output(exprPO, fcallsPO.length > 0 ? fcallsPO : undefined);
@@ -874,6 +942,12 @@ TokenParser.prototype = {
874
942
 
875
943
  return ir.output(expr, filterCalls.length > 0 ? filterCalls : undefined);
876
944
  } catch (e) {
945
+ // A top-level ternary cannot be expressed by the legacy parseToken
946
+ // path (no QMARK handling), so legacyFallback would only throw a
947
+ // worse error ("Unexpected colon") or emit garbage. Re-throw
948
+ // parseExpr's own error — including the CVE-2023-25345 guard
949
+ // message — instead of masking it.
950
+ if (hasTopTernary) { throw e; }
877
951
  return legacyFallback();
878
952
  }
879
953
  },
package/lib/tokentypes.js CHANGED
@@ -73,6 +73,8 @@ module.exports = {
73
73
  /** End of a method
74
74
  * Currently unused
75
75
  METHODEND: 26, */
76
+ /** Ternary question mark (?) */
77
+ QMARK: 27,
76
78
  /** Unknown type */
77
79
  UNKNOWN: 100
78
80
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rhinostone/swig-core",
3
- "version": "2.2.0",
3
+ "version": "2.4.0",
4
4
  "description": "Shared IR, backend, and runtime for the @rhinostone/swig family of template engines.",
5
5
  "keywords": [
6
6
  "template",