@motionscript/code 0.1.0-alpha.9 → 0.1.1-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/README.md +2 -2
  3. package/dist/browser/index.js +8 -8
  4. package/dist/browser/index.js.map +3 -3
  5. package/dist/browser/manifest.json +1 -1
  6. package/dist/code-range.d.ts +7 -47
  7. package/dist/code-range.d.ts.map +1 -1
  8. package/dist/code-range.js +8 -45
  9. package/dist/code-range.js.map +1 -1
  10. package/dist/diff.d.ts +5 -44
  11. package/dist/diff.d.ts.map +1 -1
  12. package/dist/diff.js +22 -69
  13. package/dist/diff.js.map +1 -1
  14. package/dist/engine.d.ts.map +1 -1
  15. package/dist/engine.js +3 -7
  16. package/dist/engine.js.map +1 -1
  17. package/dist/highlight.d.ts +13 -31
  18. package/dist/highlight.d.ts.map +1 -1
  19. package/dist/highlight.js +25 -87
  20. package/dist/highlight.js.map +1 -1
  21. package/dist/layout.d.ts +7 -26
  22. package/dist/layout.d.ts.map +1 -1
  23. package/dist/layout.js +5 -20
  24. package/dist/layout.js.map +1 -1
  25. package/dist/measure-cache.d.ts +5 -27
  26. package/dist/measure-cache.d.ts.map +1 -1
  27. package/dist/measure-cache.js +0 -0
  28. package/dist/measure-cache.js.map +1 -1
  29. package/dist/node.d.ts +4 -11
  30. package/dist/node.d.ts.map +1 -1
  31. package/dist/node.js +18 -41
  32. package/dist/node.js.map +1 -1
  33. package/dist/nodes.d.ts +0 -12
  34. package/dist/nodes.d.ts.map +1 -1
  35. package/dist/nodes.js +0 -12
  36. package/dist/nodes.js.map +1 -1
  37. package/dist/render.d.ts +5 -14
  38. package/dist/render.d.ts.map +1 -1
  39. package/dist/render.js +13 -54
  40. package/dist/render.js.map +1 -1
  41. package/dist/style.d.ts +6 -61
  42. package/dist/style.d.ts.map +1 -1
  43. package/dist/style.js +4 -40
  44. package/dist/style.js.map +1 -1
  45. package/dist/tokens.d.ts +1 -10
  46. package/dist/tokens.d.ts.map +1 -1
  47. package/dist/tokens.js +3 -38
  48. package/dist/tokens.js.map +1 -1
  49. package/dist/transitions.d.ts +16 -68
  50. package/dist/transitions.d.ts.map +1 -1
  51. package/dist/transitions.js +5 -17
  52. package/dist/transitions.js.map +1 -1
  53. package/package.json +4 -4
  54. package/src/code-range.ts +9 -53
  55. package/src/diff.ts +26 -92
  56. package/src/engine.ts +3 -7
  57. package/src/highlight.ts +25 -98
  58. package/src/layout.ts +10 -35
  59. package/src/measure-cache.ts +0 -0
  60. package/src/node.ts +21 -57
  61. package/src/nodes.ts +0 -12
  62. package/src/render.ts +17 -61
  63. package/src/style.ts +9 -64
  64. package/src/tokens.ts +4 -46
  65. package/src/transitions.ts +16 -72
package/src/diff.ts CHANGED
@@ -1,65 +1,32 @@
1
1
  import type { IdLine, IdToken } from "./tokens";
2
2
 
3
- /**
4
- * The result of diffing one listing structure against another: the *next*
5
- * structure, with token and line ids carried over from the previous one
6
- * wherever the content survived, plus the three sets a transition needs to know
7
- * which role each token plays.
8
- *
9
- * Identity is the whole product here. `const variable = 3` becoming
10
- * `const number = 3` should move `const` and `= 3` rather than dissolve and
11
- * rebuild them, and a token can only *move* if the frame after the edit still
12
- * knows it is the same token.
13
- */
14
- /**
15
- * How a `Code` node decides which token became which across an edit. Given
16
- * the old and new tokenized structures, return the carried-over structure
17
- * plus the added/removed/renamed-line bookkeeping a transition needs.
18
- * `diffCode` below is the default implementation (exported as
19
- * `defaultCodeDiff`) — pass a `diffStrategy` of this shape to `<Code>` to
20
- * replace it with your own.
21
- */
3
+ /** Decides which token became which across an edit; `diffCode` (exported as `defaultCodeDiff`) is the default. */
22
4
  export type CodeDiffStrategy = (from: IdLine[], to: IdLine[]) => CodeEdit;
