@motionscript/code 0.1.0-alpha.3 → 0.1.0-alpha.5

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 (59) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/LICENSE +201 -201
  3. package/NOTICE +48 -0
  4. package/README.md +161 -108
  5. package/dist/browser/THIRD_PARTY_LICENSES.txt +431 -0
  6. package/dist/browser/index.js +7 -6
  7. package/dist/browser/index.js.map +3 -3
  8. package/dist/browser/manifest.json +84 -2
  9. package/dist/code-range.d.ts +6 -0
  10. package/dist/code-range.d.ts.map +1 -1
  11. package/dist/code-range.js +8 -0
  12. package/dist/code-range.js.map +1 -1
  13. package/dist/diff.d.ts +14 -0
  14. package/dist/diff.d.ts.map +1 -1
  15. package/dist/diff.js +134 -2
  16. package/dist/diff.js.map +1 -1
  17. package/dist/engine.d.ts +1 -1
  18. package/dist/engine.d.ts.map +1 -1
  19. package/dist/engine.js +1 -1
  20. package/dist/engine.js.map +1 -1
  21. package/dist/highlight.d.ts +10 -2
  22. package/dist/highlight.d.ts.map +1 -1
  23. package/dist/highlight.js +40 -6
  24. package/dist/highlight.js.map +1 -1
  25. package/dist/index.js.map +1 -1
  26. package/dist/layout.js.map +1 -1
  27. package/dist/node.d.ts +42 -7
  28. package/dist/node.d.ts.map +1 -1
  29. package/dist/node.js +174 -47
  30. package/dist/node.js.map +1 -1
  31. package/dist/nodes.js.map +1 -1
  32. package/dist/render.d.ts +6 -1
  33. package/dist/render.d.ts.map +1 -1
  34. package/dist/render.js +59 -0
  35. package/dist/render.js.map +1 -1
  36. package/dist/style.d.ts +3 -3
  37. package/dist/style.d.ts.map +1 -1
  38. package/dist/style.js +10 -6
  39. package/dist/style.js.map +1 -1
  40. package/dist/tokens.d.ts.map +1 -1
  41. package/dist/tokens.js +5 -1
  42. package/dist/tokens.js.map +1 -1
  43. package/dist/transitions.d.ts +19 -1
  44. package/dist/transitions.d.ts.map +1 -1
  45. package/dist/transitions.js.map +1 -1
  46. package/package.json +7 -5
  47. package/registry.json +6 -6
  48. package/src/code-range.ts +165 -156
  49. package/src/diff.ts +351 -216
  50. package/src/engine.ts +21 -21
  51. package/src/highlight.ts +340 -291
  52. package/src/index.ts +3 -3
  53. package/src/layout.ts +157 -157
  54. package/src/node.ts +1038 -904
  55. package/src/nodes.ts +18 -18
  56. package/src/render.ts +312 -247
  57. package/src/style.ts +255 -251
  58. package/src/tokens.ts +79 -75
  59. package/src/transitions.ts +204 -184
package/dist/node.js CHANGED
@@ -4,14 +4,21 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
4
4
  else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
5
  return c > 3 && r && Object.defineProperty(target, key, r), r;
6
6
  };
