@kgdiff/viewer 0.0.0-stage → 0.2.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 (155) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +52 -2
  3. package/dist/compute-loader.d.ts +13 -0
  4. package/dist/compute-loader.d.ts.map +1 -0
  5. package/dist/compute-loader.js +46 -0
  6. package/dist/compute-loader.js.map +1 -0
  7. package/dist/compute.d.ts +8 -0
  8. package/dist/compute.d.ts.map +1 -0
  9. package/dist/compute.js +8 -0
  10. package/dist/compute.js.map +1 -0
  11. package/dist/controller.d.ts +199 -0
  12. package/dist/controller.d.ts.map +1 -0
  13. package/dist/controller.js +1254 -0
  14. package/dist/controller.js.map +1 -0
  15. package/dist/dom.d.ts +32 -0
  16. package/dist/dom.d.ts.map +1 -0
  17. package/dist/dom.js +105 -0
  18. package/dist/dom.js.map +1 -0
  19. package/dist/element.d.ts +43 -0
  20. package/dist/element.d.ts.map +1 -0
  21. package/dist/element.js +656 -0
  22. package/dist/element.js.map +1 -0
  23. package/dist/framing.d.ts +45 -0
  24. package/dist/framing.d.ts.map +1 -0
  25. package/dist/framing.js +107 -0
  26. package/dist/framing.js.map +1 -0
  27. package/dist/hash-sync.d.ts +18 -0
  28. package/dist/hash-sync.d.ts.map +1 -0
  29. package/dist/hash-sync.js +45 -0
  30. package/dist/hash-sync.js.map +1 -0
  31. package/dist/i18n.d.ts +222 -0
  32. package/dist/i18n.d.ts.map +1 -0
  33. package/dist/i18n.js +434 -0
  34. package/dist/i18n.js.map +1 -0
  35. package/dist/icons.d.ts +37 -0
  36. package/dist/icons.d.ts.map +1 -0
  37. package/dist/icons.js +36 -0
  38. package/dist/icons.js.map +1 -0
  39. package/dist/index.d.ts +151 -0
  40. package/dist/index.d.ts.map +1 -0
  41. package/dist/index.js +41 -0
  42. package/dist/index.js.map +1 -0
  43. package/dist/keyboard.d.ts +64 -0
  44. package/dist/keyboard.d.ts.map +1 -0
  45. package/dist/keyboard.js +95 -0
  46. package/dist/keyboard.js.map +1 -0
  47. package/dist/list-model.d.ts +35 -0
  48. package/dist/list-model.d.ts.map +1 -0
  49. package/dist/list-model.js +70 -0
  50. package/dist/list-model.js.map +1 -0
  51. package/dist/payload.d.ts +10 -0
  52. package/dist/payload.d.ts.map +1 -0
  53. package/dist/payload.js +138 -0
  54. package/dist/payload.js.map +1 -0
  55. package/dist/pipeline.d.ts +31 -0
  56. package/dist/pipeline.d.ts.map +1 -0
  57. package/dist/pipeline.js +122 -0
  58. package/dist/pipeline.js.map +1 -0
  59. package/dist/prefs.d.ts +18 -0
  60. package/dist/prefs.d.ts.map +1 -0
  61. package/dist/prefs.js +38 -0
  62. package/dist/prefs.js.map +1 -0
  63. package/dist/registry.d.ts +3 -0
  64. package/dist/registry.d.ts.map +1 -0
  65. package/dist/registry.js +3 -0
  66. package/dist/registry.js.map +1 -0
  67. package/dist/renderer-port.d.ts +14 -0
  68. package/dist/renderer-port.d.ts.map +1 -0
  69. package/dist/renderer-port.js +16 -0
  70. package/dist/renderer-port.js.map +1 -0
  71. package/dist/replay.d.ts +55 -0
  72. package/dist/replay.d.ts.map +1 -0
  73. package/dist/replay.js +147 -0
  74. package/dist/replay.js.map +1 -0
  75. package/dist/review-store.d.ts +30 -0
  76. package/dist/review-store.d.ts.map +1 -0
  77. package/dist/review-store.js +120 -0
  78. package/dist/review-store.js.map +1 -0
  79. package/dist/search.d.ts +25 -0
  80. package/dist/search.d.ts.map +1 -0
  81. package/dist/search.js +90 -0
  82. package/dist/search.js.map +1 -0
  83. package/dist/standalone/kgdiff-viewer-compute.js +1 -0
  84. package/dist/standalone/kgdiff-viewer.js +5 -0
  85. package/dist/standalone-compute.d.ts +2 -0
  86. package/dist/standalone-compute.d.ts.map +1 -0
  87. package/dist/standalone-compute.js +8 -0
  88. package/dist/standalone-compute.js.map +1 -0
  89. package/dist/standalone.d.ts +2 -0
  90. package/dist/standalone.d.ts.map +1 -0
  91. package/dist/standalone.js +18 -0
  92. package/dist/standalone.js.map +1 -0
  93. package/dist/styles.d.ts +11 -0
  94. package/dist/styles.d.ts.map +1 -0
  95. package/dist/styles.js +622 -0
  96. package/dist/styles.js.map +1 -0
  97. package/dist/text-diff.d.ts +44 -0
  98. package/dist/text-diff.d.ts.map +1 -0
  99. package/dist/text-diff.js +261 -0
  100. package/dist/text-diff.js.map +1 -0
  101. package/dist/ui/context.d.ts +32 -0
  102. package/dist/ui/context.d.ts.map +1 -0
  103. package/dist/ui/context.js +49 -0
  104. package/dist/ui/context.js.map +1 -0
  105. package/dist/ui/detail.d.ts +9 -0
  106. package/dist/ui/detail.d.ts.map +1 -0
  107. package/dist/ui/detail.js +609 -0
  108. package/dist/ui/detail.js.map +1 -0
  109. package/dist/ui/header.d.ts +14 -0
  110. package/dist/ui/header.d.ts.map +1 -0
  111. package/dist/ui/header.js +195 -0
  112. package/dist/ui/header.js.map +1 -0
  113. package/dist/ui/popovers.d.ts +20 -0
  114. package/dist/ui/popovers.d.ts.map +1 -0
  115. package/dist/ui/popovers.js +180 -0
  116. package/dist/ui/popovers.js.map +1 -0
  117. package/dist/ui/sidebar.d.ts +25 -0
  118. package/dist/ui/sidebar.d.ts.map +1 -0
  119. package/dist/ui/sidebar.js +609 -0
  120. package/dist/ui/sidebar.js.map +1 -0
  121. package/dist/ui/stage.d.ts +18 -0
  122. package/dist/ui/stage.d.ts.map +1 -0
  123. package/dist/ui/stage.js +664 -0
  124. package/dist/ui/stage.js.map +1 -0
  125. package/package.json +58 -3
  126. package/src/compute-loader.ts +58 -0
  127. package/src/compute.ts +7 -0
  128. package/src/controller.ts +1393 -0
  129. package/src/dom.ts +131 -0
  130. package/src/element.ts +697 -0
  131. package/src/framing.ts +150 -0
  132. package/src/hash-sync.ts +54 -0
  133. package/src/i18n.ts +467 -0
  134. package/src/icons.ts +48 -0
  135. package/src/index.ts +171 -0
  136. package/src/keyboard.ts +128 -0
  137. package/src/list-model.ts +86 -0
  138. package/src/payload.ts +165 -0
  139. package/src/pipeline.ts +179 -0
  140. package/src/prefs.ts +55 -0
  141. package/src/registry.ts +2 -0
  142. package/src/renderer-port.ts +31 -0
  143. package/src/replay.ts +163 -0
  144. package/src/review-store.ts +125 -0
  145. package/src/search.ts +113 -0
  146. package/src/standalone-compute.ts +8 -0
  147. package/src/standalone.ts +18 -0
  148. package/src/styles.ts +633 -0
  149. package/src/text-diff.ts +263 -0
  150. package/src/ui/context.ts +70 -0
  151. package/src/ui/detail.ts +929 -0
  152. package/src/ui/header.ts +278 -0
  153. package/src/ui/popovers.ts +251 -0
  154. package/src/ui/sidebar.ts +752 -0
  155. package/src/ui/stage.ts +853 -0
