@motionscript/code 0.0.0-stage → 0.1.0-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 (103) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/LICENSE +201 -0
  3. package/README.md +76 -3
  4. package/dist/browser/chunks/chunk-HET2RCSD.js +2 -0
  5. package/dist/browser/chunks/chunk-HET2RCSD.js.map +7 -0
  6. package/dist/browser/chunks/chunk-SYZUZB3M.js +2 -0
  7. package/dist/browser/chunks/chunk-SYZUZB3M.js.map +7 -0
  8. package/dist/browser/chunks/dist-3F5TQHM6.js +2 -0
  9. package/dist/browser/chunks/dist-3F5TQHM6.js.map +7 -0
  10. package/dist/browser/chunks/dist-3SIR23P4.js +2 -0
  11. package/dist/browser/chunks/dist-3SIR23P4.js.map +7 -0
  12. package/dist/browser/chunks/dist-AQRNACF3.js +2 -0
  13. package/dist/browser/chunks/dist-AQRNACF3.js.map +7 -0
  14. package/dist/browser/chunks/dist-B46VH2YP.js +2 -0
  15. package/dist/browser/chunks/dist-B46VH2YP.js.map +7 -0
  16. package/dist/browser/chunks/dist-FH6BHJ6A.js +7 -0
  17. package/dist/browser/chunks/dist-FH6BHJ6A.js.map +7 -0
  18. package/dist/browser/chunks/dist-FQDUBZKU.js +2 -0
  19. package/dist/browser/chunks/dist-FQDUBZKU.js.map +7 -0
  20. package/dist/browser/chunks/dist-GPNEVCJI.js +2 -0
  21. package/dist/browser/chunks/dist-GPNEVCJI.js.map +7 -0
  22. package/dist/browser/chunks/dist-LRSXSDHT.js +2 -0
  23. package/dist/browser/chunks/dist-LRSXSDHT.js.map +7 -0
  24. package/dist/browser/chunks/dist-N4CNBNHL.js +2 -0
  25. package/dist/browser/chunks/dist-N4CNBNHL.js.map +7 -0
  26. package/dist/browser/chunks/dist-OXJDTPQ6.js +2 -0
  27. package/dist/browser/chunks/dist-OXJDTPQ6.js.map +7 -0
  28. package/dist/browser/chunks/dist-SH7H4MRK.js +2 -0
  29. package/dist/browser/chunks/dist-SH7H4MRK.js.map +7 -0
  30. package/dist/browser/chunks/dist-UG3NTKWL.js +2 -0
  31. package/dist/browser/chunks/dist-UG3NTKWL.js.map +7 -0
  32. package/dist/browser/chunks/dist-W5ZXIMV7.js +2 -0
  33. package/dist/browser/chunks/dist-W5ZXIMV7.js.map +7 -0
  34. package/dist/browser/index.js +8 -0
  35. package/dist/browser/index.js.map +7 -0
  36. package/dist/browser/manifest.json +11 -0
  37. package/dist/code-range.d.ts +69 -0
  38. package/dist/code-range.d.ts.map +1 -0
  39. package/dist/code-range.js +124 -0
  40. package/dist/code-range.js.map +1 -0
  41. package/dist/diff.d.ts +42 -0
  42. package/dist/diff.d.ts.map +1 -0
  43. package/dist/diff.js +179 -0
  44. package/dist/diff.js.map +1 -0
  45. package/dist/engine.d.ts +15 -0
  46. package/dist/engine.d.ts.map +1 -0
  47. package/dist/engine.js +17 -0
  48. package/dist/engine.js.map +1 -0
  49. package/dist/highlight.d.ts +43 -0
  50. package/dist/highlight.d.ts.map +1 -0
  51. package/dist/highlight.js +243 -0
  52. package/dist/highlight.js.map +1 -0
  53. package/dist/index.d.ts +4 -0
  54. package/dist/index.d.ts.map +1 -0
  55. package/dist/index.js +4 -0
  56. package/dist/index.js.map +1 -0
  57. package/dist/layout.d.ts +77 -0
  58. package/dist/layout.d.ts.map +1 -0
  59. package/dist/layout.js +74 -0
  60. package/dist/layout.js.map +1 -0
  61. package/dist/measure-cache.d.ts +38 -0
  62. package/dist/measure-cache.d.ts.map +1 -0
  63. package/dist/measure-cache.js +0 -0
  64. package/dist/measure-cache.js.map +1 -0
  65. package/dist/node.d.ts +293 -0
  66. package/dist/node.d.ts.map +1 -0
  67. package/dist/node.js +733 -0
  68. package/dist/node.js.map +1 -0
  69. package/dist/nodes.d.ts +18 -0
  70. package/dist/nodes.d.ts.map +1 -0
  71. package/dist/nodes.js +18 -0
  72. package/dist/nodes.js.map +1 -0
  73. package/dist/render.d.ts +28 -0
  74. package/dist/render.d.ts.map +1 -0
  75. package/dist/render.js +223 -0
  76. package/dist/render.js.map +1 -0
  77. package/dist/style.d.ts +88 -0
  78. package/dist/style.d.ts.map +1 -0
  79. package/dist/style.js +203 -0
  80. package/dist/style.js.map +1 -0
  81. package/dist/tokens.d.ts +24 -0
  82. package/dist/tokens.d.ts.map +1 -0
  83. package/dist/tokens.js +50 -0
  84. package/dist/tokens.js.map +1 -0
  85. package/dist/transitions.d.ts +115 -0
  86. package/dist/transitions.d.ts.map +1 -0
  87. package/dist/transitions.js +85 -0
  88. package/dist/transitions.js.map +1 -0
  89. package/package.json +82 -3
  90. package/registry.json +6 -0
  91. package/src/code-range.ts +156 -0
  92. package/src/diff.ts +216 -0
  93. package/src/engine.ts +21 -0
  94. package/src/highlight.ts +291 -0
  95. package/src/index.ts +3 -0
  96. package/src/layout.ts +157 -0
  97. package/src/measure-cache.ts +0 -0
  98. package/src/node.ts +834 -0
  99. package/src/nodes.ts +18 -0
  100. package/src/render.ts +247 -0
  101. package/src/style.ts +251 -0
  102. package/src/tokens.ts +75 -0
  103. package/src/transitions.ts +184 -0
