@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/dist/diff.d.ts ADDED
@@ -0,0 +1,42 @@
1
+ import type { IdLine } from "./tokens.js";
2
+ /**
3
+ * The result of diffing one listing structure against another: the *next*
4
+ * structure, with token and line ids carried over from the previous one
5
+ * wherever the content survived, plus the three sets a transition needs to know
6
+ * which role each token plays.
7
+ *
8
+ * Identity is the whole product here. `const variable = 3` becoming
9
+ * `const number = 3` should move `const` and `= 3` rather than dissolve and
10
+ * rebuild them, and a token can only *move* if the frame after the edit still
11
+ * knows it is the same token.
12
+ */
13
+ /**
14
+ * How a `Code` node decides which token became which across an edit. Given
15
+ * the old and new tokenized structures, return the carried-over structure
16
+ * plus the added/removed/renamed-line bookkeeping a transition needs.
17
+ * `diffCode` below is the default implementation (exported as
18
+ * `defaultCodeDiff`) — pass a `diffStrategy` of this shape to `<Code>` to
19
+ * replace it with your own.
20
+ */
21
+ export type CodeDiffStrategy = (from: IdLine[], to: IdLine[]) => CodeEdit;
22
+ export interface CodeEdit {
23
+ /** The new structure. Ids are rewritten in place onto the tokens handed in. */
24
+ lines: IdLine[];
25
+ /** Ids present only in the old structure — these fade out. */
26
+ removedIds: Set<number>;
27
+ /** Ids present only in the new structure — these fade in. */
28
+ addedIds: Set<number>;
29
+ /** Ids of {@link lines} that did not exist before — these enter as a whole row. */
30
+ newLineIds: Set<number>;
31
+ /** Previous colour of a surviving token whose highlighting changed, for the cross-fade. */
32
+ fromColorById: Map<number, string | undefined>;
33
+ }
34
+ /**
35
+ * Carry `from`'s identities onto `to`, and report what changed.
36
+ *
37
+ * `to` is freshly tokenized and owns ids nobody has seen yet, so rewriting them
38
+ * in place is safe and is what makes the transition cheap: after this, the two
39
+ * structures share an id for every token that survived the edit.
40
+ */
41
+ export declare function diffCode(from: IdLine[], to: IdLine[]): CodeEdit;
42
+ //# sourceMappingURL=diff.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"diff.d.ts","sourceRoot":"","sources":["../src/diff.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAW,MAAM,UAAU,CAAC;AAEhD;;;;;;;;;;GAUG;AACH;;;;;;;GAOG;AACH,MAAM,MAAM,gBAAgB,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,MAAM,EAAE,KAAK,QAAQ,CAAC;AAE1E,MAAM,WAAW,QAAQ;IACrB,+EAA+E;IAC/E,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,8DAA8D;IAC9D,UAAU,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IACxB,6DAA6D;IAC7D,QAAQ,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IACtB,mFAAmF;IACnF,UAAU,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IACxB,2FAA2F;IAC3F,aAAa,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC;CAClD;AA8HD;;;;;;GAMG;AACH,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,MAAM,EAAE,GAAG,QAAQ,CAgD/D"}
package/dist/diff.js ADDED
@@ -0,0 +1,179 @@
1
+ /**
2
+ * How similar two lines must be before they are treated as *one line that was
3
+ * edited* rather than as one line deleted and another inserted.
4
+ *
5
+ * Below this, a row's tokens have so little in common that carrying ids across
6
+ * would animate glyphs sliding between unrelated words. Above it, the edit reads
7
+ * the way it was made.
8
+ */
9
+ const LINE_MATCH_FLOOR = 0.45;
10
+ /** Weight a token carries when judging similarity: its visible characters. */
11
+ function tokenWeight(tok) {
12
+ return tok.content.trim().length;
13
+ }
14
+ function lineWeight(line) {
15
+ let sum = 0;
16
+ for (const tok of line.tokens)
17
+ sum += tokenWeight(tok);
18
+ return sum;
19
+ }
20
+ /**
21
+ * Dice coefficient over the two lines' token multisets, weighted by visible
22
+ * characters so `variable` counts for far more than a run of spaces.
23
+ *
24
+ * Two blank lines are identical (1); a blank line against a non-blank one shares
25
+ * nothing (0), which keeps the blank rows a reformat introduces from anchoring
26
+ * themselves onto real code.
27
+ */
28
+ function similarity(a, b) {
29
+ const wa = lineWeight(a);
30
+ const wb = lineWeight(b);
31
+ if (wa === 0 && wb === 0)
32
+ return 1;
33
+ if (wa === 0 || wb === 0)
34
+ return 0;
35
+ const bag = new Map();
36
+ for (const tok of a.tokens) {
37
+ if (tokenWeight(tok) === 0)
38
+ continue;
39
+ bag.set(tok.content, (bag.get(tok.content) ?? 0) + 1);
40
+ }
41
+ let common = 0;
42
+ for (const tok of b.tokens) {
43
+ const w = tokenWeight(tok);
44
+ if (w === 0)
45
+ continue;
46
+ const left = bag.get(tok.content) ?? 0;
47
+ if (left > 0) {
48
+ bag.set(tok.content, left - 1);
49
+ common += w;
50
+ }
51
+ }
52
+ return (2 * common) / (wa + wb);
53
+ }
54
+ /**
55
+ * Align two sequences by maximising total `score`, skipping is free.
56
+ *
57
+ * A plain LCS would only ever pair *identical* elements, which is the wrong tool
58
+ * twice over: two lines that differ by one word are the same line, and two
59
+ * tokens that differ only in colour are the same token. Scoring the pairing and
60
+ * letting anything that scores ≤ 0 fall out expresses that directly, and because
61
+ * skips cost nothing the alignment stays in order.
62
+ *
63
+ * Returns the matched index pairs, in order.
64
+ */
65
+ function align(aLength, bLength, score) {
66
+ // dp[i][j] = best score aligning a[0..i) with b[0..j).
67
+ const width = bLength + 1;
68
+ const dp = new Float64Array((aLength + 1) * width);
69
+ const pairScore = new Float64Array(aLength * bLength);
70
+ for (let i = 1; i <= aLength; i++) {
71
+ for (let j = 1; j <= bLength; j++) {
72
+ const s = score(i - 1, j - 1);
73
+ pairScore[(i - 1) * bLength + (j - 1)] = s;
74
+ const skipA = dp[(i - 1) * width + j];
75
+ const skipB = dp[i * width + (j - 1)];
76
+ let best = skipA > skipB ? skipA : skipB;
77
+ if (s > 0) {
78
+ const paired = dp[(i - 1) * width + (j - 1)] + s;
79
+ if (paired > best)
80
+ best = paired;
81
+ }
82
+ dp[i * width + j] = best;
83
+ }
84
+ }
85
+ const pairs = [];
86
+ let i = aLength;
87
+ let j = bLength;
88
+ while (i > 0 && j > 0) {
89
+ const s = pairScore[(i - 1) * bLength + (j - 1)];
90
+ if (s > 0 && dp[i * width + j] === dp[(i - 1) * width + (j - 1)] + s) {
91
+ pairs.push([i - 1, j - 1]);
92
+ i--;
93
+ j--;
94
+ }
95
+ else if (dp[(i - 1) * width + j] >= dp[i * width + (j - 1)]) {
96
+ i--;
97
+ }
98
+ else {
99
+ j--;
100
+ }
101
+ }
102
+ pairs.reverse();
103
+ return pairs;
104
+ }
105
+ /**
106
+ * Match the tokens of one line against another's.
107
+ *
108
+ * Scored by visible characters rather than token count, so an alignment that
109
+ * keeps `items.reduce` together beats one that keeps four more spaces. A token
110
+ * pairs only when its content is unchanged — a changed word is a different word,
111
+ * and should be seen to be replaced.
112
+ */
113
+ function alignTokens(from, to) {
114
+ return align(from.length, to.length, (i, j) => {
115
+ if (from[i].content !== to[j].content)
116
+ return 0;
117
+ // Whitespace still pairs (it holds indentation together), just weakly
118
+ // enough that it never outranks real content.
119
+ return tokenWeight(to[j]) || 0.25;
120
+ });
121
+ }
122
+ /**
123
+ * Carry `from`'s identities onto `to`, and report what changed.
124
+ *
125
+ * `to` is freshly tokenized and owns ids nobody has seen yet, so rewriting them
126
+ * in place is safe and is what makes the transition cheap: after this, the two
127
+ * structures share an id for every token that survived the edit.
128
+ */
129
+ export function diffCode(from, to) {
130
+ const removedIds = new Set();
131
+ const addedIds = new Set();
132
+ const newLineIds = new Set();
133
+ const fromColorById = new Map();
134
+ const linePairs = align(from.length, to.length, (i, j) => similarity(from[i], to[j]) - LINE_MATCH_FLOOR);
135
+ const pairedTo = new Set();
136
+ const pairedFrom = new Set();
137
+ for (const [fi, ti] of linePairs) {
138
+ pairedFrom.add(fi);
139
+ pairedTo.add(ti);
140
+ const fromLine = from[fi];
141
+ const toLine = to[ti];
142
+ toLine.id = fromLine.id;
143
+ const matched = alignTokens(fromLine.tokens, toLine.tokens);
144
+ const keptFrom = new Set();
145
+ const keptTo = new Set();
146
+ for (const [a, b] of matched) {
147
+ keptFrom.add(a);
148
+ keptTo.add(b);
149
+ const oldTok = fromLine.tokens[a];
150
+ const newTok = toLine.tokens[b];
151
+ newTok.id = oldTok.id;
152
+ if (oldTok.color !== newTok.color)
153
+ fromColorById.set(oldTok.id, oldTok.color);
154
+ }
155
+ for (let a = 0; a < fromLine.tokens.length; a++) {
156
+ if (!keptFrom.has(a))
157
+ removedIds.add(fromLine.tokens[a].id);
158
+ }
159
+ for (let b = 0; b < toLine.tokens.length; b++) {
160
+ if (!keptTo.has(b))
161
+ addedIds.add(toLine.tokens[b].id);
162
+ }
163
+ }
164
+ for (let i = 0; i < from.length; i++) {
165
+ if (pairedFrom.has(i))
166
+ continue;
167
+ for (const tok of from[i].tokens)
168
+ removedIds.add(tok.id);
169
+ }
170
+ for (let j = 0; j < to.length; j++) {
171
+ if (pairedTo.has(j))
172
+ continue;
173
+ newLineIds.add(to[j].id);
174
+ for (const tok of to[j].tokens)
175
+ addedIds.add(tok.id);
176
+ }
177
+ return { lines: to, removedIds, addedIds, newLineIds, fromColorById };
178
+ }
179
+ //# sourceMappingURL=diff.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"diff.js","sourceRoot":"","sources":["../src/diff.ts"],"names":[],"mappings":"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","sourcesContent":["import type { IdLine, IdToken } from \"./tokens\";\n\n/**\n * The result of diffing one listing structure against another: the *next*\n * structure, with token and line ids carried over from the previous one\n * wherever the content survived, plus the three sets a transition needs to know\n * which role each token plays.\n *\n * Identity is the whole product here. `const variable = 3` becoming\n * `const number = 3` should move `const` and `= 3` rather than dissolve and\n * rebuild them, and a token can only *move* if the frame after the edit still\n * knows it is the same token.\n */\n/**\n * How a `Code` node decides which token became which across an edit. Given\n * the old and new tokenized structures, return the carried-over structure\n * plus the added/removed/renamed-line bookkeeping a transition needs.\n * `diffCode` below is the default implementation (exported as\n * `defaultCodeDiff`) — pass a `diffStrategy` of this shape to `<Code>` to\n * replace it with your own.\n */\nexport type CodeDiffStrategy = (from: IdLine[], to: IdLine[]) => CodeEdit;\n\nexport interface CodeEdit {\n /** The new structure. Ids are rewritten in place onto the tokens handed in. */\n lines: IdLine[];\n /** Ids present only in the old structure — these fade out. */\n removedIds: Set<number>;\n /** Ids present only in the new structure — these fade in. */\n addedIds: Set<number>;\n /** Ids of {@link lines} that did not exist before — these enter as a whole row. */\n newLineIds: Set<number>;\n /** Previous colour of a surviving token whose highlighting changed, for the cross-fade. */\n fromColorById: Map<number, string | undefined>;\n}\n\n/**\n * How similar two lines must be before they are treated as *one line that was\n * edited* rather than as one line deleted and another inserted.\n *\n * Below this, a row's tokens have so little in common that carrying ids across\n * would animate glyphs sliding between unrelated words. Above it, the edit reads\n * the way it was made.\n */\nconst LINE_MATCH_FLOOR = 0.45;\n\n/** Weight a token carries when judging similarity: its visible characters. */\nfunction tokenWeight(tok: IdToken): number {\n return tok.content.trim().length;\n}\n\nfunction lineWeight(line: IdLine): number {\n let sum = 0;\n for (const tok of line.tokens) sum += tokenWeight(tok);\n return sum;\n}\n\n/**\n * Dice coefficient over the two lines' token multisets, weighted by visible\n * characters so `variable` counts for far more than a run of spaces.\n *\n * Two blank lines are identical (1); a blank line against a non-blank one shares\n * nothing (0), which keeps the blank rows a reformat introduces from anchoring\n * themselves onto real code.\n */\nfunction similarity(a: IdLine, b: IdLine): number {\n const wa = lineWeight(a);\n const wb = lineWeight(b);\n if (wa === 0 && wb === 0) return 1;\n if (wa === 0 || wb === 0) return 0;\n\n const bag = new Map<string, number>();\n for (const tok of a.tokens) {\n if (tokenWeight(tok) === 0) continue;\n bag.set(tok.content, (bag.get(tok.content) ?? 0) + 1);\n }\n let common = 0;\n for (const tok of b.tokens) {\n const w = tokenWeight(tok);\n if (w === 0) continue;\n const left = bag.get(tok.content) ?? 0;\n if (left > 0) {\n bag.set(tok.content, left - 1);\n common += w;\n }\n }\n return (2 * common) / (wa + wb);\n}\n\n/**\n * Align two sequences by maximising total `score`, skipping is free.\n *\n * A plain LCS would only ever pair *identical* elements, which is the wrong tool\n * twice over: two lines that differ by one word are the same line, and two\n * tokens that differ only in colour are the same token. Scoring the pairing and\n * letting anything that scores ≤ 0 fall out expresses that directly, and because\n * skips cost nothing the alignment stays in order.\n *\n * Returns the matched index pairs, in order.\n */\nfunction align(\n aLength: number,\n bLength: number,\n score: (i: number, j: number) => number,\n): Array<[number, number]> {\n // dp[i][j] = best score aligning a[0..i) with b[0..j).\n const width = bLength + 1;\n const dp = new Float64Array((aLength + 1) * width);\n const pairScore = new Float64Array(aLength * bLength);\n\n for (let i = 1; i <= aLength; i++) {\n for (let j = 1; j <= bLength; j++) {\n const s = score(i - 1, j - 1);\n pairScore[(i - 1) * bLength + (j - 1)] = s;\n const skipA = dp[(i - 1) * width + j];\n const skipB = dp[i * width + (j - 1)];\n let best = skipA > skipB ? skipA : skipB;\n if (s > 0) {\n const paired = dp[(i - 1) * width + (j - 1)] + s;\n if (paired > best) best = paired;\n }\n dp[i * width + j] = best;\n }\n }\n\n const pairs: Array<[number, number]> = [];\n let i = aLength;\n let j = bLength;\n while (i > 0 && j > 0) {\n const s = pairScore[(i - 1) * bLength + (j - 1)];\n if (s > 0 && dp[i * width + j] === dp[(i - 1) * width + (j - 1)] + s) {\n pairs.push([i - 1, j - 1]);\n i--; j--;\n } else if (dp[(i - 1) * width + j] >= dp[i * width + (j - 1)]) {\n i--;\n } else {\n j--;\n }\n }\n pairs.reverse();\n return pairs;\n}\n\n/**\n * Match the tokens of one line against another's.\n *\n * Scored by visible characters rather than token count, so an alignment that\n * keeps `items.reduce` together beats one that keeps four more spaces. A token\n * pairs only when its content is unchanged — a changed word is a different word,\n * and should be seen to be replaced.\n */\nfunction alignTokens(from: IdToken[], to: IdToken[]): Array<[number, number]> {\n return align(from.length, to.length, (i, j) => {\n if (from[i].content !== to[j].content) return 0;\n // Whitespace still pairs (it holds indentation together), just weakly\n // enough that it never outranks real content.\n return tokenWeight(to[j]) || 0.25;\n });\n}\n\n/**\n * Carry `from`'s identities onto `to`, and report what changed.\n *\n * `to` is freshly tokenized and owns ids nobody has seen yet, so rewriting them\n * in place is safe and is what makes the transition cheap: after this, the two\n * structures share an id for every token that survived the edit.\n */\nexport function diffCode(from: IdLine[], to: IdLine[]): CodeEdit {\n const removedIds = new Set<number>();\n const addedIds = new Set<number>();\n const newLineIds = new Set<number>();\n const fromColorById = new Map<number, string | undefined>();\n\n const linePairs = align(from.length, to.length, (i, j) => similarity(from[i], to[j]) - LINE_MATCH_FLOOR);\n\n const pairedTo = new Set<number>();\n const pairedFrom = new Set<number>();\n\n for (const [fi, ti] of linePairs) {\n pairedFrom.add(fi);\n pairedTo.add(ti);\n const fromLine = from[fi];\n const toLine = to[ti];\n toLine.id = fromLine.id;\n\n const matched = alignTokens(fromLine.tokens, toLine.tokens);\n const keptFrom = new Set<number>();\n const keptTo = new Set<number>();\n for (const [a, b] of matched) {\n keptFrom.add(a);\n keptTo.add(b);\n const oldTok = fromLine.tokens[a];\n const newTok = toLine.tokens[b];\n newTok.id = oldTok.id;\n if (oldTok.color !== newTok.color) fromColorById.set(oldTok.id, oldTok.color);\n }\n for (let a = 0; a < fromLine.tokens.length; a++) {\n if (!keptFrom.has(a)) removedIds.add(fromLine.tokens[a].id);\n }\n for (let b = 0; b < toLine.tokens.length; b++) {\n if (!keptTo.has(b)) addedIds.add(toLine.tokens[b].id);\n }\n }\n\n for (let i = 0; i < from.length; i++) {\n if (pairedFrom.has(i)) continue;\n for (const tok of from[i].tokens) removedIds.add(tok.id);\n }\n for (let j = 0; j < to.length; j++) {\n if (pairedTo.has(j)) continue;\n newLineIds.add(to[j].id);\n for (const tok of to[j].tokens) addedIds.add(tok.id);\n }\n\n return { lines: to, removedIds, addedIds, newLineIds, fromColorById };\n}\n"]}
@@ -0,0 +1,15 @@
1
+ export * from './transitions.js';
2
+ export * from './layout.js';
3
+ export { drawCode } from './render.js';
4
+ export type { DrawCodeState } from './render.js';
5
+ export { diffCode as defaultCodeDiff } from './diff.js';
6
+ export type { CodeDiffStrategy, CodeEdit } from './diff.js';
7
+ export { loadCodeLanguage, initSyntaxHighlighter, ensureHighlighter, registerCodeTheme, resolveTheme, canHighlight } from './highlight.js';
8
+ export { tokenizeCodeToIdLines } from './tokens.js';
9
+ export type { IdLine, IdToken } from './tokens.js';
10
+ export { DefaultHighlightStyle, GithubDarkStyle, GithubLightStyle, VscodeDarkStyle, VscodeLightStyle, BUILTIN_THEMES, compileStyle } from './style.js';
11
+ export type { CodeHighlightStyle, CodeStyleRule, CodeTheme, CodeThemeName } from './style.js';
12
+ export { TokenAdvanceCache } from './measure-cache.js';
13
+ export { word, lines, rangeToCharOffsets, charOffsetsToRange, CodeRanges, CodeRangeChain } from './code-range.js';
14
+ export type { CodeRange } from './code-range.js';
15
+ //# sourceMappingURL=engine.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../src/engine.ts"],"names":[],"mappings":"AAOA,cAAc,eAAe,CAAC;AAC9B,cAAc,UAAU,CAAC;AACzB,OAAO,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAC;AACpC,YAAY,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAC9C,OAAO,EAAE,QAAQ,IAAI,eAAe,EAAE,MAAM,QAAQ,CAAC;AACrD,YAAY,EAAE,gBAAgB,EAAE,QAAQ,EAAE,MAAM,QAAQ,CAAC;AACzD,OAAO,EAAE,gBAAgB,EAAE,qBAAqB,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AACxI,OAAO,EAAE,qBAAqB,EAAE,MAAM,UAAU,CAAC;AACjD,YAAY,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;AAChD,OAAO,EAAE,qBAAqB,EAAE,eAAe,EAAE,gBAAgB,EAAE,eAAe,EAAE,gBAAgB,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACpJ,YAAY,EAAE,kBAAkB,EAAE,aAAa,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAC3F,OAAO,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AACpD,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAC/G,YAAY,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC"}
package/dist/engine.js ADDED
@@ -0,0 +1,17 @@
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.js';
9
+ export * from './layout.js';
10
+ export { drawCode } from './render.js';
11
+ export { diffCode as defaultCodeDiff } from './diff.js';
12
+ export { loadCodeLanguage, initSyntaxHighlighter, ensureHighlighter, registerCodeTheme, resolveTheme, canHighlight } from './highlight.js';
13
+ export { tokenizeCodeToIdLines } from './tokens.js';
14
+ export { DefaultHighlightStyle, GithubDarkStyle, GithubLightStyle, VscodeDarkStyle, VscodeLightStyle, BUILTIN_THEMES, compileStyle } from './style.js';
15
+ export { TokenAdvanceCache } from './measure-cache.js';
16
+ export { word, lines, rangeToCharOffsets, charOffsetsToRange, CodeRanges, CodeRangeChain } from './code-range.js';
17
+ //# sourceMappingURL=engine.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"engine.js","sourceRoot":"","sources":["../src/engine.ts"],"names":[],"mappings":"AAAA,2EAA2E;AAC3E,4EAA4E;AAC5E,4DAA4D;AAC5D,kEAAkE;AAClE,yEAAyE;AACzE,qEAAqE;AACrE,sEAAsE;AACtE,cAAc,eAAe,CAAC;AAC9B,cAAc,UAAU,CAAC;AACzB,OAAO,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAC;AAEpC,OAAO,EAAE,QAAQ,IAAI,eAAe,EAAE,MAAM,QAAQ,CAAC;AAErD,OAAO,EAAE,gBAAgB,EAAE,qBAAqB,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AACxI,OAAO,EAAE,qBAAqB,EAAE,MAAM,UAAU,CAAC;AAEjD,OAAO,EAAE,qBAAqB,EAAE,eAAe,EAAE,gBAAgB,EAAE,eAAe,EAAE,gBAAgB,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAEpJ,OAAO,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AACpD,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC","sourcesContent":["// The Code \"engine\": syntax highlighting, tokenizing, layout, diffing, and\n// transition/phase scheduling — kept separate from node.ts so `ms add code`\n// can copy just the node declaration and import this behind\n// `@motionscript/code` instead. What gets copied is whatever this\n// package's registry.json declares, which for this component is node.ts;\n// packages/site/scripts/build-registry.mjs enforces that anything it\n// reaches for is either copied alongside it or re-exported from here.\nexport * from './transitions';\nexport * from './layout';\nexport { drawCode } from './render';\nexport type { DrawCodeState } from './render';\nexport { diffCode as defaultCodeDiff } from './diff';\nexport type { CodeDiffStrategy, CodeEdit } from './diff';\nexport { loadCodeLanguage, initSyntaxHighlighter, ensureHighlighter, registerCodeTheme, resolveTheme, canHighlight } from './highlight';\nexport { tokenizeCodeToIdLines } from './tokens';\nexport type { IdLine, IdToken } from './tokens';\nexport { DefaultHighlightStyle, GithubDarkStyle, GithubLightStyle, VscodeDarkStyle, VscodeLightStyle, BUILTIN_THEMES, compileStyle } from './style';\nexport type { CodeHighlightStyle, CodeStyleRule, CodeTheme, CodeThemeName } from './style';\nexport { TokenAdvanceCache } from './measure-cache';\nexport { word, lines, rangeToCharOffsets, charOffsetsToRange, CodeRanges, CodeRangeChain } from './code-range';\nexport type { CodeRange } from './code-range';\n"]}
@@ -0,0 +1,43 @@
1
+ import { CodeHighlightStyle, CodeTheme } from "./style.js";
2
+ export declare const DEFAULT_THEMES: string[];
3
+ export declare const DEFAULT_LANGS: string[];
4
+ /** Register a custom {@link CodeHighlightStyle} so Code nodes can reference it by name. */
5
+ export declare function registerCodeTheme(style: CodeHighlightStyle): void;
6
+ /**
7
+ * Resolve a theme reference (a registered name, or a CodeHighlightStyle object)
8
+ * to a concrete style. Unknown names fall back to the Motion Canvas default so a
9
+ * stray theme string never throws.
10
+ */
11
+ export declare function resolveTheme(theme: CodeTheme): CodeHighlightStyle;
12
+ /**
13
+ * Load the parsers for `langs` (themes are synchronous, so they're a no-op here
14
+ * but kept in the signature for API parity with the former Shiki implementation).
15
+ * Resolves once every requested language's parser is available. Unknown languages
16
+ * resolve without loading anything — {@link canHighlight} stays false for them and
17
+ * the node renders them as plain text.
18
+ */
19
+ export declare function ensureHighlighter(_themes?: string[], langs?: string[]): Promise<void>;
20
+ /**
21
+ * Load a syntax-highlight language into the shared highlighter. Exposed so a Code
22
+ * node's asset loader can run it on the timeline, and so apps can preload
23
+ * languages explicitly. Returns once the parser is ready (or immediately for an
24
+ * unsupported language). The `theme` arg is accepted for backwards compatibility.
25
+ */
26
+ export declare function loadCodeLanguage(language: string, _theme?: string): Promise<void>;
27
+ export declare function initSyntaxHighlighter(themes?: string[], langs?: string[]): Promise<void>;
28
+ /** True once `language` can be tokenized (its parser has loaded). */
29
+ export declare function canHighlight(language: string, _theme?: CodeTheme): boolean;
30
+ /**
31
+ * Tokenize `code` into per-line, colored tokens — the contract the rest of the
32
+ * Code component consumes. Combines {@link tokenizeCode} (structural split) with
33
+ * {@link highlightAt} (per-run color), then breaks tokens at newlines so each
34
+ * output line is a row of single-line, single-color tokens.
35
+ *
36
+ * Callers must have checked {@link canHighlight} first; this returns a single
37
+ * uncolored line per source line if the parser isn't loaded.
38
+ */
39
+ export declare function highlightToTokens(code: string, language: string, theme?: CodeTheme): {
40
+ content: string;
41
+ color?: string;
42
+ }[][];
43
+ //# sourceMappingURL=highlight.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"highlight.d.ts","sourceRoot":"","sources":["../src/highlight.ts"],"names":[],"mappings":"AAEA,OAAO,EAEH,kBAAkB,EAClB,SAAS,EAGZ,MAAM,SAAS,CAAC;AA0DjB,eAAO,MAAM,cAAc,UAA+B,CAAC;AAC3D,eAAO,MAAM,aAAa,UAAiD,CAAC;AAO5E,2FAA2F;AAC3F,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,kBAAkB,GAAG,IAAI,CAEjE;AAED;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,SAAS,GAAG,kBAAkB,CAGjE;AAOD;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAC7B,OAAO,GAAE,MAAM,EAAmB,EAClC,KAAK,GAAE,MAAM,EAAkB,GAChC,OAAO,CAAC,IAAI,CAAC,CAyBf;AAED;;;;;GAKG;AACH,wBAAsB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAEvF;AAED,wBAAsB,qBAAqB,CACvC,MAAM,GAAE,MAAM,EAAmB,EACjC,KAAK,GAAE,MAAM,EAAkB,GAChC,OAAO,CAAC,IAAI,CAAC,CAEf;AAED,qEAAqE;AACrE,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,SAAS,GAAG,OAAO,CAE1E;AAqFD;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAC7B,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,MAAM,EAChB,KAAK,GAAE,SAAiC,GACzC;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,EAAE,EAAE,CAyCzC"}
@@ -0,0 +1,243 @@
1
+ import { highlightTree } from "@lezer/highlight";
2
+ import { BUILTIN_THEMES, DefaultHighlightStyle, compileStyle, } from "./style.js";
3
+ /**
4
+ * Lezer-backed syntax highlighting for the Code component, replacing the former
5
+ * Shiki wiring. This mirrors Motion Canvas's `LezerHighlighter`:
6
+ *
7
+ * - {@link prepareHighlight} parses code once and builds a `from:to` → color map
8
+ * by walking {@link highlightTree}.
9
+ * - {@link tokenizeCode} splits code at parse-tree leaf boundaries.
10
+ * - {@link highlightAt} resolves the color (and run length) for a character index.
11
+ *
12
+ * Parsers load lazily and asynchronously (dynamic import) so the bundle doesn't
13
+ * pull in every grammar. The node's asset loader awaits {@link ensureHighlighter}
14
+ * before tokenizing; until a parser resolves, callers fall back to plain text and
15
+ * re-tokenize once {@link canHighlight} returns true.
16
+ */
17
+ /** Dynamic-import factories for each supported language → a Lezer {@link Parser}. */
18
+ const LANGUAGE_LOADERS = {
19
+ typescript: async () => (await import("@lezer/javascript")).parser.configure({ dialect: "ts" }),
20
+ tsx: async () => (await import("@lezer/javascript")).parser.configure({ dialect: "ts jsx" }),
21
+ javascript: async () => (await import("@lezer/javascript")).parser,
22
+ jsx: async () => (await import("@lezer/javascript")).parser.configure({ dialect: "jsx" }),
23
+ python: async () => (await import("@lezer/python")).parser,
24
+ json: async () => (await import("@lezer/json")).parser,
25
+ html: async () => (await import("@lezer/html")).parser,
26
+ css: async () => (await import("@lezer/css")).parser,
27
+ rust: async () => (await import("@lezer/rust")).parser,
28
+ go: async () => (await import("@lezer/go")).parser,
29
+ java: async () => (await import("@lezer/java")).parser,
30
+ cpp: async () => (await import("@lezer/cpp")).parser,
31
+ php: async () => (await import("@lezer/php")).parser,
32
+ markdown: async () => (await import("@lezer/markdown")).parser,
33
+ xml: async () => (await import("@lezer/xml")).parser,
34
+ yaml: async () => (await import("@lezer/yaml")).parser,
35
+ };
36
+ // Aliases so common language strings resolve to a loader above.
37
+ const LANGUAGE_ALIASES = {
38
+ ts: "typescript",
39
+ js: "javascript",
40
+ py: "python",
41
+ "c++": "cpp",
42
+ md: "markdown",
43
+ yml: "yaml",
44
+ shell: "javascript", // no shell grammar bundled; fall back rather than throw
45
+ };
46
+ function resolveLanguageKey(language) {
47
+ const key = language.toLowerCase();
48
+ return LANGUAGE_ALIASES[key] ?? key;
49
+ }
50
+ // Loaded parsers and in-flight loads, keyed by resolved language key, so
51
+ // concurrent Code nodes asking for the same language share one dynamic import.
52
+ const loadedParsers = new Map();
53
+ const inFlightLangs = new Map();
54
+ export const DEFAULT_THEMES = [DefaultHighlightStyle.name];
55
+ export const DEFAULT_LANGS = ["typescript", "javascript", "json", "python"];
56
+ // Theme registry, seeded with the built-ins (Motion Canvas default + GitHub
57
+ // dark/light). The `theme` prop is a string name or a CodeHighlightStyle object;
58
+ // apps can register additional styles by name and reference them by that name.
59
+ const themesByName = new Map(Object.entries(BUILTIN_THEMES));
60
+ /** Register a custom {@link CodeHighlightStyle} so Code nodes can reference it by name. */
61
+ export function registerCodeTheme(style) {
62
+ themesByName.set(style.name, style);
63
+ }
64
+ /**
65
+ * Resolve a theme reference (a registered name, or a CodeHighlightStyle object)
66
+ * to a concrete style. Unknown names fall back to the Motion Canvas default so a
67
+ * stray theme string never throws.
68
+ */
69
+ export function resolveTheme(theme) {
70
+ if (typeof theme !== "string")
71
+ return theme;
72
+ return themesByName.get(theme) ?? DefaultHighlightStyle;
73
+ }
74
+ /** Has `language` finished loading its parser? */
75
+ function isLanguageLoaded(language) {
76
+ return loadedParsers.has(resolveLanguageKey(language));
77
+ }
78
+ /**
79
+ * Load the parsers for `langs` (themes are synchronous, so they're a no-op here
80
+ * but kept in the signature for API parity with the former Shiki implementation).
81
+ * Resolves once every requested language's parser is available. Unknown languages
82
+ * resolve without loading anything — {@link canHighlight} stays false for them and
83
+ * the node renders them as plain text.
84
+ */
85
+ export function ensureHighlighter(_themes = DEFAULT_THEMES, langs = DEFAULT_LANGS) {
86
+ const jobs = [];
87
+ for (const lang of langs) {
88
+ const key = resolveLanguageKey(lang);
89
+ if (loadedParsers.has(key))
90
+ continue;
91
+ const loader = LANGUAGE_LOADERS[key];
92
+ if (!loader)
93
+ continue; // unsupported language; leave it unhighlighted
94
+ let job = inFlightLangs.get(key);
95
+ if (!job) {
96
+ job = loader()
97
+ .then(parser => {
98
+ loadedParsers.set(key, parser);
99
+ return parser;
100
+ })
101
+ .catch(() => null)
102
+ .finally(() => inFlightLangs.delete(key));
103
+ inFlightLangs.set(key, job);
104
+ }
105
+ jobs.push(job);
106
+ }
107
+ return jobs.length === 0 ? Promise.resolve() : Promise.all(jobs).then(() => undefined);
108
+ }
109
+ /**
110
+ * Load a syntax-highlight language into the shared highlighter. Exposed so a Code
111
+ * node's asset loader can run it on the timeline, and so apps can preload
112
+ * languages explicitly. Returns once the parser is ready (or immediately for an
113
+ * unsupported language). The `theme` arg is accepted for backwards compatibility.
114
+ */
115
+ export async function loadCodeLanguage(language, _theme) {
116
+ await ensureHighlighter(DEFAULT_THEMES, [language]);
117
+ }
118
+ export async function initSyntaxHighlighter(themes = DEFAULT_THEMES, langs = DEFAULT_LANGS) {
119
+ await ensureHighlighter(themes, langs);
120
+ }
121
+ /** True once `language` can be tokenized (its parser has loaded). */
122
+ export function canHighlight(language, _theme) {
123
+ return isLanguageLoaded(language);
124
+ }
125
+ function nodeId(from, to) {
126
+ return `${from}:${to}`;
127
+ }
128
+ /**
129
+ * Parse `code` and build the per-node color map. Mirrors Motion Canvas's
130
+ * `LezerHighlighter.prepare`: for each highlighted range, walk the subtree and
131
+ * record the color against every contained node's `from:to` id, so a later
132
+ * {@link highlightAt} lookup by `resolveInner` index finds it.
133
+ */
134
+ function prepareHighlight(code, language, style) {
135
+ const parser = loadedParsers.get(resolveLanguageKey(language));
136
+ if (!parser)
137
+ return null;
138
+ const tree = parser.parse(code);
139
+ const highlighter = compileStyle(style);
140
+ const colorLookup = new Map();
141
+ highlightTree(tree, highlighter, (from, to, classes) => {
142
+ // tagHighlighter emits our color string as the "class".
143
+ const color = classes;
144
+ if (!color)
145
+ return;
146
+ const cursor = tree.cursorAt(from, 1);
147
+ do {
148
+ colorLookup.set(nodeId(cursor.from, cursor.to), color);
149
+ } while (cursor.next() && cursor.to <= to);
150
+ });
151
+ return { tree, code, colorLookup };
152
+ }
153
+ /**
154
+ * Resolve the color and run length for the token starting at `index`. Mirrors
155
+ * Motion Canvas's `LezerHighlighter.highlight`: resolve the innermost node, look
156
+ * up its color, and report how far the run extends (`skipAhead`).
157
+ */
158
+ function highlightAt(index, cache) {
159
+ const node = cache.tree.resolveInner(index, 1);
160
+ const color = cache.colorLookup.get(nodeId(node.from, node.to));
161
+ if (color) {
162
+ return { color, skipAhead: node.to - index };
163
+ }
164
+ // No color: skip past a childless (leaf) node so we don't re-resolve every char.
165
+ const skipAhead = node.firstChild ? 0 : node.to - index;
166
+ return { color: null, skipAhead };
167
+ }
168
+ /**
169
+ * Split `code` into structural tokens at parse-tree leaf boundaries, including the
170
+ * gaps between leaves (whitespace/punctuation). Mirrors Motion Canvas's
171
+ * `LezerHighlighter.tokenize`; this segmentation is what the token-level
172
+ * append/insert/erase/replace animations diff against, so matching it keeps the
173
+ * animation behavior identical to Motion Canvas.
174
+ */
175
+ function tokenizeCode(code, language) {
176
+ const parser = loadedParsers.get(resolveLanguageKey(language));
177
+ if (!parser)
178
+ return [code];
179
+ const tree = parser.parse(code);
180
+ const cursor = tree.cursor();
181
+ const tokens = [];
182
+ let current = 0;
183
+ do {
184
+ if (!cursor.node.firstChild) {
185
+ if (cursor.from > current)
186
+ tokens.push(code.slice(current, cursor.from));
187
+ if (cursor.from < cursor.to)
188
+ tokens.push(code.slice(cursor.from, cursor.to));
189
+ current = cursor.to;
190
+ }
191
+ } while (cursor.next());
192
+ if (current < code.length)
193
+ tokens.push(code.slice(current));
194
+ return tokens;
195
+ }
196
+ /**
197
+ * Tokenize `code` into per-line, colored tokens — the contract the rest of the
198
+ * Code component consumes. Combines {@link tokenizeCode} (structural split) with
199
+ * {@link highlightAt} (per-run color), then breaks tokens at newlines so each
200
+ * output line is a row of single-line, single-color tokens.
201
+ *
202
+ * Callers must have checked {@link canHighlight} first; this returns a single
203
+ * uncolored line per source line if the parser isn't loaded.
204
+ */
205
+ export function highlightToTokens(code, language, theme = DefaultHighlightStyle) {
206
+ const style = resolveTheme(theme);
207
+ const cache = prepareHighlight(code, language, style);
208
+ const lines = [[]];
209
+ const pushLine = () => lines.push([]);
210
+ if (!cache) {
211
+ // Parser unavailable: one uncolored token per source line.
212
+ return code.split("\n").map(line => [{ content: line, color: style.defaultColor }]);
213
+ }
214
+ // Walk the structural tokens, coloring each by its starting index and
215
+ // splitting on newlines so no token spans rows.
216
+ let index = 0;
217
+ for (const token of tokenizeCode(code, language)) {
218
+ let offset = 0;
219
+ while (offset < token.length) {
220
+ const { color, skipAhead } = highlightAt(index + offset, cache);
221
+ // Extend the run as far as the highlight says, but never past this
222
+ // structural token and never across a newline (lines are drawn rows).
223
+ let runEnd = offset + Math.max(skipAhead, 1);
224
+ if (runEnd > token.length)
225
+ runEnd = token.length;
226
+ const newlineRel = token.indexOf("\n", offset);
227
+ if (newlineRel !== -1 && newlineRel < runEnd)
228
+ runEnd = newlineRel;
229
+ const slice = token.slice(offset, runEnd);
230
+ if (slice.length > 0) {
231
+ lines[lines.length - 1].push({ content: slice, color: color ?? style.defaultColor });
232
+ }
233
+ if (newlineRel === runEnd) {
234
+ pushLine();
235
+ runEnd += 1; // consume the newline itself
236
+ }
237
+ offset = runEnd;
238
+ }
239
+ index += token.length;
240
+ }
241
+ return lines;
242
+ }
243
+ //# sourceMappingURL=highlight.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"highlight.js","sourceRoot":"","sources":["../src/highlight.ts"],"names":[],"mappings":"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","sourcesContent":["import { Parser, Tree } from \"@lezer/common\";\nimport { highlightTree } from \"@lezer/highlight\";\nimport {\n BUILTIN_THEMES,\n CodeHighlightStyle,\n CodeTheme,\n DefaultHighlightStyle,\n compileStyle,\n} from \"./style\";\n\n/**\n * Lezer-backed syntax highlighting for the Code component, replacing the former\n * Shiki wiring. This mirrors Motion Canvas's `LezerHighlighter`:\n *\n * - {@link prepareHighlight} parses code once and builds a `from:to` → color map\n * by walking {@link highlightTree}.\n * - {@link tokenizeCode} splits code at parse-tree leaf boundaries.\n * - {@link highlightAt} resolves the color (and run length) for a character index.\n *\n * Parsers load lazily and asynchronously (dynamic import) so the bundle doesn't\n * pull in every grammar. The node's asset loader awaits {@link ensureHighlighter}\n * before tokenizing; until a parser resolves, callers fall back to plain text and\n * re-tokenize once {@link canHighlight} returns true.\n */\n\n/** Dynamic-import factories for each supported language → a Lezer {@link Parser}. */\nconst LANGUAGE_LOADERS: Record<string, () => Promise<Parser>> = {\n typescript: async () => (await import(\"@lezer/javascript\")).parser.configure({ dialect: \"ts\" }),\n tsx: async () => (await import(\"@lezer/javascript\")).parser.configure({ dialect: \"ts jsx\" }),\n javascript: async () => (await import(\"@lezer/javascript\")).parser,\n jsx: async () => (await import(\"@lezer/javascript\")).parser.configure({ dialect: \"jsx\" }),\n python: async () => (await import(\"@lezer/python\")).parser,\n json: async () => (await import(\"@lezer/json\")).parser,\n html: async () => (await import(\"@lezer/html\")).parser,\n css: async () => (await import(\"@lezer/css\")).parser,\n rust: async () => (await import(\"@lezer/rust\")).parser,\n go: async () => (await import(\"@lezer/go\")).parser,\n java: async () => (await import(\"@lezer/java\")).parser,\n cpp: async () => (await import(\"@lezer/cpp\")).parser,\n php: async () => (await import(\"@lezer/php\")).parser,\n markdown: async () => (await import(\"@lezer/markdown\")).parser,\n xml: async () => (await import(\"@lezer/xml\")).parser,\n yaml: async () => (await import(\"@lezer/yaml\")).parser,\n};\n\n// Aliases so common language strings resolve to a loader above.\nconst LANGUAGE_ALIASES: Record<string, string> = {\n ts: \"typescript\",\n js: \"javascript\",\n py: \"python\",\n \"c++\": \"cpp\",\n md: \"markdown\",\n yml: \"yaml\",\n shell: \"javascript\", // no shell grammar bundled; fall back rather than throw\n};\n\nfunction resolveLanguageKey(language: string): string {\n const key = language.toLowerCase();\n return LANGUAGE_ALIASES[key] ?? key;\n}\n\n// Loaded parsers and in-flight loads, keyed by resolved language key, so\n// concurrent Code nodes asking for the same language share one dynamic import.\nconst loadedParsers = new Map<string, Parser>();\nconst inFlightLangs = new Map<string, Promise<Parser | null>>();\n\nexport const DEFAULT_THEMES = [DefaultHighlightStyle.name];\nexport const DEFAULT_LANGS = [\"typescript\", \"javascript\", \"json\", \"python\"];\n\n// Theme registry, seeded with the built-ins (Motion Canvas default + GitHub\n// dark/light). The `theme` prop is a string name or a CodeHighlightStyle object;\n// apps can register additional styles by name and reference them by that name.\nconst themesByName = new Map<string, CodeHighlightStyle>(Object.entries(BUILTIN_THEMES));\n\n/** Register a custom {@link CodeHighlightStyle} so Code nodes can reference it by name. */\nexport function registerCodeTheme(style: CodeHighlightStyle): void {\n themesByName.set(style.name, style);\n}\n\n/**\n * Resolve a theme reference (a registered name, or a CodeHighlightStyle object)\n * to a concrete style. Unknown names fall back to the Motion Canvas default so a\n * stray theme string never throws.\n */\nexport function resolveTheme(theme: CodeTheme): CodeHighlightStyle {\n if (typeof theme !== \"string\") return theme;\n return themesByName.get(theme) ?? DefaultHighlightStyle;\n}\n\n/** Has `language` finished loading its parser? */\nfunction isLanguageLoaded(language: string): boolean {\n return loadedParsers.has(resolveLanguageKey(language));\n}\n\n/**\n * Load the parsers for `langs` (themes are synchronous, so they're a no-op here\n * but kept in the signature for API parity with the former Shiki implementation).\n * Resolves once every requested language's parser is available. Unknown languages\n * resolve without loading anything — {@link canHighlight} stays false for them and\n * the node renders them as plain text.\n */\nexport function ensureHighlighter(\n _themes: string[] = DEFAULT_THEMES,\n langs: string[] = DEFAULT_LANGS,\n): Promise<void> {\n const jobs: Promise<unknown>[] = [];\n\n for (const lang of langs) {\n const key = resolveLanguageKey(lang);\n if (loadedParsers.has(key)) continue;\n\n const loader = LANGUAGE_LOADERS[key];\n if (!loader) continue; // unsupported language; leave it unhighlighted\n\n let job = inFlightLangs.get(key);\n if (!job) {\n job = loader()\n .then(parser => {\n loadedParsers.set(key, parser);\n return parser;\n })\n .catch(() => null)\n .finally(() => inFlightLangs.delete(key));\n inFlightLangs.set(key, job);\n }\n jobs.push(job);\n }\n\n return jobs.length === 0 ? Promise.resolve() : Promise.all(jobs).then(() => undefined);\n}\n\n/**\n * Load a syntax-highlight language into the shared highlighter. Exposed so a Code\n * node's asset loader can run it on the timeline, and so apps can preload\n * languages explicitly. Returns once the parser is ready (or immediately for an\n * unsupported language). The `theme` arg is accepted for backwards compatibility.\n */\nexport async function loadCodeLanguage(language: string, _theme?: string): Promise<void> {\n await ensureHighlighter(DEFAULT_THEMES, [language]);\n}\n\nexport async function initSyntaxHighlighter(\n themes: string[] = DEFAULT_THEMES,\n langs: string[] = DEFAULT_LANGS,\n): Promise<void> {\n await ensureHighlighter(themes, langs);\n}\n\n/** True once `language` can be tokenized (its parser has loaded). */\nexport function canHighlight(language: string, _theme?: CodeTheme): boolean {\n return isLanguageLoaded(language);\n}\n\n/** A parsed-and-prepared highlight cache for one snapshot of code. */\ninterface HighlightCache {\n tree: Tree;\n code: string;\n // `${from}:${to}` node id → color, exactly as Motion Canvas keys it.\n colorLookup: Map<string, string>;\n}\n\nfunction nodeId(from: number, to: number): string {\n return `${from}:${to}`;\n}\n\n/**\n * Parse `code` and build the per-node color map. Mirrors Motion Canvas's\n * `LezerHighlighter.prepare`: for each highlighted range, walk the subtree and\n * record the color against every contained node's `from:to` id, so a later\n * {@link highlightAt} lookup by `resolveInner` index finds it.\n */\nfunction prepareHighlight(code: string, language: string, style: CodeHighlightStyle): HighlightCache | null {\n const parser = loadedParsers.get(resolveLanguageKey(language));\n if (!parser) return null;\n\n const tree = parser.parse(code);\n const highlighter = compileStyle(style);\n const colorLookup = new Map<string, string>();\n\n highlightTree(tree, highlighter, (from, to, classes) => {\n // tagHighlighter emits our color string as the \"class\".\n const color = classes;\n if (!color) return;\n const cursor = tree.cursorAt(from, 1);\n do {\n colorLookup.set(nodeId(cursor.from, cursor.to), color);\n } while (cursor.next() && cursor.to <= to);\n });\n\n return { tree, code, colorLookup };\n}\n\n/**\n * Resolve the color and run length for the token starting at `index`. Mirrors\n * Motion Canvas's `LezerHighlighter.highlight`: resolve the innermost node, look\n * up its color, and report how far the run extends (`skipAhead`).\n */\nfunction highlightAt(index: number, cache: HighlightCache): { color: string | null; skipAhead: number } {\n const node = cache.tree.resolveInner(index, 1);\n const color = cache.colorLookup.get(nodeId(node.from, node.to));\n if (color) {\n return { color, skipAhead: node.to - index };\n }\n // No color: skip past a childless (leaf) node so we don't re-resolve every char.\n const skipAhead = node.firstChild ? 0 : node.to - index;\n return { color: null, skipAhead };\n}\n\n/**\n * Split `code` into structural tokens at parse-tree leaf boundaries, including the\n * gaps between leaves (whitespace/punctuation). Mirrors Motion Canvas's\n * `LezerHighlighter.tokenize`; this segmentation is what the token-level\n * append/insert/erase/replace animations diff against, so matching it keeps the\n * animation behavior identical to Motion Canvas.\n */\nfunction tokenizeCode(code: string, language: string): string[] {\n const parser = loadedParsers.get(resolveLanguageKey(language));\n if (!parser) return [code];\n\n const tree = parser.parse(code);\n const cursor = tree.cursor();\n const tokens: string[] = [];\n let current = 0;\n\n do {\n if (!cursor.node.firstChild) {\n if (cursor.from > current) tokens.push(code.slice(current, cursor.from));\n if (cursor.from < cursor.to) tokens.push(code.slice(cursor.from, cursor.to));\n current = cursor.to;\n }\n } while (cursor.next());\n\n if (current < code.length) tokens.push(code.slice(current));\n return tokens;\n}\n\n/**\n * Tokenize `code` into per-line, colored tokens — the contract the rest of the\n * Code component consumes. Combines {@link tokenizeCode} (structural split) with\n * {@link highlightAt} (per-run color), then breaks tokens at newlines so each\n * output line is a row of single-line, single-color tokens.\n *\n * Callers must have checked {@link canHighlight} first; this returns a single\n * uncolored line per source line if the parser isn't loaded.\n */\nexport function highlightToTokens(\n code: string,\n language: string,\n theme: CodeTheme = DefaultHighlightStyle,\n): { content: string; color?: string }[][] {\n const style = resolveTheme(theme);\n const cache = prepareHighlight(code, language, style);\n\n const lines: { content: string; color?: string }[][] = [[]];\n const pushLine = () => lines.push([]);\n\n if (!cache) {\n // Parser unavailable: one uncolored token per source line.\n return code.split(\"\\n\").map(line => [{ content: line, color: style.defaultColor }]);\n }\n\n // Walk the structural tokens, coloring each by its starting index and\n // splitting on newlines so no token spans rows.\n let index = 0;\n for (const token of tokenizeCode(code, language)) {\n let offset = 0;\n while (offset < token.length) {\n const { color, skipAhead } = highlightAt(index + offset, cache);\n // Extend the run as far as the highlight says, but never past this\n // structural token and never across a newline (lines are drawn rows).\n let runEnd = offset + Math.max(skipAhead, 1);\n if (runEnd > token.length) runEnd = token.length;\n const newlineRel = token.indexOf(\"\\n\", offset);\n if (newlineRel !== -1 && newlineRel < runEnd) runEnd = newlineRel;\n\n const slice = token.slice(offset, runEnd);\n if (slice.length > 0) {\n lines[lines.length - 1].push({ content: slice, color: color ?? style.defaultColor });\n }\n\n if (newlineRel === runEnd) {\n pushLine();\n runEnd += 1; // consume the newline itself\n }\n offset = runEnd;\n }\n index += token.length;\n }\n\n return lines;\n}\n"]}
@@ -0,0 +1,4 @@
1
+ export * from './node.js';
2
+ export * from './engine.js';
3
+ export { NODES } from "./nodes.js";
4
+ //# sourceMappingURL=index.d.ts.map