@motionscript/code 0.1.0 → 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 +19 -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/node.ts CHANGED
@@ -52,11 +52,7 @@ import {
52
52
  const ROW_BAND_COLOR = "rgba(255, 255, 255, 0.12)";
53
53
  const ROW_BAND_RADIUS = 6;
54
54
 
55
- /**
56
- * Which token became which across an edit, and when each part of it happens.
57
- * Arguments of {@link defineCode}: a strategy is a function, which a prop would
58
- * take for a binding, so it is part of the component's definition instead.
59
- */
55
+ /** Arguments of {@link defineCode}: a strategy is a function, which a prop would take for a binding, so it belongs to the definition. */
60
56
  export interface CodeStrategies {
61
57
  /** Decides which token became which across an edit. Defaults to `defaultCodeDiff`. */
62
58
  diffStrategy?: CodeDiffStrategy;
@@ -123,12 +119,9 @@ const TARGET = Types.custom<CodeTarget | undefined>(undefined, { kind: "selector
123
119
  const TEXT = Types.text("", { multiline: true });
124
120
 
125
121
  /**
126
- * A `Code` whose edits are paired and timed by `strategies`. {@link Code} is
127
- * the one with the defaults; a project wanting another hands its own to the
128
- * engine in its place (the key is the same).
129
- *
130
- * A listing and nothing else: no paint slots, and no box behind the tokens. A
131
- * listing set on a card is a `Code` inside a hugging `Rect`.
122
+ * A `Code` whose edits are paired and timed by `strategies`; {@link Code} has the
123
+ * defaults, and a project wanting another hands its own to the engine (same key).
124
+ * A listing only, with no box behind it: a card is a `Code` inside a hugging `Rect`.
132
125
  */
133
126
  export function defineCode(strategies: CodeStrategies = {}) {
134
127
  const kit: Kit = {
@@ -304,8 +297,6 @@ export function findFirstRange(source: CodeSource, text: string): CodeRange | nu
304
297
  return findRangeAt(source, text, 0);
305
298
  }
306
299
 
307
- // ── What a listing shows ────────────────────────────────────────────────────
308
-
309
300
  interface Kit {
310
301
  readonly diff: CodeDiffStrategy;
311
302
  readonly phases: CodePhaseStrategy;
@@ -318,7 +309,7 @@ interface Edited {
318
309
  readonly listing: CodeListing;
319
310
  }
320
311
 
321
- /** What the structure resolves from. A command's snapshot and a node's own state view both are one. */
312
+ /** What the structure resolves from; a command's snapshot and a node's state view both qualify. */
322
313
  interface ListingState {
323
314
  readonly code: CodeSource;
324
315
  readonly language: string;
@@ -350,10 +341,7 @@ interface Shown {
350
341
  readonly progress: number;
351
342
  }
352
343
 
353
- /**
354
- * A `to({ code })` mid-flight comes first: it wrote `code` after any edit
355
- * command did. Then an edit command in flight, then the settled listing.
356
- */
344
+ /** A `to({ code })` mid-flight wins, as it wrote `code` after any edit command; then an edit in flight, then the settled listing. */
357
345
  function shown(s: ListingState, kit: Kit): Shown {
358
346
  const code = s.code;
359
347
  if (typeof code !== "string") {
@@ -375,10 +363,9 @@ function current(s: ListingState, kit: Kit): CodeListing {
375
363
  }
376
364
 
377
365
  /**
378
- * The structure `source` settles on. Until a listing command has acted, the
379
- * props are all there is; after one, it is the listing that command left,
380
- * recoloured keeping its ids when only the theme moved, and diffed onward
381
- * when the source has (a `to({ code })` that landed, a binding).
366
+ * The structure `source` settles on: from the props until a listing command has
367
+ * acted, then the listing it left, recoloured (ids kept) when only the theme moved
368
+ * and diffed onward when the source has.
382
369
  */
383
370
  function settled(s: ListingState, source: string, kit: Kit): CodeListing {
384
371
  const basis = basisOf(s);
@@ -391,7 +378,7 @@ function settled(s: ListingState, source: string, kit: Kit): CodeListing {
391
378
  }
392
379
 
393
380
  function basisOf(s: { readonly language: string; readonly theme: CodeTheme }): CodeTokenBasis {
394
- return { language: s.language, theme: s.theme, highlighted: canHighlight(s.language, s.theme) };
381
+ return { language: s.language, theme: s.theme, highlighted: canHighlight(s.language) };
395
382
  }
396
383
 
397
384
  function sameBasis(a: CodeTokenBasis, b: CodeTokenBasis): boolean {
@@ -403,10 +390,7 @@ function sourceText(code: CodeSource): string {
403
390
  return typeof code === "string" ? code : code.to;
404
391
  }
405
392
 
406
- /**
407
- * Mid-tween `code` holds both sources, so the frame can draw the edit between
408
- * them; at either end it is a source again.
409
- */
393
+ /** Mid-tween `code` holds both sources so the frame can draw the edit between them; at either end it is a source again. */
410
394
  function blendSource(from: CodeSource, to: CodeSource, t: number): CodeSource {
411
395
  const leaving = sourceText(from);
412
396
  const arriving = sourceText(to);
@@ -468,7 +452,7 @@ function buildEdit(kit: Kit, from: IdLine[], next: string, basis: CodeTokenBasis
468
452
 
469
453
  function tokenize(source: string, basis: CodeTokenBasis): IdLine[] {
470
454
  // Best effort outside the asset pipeline (a test, a headless measure): on the timeline `load` has the grammar in first.
471
- if (!basis.highlighted) ensureHighlighter(undefined, [basis.language]).catch(() => { });
455
+ if (!basis.highlighted) ensureHighlighter([basis.language]).catch(() => { });
472
456
  return tokenizeCodeToIdLines(source, basis.language, basis.theme);
473
457
  }
474
458
 
@@ -517,8 +501,6 @@ function adoptIds(from: IdLine[], to: IdLine[]): void {
517
501
  }
518
502
  }
519
503
 
520
- // ── Commands ────────────────────────────────────────────────────────────────
521
-
522
504
  /** What a listing command writes at a point of its own eased time. */
523
505
  interface ListingWrites {
524
506
  readonly code?: string;
@@ -530,11 +512,7 @@ interface ListingWrites {
530
512
 
531
513
  type Frame = (t: number) => ListingWrites;
532
514
 
533
- /**
534
- * Each command writes the listing it acted on, at every time it is asked for,
535
- * so the ids it captured are the ones drawn; before it starts the timeline has
536
- * put back the props, and the listing is derived from them again.
537
- */
515
+ /** Each command writes the listing it acted on at every time asked, so the ids it captured are the ones drawn. */
538
516
  function editFrame(kit: Kit, seen: CodeListing, source: string, basis: CodeTokenBasis): Frame | null {
539
517
  const next = normalizeSource(source);
540
518
  if (next === seen.source) return null;
@@ -670,10 +648,7 @@ function rowBandsIn(lines: IdLine[], codeRanges: CodeRange[], color: NormalizedC
670
648
  return bands;
671
649
  }
672
650
 
673
- /**
674
- * A single range (a plain object) as a one-element array; a chain or an array
675
- * of ranges as itself; the `{ list }` a chain serialises as, as that chain.
676
- */
651
+ /** One range as a one-element array; a chain or array as itself; the `{ list }` a chain serialises as, as that chain. */
677
652
  function normalizeCodeRanges(codeRange: CodeTarget | undefined): CodeRange[] {
678
653
  const maybeIterable = codeRange as Partial<Iterable<CodeRange>>;
679
654
  if (typeof maybeIterable[Symbol.iterator] === "function") {
@@ -686,28 +661,22 @@ function normalizeCodeRanges(codeRange: CodeTarget | undefined): CodeRange[] {
686
661
 
687
662
  const grammarLoaders = new Map<string, () => Promise<void>>();
688
663
 
689
- // One function per language: blocks sharing a grammar declare one key, and a
690
- // second load function for a key already declared is ignored with a warning.
664
+ // One function per language: a second load function for an already-declared task key is ignored with a warning.
691
665
  function grammarLoader(language: string): () => Promise<void> {
692
666
  let load = grammarLoaders.get(language);
693
667
  if (load === undefined) {
694
- load = () => ensureHighlighter(undefined, [language]);
668
+ load = () => ensureHighlighter([language]);
695
669
  grammarLoaders.set(language, load);
696
670
  }
697
671
  return load;
698
672
  }
699
673
 
700
- // ── Geometry and drawing ────────────────────────────────────────────────────
701
-
702
674
  interface Layouts {
703
675
  readonly from: CodeLayout;
704
676
  readonly to: CodeLayout;
705
677
  }
706
678
 
707
- /**
708
- * A node's measurements, kept between frames: the advance of each token, and
709
- * the layouts last built, reused while what they were built from is unchanged.
710
- */
679
+ /** A node's measurements between frames: token advances, and the layouts last built, reused while their inputs are unchanged. */
711
680
  interface Measured {
712
681
  readonly advances: TokenAdvanceCache;
713
682
  measurer: Measurer | null;
@@ -738,11 +707,7 @@ function metricsOf(s: LayoutState): CodeMetrics {
738
707
  };
739
708
  }
740
709
 
741
- /**
742
- * The one or two layouts a frame is drawn from: a settled frame has one, a
743
- * frame inside an edit two (where every token was, and where it is going).
744
- * `null` for a node never measured, which has nothing to measure them with.
745
- */
710
+ /** One layout for a settled frame, two (where every token was, where it goes) inside an edit; `null` for a node never measured. */
746
711
  function layoutsOf(measured: Measured, view: Shown, m: CodeMetrics): Layouts | null {
747
712
  const signature = measured.advances.signature(m.fontSize, m.fontFamily);
748
713
  measured.advances.sync(signature);
@@ -817,10 +782,9 @@ function drawListing(ctx: RenderContext, s: DrawState, kit: Kit): void {
817
782
  }
818
783
 
819
784
  /**
820
- * A listing is grabbed by its text, line by ragged line: each line a slot as
821
- * wide as its ink, plus the gutter where numbers show, inside the box. Code is
822
- * mostly whitespace, and its empty corners would swallow clicks meant for what
823
- * is behind it. Mid-edit, or before it is laid out, the box.
785
+ * A listing is picked by its ink, line by ragged line (plus the gutter), inside the
786
+ * box: code is mostly whitespace and empty corners would swallow clicks meant for
787
+ * what is behind. Mid-edit, or before layout, the box.
824
788
  */
825
789
  function outlineOf(view: Shown, layouts: Layouts | null, m: CodeMetrics, size: Size2D): Graphics2D {
826
790
  const box = { x: 0, y: 0, width: size.width, height: size.height };
package/src/nodes.ts CHANGED
@@ -1,16 +1,4 @@
1
1
  import { Code } from "./node";
2
2
 
3
- /**
4
- * Every node type this package publishes.
5
- *
6
- * A host registers these by handing the array to an engine —
7
- * `new Engine(platform, { nodes: NODES })` — which reads each component's key.
8
- * Defining a component only *declares* that key: nothing is registered by
9
- * importing a module, so a registry holds exactly what its owner asked for.
10
- *
11
- * Listing the component **values** is also what survives bundling: this package
12
- * declares `sideEffects: false`, and a document names a node type by string, so
13
- * an import kept only for its side effect is exactly what gets shaken out.
14
- */
15
3
  // Typed by name, so the declaration names each component rather than spelling out its type through core.
16
4
  export const NODES: (typeof Code)[] = [Code];
package/src/render.ts CHANGED
@@ -5,14 +5,10 @@ import { windowProgress, moveProgress } from "./transitions";
5
5
  import type { TokenState, StructuralTransition, RowBand, RowsTransition } from "./transitions";
6
6
  import type { TokenAdvanceCache } from "./measure-cache";
7
7
 
8
- /** Colour of a token the grammar had no opinion about. */
9
8
  const DEFAULT_TOKEN_COLOR: NormalizedColor = [0.82, 0.84, 0.86, 1];
10
- /** Gutter line-number colour: a muted version of the standard text colour. */
11
9
  const LINE_NUMBER_COLOR: NormalizedColor = [0.45, 0.5, 0.55, 1];
12
10
 
13
- // `parseColor` walks a CSS string; a listing asks for the same handful of theme
14
- // colours on every token of every frame, so the parse is memoized. Module-level,
15
- // because the answer depends only on the string.
11
+ // A listing asks for the same few theme colours on every token of every frame.
16
12
  const colorCache = new Map<string, NormalizedColor>();
17
13
  function tokenColor(css: string | undefined): NormalizedColor {
18
14
  if (!css) return DEFAULT_TOKEN_COLOR;
@@ -35,10 +31,7 @@ function lerpColor(from: NormalizedColor, to: NormalizedColor, t: number): Norma
35
31
  ];
36
32
  }
37
33
 
38
- // Opacity multiplier for a line's number under the active highlight. The
39
- // number stays bright only when the WHOLE line is highlighted, so we take the
40
- // min token opacity on the line. Returns 1 (no dimming) when no highlight is
41
- // active, so an edit's entries don't drag the number down.
34
+ // A number stays bright only when the whole line is highlighted, so it takes the line's minimum token opacity.
42
35
  function lineHighlightOpacity(line: IdLine, stateById: Map<number, TokenState>, highlightActive: boolean): number {
43
36
  if (!highlightActive) return 1;
44
37
  let min = 1;
@@ -50,36 +43,27 @@ function lineHighlightOpacity(line: IdLine, stateById: Map<number, TokenState>,
50
43
  return min;
51
44
  }
52
45
 
53
- /** Everything a `Code` node's own instance state supplies to draw one frame. */
46
+ /** What a `Code` node supplies to draw one frame. */
54
47
  export interface DrawCodeState {
55
48
  tokenLines: IdLine[];
56
49
  metrics: CodeMetrics;
57
50
  advanceCache: TokenAdvanceCache;
58
- /** The one or two layouts this frame draws from — see `frameLayout` in `layout.ts`. */
59
51
  from: CodeLayout;
60
52
  to: CodeLayout;
61
53
  edit: StructuralTransition | null;
62
- /** Per-token opacity from `resolveTokenStates` (persistent highlight + any active dim transition). */
63
54
  tokenStates: Map<number, TokenState>;
64
- /** Whether a highlight (persistent dim, or a highlight()/resetHighlight() cross-fade) is currently engaged. */
55
+ /** A persistent dim or a highlight cross-fade is engaged. */
65
56
  highlightActive: boolean;
66
- /** The settled row bands, and the change between two sets of them in flight, if any. */
67
57
  rowBands?: RowBand[];
68
58
  rowsEdit?: RowsTransition | null;
69
- /** The node's laid-out width, which a band spans when it is wider than the block. */
59
+ /** A band spans this when it is wider than the block. */
70
60
  boxW?: number;
71
61
  }
72
62
 
73
- /** What a frame is drawn onto: somewhere to draw, and the face's measurements. A render context is one. */
63
+ /** Somewhere to draw and the face's measurements; a render context is one. */
74
64
  export type CodeCanvas = Pick<RenderContext2D, "draw" | "measureText">;
75
65
 
76
- /**
77
- * Draw one frame of a code listing: the settled structure, or — mid
78
- * structural edit — the two structures it is interpolating between (what
79
- * stays reflows, what is leaving fades out where it was, what is arriving
80
- * fades/slides in where it lands). Pure over `state`; touches nothing but
81
- * `draw`.
82
- */
66
+ /** Draws the settled structure, or mid-edit the two it interpolates between. Pure over `state`. */
83
67
  export function drawCode(draw: CodeCanvas, state: DrawCodeState): void {
84
68
  const { tokenLines, metrics, advanceCache, from, to, edit, tokenStates: dim } = state;
85
69
 
@@ -91,8 +75,6 @@ export function drawCode(draw: CodeCanvas, state: DrawCodeState): void {
91
75
  const blockW = edit ? lerpNumber(from.blockW, to.blockW, pMove) : to.blockW;
92
76
  const gutter = edit ? lerpNumber(from.gutter, to.gutter, pMove) : to.gutter;
93
77
  const gutterGap = to.gutterGap || from.gutterGap;
94
- // Right edge of the line-number column: one gap to the left of where the
95
- // code text begins.
96
78
  const numberRight = -blockW / 2 + metrics.padding.left + gutter - gutterGap;
97
79
 
98
80
  drawRowBands(draw, state, Math.max(blockW, state.boxW ?? 0), pMove);
@@ -136,8 +118,7 @@ export function drawCode(draw: CodeCanvas, state: DrawCodeState): void {
136
118
  .fill([{ type: "solid", color, opacity }]));
137
119
  };
138
120
 
139
- // What survived the edit, and what it brought with it — drawn from the
140
- // structure the edit lands on.
121
+ // Survivors and arrivals, from the structure the edit lands on.
141
122
  for (let i = 0; i < tokenLines.length; i++) {
142
123
  const line = tokenLines[i];
143
124
  const toIndex = to.lineIndex.get(line.id);
@@ -148,10 +129,7 @@ export function drawCode(draw: CodeCanvas, state: DrawCodeState): void {
148
129
  let rowY = toY;
149
130
  let rowAlpha = 1;
150
131
  if (edit && entering) {
151
- // A wholly new row fades as one piece, in the place it lands — the
152
- // line arrives, it does not travel to get there, and it is not a
153
- // spray of glyphs each finding its own way in. See
154
- // `StructuralTransition.enteringLineIds`.
132
+ // A new row fades as one piece where it lands (see `enteringLineIds`).
155
133
  rowAlpha = pIn;
156
134
  } else if (edit) {
157
135
  const fromIndex = from.lineIndex.get(line.id);
@@ -168,8 +146,7 @@ export function drawCode(draw: CodeCanvas, state: DrawCodeState): void {
168
146
 
169
147
  if (edit && !entering) {
170
148
  if (edit.addedIds.has(token.id)) {
171
- // Spliced into a row that already existed: the reflow
172
- // already opened the space, so this only has to appear.
149
+ // Spliced into an existing row: the reflow opened the space already.
173
150
  alpha = pIn;
174
151
  } else {
175
152
  const was = from.tokens.get(token.id);
@@ -184,17 +161,14 @@ export function drawCode(draw: CodeCanvas, state: DrawCodeState): void {
184
161
  }
185
162
  }
186
163
 
187
- // What the edit takes away, drawn from the structure it is leaving — it
188
- // has no place in the new one, which is the whole reason it is fading.
164
+ // What the edit takes away, from the structure it is leaving.
189
165
  if (edit && pOut < 1) {
190
166
  for (let i = 0; i < edit.from.length; i++) {
191
167
  const line = edit.from[i];
192
168
  const fromIndex = from.lineIndex.get(line.id);
193
169
  if (fromIndex === undefined) continue;
194
170
  const toIndex = to.lineIndex.get(line.id);
195
- // A row that survived the edit is still moving, so its outgoing
196
- // tokens travel with it; a deleted row stays where it was and
197
- // dissolves while the rows below close over it.
171
+ // A surviving row's outgoing tokens travel with it; a deleted row dissolves in place.
198
172
  const y = toIndex === undefined
199
173
  ? from.lineY[fromIndex]
200
174
  : lerpNumber(from.lineY[fromIndex], to.lineY[toIndex], pMove);
@@ -228,11 +202,7 @@ function bandEdges(band: RowBand, state: DrawCodeState, pMove: number): { top: n
228
202
  return { top: (first ?? last as number) + half, bottom: (last ?? first as number) - half };
229
203
  }
230
204
 
231
- /**
232
- * Draw the row bands, under the text and across the whole block. A band rides
233
- * its rows through a structural edit; between two sets, bands paired by position
234
- * slide and recolour while an unpaired one fades.
235
- */
205
+ /** Bands ride their rows through an edit; between two sets, those paired by position slide and recolour and an unpaired one fades. */
236
206
  function drawRowBands(draw: CodeCanvas, state: DrawCodeState, width: number, pMove: number): void {
237
207
  const { rowBands = [], rowsEdit = null } = state;
238
208
  const paint = (top: number, bottom: number, color: NormalizedColor, radius: number, opacity: number): void => {
@@ -270,22 +240,9 @@ function drawRowBands(draw: CodeCanvas, state: DrawCodeState, width: number, pMo
270
240
  }
271
241
 
272
242
  /**
273
- * Draw the gutter — one number per *slot*, not one per row.
274
- *
275
- * That distinction is the whole of it. A line number is an ordinal of the
276
- * block: "3" means the third line, and it is there for exactly as long as the
277
- * listing has a third line. Drawing it alongside the row it labels — the
278
- * obvious thing, since that is where it appears — tied it to the row's identity
279
- * instead, so a line rewritten past the point the diff could pair it took its
280
- * number down with it and brought an identical one back up. The number blinked
281
- * although it had never gone away, and every substantially rewritten line did
282
- * it at once.
283
- *
284
- * Numbering the slots means only the numbers at the end move at all: those the
285
- * block grew to reach fade in, those it shrank past fade out, and every number
286
- * both structures have simply travels to its new row height. Which is also the
287
- * honest reading of the edit — inserting a line in the middle does not make a
288
- * new number appear there, it makes the block one line longer.
243
+ * One number per slot, not per row: "3" lasts as long as the listing has a third
244
+ * line. Tied to a row's identity, a line rewritten past what the diff could pair
245
+ * would take its number down and bring an identical one back up (a blink).
289
246
  */
290
247
  function drawLineNumbers(
291
248
  state: DrawCodeState,
@@ -302,8 +259,7 @@ function drawLineNumbers(
302
259
  for (let i = 0; i < Math.max(fromCount, toCount); i++) {
303
260
  const inFrom = i < fromCount;
304
261
  const inTo = i < toCount;
305
- // Highlight dimming follows whatever content occupies the slot, which
306
- // is the new listing wherever there is one.
262
+ // Dimming follows the content in the slot: the new listing where there is one.
307
263
  const line = inTo ? tokenLines[i] : edit?.from[i];
308
264
  const dimming = line ? lineHighlightOpacity(line, dim, highlightActive) : 1;
309
265
 
package/src/style.ts CHANGED
@@ -1,40 +1,22 @@
1
1
  import { Highlighter, Tag, tagHighlighter, tags as t } from "@lezer/highlight";
2
2
 
3
- /**
4
- * A single style rule: map one or more Lezer highlight {@link Tag}s to a color.
5
- * This mirrors a `@codemirror/language` HighlightStyle entry, but instead of
6
- * emitting CSS class names it carries the color directly — we render to a canvas,
7
- * not the DOM, so there is no stylesheet to resolve against.
8
- */
3
+ /** Lezer highlight {@link Tag}s mapped to a color; a canvas has no stylesheet, so the color is carried directly. */
9
4
  export interface CodeStyleRule {
10
5
  tag: Tag | readonly Tag[];
11
6
  color: string;
12
7
  }
13
8
 
14
- /**
15
- * A code theme: an ordered list of tag→color rules plus a fallback color for any
16
- * text the parser produces no highlight tag for (operators, punctuation, plain
17
- * identifiers in some grammars, etc.). Pass a custom one via the Code node's
18
- * `theme` prop to recolor highlighting without touching the highlighter wiring.
19
- *
20
- * @remarks
21
- * Later rules win when several match the same tag, matching how
22
- * `@lezer/highlight`'s {@link tagHighlighter} resolves precedence.
23
- */
9
+ /** Ordered tag rules plus a fallback color; later rules win when several match a tag, as in {@link tagHighlighter}. */
24
10
  export interface CodeHighlightStyle {
25
- /** Stable name, used to register the style and to refer to it from a `theme` prop. */
11
+ /** Registers the style and names it in a `theme` prop. */
26
12
  name: string;
27
- /** Color applied to text with no matching tag. */
13
+ /** For text with no matching tag. */
28
14
  defaultColor: string;
29
15
  rules: CodeStyleRule[];
30
16
  }
31
17
 
32
- /**
33
- * Motion Canvas's default highlight palette (a Nord-derived theme), ported from
34
- * `@motion-canvas/2d`'s DefaultHighlightStyle so highlighting matches Motion
35
- * Canvas token-for-token. Colors and tag groupings are kept identical; only the
36
- * shape changes (color strings in place of CSS rules).
37
- */
18
+ // The default palette is ported from `@motion-canvas/2d`; the GitHub and VS Code ones are adapted from
19
+ // `@uiw/codemirror-theme-github` and `-vscode`, with their DOM-only style props dropped.
38
20
  export const DefaultHighlightStyle: CodeHighlightStyle = {
39
21
  name: "motion-canvas-default",
40
22
  defaultColor: "#d8dee9",
@@ -77,12 +59,6 @@ export const DefaultHighlightStyle: CodeHighlightStyle = {
77
59
  ],
78
60
  };
79
61
 
80
- /**
81
- * GitHub Dark highlight palette, adapted from `@uiw/codemirror-theme-github`.
82
- * `defaultColor` is GitHub's editor foreground (`#c9d1d9`), used for any text the
83
- * parser tags don't cover. DOM-only style props (backgroundColor, textDecoration,
84
- * fontWeight, fontStyle) are dropped — we only color tokens on the canvas.
85
- */
86
62
  export const GithubDarkStyle: CodeHighlightStyle = {
87
63
  name: "github-dark",
88
64
  defaultColor: "#c9d1d9",
@@ -102,11 +78,6 @@ export const GithubDarkStyle: CodeHighlightStyle = {
102
78
  ],
103
79
  };
104
80
 
105
- /**
106
- * GitHub Light highlight palette, adapted from `@uiw/codemirror-theme-github`.
107
- * `defaultColor` is GitHub's editor foreground (`#24292e`). See {@link GithubDarkStyle}
108
- * for the note on dropped DOM-only style props.
109
- */
110
81
  export const GithubLightStyle: CodeHighlightStyle = {
111
82
  name: "github-light",
112
83
  defaultColor: "#24292e",
@@ -127,11 +98,6 @@ export const GithubLightStyle: CodeHighlightStyle = {
127
98
  ],
128
99
  };
129
100
 
130
- /**
131
- * VS Code Dark+ highlight palette, adapted from the `@uiw/codemirror-theme-vscode`
132
- * snippet. `defaultColor` is VS Code's editor foreground (`#9cdcfe`). DOM-only
133
- * style props are dropped; later rules win, matching {@link tagHighlighter}.
134
- */
135
101
  export const VscodeDarkStyle: CodeHighlightStyle = {
136
102
  name: "vscode-dark",
137
103
  defaultColor: "#9cdcfe",
@@ -166,11 +132,6 @@ export const VscodeDarkStyle: CodeHighlightStyle = {
166
132
  ],
167
133
  };
168
134
 
169
- /**
170
- * VS Code Light+ highlight palette, adapted from the `@uiw/codemirror-theme-vscode`
171
- * snippet. `defaultColor` is VS Code's editor foreground (`#383a42`). See
172
- * {@link VscodeDarkStyle} for the note on dropped DOM-only style props.
173
- */
174
135
  export const VscodeLightStyle: CodeHighlightStyle = {
175
136
  name: "vscode-light",
176
137
  defaultColor: "#383a42",
@@ -205,11 +166,6 @@ export const VscodeLightStyle: CodeHighlightStyle = {
205
166
  ],
206
167
  };
207
168
 
208
- /**
209
- * The built-in themes, keyed by their string name. {@link CodeThemeName} is the
210
- * union of these keys — pass one of those strings (or any {@link CodeHighlightStyle}
211
- * object) to a Code node's `theme` prop.
212
- */
213
169
  export const BUILTIN_THEMES = {
214
170
  // Literal keys (not computed from `.name`) so `keyof` yields a string-literal
215
171
  // union for CodeThemeName; these must stay in sync with each style's `name`.
@@ -223,27 +179,16 @@ export const BUILTIN_THEMES = {
223
179
  /** Names of the built-in themes, e.g. `'github-dark' | 'github-light'`. */
224
180
  export type CodeThemeName = keyof typeof BUILTIN_THEMES;
225
181
 
226
- /**
227
- * What a Code node's `theme` prop accepts: a built-in theme name (with
228
- * autocomplete), any other registered theme name, or a {@link CodeHighlightStyle}
229
- * object passed inline. The `string & {}` keeps the literal-name suggestions while
230
- * still allowing arbitrary registered names.
231
- */
182
+ /** A built-in name, any registered name, or a style inline; `string & {}` keeps the autocomplete for the built-in names. */
232
183
  export type CodeTheme = CodeThemeName | (string & {}) | CodeHighlightStyle;
233
184
 
234
- // Cache one compiled Highlighter per style *object*. Compiling builds a tag-rule
235
- // matcher; the parsed result is pure data so it's safe to share across Code nodes.
236
- // Keyed by the object rather than its `name`: two styles may share a name (an
185
+ // One compiled Highlighter per style *object*, keyed by the object rather than its `name`: two styles may share a name (an
237
186
  // inline theme, or one re-registered under the same name with new colours), and
238
187
  // the second must not colour with the first one's rules. A WeakMap, so a style
239
188
  // nothing refers to any more takes its matcher with it.
240
189
  const compiledByStyle = new WeakMap<CodeHighlightStyle, Highlighter>();
241
190
 
242
- /**
243
- * Compile a {@link CodeHighlightStyle} into a `@lezer/highlight` {@link Highlighter}
244
- * whose "class" output is the literal color string for each tag. Cached per style
245
- * object so repeated lookups (every tokenize) don't rebuild the matcher.
246
- */
191
+ /** A {@link Highlighter} whose "class" output is the color string for each tag. */
247
192
  export function compileStyle(style: CodeHighlightStyle): Highlighter {
248
193
  const cached = compiledByStyle.get(style);
249
194
  if (cached) return cached;
package/src/tokens.ts CHANGED
@@ -1,26 +1,18 @@
1
1
  import { canHighlight, highlightToTokens, normalizeSource, resolveTheme } from "./highlight";
2
2
  import { CodeTheme } from "./style";
3
3
 
4
- // IdToken is our internal wrapper around a highlighter token that gives every
5
- // token a stable identity across line insertions/removals. All animation state
6
- // is keyed by `id`, so concurrent edits at different positions don't clobber
7
- // each other while one is mid-animation.
4
+ /** A highlighter token with a stable identity: animation state is keyed by `id`, so concurrent edits at different positions don't clobber each other. */
8
5
  export interface IdToken {
9
6
  id: number;
10
7
  content: string;
11
8
  color?: string;
12
9
  }
13
10
 
14
- // IdLine wraps a row of tokens with its own stable id, so per-line animations
15
- // (heightScale during insert/remove) survive line insertions/removals.
16
11
  export interface IdLine {
17
12
  id: number;
18
13
  tokens: IdToken[];
19
14
  }
20
15
 
21
- // Module-global, monotonic id counters shared across all Code nodes — ids only
22
- // need to be unique within a node, but a global counter is the simplest way to
23
- // guarantee that and never collides.
24
16
  let nextTokenId = 1;
25
17
  let nextLineId = 1;
26
18
 
@@ -33,47 +25,13 @@ export function makeIdLine(tokens: IdToken[]): IdLine {
33
25
  }
34
26
 
35
27
  export function tokenizeCodeToIdLines(code: string, language: string, theme: CodeTheme): IdLine[] {
36
- // Until the language parser has loaded (it streams in via the asset loader),
37
- // fall back to flat, uncolored lines instead of throwing on an unloaded
38
- // grammar. The node re-tokenizes once the load completes.
39
28
  code = normalizeSource(code);
40
- if (!canHighlight(language, theme)) {
41
- // Resolved for its warning: an unknown theme is reported whether or not
42
- // the grammar has landed yet.
43
- resolveTheme(theme);
29
+ if (!canHighlight(language)) {
30
+ // Flat lines until the grammar loads; the node re-tokenizes then.
31
+ resolveTheme(theme); // so an unknown theme still warns before the grammar lands
44
32
  return code.split('\n').map(line => makeIdLine([makeIdToken(line, '#d1d5db')]));
45
33
  }
46
34
  return highlightToTokens(code, language, theme).map(line =>
47
35
  makeIdLine(line.map(tok => makeIdToken(tok.content, tok.color))),
48
36
  );
49
37
  }
50
-
51
- /**
52
- * Split `tokens` at character offset `col` (relative to the joined content of
53
- * the tokens). If `col` falls inside a token, that token is split into two new
54
- * tokens with fresh ids (the original id is discarded — it didn't exist as a
55
- * single unit on either side of the cut).
56
- */
57
- export function splitTokensAt(
58
- tokens: IdToken[],
59
- col: number,
60
- ): { before: IdToken[]; after: IdToken[] } {
61
- const before: IdToken[] = [];
62
- const after: IdToken[] = [];
63
- let off = 0;
64
- for (const tok of tokens) {
65
- const tStart = off;
66
- const tEnd = off + tok.content.length;
67
- if (tEnd <= col) {
68
- before.push(tok);
69
- } else if (tStart >= col) {
70
- after.push(tok);
71
- } else {
72
- const cut = col - tStart;
73
- before.push(makeIdToken(tok.content.slice(0, cut), tok.color));
74
- after.push(makeIdToken(tok.content.slice(cut), tok.color));
75
- }
76
- off = tEnd;
77
- }
78
- return { before, after };
79
- }