@mrhenry/twig-tokenizer 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,8 @@
1
+ # Changelog
2
+
3
+ ## 0.1.2 (2026-09-26)
4
+
5
+ * Reuse a single `TextEncoder` for UTF-8 byte counts (`Source#getColumn`) and
6
+ for `SyntaxError#addSuggestions` instead of allocating one per call.
7
+ * `byteLevenshtein` now uses two rolling rows instead of a full matrix, so the
8
+ "Did you mean …?" suggestions no longer allocate a matrix per candidate.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrhenry/twig-tokenizer",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
package/src/errors.js CHANGED
@@ -194,18 +194,24 @@ export class SyntaxError extends TwigError {
194
194
  }
195
195
  }
196
196
 
197
+ /** Shared UTF-8 encoder: `TextEncoder` is stateless, so one instance serves all calls. */
198
+ const UTF8_ENCODER = new TextEncoder();
199
+
197
200
  /**
198
201
  * @param {string} value
199
202
  * @returns {Uint8Array} The UTF-8 bytes of the string.
200
203
  */
201
204
  function toBytes(value) {
202
- return new TextEncoder().encode(value);
205
+ return UTF8_ENCODER.encode(value);
203
206
  }
204
207
 
205
208
  /**
206
209
  * The Levenshtein distance between two byte sequences (mirrors PHP
207
210
  * `levenshtein()`, which operates on bytes).
208
211
  *
212
+ * Uses two rolling rows instead of a full matrix, so the working set stays
213
+ * O(min(a, b)) for the many candidate strings `addSuggestions` compares.
214
+ *
209
215
  * @param {Uint8Array} a
210
216
  * @param {Uint8Array} b
211
217
  * @returns {number}
@@ -217,24 +223,33 @@ function byteLevenshtein(a, b) {
217
223
  if (b.length === 0) {
218
224
  return a.length;
219
225
  }
220
- const matrix = new Uint16Array((a.length + 1) * (b.length + 1));
221
- for (let i = 0; i <= a.length; i++) {
222
- matrix[i * (b.length + 1)] = i;
226
+ // iterate over the shorter sequence so the rows are as small as possible
227
+ if (a.length > b.length) {
228
+ const swap = a;
229
+ a = b;
230
+ b = swap;
223
231
  }
224
- for (let j = 0; j <= b.length; j++) {
225
- matrix[j] = j;
232
+ const width = b.length;
233
+ let previous = new Uint16Array(width + 1);
234
+ let current = new Uint16Array(width + 1);
235
+ for (let j = 0; j <= width; j++) {
236
+ previous[j] = j;
226
237
  }
227
238
  for (let i = 1; i <= a.length; i++) {
228
- for (let j = 1; j <= b.length; j++) {
229
- const cost = a[i - 1] === b[j - 1] ? 0 : 1;
230
- matrix[i * (b.length + 1) + j] = Math.min(
231
- matrix[(i - 1) * (b.length + 1) + j] + 1,
232
- matrix[i * (b.length + 1) + j - 1] + 1,
233
- matrix[(i - 1) * (b.length + 1) + j - 1] + cost,
234
- );
239
+ current[0] = i;
240
+ const aByte = a[i - 1];
241
+ for (let j = 1; j <= width; j++) {
242
+ const cost = aByte === b[j - 1] ? 0 : 1;
243
+ const deletion = previous[j] + 1;
244
+ const insertion = current[j - 1] + 1;
245
+ const substitution = previous[j - 1] + cost;
246
+ current[j] = deletion < insertion ? (deletion < substitution ? deletion : substitution) : insertion < substitution ? insertion : substitution;
235
247
  }
248
+ const swap = previous;
249
+ previous = current;
250
+ current = swap;
236
251
  }
237
- return matrix[a.length * (b.length + 1) + b.length];
252
+ return previous[width];
238
253
  }
239
254
 
240
255
  /**
package/src/lexer.js CHANGED
@@ -95,7 +95,7 @@ export class Source {
95
95
  const before = this.code.slice(0, offset).replace(/\r\n|\r/g, '\n');
96
96
  const lineStart = before.lastIndexOf('\n');
97
97
  const prefix = lineStart === -1 ? before : before.slice(lineStart + 1);
98
- return new TextEncoder().encode(prefix).length + 1;
98
+ return UTF8_ENCODER.encode(prefix).length + 1;
99
99
  }
100
100
  }
101
101
 
@@ -187,6 +187,9 @@ const CLOSING_BRACKETS = ['}', ')', ']'];
187
187
  /** Default punctuation characters. */
188
188
  const PUNCTUATION = '()[]{}?:.,|';
189
189
 
190
+ /** Shared UTF-8 encoder: `TextEncoder` is stateless, so one instance serves all calls. */
191
+ const UTF8_ENCODER = new TextEncoder();
192
+
190
193
  /**
191
194
  * Whether a code unit is one of the default punctuation characters.
192
195
  *