@@ -0,0 +1,263 @@
1
+ /**
2
+ * Myers O((N+M)·D) sequence diff for the detail panel: character-level name diffs, word-level
3
+ * attribute diffs and set changes read as edits. Small, deterministic, no dependencies.
4
+ */
5
+
6
+ export type DiffOp = 'equal' | 'insert' | 'delete';
7
+
8
+ export interface DiffSegment {
9
+ readonly op: DiffOp;
10
+ readonly text: string;
11
+ }
12
+
13
+ /** Above this edit distance the diff degrades to "common prefix / suffix + replace". */
14
+ const MAX_D = 1024;
15
+
16
+ /**
17
+ * Edit script between `a` and `b` (Myers 1986, greedy forward search with a trace for the
18
+ * backtrack). Deletions come before insertions within a change. Common prefix / suffix are
19
+ * stripped first, so typical edits cost O(N + M). Inputs larger than the `MAX_D` budget fall
20
+ * back to a single replace of the differing middle.
21
+ */
22
+ export function diffSequences<T>(a: readonly T[], b: readonly T[]): { op: DiffOp; items: T[] }[] {
23
+ let start = 0;
24
+ while (start < a.length && start < b.length && a[start] === b[start]) start++;
25
+ let endA = a.length;
26
+ let endB = b.length;
27
+ while (endA > start && endB > start && a[endA - 1] === b[endB - 1]) {
28
+ endA--;
29
+ endB--;
30
+ }
31
+ const out: { op: DiffOp; items: T[] }[] = [];
32
+ const push = (op: DiffOp, item: T): void => {
33
+ const last = out[out.length - 1];
34
+ if (last !== undefined && last.op === op) last.items.push(item);
35
+ else out.push({ op, items: [item] });
36
+ };
37
+ for (let i = 0; i < start; i++) push('equal', a[i] as T);
38
+ const middle = myers(a.slice(start, endA), b.slice(start, endB));
39
+ for (const [op, item] of middle) push(op, item);
40
+ for (let i = endA; i < a.length; i++) push('equal', a[i] as T);
41
+ return normalizeOrder(out);
42
+ }
43
+
44
+ function myers<T>(a: readonly T[], b: readonly T[]): [DiffOp, T][] {
45
+ const n = a.length;
46
+ const m = b.length;
47
+ if (n === 0) return b.map((x) => ['insert', x]);
48
+ if (m === 0) return a.map((x) => ['delete', x]);
49
+ const max = n + m;
50
+ const limit = Math.min(max, MAX_D);
51
+ const offset = max;
52
+ const v = new Int32Array(2 * max + 2);
53
+ const trace: Int32Array[] = [];
54
+ let found = -1;
55
+ for (let d = 0; d <= limit; d++) {
56
+ // State after step d − 1; the backtrack reads k ± 1 for |k| ≤ d, i.e. offsets [−d, d].
57
+ trace.push(v.slice(offset - d, offset + d + 1));
58
+ for (let k = -d; k <= d; k += 2) {
59
+ let x: number;
60
+ if (k === -d || (k !== d && (v[offset + k - 1] as number) < (v[offset + k + 1] as number))) {
61
+ x = v[offset + k + 1] as number;
62
+ } else {
63
+ x = (v[offset + k - 1] as number) + 1;
64
+ }
65
+ let y = x - k;
66
+ while (x < n && y < m && a[x] === b[y]) {
67
+ x++;
68
+ y++;
69
+ }
70
+ v[offset + k] = x;
71
+ if (x >= n && y >= m) {
72
+ found = d;
73
+ break;
74
+ }
75
+ }
76
+ if (found >= 0) break;
77
+ }
78
+ if (found < 0) {
79
+ return [
80
+ ...a.map((x): [DiffOp, T] => ['delete', x]),
81
+ ...b.map((x): [DiffOp, T] => ['insert', x]),
82
+ ];
83
+ }
84
+ // Backtrack.
85
+ const ops: [DiffOp, T][] = [];
86
+ let x = n;
87
+ let y = m;
88
+ for (let d = found; d > 0; d--) {
89
+ const vd = trace[d] as Int32Array;
90
+ const at = (kk: number): number => vd[kk + d] as number;
91
+ const k = x - y;
92
+ const down = k === -d || (k !== d && at(k - 1) < at(k + 1));
93
+ const prevK = down ? k + 1 : k - 1;
94
+ const prevX = at(prevK);
95
+ const prevY = prevX - prevK;
96
+ while (x > prevX && y > prevY) {
97
+ ops.push(['equal', a[x - 1] as T]);
98
+ x--;
99
+ y--;
100
+ }
101
+ if (down) ops.push(['insert', b[y - 1] as T]);
102
+ else ops.push(['delete', a[x - 1] as T]);
103
+ x = prevX;
104
+ y = prevY;
105
+ }
106
+ while (x > 0 && y > 0) {
107
+ ops.push(['equal', a[x - 1] as T]);
108
+ x--;
109
+ y--;
110
+ }
111
+ return ops.reverse();
112
+ }
113
+
114
+ /** Within each run of changes, deletions first, then insertions (stable, readable). */
115
+ function normalizeOrder<T>(segs: { op: DiffOp; items: T[] }[]): { op: DiffOp; items: T[] }[] {
116
+ const out: { op: DiffOp; items: T[] }[] = [];
117
+ let del: T[] = [];
118
+ let ins: T[] = [];
119
+ const flush = (): void => {
120
+ if (del.length) out.push({ op: 'delete', items: del });
121
+ if (ins.length) out.push({ op: 'insert', items: ins });
122
+ del = [];
123
+ ins = [];
124
+ };
125
+ for (const s of segs) {
126
+ if (s.op === 'equal') {
127
+ flush();
128
+ out.push(s);
129
+ continue;
130
+ }
131
+ const run = s.op === 'delete' ? del : ins;
132
+ for (const item of s.items) run.push(item);
133
+ }
134
+ flush();
135
+ return out;
136
+ }
137
+
138
+ function join(segs: { op: DiffOp; items: string[] }[]): DiffSegment[] {
139
+ return segs.map((s) => ({ op: s.op, text: s.items.join('') }));
140
+ }
141
+
142
+ /**
143
+ * Merge short equal runs sandwiched between changes into the changes ("semantic cleanup"),
144
+ * so that `高管任职 → 董监高任职` reads as one replaced prefix rather than confetti.
145
+ */
146
+ export function cleanupSemantic(segs: readonly DiffSegment[], minEqual = 3): DiffSegment[] {
147
+ const out: DiffSegment[] = [];
148
+ let del = '';
149
+ let ins = '';
150
+ const flush = (): void => {
151
+ if (del) out.push({ op: 'delete', text: del });
152
+ if (ins) out.push({ op: 'insert', text: ins });
153
+ del = '';
154
+ ins = '';
155
+ };
156
+ for (let i = 0; i < segs.length; i++) {
157
+ const s = segs[i] as DiffSegment;
158
+ if (s.op === 'delete') del += s.text;
159
+ else if (s.op === 'insert') ins += s.text;
160
+ else {
161
+ const inner = i > 0 && i < segs.length - 1 && (del !== '' || ins !== '');
162
+ const short = [...s.text].length < minEqual && s.text.trim().length <= minEqual;
163
+ if (inner && short && hasChangeAfter(segs, i)) {
164
+ del += s.text;
165
+ ins += s.text;
166
+ } else {
167
+ flush();
168
+ out.push(s);
169
+ }
170
+ }
171
+ }
172
+ flush();
173
+ return out;
174
+ }
175
+
176
+ function hasChangeAfter(segs: readonly DiffSegment[], i: number): boolean {
177
+ const next = segs[i + 1];
178
+ return next !== undefined && next.op !== 'equal';
179
+ }
180
+
181
+ /** Character (code point) diff with semantic cleanup. */
182
+ export function diffChars(a: string, b: string): DiffSegment[] {
183
+ return cleanupSemantic(join(diffSequences(Array.from(a), Array.from(b))));
184
+ }
185
+
186
+ /**
187
+ * Tokens for word diffs: whitespace runs, words (letters / digits / `_'-`), single CJK
188
+ * characters (CJK has no spaces), single punctuation marks.
189
+ */
190
+ const TOKEN =
191
+ /\s+|[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Hangul}]|[\p{L}\p{N}\p{M}_'’-]+|[^\s]/gu;
192
+
193
+ export function tokenize(text: string): string[] {
194
+ return text.match(TOKEN) ?? [];
195
+ }
196
+
197
+ /** Word-level diff (CJK per character). */
198
+ export function diffWords(a: string, b: string): DiffSegment[] {
199
+ return cleanupSemantic(join(diffSequences(tokenize(a), tokenize(b))), 2);
200
+ }
201
+
202
+ /** Rebuild the old (`equal` + `delete`) or new (`equal` + `insert`) text from a diff. */
203
+ export function sideOf(segs: readonly DiffSegment[], side: 'before' | 'after'): string {
204
+ const skip: DiffOp = side === 'before' ? 'insert' : 'delete';
205
+ let s = '';
206
+ for (const seg of segs) if (seg.op !== skip) s += seg.text;
207
+ return s;
208
+ }
209
+
210
+ /** A set change read as edits: removed → added pairs, and the items left over. */
211
+ export interface PairedItems {
212
+ readonly pairs: readonly (readonly [removed: string, added: string])[];
213
+ readonly removed: readonly string[];
214
+ readonly added: readonly string[];
215
+ }
216
+
217
+ /** Below this share of common characters a removal and an addition stay apart. */
218
+ const PAIR_MIN = 0.5;
219
+ /** Above this many removal × addition candidates, only a lone pair is paired. */
220
+ const PAIR_MAX_CANDIDATES = 400;
221
+
222
+ /** 2 · common code points / total code points. */
223
+ function closeness(a: readonly string[], b: readonly string[]): number {
224
+ let common = 0;
225
+ for (const s of diffSequences(a, b)) if (s.op === 'equal') common += s.items.length;
226
+ return (2 * common) / (a.length + b.length);
227
+ }
228
+
229
+ /**
230
+ * Read a set change (synonyms, types, parents, list values) as edits: a lone removal and a lone
231
+ * addition pair up (`老赖 → 失信人`); otherwise removals and additions sharing at least half
232
+ * their characters pair up, closest first. Pairs keep the order of `removed`.
233
+ */
234
+ export function pairItems(removed: readonly string[], added: readonly string[]): PairedItems {
235
+ const found: [score: number, i: number, j: number][] = [];
236
+ if (removed.length === 1 && added.length === 1) found.push([1, 0, 0]);
237
+ else if (removed.length * added.length <= PAIR_MAX_CANDIDATES) {
238
+ const as = added.map((x) => Array.from(x));
239
+ removed.forEach((r, i) => {
240
+ const rs = Array.from(r);
241
+ as.forEach((a, j) => {
242
+ const score = closeness(rs, a);
243
+ if (score >= PAIR_MIN) found.push([score, i, j]);
244
+ });
245
+ });
246
+ found.sort((x, y) => y[0] - x[0] || x[1] - y[1] || x[2] - y[2]);
247
+ }
248
+ const usedR = new Set<number>();
249
+ const usedA = new Set<number>();
250
+ const pairs: [number, number][] = [];
251
+ for (const [, i, j] of found) {
252
+ if (usedR.has(i) || usedA.has(j)) continue;
253
+ usedR.add(i);
254
+ usedA.add(j);
255
+ pairs.push([i, j]);
256
+ }
257
+ pairs.sort((x, y) => x[0] - y[0]);
258
+ return {
259
+ pairs: pairs.map(([i, j]) => [removed[i] as string, added[j] as string] as const),
260
+ removed: removed.filter((_, i) => !usedR.has(i)),
261
+ added: added.filter((_, j) => !usedA.has(j)),
262
+ };
263
+ }
@@ -0,0 +1,70 @@
1
+ /**
2
+ * What UI components share: the controller, the current translator and element-level actions
3
+ * (attribute-reflected preferences, sidebar, popovers).
4
+ */
5
+ import type { AttrValue, HunkKind, Locale } from '@kgdiff/core';
6
+ import type { ChangeFamily, PaletteName, SceneHunk } from '@kgdiff/scene';
7
+ import type { ViewerController } from '../controller.js';
8
+ import type { Translator } from '../i18n.js';
9
+ import type { ThemePreference, ViewerMode } from '../index.js';
10
+
11
+ export interface UiContext {
12
+ readonly controller: ViewerController;
13
+ readonly tr: () => Translator;
14
+ setMode(mode: ViewerMode): void;
15
+ setThemePreference(pref: ThemePreference): void;
16
+ setPalette(palette: PaletteName): void;
17
+ setLocale(locale: Locale): void;
18
+ toggleSidebar(open?: boolean): void;
19
+ sidebarOpen(): boolean;
20
+ toggleMenu(open?: boolean): void;
21
+ openHelp(): void;
22
+ focusSearch(): void;
23
+ /** Root of the shadow tree (for focus management). */
24
+ readonly root: ShadowRoot;
25
+ }
26
+
27
+ /** One-character glyph of a hunk (docs/interaction.md §Change list): `+ − ~ ⇄ ⊕ ⑂`. */
28
+ export function hunkGlyph(kind: HunkKind, family: ChangeFamily): string {
29
+ switch (kind) {
30
+ case 'add':
31
+ return '+';
32
+ case 'remove':
33
+ return '−';
34
+ case 'merge':
35
+ return '⊕';
36
+ case 'split':
37
+ return '⑂';
38
+ case 'relations':
39
+ return '⇄';
40
+ case 'modify':
41
+ return family === 'structure' ? '⇄' : family === 'identity' ? '≡' : '~';
42
+ default:
43
+ return '~';
44
+ }
45
+ }
46
+
47
+ export function sceneHunkGlyph(h: SceneHunk): string {
48
+ return hunkGlyph(h.kind, h.family);
49
+ }
50
+
51
+ /** Single-line display form of an attribute value. */
52
+ export function formatValue(value: AttrValue | undefined): string {
53
+ if (value === undefined) return '';
54
+ if (value === null) return 'null';
55
+ if (typeof value === 'string') return value;
56
+ if (typeof value === 'number' || typeof value === 'boolean') return String(value);
57
+ if (Array.isArray(value)) return value.map((v) => formatValue(v as AttrValue)).join(', ');
58
+ return JSON.stringify(value);
59
+ }
60
+
61
+ /** Items of a list-like value (arrays of scalars), else null. */
62
+ export function listItems(value: AttrValue | undefined): string[] | null {
63
+ if (!Array.isArray(value)) return null;
64
+ const out: string[] = [];
65
+ for (const v of value as readonly AttrValue[]) {
66
+ if (typeof v === 'object' && v !== null) return null;
67
+ out.push(formatValue(v));
68
+ }
69
+ return out;
70
+ }