package/package.json CHANGED
@@ -1,6 +1,85 @@
1
1
  {
2
2
  "name": "@motionscript/code",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.1.0-alpha.0",
4
+ "type": "module",
5
+ "description": "Animated, syntax-highlighted code blocks for Motion Script, with token-level append, insert, remove, replace, and highlight transitions.",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/motionscript-dev/motionscript.git",
9
+ "directory": "packages/components/code"
10
+ },
11
+ "homepage": "https://motionscript.dev",
12
+ "bugs": {
13
+ "url": "https://github.com/motionscript-dev/motionscript/issues"
14
+ },
15
+ "main": "dist/index.js",
16
+ "types": "dist/index.d.ts",
17
+ "sideEffects": false,
18
+ "files": [
19
+ "dist",
20
+ "src",
21
+ "registry.json",
22
+ "CHANGELOG.md",
23
+ "!dist/**/*.tsbuildinfo",
24
+ "!src/**/tests/**",
25
+ "!src/**/*.test.ts",
26
+ "!src/**/*.test.tsx",
27
+ "!src/**/*.fixtures.ts"
28
+ ],
29
+ "publishConfig": {
30
+ "access": "public"
31
+ },
32
+ "exports": {
33
+ ".": {
34
+ "types": "./dist/index.d.ts",
35
+ "default": "./dist/index.js"
36
+ }
37
+ },
38
+ "keywords": [
39
+ "motion-script",
40
+ "motion script",
41
+ "code",
42
+ "syntax-highlighting",
43
+ "animation"
44
+ ],
45
+ "license": "Apache-2.0",
46
+ "peerDependencies": {
47
+ "@motionscript/core": "^0.1.0-alpha.0"
48
+ },
49
+ "dependencies": {
50
+ "@lezer/common": "^1.5.2",
51
+ "@lezer/cpp": "^1.1.6",
52
+ "@lezer/css": "^1.3.3",
53
+ "@lezer/go": "^1.0.1",
54
+ "@lezer/highlight": "^1.2.3",
55
+ "@lezer/html": "^1.3.13",
56
+ "@lezer/java": "^1.1.3",
57
+ "@lezer/javascript": "^1.5.4",
58
+ "@lezer/json": "^1.0.3",
59
+ "@lezer/markdown": "^1.6.4",
60
+ "@lezer/php": "^1.0.5",
61
+ "@lezer/python": "^1.1.19",
62
+ "@lezer/rust": "^1.0.2",
63
+ "@lezer/xml": "^1.0.6",
64
+ "@lezer/yaml": "^1.0.4"
65
+ },
66
+ "devDependencies": {
67
+ "@types/node": "^25.6.2",
68
+ "eslint": "^9.39.4",
69
+ "eslint-import-resolver-typescript": "^4.4.4",
70
+ "eslint-plugin-import-x": "^4.16.1",
71
+ "tsc-alias": "^1.8.16",
72
+ "typescript": "^6.0.3",
73
+ "typescript-eslint": "^8.48.0",
74
+ "vitest": "^4.1.5",
75
+ "@motionscript/core": "0.1.0-alpha.0"
76
+ },
77
+ "scripts": {
78
+ "build": "tsc -p tsconfig.build.json && tsc-alias -p tsconfig.build.json --resolve-full-paths && node ../../../scripts/build-browser.mjs",
79
+ "dev": "tsc -p tsconfig.build.json --watch & tsc-alias -p tsconfig.build.json --resolve-full-paths -w",
80
+ "clean": "rimraf --glob dist .turbo *.tsbuildinfo",
81
+ "test": "vitest --passWithNoTests",
82
+ "lint": "eslint .",
83
+ "typecheck": "tsc -p tsconfig.json --noEmit"
84
+ }
6
85
  }
