solve-engine 1.0.0-beta.2 → 1.0.0-beta.3

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.
Files changed (97) hide show
  1. package/dist/{Lexer-BJdhlnej.d.cts → Lexer-Dkt7KGaU.d.ts} +18 -2
  2. package/dist/{Lexer-D9l4Yrk2.d.ts → Lexer-y8uY6n6t.d.cts} +18 -2
  3. package/dist/{PackageRegistry-D-Tv_7ca.d.cts → PackageRegistry-CblRmrHc.d.cts} +60 -4
  4. package/dist/{PackageRegistry-DaH4aIKP.d.ts → PackageRegistry-wUQyVEyw.d.ts} +60 -4
  5. package/dist/{Parselet-CK8cQu2a.d.cts → Parselet-C974vhZG.d.cts} +1 -1
  6. package/dist/{Parselet-ConOIdRO.d.ts → Parselet-CsmtM3It.d.ts} +1 -1
  7. package/dist/{Token-0jpvUdPY.d.cts → Token-DnU2hwFY.d.cts} +14 -0
  8. package/dist/{Token-0jpvUdPY.d.ts → Token-DnU2hwFY.d.ts} +14 -0
  9. package/dist/TokenNormalizer-B8zmhO5X.d.ts +377 -0
  10. package/dist/TokenNormalizer-CqBCrYkR.d.cts +377 -0
  11. package/dist/{chunk-M5LX5AOO.js → chunk-23MFG6GG.js} +21 -8
  12. package/dist/chunk-23MFG6GG.js.map +1 -0
  13. package/dist/{chunk-SDGRK7EP.js → chunk-2A3VOGVG.js} +4 -4
  14. package/dist/{chunk-SDGRK7EP.js.map → chunk-2A3VOGVG.js.map} +1 -1
  15. package/dist/{chunk-NMD5VRN4.cjs → chunk-2R7SVDDB.cjs} +68 -67
  16. package/dist/chunk-2R7SVDDB.cjs.map +1 -0
  17. package/dist/{chunk-RIN643A3.js → chunk-626UW5RU.js} +2 -2
  18. package/dist/chunk-626UW5RU.js.map +1 -0
  19. package/dist/{chunk-CLL7RUQV.cjs → chunk-7J2Q77OH.cjs} +34 -13
  20. package/dist/chunk-7J2Q77OH.cjs.map +1 -0
  21. package/dist/{chunk-HWSZJQCI.js → chunk-AF5AE52C.js} +33 -12
  22. package/dist/chunk-AF5AE52C.js.map +1 -0
  23. package/dist/{chunk-RZCWSXTA.cjs → chunk-BOAVE4FC.cjs} +129 -129
  24. package/dist/{chunk-RZCWSXTA.cjs.map → chunk-BOAVE4FC.cjs.map} +1 -1
  25. package/dist/{chunk-EBSPLUW4.cjs → chunk-CGIDYRAX.cjs} +19 -9
  26. package/dist/chunk-CGIDYRAX.cjs.map +1 -0
  27. package/dist/{chunk-C4XZV6E7.cjs → chunk-EEJSQEHC.cjs} +20 -20
  28. package/dist/{chunk-C4XZV6E7.cjs.map → chunk-EEJSQEHC.cjs.map} +1 -1
  29. package/dist/{chunk-LR7YASZF.cjs → chunk-F7G7ZFIZ.cjs} +3 -3
  30. package/dist/chunk-F7G7ZFIZ.cjs.map +1 -0
  31. package/dist/{chunk-GW32KPCU.cjs → chunk-FI4WDYSL.cjs} +2 -2
  32. package/dist/chunk-FI4WDYSL.cjs.map +1 -0
  33. package/dist/{chunk-EIGTWK5N.js → chunk-KFRBAXKW.js} +3 -3
  34. package/dist/chunk-KFRBAXKW.js.map +1 -0
  35. package/dist/{chunk-64W6GLLZ.js → chunk-OJ6LW7VQ.js} +15 -5
  36. package/dist/chunk-OJ6LW7VQ.js.map +1 -0
  37. package/dist/{chunk-3PPFLFH4.js → chunk-OWXIOHQB.js} +6 -6
  38. package/dist/{chunk-3PPFLFH4.js.map → chunk-OWXIOHQB.js.map} +1 -1
  39. package/dist/{chunk-JBSYC7BB.cjs → chunk-PG2K5BL6.cjs} +41 -28
  40. package/dist/chunk-PG2K5BL6.cjs.map +1 -0
  41. package/dist/{chunk-XVWCOTR6.js → chunk-VM5ZKDLH.js} +5 -4
  42. package/dist/chunk-VM5ZKDLH.js.map +1 -0
  43. package/dist/{chunk-NMCRQP3Z.cjs → chunk-ZH2PCJ4V.cjs} +68 -68
  44. package/dist/{chunk-NMCRQP3Z.cjs.map → chunk-ZH2PCJ4V.cjs.map} +1 -1
  45. package/dist/{chunk-5YEMOYSE.js → chunk-ZS6CRJZP.js} +3 -3
  46. package/dist/{chunk-5YEMOYSE.js.map → chunk-ZS6CRJZP.js.map} +1 -1
  47. package/dist/constants.cjs +4 -4
  48. package/dist/constants.js +1 -1
  49. package/dist/engine.cjs +19 -19
  50. package/dist/engine.d.cts +6 -6
  51. package/dist/engine.d.ts +6 -6
  52. package/dist/engine.js +9 -9
  53. package/dist/index.cjs +18 -18
  54. package/dist/index.d.cts +6 -6
  55. package/dist/index.d.ts +6 -6
  56. package/dist/index.js +10 -10
  57. package/dist/language.cjs +63 -1
  58. package/dist/language.cjs.map +1 -1
  59. package/dist/language.d.cts +6 -6
  60. package/dist/language.d.ts +6 -6
  61. package/dist/language.js +63 -1
  62. package/dist/language.js.map +1 -1
  63. package/dist/lexer.cjs +14 -14
  64. package/dist/lexer.d.cts +3 -3
  65. package/dist/lexer.d.ts +3 -3
  66. package/dist/lexer.js +3 -3
  67. package/dist/normalizer.cjs +9 -9
  68. package/dist/normalizer.d.cts +4 -210
  69. package/dist/normalizer.d.ts +4 -210
  70. package/dist/normalizer.js +3 -3
  71. package/dist/packages.cjs +28 -28
  72. package/dist/packages.d.cts +5 -5
  73. package/dist/packages.d.ts +5 -5
  74. package/dist/packages.js +5 -5
  75. package/dist/parser.cjs +11 -11
  76. package/dist/parser.d.cts +2 -2
  77. package/dist/parser.d.ts +2 -2
  78. package/dist/parser.js +3 -3
  79. package/dist/resolvers.d.cts +1 -1
  80. package/dist/resolvers.d.ts +1 -1
  81. package/dist/uom.d.cts +1 -1
  82. package/dist/uom.d.ts +1 -1
  83. package/package.json +2 -1
  84. package/dist/NormalizerRule-BrVoVjmP.d.cts +0 -163
  85. package/dist/NormalizerRule-CEjf1FyD.d.ts +0 -163
  86. package/dist/chunk-64W6GLLZ.js.map +0 -1
  87. package/dist/chunk-CLL7RUQV.cjs.map +0 -1
  88. package/dist/chunk-EBSPLUW4.cjs.map +0 -1
  89. package/dist/chunk-EIGTWK5N.js.map +0 -1
  90. package/dist/chunk-GW32KPCU.cjs.map +0 -1
  91. package/dist/chunk-HWSZJQCI.js.map +0 -1
  92. package/dist/chunk-JBSYC7BB.cjs.map +0 -1
  93. package/dist/chunk-LR7YASZF.cjs.map +0 -1
  94. package/dist/chunk-M5LX5AOO.js.map +0 -1
  95. package/dist/chunk-NMD5VRN4.cjs.map +0 -1
  96. package/dist/chunk-RIN643A3.js.map +0 -1
  97. package/dist/chunk-XVWCOTR6.js.map +0 -1
package/dist/language.js CHANGED
@@ -57,6 +57,7 @@ var LanguageService = class {
57
57
  this.staticCompletionIndex = null;
58
58
  this.engine = engine ?? null;
59
59
  this.variableNameSource = options?.variableNameSource ?? (() => this.defaultVariableNames());
60
+ this.normalizeForHighlighting = options?.normalizeForHighlighting ?? false;
60
61
  }
61
62
  defaultVariableNames() {
62
63
  if (!this.engine) return [];
@@ -67,6 +68,67 @@ var LanguageService = class {
67
68
  }
68
69
  return names;
69
70
  }