23
5
 
6
+ /** The next structure with ids carried over wherever content survived, so a surviving token moves rather than dissolves and rebuilds. */
24
7
  export interface CodeEdit {
25
- /** The new structure. Ids are rewritten in place onto the tokens handed in. */
8
+ /** The new structure; ids are rewritten in place onto the tokens handed in. */
26
9
  lines: IdLine[];
27
- /** Ids present only in the old structure — these fade out. */
28
10
  removedIds: Set<number>;
29
- /** Ids present only in the new structure — these fade in. */
30
11
  addedIds: Set<number>;
31
- /** Ids of {@link lines} that did not exist before — these enter as a whole row. */
12
+ /** Lines that did not exist before; they enter as a whole row. */
32
13
  newLineIds: Set<number>;
33
14
  /** Previous colour of a surviving token whose highlighting changed, for the cross-fade. */
34
15
  fromColorById: Map<number, string | undefined>;
35
16
  }
36
17
 
37
- /**
38
- * How similar two lines must be before they are treated as *one line that was
39
- * edited* rather than as one line deleted and another inserted.
40
- *
41
- * Below this, a row's tokens have so little in common that carrying ids across
42
- * would animate glyphs sliding between unrelated words. Above it, the edit reads
43
- * the way it was made.
44
- */
18
+ /** Below this similarity two lines are one deleted and one inserted, not one edited: carrying ids across would slide glyphs between unrelated words. */
45
19
  const LINE_MATCH_FLOOR = 0.45;
46
20
 
47
21
  /**
48
- * The most line pairs one run of changed lines is scored against each other, and
49
- * the most across a whole edit.
50
- *
51
- * Scoring every pair is quadratic: with 50-character lines it took 31 ms at 200
52
- * lines and 741 ms at 1,000, paid again for each edit command when it is built.
53
- * So the lines are first paired where they cannot be in doubt (see
54
- * {@link alignLines}), and only a run left between two such anchors is scored.
55
- * A run past the first limit, or what is left of the second, is not compared: its
56
- * lines leave and enter as whole rows. That is the budget, and an edit that stays
57
- * inside it is diffed exactly as before.
22
+ * Most line pairs scored in one run of changed lines, and across a whole edit.
23
+ * Scoring every pair is quadratic (741 ms at 1,000 lines), so only a run between
24
+ * two anchors (see {@link alignLines}) is scored; past the budget its lines leave
25
+ * and enter as whole rows.
58
26
  */
59
- export const LINE_ALIGN_CELLS = 40_000;
60
- export const EDIT_ALIGN_CELLS = 160_000;
27
+ const LINE_ALIGN_CELLS = 40_000;
28
+ const EDIT_ALIGN_CELLS = 160_000;
61
29
 
62
- /** Weight a token carries when judging similarity: its visible characters. */
63
30
  function tokenWeight(tok: IdToken): number {
64
31
  return tok.content.trim().length;
65
32
  }
@@ -71,12 +38,9 @@ function lineWeight(line: IdLine): number {
71
38
  }
72
39
 
73
40
  /**
74
- * Dice coefficient over the two lines' token multisets, weighted by visible
75
- * characters so `variable` counts for far more than a run of spaces.
76
- *
77
- * Two blank lines are identical (1); a blank line against a non-blank one shares
78
- * nothing (0), which keeps the blank rows a reformat introduces from anchoring
79
- * themselves onto real code.
41
+ * Dice coefficient over the lines' token multisets, weighted by visible characters.
42
+ * Two blank lines are identical (1) but a blank against a non-blank shares nothing
43
+ * (0), so blank rows a reformat introduces never anchor onto real code.
80
44
  */