package/registry.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "kit": { "barrel": "src/engine.ts", "specifier": "@motionscript/code" },
3
+ "items": [
4
+ { "name": "code", "root": "src", "files": ["node.ts"], "flatten": true }
5
+ ]
6
+ }
@@ -0,0 +1,156 @@
1
+ /**
2
+ * A range over a code document. Lines and columns are 1-indexed.
3
+ *
4
+ * `endLine`/`endCol` may be `Infinity` to mean "to the end of the line" or
5
+ * "to the end of the document".
6
+ */
7
+ export interface CodeRange {
8
+ startLine: number;
9
+ startCol: number;
10
+ endLine: number;
11
+ endCol: number;
12
+ }
13
+
14
+ /**
15
+ * Create a range that spans `length` characters starting at (line, col).
16
+ * If `length` is omitted, the range extends to the end of the line.
17
+ *
18
+ * Example:
19
+ * word(1, 3, 3) // line 1, column 3, spans 3 characters
20
+ * word(1, 3) // line 1, column 3, to end of line
21
+ */
22
+ export function word(line: number, col: number, length?: number): CodeRange {
23
+ return {
24
+ startLine: line,
25
+ startCol: col,
26
+ endLine: line,
27
+ endCol: length === undefined ? Infinity : col + length,
28
+ };
29
+ }
30
+
31
+ /**
32
+ * Create a range that covers entire lines from `from` to `to` (inclusive).
33
+ * If `to` is omitted, only line `from` is covered.
34
+ *
35
+ * Example:
36
+ * lines(1, 3) // lines 1 through 3
37
+ * lines(2) // line 2 only
38
+ */
39
+ export function lines(from: number, to?: number): CodeRange {
40
+ return {
41
+ startLine: from,
42
+ startCol: 1,
43
+ endLine: to ?? from,
44
+ endCol: Infinity,
45
+ };
46
+ }
47
+
48
+
49
+
50
+ /**
51
+ * Immutable, chainable, iterable builder over multiple ranges — mirrors
52
+ * {@link FillChain}/`Fills` in `@motionscript/core`. Every method returns a
53
+ * *new* chain with one more range appended, so the next range-kind method is
54
+ * called directly rather than through a separate combinator:
55
+ *
56
+ * @example
57
+ * CodeRanges.lines(2).word(5, 1, 3) // a 2-range selection, one expression
58
+ */
59
+ export class CodeRangeChain {
60
+ constructor(public readonly list: CodeRange[] = []) { }
61
+
62
+ lines(from: number, to?: number): CodeRangeChain {
63
+ return new CodeRangeChain([...this.list, lines(from, to)]);
64
+ }
65
+
66
+ word(line: number, col: number, length?: number): CodeRangeChain {
67
+ return new CodeRangeChain([...this.list, word(line, col, length)]);
68
+ }
69
+
70
+ *[Symbol.iterator](): Iterator<CodeRange> {
71
+ yield* this.list;
72
+ }
73
+ }
74
+
75
+ /**
76
+ * Entry point for building multi-range selections fluently — an empty
77
+ * {@link CodeRangeChain}, so `CodeRanges.lines(2)` *is* `new
78
+ * CodeRangeChain().lines(2)`. `word`/`lines` stay directly importable and
79
+ * usable standalone for a single range; reach for `CodeRanges` when a call
80
+ * needs more than one.
81
+ */
82
+ export const CodeRanges = new CodeRangeChain();
83
+
84
+ /**
85
+ * Convert a CodeRange to absolute character offsets [start, end) in the joined
86
+ * source text, given the per-line lengths of that source. Lines/columns are
87
+ * clamped into the document so out-of-bounds inputs produce a valid range.
88
+ */
89
+ export function rangeToCharOffsets(
90
+ r: CodeRange,
91
+ lineLengths: number[],
92
+ ): { start: number; end: number } {
93
+ if (lineLengths.length === 0) return { start: 0, end: 0 };
94
+
95
+ // Cumulative line starts in the joined string (lines joined with '\n').
96
+ const lineStart: number[] = new Array(lineLengths.length);
97
+ let acc = 0;
98
+ for (let i = 0; i < lineLengths.length; i++) {
99
+ lineStart[i] = acc;
100
+ acc += lineLengths[i] + 1; // +1 for the joining newline
101
+ }
102
+ const docEnd = acc > 0 ? acc - 1 : 0; // strip trailing newline
103
+
104
+ const clampLine = (n: number) =>
105
+ Math.max(0, Math.min(lineLengths.length - 1, n - 1)); // to 0-indexed
106
+
107
+ // A range read back from JSON holds `Infinity` as `null`; both mean "to the end".
108
+ const endLine = r.endLine ?? Infinity;
109
+ const endCol = r.endCol ?? Infinity;
110
+
111
+ const sLine = clampLine(r.startLine);
112
+ const eLine = clampLine(endLine);
113
+ const sColMax = lineLengths[sLine];
114
+ const eColMax = lineLengths[eLine];
115
+
116
+ // Columns are 1-indexed; clamp to [1, lineLength+1] so endCol can sit past
117
+ // the last char (one-past-end).
118
+ const sCol = Math.max(0, Math.min(sColMax, r.startCol - 1));
119
+ const eCol = endCol === Infinity
120
+ ? eColMax
121
+ : Math.max(0, Math.min(eColMax, endCol - 1));
122
+
123
+ const start = Math.min(docEnd, lineStart[sLine] + sCol);
124
+ const end = Math.min(docEnd, lineStart[eLine] + eCol);
125
+ return { start: Math.min(start, end), end: Math.max(start, end) };
126
+ }
127
+
128
+ /**
129
+ * Convert [start, end) character offsets in joined source to a CodeRange.
130
+ * Lines/cols in the returned range are 1-indexed.
131
+ */
132
+ export function charOffsetsToRange(
133
+ start: number,
134
+ end: number,
135
+ lineLengths: number[],
136
+ ): CodeRange {
137
+ let off = 0;
138
+ let startLine = 1, startCol = 1, endLine = 1, endCol = 1;
139
+ let foundStart = false;
140
+ for (let li = 0; li < lineLengths.length; li++) {
141
+ const lineStart = off;
142
+ const lineEnd = off + lineLengths[li];
143
+ if (!foundStart && start >= lineStart && start <= lineEnd) {
144
+ startLine = li + 1;
145
+ startCol = (start - lineStart) + 1;
146
+ foundStart = true;
147
+ }
148
+ if (end >= lineStart && end <= lineEnd) {
149
+ endLine = li + 1;
150
+ endCol = (end - lineStart) + 1;
151
+ break;
152
+ }
153
+ off = lineEnd + 1;
154
+ }
155
+ return { startLine, startCol, endLine, endCol };
156
+ }
package/src/diff.ts ADDED
@@ -0,0 +1,216 @@
1
+ import type { IdLine, IdToken } from "./tokens";
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
+ */
22
+ export type CodeDiffStrategy = (from: IdLine[], to: IdLine[]) => CodeEdit;
23
+
24
+ export interface CodeEdit {
25
+ /** The new structure. Ids are rewritten in place onto the tokens handed in. */
26
+ lines: IdLine[];
27
+ /** Ids present only in the old structure — these fade out. */
28
+ removedIds: Set<number>;
29
+ /** Ids present only in the new structure — these fade in. */
30
+ addedIds: Set<number>;
31
+ /** Ids of {@link lines} that did not exist before — these enter as a whole row. */
32
+ newLineIds: Set<number>;
33
+ /** Previous colour of a surviving token whose highlighting changed, for the cross-fade. */
34
+ fromColorById: Map<number, string | undefined>;
35
+ }
36
+
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
+ */
45
+ const LINE_MATCH_FLOOR = 0.45;
46
+
47
+ /** Weight a token carries when judging similarity: its visible characters. */
48
+ function tokenWeight(tok: IdToken): number {
49
+ return tok.content.trim().length;
50
+ }
51
+
52
+ function lineWeight(line: IdLine): number {
53
+ let sum = 0;
54
+ for (const tok of line.tokens) sum += tokenWeight(tok);
55
+ return sum;
56
+ }
57
+
58
+ /**
59
+ * Dice coefficient over the two lines' token multisets, weighted by visible
60
+ * characters so `variable` counts for far more than a run of spaces.
61
+ *
62
+ * Two blank lines are identical (1); a blank line against a non-blank one shares
63
+ * nothing (0), which keeps the blank rows a reformat introduces from anchoring
64
+ * themselves onto real code.
65
+ */
66
+ function similarity(a: IdLine, b: IdLine): number {
67
+ const wa = lineWeight(a);
68
+ const wb = lineWeight(b);
69
+ if (wa === 0 && wb === 0) return 1;
70
+ if (wa === 0 || wb === 0) return 0;
71
+
72
+ const bag = new Map<string, number>();
73
+ for (const tok of a.tokens) {
74
+ if (tokenWeight(tok) === 0) continue;
75
+ bag.set(tok.content, (bag.get(tok.content) ?? 0) + 1);
76
+ }
77
+ let common = 0;
78
+ for (const tok of b.tokens) {
79
+ const w = tokenWeight(tok);
80
+ if (w === 0) continue;
81
+ const left = bag.get(tok.content) ?? 0;
82
+ if (left > 0) {
83
+ bag.set(tok.content, left - 1);
84
+ common += w;
85
+ }
86
+ }
87
+ return (2 * common) / (wa + wb);
88
+ }
89
+
90
+ /**
91
+ * Align two sequences by maximising total `score`, skipping is free.
92
+ *
93
+ * A plain LCS would only ever pair *identical* elements, which is the wrong tool
94
+ * twice over: two lines that differ by one word are the same line, and two
95
+ * tokens that differ only in colour are the same token. Scoring the pairing and
96
+ * letting anything that scores ≤ 0 fall out expresses that directly, and because
97
+ * skips cost nothing the alignment stays in order.
98
+ *
99
+ * Returns the matched index pairs, in order.
100
+ */
101
+ function align(
102
+ aLength: number,
103
+ bLength: number,
104
+ score: (i: number, j: number) => number,
105
+ ): Array<[number, number]> {
106
+ // dp[i][j] = best score aligning a[0..i) with b[0..j).
107
+ const width = bLength + 1;
108
+ const dp = new Float64Array((aLength + 1) * width);
109
+ const pairScore = new Float64Array(aLength * bLength);
110
+
111
+ for (let i = 1; i <= aLength; i++) {
112
+ for (let j = 1; j <= bLength; j++) {
113
+ const s = score(i - 1, j - 1);
114
+ pairScore[(i - 1) * bLength + (j - 1)] = s;
115
+ const skipA = dp[(i - 1) * width + j];
116
+ const skipB = dp[i * width + (j - 1)];
117
+ let best = skipA > skipB ? skipA : skipB;
118
+ if (s > 0) {
119
+ const paired = dp[(i - 1) * width + (j - 1)] + s;
120
+ if (paired > best) best = paired;
121
+ }
122
+ dp[i * width + j] = best;
123
+ }
124
+ }
125
+
126
+ const pairs: Array<[number, number]> = [];
127
+ let i = aLength;
128
+ let j = bLength;
129
+ while (i > 0 && j > 0) {
130
+ const s = pairScore[(i - 1) * bLength + (j - 1)];
131
+ if (s > 0 && dp[i * width + j] === dp[(i - 1) * width + (j - 1)] + s) {
132
+ pairs.push([i - 1, j - 1]);
133
+ i--; j--;
134
+ } else if (dp[(i - 1) * width + j] >= dp[i * width + (j - 1)]) {
135
+ i--;
136
+ } else {
137
+ j--;
138
+ }
139
+ }
140
+ pairs.reverse();
141
+ return pairs;
142
+ }
143
+
144
+ /**
145
+ * Match the tokens of one line against another's.
146
+ *
147
+ * Scored by visible characters rather than token count, so an alignment that
148
+ * keeps `items.reduce` together beats one that keeps four more spaces. A token
149
+ * pairs only when its content is unchanged — a changed word is a different word,
150
+ * and should be seen to be replaced.
151
+ */
152
+ function alignTokens(from: IdToken[], to: IdToken[]): Array<[number, number]> {
153
+ return align(from.length, to.length, (i, j) => {
154
+ if (from[i].content !== to[j].content) return 0;
155
+ // Whitespace still pairs (it holds indentation together), just weakly
156
+ // enough that it never outranks real content.
157
+ return tokenWeight(to[j]) || 0.25;
158
+ });
159
+ }
160
+
161
+ /**
162
+ * Carry `from`'s identities onto `to`, and report what changed.
163
+ *
164
+ * `to` is freshly tokenized and owns ids nobody has seen yet, so rewriting them
165
+ * in place is safe and is what makes the transition cheap: after this, the two
166
+ * structures share an id for every token that survived the edit.
167
+ */
168
+ export function diffCode(from: IdLine[], to: IdLine[]): CodeEdit {
169
+ const removedIds = new Set<number>();
170
+ const addedIds = new Set<number>();
171
+ const newLineIds = new Set<number>();
172
+ const fromColorById = new Map<number, string | undefined>();
173
+
174
+ const linePairs = align(from.length, to.length, (i, j) => similarity(from[i], to[j]) - LINE_MATCH_FLOOR);
175
+
176
+ const pairedTo = new Set<number>();
177
+ const pairedFrom = new Set<number>();
178
+
179
+ for (const [fi, ti] of linePairs) {
180
+ pairedFrom.add(fi);
181
+ pairedTo.add(ti);
182
+ const fromLine = from[fi];
183
+ const toLine = to[ti];
184
+ toLine.id = fromLine.id;
185
+
186
+ const matched = alignTokens(fromLine.tokens, toLine.tokens);
187
+ const keptFrom = new Set<number>();
188
+ const keptTo = new Set<number>();
189
+ for (const [a, b] of matched) {
190
+ keptFrom.add(a);
191
+ keptTo.add(b);
192
+ const oldTok = fromLine.tokens[a];
193
+ const newTok = toLine.tokens[b];
194
+ newTok.id = oldTok.id;
195
+ if (oldTok.color !== newTok.color) fromColorById.set(oldTok.id, oldTok.color);
196
+ }
197
+ for (let a = 0; a < fromLine.tokens.length; a++) {
198
+ if (!keptFrom.has(a)) removedIds.add(fromLine.tokens[a].id);
199
+ }
200
+ for (let b = 0; b < toLine.tokens.length; b++) {
201
+ if (!keptTo.has(b)) addedIds.add(toLine.tokens[b].id);
202
+ }
203
+ }
204
+
205
+ for (let i = 0; i < from.length; i++) {
206
+ if (pairedFrom.has(i)) continue;
207
+ for (const tok of from[i].tokens) removedIds.add(tok.id);
208
+ }
209
+ for (let j = 0; j < to.length; j++) {
210
+ if (pairedTo.has(j)) continue;
211
+ newLineIds.add(to[j].id);
212
+ for (const tok of to[j].tokens) addedIds.add(tok.id);
213
+ }
214
+
215
+ return { lines: to, removedIds, addedIds, newLineIds, fromColorById };
216
+ }
package/src/engine.ts ADDED
@@ -0,0 +1,21 @@
1
+ // The Code "engine": syntax highlighting, tokenizing, layout, diffing, and
2
+ // transition/phase scheduling — kept separate from node.ts so `ms add code`
3
+ // can copy just the node 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
+ // packages/site/scripts/build-registry.mjs enforces that anything it
7
+ // reaches for is either copied alongside it or re-exported from here.
8
+ export * from './transitions';
9
+ export * from './layout';
10
+ export { drawCode } from './render';
11
+ export type { DrawCodeState } from './render';
12
+ export { diffCode as defaultCodeDiff } from './diff';
13
+ export type { CodeDiffStrategy, CodeEdit } from './diff';
14
+ export { loadCodeLanguage, initSyntaxHighlighter, ensureHighlighter, registerCodeTheme, resolveTheme, canHighlight } from './highlight';
15
+ export { tokenizeCodeToIdLines } from './tokens';
16
+ export type { IdLine, IdToken } from './tokens';
17
+ export { DefaultHighlightStyle, GithubDarkStyle, GithubLightStyle, VscodeDarkStyle, VscodeLightStyle, BUILTIN_THEMES, compileStyle } from './style';
18
+ export type { CodeHighlightStyle, CodeStyleRule, CodeTheme, CodeThemeName } from './style';
19
+ export { TokenAdvanceCache } from './measure-cache';
20
+ export { word, lines, rangeToCharOffsets, charOffsetsToRange, CodeRanges, CodeRangeChain } from './code-range';
21
+ export type { CodeRange } from './code-range';