71
+ /**
72
+ * Lex a line, normalize it, and place every resulting token back in the
73
+ * source text.
74
+ *
75
+ * ## Why placing them back is the hard part
76
+ *
77
+ * Normalization produces three kinds of token and only one of them can be
78
+ * highlighted the obvious way.
79
+ *
80
+ * A token the normalizer left alone still describes its own text, so its
81
+ * span is `offset` to `offset + value.length`, exactly as before.
82
+ *
83
+ * A FUSED token does not. `10 frames` becomes a FRAME_COUNT whose value is
84
+ * `10`, and a timecode becomes a token whose value is a comma-separated
85
+ * tuple appearing nowhere on the line. Those carry `sourceEnd`, set by
86
+ * `createFusedToken`, which is the only place that knows where the fusion
87
+ * ended.
88
+ *
89
+ * An INSERTED token has no text at all. Implicit multiplication puts a STAR
90
+ * at the following token's offset, so `5(3)` gains a `*` sitting exactly
91
+ * where the `(` is. Painting it would colour a character the reader never
92
+ * typed as an operator, and would overlap the token that really is there.
93
+ * These are dropped, detected by the one test that needs no cooperation
94
+ * from any rule: a token with no recorded fusion span must match the text
95
+ * at its own offset, and an inserted one does not.
96
+ *
97
+ * @param lineText - The raw line.
98
+ * @returns Spans in the same shape `Lexer.getHighlightTokens` returns.
99
+ */
100
+ classifyNormalized(lineText) {
101
+ const lexer = this.engine.getLexer();
102
+ const raw = lexer.getHighlightTokenObjects(lineText);
103
+ if (raw.length === 0) return [];
104
+ const source = lineText.startsWith("> ") && lexer.classifyLine(lineText).skip ? lineText.slice(2) : lineText;
105
+ let normalized;
106
+ try {
107
+ normalized = this.engine.getNormalizer().normalize(raw);
108
+ } catch {
109
+ normalized = raw;
110
+ }
111
+ const out = [];
112
+ for (const token of normalized) {
113
+ let end;
114
+ if (token.sourceEnd !== void 0) {
115
+ end = token.sourceEnd;
116
+ } else if (source.startsWith(token.value, token.offset)) {
117
+ end = token.offset + token.value.length;
118
+ } else {
119
+ continue;
120
+ }
121
+ out.push({
122
+ type: token.type,
123
+ value: token.value,
124
+ offset: token.offset,
125
+ col: token.col,
126
+ length: end - token.offset,
127
+ category: getTokenCategory(token.type)
128
+ });
129
+ }
130
+ return out;
131
+ }
70
132
  /**
71
133
  * Classify every recognized token on one line.
72
134
  *
@@ -87,7 +149,7 @@ var LanguageService = class {
87
149
  return [];
88
150
  }
89
151
  const lexer = this.engine.getLexer();
90
- const lexed = lexer.getHighlightTokens(lineText);
152
+ const lexed = this.normalizeForHighlighting ? this.classifyNormalized(lineText) : lexer.getHighlightTokens(lineText);
91
153
  if (lexed.length === 0) {
92
154
  this.putCache(lineNumber, lineText, []);
93
155
  return [];
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/language/LanguageService.ts","../src/language/tokenClassName.ts","../src/language/adapters/codemirror.ts"],"names":[],"mappings":";;;;;;;;AAiCA,IAAM,eAAA,GAAkB,EAAA;AAGxB,IAAM,aAAA,GAAwD;AAAA,EAC7D,QAAA,EAAU,CAAA;AAAA,EACV,QAAA,EAAU,CAAA;AAAA,EACV,OAAA,EAAS,CAAA;AAAA,EACT,QAAA,EAAU,CAAA;AAAA,EACV,UAAA,EAAY,CAAA;AAAA,EACZ,OAAA,EAAS,CAAA;AAAA,EACT,QAAA,EAAU,CAAA;AAAA,EACV,MAAA,EAAQ,CAAA;AAAA,EACR,IAAA,EAAM;AACP,CAAA;AAGA,IAAM,gBAAA,GAAmB,GAAA;AAyFlB,IAAM,kBAAN,MAAsB;AAAA,EAyC5B,WAAA,CAAY,QAAkC,OAAA,EAAkC;AAtBhF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAAA,IAAA,CAAQ,KAAA,uBAAY,GAAA,EAAwB;AAS5C;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAAA,IAAA,CAAQ,0BAAA,GAAsD,IAAA;AAW9D;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAAA,IAAA,CAAQ,qBAAA,GAA0E,IAAA;AAGjF,IAAA,IAAA,CAAK,SAAS,MAAA,IAAU,IAAA;AACxB,IAAA,IAAA,CAAK,kBAAA,GAAqB,OAAA,EAAS,kBAAA,KAAuB,MAAM,KAAK,oBAAA,EAAqB,CAAA;AAAA,EAC3F;AAAA,EAEQ,oBAAA,GAAyC;AAChD,IAAA,IAAI,CAAC,IAAA,CAAK,MAAA,EAAQ,OAAO,EAAC;AAC1B,IAAA,MAAM,QAAA,GAAW,IAAA,CAAK,MAAA,CAAO,MAAA,GAAS,WAAA,EAAY;AAClD,IAAA,MAAM,QAAQ,IAAI,GAAA,CAAY,OAAO,IAAA,CAAK,QAAA,CAAS,SAAS,CAAC,CAAA;AAC7D,IAAA,KAAA,MAAW,OAAA,IAAW,MAAA,CAAO,MAAA,CAAO,QAAA,CAAS,MAAM,CAAA,EAAG;AACrD,MAAA,KAAA,MAAW,IAAA,IAAQ,OAAA,EAAS,KAAA,CAAM,GAAA,CAAI,IAAI,CAAA;AAAA,IAC3C;AACA,IAAA,OAAO,KAAA;AAAA,EACR;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUA,iBAAA,CAAkB,UAAkB,UAAA,EAAqC;AACxE,IAAA,MAAM,MAAA,GAAS,IAAA,CAAK,KAAA,CAAM,GAAA,CAAI,UAAU,CAAA;AACxC,IAAA,IAAI,MAAA,IAAU,MAAA,CAAO,IAAA,KAAS,QAAA,EAAU;AACvC,MAAA,IAAI,MAAA,CAAO,sBAAsB,MAAA,EAAW;AAC3C,QAAA,OAAO,KAAK,eAAA,CAAgB,MAAA,CAAO,iBAAiB,CAAA,GAAI,MAAA,CAAO,SAAS,EAAC;AAAA,MAC1E;AACA,MAAA,OAAO,MAAA,CAAO,MAAA;AAAA,IACf;AAEA,IAAA,IAAI,CAAC,KAAK,MAAA,EAAQ;AAGjB,MAAA,OAAO,EAAC;AAAA,IACT;AAEA,IAAA,MAAM,KAAA,GAAQ,IAAA,CAAK,MAAA,CAAO,QAAA,EAAS;AACnC,IAAA,MAAM,KAAA,GAAQ,KAAA,CAAM,kBAAA,CAAmB,QAAQ,CAAA;AAC/C,IAAA,IAAI,KAAA,CAAM,WAAW,CAAA,EAAG;AACvB,MAAA,IAAA,CAAK,QAAA,CAAS,UAAA,EAAY,QAAA,EAAU,EAAE,CAAA;AACtC,MAAA,OAAO,EAAC;AAAA,IACT;AAEA,IAAA,MAAM,cAAA,GAAiB,KAAA,CAAM,YAAA,CAAa,QAAQ,CAAA;AAClD,IAAA,MAAM,SAA0B,EAAC;AAEjC,IAAA,IAAI,eAAe,cAAA,EAAgB;AAQlC,MAAA,MAAM,UAAA,GAAa,MACjB,gBAAA,CAAiB,QAAQ,EACzB,MAAA,CAAO,CAAA,IAAA,KAAQ,IAAA,CAAK,kBAAA,CAAmB,IAAA,CAAK,UAAU,CAAC,CAAA,CACvD,GAAA,CAAI,WAAS,EAAE,IAAA,EAAM,KAAK,KAAA,EAAO,EAAA,EAAI,IAAA,CAAK,GAAA,EAAI,CAAE,CAAA;AAClD,MAAA,KAAA,MAAW,SAAS,KAAA,EAAO;AAC1B,QAAA,IAAI,CAAC,MAAM,QAAA,EAAU;AACrB,QAAA,MAAM,OAAO,KAAA,CAAM,MAAA;AACnB,QAAA,MAAM,EAAA,GAAK,KAAA,CAAM,MAAA,GAAS,KAAA,CAAM,MAAA;AAChC,QAAA,IAAI,CAAC,UAAA,CAAW,IAAA,CAAK,CAAA,CAAA,KAAK,IAAA,IAAQ,EAAE,IAAA,IAAQ,EAAA,IAAM,CAAA,CAAE,EAAE,CAAA,EAAG;AACzD,QAAA,MAAA,CAAO,KAAK,EAAE,IAAA,EAAM,IAAI,QAAA,EAAU,KAAA,CAAM,UAAU,CAAA;AAAA,MACnD;AAAA,IACD,CAAA,MAAO;AAKN,MAAA,MAAM,IAAA,GAAO,QAAA,CAAS,UAAA,CAAW,IAAI,CAAA,IAAK,eAAe,IAAA,GACtD,QAAA,CAAS,KAAA,CAAM,CAAC,CAAA,GAChB,QAAA;AACH,MAAA,IAAI,IAAA,CAAK,kBAAA,CAAmB,IAAI,CAAA,EAAG;AAClC,QAAA,KAAA,MAAW,SAAS,KAAA,EAAO;AAC1B,UAAA,IAAI,CAAC,MAAM,QAAA,EAAU;AACrB,UAAA,MAAA,CAAO,IAAA,CAAK,EAAE,IAAA,EAAM,KAAA,CAAM,MAAA,EAAQ,EAAA,EAAI,KAAA,CAAM,MAAA,GAAS,KAAA,CAAM,MAAA,EAAQ,QAAA,EAAU,KAAA,CAAM,UAAU,CAAA;AAAA,QAC9F;AAAA,MACD;AAAA,IACD;AAcA,IAAA,IAAI,OAAO,MAAA,KAAW,CAAA,IAAK,OAAO,CAAC,CAAA,CAAE,aAAa,UAAA,EAAY;AAC7D,MAAA,MAAM,IAAA,GAAO,QAAA,CAAS,KAAA,CAAM,MAAA,CAAO,CAAC,EAAE,IAAA,EAAM,MAAA,CAAO,CAAC,CAAA,CAAE,EAAE,CAAA;AACxD,MAAA,IAAA,CAAK,QAAA,CAAS,UAAA,EAAY,QAAA,EAAU,MAAA,EAAQ,IAAI,CAAA;AAChD,MAAA,OAAO,IAAA,CAAK,eAAA,CAAgB,IAAI,CAAA,GAAI,SAAS,EAAC;AAAA,IAC/C;AAEA,IAAA,IAAA,CAAK,QAAA,CAAS,UAAA,EAAY,QAAA,EAAU,MAAM,CAAA;AAC1C,IAAA,OAAO,MAAA;AAAA,EACR;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAkBA,cAAA,CAAe,UAAkB,YAAA,EAAwC;AACxE,IAAA,MAAM,cAAc,gBAAA,CAAiB,IAAA,CAAK,SAAS,KAAA,CAAM,CAAA,EAAG,YAAY,CAAC,CAAA;AACzE,IAAA,IAAI,CAAC,WAAA,EAAa,OAAO,EAAC;AAC1B,IAAA,MAAM,MAAA,GAAS,WAAA,CAAY,CAAC,CAAA,CAAE,WAAA,EAAY;AAE1C,IAAA,IAAI,CAAC,IAAA,CAAK,MAAA,EAAQ,OAAO,EAAC;AAE1B,IAAA,MAAM,UAA4B,EAAC;AAInC,IAAA,KAAA,MAAW,IAAA,IAAQ,IAAA,CAAK,kBAAA,EAAmB,EAAG;AAC7C,MAAA,IAAI,IAAA,CAAK,WAAA,EAAY,CAAE,UAAA,CAAW,MAAM,CAAA,EAAG;AAC1C,QAAA,OAAA,CAAQ,KAAK,EAAE,KAAA,EAAO,IAAA,EAAM,QAAA,EAAU,YAAY,CAAA;AAAA,MACnD;AAAA,IACD;AAIA,IAAA,MAAM,SAAS,IAAA,CAAK,wBAAA,GAA2B,GAAA,CAAI,MAAA,CAAO,CAAC,CAAC,CAAA;AAC5D,IAAA,IAAI,MAAA,EAAQ;AACX,MAAA,KAAA,MAAW,aAAa,MAAA,EAAQ;AAC/B,QAAA,IAAI,SAAA,CAAU,WAAW,UAAA,CAAW,MAAM,GAAG,OAAA,CAAQ,IAAA,CAAK,UAAU,IAAI,CAAA;AAAA,MACzE;AAAA,IACD;AAEA,IAAA,OAAA,CAAQ,IAAA,CAAK,CAAC,CAAA,EAAG,CAAA,KAAM;AACtB,MAAA,MAAM,QAAA,GAAA,CAAY,cAAc,CAAA,CAAE,QAAQ,KAAK,CAAA,KAAM,aAAA,CAAc,CAAA,CAAE,QAAQ,CAAA,IAAK,CAAA,CAAA;AAClF,MAAA,IAAI,QAAA,KAAa,GAAG,OAAO,QAAA;AAC3B,MAAA,OAAO,CAAA,CAAE,KAAA,CAAM,aAAA,CAAc,CAAA,CAAE,KAAK,CAAA;AAAA,IACrC,CAAC,CAAA;AAED,IAAA,OAAO,OAAA,CAAQ,KAAA,CAAM,CAAA,EAAG,eAAe,CAAA;AAAA,EACxC;AAAA;AAAA,EAGQ,6BAAA,GAAkD;AACzD,IAAA,IAAI,IAAA,CAAK,0BAAA,EAA4B,OAAO,IAAA,CAAK,0BAAA;AAEjD,IAAA,MAAM,QAA0B,EAAC;AACjC,IAAA,KAAA,MAAW,CAAC,IAAA,EAAM,SAAS,CAAA,IAAK,MAAA,CAAO,OAAA,CAAQ,IAAA,CAAK,MAAA,CAAQ,QAAA,EAAS,CAAE,WAAA,EAAa,CAAA,EAAG;AACtF,MAAA,MAAM,QAAA,GAAW,iBAAiB,SAAS,CAAA;AAC3C,MAAA,IAAI,CAAC,QAAA,EAAU;AACf,MAAA,KAAA,CAAM,IAAA,CAAK,EAAE,KAAA,EAAO,IAAA,EAAM,UAAU,CAAA;AAAA,IACrC;AACA,IAAA,KAAA,MAAW,QAAQ,UAAA,EAAY;AAC9B,MAAA,KAAA,CAAM,IAAA,CAAK,EAAE,KAAA,EAAO,IAAA,EAAM,QAAA,EAAU,QAAQ,MAAA,EAAQ,UAAA,CAAW,IAAI,CAAA,EAAG,CAAA;AAAA,IACvE;AACA,IAAA,KAAA,CAAM,IAAA,CAAK,GAAG,IAAA,CAAK,MAAA,CAAQ,2BAA2B,CAAA;AAEtD,IAAA,IAAA,CAAK,0BAAA,GAA6B,KAAA;AAClC,IAAA,OAAO,KAAA;AAAA,EACR;AAAA;AAAA;AAAA;AAAA;AAAA,EAMQ,wBAAA,GAAsE;AAC7E,IAAA,IAAI,IAAA,CAAK,qBAAA,EAAuB,OAAO,IAAA,CAAK,qBAAA;AAE5C,IAAA,MAAM,KAAA,uBAAY,GAAA,EAA0C;AAC5D,IAAA,KAAA,MAAW,IAAA,IAAQ,IAAA,CAAK,6BAAA,EAA8B,EAAG;AACxD,MAAA,MAAM,UAAA,GAAa,IAAA,CAAK,KAAA,CAAM,WAAA,EAAY;AAC1C,MAAA,MAAM,cAAA,GAAiB,WAAW,CAAC,CAAA;AAGnC,MAAA,IAAI,mBAAmB,MAAA,EAAW;AAClC,MAAA,IAAI,MAAA,GAAS,KAAA,CAAM,GAAA,CAAI,cAAc,CAAA;AACrC,MAAA,IAAI,WAAW,MAAA,EAAW;AACzB,QAAA,MAAA,GAAS,EAAC;AACV,QAAA,KAAA,CAAM,GAAA,CAAI,gBAAgB,MAAM,CAAA;AAAA,MACjC;AACA,MAAA,MAAA,CAAO,IAAA,CAAK,EAAE,IAAA,EAAM,UAAA,EAAY,CAAA;AAAA,IACjC;AAEA,IAAA,IAAA,CAAK,qBAAA,GAAwB,KAAA;AAC7B,IAAA,OAAO,KAAA;AAAA,EACR;AAAA,EAEQ,gBAAgB,IAAA,EAAuB;AAC9C,IAAA,KAAA,MAAW,KAAA,IAAS,IAAA,CAAK,kBAAA,EAAmB,EAAG;AAC9C,MAAA,IAAI,KAAA,KAAU,MAAM,OAAO,IAAA;AAAA,IAC5B;AACA,IAAA,OAAO,KAAA;AAAA,EACR;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAmBQ,mBAAmB,IAAA,EAAuB;AACjD,IAAA,OAAO,IAAA,CAAK,MAAA,CAAQ,oBAAA,CAAqB,IAAI,CAAA;AAAA,EAC9C;AAAA,EAEQ,QAAA,CAAS,UAAA,EAAoB,IAAA,EAAc,MAAA,EAAyB,iBAAA,EAAkC;AAC7G,IAAA,IAAI,CAAC,KAAK,KAAA,CAAM,GAAA,CAAI,UAAU,CAAA,IAAK,IAAA,CAAK,KAAA,CAAM,IAAA,IAAQ,gBAAA,EAAkB;AACvE,MAAA,MAAM,YAAY,IAAA,CAAK,KAAA,CAAM,IAAA,EAAK,CAAE,MAAK,CAAE,KAAA;AAC3C,MAAA,IAAI,SAAA,KAAc,MAAA,EAAW,IAAA,CAAK,KAAA,CAAM,OAAO,SAAS,CAAA;AAAA,IACzD;AACA,IAAA,IAAA,CAAK,MAAM,GAAA,CAAI,UAAA,EAAY,EAAE,IAAA,EAAM,MAAA,EAAQ,mBAAmB,CAAA;AAAA,EAC/D;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,gBAAgB,WAAA,EAAqC;AACpD,IAAA,KAAA,MAAW,cAAc,WAAA,EAAa;AACrC,MAAA,IAAA,CAAK,KAAA,CAAM,OAAO,UAAU,CAAA;AAAA,IAC7B;AAAA,EACD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUA,eAAA,GAAwB;AACvB,IAAA,IAAA,CAAK,MAAM,KAAA,EAAM;AACjB,IAAA,IAAA,CAAK,0BAAA,GAA6B,IAAA;AAClC,IAAA,IAAA,CAAK,qBAAA,GAAwB,IAAA;AAAA,EAC9B;AACD;;;AChbO,IAAM,0BAAA,GAA6B;AAqBnC,SAAS,cAAA,CAAe,QAAA,EAAyB,MAAA,GAAiB,0BAAA,EAAoC;AAC5G,EAAA,OAAO,CAAA,EAAG,MAAM,CAAA,EAAG,QAAQ,CAAA,CAAA;AAC5B;AAmBO,SAAS,qBAAqB,MAAA,EAAqD;AACzF,EAAA,OAAO,CAAC,QAAA,KAA4B,CAAA,EAAG,MAAM,GAAG,QAAQ,CAAA,CAAA;AACzD;;;ACpCA,IAAM,2BAAA,GAAsE;AAAA,EAC3E,OAAA,EAAS,SAAA;AAAA,EACT,QAAA,EAAU,SAAA;AAAA,EACV,UAAA,EAAY,SAAA;AAAA,EACZ,OAAA,EAAS,SAAA;AAAA,EACT,QAAA,EAAU,UAAA;AAAA,EACV,QAAA,EAAU,UAAA;AAAA,EACV,IAAA,EAAM,MAAA;AAAA,EACN,QAAA,EAAU,SAAA;AAAA,EACV,MAAA,EAAQ;AACT,CAAA;AAUO,SAAS,uBAAuB,IAAA,EAAwE;AAC9G,EAAA,OAAO,EAAE,KAAA,EAAO,IAAA,CAAK,KAAA,EAAO,IAAA,EAAM,2BAAA,CAA4B,IAAA,CAAK,QAAQ,CAAA,IAAK,MAAA,EAAQ,MAAA,EAAQ,IAAA,CAAK,MAAA,EAAO;AAC7G","file":"language.js","sourcesContent":["import type { ExpressionEngine } from \"@solve-js/engine/ExpressionEngine\";\nimport type { TokenCategory } from \"@solve-js/language/TokenCategory\";\nimport { getTokenCategory } from \"@solve-js/language/TokenCategoryMap\";\nimport { knownUnits } from \"@solve-js/lexer/units\";\nimport { getMeasure } from \"@solve-js/uom/UomConverter\";\n\n/** A single classified span within a line, the entire output contract of the language service. */\nexport interface SemanticToken {\n\tfrom: number;\n\tto: number;\n\tcategory: TokenCategory;\n}\n\n/** A single completion candidate, the entire output contract of `getCompletions()`. */\nexport interface CompletionItem {\n\tlabel: string;\n\t/** Reuses the highlighting taxonomy, one adapter can serve both features. */\n\tcategory: TokenCategory;\n\t/** e.g. a unit's measure (\"length\"), or the category name for keywords/functions. */\n\tdetail?: string;\n}\n\n/**\n * A completion candidate with its lowercased label precomputed.\n *\n * Internal to the prefix index; callers only ever see the {@link CompletionItem}.\n */\ninterface IndexedCompletionCandidate {\n\titem: CompletionItem;\n\tlowerLabel: string;\n}\n\n/** Completion results are capped, a document-wide candidate pool has no reason to return more than this. */\nconst MAX_COMPLETIONS = 50;\n\n/** Tier ordering for completion results: user-authored variables first, then grammar, then units. */\nconst CATEGORY_TIER: Partial<Record<TokenCategory, number>> = {\n\tvariable: 0,\n\tfunction: 1,\n\tkeyword: 1,\n\toperator: 1,\n\tcomparison: 1,\n\tbitwise: 1,\n\tdatetime: 1,\n\tvector: 1,\n\tunit: 2,\n};\n\n/** Bounded cache size. See the eviction-policy note on `LanguageService.cache`. */\nconst MAX_CACHED_LINES = 2000;\n\ninterface CacheEntry {\n\ttext: string;\n\ttokens: SemanticToken[];\n\t// When set, `tokens` is a single bare-identifier token whose validity\n\t// depends on document-wide DAG state (see the bare-word gate in\n\t// getSemanticTokens), not just this line's own text, so it can't be\n\t// cached as a plain pass/fail result the way every other line can. The\n\t// lex+parse work that produced `tokens` is still cached normally; only\n\t// the DAG membership check is re-run on every lookup (cache hit or\n\t// miss alike), since it's cheap (a Set lookup) and the alternative\n\t// caching the gated result, would go stale the moment some OTHER\n\t// line's edit changes what variables exist, with nothing to trigger a\n\t// re-check of this untouched line.\n\tbareWordCandidate?: string;\n}\n\n/** Options for the editor-facing language service: completions and highlighting. */\nexport interface LanguageServiceOptions {\n\t/**\n\t * Overrides how the service discovers \"variable names known in this\n\t * document\", used to legitimize a lone bare identifier line (see\n\t * `getSemanticTokens`'s single-token gate) and variable-name\n\t * completions (`getCompletions`). Defaults to reading\n\t * `engine.getDag().getSnapshot()`, which works for any consumer\n\t * sharing one `ExpressionEngine` between evaluation and the language\n\t * service (the real Obsidian editor).\n\t *\n\t * Required for consumers whose language service is backed by a\n\t * *different*, non-evaluating engine than the one that actually runs\n\t * the document (the playground's dedicated lexing-only engine, whose\n\t * own DAG is always empty), pass a function reading the real\n\t * evaluation engine's DAG snapshot instead.\n\t */\n\tvariableNameSource?: () => Iterable<string>;\n}\n\n/**\n * Editor-agnostic \"language server\" for solve expressions: turns a line of\n * text into semantic token ranges, using the exact same lexer real\n * evaluation uses (so it only ever classifies what the engine's grammar\n * actually recognizes, never a separate/duplicated tokenizer). No\n * knowledge of CSS, CodeMirror, VS Code, or any other rendering concept\n * lives here. See `language/adapters/` for that.\n *\n * Classification happens at the LEXER stage, before the normalizer runs\n * (normalization, phrase fusion, implicit multiply, and package-specific\n * rules, happens later, only on the real evaluation path). A package's\n * lexer-level custom token types (e.g. a custom keyword) are recognized\n * here exactly as evaluation would see them. A package's *normalizer*-fused\n * synthetic tokens (e.g. OSRS's GAME_ITEM, built by fusing several\n * consecutive IDENT tokens against an item-name trie) are NOT. This\n * service still shows the pre-fusion IDENT tokens individually for those.\n * `IEnginePackage.tokenCategories` entries for normalizer-only token types\n * are still valid, correct registrations (queryable via getTokenCategory)\n *, they just won't currently be reachable through this lexer-only\n * classification path. Folding normalization in would require running it\n * per keystroke on the highlighting path too, which needs its own careful\n * design (span recomputation for fused multi-token ranges, in particular)\n * rather than a quick addition here.\n *\n * Lexing alone is NOT sufficient to decide \"recognized\", though: a run of\n * plain-English words (\"My name is ron\") lexes into a sequence of\n * individually-valid IDENT tokens with no grammar tying them together\n * every word \"recognized\" at the token level, but the line as a whole is\n * not something the engine would ever accept as an expression. Surfacing\n * per-token colors for that case looks like the editor mistook prose for\n * code. So a line's tokens are only surfaced once the line as a whole\n * parses successfully (via `ExpressionEngine.compileExpression`, the same\n * parse pipeline, and the same bytecode cache, real evaluation uses; no\n * separate/duplicated grammar check). A single bare word (\"hello\", a valid\n * variable reference) or a keyword-only line (\"pi\") still parses and still\n * highlights, only genuinely ungrammatical text is suppressed, unless it's\n * a known variable elsewhere in the document (see `variableNameSource`).\n *\n * `getCompletions()` is the other half of this \"language server\": unlike\n * `getSemanticTokens()`, it's explicitly for *incomplete*, mid-typing text\n *, it deliberately does NOT gate on parse validity (a half-typed\n * expression almost never parses), using simple prefix matching instead.\n *\n * Must be constructed with an already-configured `ExpressionEngine` (one\n * with all currently-relevant packages registered) rather than a bare\n * lexer, reusing an existing engine is both the fast path (no throwaway\n * lexer construction) and the *correct* one: a highlighting-only lexer\n * built independently of the evaluation engine would silently fail to\n * recognize plugin-contributed tokens (e.g. a package's custom keywords)\n * unless it happened to have the identical packages registered.\n */\nexport class LanguageService {\n\tprivate engine: ExpressionEngine | null;\n\tprivate variableNameSource: () => Iterable<string>;\n\n\t// Bounded cache keyed by line number ALONE, not `${lineNumber}:${lineText}`\n\t// as an earlier version of this class did. A line's previous text state is\n\t// never useful once it changes, so keying on text too was pure waste:\n\t// every keystroke on a line minted a brand-new, never-reclaimed cache\n\t// entry (an effective per-keystroke memory leak over a long editing\n\t// session). Keying on line number alone makes \"same line, new text\" a\n\t// cheap overwrite instead.\n\t//\n\t// Eviction is oldest-inserted (Map iteration order) when at capacity\n\t// mirroring the same bounded-cache pattern ExpressionEngine's own\n\t// bytecodeCache already uses elsewhere in this codebase. Deliberately\n\t// NOT an LFU (least-frequently-used) policy: LFU would keep resisting\n\t// eviction of old, once-popular lines while punishing a line that just\n\t// scrolled into view (frequency 1), the opposite of what a \"currently\n\t// visible\" cache should prioritize.\n\tprivate cache = new Map<number, CacheEntry>();\n\n\t// Keyword/unit/package-contributed completion candidates don't depend\n\t// on any particular line, built lazily on first getCompletions() call\n\t// and reused after that, since a package registration is the only thing\n\t// that could ever change this list mid-session (see invalidateCache()).\n\t// Variable-name candidates are NOT part of this, they're read fresh on\n\t// every call from variableNameSource(), since those genuinely change on\n\t// every edit.\n\tprivate staticCompletionCandidates: CompletionItem[] | null = null;\n\n\t// The same candidates, bucketed by their lowercased first character with the\n\t// lowercased label precomputed. getCompletions() runs on every keystroke and\n\t// only ever wants candidates sharing the prefix's first character, so\n\t// scanning the whole list and lowercasing each label per call was doing two\n\t// avoidable things: touching entries that could not possibly match, and\n\t// allocating a string per candidate per keystroke. That was affordable when\n\t// the vocabulary was a few hundred entries. Deriving the unit list from the\n\t// conversion tables took it past a thousand, and the warm completion\n\t// benchmarks regressed roughly 2.9x until this was added.\n\tprivate staticCompletionIndex: Map<string, IndexedCompletionCandidate[]> | null = null;\n\n\tconstructor(engine?: ExpressionEngine | null, options?: LanguageServiceOptions) {\n\t\tthis.engine = engine ?? null;\n\t\tthis.variableNameSource = options?.variableNameSource ?? (() => this.defaultVariableNames());\n\t}\n\n\tprivate defaultVariableNames(): Iterable<string> {\n\t\tif (!this.engine) return [];\n\t\tconst snapshot = this.engine.getDag().getSnapshot();\n\t\tconst names = new Set<string>(Object.keys(snapshot.consumers));\n\t\tfor (const written of Object.values(snapshot.writes)) {\n\t\t\tfor (const name of written) names.add(name);\n\t\t}\n\t\treturn names;\n\t}\n\n\t/**\n\t * Classify every recognized token on one line.\n\t *\n\t * @param lineText - The raw line text (may be a markdown-structural line\n\t * the engine's classifier skips, that's handled by the underlying\n\t * lexer, which returns no tokens for those).\n\t * @param lineNumber - 1-based line number, used purely as a cache key.\n\t */\n\tgetSemanticTokens(lineText: string, lineNumber: number): SemanticToken[] {\n\t\tconst cached = this.cache.get(lineNumber);\n\t\tif (cached && cached.text === lineText) {\n\t\t\tif (cached.bareWordCandidate !== undefined) {\n\t\t\t\treturn this.isKnownVariable(cached.bareWordCandidate) ? cached.tokens : [];\n\t\t\t}\n\t\t\treturn cached.tokens;\n\t\t}\n\n\t\tif (!this.engine) {\n\t\t\t// No engine available (e.g. a consumer that hasn't wired one up yet)\n\t\t\t//, no highlighting, not an error.\n\t\t\treturn [];\n\t\t}\n\n\t\tconst lexer = this.engine.getLexer();\n\t\tconst lexed = lexer.getHighlightTokens(lineText);\n\t\tif (lexed.length === 0) {\n\t\t\tthis.putCache(lineNumber, lineText, []);\n\t\t\treturn [];\n\t\t}\n\n\t\tconst classification = lexer.classifyLine(lineText);\n\t\tconst tokens: SemanticToken[] = [];\n\n\t\tif (classification.hasInlineSolve) {\n\t\t\t// A line can mix markdown prose with one or more embedded\n\t\t\t// `s`...`` expressions. Only the text actually inside a\n\t\t\t// well-formed marker is a recognized expression, surrounding\n\t\t\t// prose lexes into individually-valid tokens too (see the class\n\t\t\t// doc comment) but is never something the engine would parse,\n\t\t\t// so it's excluded token-by-token via span membership rather\n\t\t\t// than gating the whole line pass/fail.\n\t\t\tconst validSpans = lexer\n\t\t\t\t.findInlineSolves(lineText)\n\t\t\t\t.filter(span => this.parsesAsExpression(span.expression))\n\t\t\t\t.map(span => ({ from: span.start, to: span.end }));\n\t\t\tfor (const token of lexed) {\n\t\t\t\tif (!token.category) continue;\n\t\t\t\tconst from = token.offset;\n\t\t\t\tconst to = token.offset + token.length;\n\t\t\t\tif (!validSpans.some(s => from >= s.from && to <= s.to)) continue;\n\t\t\t\ttokens.push({ from, to, category: token.category });\n\t\t\t}\n\t\t} else {\n\t\t\t// Blockquote content is stripped of its \"> \" prefix before being\n\t\t\t// tokenized (see Lexer.getHighlightTokens), token offsets are\n\t\t\t// already relative to the stripped text, so the parse check must\n\t\t\t// run against that same substring to match.\n\t\t\tconst text = lineText.startsWith(\"> \") && classification.skip\n\t\t\t\t? lineText.slice(2)\n\t\t\t\t: lineText;\n\t\t\tif (this.parsesAsExpression(text)) {\n\t\t\t\tfor (const token of lexed) {\n\t\t\t\t\tif (!token.category) continue;\n\t\t\t\t\ttokens.push({ from: token.offset, to: token.offset + token.length, category: token.category });\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// A lone bare word (\"hello\") is exactly as ambiguous as a run of\n\t\t// prose (\"My name is dave\"), it happens to parse as a\n\t\t// single-identifier variable-reference expression, but that's true\n\t\t// of literally any English word, so on its own it isn't \"recognized\"\n\t\t// in any meaningful sense. Sigil-marked variables (\":x\", \"$x\") are\n\t\t// unaffected, those lex to TWO tokens (sigil + ident), never\n\t\t// hitting this single-token check. Keywords (\"pi\") are unaffected\n\t\t// too, their category is \"keyword\", not \"variable\". Only surface\n\t\t// it once it's an actual known variable elsewhere in the document\n\t\t// checked live (see the `bareWordCandidate` cache field), not baked\n\t\t// into the cached result, since another line's edit can make this\n\t\t// check flip without this line's own text ever changing.\n\t\tif (tokens.length === 1 && tokens[0].category === \"variable\") {\n\t\t\tconst name = lineText.slice(tokens[0].from, tokens[0].to);\n\t\t\tthis.putCache(lineNumber, lineText, tokens, name);\n\t\t\treturn this.isKnownVariable(name) ? tokens : [];\n\t\t}\n\n\t\tthis.putCache(lineNumber, lineText, tokens);\n\t\treturn tokens;\n\t}\n\n\t/**\n\t * Completion candidates for the identifier prefix immediately before\n\t * `cursorOffset` on `lineText`. Deliberately simple prefix matching, not\n\t * parser-driven \"what's grammatically valid here\" prediction, a\n\t * half-typed expression almost never parses, so gating on parse\n\t * validity (the way `getSemanticTokens` does) would suppress\n\t * completions almost always. This is the safest, fastest option that\n\t * still delivers real value.\n\t *\n\t * Candidates come from three sources: keywords (which already include\n\t * function names. See `ExpressionLexer.getKeywords()`'s doc comment)\n\t * and units, both static per engine configuration and cached lazily;\n\t * package-contributed items (`IEnginePackage.completionItems`), same\n\t * cache; and variable names, read fresh from `variableNameSource()` on\n\t * every call since those change on every edit.\n\t */\n\tgetCompletions(lineText: string, cursorOffset: number): CompletionItem[] {\n\t\tconst prefixMatch = /[A-Za-z0-9_]+$/.exec(lineText.slice(0, cursorOffset));\n\t\tif (!prefixMatch) return [];\n\t\tconst prefix = prefixMatch[0].toLowerCase();\n\n\t\tif (!this.engine) return [];\n\n\t\tconst matches: CompletionItem[] = [];\n\n\t\t// Variables are read fresh every call and there are few of them, so they\n\t\t// stay a linear scan.\n\t\tfor (const name of this.variableNameSource()) {\n\t\t\tif (name.toLowerCase().startsWith(prefix)) {\n\t\t\t\tmatches.push({ label: name, category: \"variable\" });\n\t\t\t}\n\t\t}\n\n\t\t// Static candidates only ever match if they share the prefix's first\n\t\t// character, so consult that bucket alone.\n\t\tconst bucket = this.getStaticCompletionIndex().get(prefix[0]);\n\t\tif (bucket) {\n\t\t\tfor (const candidate of bucket) {\n\t\t\t\tif (candidate.lowerLabel.startsWith(prefix)) matches.push(candidate.item);\n\t\t\t}\n\t\t}\n\n\t\tmatches.sort((a, b) => {\n\t\t\tconst tierDiff = (CATEGORY_TIER[a.category] ?? 3) - (CATEGORY_TIER[b.category] ?? 3);\n\t\t\tif (tierDiff !== 0) return tierDiff;\n\t\t\treturn a.label.localeCompare(b.label);\n\t\t});\n\n\t\treturn matches.slice(0, MAX_COMPLETIONS);\n\t}\n\n\t/** Lazily builds and caches the keyword/unit/package-item candidate list. See `staticCompletionCandidates`. */\n\tprivate getStaticCompletionCandidates(): CompletionItem[] {\n\t\tif (this.staticCompletionCandidates) return this.staticCompletionCandidates;\n\n\t\tconst items: CompletionItem[] = [];\n\t\tfor (const [word, tokenType] of Object.entries(this.engine!.getLexer().getKeywords())) {\n\t\t\tconst category = getTokenCategory(tokenType);\n\t\t\tif (!category) continue;\n\t\t\titems.push({ label: word, category });\n\t\t}\n\t\tfor (const unit of knownUnits) {\n\t\t\titems.push({ label: unit, category: \"unit\", detail: getMeasure(unit) });\n\t\t}\n\t\titems.push(...this.engine!.getPackageCompletionItems());\n\n\t\tthis.staticCompletionCandidates = items;\n\t\treturn items;\n\t}\n\n\t/**\n\t * The static candidates bucketed by lowercased first character, built once\n\t * from {@link getStaticCompletionCandidates} and invalidated alongside it.\n\t */\n\tprivate getStaticCompletionIndex(): Map<string, IndexedCompletionCandidate[]> {\n\t\tif (this.staticCompletionIndex) return this.staticCompletionIndex;\n\n\t\tconst index = new Map<string, IndexedCompletionCandidate[]>();\n\t\tfor (const item of this.getStaticCompletionCandidates()) {\n\t\t\tconst lowerLabel = item.label.toLowerCase();\n\t\t\tconst firstCharacter = lowerLabel[0];\n\t\t\t// A label cannot match any prefix if it is empty, and the prefix\n\t\t\t// regex guarantees at least one character on the query side.\n\t\t\tif (firstCharacter === undefined) continue;\n\t\t\tlet bucket = index.get(firstCharacter);\n\t\t\tif (bucket === undefined) {\n\t\t\t\tbucket = [];\n\t\t\t\tindex.set(firstCharacter, bucket);\n\t\t\t}\n\t\t\tbucket.push({ item, lowerLabel });\n\t\t}\n\n\t\tthis.staticCompletionIndex = index;\n\t\treturn index;\n\t}\n\n\tprivate isKnownVariable(name: string): boolean {\n\t\tfor (const known of this.variableNameSource()) {\n\t\t\tif (known === name) return true;\n\t\t}\n\t\treturn false;\n\t}\n\n\t/**\n\t * Whether the engine's parser actually accepts a piece of text as a\n\t * well-formed expression, not merely whether it lexes into individually\n\t * recognized token types. See the class doc comment's prose example.\n\t * `tryCompileExpression` is compile-only (lex → normalize → parse →\n\t * cache bytecode, no VM execution, no network/async side effects) and\n\t * reuses the engine's existing bytecode cache, so text that's already\n\t * been evaluated (or previously highlight-checked) is a cache hit here\n\t * too.\n\t *\n\t * Deliberately calls the non-throwing `tryCompileExpression` rather than\n\t * try/catching `compileExpression`. This runs on every visible line on\n\t * every keystroke, and the common case for a real markdown document is\n\t * lines that DON'T parse (prose), not lines that do. Throwing there would\n\t * mean constructing a EngineError (with V8 stack-trace capture) for the\n\t * common case instead of the rare one.\n\t */\n\tprivate parsesAsExpression(text: string): boolean {\n\t\treturn this.engine!.tryCompileExpression(text);\n\t}\n\n\tprivate putCache(lineNumber: number, text: string, tokens: SemanticToken[], bareWordCandidate?: string): void {\n\t\tif (!this.cache.has(lineNumber) && this.cache.size >= MAX_CACHED_LINES) {\n\t\t\tconst oldestKey = this.cache.keys().next().value;\n\t\t\tif (oldestKey !== undefined) this.cache.delete(oldestKey);\n\t\t}\n\t\tthis.cache.set(lineNumber, { text, tokens, bareWordCandidate });\n\t}\n\n\t/**\n\t * Evict specific lines (e.g. the lines actually touched by a CodeMirror\n\t * change set) instead of the whole cache, the surgical counterpart to\n\t * {@link invalidateCache}, letting a single-line edit stay cheap even in\n\t * a large document: every other cached line is untouched and still hits\n\t * on the next call.\n\t */\n\tinvalidateLines(lineNumbers: Iterable<number>): void {\n\t\tfor (const lineNumber of lineNumbers) {\n\t\t\tthis.cache.delete(lineNumber);\n\t\t}\n\t}\n\n\t/**\n\t * Full cache clear. Reserved for cases with no meaningful \"which lines\n\t * changed\" (e.g. the document was swapped wholesale, or a package was\n\t * registered/unregistered mid-session, changing what categories exist).\n\t * Prefer {@link invalidateLines} for ordinary edits. Also rebuilds the\n\t * lazily-cached keyword/unit/package-item completion candidates on next\n\t * use, the only thing that can change that list mid-session.\n\t */\n\tinvalidateCache(): void {\n\t\tthis.cache.clear();\n\t\tthis.staticCompletionCandidates = null;\n\t\tthis.staticCompletionIndex = null;\n\t}\n}\n","import type { TokenCategory } from \"@solve-js/language/TokenCategory\";\n\n/**\n * Namespace applied to a semantic category when no other one is asked for.\n *\n * A prefix is needed because a bare category name like `number` or `error`\n * would collide with almost any host stylesheet. `solve-` is the narrowest\n * namespace that still says where the class came from, and it deliberately\n * names this library rather than any particular editor.\n */\nexport const DEFAULT_TOKEN_CLASS_PREFIX = \"solve-\";\n\n/**\n * Maps a semantic category to a predictable, stable CSS class name.\n *\n * Deliberately trivial: the category name IS the class-name key\n * (`\"number\"` → `solve-number`), so there is no separate table to keep in\n * sync as categories grow. A brand-new category, including one contributed\n * by a solve-js package at runtime, gets a matching class name with no\n * changes here.\n *\n * This is editor-agnostic on purpose. It produces a class-name string and\n * nothing else, so it works the same whether the host is CodeMirror\n * (`Decoration.mark({ class: tokenClassName(token.category) })`), Slate,\n * ProseMirror, Monaco or a plain `<span>`. Actual colors are pure CSS,\n * resolved from `--solve-hl-{category}` custom properties by each consumer.\n *\n * @param category - Semantic category from `getSemanticTokens()`.\n * @param prefix - Namespace to apply. Defaults to `solve-`.\n * @returns The class name for that category.\n */\nexport function tokenClassName(category: TokenCategory, prefix: string = DEFAULT_TOKEN_CLASS_PREFIX): string {\n\treturn `${prefix}${category}`;\n}\n\n/**\n * Builds a `tokenClassName` bound to a fixed prefix.\n *\n * The prefix is configurable because a host may already own a namespace it\n * wants these classes to sit inside: a CodeMirror theme that keys off `cm-`,\n * a design system that scopes everything under one token, or an app that\n * embeds two editors and wants to style them apart. Passing the prefix once\n * here is cheaper than threading it through every call site.\n *\n * ```ts\n * const className = createTokenClassName(\"cm-solve-\");\n * className(\"number\"); // \"cm-solve-number\"\n * ```\n *\n * @param prefix - Namespace to apply to every category.\n * @returns A function mapping a category to a prefixed class name.\n */\nexport function createTokenClassName(prefix: string): (category: TokenCategory) => string {\n\treturn (category: TokenCategory) => `${prefix}${category}`;\n}\n","import type { TokenCategory } from \"@solve-js/language/TokenCategory\";\nimport type { CompletionItem } from \"@solve-js/language/LanguageService\";\n\n/**\n * Maps a semantic category to one of `@codemirror/autocomplete`'s built-in\n * completion \"type\" strings (which drive its default gutter icon), the one\n * genuinely CodeMirror-specific thing this feature needs. Falls back to\n * \"text\" for anything unmapped, including plugin-contributed categories\n * (e.g. OSRS's \"osrs-item\"), a reasonable neutral default rather than a\n * hard failure for a category this adapter doesn't know about yet.\n *\n * No `@codemirror/autocomplete` import here, the returned object shape is\n * structurally compatible with CM6's `Completion` type by duck typing, so\n * `solve-js` gains no new dependency; the actual `CompletionSource`\n * function (reading `CompletionContext`, building a `CompletionResult`)\n * lives in each consumer (src/app, playground), same tier as\n * `buildDecorations()` already is for highlighting.\n */\nconst CATEGORY_TO_COMPLETION_TYPE: Partial<Record<TokenCategory, string>> = {\n\tkeyword: \"keyword\",\n\toperator: \"keyword\",\n\tcomparison: \"keyword\",\n\tbitwise: \"keyword\",\n\tfunction: \"function\",\n\tvariable: \"variable\",\n\tunit: \"type\",\n\tdatetime: \"keyword\",\n\tvector: \"type\",\n};\n\n/**\n * Convert a completion into CodeMirror's option shape.\n *\n * Kept in an adapter so the language service itself stays editor-agnostic.\n *\n * @param item - Completion produced by the language service.\n * @returns The equivalent CodeMirror option.\n */\nexport function completionItemToOption(item: CompletionItem): { label: string; type: string; detail?: string } {\n\treturn { label: item.label, type: CATEGORY_TO_COMPLETION_TYPE[item.category] ?? \"text\", detail: item.detail };\n}\n"]}
1
+ {"version":3,"sources":["../src/language/LanguageService.ts","../src/language/tokenClassName.ts","../src/language/adapters/codemirror.ts"],"names":[],"mappings":"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","file":"language.js","sourcesContent":["import type { ExpressionEngine } from \"@solve-js/engine/ExpressionEngine\";\nimport type { TokenCategory } from \"@solve-js/language/TokenCategory\";\nimport { getTokenCategory } from \"@solve-js/language/TokenCategoryMap\";\nimport type { Token } from \"@solve-js/lexer/Token\";\nimport { knownUnits } from \"@solve-js/lexer/units\";\nimport { getMeasure } from \"@solve-js/uom/UomConverter\";\n\n/** A single classified span within a line, the entire output contract of the language service. */\nexport interface SemanticToken {\n\tfrom: number;\n\tto: number;\n\tcategory: TokenCategory;\n}\n\n/** A single completion candidate, the entire output contract of `getCompletions()`. */\nexport interface CompletionItem {\n\tlabel: string;\n\t/** Reuses the highlighting taxonomy, one adapter can serve both features. */\n\tcategory: TokenCategory;\n\t/** e.g. a unit's measure (\"length\"), or the category name for keywords/functions. */\n\tdetail?: string;\n}\n\n/**\n * A completion candidate with its lowercased label precomputed.\n *\n * Internal to the prefix index; callers only ever see the {@link CompletionItem}.\n */\ninterface IndexedCompletionCandidate {\n\titem: CompletionItem;\n\tlowerLabel: string;\n}\n\n/** Completion results are capped, a document-wide candidate pool has no reason to return more than this. */\nconst MAX_COMPLETIONS = 50;\n\n/** Tier ordering for completion results: user-authored variables first, then grammar, then units. */\nconst CATEGORY_TIER: Partial<Record<TokenCategory, number>> = {\n\tvariable: 0,\n\tfunction: 1,\n\tkeyword: 1,\n\toperator: 1,\n\tcomparison: 1,\n\tbitwise: 1,\n\tdatetime: 1,\n\tvector: 1,\n\tunit: 2,\n};\n\n/** Bounded cache size. See the eviction-policy note on `LanguageService.cache`. */\nconst MAX_CACHED_LINES = 2000;\n\ninterface CacheEntry {\n\ttext: string;\n\ttokens: SemanticToken[];\n\t// When set, `tokens` is a single bare-identifier token whose validity\n\t// depends on document-wide DAG state (see the bare-word gate in\n\t// getSemanticTokens), not just this line's own text, so it can't be\n\t// cached as a plain pass/fail result the way every other line can. The\n\t// lex+parse work that produced `tokens` is still cached normally; only\n\t// the DAG membership check is re-run on every lookup (cache hit or\n\t// miss alike), since it's cheap (a Set lookup) and the alternative\n\t// caching the gated result, would go stale the moment some OTHER\n\t// line's edit changes what variables exist, with nothing to trigger a\n\t// re-check of this untouched line.\n\tbareWordCandidate?: string;\n}\n\n/** Options for the editor-facing language service: completions and highlighting. */\nexport interface LanguageServiceOptions {\n\t/**\n\t * Overrides how the service discovers \"variable names known in this\n\t * document\", used to legitimize a lone bare identifier line (see\n\t * `getSemanticTokens`'s single-token gate) and variable-name\n\t * completions (`getCompletions`). Defaults to reading\n\t * `engine.getDag().getSnapshot()`, which works for any consumer\n\t * sharing one `ExpressionEngine` between evaluation and the language\n\t * service (the real Obsidian editor).\n\t *\n\t * Required for consumers whose language service is backed by a\n\t * *different*, non-evaluating engine than the one that actually runs\n\t * the document (the playground's dedicated lexing-only engine, whose\n\t * own DAG is always empty), pass a function reading the real\n\t * evaluation engine's DAG snapshot instead.\n\t */\n\tvariableNameSource?: () => Iterable<string>;\n\n\t/**\n\t * Run the normalizer on the highlighting path, so phrase-fused tokens are\n\t * classified as the thing the parser will actually see.\n\t *\n\t * Off by default, and the default is a judgement rather than an oversight.\n\t * Highlighting runs per keystroke, normalization is real work, and a host\n\t * that is happy with lexer-level categories should not start paying for\n\t * fusion because a new version shipped. Turn it on and `12/09/2026` is one\n\t * `datetime` span instead of five number and operator spans; leave it off\n\t * and nothing about this class changes.\n\t *\n\t * See `benchmarks/languageServiceBenchmarks.spec.ts` for what it costs.\n\t */\n\tnormalizeForHighlighting?: boolean;\n}\n\n/**\n * Editor-agnostic \"language server\" for solve expressions: turns a line of\n * text into semantic token ranges, using the exact same lexer real\n * evaluation uses (so it only ever classifies what the engine's grammar\n * actually recognizes, never a separate/duplicated tokenizer). No\n * knowledge of CSS, CodeMirror, VS Code, or any other rendering concept\n * lives here. See `language/adapters/` for that.\n *\n * Classification happens at the LEXER stage, before the normalizer runs\n * (normalization, phrase fusion, implicit multiply, and package-specific\n * rules, happens later, only on the real evaluation path). A package's\n * lexer-level custom token types (e.g. a custom keyword) are recognized\n * here exactly as evaluation would see them. A package's *normalizer*-fused\n * synthetic tokens (e.g. OSRS's GAME_ITEM, built by fusing several\n * consecutive IDENT tokens against an item-name trie) are NOT. This\n * service still shows the pre-fusion IDENT tokens individually for those.\n * `IEnginePackage.tokenCategories` entries for normalizer-only token types\n * are still valid, correct registrations (queryable via getTokenCategory)\n *, they just won't currently be reachable through this lexer-only\n * classification path. Folding normalization in would require running it\n * per keystroke on the highlighting path too, which needs its own careful\n * design (span recomputation for fused multi-token ranges, in particular)\n * rather than a quick addition here.\n *\n * Lexing alone is NOT sufficient to decide \"recognized\", though: a run of\n * plain-English words (\"My name is ron\") lexes into a sequence of\n * individually-valid IDENT tokens with no grammar tying them together\n * every word \"recognized\" at the token level, but the line as a whole is\n * not something the engine would ever accept as an expression. Surfacing\n * per-token colors for that case looks like the editor mistook prose for\n * code. So a line's tokens are only surfaced once the line as a whole\n * parses successfully (via `ExpressionEngine.compileExpression`, the same\n * parse pipeline, and the same bytecode cache, real evaluation uses; no\n * separate/duplicated grammar check). A single bare word (\"hello\", a valid\n * variable reference) or a keyword-only line (\"pi\") still parses and still\n * highlights, only genuinely ungrammatical text is suppressed, unless it's\n * a known variable elsewhere in the document (see `variableNameSource`).\n *\n * `getCompletions()` is the other half of this \"language server\": unlike\n * `getSemanticTokens()`, it's explicitly for *incomplete*, mid-typing text\n *, it deliberately does NOT gate on parse validity (a half-typed\n * expression almost never parses), using simple prefix matching instead.\n *\n * Must be constructed with an already-configured `ExpressionEngine` (one\n * with all currently-relevant packages registered) rather than a bare\n * lexer, reusing an existing engine is both the fast path (no throwaway\n * lexer construction) and the *correct* one: a highlighting-only lexer\n * built independently of the evaluation engine would silently fail to\n * recognize plugin-contributed tokens (e.g. a package's custom keywords)\n * unless it happened to have the identical packages registered.\n */\nexport class LanguageService {\n\tprivate engine: ExpressionEngine | null;\n\tprivate variableNameSource: () => Iterable<string>;\n\tprivate readonly normalizeForHighlighting: boolean;\n\n\t// Bounded cache keyed by line number ALONE, not `${lineNumber}:${lineText}`\n\t// as an earlier version of this class did. A line's previous text state is\n\t// never useful once it changes, so keying on text too was pure waste:\n\t// every keystroke on a line minted a brand-new, never-reclaimed cache\n\t// entry (an effective per-keystroke memory leak over a long editing\n\t// session). Keying on line number alone makes \"same line, new text\" a\n\t// cheap overwrite instead.\n\t//\n\t// Eviction is oldest-inserted (Map iteration order) when at capacity\n\t// mirroring the same bounded-cache pattern ExpressionEngine's own\n\t// bytecodeCache already uses elsewhere in this codebase. Deliberately\n\t// NOT an LFU (least-frequently-used) policy: LFU would keep resisting\n\t// eviction of old, once-popular lines while punishing a line that just\n\t// scrolled into view (frequency 1), the opposite of what a \"currently\n\t// visible\" cache should prioritize.\n\tprivate cache = new Map<number, CacheEntry>();\n\n\t// Keyword/unit/package-contributed completion candidates don't depend\n\t// on any particular line, built lazily on first getCompletions() call\n\t// and reused after that, since a package registration is the only thing\n\t// that could ever change this list mid-session (see invalidateCache()).\n\t// Variable-name candidates are NOT part of this, they're read fresh on\n\t// every call from variableNameSource(), since those genuinely change on\n\t// every edit.\n\tprivate staticCompletionCandidates: CompletionItem[] | null = null;\n\n\t// The same candidates, bucketed by their lowercased first character with the\n\t// lowercased label precomputed. getCompletions() runs on every keystroke and\n\t// only ever wants candidates sharing the prefix's first character, so\n\t// scanning the whole list and lowercasing each label per call was doing two\n\t// avoidable things: touching entries that could not possibly match, and\n\t// allocating a string per candidate per keystroke. That was affordable when\n\t// the vocabulary was a few hundred entries. Deriving the unit list from the\n\t// conversion tables took it past a thousand, and the warm completion\n\t// benchmarks regressed roughly 2.9x until this was added.\n\tprivate staticCompletionIndex: Map<string, IndexedCompletionCandidate[]> | null = null;\n\n\tconstructor(engine?: ExpressionEngine | null, options?: LanguageServiceOptions) {\n\t\tthis.engine = engine ?? null;\n\t\tthis.variableNameSource = options?.variableNameSource ?? (() => this.defaultVariableNames());\n\t\tthis.normalizeForHighlighting = options?.normalizeForHighlighting ?? false;\n\t}\n\n\tprivate defaultVariableNames(): Iterable<string> {\n\t\tif (!this.engine) return [];\n\t\tconst snapshot = this.engine.getDag().getSnapshot();\n\t\tconst names = new Set<string>(Object.keys(snapshot.consumers));\n\t\tfor (const written of Object.values(snapshot.writes)) {\n\t\t\tfor (const name of written) names.add(name);\n\t\t}\n\t\treturn names;\n\t}\n\n\t/**\n\t * Lex a line, normalize it, and place every resulting token back in the\n\t * source text.\n\t *\n\t * ## Why placing them back is the hard part\n\t *\n\t * Normalization produces three kinds of token and only one of them can be\n\t * highlighted the obvious way.\n\t *\n\t * A token the normalizer left alone still describes its own text, so its\n\t * span is `offset` to `offset + value.length`, exactly as before.\n\t *\n\t * A FUSED token does not. `10 frames` becomes a FRAME_COUNT whose value is\n\t * `10`, and a timecode becomes a token whose value is a comma-separated\n\t * tuple appearing nowhere on the line. Those carry `sourceEnd`, set by\n\t * `createFusedToken`, which is the only place that knows where the fusion\n\t * ended.\n\t *\n\t * An INSERTED token has no text at all. Implicit multiplication puts a STAR\n\t * at the following token's offset, so `5(3)` gains a `*` sitting exactly\n\t * where the `(` is. Painting it would colour a character the reader never\n\t * typed as an operator, and would overlap the token that really is there.\n\t * These are dropped, detected by the one test that needs no cooperation\n\t * from any rule: a token with no recorded fusion span must match the text\n\t * at its own offset, and an inserted one does not.\n\t *\n\t * @param lineText - The raw line.\n\t * @returns Spans in the same shape `Lexer.getHighlightTokens` returns.\n\t */\n\tprivate classifyNormalized(\n\t\tlineText: string,\n\t): { type: string; value: string; offset: number; col: number; length: number; category: TokenCategory | undefined }[] {\n\t\tconst lexer = this.engine!.getLexer();\n\t\tconst raw = lexer.getHighlightTokenObjects(lineText);\n\t\tif (raw.length === 0) return [];\n\n\t\t// Offsets from a blockquote line are relative to the stripped text, so\n\t\t// the text this checks against has to be stripped the same way.\n\t\tconst source =\n\t\t\tlineText.startsWith(\"> \") && lexer.classifyLine(lineText).skip ? lineText.slice(2) : lineText;\n\n\t\tlet normalized: Token[];\n\t\ttry {\n\t\t\tnormalized = this.engine!.getNormalizer().normalize(raw);\n\t\t} catch {\n\t\t\t// Normalization is an enhancement here, not a requirement. A rule\n\t\t\t// that throws on a half-typed line should cost the reader phrase\n\t\t\t// colouring, not all colouring.\n\t\t\tnormalized = raw;\n\t\t}\n\n\t\tconst out: {\n\t\t\ttype: string;\n\t\t\tvalue: string;\n\t\t\toffset: number;\n\t\t\tcol: number;\n\t\t\tlength: number;\n\t\t\tcategory: TokenCategory | undefined;\n\t\t}[] = [];\n\n\t\tfor (const token of normalized) {\n\t\t\tlet end: number;\n\t\t\tif (token.sourceEnd !== undefined) {\n\t\t\t\tend = token.sourceEnd;\n\t\t\t} else if (source.startsWith(token.value, token.offset)) {\n\t\t\t\tend = token.offset + token.value.length;\n\t\t\t} else {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tout.push({\n\t\t\t\ttype: token.type,\n\t\t\t\tvalue: token.value,\n\t\t\t\toffset: token.offset,\n\t\t\t\tcol: token.col,\n\t\t\t\tlength: end - token.offset,\n\t\t\t\tcategory: getTokenCategory(token.type),\n\t\t\t});\n\t\t}\n\n\t\treturn out;\n\t}\n\n\t/**\n\t * Classify every recognized token on one line.\n\t *\n\t * @param lineText - The raw line text (may be a markdown-structural line\n\t * the engine's classifier skips, that's handled by the underlying\n\t * lexer, which returns no tokens for those).\n\t * @param lineNumber - 1-based line number, used purely as a cache key.\n\t */\n\tgetSemanticTokens(lineText: string, lineNumber: number): SemanticToken[] {\n\t\tconst cached = this.cache.get(lineNumber);\n\t\tif (cached && cached.text === lineText) {\n\t\t\tif (cached.bareWordCandidate !== undefined) {\n\t\t\t\treturn this.isKnownVariable(cached.bareWordCandidate) ? cached.tokens : [];\n\t\t\t}\n\t\t\treturn cached.tokens;\n\t\t}\n\n\t\tif (!this.engine) {\n\t\t\t// No engine available (e.g. a consumer that hasn't wired one up yet)\n\t\t\t//, no highlighting, not an error.\n\t\t\treturn [];\n\t\t}\n\n\t\tconst lexer = this.engine.getLexer();\n\t\tconst lexed = this.normalizeForHighlighting\n\t\t\t? this.classifyNormalized(lineText)\n\t\t\t: lexer.getHighlightTokens(lineText);\n\t\tif (lexed.length === 0) {\n\t\t\tthis.putCache(lineNumber, lineText, []);\n\t\t\treturn [];\n\t\t}\n\n\t\tconst classification = lexer.classifyLine(lineText);\n\t\tconst tokens: SemanticToken[] = [];\n\n\t\tif (classification.hasInlineSolve) {\n\t\t\t// A line can mix markdown prose with one or more embedded\n\t\t\t// `s`...`` expressions. Only the text actually inside a\n\t\t\t// well-formed marker is a recognized expression, surrounding\n\t\t\t// prose lexes into individually-valid tokens too (see the class\n\t\t\t// doc comment) but is never something the engine would parse,\n\t\t\t// so it's excluded token-by-token via span membership rather\n\t\t\t// than gating the whole line pass/fail.\n\t\t\tconst validSpans = lexer\n\t\t\t\t.findInlineSolves(lineText)\n\t\t\t\t.filter(span => this.parsesAsExpression(span.expression))\n\t\t\t\t.map(span => ({ from: span.start, to: span.end }));\n\t\t\tfor (const token of lexed) {\n\t\t\t\tif (!token.category) continue;\n\t\t\t\tconst from = token.offset;\n\t\t\t\tconst to = token.offset + token.length;\n\t\t\t\tif (!validSpans.some(s => from >= s.from && to <= s.to)) continue;\n\t\t\t\ttokens.push({ from, to, category: token.category });\n\t\t\t}\n\t\t} else {\n\t\t\t// Blockquote content is stripped of its \"> \" prefix before being\n\t\t\t// tokenized (see Lexer.getHighlightTokens), token offsets are\n\t\t\t// already relative to the stripped text, so the parse check must\n\t\t\t// run against that same substring to match.\n\t\t\tconst text = lineText.startsWith(\"> \") && classification.skip\n\t\t\t\t? lineText.slice(2)\n\t\t\t\t: lineText;\n\t\t\tif (this.parsesAsExpression(text)) {\n\t\t\t\tfor (const token of lexed) {\n\t\t\t\t\tif (!token.category) continue;\n\t\t\t\t\ttokens.push({ from: token.offset, to: token.offset + token.length, category: token.category });\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// A lone bare word (\"hello\") is exactly as ambiguous as a run of\n\t\t// prose (\"My name is dave\"), it happens to parse as a\n\t\t// single-identifier variable-reference expression, but that's true\n\t\t// of literally any English word, so on its own it isn't \"recognized\"\n\t\t// in any meaningful sense. Sigil-marked variables (\":x\", \"$x\") are\n\t\t// unaffected, those lex to TWO tokens (sigil + ident), never\n\t\t// hitting this single-token check. Keywords (\"pi\") are unaffected\n\t\t// too, their category is \"keyword\", not \"variable\". Only surface\n\t\t// it once it's an actual known variable elsewhere in the document\n\t\t// checked live (see the `bareWordCandidate` cache field), not baked\n\t\t// into the cached result, since another line's edit can make this\n\t\t// check flip without this line's own text ever changing.\n\t\tif (tokens.length === 1 && tokens[0].category === \"variable\") {\n\t\t\tconst name = lineText.slice(tokens[0].from, tokens[0].to);\n\t\t\tthis.putCache(lineNumber, lineText, tokens, name);\n\t\t\treturn this.isKnownVariable(name) ? tokens : [];\n\t\t}\n\n\t\tthis.putCache(lineNumber, lineText, tokens);\n\t\treturn tokens;\n\t}\n\n\t/**\n\t * Completion candidates for the identifier prefix immediately before\n\t * `cursorOffset` on `lineText`. Deliberately simple prefix matching, not\n\t * parser-driven \"what's grammatically valid here\" prediction, a\n\t * half-typed expression almost never parses, so gating on parse\n\t * validity (the way `getSemanticTokens` does) would suppress\n\t * completions almost always. This is the safest, fastest option that\n\t * still delivers real value.\n\t *\n\t * Candidates come from three sources: keywords (which already include\n\t * function names. See `ExpressionLexer.getKeywords()`'s doc comment)\n\t * and units, both static per engine configuration and cached lazily;\n\t * package-contributed items (`IEnginePackage.completionItems`), same\n\t * cache; and variable names, read fresh from `variableNameSource()` on\n\t * every call since those change on every edit.\n\t */\n\tgetCompletions(lineText: string, cursorOffset: number): CompletionItem[] {\n\t\tconst prefixMatch = /[A-Za-z0-9_]+$/.exec(lineText.slice(0, cursorOffset));\n\t\tif (!prefixMatch) return [];\n\t\tconst prefix = prefixMatch[0].toLowerCase();\n\n\t\tif (!this.engine) return [];\n\n\t\tconst matches: CompletionItem[] = [];\n\n\t\t// Variables are read fresh every call and there are few of them, so they\n\t\t// stay a linear scan.\n\t\tfor (const name of this.variableNameSource()) {\n\t\t\tif (name.toLowerCase().startsWith(prefix)) {\n\t\t\t\tmatches.push({ label: name, category: \"variable\" });\n\t\t\t}\n\t\t}\n\n\t\t// Static candidates only ever match if they share the prefix's first\n\t\t// character, so consult that bucket alone.\n\t\tconst bucket = this.getStaticCompletionIndex().get(prefix[0]);\n\t\tif (bucket) {\n\t\t\tfor (const candidate of bucket) {\n\t\t\t\tif (candidate.lowerLabel.startsWith(prefix)) matches.push(candidate.item);\n\t\t\t}\n\t\t}\n\n\t\tmatches.sort((a, b) => {\n\t\t\tconst tierDiff = (CATEGORY_TIER[a.category] ?? 3) - (CATEGORY_TIER[b.category] ?? 3);\n\t\t\tif (tierDiff !== 0) return tierDiff;\n\t\t\treturn a.label.localeCompare(b.label);\n\t\t});\n\n\t\treturn matches.slice(0, MAX_COMPLETIONS);\n\t}\n\n\t/** Lazily builds and caches the keyword/unit/package-item candidate list. See `staticCompletionCandidates`. */\n\tprivate getStaticCompletionCandidates(): CompletionItem[] {\n\t\tif (this.staticCompletionCandidates) return this.staticCompletionCandidates;\n\n\t\tconst items: CompletionItem[] = [];\n\t\tfor (const [word, tokenType] of Object.entries(this.engine!.getLexer().getKeywords())) {\n\t\t\tconst category = getTokenCategory(tokenType);\n\t\t\tif (!category) continue;\n\t\t\titems.push({ label: word, category });\n\t\t}\n\t\tfor (const unit of knownUnits) {\n\t\t\titems.push({ label: unit, category: \"unit\", detail: getMeasure(unit) });\n\t\t}\n\t\titems.push(...this.engine!.getPackageCompletionItems());\n\n\t\tthis.staticCompletionCandidates = items;\n\t\treturn items;\n\t}\n\n\t/**\n\t * The static candidates bucketed by lowercased first character, built once\n\t * from {@link getStaticCompletionCandidates} and invalidated alongside it.\n\t */\n\tprivate getStaticCompletionIndex(): Map<string, IndexedCompletionCandidate[]> {\n\t\tif (this.staticCompletionIndex) return this.staticCompletionIndex;\n\n\t\tconst index = new Map<string, IndexedCompletionCandidate[]>();\n\t\tfor (const item of this.getStaticCompletionCandidates()) {\n\t\t\tconst lowerLabel = item.label.toLowerCase();\n\t\t\tconst firstCharacter = lowerLabel[0];\n\t\t\t// A label cannot match any prefix if it is empty, and the prefix\n\t\t\t// regex guarantees at least one character on the query side.\n\t\t\tif (firstCharacter === undefined) continue;\n\t\t\tlet bucket = index.get(firstCharacter);\n\t\t\tif (bucket === undefined) {\n\t\t\t\tbucket = [];\n\t\t\t\tindex.set(firstCharacter, bucket);\n\t\t\t}\n\t\t\tbucket.push({ item, lowerLabel });\n\t\t}\n\n\t\tthis.staticCompletionIndex = index;\n\t\treturn index;\n\t}\n\n\tprivate isKnownVariable(name: string): boolean {\n\t\tfor (const known of this.variableNameSource()) {\n\t\t\tif (known === name) return true;\n\t\t}\n\t\treturn false;\n\t}\n\n\t/**\n\t * Whether the engine's parser actually accepts a piece of text as a\n\t * well-formed expression, not merely whether it lexes into individually\n\t * recognized token types. See the class doc comment's prose example.\n\t * `tryCompileExpression` is compile-only (lex → normalize → parse →\n\t * cache bytecode, no VM execution, no network/async side effects) and\n\t * reuses the engine's existing bytecode cache, so text that's already\n\t * been evaluated (or previously highlight-checked) is a cache hit here\n\t * too.\n\t *\n\t * Deliberately calls the non-throwing `tryCompileExpression` rather than\n\t * try/catching `compileExpression`. This runs on every visible line on\n\t * every keystroke, and the common case for a real markdown document is\n\t * lines that DON'T parse (prose), not lines that do. Throwing there would\n\t * mean constructing a EngineError (with V8 stack-trace capture) for the\n\t * common case instead of the rare one.\n\t */\n\tprivate parsesAsExpression(text: string): boolean {\n\t\treturn this.engine!.tryCompileExpression(text);\n\t}\n\n\tprivate putCache(lineNumber: number, text: string, tokens: SemanticToken[], bareWordCandidate?: string): void {\n\t\tif (!this.cache.has(lineNumber) && this.cache.size >= MAX_CACHED_LINES) {\n\t\t\tconst oldestKey = this.cache.keys().next().value;\n\t\t\tif (oldestKey !== undefined) this.cache.delete(oldestKey);\n\t\t}\n\t\tthis.cache.set(lineNumber, { text, tokens, bareWordCandidate });\n\t}\n\n\t/**\n\t * Evict specific lines (e.g. the lines actually touched by a CodeMirror\n\t * change set) instead of the whole cache, the surgical counterpart to\n\t * {@link invalidateCache}, letting a single-line edit stay cheap even in\n\t * a large document: every other cached line is untouched and still hits\n\t * on the next call.\n\t */\n\tinvalidateLines(lineNumbers: Iterable<number>): void {\n\t\tfor (const lineNumber of lineNumbers) {\n\t\t\tthis.cache.delete(lineNumber);\n\t\t}\n\t}\n\n\t/**\n\t * Full cache clear. Reserved for cases with no meaningful \"which lines\n\t * changed\" (e.g. the document was swapped wholesale, or a package was\n\t * registered/unregistered mid-session, changing what categories exist).\n\t * Prefer {@link invalidateLines} for ordinary edits. Also rebuilds the\n\t * lazily-cached keyword/unit/package-item completion candidates on next\n\t * use, the only thing that can change that list mid-session.\n\t */\n\tinvalidateCache(): void {\n\t\tthis.cache.clear();\n\t\tthis.staticCompletionCandidates = null;\n\t\tthis.staticCompletionIndex = null;\n\t}\n}\n","import type { TokenCategory } from \"@solve-js/language/TokenCategory\";\n\n/**\n * Namespace applied to a semantic category when no other one is asked for.\n *\n * A prefix is needed because a bare category name like `number` or `error`\n * would collide with almost any host stylesheet. `solve-` is the narrowest\n * namespace that still says where the class came from, and it deliberately\n * names this library rather than any particular editor.\n */\nexport const DEFAULT_TOKEN_CLASS_PREFIX = \"solve-\";\n\n/**\n * Maps a semantic category to a predictable, stable CSS class name.\n *\n * Deliberately trivial: the category name IS the class-name key\n * (`\"number\"` → `solve-number`), so there is no separate table to keep in\n * sync as categories grow. A brand-new category, including one contributed\n * by a solve-js package at runtime, gets a matching class name with no\n * changes here.\n *\n * This is editor-agnostic on purpose. It produces a class-name string and\n * nothing else, so it works the same whether the host is CodeMirror\n * (`Decoration.mark({ class: tokenClassName(token.category) })`), Slate,\n * ProseMirror, Monaco or a plain `<span>`. Actual colors are pure CSS,\n * resolved from `--solve-hl-{category}` custom properties by each consumer.\n *\n * @param category - Semantic category from `getSemanticTokens()`.\n * @param prefix - Namespace to apply. Defaults to `solve-`.\n * @returns The class name for that category.\n */\nexport function tokenClassName(category: TokenCategory, prefix: string = DEFAULT_TOKEN_CLASS_PREFIX): string {\n\treturn `${prefix}${category}`;\n}\n\n/**\n * Builds a `tokenClassName` bound to a fixed prefix.\n *\n * The prefix is configurable because a host may already own a namespace it\n * wants these classes to sit inside: a CodeMirror theme that keys off `cm-`,\n * a design system that scopes everything under one token, or an app that\n * embeds two editors and wants to style them apart. Passing the prefix once\n * here is cheaper than threading it through every call site.\n *\n * ```ts\n * const className = createTokenClassName(\"cm-solve-\");\n * className(\"number\"); // \"cm-solve-number\"\n * ```\n *\n * @param prefix - Namespace to apply to every category.\n * @returns A function mapping a category to a prefixed class name.\n */\nexport function createTokenClassName(prefix: string): (category: TokenCategory) => string {\n\treturn (category: TokenCategory) => `${prefix}${category}`;\n}\n","import type { TokenCategory } from \"@solve-js/language/TokenCategory\";\nimport type { CompletionItem } from \"@solve-js/language/LanguageService\";\n\n/**\n * Maps a semantic category to one of `@codemirror/autocomplete`'s built-in\n * completion \"type\" strings (which drive its default gutter icon), the one\n * genuinely CodeMirror-specific thing this feature needs. Falls back to\n * \"text\" for anything unmapped, including plugin-contributed categories\n * (e.g. OSRS's \"osrs-item\"), a reasonable neutral default rather than a\n * hard failure for a category this adapter doesn't know about yet.\n *\n * No `@codemirror/autocomplete` import here, the returned object shape is\n * structurally compatible with CM6's `Completion` type by duck typing, so\n * `solve-js` gains no new dependency; the actual `CompletionSource`\n * function (reading `CompletionContext`, building a `CompletionResult`)\n * lives in each consumer (src/app, playground), same tier as\n * `buildDecorations()` already is for highlighting.\n */\nconst CATEGORY_TO_COMPLETION_TYPE: Partial<Record<TokenCategory, string>> = {\n\tkeyword: \"keyword\",\n\toperator: \"keyword\",\n\tcomparison: \"keyword\",\n\tbitwise: \"keyword\",\n\tfunction: \"function\",\n\tvariable: \"variable\",\n\tunit: \"type\",\n\tdatetime: \"keyword\",\n\tvector: \"type\",\n};\n\n/**\n * Convert a completion into CodeMirror's option shape.\n *\n * Kept in an adapter so the language service itself stays editor-agnostic.\n *\n * @param item - Completion produced by the language service.\n * @returns The equivalent CodeMirror option.\n */\nexport function completionItemToOption(item: CompletionItem): { label: string; type: string; detail?: string } {\n\treturn { label: item.label, type: CATEGORY_TO_COMPLETION_TYPE[item.category] ?? \"text\", detail: item.detail };\n}\n"]}
package/dist/lexer.cjs CHANGED
@@ -1,9 +1,9 @@
1
1
  'use strict';
2
2
 
3
- var chunkCLL7RUQV_cjs = require('./chunk-CLL7RUQV.cjs');
3
+ var chunk7J2Q77OH_cjs = require('./chunk-7J2Q77OH.cjs');
4
4
  require('./chunk-6BKTCEUP.cjs');
5
- var chunkNMD5VRN4_cjs = require('./chunk-NMD5VRN4.cjs');
6
- var chunkGW32KPCU_cjs = require('./chunk-GW32KPCU.cjs');
5
+ var chunk2R7SVDDB_cjs = require('./chunk-2R7SVDDB.cjs');
6
+ var chunkFI4WDYSL_cjs = require('./chunk-FI4WDYSL.cjs');
7
7
  require('./chunk-3YNVWKR2.cjs');
8
8
  var chunkEHAHVROS_cjs = require('./chunk-EHAHVROS.cjs');
9
9
  require('./chunk-53B6KDDJ.cjs');
@@ -14,47 +14,47 @@ require('./chunk-B7LXJ6LC.cjs');
14
14
 
15
15
  Object.defineProperty(exports, "Lexer", {
16
16
  enumerable: true,
17
- get: function () { return chunkCLL7RUQV_cjs.Lexer; }
17
+ get: function () { return chunk7J2Q77OH_cjs.Lexer; }
18
18
  });
19
19
  Object.defineProperty(exports, "LexerState", {
20
20
  enumerable: true,
21
- get: function () { return chunkCLL7RUQV_cjs.LexerState; }
21
+ get: function () { return chunk7J2Q77OH_cjs.LexerState; }
22
22
  });
23
23
  Object.defineProperty(exports, "buildTokenLookup", {
24
24
  enumerable: true,
25
- get: function () { return chunkCLL7RUQV_cjs.buildTokenLookup; }
25
+ get: function () { return chunk7J2Q77OH_cjs.buildTokenLookup; }
26
26
  });
27
27
  Object.defineProperty(exports, "sharedLexer", {
28
28
  enumerable: true,
29
- get: function () { return chunkCLL7RUQV_cjs.sharedLexer; }
29
+ get: function () { return chunk7J2Q77OH_cjs.sharedLexer; }
30
30
  });
31
31
  Object.defineProperty(exports, "ExpressionLexer", {
32
32
  enumerable: true,
33
- get: function () { return chunkNMD5VRN4_cjs.ExpressionLexer; }
33
+ get: function () { return chunk2R7SVDDB_cjs.ExpressionLexer; }
34
34
  });
35
35
  Object.defineProperty(exports, "LexerToken", {
36
36
  enumerable: true,
37
- get: function () { return chunkNMD5VRN4_cjs.LexerToken; }
37
+ get: function () { return chunk2R7SVDDB_cjs.LexerToken; }
38
38
  });
39
39
  Object.defineProperty(exports, "TokenTypes", {
40
40
  enumerable: true,
41
- get: function () { return chunkGW32KPCU_cjs.TokenTypes; }
41
+ get: function () { return chunkFI4WDYSL_cjs.TokenTypes; }
42
42
  });
43
43
  Object.defineProperty(exports, "registerAllTokenTypes", {
44
44
  enumerable: true,
45
- get: function () { return chunkGW32KPCU_cjs.registerAllTokenTypes; }
45
+ get: function () { return chunkFI4WDYSL_cjs.registerAllTokenTypes; }
46
46
  });
47
47
  Object.defineProperty(exports, "registerTokenType", {
48
48
  enumerable: true,
49
- get: function () { return chunkGW32KPCU_cjs.registerTokenType; }
49
+ get: function () { return chunkFI4WDYSL_cjs.registerTokenType; }
50
50
  });
51
51
  Object.defineProperty(exports, "tokenTypeId", {
52
52
  enumerable: true,
53
- get: function () { return chunkGW32KPCU_cjs.tokenTypeId; }
53
+ get: function () { return chunkFI4WDYSL_cjs.tokenTypeId; }
54
54
  });
55
55
  Object.defineProperty(exports, "tokenTypeName", {
56
56
  enumerable: true,
57
- get: function () { return chunkGW32KPCU_cjs.tokenTypeName; }
57
+ get: function () { return chunkFI4WDYSL_cjs.tokenTypeName; }
58
58
  });
59
59
  Object.defineProperty(exports, "knownUnits", {
60
60
  enumerable: true,
package/dist/lexer.d.cts CHANGED
@@ -1,6 +1,6 @@
1
- import { T as TokenLookup } from './Lexer-BJdhlnej.cjs';
2
- export { E as ExpressionLexer, I as InlineSolveSpan, L as Lexer, a as LexerState, b as LexerToken, c as LexerVocabulary, d as LineClassification, M as MarkdownLineType, S as ScanLineResult, s as sharedLexer } from './Lexer-BJdhlnej.cjs';
3
- export { T as Token, a as TokenType, b as TokenTypes, r as registerAllTokenTypes, c as registerTokenType, t as tokenTypeId, d as tokenTypeName } from './Token-0jpvUdPY.cjs';
1
+ import { T as TokenLookup } from './Lexer-y8uY6n6t.cjs';
2
+ export { E as ExpressionLexer, I as InlineSolveSpan, L as Lexer, a as LexerState, b as LexerToken, c as LexerVocabulary, d as LineClassification, M as MarkdownLineType, S as ScanLineResult, s as sharedLexer } from './Lexer-y8uY6n6t.cjs';
3
+ export { T as Token, a as TokenType, b as TokenTypes, r as registerAllTokenTypes, c as registerTokenType, t as tokenTypeId, d as tokenTypeName } from './Token-DnU2hwFY.cjs';
4
4
 
5
5
  /**
6
6
  * The set of unit identifiers the lexer will turn into a UNIT token.
package/dist/lexer.d.ts CHANGED
@@ -1,6 +1,6 @@
1
- import { T as TokenLookup } from './Lexer-D9l4Yrk2.js';
2
- export { E as ExpressionLexer, I as InlineSolveSpan, L as Lexer, a as LexerState, b as LexerToken, c as LexerVocabulary, d as LineClassification, M as MarkdownLineType, S as ScanLineResult, s as sharedLexer } from './Lexer-D9l4Yrk2.js';
3
- export { T as Token, a as TokenType, b as TokenTypes, r as registerAllTokenTypes, c as registerTokenType, t as tokenTypeId, d as tokenTypeName } from './Token-0jpvUdPY.js';
1
+ import { T as TokenLookup } from './Lexer-Dkt7KGaU.js';
2
+ export { E as ExpressionLexer, I as InlineSolveSpan, L as Lexer, a as LexerState, b as LexerToken, c as LexerVocabulary, d as LineClassification, M as MarkdownLineType, S as ScanLineResult, s as sharedLexer } from './Lexer-Dkt7KGaU.js';
3
+ export { T as Token, a as TokenType, b as TokenTypes, r as registerAllTokenTypes, c as registerTokenType, t as tokenTypeId, d as tokenTypeName } from './Token-DnU2hwFY.js';
4
4
 
5
5
  /**
6
6
  * The set of unit identifiers the lexer will turn into a UNIT token.
package/dist/lexer.js CHANGED
@@ -1,7 +1,7 @@
1
- export { Lexer, LexerState, buildTokenLookup, sharedLexer } from './chunk-HWSZJQCI.js';
1
+ export { Lexer, LexerState, buildTokenLookup, sharedLexer } from './chunk-AF5AE52C.js';
2
2
  import './chunk-KVILKGMS.js';
3
- export { ExpressionLexer, LexerToken } from './chunk-XVWCOTR6.js';
4
- export { TokenTypes, registerAllTokenTypes, registerTokenType, tokenTypeId, tokenTypeName } from './chunk-RIN643A3.js';
3
+ export { ExpressionLexer, LexerToken } from './chunk-VM5ZKDLH.js';
4
+ export { TokenTypes, registerAllTokenTypes, registerTokenType, tokenTypeId, tokenTypeName } from './chunk-626UW5RU.js';
5
5
  import './chunk-GOLDJNMZ.js';
6
6
  export { knownUnits } from './chunk-LIPPNDBE.js';
7
7
  import './chunk-4MG4XKO2.js';
@@ -1,9 +1,9 @@
1
1
  'use strict';
2
2
 
3
3
  require('./chunk-NGBBNL4K.cjs');
4
- var chunkEBSPLUW4_cjs = require('./chunk-EBSPLUW4.cjs');
5
- require('./chunk-NMD5VRN4.cjs');
6
- require('./chunk-GW32KPCU.cjs');
4
+ var chunkCGIDYRAX_cjs = require('./chunk-CGIDYRAX.cjs');
5
+ require('./chunk-2R7SVDDB.cjs');
6
+ require('./chunk-FI4WDYSL.cjs');
7
7
  require('./chunk-3YNVWKR2.cjs');
8
8
  require('./chunk-EHAHVROS.cjs');
9
9
  require('./chunk-53B6KDDJ.cjs');
@@ -14,27 +14,27 @@ require('./chunk-B7LXJ6LC.cjs');
14
14
 
15
15
  Object.defineProperty(exports, "BUILTIN_PHRASES", {
16
16
  enumerable: true,
17
- get: function () { return chunkEBSPLUW4_cjs.BUILTIN_PHRASES; }
17
+ get: function () { return chunkCGIDYRAX_cjs.BUILTIN_PHRASES; }
18
18
  });
19
19
  Object.defineProperty(exports, "PhraseTrie", {
20
20
  enumerable: true,
21
- get: function () { return chunkEBSPLUW4_cjs.PhraseTrie; }
21
+ get: function () { return chunkCGIDYRAX_cjs.PhraseTrie; }
22
22
  });
23
23
  Object.defineProperty(exports, "TokenNormalizer", {
24
24
  enumerable: true,
25
- get: function () { return chunkEBSPLUW4_cjs.TokenNormalizer; }
25
+ get: function () { return chunkCGIDYRAX_cjs.TokenNormalizer; }
26
26
  });
27
27
  Object.defineProperty(exports, "createBuiltinNormalizerRules", {
28
28
  enumerable: true,
29
- get: function () { return chunkEBSPLUW4_cjs.createBuiltinNormalizerRules; }
29
+ get: function () { return chunkCGIDYRAX_cjs.createBuiltinNormalizerRules; }
30
30
  });
31
31
  Object.defineProperty(exports, "createFusedToken", {
32
32
  enumerable: true,
33
- get: function () { return chunkEBSPLUW4_cjs.createFusedToken; }
33
+ get: function () { return chunkCGIDYRAX_cjs.createFusedToken; }
34
34
  });
35
35
  Object.defineProperty(exports, "implicitMultiplyRule", {
36
36
  enumerable: true,
37
- get: function () { return chunkEBSPLUW4_cjs.implicitMultiplyRule; }
37
+ get: function () { return chunkCGIDYRAX_cjs.implicitMultiplyRule; }
38
38
  });
39
39
  //# sourceMappingURL=normalizer.cjs.map
40
40
  //# sourceMappingURL=normalizer.cjs.map
@@ -1,212 +1,6 @@
1
- import { T as Token } from './Token-0jpvUdPY.cjs';
2
- import { T as TokenFusion, N as NormalizerRule, a as NormalizerMatch } from './NormalizerRule-BrVoVjmP.cjs';
3
-
4
- /**
5
- * TokenNormalizer, post-lexer token normalization pass.
6
- *
7
- * ## Purpose
8
- * Applies domain-specific {@link NormalizerRule | NormalizerRules} to the raw
9
- * token stream produced by the {@link ExpressionLexer}. This keeps the lexer
10
- * slim and focused on single-token production, while multi-token pattern
11
- * matching (phrases, implicit operators, domain merges) lives here.
12
- *
13
- * ## What rules can do
14
- * - **Phrase fusion**: Merge consecutive words into compound tokens
15
- * (e.g., `IDENT + ... + IDENT` → `CARET`)
16
- * - **Implicit operator insertion**: Insert missing operators between tokens
17
- * (e.g., `NUMBER IDENT` → `NUMBER STAR IDENT`)
18
- * - **Domain-specific transformations**: Coalesce item names, currency pairs,
19
- * percentage syntax, etc.
20
- *
21
- * ## Architecture
22
- * Providers register NormalizerRules alongside Parselets and OpCode handlers
23
- * via {@link IEnginePackage.normalizerRules}. The normalizer applies them
24
- * greedily left-to-right in multiple passes with safety limits.
25
- *
26
- * @module TokenNormalizer
27
- */
28
-
29
- /**
30
- * Configuration options for the normalization pass.
31
- *
32
- * These control safety limits and diagnostic callbacks. The defaults
33
- * are chosen to be generous enough for any realistic expression while
34
- * preventing runaway token expansion from recursive rules.
35
- */
36
- interface NormalizerOptions {
37
- /**
38
- * Maximum number of full passes over the token stream before bailing out.
39
- * Prevents infinite loops from recursive rule chains.
40
- * @default 100
41
- */
42
- maxPasses?: number;
43
- /**
44
- * Maximum number of tokens allowed after normalization.
45
- * If exceeded, an Error is thrown rather than passing a bloated stream
46
- * to the parser.
47
- * @default 10000
48
- */
49
- maxTokens?: number;
50
- /**
51
- * Callback invoked for each fusion event during normalization.
52
- * Used by diagnostic mode to populate {@link NormalizerOutput.fusions}.
53
- * When `undefined`, fusions are still tracked internally but no callbacks fire.
54
- */
55
- onFusion?: (fusion: TokenFusion) => void;
56
- }
57
- /**
58
- * Creates a new normalized token from fused source tokens.
59
- *
60
- * The fused token inherits position information (offset, line, column)
61
- * from the first source token, which preserves source-map accuracy
62
- * for error messages and diagnostic highlighting.
63
- *
64
- * @param type - The new token type (e.g., "CARET", "TIMES_BY")
65
- * @param text - The combined text representation (e.g., "to the power of")
66
- * @param sourceTokens - The original tokens being fused (at least 2)
67
- * @returns A new {@link LexerToken} with the fused type and combined text
68
- */
69
- declare function createFusedToken(type: string, text: string, sourceTokens: Token[]): Token;
70
- /**
71
- * Token normalizer: applies {@link NormalizerRule | NormalizerRules} to a token stream.
72
- *
73
- * ## Lifecycle
74
- * 1. **Registration**: Rules are added via {@link register} and sorted by priority
75
- * 2. **Normalization**: {@link normalize} applies rules greedily left-to-right
76
- * 3. **Cleanup**: {@link clear} or {@link unregister} removes rules
77
- *
78
- * ## Normalization algorithm
79
- * The normalizer uses a greedy left-to-right multi-pass algorithm:
80
- * - At each token position, rules are tried in priority order (highest first)
81
- * - When a rule matches, matched tokens are consumed and replaced
82
- * - Processing continues from the replacement position
83
- * - Multiple passes handle cascading matches (one rule's output triggers another)
84
- * - Safety limits ({@link NormalizerOptions.maxPasses}) prevent infinite loops
85
- *
86
- * @example
87
- * ```ts
88
- * const normalizer = new TokenNormalizer();
89
- * normalizer.register(phraseRule); // "to the power of" → CARET
90
- * normalizer.register(implicitMultRule); // "2 x" → "2 * x"
91
- * const normalized = normalizer.normalize(rawTokens);
92
- * ```
93
- */
94
- declare class TokenNormalizer {
95
- /** Registered rules, unsorted, the source of truth. */
96
- private rules;
97
- /**
98
- * Priority-sorted copy of {@link rules}, rebuilt lazily on the next
99
- * {@link normalize} call after a mutation. Rules are registered once at
100
- * engine/package-registration time and essentially never change during a
101
- * session, but normalize() runs on every keystroke-driven evaluation, an
102
- * earlier version re-sorted a fresh copy of `rules` on every single call,
103
- * which meant every keystroke paid for an allocation + sort of a list that
104
- * had usually not changed since the last one. `null` means "stale, rebuild
105
- * on next use"; {@link register}/{@link unregister}/{@link clear} all
106
- * invalidate it.
107
- */
108
- private sortedRulesCache;
109
- /**
110
- * Phrase trie for single-pass multi-word phrase fusion.
111
- * Tried at each token position BEFORE other rules, the trie walk
112
- * is O(depth) vs O(R × W) for separate rule matching.
113
- */
114
- private phraseTrie;
115
- /** Merged options with defaults applied. */
116
- private options;
117
- /**
118
- * @param options - Configuration overrides for safety limits and diagnostic callbacks
119
- */
120
- constructor(options?: NormalizerOptions);
121
- /**
122
- * Register a normalization rule.
123
- *
124
- * Rules are sorted by priority (descending) on each {@link normalize} call.
125
- * Multiple rules can share the same priority, they are tried in registration
126
- * order when priorities are equal.
127
- *
128
- * @param rule - The rule to register
129
- */
130
- register(rule: NormalizerRule): void;
131
- /**
132
- * Unregister a normalization rule by its {@link NormalizerRule.name | name}.
133
- *
134
- * If multiple rules share the same name, all are removed. This is safe to
135
- * call with a name that doesn't match any rule, it simply has no effect.
136
- *
137
- * @param ruleName - The name of the rule to remove
138
- */
139
- unregister(ruleName: string): void;
140
- /**
141
- * Remove all registered rules, resetting the normalizer to its initial state.
142
- * Also clears the phrase trie.
143
- */
144
- clear(): void;
145
- /**
146
- * Priority-sorted view of {@link rules} (descending priority; registration
147
- * order preserved for ties, since {@link Array.prototype.sort} is stable).
148
- * Cached until the next mutation. See {@link sortedRulesCache}.
149
- */
150
- private getSortedRules;
151
- /**
152
- * Get the number of currently registered rules (excludes phrase trie entries).
153
- */
154
- get ruleCount(): number;
155
- /**
156
- * Register a multi-word phrase for fusion into a single compound token.
157
- *
158
- * This is the preferred way to add phrase patterns. It inserts into the
159
- * internal {@link PhraseTrie}, which collapses all phrase rules into a
160
- * single O(depth) trie walk per position, no separate rule scanning.
161
- *
162
- * @param phrase - Multi-word phrase (e.g., "to the power of", "abyssal whip")
163
- * @param tokenType - Target token type after fusion (e.g., "CARET", "ITEM")
164
- */
165
- addPhrase(phrase: string, tokenType: string): void;
166
- /**
167
- * Check whether a word can start any registered phrase.
168
- *
169
- * Used by {@link implicitMultiplyRule} to suppress `*` insertion
170
- * before phrase-starting identifiers (e.g., "2 power of 3" → `2 ^ 3`,
171
- * not `2 * power of 3`). Delegates to {@link PhraseTrie.canStart}.
172
- */
173
- /**
174
- * Get all registered phrases and their target token types.
175
- *
176
- * Exposes the full phrase trie structure for diagnostic rendering
177
- * in the playground's NormalizerTab. Returns ALL registered phrases,
178
- * not just the ones that matched in the last evaluation.
179
- */
180
- getPhrases(): Record<string, string>;
181
- canStartPhrase(word: string): boolean;
182
- /**
183
- * Normalize a token stream by applying all registered rules.
184
- *
185
- * ## Algorithm
186
- * Applies rules greedily left-to-right in multiple passes:
187
- * 1. Sort rules by priority (descending)
188
- * 2. Walk the token stream left to right
189
- * 3. At each position, try rules in priority order
190
- * 4. On match: consume matched tokens, insert replacements, restart from insert point
191
- * 5. On no match: pass token through unchanged
192
- * 6. Repeat until a full pass produces no changes, or maxPasses is reached
193
- *
194
- * ## Fusion tracking
195
- * When a rule consumes more tokens than it produces, the normalizer calls
196
- * `onFusion` with a {@link TokenFusion} record for diagnostic collection.
197
- * This populates {@link NormalizerOutput.fusions} in the playground pipeline view.
198
- *
199
- * ## Safety
200
- * If the normalized token count exceeds {@link NormalizerOptions.maxTokens},
201
- * an Error is thrown to prevent memory exhaustion from runaway rule expansion.
202
- *
203
- * @param tokens - Raw tokens from the lexer
204
- * @param onFusion - Optional fusion callback (overrides {@link NormalizerOptions.onFusion})
205
- * @returns Normalized tokens ready for parsing
206
- * @throws {Error} If the normalized token count exceeds maxTokens
207
- */
208
- normalize(tokens: Token[], onFusion?: (fusion: TokenFusion) => void): Token[];
209
- }
1
+ import { N as NormalizerMatch, a as NormalizerRule } from './TokenNormalizer-CqBCrYkR.cjs';
2
+ export { b as NormalizerOptions, T as TokenFusion, c as TokenNormalizer, d as createFusedToken } from './TokenNormalizer-CqBCrYkR.cjs';
3
+ import { T as Token } from './Token-DnU2hwFY.cjs';
210
4
 
211
5
  /**
212
6
  * PhraseTrie, optimized word-level trie for multi-word phrase fusion.
@@ -361,4 +155,4 @@ declare function createBuiltinNormalizerRules(): NormalizerRule[];
361
155
  */
362
156
  declare const BUILTIN_PHRASES: Record<string, string>;
363
157
 
364
- export { BUILTIN_PHRASES, NormalizerMatch, type NormalizerOptions, NormalizerRule, PhraseTrie, TokenFusion, TokenNormalizer, createBuiltinNormalizerRules, createFusedToken, implicitMultiplyRule };
158
+ export { BUILTIN_PHRASES, NormalizerMatch, NormalizerRule, PhraseTrie, createBuiltinNormalizerRules, implicitMultiplyRule };