81
45
  function similarity(a: IdLine, b: IdLine): number {
82
46
  const wa = lineWeight(a);
@@ -103,22 +67,15 @@ function similarity(a: IdLine, b: IdLine): number {
103
67
  }
104
68
 
105
69
  /**
106
- * Align two sequences by maximising total `score`, skipping is free.
107
- *
108
- * A plain LCS would only ever pair *identical* elements, which is the wrong tool
109
- * twice over: two lines that differ by one word are the same line, and two
110
- * tokens that differ only in colour are the same token. Scoring the pairing and
111
- * letting anything that scores ≤ 0 fall out expresses that directly, and because
112
- * skips cost nothing the alignment stays in order.
113
- *
114
- * Returns the matched index pairs, in order.
70
+ * Matched index pairs, in order, maximising total `score`; a pair scoring <= 0 is
71
+ * skipped. An LCS would pair only identical elements, but two lines that differ by
72
+ * one word are the same line.
115
73
  */
116
74
  function align(
117
75
  aLength: number,
118
76
  bLength: number,
119
77
  score: (i: number, j: number) => number,
120
78
  ): Array<[number, number]> {
121
- // dp[i][j] = best score aligning a[0..i) with b[0..j).
122
79
  const width = bLength + 1;
123
80
  const dp = new Float64Array((aLength + 1) * width);
124
81
  const pairScore = new Float64Array(aLength * bLength);
@@ -156,19 +113,11 @@ function align(
156
113
  return pairs;
157
114
  }
158
115
 
159
- /**
160
- * Match the tokens of one line against another's.
161
- *
162
- * Scored by visible characters rather than token count, so an alignment that
163
- * keeps `items.reduce` together beats one that keeps four more spaces. A token
164
- * pairs only when its content is unchanged — a changed word is a different word,
165
- * and should be seen to be replaced.
166
- */
116
+ /** Scored by visible characters, so keeping `items.reduce` beats keeping four spaces; a changed word is a different token. */
167
117
  function alignTokens(from: IdToken[], to: IdToken[]): Array<[number, number]> {
168
118
  return align(from.length, to.length, (i, j) => {
169
119
  if (from[i].content !== to[j].content) return 0;
170
- // Whitespace still pairs (it holds indentation together), just weakly
171
- // enough that it never outranks real content.
120
+ // Whitespace pairs weakly: it holds indentation together but never outranks content.
172
121
  return tokenWeight(to[j]) || 0.25;
173
122
  });
174
123
  }
@@ -179,7 +128,7 @@ function sameTokens(a: IdLine, b: IdLine): boolean {
179
128
  return true;
180
129
  }
181
130
 
182
- /** A line's text, token boundaries included, for telling identical lines apart from similar ones. */
131
+ /** Text with token boundaries, so identical lines tell apart from similar ones. */
183
132
  function signature(line: IdLine): string {
184
133
  let out = "";
185
134
  for (const tok of line.tokens) out += tok.content + "\u0001";
@@ -187,14 +136,10 @@ function signature(line: IdLine): string {
187
136
  }
188
137
 
189
138
  /**
190
- * Pair the lines of two listings, in order.
191
- *
192
- * Lines that are the same at the start and at the end are paired outright. In
193
- * what is left, a line that occurs exactly once on each side is an anchor, and
194
- * the longest run of anchors that keeps their order splits the rest into runs of
195
- * changed lines, each scored against itself within the budget (see
196
- * {@link LINE_ALIGN_CELLS}). A one-line edit to a long listing therefore never
197
- * builds a table bigger than the lines around it.
139
+ * Pairs lines in order: identical head and tail outright, then the longest ordered
140
+ * run of lines occurring once on each side as anchors, with each run between
141
+ * anchors scored within the budget. A one-line edit to a long listing therefore
142
+ * never builds a table bigger than the lines around it.
198
143
  */
199
144
  function alignLines(from: IdLine[], to: IdLine[]): Array<[number, number]> {
200
145
  const fromSig = from.map(signature);
@@ -234,11 +179,7 @@ function alignLines(from: IdLine[], to: IdLine[]): Array<[number, number]> {
234
179
  return pairs;
235
180
  }
236
181
 
237
- /**
238
- * The lines that occur once in `from[fa, fb)` and once in `to[ta, tb)`, reduced to
239
- * the longest subsequence that is increasing on both sides (patience sorting).
240
- * Blank lines are never anchors: they say nothing about where they belong.
241
- */
182
+ /** Lines occurring once on each side, reduced to the longest subsequence increasing on both (patience sorting); blanks never anchor. */
242
183
  function anchorLines(
243
184
  fromSig: string[],
244
185
  toSig: string[],
@@ -264,7 +205,6 @@ function anchorLines(
264
205
  seenTo.add(sig);
265
206
  }
266
207
 
267
- // In `to` order, each candidate carries its `from` position.
268
208
  const candidates: Array<[number, number]> = [];
269
209
  for (let j = ta; j < tb; j++) {
270
210
  const i = fromAt.get(toSig[j]);
@@ -291,13 +231,7 @@ function anchorLines(
291
231
  return chain.reverse();
292
232
  }
293
233
 
294
- /**
295
- * Carry `from`'s identities onto `to`, and report what changed.
296
- *
297
- * `to` is freshly tokenized and owns ids nobody has seen yet, so rewriting them
298
- * in place is safe and is what makes the transition cheap: after this, the two
299
- * structures share an id for every token that survived the edit.
300
- */
234
+ /** Carries `from`'s ids onto `to`; `to` is freshly tokenized, so rewriting its ids in place is safe. */
301
235
  export function diffCode(from: IdLine[], to: IdLine[]): CodeEdit {
302
236
  const removedIds = new Set<number>();
303
237
  const addedIds = new Set<number>();
package/src/engine.ts CHANGED
@@ -1,10 +1,6 @@
1
- // The Code "engine": syntax highlighting, tokenizing, layout, diffing, and
2
- // transition/phase scheduling — kept separate from node.ts so a copy of the node
3
- // can take just its declaration and import this behind
4
- // `@motionscript/code` instead. What gets copied is whatever this
5
- // package's registry.json declares, which for this component is node.ts;
6
- // src/tests/registry.test.ts checks that anything it
7
- // reaches for is either copied alongside it or re-exported from here.
1
+ // Everything node.ts imports besides its own declaration, so a copy of the node can take
2
+ // just node.ts and import this from `@motionscript/code`. registry.test.ts checks that
3
+ // whatever node.ts reaches for is copied alongside it or re-exported from here.
8
4
  export * from './transitions';
9
5
  export * from './layout';
10
6
  export { drawCode } from './render';
package/src/highlight.ts CHANGED
@@ -8,22 +8,7 @@ import {
8
8
  compileStyle,
9
9
  } from "./style";
10
10
 
11
- /**
12
- * Lezer-backed syntax highlighting for the Code component, replacing the former
13
- * Shiki wiring. This mirrors Motion Canvas's `LezerHighlighter`:
14
- *
15
- * - {@link prepareHighlight} parses code once and builds a `from:to` → color map
16
- * by walking {@link highlightTree}.
17
- * - {@link tokenizeCode} splits code at parse-tree leaf boundaries.
18
- * - {@link highlightAt} resolves the color (and run length) for a character index.
19
- *
20
- * Parsers load lazily and asynchronously (dynamic import) so the bundle doesn't
21
- * pull in every grammar. The node's asset loader awaits {@link ensureHighlighter}
22
- * before tokenizing; until a parser resolves, callers fall back to plain text and
23
- * re-tokenize once {@link canHighlight} returns true.
24
- */
25
-
26
- /** Dynamic-import factories for each supported language → a Lezer {@link Parser}. */
11
+ // Dynamic imports keep the bundle from pulling in every grammar.
27
12
  const LANGUAGE_LOADERS: Record<string, () => Promise<Parser>> = {
28
13
  typescript: async () => (await import("@lezer/javascript")).parser.configure({ dialect: "ts" }),
29
14
  tsx: async () => (await import("@lezer/javascript")).parser.configure({ dialect: "ts jsx" }),
@@ -43,7 +28,6 @@ const LANGUAGE_LOADERS: Record<string, () => Promise<Parser>> = {
43
28
  yaml: async () => (await import("@lezer/yaml")).parser,
44
29
  };
45
30
 
46
- // Aliases so common language strings resolve to a loader above.
47
31
  const LANGUAGE_ALIASES: Record<string, string> = {
48
32
  ts: "typescript",
49
33
  js: "javascript",
@@ -81,17 +65,12 @@ function resolveLanguageKey(language: string): string {
81
65
  return LANGUAGE_ALIASES[key] ?? key;
82
66
  }
83
67
 
84
- // Loaded parsers and in-flight loads, keyed by resolved language key, so
85
- // concurrent Code nodes asking for the same language share one dynamic import.
68
+ // Keyed by resolved language, so concurrent nodes share one import.
86
69
  const loadedParsers = new Map<string, Parser>();
87
70
  const inFlightLangs = new Map<string, Promise<Parser | null>>();
88
71
 
89
- export const DEFAULT_THEMES = [DefaultHighlightStyle.name];
90
72
  export const DEFAULT_LANGS = ["typescript", "javascript", "json", "python"];
91
73
 
92
- // Theme registry, seeded with the built-ins (Motion Canvas default + GitHub
93
- // dark/light). The `theme` prop is a string name or a CodeHighlightStyle object;
94
- // apps can register additional styles by name and reference them by that name.
95
74
  const themesByName = new Map<string, CodeHighlightStyle>(Object.entries(BUILTIN_THEMES));
96
75
 
97
76
  /** Register a custom {@link CodeHighlightStyle} so Code nodes can reference it by name. */
@@ -99,11 +78,7 @@ export function registerCodeTheme(style: CodeHighlightStyle): void {
99
78
  themesByName.set(style.name, style);
100
79
  }
101
80
 
102
- /**
103
- * Resolve a theme reference (a registered name, or a CodeHighlightStyle object)
104
- * to a concrete style. Unknown names fall back to the Motion Canvas default so a
105
- * stray theme string never throws, with one warning per name.
106
- */
81
+ /** An unknown theme name falls back to the default style with one warning, never a throw. */
107
82
  export function resolveTheme(theme: CodeTheme): CodeHighlightStyle {
108
83
  if (typeof theme !== "string") return theme;
109
84
  const style = themesByName.get(theme);
@@ -116,24 +91,13 @@ export function resolveTheme(theme: CodeTheme): CodeHighlightStyle {
116
91
  return DefaultHighlightStyle;
117
92
  }
118
93
 
119
- /** Has `language` finished loading its parser? */
120
- function isLanguageLoaded(language: string): boolean {
121
- return loadedParsers.has(resolveLanguageKey(language));
122
- }
123
-
124
94
  /**
125
- * Load the parsers for `langs` (themes are synchronous, so they're a no-op here
126
- * but kept in the signature for API parity with the former Shiki implementation).
127
- * Resolves once every requested language's parser is available. Unknown languages
128
- * resolve without loading anything — {@link canHighlight} stays false for them and
129
- * the node renders them as plain text, with one warning per language. A grammar
130
- * whose import is rejected is reported the same way and is tried again by the
131
- * next call.
95
+ * Resolves once every requested language's parser is loaded. An unknown language
96
+ * resolves without loading (`canHighlight` stays false and it draws as plain
97
+ * text); a grammar whose import rejects is reported the same way and retried by
98
+ * the next call. Each warns once.
132
99
  */
133
- export function ensureHighlighter(
134
- _themes: string[] = DEFAULT_THEMES,
135
- langs: string[] = DEFAULT_LANGS,
136
- ): Promise<void> {
100
+ export function ensureHighlighter(langs: string[] = DEFAULT_LANGS): Promise<void> {
137
101
  const jobs: Promise<unknown>[] = [];
138
102
 
139
103
  for (const lang of langs) {
@@ -142,7 +106,6 @@ export function ensureHighlighter(
142
106
 
143
107
  const loader = LANGUAGE_LOADERS[key];
144
108
  if (!loader) {
145
- // Unsupported language; leave it unhighlighted, and say so.
146
109
  if (!PLAIN_LANGUAGES.has(key)) {
147
110
  warnOnce(
148
111
  "language",
@@ -177,33 +140,22 @@ export function ensureHighlighter(
177
140
  return jobs.length === 0 ? Promise.resolve() : Promise.all(jobs).then(() => undefined);
178
141
  }
179
142
 
180
- /**
181
- * Load a syntax-highlight language into the shared highlighter. Exposed so a Code
182
- * node's asset loader can run it on the timeline, and so apps can preload
183
- * languages explicitly. Returns once the parser is ready (or immediately for an
184
- * unsupported language). The `theme` arg is accepted for backwards compatibility.
185
- */
186
- export async function loadCodeLanguage(language: string, _theme?: string): Promise<void> {
187
- await ensureHighlighter(DEFAULT_THEMES, [language]);
143
+ /** Loads one language; resolves at once for an unsupported one. */
144
+ export async function loadCodeLanguage(language: string): Promise<void> {
145
+ await ensureHighlighter([language]);
188
146
  }
189
147
 
190
- export async function initSyntaxHighlighter(
191
- themes: string[] = DEFAULT_THEMES,
192
- langs: string[] = DEFAULT_LANGS,
193
- ): Promise<void> {
194
- await ensureHighlighter(themes, langs);
148
+ export async function initSyntaxHighlighter(langs: string[] = DEFAULT_LANGS): Promise<void> {
149
+ await ensureHighlighter(langs);
195
150
  }
196
151
 
197
- /** True once `language` can be tokenized (its parser has loaded). */
198
- export function canHighlight(language: string, _theme?: CodeTheme): boolean {
199
- return isLanguageLoaded(language);
152
+ /** True once `language`'s parser has loaded. */
153
+ export function canHighlight(language: string): boolean {
154
+ return loadedParsers.has(resolveLanguageKey(language));
200
155
  }
201
156
 
202
- /** A parsed-and-prepared highlight cache for one snapshot of code. */
203
157
  interface HighlightCache {
204
158
  tree: Tree;
205
- code: string;
206
- // `${from}:${to}` node id → color, exactly as Motion Canvas keys it.
207
159
  colorLookup: Map<string, string>;
208
160
  }
209
161
 
@@ -211,12 +163,7 @@ function nodeId(from: number, to: number): string {
211
163
  return `${from}:${to}`;
212
164
  }
213
165
 
214
- /**
215
- * Parse `code` and build the per-node color map. Mirrors Motion Canvas's
216
- * `LezerHighlighter.prepare`: for each highlighted range, walk the subtree and
217
- * record the color against every contained node's `from:to` id, so a later
218
- * {@link highlightAt} lookup by `resolveInner` index finds it.
219
- */
166
+ /** Maps every node inside a highlighted range to its color by `from:to` id, for {@link highlightAt}. */
220
167
  function prepareHighlight(code: string, language: string, style: CodeHighlightStyle): HighlightCache | null {
221
168
  const parser = loadedParsers.get(resolveLanguageKey(language));
222
169
  if (!parser) return null;
@@ -226,8 +173,7 @@ function prepareHighlight(code: string, language: string, style: CodeHighlightSt
226
173
  const colorLookup = new Map<string, string>();
227
174
 
228
175
  highlightTree(tree, highlighter, (from, to, classes) => {
229
- // tagHighlighter emits our color string as the "class".
230
- const color = classes;
176
+ const color = classes; // the tag highlighter emits the color as the "class"
231
177
  if (!color) return;
232
178
  const cursor = tree.cursorAt(from, 1);
233
179
  do {
@@ -235,32 +181,22 @@ function prepareHighlight(code: string, language: string, style: CodeHighlightSt
235
181
  } while (cursor.next() && cursor.to <= to);
236
182
  });
237
183
 
238
- return { tree, code, colorLookup };
184
+ return { tree, colorLookup };
239
185
  }
240
186
 
241
- /**
242
- * Resolve the color and run length for the token starting at `index`. Mirrors
243
- * Motion Canvas's `LezerHighlighter.highlight`: resolve the innermost node, look
244
- * up its color, and report how far the run extends (`skipAhead`).
245
- */
187
+ /** The color and run length (`skipAhead`) of the innermost node at `index`. */
246
188
  function highlightAt(index: number, cache: HighlightCache): { color: string | null; skipAhead: number } {
247
189
  const node = cache.tree.resolveInner(index, 1);
248
190
  const color = cache.colorLookup.get(nodeId(node.from, node.to));
249
191
  if (color) {
250
192
  return { color, skipAhead: node.to - index };
251
193
  }
252
- // No color: skip past a childless (leaf) node so we don't re-resolve every char.
194
+ // Uncolored: skip a whole leaf rather than re-resolving every character.
253
195
  const skipAhead = node.firstChild ? 0 : node.to - index;
254
196
  return { color: null, skipAhead };
255
197
  }
256
198
 
257
- /**
258
- * Split `code` into structural tokens at parse-tree leaf boundaries, including the
259
- * gaps between leaves (whitespace/punctuation). Mirrors Motion Canvas's
260
- * `LezerHighlighter.tokenize`; this segmentation is what the token-level
261
- * append/insert/erase/replace animations diff against, so matching it keeps the
262
- * animation behavior identical to Motion Canvas.
263
- */
199
+ /** Splits at parse-tree leaf boundaries, gaps between leaves included; the edit animations diff against this segmentation. */
264
200
  function tokenizeCode(code: string, language: string): string[] {
265
201
  const parser = loadedParsers.get(resolveLanguageKey(language));
266
202
  if (!parser) return [code];
@@ -283,13 +219,8 @@ function tokenizeCode(code: string, language: string): string[] {
283
219
  }
284
220
 
285
221
  /**
286
- * Tokenize `code` into per-line, colored tokens — the contract the rest of the
287
- * Code component consumes. Combines {@link tokenizeCode} (structural split) with
288
- * {@link highlightAt} (per-run color), then breaks tokens at newlines so each
289
- * output line is a row of single-line, single-color tokens.
290
- *
291
- * Callers must have checked {@link canHighlight} first; this returns a single
292
- * uncolored line per source line if the parser isn't loaded.
222
+ * Per-line rows of single-line, single-color tokens. Without a loaded parser it
223
+ * returns one uncolored token per source line, so callers check {@link canHighlight}.
293
224
  */
294
225
  export function highlightToTokens(
295
226
  code: string,
@@ -304,19 +235,15 @@ export function highlightToTokens(
304
235
  const pushLine = () => lines.push([]);
305
236
 
306
237
  if (!cache) {
307
- // Parser unavailable: one uncolored token per source line.
308
238
  return code.split("\n").map(line => [{ content: line, color: style.defaultColor }]);
309
239
  }
310
240
 
311
- // Walk the structural tokens, coloring each by its starting index and
312
- // splitting on newlines so no token spans rows.
313
241
  let index = 0;
314
242
  for (const token of tokenizeCode(code, language)) {
315
243
  let offset = 0;
316
244
  while (offset < token.length) {
317
245
  const { color, skipAhead } = highlightAt(index + offset, cache);
318
- // Extend the run as far as the highlight says, but never past this
319
- // structural token and never across a newline (lines are drawn rows).
246
+ // A run never passes its structural token or a newline.
320
247
  let runEnd = offset + Math.max(skipAhead, 1);
321
248
  if (runEnd > token.length) runEnd = token.length;
322
249
  const newlineRel = token.indexOf("\n", offset);
package/src/layout.ts CHANGED
@@ -19,7 +19,7 @@ export interface CodeMetrics {
19
19
  lineNumberGap: number;
20
20
  }
21
21
 
22
- /** Where one token sits: its LEFT edge, and the vertical CENTRE of its line's slot. */
22
+ /** A token's LEFT edge and the vertical CENTRE of its line's slot. */
23
23
  export interface TokenBox {
24
24
  x: number;
25
25
  y: number;
@@ -37,38 +37,25 @@ export interface TokenBox {
37
37
  * the left margin springing apart.
38
38
  */
39
39
  export interface CodeLayout {
40
- /** Token id → placement. */
41
40
  tokens: Map<number, TokenBox>;
42
- /** Vertical centre of each line's slot, by line index. */
41
+ /** By line index. */
43
42
  lineY: number[];
44
- /**
45
- * Width of each line's ink, by line index — the sum of its tokens' advances,
46
- * so a blank line is `0` and a short line is short.
47
- *
48
- * Returned rather than kept local because it is the only thing that knows a
49
- * listing is **ragged**: `innerW` is the longest line, and everything that
50
- * asks where the text actually is (hit testing, for one) would otherwise have
51
- * to treat the block as a rectangle the width of its longest row.
52
- */
43
+ /** Ink width per line (blank is 0): the only record that a listing is ragged, which hit testing needs. */
53
44
  lineW: number[];
54
- /** Line id → its index in this structure (i.e. its line *number* minus one). */
55
45
  lineIndex: Map<number, number>;
56
46
  gutter: number;
57
47
  gutterGap: number;
58
48
  /** Content size, no padding. */
59
49
  innerW: number;
60
50
  innerH: number;
61
- /** Content size plus padding — what the block draws and measures as. */
51
+ /** Content plus padding. */
62
52
  blockW: number;
63
53
  blockH: number;
64
- /** Left edge of the code column (after padding and gutter). */
54
+ /** Left edge of the code column, after padding and gutter. */
65
55
  startX: number;
66
56
  }
67
57
 
68
- /**
69
- * Signature of everything a layout depends on other than the token structure
70
- * itself. A cached layout survives exactly as long as this is unchanged.
71
- */
58
+ /** Everything a layout depends on besides the token structure; a cached layout lives while it is unchanged. */
72
59
  export function metricsSignature(m: CodeMetrics): string {
73
60
  const p = m.padding;
74
61
  return [
@@ -78,16 +65,12 @@ export function metricsSignature(m: CodeMetrics): string {
78
65
  ].join("|");
79
66
  }
80
67
 
81
- /**
82
- * Horizontal gap between the line-number column and the code text. Sized in
83
- * space-widths so it scales with the font, and measured with letterSpacing 0 —
84
- * line numbers and the gap don't carry the code's letter-spacing.
85
- */
68
+ /** In space-widths, measured with letterSpacing 0: line numbers and the gap don't carry the code's. */
86
69
  function gutterGapOf(scope: MeasureContext2D | RenderContext2D, m: CodeMetrics, cache: TokenAdvanceCache): number {
87
70
  return cache.advance(scope, " ", m.fontSize, m.fontFamily, 0) * m.lineNumberGap;
88
71
  }
89
72
 
90
- /** Width of the line-number column for a structure of `lineCount` lines, gap included. */
73
+ /** Gap included. */
91
74
  function gutterOf(
92
75
  scope: MeasureContext2D | RenderContext2D,
93
76
  m: CodeMetrics,
@@ -100,13 +83,7 @@ function gutterOf(
100
83
  return cache.advance(scope, sample, m.fontSize, m.fontFamily, 0) + gap;
101
84
  }
102
85
 
103
- /**
104
- * Place every token of `lines`.
105
- *
106
- * Pure in (lines, metrics, font measurements), which is what lets a caller cache
107
- * one per structure rather than recomputing it per frame — a transition's two
108
- * endpoints are both fixed for its whole duration.
109
- */
86
+ /** Pure in (lines, metrics, measurements), so a caller caches one per structure. */
110
87
  export function layoutCode(
111
88
  lines: IdLine[],
112
89
  m: CodeMetrics,
@@ -133,9 +110,7 @@ export function layoutCode(
133
110
  const blockW = innerW + m.padding.left + m.padding.right;
134
111
  const blockH = innerH + m.padding.top + m.padding.bottom;
135
112
  const startX = -blockW / 2 + m.padding.left + gutter;
136
- // y-up author space: the first line sits at the TOP of the block, so the
137
- // cursor starts at the top edge (+half height, minus top padding) and steps
138
- // DOWNWARD by subtracting a line height per row.
113
+ // y-up: the first line sits at the TOP, and each row steps down by a line height.
139
114
  const startY = blockH / 2 - m.padding.top;
140
115
 
141
116
  const tokens = new Map<number, TokenBox>();
Binary file