@kgdiff/viewer 0.1.0 → 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.
- package/dist/i18n.d.ts +2 -3
- package/dist/i18n.d.ts.map +1 -1
- package/dist/i18n.js +4 -6
- package/dist/i18n.js.map +1 -1
- package/dist/standalone/kgdiff-viewer-compute.js +1 -1
- package/dist/standalone/kgdiff-viewer.js +4 -4
- package/dist/styles.d.ts.map +1 -1
- package/dist/styles.js +12 -17
- package/dist/styles.js.map +1 -1
- package/dist/text-diff.d.ts +14 -2
- package/dist/text-diff.d.ts.map +1 -1
- package/dist/text-diff.js +52 -2
- package/dist/text-diff.js.map +1 -1
- package/dist/ui/detail.d.ts.map +1 -1
- package/dist/ui/detail.js +194 -151
- package/dist/ui/detail.js.map +1 -1
- package/package.json +6 -6
- package/src/i18n.ts +4 -6
- package/src/styles.ts +12 -17
- package/src/text-diff.ts +57 -2
- package/src/ui/detail.ts +253 -245
package/src/ui/detail.ts
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Detail panel (docs/interaction.md §Detail panel): for the selected hunk (or node) — title,
|
|
3
|
-
* family pill, prev / next, reviewed toggle;
|
|
4
|
-
*
|
|
5
|
-
*
|
|
3
|
+
* family pill, prev / next, reviewed toggle; then what changed, first: one row per changed
|
|
4
|
+
* facet, before → after (character diff for names, word diff for text, set changes paired into
|
|
5
|
+
* edits); merge / split composition, relations as sentences and match evidence; the unchanged
|
|
6
|
+
* rest last, its attributes folded. Added, removed and inspected nodes show their overview,
|
|
7
|
+
* attributes and relations.
|
|
6
8
|
*/
|
|
7
9
|
import type {
|
|
8
10
|
AttrChange,
|
|
9
|
-
AttrValue,
|
|
10
11
|
DiffResult,
|
|
11
12
|
EdgeFacet,
|
|
12
13
|
MatchEvidence,
|
|
@@ -19,12 +20,14 @@ import type { Family, Scene, SceneHunk } from '@kgdiff/scene';
|
|
|
19
20
|
import type { Topic } from '../controller.js';
|
|
20
21
|
import { attr, h, icon, iconButton, replaceKeepingFocus } from '../dom.js';
|
|
21
22
|
import { formatPercent, type Translator } from '../i18n.js';
|
|
22
|
-
import { type DiffSegment, diffChars, diffWords } from '../text-diff.js';
|
|
23
|
+
import { type DiffSegment, diffChars, diffWords, pairItems } from '../text-diff.js';
|
|
23
24
|
import { formatValue, listItems, sceneHunkGlyph, type UiContext } from './context.js';
|
|
24
25
|
|
|
25
26
|
const MAX_NODES = 12;
|
|
26
27
|
const MAX_RELATIONS = 12;
|
|
27
28
|
const CLAMP_CHARS = 180;
|
|
29
|
+
/** Up to this many characters (both sides together) a value change reads `before → after`. */
|
|
30
|
+
const INLINE_CHARS = 60;
|
|
28
31
|
|
|
29
32
|
type Child = Node | string | null | false | undefined;
|
|
30
33
|
|
|
@@ -80,6 +83,63 @@ function chips(
|
|
|
80
83
|
);
|
|
81
84
|
}
|
|
82
85
|
|
|
86
|
+
/** `before → after` on one line, wrapping when long. */
|
|
87
|
+
function pair(before: Child[], after: Child[], tr: Translator): HTMLElement {
|
|
88
|
+
return h(
|
|
89
|
+
'div',
|
|
90
|
+
{ class: 'pair' },
|
|
91
|
+
h('span', { class: 'b' }, before),
|
|
92
|
+
h('span', { class: 'arr', 'aria-hidden': 'true', text: '→' }),
|
|
93
|
+
h('span', { class: 'sr-only', text: tr.t('detail.changedTo') }),
|
|
94
|
+
h('span', null, after),
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** `before → after` with the changed characters marked. */
|
|
99
|
+
function textPair(before: string, after: string, tr: Translator): HTMLElement {
|
|
100
|
+
const segs = diffChars(before, after);
|
|
101
|
+
return pair(marked(segs, 'before'), marked(segs, 'after'), tr);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** One side of a change on a line of its own: `− before` or `+ after`. */
|
|
105
|
+
function signed(side: 'old' | 'new', content: Child): HTMLElement {
|
|
106
|
+
return h(
|
|
107
|
+
'div',
|
|
108
|
+
{ class: `line ${side}` },
|
|
109
|
+
h('span', { class: 'sign', 'aria-hidden': 'true', text: side === 'old' ? '−' : '+' }),
|
|
110
|
+
h('div', null, content),
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** A set change (synonyms, types, parents, list values): edits as pairs, the rest as chips. */
|
|
115
|
+
function items(
|
|
116
|
+
added: readonly string[],
|
|
117
|
+
removed: readonly string[],
|
|
118
|
+
tr: Translator,
|
|
119
|
+
same: readonly string[] = [],
|
|
120
|
+
): Child[] {
|
|
121
|
+
const p = pairItems(removed, added);
|
|
122
|
+
const out: Child[] = p.pairs.map(([b, a]) => textPair(b, a, tr));
|
|
123
|
+
if (p.added.length > 0 || p.removed.length > 0 || same.length > 0) {
|
|
124
|
+
out.push(chips(p.added, p.removed, same));
|
|
125
|
+
}
|
|
126
|
+
return out;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Key · value rows. */
|
|
130
|
+
function table(rows: readonly (readonly [string, Child | Child[]])[], cls = 'attrs'): HTMLElement {
|
|
131
|
+
return h(
|
|
132
|
+
'table',
|
|
133
|
+
{ class: cls },
|
|
134
|
+
h('colgroup', null, h('col', { class: 'k' }), h('col')),
|
|
135
|
+
h(
|
|
136
|
+
'tbody',
|
|
137
|
+
null,
|
|
138
|
+
rows.map(([k, v]) => h('tr', null, h('td', { class: 'k', text: k }), h('td', null, v))),
|
|
139
|
+
),
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
83
143
|
function familyOfEdge(e: UnionEdge): Family {
|
|
84
144
|
if (e.status === 'added') return 'added';
|
|
85
145
|
if (e.status === 'removed') {
|
|
@@ -228,11 +288,15 @@ export class Detail {
|
|
|
228
288
|
];
|
|
229
289
|
const nodeIds = core?.nodes ?? [];
|
|
230
290
|
if (nodeIds.length > 1) parts.push(this.#nodeList(nodeIds, focusId, scene, diff, tr));
|
|
231
|
-
|
|
291
|
+
const [lead, rest] = focus
|
|
292
|
+
? this.#nodeSections(focus, diff, tr, nodeIds.length > 1, true)
|
|
293
|
+
: [[], []];
|
|
232
294
|
const edges = (core?.edges ?? [])
|
|
233
295
|
.map((id) => diff.index.edge(id))
|
|
234
296
|
.filter((e): e is UnionEdge => e !== undefined);
|
|
297
|
+
parts.push(...lead);
|
|
235
298
|
if (edges.length > 0) parts.push(this.#relations(edges, focusId, scene, diff, tr, true));
|
|
299
|
+
parts.push(...rest);
|
|
236
300
|
replaceKeepingFocus(this.#ctx.root, this.#body, this.#body, ...parts);
|
|
237
301
|
}
|
|
238
302
|
|
|
@@ -245,6 +309,7 @@ export class Detail {
|
|
|
245
309
|
return;
|
|
246
310
|
}
|
|
247
311
|
const edges = diff.index.edgesOf(node.id);
|
|
312
|
+
const [lead, rest] = this.#nodeSections(node, diff, tr, false, false);
|
|
248
313
|
replaceKeepingFocus(
|
|
249
314
|
this.#ctx.root,
|
|
250
315
|
this.#body,
|
|
@@ -261,8 +326,9 @@ export class Detail {
|
|
|
261
326
|
this.#nav(tr, undefined),
|
|
262
327
|
),
|
|
263
328
|
h('h2', { class: 'd-title', text: node.label }),
|
|
264
|
-
...
|
|
329
|
+
...lead,
|
|
265
330
|
edges.length > 0 ? this.#relations(edges, node.id, scene, diff, tr, false) : null,
|
|
331
|
+
...rest,
|
|
266
332
|
);
|
|
267
333
|
}
|
|
268
334
|
|
|
@@ -329,265 +395,207 @@ export class Detail {
|
|
|
329
395
|
);
|
|
330
396
|
}
|
|
331
397
|
|
|
398
|
+
/**
|
|
399
|
+
* The node's sections, split around the relations. A modified node (or an unchanged one a hunk
|
|
400
|
+
* points at) leads with what changed and ends with its unchanged rest; added, removed and
|
|
401
|
+
* inspected nodes lead with what they are.
|
|
402
|
+
*/
|
|
332
403
|
#nodeSections(
|
|
333
404
|
node: UnionNode,
|
|
334
405
|
diff: DiffResult,
|
|
335
406
|
tr: Translator,
|
|
336
407
|
showName: boolean,
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
const facets = node.facets;
|
|
340
|
-
const find = <K extends NodeFacet['kind']>(kind: K) =>
|
|
341
|
-
facets.find((f): f is Extract<NodeFacet, { kind: K }> => f.kind === kind);
|
|
408
|
+
inHunk: boolean,
|
|
409
|
+
): [HTMLElement[], HTMLElement[]] {
|
|
342
410
|
const label = (id: UnionId): string => diff.index.node(id)?.label ?? id;
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
const rekey = find('rekey');
|
|
346
|
-
const types = find('types');
|
|
347
|
-
const deprecate = find('deprecate');
|
|
348
|
-
const current = node.after ?? node.before;
|
|
349
|
-
const idRows: [string, Child | Child[]][] = [];
|
|
350
|
-
if (showName) idRows.push([tr.t('detail.name'), h('span', { text: node.label })]);
|
|
351
|
-
idRows.push([
|
|
352
|
-
tr.t('detail.id'),
|
|
353
|
-
rekey
|
|
354
|
-
? [
|
|
355
|
-
h(
|
|
356
|
-
'span',
|
|
357
|
-
{ class: 'id-text' },
|
|
358
|
-
h('del', { class: 'hl', text: rekey.from }),
|
|
359
|
-
' → ',
|
|
360
|
-
h('ins', { class: 'hl', text: rekey.to }),
|
|
361
|
-
),
|
|
362
|
-
rekey.score < 1
|
|
363
|
-
? h('span', {
|
|
364
|
-
class: 'note-line',
|
|
365
|
-
text: ` · ${tr.t('detail.matchScore', { p: formatPercent(rekey.score) })}`,
|
|
366
|
-
})
|
|
367
|
-
: null,
|
|
368
|
-
]
|
|
369
|
-
: h('span', { class: 'id-text', text: node.afterId ?? node.beforeId ?? node.id }),
|
|
370
|
-
]);
|
|
371
|
-
if (types) idRows.push([tr.t('detail.types'), chips(types.added, types.removed)]);
|
|
372
|
-
else if (node.types.length > 0) idRows.push([tr.t('detail.types'), chips([], [], node.types)]);
|
|
373
|
-
const statusBits: Child[] = [tr.t(`status.${node.status}`)];
|
|
374
|
-
if (current?.deprecated && !deprecate) statusBits.push(` · ${tr.t('detail.deprecated')}`);
|
|
375
|
-
if (deprecate) {
|
|
376
|
-
statusBits.push(
|
|
377
|
-
' · ',
|
|
378
|
-
h('strong', {
|
|
379
|
-
text: tr.t(deprecate.deprecated ? 'detail.deprecated' : 'detail.undeprecated'),
|
|
380
|
-
}),
|
|
381
|
-
);
|
|
382
|
-
}
|
|
383
|
-
if (current?.implicit) statusBits.push(` · ${tr.t('detail.implicit')}`);
|
|
384
|
-
idRows.push([tr.t('detail.status'), statusBits]);
|
|
385
|
-
out.push(section(tr.t('detail.identity'), null, kv(idRows)));
|
|
386
|
-
|
|
387
|
-
// Name
|
|
388
|
-
const rename = find('rename');
|
|
389
|
-
if (rename) {
|
|
390
|
-
const segs = diffChars(rename.from, rename.to);
|
|
391
|
-
out.push(
|
|
392
|
-
section(
|
|
393
|
-
tr.t('detail.name'),
|
|
394
|
-
null,
|
|
395
|
-
h(
|
|
396
|
-
'div',
|
|
397
|
-
{ class: 'namediff' },
|
|
398
|
-
h(
|
|
399
|
-
'div',
|
|
400
|
-
{ class: 'line old', 'aria-label': `${tr.t('detail.before')}: ${rename.from}` },
|
|
401
|
-
h('span', { class: 'sign', 'aria-hidden': 'true', text: '−' }),
|
|
402
|
-
h('span', null, ...marked(segs, 'before')),
|
|
403
|
-
),
|
|
404
|
-
h(
|
|
405
|
-
'div',
|
|
406
|
-
{ class: 'line new', 'aria-label': `${tr.t('detail.after')}: ${rename.to}` },
|
|
407
|
-
h('span', { class: 'sign', 'aria-hidden': 'true', text: '+' }),
|
|
408
|
-
h('span', null, ...marked(segs, 'after')),
|
|
409
|
-
),
|
|
410
|
-
),
|
|
411
|
-
),
|
|
412
|
-
);
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
// Attributes
|
|
416
|
-
const attrs = find('attrs');
|
|
417
|
-
const changes = attrs?.changes ?? [];
|
|
418
|
-
const all = current?.attrs ?? {};
|
|
419
|
-
const changedKeys = new Set(changes.map((x) => x.key));
|
|
420
|
-
const unchanged = Object.keys(all)
|
|
421
|
-
.filter((k) => !changedKeys.has(k))
|
|
422
|
-
.sort();
|
|
423
|
-
if (changes.length > 0 || unchanged.length > 0) {
|
|
424
|
-
out.push(this.#attributes(node.id, changes, unchanged, all, tr));
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
// Synonyms
|
|
428
|
-
const aliases = find('aliases');
|
|
429
|
-
if (aliases)
|
|
430
|
-
out.push(section(tr.t('detail.aliases'), null, chips(aliases.added, aliases.removed)));
|
|
431
|
-
else if (current && current.aliases.length > 0 && node.status === 'unchanged') {
|
|
432
|
-
out.push(section(tr.t('detail.aliases'), null, chips([], [], current.aliases)));
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
// Hierarchy
|
|
436
|
-
const move = find('move');
|
|
437
|
-
if (move) {
|
|
438
|
-
const dl = h('dl', { class: 'hier' });
|
|
439
|
-
dl.append(
|
|
440
|
-
h('dt', { text: tr.t('detail.before') }),
|
|
441
|
-
h(
|
|
442
|
-
'dd',
|
|
443
|
-
null,
|
|
444
|
-
move.from.length
|
|
445
|
-
? move.from.map((id) => chip(label(id), 'del'))
|
|
446
|
-
: chip(tr.t('detail.none'), 'same'),
|
|
447
|
-
),
|
|
448
|
-
h('dt', { text: tr.t('detail.after') }),
|
|
449
|
-
h(
|
|
450
|
-
'dd',
|
|
451
|
-
null,
|
|
452
|
-
move.to.length
|
|
453
|
-
? move.to.map((id) => chip(label(id), 'add'))
|
|
454
|
-
: chip(tr.t('detail.none'), 'same'),
|
|
455
|
-
),
|
|
456
|
-
);
|
|
457
|
-
out.push(section(tr.t('detail.hierarchy'), null, dl));
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
// Replacement links
|
|
461
|
-
const replacement = find('replacement');
|
|
462
|
-
if (replacement) {
|
|
463
|
-
const rows: [string, Child][] = [];
|
|
464
|
-
const { replacedBy, replaces } = replacement;
|
|
465
|
-
if (replacedBy.added.length || replacedBy.removed.length) {
|
|
466
|
-
rows.push([tr.t('detail.replacedBy'), chips(replacedBy.added, replacedBy.removed)]);
|
|
467
|
-
}
|
|
468
|
-
if (replaces.added.length || replaces.removed.length) {
|
|
469
|
-
rows.push([tr.t('detail.replaces'), chips(replaces.added, replaces.removed)]);
|
|
470
|
-
}
|
|
471
|
-
if (rows.length) out.push(section(tr.t('detail.deprecated'), null, kv(rows)));
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
// Composition
|
|
411
|
+
const fold = node.status === 'modified' || (inHunk && node.status === 'unchanged');
|
|
412
|
+
const changes = this.#changes(node, diff, tr, label);
|
|
475
413
|
const comp = this.#composition(node, diff, tr, label);
|
|
476
|
-
if (comp) out.push(comp);
|
|
477
|
-
|
|
478
|
-
// Evidence
|
|
479
414
|
const evidence = this.#evidence(node, diff, tr, label);
|
|
480
|
-
|
|
481
|
-
|
|
415
|
+
const overview = this.#overview(node, tr, showName, fold);
|
|
416
|
+
const present = (xs: readonly (HTMLElement | null)[]): HTMLElement[] =>
|
|
417
|
+
xs.filter((x): x is HTMLElement => x !== null);
|
|
418
|
+
return fold
|
|
419
|
+
? [present([comp, changes]), present([evidence, ...overview])]
|
|
420
|
+
: [present([...overview, changes, comp, evidence]), []];
|
|
482
421
|
}
|
|
483
422
|
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
all: Readonly<Record<string, AttrValue>>,
|
|
423
|
+
/** What changed on the node: one row per facet (per key for attributes), in facet order. */
|
|
424
|
+
#changes(
|
|
425
|
+
node: UnionNode,
|
|
426
|
+
diff: DiffResult,
|
|
489
427
|
tr: Translator,
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
428
|
+
label: (id: UnionId) => string,
|
|
429
|
+
): HTMLElement | null {
|
|
430
|
+
// Replacement links hold ids as written: name the node they point at when there is one.
|
|
431
|
+
const named = (ids: readonly string[]): string[] =>
|
|
432
|
+
ids.map((id) => (diff.index.nodeByAfter(id) ?? diff.index.nodeByBefore(id))?.label ?? id);
|
|
433
|
+
const rows: [string, Child | Child[]][] = [];
|
|
434
|
+
for (const f of node.facets) {
|
|
435
|
+
switch (f.kind) {
|
|
436
|
+
case 'rekey':
|
|
437
|
+
rows.push([
|
|
438
|
+
tr.t('detail.id'),
|
|
439
|
+
[
|
|
440
|
+
h('div', { class: 'id-text' }, textPair(f.from, f.to, tr)),
|
|
441
|
+
f.score < 1
|
|
442
|
+
? h('div', {
|
|
443
|
+
class: 'note-line',
|
|
444
|
+
text: tr.t('detail.matchScore', { p: formatPercent(f.score) }),
|
|
445
|
+
})
|
|
446
|
+
: null,
|
|
447
|
+
],
|
|
448
|
+
]);
|
|
449
|
+
break;
|
|
450
|
+
case 'rename':
|
|
451
|
+
rows.push([tr.t('detail.name'), textPair(f.from, f.to, tr)]);
|
|
452
|
+
break;
|
|
453
|
+
case 'types':
|
|
454
|
+
case 'aliases':
|
|
455
|
+
rows.push([tr.t(`detail.${f.kind}`), items(f.added, f.removed, tr)]);
|
|
456
|
+
break;
|
|
457
|
+
case 'attrs':
|
|
458
|
+
for (const ch of f.changes) rows.push([ch.key, this.#attrChange(node.id, ch, tr)]);
|
|
459
|
+
break;
|
|
460
|
+
case 'move':
|
|
461
|
+
rows.push([tr.t('detail.hierarchy'), items(f.to.map(label), f.from.map(label), tr)]);
|
|
462
|
+
break;
|
|
463
|
+
case 'deprecate':
|
|
464
|
+
rows.push([
|
|
465
|
+
tr.t('detail.status'),
|
|
466
|
+
h('strong', {
|
|
467
|
+
text: tr.t(f.deprecated ? 'detail.deprecated' : 'detail.undeprecated'),
|
|
468
|
+
}),
|
|
469
|
+
]);
|
|
470
|
+
break;
|
|
471
|
+
case 'replacement':
|
|
472
|
+
for (const [key, links] of [
|
|
473
|
+
['detail.replacedBy', f.replacedBy],
|
|
474
|
+
['detail.replaces', f.replaces],
|
|
475
|
+
] as const) {
|
|
476
|
+
if (links.added.length > 0 || links.removed.length > 0) {
|
|
477
|
+
rows.push([tr.t(key), items(named(links.added), named(links.removed), tr)]);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
break;
|
|
481
|
+
default:
|
|
482
|
+
// Merges and splits: the composition section.
|
|
483
|
+
break;
|
|
523
484
|
}
|
|
524
485
|
}
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
? h(
|
|
529
|
-
'button',
|
|
530
|
-
{
|
|
531
|
-
type: 'button',
|
|
532
|
-
class: 'disclosure',
|
|
533
|
-
'data-focus-key': 'attrs-more',
|
|
534
|
-
'aria-expanded': String(this.#showUnchangedAttrs),
|
|
535
|
-
onclick: () => {
|
|
536
|
-
this.#showUnchangedAttrs = !this.#showUnchangedAttrs;
|
|
537
|
-
this.#render();
|
|
538
|
-
},
|
|
539
|
-
},
|
|
540
|
-
tr.plural('detail.unchangedAttrs', unchanged.length),
|
|
541
|
-
icon('chevron-down'),
|
|
542
|
-
)
|
|
543
|
-
: null;
|
|
544
|
-
return section(tr.t('detail.attributes'), null, table, toggle);
|
|
486
|
+
return rows.length > 0
|
|
487
|
+
? section(tr.t('detail.changes'), rows.length, table(rows, 'attrs changes'))
|
|
488
|
+
: null;
|
|
545
489
|
}
|
|
546
490
|
|
|
547
|
-
|
|
548
|
-
|
|
491
|
+
/** One attribute's change: lists as set changes, short values inline, long ones a line a side. */
|
|
492
|
+
#attrChange(nodeId: string, ch: AttrChange, tr: Translator): Child[] {
|
|
549
493
|
const beforeList = listItems(ch.before);
|
|
550
494
|
const afterList = listItems(ch.after);
|
|
551
|
-
let before: Child | Child[];
|
|
552
|
-
let after: Child | Child[];
|
|
553
495
|
if (beforeList && afterList) {
|
|
554
|
-
const a = new Set(afterList);
|
|
555
496
|
const b = new Set(beforeList);
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
beforeList.
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
'div',
|
|
563
|
-
{ class: 'chips' },
|
|
564
|
-
afterList.map((x) => chip(x, b.has(x) ? 'same' : 'add')),
|
|
497
|
+
const a = new Set(afterList);
|
|
498
|
+
return items(
|
|
499
|
+
afterList.filter((x) => !b.has(x)),
|
|
500
|
+
beforeList.filter((x) => !a.has(x)),
|
|
501
|
+
tr,
|
|
502
|
+
afterList.filter((x) => b.has(x)),
|
|
565
503
|
);
|
|
566
|
-
} else if (typeof ch.before === 'string' && typeof ch.after === 'string') {
|
|
567
|
-
const segs = diffWords(ch.before, ch.after);
|
|
568
|
-
before = this.#value(`${nodeId}|b|${ch.key}`, ch.before, tr, marked(segs, 'before'));
|
|
569
|
-
after = this.#value(`${nodeId}|a|${ch.key}`, ch.after, tr, marked(segs, 'after'));
|
|
570
|
-
} else {
|
|
571
|
-
before =
|
|
572
|
-
ch.before === undefined
|
|
573
|
-
? empty()
|
|
574
|
-
: this.#value(`${nodeId}|b|${ch.key}`, formatValue(ch.before), tr, [
|
|
575
|
-
h('del', { class: 'hl', text: formatValue(ch.before) }),
|
|
576
|
-
]);
|
|
577
|
-
after =
|
|
578
|
-
ch.after === undefined
|
|
579
|
-
? empty()
|
|
580
|
-
: this.#value(`${nodeId}|a|${ch.key}`, formatValue(ch.after), tr, [
|
|
581
|
-
h('ins', { class: 'hl', text: formatValue(ch.after) }),
|
|
582
|
-
]);
|
|
583
504
|
}
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
h('
|
|
589
|
-
|
|
590
|
-
|
|
505
|
+
const key = `${nodeId}|${ch.key}`;
|
|
506
|
+
const before = formatValue(ch.before);
|
|
507
|
+
const after = formatValue(ch.after);
|
|
508
|
+
if (ch.before === undefined) {
|
|
509
|
+
const ins = h('ins', { class: 'hl', text: after });
|
|
510
|
+
return [signed('new', this.#value(`${key}|a`, after, tr, [ins]))];
|
|
511
|
+
}
|
|
512
|
+
if (ch.after === undefined) {
|
|
513
|
+
const del = h('del', { class: 'hl', text: before });
|
|
514
|
+
return [signed('old', this.#value(`${key}|b`, before, tr, [del]))];
|
|
515
|
+
}
|
|
516
|
+
const segs: readonly DiffSegment[] =
|
|
517
|
+
typeof ch.before === 'string' && typeof ch.after === 'string'
|
|
518
|
+
? diffWords(before, after)
|
|
519
|
+
: [
|
|
520
|
+
{ op: 'delete', text: before },
|
|
521
|
+
{ op: 'insert', text: after },
|
|
522
|
+
];
|
|
523
|
+
const b = marked(segs, 'before');
|
|
524
|
+
const a = marked(segs, 'after');
|
|
525
|
+
return before.length + after.length <= INLINE_CHARS && !/\n/.test(before + after)
|
|
526
|
+
? [pair(b, a, tr)]
|
|
527
|
+
: [
|
|
528
|
+
signed('old', this.#value(`${key}|b`, before, tr, b)),
|
|
529
|
+
signed('new', this.#value(`${key}|a`, after, tr, a)),
|
|
530
|
+
];
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/**
|
|
534
|
+
* Identity and attributes, without what the changes section shows. Folded, the attributes
|
|
535
|
+
* (all unchanged) hide behind a toggle; otherwise they get a section of their own.
|
|
536
|
+
*/
|
|
537
|
+
#overview(node: UnionNode, tr: Translator, showName: boolean, fold: boolean): HTMLElement[] {
|
|
538
|
+
const has = (kind: NodeFacet['kind']): boolean => node.facets.some((f) => f.kind === kind);
|
|
539
|
+
const current = node.after ?? node.before;
|
|
540
|
+
const rows: [string, Child | Child[]][] = [];
|
|
541
|
+
if (showName && !has('rename')) rows.push([tr.t('detail.name'), node.label]);
|
|
542
|
+
if (!has('rekey')) {
|
|
543
|
+
rows.push([
|
|
544
|
+
tr.t('detail.id'),
|
|
545
|
+
h('span', { class: 'id-text', text: node.afterId ?? node.beforeId ?? node.id }),
|
|
546
|
+
]);
|
|
547
|
+
}
|
|
548
|
+
if (!has('types') && node.types.length > 0) {
|
|
549
|
+
rows.push([tr.t('detail.types'), chips([], [], node.types)]);
|
|
550
|
+
}
|
|
551
|
+
if (!has('aliases') && current && current.aliases.length > 0) {
|
|
552
|
+
rows.push([tr.t('detail.aliases'), chips([], [], current.aliases)]);
|
|
553
|
+
}
|
|
554
|
+
const status: Child[] = [tr.t(`status.${node.status}`)];
|
|
555
|
+
if (current?.deprecated && !has('deprecate')) status.push(` · ${tr.t('detail.deprecated')}`);
|
|
556
|
+
if (current?.implicit) status.push(` · ${tr.t('detail.implicit')}`);
|
|
557
|
+
rows.push([tr.t('detail.status'), status]);
|
|
558
|
+
|
|
559
|
+
const all = current?.attrs ?? {};
|
|
560
|
+
const changed = new Set<string>();
|
|
561
|
+
for (const f of node.facets) {
|
|
562
|
+
if (f.kind === 'attrs') for (const c of f.changes) changed.add(c.key);
|
|
563
|
+
}
|
|
564
|
+
const keys = Object.keys(all)
|
|
565
|
+
.filter((k) => !changed.has(k))
|
|
566
|
+
.sort();
|
|
567
|
+
const attrs = (): HTMLElement =>
|
|
568
|
+
table(keys.map((k) => [k, this.#value(`${node.id}|u|${k}`, formatValue(all[k]), tr)]));
|
|
569
|
+
if (!fold) {
|
|
570
|
+
const out = [section(tr.t('detail.identity'), null, kv(rows))];
|
|
571
|
+
if (keys.length > 0) out.push(section(tr.t('detail.attributes'), null, attrs()));
|
|
572
|
+
return out;
|
|
573
|
+
}
|
|
574
|
+
return [
|
|
575
|
+
section(
|
|
576
|
+
tr.t('detail.identity'),
|
|
577
|
+
null,
|
|
578
|
+
kv(rows),
|
|
579
|
+
keys.length > 0
|
|
580
|
+
? h(
|
|
581
|
+
'button',
|
|
582
|
+
{
|
|
583
|
+
type: 'button',
|
|
584
|
+
class: 'disclosure',
|
|
585
|
+
'data-focus-key': 'attrs-more',
|
|
586
|
+
'aria-expanded': String(this.#showUnchangedAttrs),
|
|
587
|
+
onclick: () => {
|
|
588
|
+
this.#showUnchangedAttrs = !this.#showUnchangedAttrs;
|
|
589
|
+
this.#render();
|
|
590
|
+
},
|
|
591
|
+
},
|
|
592
|
+
tr.plural('detail.unchangedAttrs', keys.length),
|
|
593
|
+
icon('chevron-down'),
|
|
594
|
+
)
|
|
595
|
+
: null,
|
|
596
|
+
keys.length > 0 && this.#showUnchangedAttrs ? attrs() : null,
|
|
597
|
+
),
|
|
598
|
+
];
|
|
591
599
|
}
|
|
592
600
|
|
|
593
601
|
/** A value cell, clamped to 4 lines with “Show more” when long. */
|