7
- import { node, Clip, Node2D, property, insetsProperty, lerpNumber, command, driveCommand, } from "@motionscript/sdk";
8
- import { rangeToCharOffsets, charOffsetsToRange, TokenAdvanceCache, tokenizeCodeToIdLines, defaultCodeDiff, layoutCode, metricsSignature, makeAnim, defaultCodePhases, resolveTokenStates, moveProgress, canHighlight, ensureHighlighter, DefaultHighlightStyle, drawCode, } from "./engine.js";
7
+ import { node, Clip, Node2D, property, insetsProperty, lerpNumber, colorOps, command, driveCommand, } from "@motionscript/sdk";
8
+ import { rangeToCharOffsets, charOffsetsToRange, TokenAdvanceCache, tokenizeCodeToIdLines, defaultCodeDiff, layoutCode, metricsSignature, makeAnim, defaultCodePhases, resolveTokenStates, moveProgress, canHighlight, ensureHighlighter, normalizeSource, DefaultHighlightStyle, drawCode, } from "./engine.js";
9
+ /** A row band's paint and corner radius when `highlightRows` is given neither. */
10
+ const ROW_BAND_COLOR = "rgba(255, 255, 255, 0.12)";
11
+ const ROW_BAND_RADIUS = 6;
9
12
  let Code = class Code extends Node2D {
10
13
  /** The settled structure. During an edit, the structure the edit lands on. */
11
14
  tokenLines = [];
12
- tokenized = false;
13
15
  /** The source `tokenLines` spells, so a frame compares it with `code` without joining every token. */
14
16
  _linesSource = "";
17
+ /** What `tokenLines` were coloured and split with, so a frame notices a `language` or `theme` that has moved since. */
18
+ _linesBasis = { language: "", theme: "", highlighted: false };
19
+ /** The last `code` seen and its normalised form, so a frame that finds it unchanged does not scan it. */
20
+ _rawCode = null;
21
+ _normalCode = "";
15
22
  transitions = [];
16
23
  // Caches expensive scope.measureText() calls; cleared when the font
17
24
  // signature (fontSize|fontFamily) changes. See TokenAdvanceCache.
@@ -27,6 +34,8 @@ let Code = class Code extends Node2D {
27
34
  // tokens whose id is NOT in the highlight set. null means "not highlighting".
28
35
  highlightDimOpacity = null;
29
36
  highlightedIds = new Set();
37
+ // The bands highlightRows() left behind whole rows; empty means none.
38
+ rowBands = [];
30
39
  /**
31
40
  * Which token became which across an edit, and when each part of that
32
41
  * edit happens. Construction-time-only (see {@link CodeStrategies}) —
@@ -36,10 +45,10 @@ let Code = class Code extends Node2D {
36
45
  phaseStrategy;
37
46
  constructor(props) {
38
47
  super(props);
39
- this.diffStrategy = props.diffStrategy ?? defaultCodeDiff;
40
- this.phaseStrategy = props.phaseStrategy ?? defaultCodePhases;
41
- this.applyProp("width", props.width ?? "hug");
42
- this.applyProp("height", props.height ?? "hug");
48
+ this.diffStrategy = props?.diffStrategy ?? defaultCodeDiff;
49
+ this.phaseStrategy = props?.phaseStrategy ?? defaultCodePhases;
50
+ this.applyProp("width", props?.width ?? "hug");
51
+ this.applyProp("height", props?.height ?? "hug");
43
52
  this.applyProp("_driven", false);
44
53
  // Best-effort, for a highlighter constructed outside the asset pipeline
45
54
  // entirely (a bare `new Code(...)` in a test or a headless measure). On
@@ -48,19 +57,7 @@ let Code = class Code extends Node2D {
48
57
  // there the tokens are right the first time rather than upgrading later.
49
58
  // (Theme is a synchronous color style; only the language parser loads.)
50
59
  ensureHighlighter(undefined, [this.language]).catch(() => { });
51
- this.tokenize();
52
- }
53
- set(props) {
54
- super.set(props);
55
- if (props.code !== undefined || props.language !== undefined || props.theme !== undefined) {
56
- this.tokenized = false;
57
- }
58
- }
59
- tokenize() {
60
- this.setLines(this._structureFor(this.code), this.code);
61
- // Only consider ourselves tokenized once we actually highlighted; while
62
- // the language is still loading we keep retrying on each render.
63
- this.tokenized = canHighlight(this.language, this.theme);
60
+ this._sync();
64
61
  }
65
62
  /**
66
63
  * Swap in a new settled structure, invalidating the layout cache.
@@ -69,11 +66,12 @@ let Code = class Code extends Node2D {
69
66
  * counter rather than a hash of the content — a write that skipped the bump
70
67
  * would keep serving the previous frame's geometry for the new listing.
71
68
  */
72
- setLines(lines, source) {
69
+ setLines(lines, source, basis = this._basis()) {
73
70
  if (this.tokenLines === lines)
74
71
  return;
75
72
  this.tokenLines = lines;
76
73
  this._linesSource = source;
74
+ this._linesBasis = basis;
77
75
  this.structureVersion++;
78
76
  this.layoutCache = null;
79
77
  this.layoutCacheKey = null;
@@ -116,6 +114,15 @@ let Code = class Code extends Node2D {
116
114
  }
117
115
  return null;
118
116
  }
117
+ /** The row-band change in flight, if any; the latest wins. */
118
+ activeRows() {
119
+ for (let i = this.transitions.length - 1; i >= 0; i--) {
120
+ const tr = this.transitions[i];
121
+ if (tr.kind === "rows")
122
+ return tr;
123
+ }
124
+ return null;
125
+ }
119
126
  settledLayout(m, scope) {
120
127
  const key = `${this.structureVersion}|${metricsSignature(m)}|${this.advanceCache.signature(m.fontSize, m.fontFamily)}`;
121
128
  if (this.layoutCache && this.layoutCacheKey === key)
@@ -188,14 +195,9 @@ let Code = class Code extends Node2D {
188
195
  // column-by-column rather than merely look different.
189
196
  ctx.pushTextStyle(null);
190
197
  try {
198
+ // Also what upgrades a frame drawn as plain text to full highlighting
199
+ // the moment the asset loader resolves the grammar.
191
200
  this._sync();
192
- // Keep retrying until the language+theme have actually loaded, so a frame
193
- // that rendered as plain text upgrades to full highlighting the moment the
194
- // asset loader resolves. Never mid-edit: re-tokenizing mints fresh ids,
195
- // and an in-flight transition is keyed by the ones it captured.
196
- if (!this.tokenized && !this.activeEdit() && canHighlight(this.language, this.theme)) {
197
- this.tokenize();
198
- }
199
201
  super.renderContent(ctx);
200
202
  }
201
203
  finally {
@@ -425,6 +427,25 @@ let Code = class Code extends Node2D {
425
427
  this.highlightedIds = done ? new Set() : previousIds;
426
428
  });
427
429
  }
430
+ /**
431
+ * Lay a band of `color` behind whole rows: every line a range touches, across
432
+ * the block. Persistent, and independent of {@link highlight}'s dimming. A
433
+ * second call slides and resizes the bands onto their new rows, pairing them
434
+ * in the order given; `resetRowHighlight()` fades them out. A band keeps to
435
+ * its lines when an edit moves them.
436
+ */
437
+ highlightRows(args) {
438
+ this._sync();
439
+ const { target, color, radius } = args.data;
440
+ const paint = colorOps.resolve(color ?? ROW_BAND_COLOR);
441
+ const bands = this.rowBandsIn(normalizeCodeRanges(target), paint, Math.max(0, radius ?? ROW_BAND_RADIUS));
442
+ return this.runRows(this.rowBands, bands, args.duration, args.easing);
443
+ }
444
+ /** Fade every row band out and clear them. */
445
+ resetRowHighlight(args) {
446
+ this._sync();
447
+ return this.runRows(this.rowBands, [], args.duration, args.easing);
448
+ }
428
449
  /**
429
450
  * Animate the listing to a new source.
430
451
  *
@@ -440,10 +461,15 @@ let Code = class Code extends Node2D {
440
461
  * of two unrelated listings rather than as an edit being made.
441
462
  */
442
463
  editTo(next, duration, easing) {
464
+ if (!this.mounted)
465
+ return driveCommand(duration, () => { });
466
+ next = normalizeSource(next);
443
467
  const from = this.tokenLines;
444
468
  const fromCode = this.joinedSource();
445
469
  if (next === fromCode)
446
470
  return driveCommand(duration, () => { });
471
+ const fromBasis = this._linesBasis;
472
+ const basis = this._basis();
447
473
  const edit = this.diffStrategy(from, tokenizeCodeToIdLines(next, this.language, this.theme));
448
474
  const transition = {
449
475
  kind: "structural",
@@ -459,12 +485,10 @@ let Code = class Code extends Node2D {
459
485
  fromLayout: null,
460
486
  toLayout: null,
461
487
  };
462
- // A tokenize triggered by the grammar landing mid-edit would mint fresh
488
+ // A re-tokenize triggered by the grammar landing mid-edit would mint fresh
463
489
  // ids underneath this transition, and the transition is keyed by the ones
464
- // it captured — so the retry in `onRender` is gated on there being no edit
465
- // in flight, which means this flag has to be honest now rather than once
466
- // the edit settles.
467
- this.tokenized = canHighlight(this.language, this.theme);
490
+ // it captured — so `_sync` leaves the structure alone while an edit is in
491
+ // flight, and compares the basis recorded here once it settles.
468
492
  return driveCommand(duration, (t) => {
469
493
  this._drive();
470
494
  const running = t > 0 && t < 1;
@@ -478,11 +502,11 @@ let Code = class Code extends Node2D {
478
502
  // starts, the listing is what preceded it; at or past the end, the
479
503
  // result. A `finally` could only ever have run forwards.
480
504
  if (t <= 0) {
481
- this.setLines(from, fromCode);
505
+ this.setLines(from, fromCode, fromBasis);
482
506
  this._writeProp("code", fromCode);
483
507
  }
484
508
  else {
485
- this.setLines(edit.lines, next);
509
+ this.setLines(edit.lines, next, basis);
486
510
  this._writeProp("code", next);
487
511
  }
488
512
  });
@@ -497,6 +521,8 @@ let Code = class Code extends Node2D {
497
521
  * way*, and `progress` is assigned from `t` rather than accumulated.
498
522
  */
499
523
  runDim(tokens, duration, easing, settle) {
524
+ if (!this.mounted)
525
+ return driveCommand(duration, () => { });
500
526
  const transition = { kind: "dim", tokens, progress: 0 };
501
527
  return driveCommand(duration, (t) => {
502
528
  this._drive();
@@ -510,6 +536,23 @@ let Code = class Code extends Node2D {
510
536
  settle?.(t >= 1);
511
537
  });
512
538
  }
539
+ /** The same membership rule as {@link runDim}: on the stack strictly inside the run, settled at either end. */
540
+ runRows(from, to, duration, easing) {
541
+ if (!this.mounted || (from.length === 0 && to.length === 0))
542
+ return driveCommand(duration, () => { });
543
+ const transition = { kind: "rows", from, to, progress: 0 };
544
+ return driveCommand(duration, (t) => {
545
+ this._drive();
546
+ const running = t > 0 && t < 1;
547
+ const index = this.transitions.indexOf(transition);
548
+ if (running && index < 0)
549
+ this.transitions.push(transition);
550
+ else if (!running && index >= 0)
551
+ this.transitions.splice(index, 1);
552
+ transition.progress = easing ? easing(t) : t;
553
+ this.rowBands = t >= 1 ? to : from;
554
+ });
555
+ }
513
556
  /**
514
557
  * Route a tweened `code` through the diff engine.
515
558
  *
@@ -520,9 +563,11 @@ let Code = class Code extends Node2D {
520
563
  * the rest of the step's props tween alongside it untouched.
521
564
  */
522
565
  _prepareStep(to, duration, easing) {
566
+ if (!this.mounted)
567
+ return super._prepareStep(to, duration, easing);
523
568
  this._sync();
524
- const next = to.code;
525
- if (typeof next !== "string" || next === this.joinedSource()) {
569
+ const next = typeof to.code === "string" ? normalizeSource(to.code) : undefined;
570
+ if (next === undefined || next === this.joinedSource()) {
526
571
  return super._prepareStep(to, duration, easing);
527
572
  }
528
573
  const rest = { ...to };
@@ -625,6 +670,19 @@ let Code = class Code extends Node2D {
625
670
  return result;
626
671
  }
627
672
  // ── Drawing ─────────────────────────────────────────────────────────────
673
+ /** One band per range, over every line it touches; a range past the end of the listing gives none. */
674
+ rowBandsIn(codeRanges, color, radius) {
675
+ const count = this.tokenLines.length;
676
+ const bands = [];
677
+ for (const range of codeRanges) {
678
+ const first = Math.max(1, Math.round(range.startLine ?? 1));
679
+ const last = Math.min(count, range.endLine == null || range.endLine === Infinity ? count : Math.round(range.endLine));
680
+ if (first > last)
681
+ continue;
682
+ bands.push({ startId: this.tokenLines[first - 1].id, endId: this.tokenLines[last - 1].id, color, radius });
683
+ }
684
+ return bands;
685
+ }
628
686
  /**
629
687
  * Thin delegator: everything about *how* a frame is painted (settled or
630
688
  * mid structural-edit, entry/exit motion, colour cross-fade) lives in
@@ -641,6 +699,9 @@ let Code = class Code extends Node2D {
641
699
  ...this.frameLayout(draw),
642
700
  tokenStates: resolveTokenStates(this.tokenLines, this.transitions, this.highlightDimOpacity, this.highlightedIds),
643
701
  highlightActive: this.highlightDimOpacity !== null || this.transitions.some(tr => tr.kind === "dim"),
702
+ rowBands: this.rowBands,
703
+ rowsEdit: this.activeRows(),
704
+ boxW: this.layoutBounds.width,
644
705
  });
645
706
  }
646
707
  /** Each source the props have named, tokenized once, so a listing derived again keeps the ids a highlight selected. */
@@ -650,28 +711,59 @@ let Code = class Code extends Node2D {
650
711
  this._sync();
651
712
  this._writeProp("_driven", true);
652
713
  }
714
+ /**
715
+ * Bring the settled structure up to the props it is derived from.
716
+ *
717
+ * Pulled from every reader rather than pushed from a prop write, because
718
+ * `code`, `language` and `theme` can each be tweened or bound, and neither
719
+ * has a write to hang a subscription on. Never mid-edit: re-tokenizing mints
720
+ * fresh ids, and an in-flight transition is keyed by the ones it captured.
721
+ */
653
722
  _sync() {
723
+ const code = this._normalised();
654
724
  if (!this._driven) {
655
725
  this.transitions.length = 0;
656
726
  this.highlightDimOpacity = null;
657
727
  if (this.highlightedIds.size > 0)
658
728
  this.highlightedIds = new Set();
659
- this.setLines(this._structureFor(this.code), this.code);
729
+ if (this.rowBands.length > 0)
730
+ this.rowBands = [];
731
+ this.setLines(this._structureFor(code), code);
732
+ }
733
+ else if (!this.activeEdit()) {
734
+ const basis = this._basis();
735
+ const was = this._linesBasis;
736
+ if (this._linesSource === code && was.language === basis.language && was.theme === basis.theme && was.highlighted === basis.highlighted)
737
+ return;
738
+ const lines = this._structureFor(code);
739
+ // The same text split the same way, only coloured differently: the
740
+ // tokens are the ones a highlight or a transition already names.
741
+ if (this._linesSource === code && was.language === basis.language && was.highlighted === basis.highlighted) {
742
+ adoptIds(this.tokenLines, lines);
743
+ }
744
+ this.setLines(lines, code, basis);
660
745
  }
661
- else if (this._linesSource !== this.code && !this.activeEdit()) {
662
- // A `set` or a binding moved `code` outside a listing command.
663
- this.setLines(this._structureFor(this.code), this.code);
746
+ }
747
+ _normalised() {
748
+ const code = this.code;
749
+ if (code !== this._rawCode) {
750
+ this._rawCode = code;
751
+ this._normalCode = normalizeSource(code);
664
752
  }
753
+ return this._normalCode;
754
+ }
755
+ _basis() {
756
+ return { language: this.language, theme: this.theme, highlighted: canHighlight(this.language, this.theme) };
665
757
  }
666
758
  _structureFor(code) {
667
- const highlighted = canHighlight(this.language, this.theme);
759
+ const { language, theme, highlighted } = this._basis();
668
760
  let entry = this._derived.get(code);
669
- if (!entry || entry.language !== this.language || entry.theme !== this.theme || entry.highlighted !== highlighted) {
761
+ if (!entry || entry.language !== language || entry.theme !== theme || entry.highlighted !== highlighted) {
670
762
  entry = {
671
- language: this.language,
672
- theme: this.theme,
763
+ language,
764
+ theme,
673
765
  highlighted,
674
- lines: tokenizeCodeToIdLines(code, this.language, this.theme),
766
+ lines: tokenizeCodeToIdLines(code, language, theme),
675
767
  };
676
768
  this._derived.delete(code);
677
769
  this._derived.set(code, entry);
@@ -745,6 +837,18 @@ __decorate([
745
837
  __decorate([
746
838
  command()
747
839
  ], Code.prototype, "resetHighlight", null);
840
+ __decorate([
841
+ command({
842
+ args: [
843
+ { key: "target", kind: "selector" },
844
+ { key: "color", kind: "color", default: ROW_BAND_COLOR },
845
+ { key: "radius", kind: "number", default: ROW_BAND_RADIUS, min: 0, step: 1 },
846
+ ],
847
+ })
848
+ ], Code.prototype, "highlightRows", null);
849
+ __decorate([
850
+ command()
851
+ ], Code.prototype, "resetRowHighlight", null);
748
852
  Code = __decorate([
749
853
  node({
750
854
  key: "code",
@@ -758,6 +862,25 @@ Code = __decorate([
758
862
  })
759
863
  ], Code);
760
864
  export { Code };
865
+ /** Carry `from`'s line and token ids onto `to` when the two spell the same tokens. */
866
+ function adoptIds(from, to) {
867
+ if (from.length !== to.length)
868
+ return;
869
+ for (let i = 0; i < from.length; i++) {
870
+ const a = from[i].tokens;
871
+ const b = to[i].tokens;
872
+ if (a.length !== b.length)
873
+ return;
874
+ for (let j = 0; j < a.length; j++)
875
+ if (a[j].content !== b[j].content)
876
+ return;
877
+ }
878
+ for (let i = 0; i < from.length; i++) {
879
+ to[i].id = from[i].id;
880
+ for (let j = 0; j < from[i].tokens.length; j++)
881
+ to[i].tokens[j].id = from[i].tokens[j].id;
882
+ }
883
+ }
761
884
  /** Sources a node keeps tokenized, oldest dropped first; a bound `code` can name any number of them. */
762
885
  const DERIVED_LISTINGS = 16;
763
886
  /**
@@ -765,13 +888,17 @@ const DERIVED_LISTINGS = 16;
765
888
  * `CodeRange` (a plain object, not iterable) becomes a one-element array; a
766
889
  * `CodeRangeChain`/plain array of ranges spreads as-is. `CodeRange` and
767
890
  * `CodeRangeChain` are distinguished by iterability, not `instanceof` — a
768
- * bare array of `CodeRange`s (no chain) works the same way.
891
+ * bare array of `CodeRange`s (no chain) works the same way. The `{ list }` shape
892
+ * a chain used to serialise as is read as the chain it came from.
769
893
  */
770
894
  function normalizeCodeRanges(codeRange) {
771
895
  const maybeIterable = codeRange;
772
896
  if (typeof maybeIterable[Symbol.iterator] === "function") {
773
897
  return [...codeRange];
774
898
  }
899
+ const stored = codeRange.list;
900
+ if (Array.isArray(stored))
901
+ return [...stored];
775
902
  return [codeRange];
776
903
  }
777
904
  const grammarLoaders = new Map();