@ascribed/review 0.1.2-next.8 → 0.2.1-next.10

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.
@@ -1,17 +1,5 @@
1
- // @ascribed/review/marks: marks what changed on a rendered page, in the
2
- // browser.
3
- //
4
- // The page is Ascribe's site output rendered with source anchors (the
5
- // site-render contract, §7): every block's element carries
6
- // `data-ascribe-source`, and `data-ascribe-via` when it came through
7
- // includes. The changes are `ascribe diff --format json`'s, for that page.
8
- // `markChanges` applies them to the page's elements; `marks.css` (exported
9
- // as `@ascribed/review/marks.css`) draws them, and `setShow` switches between
10
- // the changes, the page as it will be, and the page as it was.
11
- //
12
- // Everything the marks add is an element with `data-ascribe-ui`, and every
13
- // element they change gets `data-ascribe-change`, so `clearMarks` can put the
14
- // page back as it was rendered.
1
+ import { CLASS_DEL, CLASS_FLASH, CLASS_HINT, CLASS_INS, CLASS_LABEL, CLASS_LABEL_BEFORE, CLASS_MARKS, CLASS_MOVED_FROM, CLASS_MOVE_AFTER, CLASS_MOVE_LINK, CLASS_MOVE_NOTE, CLASS_OPEN, CLASS_REMOVED, CLASS_REMOVED_BODY, CLASS_TOGGLE, CLASS_WAS_COPY, CLASS_WHERE, DATA_CHANGE, DATA_HAS_WAS, DATA_HINT, DATA_LABEL, DATA_MOVE, DATA_SHOW, DATA_SOURCE, DATA_UI, DATA_VIA, DATA_WAS_ONLY, DATA_WAS_SOURCE, ELEMENT_TAB, } from "../names.js";
2
+ export { titleNodes } from "./title.js";
15
3
  const LABELS = {
16
4
  added: "Added",
17
5
  changed: "Changed",
@@ -34,7 +22,7 @@ const LABEL_INSIDE = new Set([
34
22
  "td",
35
23
  "th",
36
24
  // A tab's panel: before it would put the label among the tab list's panels.
37
- "ascribe-tab",
25
+ ELEMENT_TAB,
38
26
  ]);
39
27
  /** Parses `<path>:<first>-<last>`, splitting at the last `:`. */
40
28
  export function parseSource(source) {
@@ -78,7 +66,7 @@ export function describeSource(source, via) {
78
66
  return text;
79
67
  }
80
68
  function viaOf(element) {
81
- return element.getAttribute("data-ascribe-via") ?? "";
69
+ return element.getAttribute(DATA_VIA) ?? "";
82
70
  }
83
71
  /**
84
72
  * The element of a block: the one with exactly its anchor, or, when there's
@@ -88,8 +76,8 @@ function viaOf(element) {
88
76
  */
89
77
  export function findBlock(root, anchor) {
90
78
  const via = anchor.via.join(" ");
91
- const anchored = Array.from(root.querySelectorAll("[data-ascribe-source]"));
92
- const exact = anchored.find((el) => el.getAttribute("data-ascribe-source") === anchor.source && viaOf(el) === via);
79
+ const anchored = Array.from(root.querySelectorAll(`[${DATA_SOURCE}]`));
80
+ const exact = anchored.find((el) => el.getAttribute(DATA_SOURCE) === anchor.source && viaOf(el) === via);
93
81
  if (exact)
94
82
  return exact;
95
83
  const want = parseSource(anchor.source);
@@ -100,7 +88,7 @@ export function findBlock(root, anchor) {
100
88
  for (const el of anchored) {
101
89
  if (viaOf(el) !== via)
102
90
  continue;
103
- const has = parseSource(el.getAttribute("data-ascribe-source") ?? "");
91
+ const has = parseSource(el.getAttribute(DATA_SOURCE) ?? "");
104
92
  if (!has || has.path !== want.path)
105
93
  continue;
106
94
  if (has.first > want.first || has.last < want.last)
@@ -116,7 +104,7 @@ export function findBlock(root, anchor) {
116
104
  function ui(doc, tag, className, text) {
117
105
  const el = doc.createElement(tag);
118
106
  el.className = className;
119
- el.setAttribute("data-ascribe-ui", "");
107
+ el.setAttribute(DATA_UI, "");
120
108
  if (text !== undefined)
121
109
  el.textContent = text;
122
110
  return el;
@@ -124,13 +112,13 @@ function ui(doc, tag, className, text) {
124
112
  /** A label, inside the block at its start, or just before it. */
125
113
  function addLabel(element, kind) {
126
114
  const doc = element.ownerDocument;
127
- const label = ui(doc, "span", "ascribe-label", LABELS[kind]);
128
- label.setAttribute("data-ascribe-label", kind);
115
+ const label = ui(doc, "span", CLASS_LABEL, LABELS[kind]);
116
+ label.setAttribute(DATA_LABEL, kind);
129
117
  if (LABEL_INSIDE.has(element.localName)) {
130
118
  element.prepend(label);
131
119
  }
132
120
  else {
133
- label.classList.add("ascribe-label-before");
121
+ label.classList.add(CLASS_LABEL_BEFORE);
134
122
  element.before(label);
135
123
  }
136
124
  }
@@ -148,8 +136,8 @@ function contentOf(holder, before) {
148
136
  function copyOf(element) {
149
137
  const copy = element.cloneNode(true);
150
138
  for (const el of [copy, ...Array.from(copy.querySelectorAll("*"))]) {
151
- el.removeAttribute("data-ascribe-source");
152
- el.removeAttribute("data-ascribe-via");
139
+ el.removeAttribute(DATA_SOURCE);
140
+ el.removeAttribute(DATA_VIA);
153
141
  el.removeAttribute("id");
154
142
  }
155
143
  return copy;
@@ -181,7 +169,7 @@ function ownText(element) {
181
169
  for (const child of Array.from(node.childNodes)) {
182
170
  if (child.nodeType === Node.TEXT_NODE)
183
171
  text += child.nodeValue ?? "";
184
- else if (child instanceof Element && !child.hasAttribute("data-ascribe-ui"))
172
+ else if (child instanceof Element && !child.hasAttribute(DATA_UI))
185
173
  walk(child);
186
174
  }
187
175
  };
@@ -205,9 +193,7 @@ class Placer {
205
193
  let previous = this.#last.get(after) ?? after;
206
194
  // Past what the marks put after the block itself: its copy as it
207
195
  // was, or its "moved from" link.
208
- for (let next = previous.nextElementSibling; next &&
209
- (next.hasAttribute("data-ascribe-was-only") ||
210
- next.classList.contains("ascribe-move-after")); next = next.nextElementSibling) {
196
+ for (let next = previous.nextElementSibling; next && (next.hasAttribute(DATA_WAS_ONLY) || next.classList.contains(CLASS_MOVE_AFTER)); next = next.nextElementSibling) {
211
197
  previous = next;
212
198
  }
213
199
  const el = make(after.parentElement);
@@ -226,7 +212,7 @@ class Placer {
226
212
  else {
227
213
  // First among the container's blocks: before its first anchored
228
214
  // child, or its first label, or at its end.
229
- const first = Array.from(container.children).find((c) => c.hasAttribute("data-ascribe-source") || c.hasAttribute("data-ascribe-ui"));
215
+ const first = Array.from(container.children).find((c) => c.hasAttribute(DATA_SOURCE) || c.hasAttribute(DATA_UI));
230
216
  if (first)
231
217
  first.before(el);
232
218
  else
@@ -270,8 +256,8 @@ function textMap(element) {
270
256
  }
271
257
  }
272
258
  else if (child instanceof Element &&
273
- !child.hasAttribute("data-ascribe-ui") &&
274
- !child.hasAttribute("data-ascribe-source")) {
259
+ !child.hasAttribute(DATA_UI) &&
260
+ !child.hasAttribute(DATA_SOURCE)) {
275
261
  walk(child);
276
262
  }
277
263
  }
@@ -342,10 +328,10 @@ function markWords(element, words) {
342
328
  edits.sort((a, b) => b.at - a.at || (a.kind === "del" ? -1 : 1));
343
329
  for (const edit of edits) {
344
330
  if (edit.kind === "ins") {
345
- wrapRange(map, offset + edit.at, offset + edit.end, () => ui(doc, "ins", "ascribe-ins"));
331
+ wrapRange(map, offset + edit.at, offset + edit.end, () => ui(doc, "ins", CLASS_INS));
346
332
  }
347
333
  else {
348
- const del = ui(doc, "del", "ascribe-del", edit.text);
334
+ const del = ui(doc, "del", CLASS_DEL, edit.text);
349
335
  insertAt(map, offset + edit.at, del, element);
350
336
  }
351
337
  }
@@ -407,33 +393,33 @@ export function markChanges(root, changes, options = {}) {
407
393
  const was = options.was ?? null;
408
394
  const placer = new Placer(root);
409
395
  const marks = [];
410
- root.classList.add("ascribe-marks");
411
- if (!root.hasAttribute("data-ascribe-show"))
412
- root.setAttribute("data-ascribe-show", "changes");
396
+ root.classList.add(CLASS_MARKS);
397
+ if (!root.hasAttribute(DATA_SHOW))
398
+ root.setAttribute(DATA_SHOW, "changes");
413
399
  let moveCount = 0;
414
400
  for (const change of changes) {
415
401
  switch (change.kind) {
416
402
  case "added":
417
403
  case "changed": {
418
404
  const element = change.now ? findBlock(root, change.now) : null;
419
- if (!element || element.hasAttribute("data-ascribe-change"))
405
+ if (!element || element.hasAttribute(DATA_CHANGE))
420
406
  break;
421
- element.setAttribute("data-ascribe-change", change.kind);
407
+ element.setAttribute(DATA_CHANGE, change.kind);
422
408
  addLabel(element, change.kind);
423
409
  if (change.kind === "changed") {
424
410
  // Where it was, for threads on its old text.
425
411
  if (change.was)
426
- element.setAttribute("data-ascribe-was-source", change.was.source);
412
+ element.setAttribute(DATA_WAS_SOURCE, change.was.source);
427
413
  const before = was && change.was ? findBlock(was, change.was) : null;
428
414
  if (change.words)
429
415
  markWords(element, change.words);
430
416
  if (before) {
431
417
  // As it was, the block as the old page rendered it.
432
418
  const copy = copyOf(before);
433
- copy.setAttribute("data-ascribe-ui", "");
434
- copy.setAttribute("data-ascribe-was-only", "");
419
+ copy.setAttribute(DATA_UI, "");
420
+ copy.setAttribute(DATA_WAS_ONLY, "");
435
421
  element.after(copy);
436
- element.setAttribute("data-ascribe-has-was", "");
422
+ element.setAttribute(DATA_HAS_WAS, "");
437
423
  }
438
424
  }
439
425
  marks.push({ change, element });
@@ -442,23 +428,23 @@ export function markChanges(root, changes, options = {}) {
442
428
  case "removed": {
443
429
  const before = was && change.was ? findBlock(was, change.was) : null;
444
430
  const element = placer.place(change, (container) => {
445
- const holder = ui(doc, holderFor(container), "ascribe-removed");
446
- holder.setAttribute("data-ascribe-change", "removed");
431
+ const holder = ui(doc, holderFor(container), CLASS_REMOVED);
432
+ holder.setAttribute(DATA_CHANGE, "removed");
447
433
  if (change.was)
448
- holder.setAttribute("data-ascribe-was-source", change.was.source);
449
- const label = ui(doc, "span", "ascribe-label", LABELS.removed);
450
- label.setAttribute("data-ascribe-label", "removed");
451
- const body = ui(doc, "div", "ascribe-removed-body");
434
+ holder.setAttribute(DATA_WAS_SOURCE, change.was.source);
435
+ const label = ui(doc, "span", CLASS_LABEL, LABELS.removed);
436
+ label.setAttribute(DATA_LABEL, "removed");
437
+ const body = ui(doc, "div", CLASS_REMOVED_BODY);
452
438
  if (before)
453
439
  body.append(...contentOf(holder, before));
454
440
  else
455
441
  body.textContent = change.text ?? "";
456
- const toggle = ui(doc, "button", "ascribe-toggle", "Show");
442
+ const toggle = ui(doc, "button", CLASS_TOGGLE, "Show");
457
443
  toggle.type = "button";
458
444
  toggle.setAttribute("aria-expanded", "false");
459
445
  toggle.addEventListener("click", (event) => {
460
446
  event.stopPropagation();
461
- const open = holder.classList.toggle("ascribe-open");
447
+ const open = holder.classList.toggle(CLASS_OPEN);
462
448
  toggle.textContent = open ? "Collapse" : "Show";
463
449
  toggle.setAttribute("aria-expanded", String(open));
464
450
  });
@@ -470,25 +456,25 @@ export function markChanges(root, changes, options = {}) {
470
456
  }
471
457
  case "moved": {
472
458
  const element = change.now ? findBlock(root, change.now) : null;
473
- if (!element || element.hasAttribute("data-ascribe-change"))
459
+ if (!element || element.hasAttribute(DATA_CHANGE))
474
460
  break;
475
461
  const id = `ascribe-move-${++moveCount}`;
476
462
  const before = was && change.was ? findBlock(was, change.was) : null;
477
463
  const stub = placer.place(change, (container) => {
478
- const holder = ui(doc, holderFor(container), "ascribe-moved-from");
464
+ const holder = ui(doc, holderFor(container), CLASS_MOVED_FROM);
479
465
  holder.id = `${id}-from`;
480
- holder.setAttribute("data-ascribe-change", "moved-from");
466
+ holder.setAttribute(DATA_CHANGE, "moved-from");
481
467
  if (change.was)
482
- holder.setAttribute("data-ascribe-was-source", change.was.source);
483
- const note = ui(doc, "span", "ascribe-move-note");
468
+ holder.setAttribute(DATA_WAS_SOURCE, change.was.source);
469
+ const note = ui(doc, "span", CLASS_MOVE_NOTE);
484
470
  note.append(doc.createTextNode(`A ${blockWord(element)} moved from here `));
485
- const link = ui(doc, "a", "ascribe-move-link");
471
+ const link = ui(doc, "a", CLASS_MOVE_LINK);
486
472
  link.href = `#${id}`;
487
473
  note.append(link);
488
474
  holder.append(note);
489
475
  // As it was, the block itself.
490
- const copy = ui(doc, "div", "ascribe-was-copy");
491
- copy.setAttribute("data-ascribe-was-only", "");
476
+ const copy = ui(doc, "div", CLASS_WAS_COPY);
477
+ copy.setAttribute(DATA_WAS_ONLY, "");
492
478
  if (before)
493
479
  copy.append(...contentOf(holder, before));
494
480
  else
@@ -496,26 +482,26 @@ export function markChanges(root, changes, options = {}) {
496
482
  holder.append(copy);
497
483
  return holder;
498
484
  });
499
- element.setAttribute("data-ascribe-change", "moved");
500
- element.setAttribute("data-ascribe-move", id);
485
+ element.setAttribute(DATA_CHANGE, "moved");
486
+ element.setAttribute(DATA_MOVE, id);
501
487
  if (!element.id)
502
488
  element.id = id;
503
489
  addLabel(element, "moved");
504
- const back = ui(doc, "a", "ascribe-move-link");
490
+ const back = ui(doc, "a", CLASS_MOVE_LINK);
505
491
  back.href = `#${stub.id}`;
506
492
  back.textContent = `from “${headingBefore(root, stub) ?? "the top of the page"}” ↑`;
507
- const forward = stub.querySelector(".ascribe-move-link");
493
+ const forward = stub.querySelector(`.${CLASS_MOVE_LINK}`);
508
494
  if (forward) {
509
495
  forward.href = `#${element.id}`;
510
496
  forward.textContent = `to “${headingBefore(root, element) ?? "the top of the page"}” ↓`;
511
497
  }
512
498
  if (LABEL_INSIDE.has(element.localName)) {
513
- const note = ui(doc, "span", "ascribe-move-note");
499
+ const note = ui(doc, "span", CLASS_MOVE_NOTE);
514
500
  note.append(" ", back);
515
501
  element.append(note);
516
502
  }
517
503
  else {
518
- const holder = ui(doc, "div", "ascribe-move-note ascribe-move-after");
504
+ const holder = ui(doc, "div", `${CLASS_MOVE_NOTE} ${CLASS_MOVE_AFTER}`);
519
505
  holder.append(back);
520
506
  element.after(holder);
521
507
  }
@@ -552,7 +538,7 @@ function hintHidden(root, marks) {
552
538
  const summaries = new Map();
553
539
  for (const { change, element } of marks) {
554
540
  for (let el = element; el && el !== root; el = el.parentElement) {
555
- if (el.localName === "ascribe-tab") {
541
+ if (el.localName === ELEMENT_TAB) {
556
542
  const tab = tabs.get(el) ?? { added: false, count: 0 };
557
543
  if (el === element && change.kind === "added")
558
544
  tab.added = true;
@@ -570,8 +556,8 @@ function hintHidden(root, marks) {
570
556
  const hint = (kind, text) => {
571
557
  // A space before it, not only a margin, so the label reads "Linux 1
572
558
  // change" to a screen reader, not "Linux1 change".
573
- const el = ui(doc, "span", "ascribe-hint", ` ${text}`);
574
- el.setAttribute("data-ascribe-hint", kind);
559
+ const el = ui(doc, "span", CLASS_HINT, ` ${text}`);
560
+ el.setAttribute(DATA_HINT, kind);
575
561
  return el;
576
562
  };
577
563
  const watching = [];
@@ -615,9 +601,9 @@ function summaryOf(element) {
615
601
  */
616
602
  export function tabButton(tab) {
617
603
  const group = tab.parentElement;
618
- if (tab.localName !== "ascribe-tab" || !group)
604
+ if (tab.localName !== ELEMENT_TAB || !group)
619
605
  return null;
620
- const tabs = Array.from(group.children).filter((c) => c.localName === "ascribe-tab");
606
+ const tabs = Array.from(group.children).filter((c) => c.localName === ELEMENT_TAB);
621
607
  const buttons = group.querySelectorAll(':scope > [role="tablist"] > [role="tab"]');
622
608
  return buttons[tabs.indexOf(tab)] ?? null;
623
609
  }
@@ -643,27 +629,27 @@ export function clearMarks(root) {
643
629
  for (const observer of tabWatchers.get(root) ?? [])
644
630
  observer.disconnect();
645
631
  tabWatchers.delete(root);
646
- for (const el of Array.from(root.querySelectorAll("[data-ascribe-ui]"))) {
632
+ for (const el of Array.from(root.querySelectorAll(`[${DATA_UI}]`))) {
647
633
  if (el.localName === "ins")
648
634
  el.replaceWith(...Array.from(el.childNodes));
649
635
  else
650
636
  el.remove();
651
637
  }
652
- for (const el of Array.from(root.querySelectorAll("[data-ascribe-change]"))) {
653
- el.removeAttribute("data-ascribe-change");
654
- el.removeAttribute("data-ascribe-has-was");
655
- el.removeAttribute("data-ascribe-was-source");
656
- if (el.hasAttribute("data-ascribe-move")) {
657
- if (el.id === el.getAttribute("data-ascribe-move"))
638
+ for (const el of Array.from(root.querySelectorAll(`[${DATA_CHANGE}]`))) {
639
+ el.removeAttribute(DATA_CHANGE);
640
+ el.removeAttribute(DATA_HAS_WAS);
641
+ el.removeAttribute(DATA_WAS_SOURCE);
642
+ if (el.hasAttribute(DATA_MOVE)) {
643
+ if (el.id === el.getAttribute(DATA_MOVE))
658
644
  el.removeAttribute("id");
659
- el.removeAttribute("data-ascribe-move");
645
+ el.removeAttribute(DATA_MOVE);
660
646
  }
661
647
  }
662
648
  root.normalize();
663
649
  }
664
650
  /** Shows the changes, the page as it will be, or the page as it was. */
665
651
  export function setShow(root, show) {
666
- root.setAttribute("data-ascribe-show", show);
652
+ root.setAttribute(DATA_SHOW, show);
667
653
  }
668
654
  /**
669
655
  * Makes `element` visible: opens the `<details>` and selects the tabs it's
@@ -673,7 +659,7 @@ export function reveal(element) {
673
659
  for (let el = element; el; el = el.parentElement) {
674
660
  if (el instanceof HTMLDetailsElement && !el.open)
675
661
  el.open = true;
676
- if (el.localName === "ascribe-tab" && el.hidden)
662
+ if (el.localName === ELEMENT_TAB && el.hidden)
677
663
  tabButton(el)?.click();
678
664
  }
679
665
  }
@@ -684,10 +670,10 @@ export function goTo(element) {
684
670
  if (typeof element.scrollIntoView === "function") {
685
671
  element.scrollIntoView({ block: "center", behavior: "smooth" });
686
672
  }
687
- element.classList.remove("ascribe-flash");
673
+ element.classList.remove(CLASS_FLASH);
688
674
  // Restart the animation.
689
675
  void element.offsetWidth;
690
- element.classList.add("ascribe-flash");
676
+ element.classList.add(CLASS_FLASH);
691
677
  if (!element.hasAttribute("tabindex"))
692
678
  element.setAttribute("tabindex", "-1");
693
679
  element.focus({ preventScroll: true });
@@ -698,13 +684,11 @@ export function goTo(element) {
698
684
  */
699
685
  export function showSources(root) {
700
686
  const doc = root.ownerDocument;
701
- const tip = ui(doc, "span", "ascribe-where");
687
+ const tip = ui(doc, "span", CLASS_WHERE);
702
688
  tip.setAttribute("role", "status");
703
689
  let current = null;
704
690
  const show = (target) => {
705
- const el = target instanceof Element
706
- ? target.closest("[data-ascribe-source], [data-ascribe-was-source]")
707
- : null;
691
+ const el = target instanceof Element ? target.closest(`[${DATA_SOURCE}], [${DATA_WAS_SOURCE}]`) : null;
708
692
  if (el === current)
709
693
  return;
710
694
  current = el;
@@ -713,10 +697,10 @@ export function showSources(root) {
713
697
  current = null;
714
698
  return;
715
699
  }
716
- const source = el.getAttribute("data-ascribe-source");
700
+ const source = el.getAttribute(DATA_SOURCE);
717
701
  tip.textContent = source
718
- ? describeSource(source, el.getAttribute("data-ascribe-via"))
719
- : `was ${describeSource(el.getAttribute("data-ascribe-was-source") ?? "")}`;
702
+ ? describeSource(source, el.getAttribute(DATA_VIA))
703
+ : `was ${describeSource(el.getAttribute(DATA_WAS_SOURCE) ?? "")}`;
720
704
  const box = el.getBoundingClientRect();
721
705
  const host = root.getBoundingClientRect();
722
706
  tip.style.top = `${box.top - host.top}px`;