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