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

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/README.md CHANGED
@@ -36,7 +36,7 @@ The colors are the `--ascribe-review-*` custom properties at the top of `marks.c
36
36
 
37
37
  ## The static report
38
38
 
39
- `src/report/` is the script of `ascribe diff --format html`'s report, which draws the list of changed pages and each page with its marks. It isn't an entry point: `pnpm --filter @ascribed/review embed` bundles it with the marks and `@ascribed/elements` into `crates/tessera-diff/src/html/`, where the binary embeds it, along with one stylesheet made of the element library's, `marks.css`, and `src/report/report.css`. It also writes the script's SHA-256 beside it, which the report's content security policy names as the one script allowed to run. Run it after changing any of them; `test/embedded.test.ts` fails until you do.
39
+ `src/report/` is the script of `ascribe diff --format html`'s report, which draws the list of changed pages and each page with its marks. It isn't an entry point: `pnpm --filter @ascribed/review embed` bundles it with the marks and `@ascribed/elements` into `crates/ascribe-diff/src/html/`, where the binary embeds it, along with one stylesheet made of the element library's, `marks.css`, and `src/report/report.css`. It also writes the script's SHA-256 beside it, which the report's content security policy names as the one script allowed to run. Run it after changing any of them; `test/embedded.test.ts` fails until you do.
40
40
 
41
41
  ## `@ascribed/review/overlay`
42
42
 
@@ -14,8 +14,6 @@ export interface MarkedSection {
14
14
  * this, which reading and submitting remove.
15
15
  */
16
16
  export declare const PLACEHOLDER = "<!-- ascribe:review -->";
17
- /** Ends a review's summary when anchored comments follow it. */
18
- export declare const SUMMARY_END = "<!-- ascribe:summary -->";
19
17
  /** `body` without the placeholder summary. */
20
18
  export declare function withoutPlaceholder(body: string): string;
21
19
  /**
@@ -21,7 +21,7 @@ const MARKER = /<!-- ascribe:anchor (\S+)(?: build=(\S+))?( quote=text)? -->/g;
21
21
  */
22
22
  export const PLACEHOLDER = "<!-- ascribe:review -->";
23
23
  /** Ends a review's summary when anchored comments follow it. */
24
- export const SUMMARY_END = "<!-- ascribe:summary -->";
24
+ const SUMMARY_END = "<!-- ascribe:summary -->";
25
25
  /** `body` without the placeholder summary. */
26
26
  export function withoutPlaceholder(body) {
27
27
  return body.split(PLACEHOLDER).join("").trim();
@@ -1 +1 @@
1
- {"version":3,"file":"marker.js","sourceRoot":"","sources":["../../src/github/marker.ts"],"names":[],"mappings":"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","sourcesContent":["// Comments GitHub can't anchor to a line go on the pull request's\n// conversation, each ending with a hidden marker naming its block:\n//\n// <!-- ascribe:anchor guides/install.md:12-14 build=site -->\n//\n// Several can share one body (a review's), one after another. A review's\n// summary goes before them, ended by another marker:\n//\n// Looks good, a few notes below.\n//\n// <!-- ascribe:summary -->\n//\n// A comment quotes its block's source, or its text as the page shows it,\n// escaped so GitHub shows it as written; the marker of one that quotes the\n// shown text ends ` quote=text`.\n\n/** One anchored comment, as written in a body. */\nexport interface MarkedSection {\n /** The block's source. */\n source: string;\n build: string | undefined;\n /** The comment, without the quote, the link line, or the marker. */\n body: string;\n /** The block's text the comment quoted, if it did. */\n quote: string | undefined;\n}\n\nconst MARKER = /<!-- ascribe:anchor (\\S+)(?: build=(\\S+))?( quote=text)? -->/g;\n\n/**\n * The summary a pending review starts with. GitHub can't edit the summary of\n * a pending review created without one, so the session's reviews start with\n * this, which reading and submitting remove.\n */\nexport const PLACEHOLDER = \"<!-- ascribe:review -->\";\n\n/** Ends a review's summary when anchored comments follow it. */\nexport const SUMMARY_END = \"<!-- ascribe:summary -->\";\n\n/** `body` without the placeholder summary. */\nexport function withoutPlaceholder(body: string): string {\n return body.split(PLACEHOLDER).join(\"\").trim();\n}\n\n/**\n * The marker for a block. Without a build (or with `\"\"`), the comment is on\n * every build's page. `shown` marks a quote of the shown text.\n */\nexport function marker(source: string, build: string | undefined, shown = false): string {\n return `<!-- ascribe:anchor ${source}${build === undefined || build === \"\" ? \"\" : ` build=${build}`}${shown ? \" quote=text\" : \"\"} -->`;\n}\n\n/** Joins a mention or reference sign to what follows, so GitHub doesn't link it. */\nconst JOINER = \"\\u2060\";\n\n/**\n * Plain text as Markdown that GitHub shows as the text: punctuation\n * escaped, `@name` and `#123` not linked, line breaks and leading spaces\n * kept.\n */\nexport function escapeText(text: string): string {\n const lines = text.split(/\\r?\\n/).map((line) => {\n const indent = /^ */.exec(line)?.[0].length ?? 0;\n const rest = line\n .slice(indent)\n .replace(/[!-/:-@[-`{-~]/g, (c) => (c === \"<\" ? \"&lt;\" : `\\\\${c}`))\n .replace(/[@#]/g, `$&${JOINER}`);\n return \"\\u00a0\".repeat(indent) + rest;\n });\n // A backslash at the end of a line breaks it, but not before a blank line.\n return lines\n .map((line, i) => (line !== \"\" && (lines[i + 1] ?? \"\") !== \"\" ? `${line}\\\\` : line))\n .join(\"\\n\");\n}\n\n/** Text that `escapeText` wrote, as it was. */\nexport function unescapeText(text: string): string {\n return text\n .split(/\\r?\\n/)\n .map((line) => {\n const trailing = /\\\\*$/.exec(line)?.[0].length ?? 0;\n const unbroken = trailing % 2 === 1 ? line.slice(0, -1) : line;\n const indent = /^\\u00a0*/.exec(unbroken)?.[0].length ?? 0;\n return (\n \" \".repeat(indent) +\n unbroken\n .slice(indent)\n .replace(new RegExp(`([@#])${JOINER}`, \"g\"), \"$1\")\n .replace(/\\\\([!-/:-@[-`{-~])|&lt;/g, (_, c: string | undefined) => c ?? \"<\")\n );\n })\n .join(\"\\n\");\n}\n\n/**\n * Writes a section: the block's text quoted, the comment, a line linking the\n * block's lines, and the marker.\n */\nexport function formatSection(options: {\n source: string;\n build: string | undefined;\n body: string;\n quote: string | undefined;\n /** Whether `quote` is the block's text as the page shows it, rather than its source. */\n shown?: boolean;\n link: { label: string; url: string } | undefined;\n}): string {\n const parts: string[] = [];\n const shown = options.shown === true;\n if (options.quote !== undefined && options.quote.trim() !== \"\") {\n parts.push(\n (shown ? escapeText(options.quote) : options.quote)\n .split(/\\r?\\n/)\n .map((line) => (line === \"\" ? \">\" : `> ${line}`))\n .join(\"\\n\"),\n );\n }\n parts.push(options.body.trim());\n const end = marker(options.source, options.build, shown);\n parts.push(\n options.link === undefined\n ? end\n : `<sub>On [${options.link.label.replace(/[[\\]]/g, \"\\\\$&\")}](${options.link.url})</sub>\\n${end}`,\n );\n return parts.join(\"\\n\\n\");\n}\n\n/**\n * Splits a body into its summary, its marked sections, and the text after\n * the last marker (`rest`), which isn't anchored either.\n */\nexport function parseSections(text: string): {\n summary: string;\n sections: MarkedSection[];\n rest: string;\n} {\n let body = withoutPlaceholder(text);\n let summary = \"\";\n // The summary ends before the first comment, if it's there at all.\n const end = body.indexOf(SUMMARY_END);\n const first = body.search(new RegExp(MARKER.source));\n if (end >= 0 && (first < 0 || end < first)) {\n summary = body.slice(0, end).trim();\n body = body.slice(end + SUMMARY_END.length).trim();\n }\n const sections: MarkedSection[] = [];\n let start = 0;\n for (const match of body.matchAll(MARKER)) {\n const source = match[1] ?? \"\";\n const text = body.slice(start, match.index);\n start = match.index + match[0].length;\n const { body: comment, quote } = unwrap(text);\n sections.push({\n source,\n build: match[2],\n body: comment,\n quote: quote !== undefined && match[3] !== undefined ? unescapeText(quote) : quote,\n });\n }\n return { summary, sections, rest: body.slice(start).trim() };\n}\n\nfunction unwrap(text: string): { body: string; quote: string | undefined } {\n let rest = text.replace(/\\s*<sub>On \\[[^\\n]*\\]\\([^\\n)]*\\)<\\/sub>\\s*$/, \"\").trim();\n let quote: string | undefined;\n const lines = rest.split(/\\r?\\n/);\n let quoted = 0;\n while (quoted < lines.length && /^>( |$)/.test(lines[quoted] ?? \"\")) quoted++;\n if (quoted > 0 && quoted < lines.length && (lines[quoted] ?? \"\").trim() === \"\") {\n quote = lines\n .slice(0, quoted)\n .map((line) => line.replace(/^> ?/, \"\"))\n .join(\"\\n\");\n rest = lines\n .slice(quoted + 1)\n .join(\"\\n\")\n .trim();\n }\n return { body: rest, quote };\n}\n\n/**\n * Joins a summary and sections into one body: the summary first, so it\n * doesn't read as a remark on the last section.\n */\nexport function joinSections(sections: readonly string[], summary: string | undefined): string {\n const held = sections.filter((part) => part.trim() !== \"\");\n // A marker in the summary would be read as one of Ascribe's.\n const lead = (summary ?? \"\").trim().replace(/(<!--\\s*ascribe):/g, `$1${JOINER}:`);\n if (lead === \"\") return held.join(\"\\n\\n\");\n if (held.length === 0) return lead;\n return [lead, SUMMARY_END, ...held].join(\"\\n\\n\");\n}\n"]}
1
+ {"version":3,"file":"marker.js","sourceRoot":"","sources":["../../src/github/marker.ts"],"names":[],"mappings":"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","sourcesContent":["// Comments GitHub can't anchor to a line go on the pull request's\n// conversation, each ending with a hidden marker naming its block:\n//\n// <!-- ascribe:anchor guides/install.md:12-14 build=site -->\n//\n// Several can share one body (a review's), one after another. A review's\n// summary goes before them, ended by another marker:\n//\n// Looks good, a few notes below.\n//\n// <!-- ascribe:summary -->\n//\n// A comment quotes its block's source, or its text as the page shows it,\n// escaped so GitHub shows it as written; the marker of one that quotes the\n// shown text ends ` quote=text`.\n\n/** One anchored comment, as written in a body. */\nexport interface MarkedSection {\n /** The block's source. */\n source: string;\n build: string | undefined;\n /** The comment, without the quote, the link line, or the marker. */\n body: string;\n /** The block's text the comment quoted, if it did. */\n quote: string | undefined;\n}\n\nconst MARKER = /<!-- ascribe:anchor (\\S+)(?: build=(\\S+))?( quote=text)? -->/g;\n\n/**\n * The summary a pending review starts with. GitHub can't edit the summary of\n * a pending review created without one, so the session's reviews start with\n * this, which reading and submitting remove.\n */\nexport const PLACEHOLDER = \"<!-- ascribe:review -->\";\n\n/** Ends a review's summary when anchored comments follow it. */\nconst SUMMARY_END = \"<!-- ascribe:summary -->\";\n\n/** `body` without the placeholder summary. */\nexport function withoutPlaceholder(body: string): string {\n return body.split(PLACEHOLDER).join(\"\").trim();\n}\n\n/**\n * The marker for a block. Without a build (or with `\"\"`), the comment is on\n * every build's page. `shown` marks a quote of the shown text.\n */\nexport function marker(source: string, build: string | undefined, shown = false): string {\n return `<!-- ascribe:anchor ${source}${build === undefined || build === \"\" ? \"\" : ` build=${build}`}${shown ? \" quote=text\" : \"\"} -->`;\n}\n\n/** Joins a mention or reference sign to what follows, so GitHub doesn't link it. */\nconst JOINER = \"\\u2060\";\n\n/**\n * Plain text as Markdown that GitHub shows as the text: punctuation\n * escaped, `@name` and `#123` not linked, line breaks and leading spaces\n * kept.\n */\nexport function escapeText(text: string): string {\n const lines = text.split(/\\r?\\n/).map((line) => {\n const indent = /^ */.exec(line)?.[0].length ?? 0;\n const rest = line\n .slice(indent)\n .replace(/[!-/:-@[-`{-~]/g, (c) => (c === \"<\" ? \"&lt;\" : `\\\\${c}`))\n .replace(/[@#]/g, `$&${JOINER}`);\n return \"\\u00a0\".repeat(indent) + rest;\n });\n // A backslash at the end of a line breaks it, but not before a blank line.\n return lines\n .map((line, i) => (line !== \"\" && (lines[i + 1] ?? \"\") !== \"\" ? `${line}\\\\` : line))\n .join(\"\\n\");\n}\n\n/** Text that `escapeText` wrote, as it was. */\nexport function unescapeText(text: string): string {\n return text\n .split(/\\r?\\n/)\n .map((line) => {\n const trailing = /\\\\*$/.exec(line)?.[0].length ?? 0;\n const unbroken = trailing % 2 === 1 ? line.slice(0, -1) : line;\n const indent = /^\\u00a0*/.exec(unbroken)?.[0].length ?? 0;\n return (\n \" \".repeat(indent) +\n unbroken\n .slice(indent)\n .replace(new RegExp(`([@#])${JOINER}`, \"g\"), \"$1\")\n .replace(/\\\\([!-/:-@[-`{-~])|&lt;/g, (_, c: string | undefined) => c ?? \"<\")\n );\n })\n .join(\"\\n\");\n}\n\n/**\n * Writes a section: the block's text quoted, the comment, a line linking the\n * block's lines, and the marker.\n */\nexport function formatSection(options: {\n source: string;\n build: string | undefined;\n body: string;\n quote: string | undefined;\n /** Whether `quote` is the block's text as the page shows it, rather than its source. */\n shown?: boolean;\n link: { label: string; url: string } | undefined;\n}): string {\n const parts: string[] = [];\n const shown = options.shown === true;\n if (options.quote !== undefined && options.quote.trim() !== \"\") {\n parts.push(\n (shown ? escapeText(options.quote) : options.quote)\n .split(/\\r?\\n/)\n .map((line) => (line === \"\" ? \">\" : `> ${line}`))\n .join(\"\\n\"),\n );\n }\n parts.push(options.body.trim());\n const end = marker(options.source, options.build, shown);\n parts.push(\n options.link === undefined\n ? end\n : `<sub>On [${options.link.label.replace(/[[\\]]/g, \"\\\\$&\")}](${options.link.url})</sub>\\n${end}`,\n );\n return parts.join(\"\\n\\n\");\n}\n\n/**\n * Splits a body into its summary, its marked sections, and the text after\n * the last marker (`rest`), which isn't anchored either.\n */\nexport function parseSections(text: string): {\n summary: string;\n sections: MarkedSection[];\n rest: string;\n} {\n let body = withoutPlaceholder(text);\n let summary = \"\";\n // The summary ends before the first comment, if it's there at all.\n const end = body.indexOf(SUMMARY_END);\n const first = body.search(new RegExp(MARKER.source));\n if (end >= 0 && (first < 0 || end < first)) {\n summary = body.slice(0, end).trim();\n body = body.slice(end + SUMMARY_END.length).trim();\n }\n const sections: MarkedSection[] = [];\n let start = 0;\n for (const match of body.matchAll(MARKER)) {\n const source = match[1] ?? \"\";\n const text = body.slice(start, match.index);\n start = match.index + match[0].length;\n const { body: comment, quote } = unwrap(text);\n sections.push({\n source,\n build: match[2],\n body: comment,\n quote: quote !== undefined && match[3] !== undefined ? unescapeText(quote) : quote,\n });\n }\n return { summary, sections, rest: body.slice(start).trim() };\n}\n\nfunction unwrap(text: string): { body: string; quote: string | undefined } {\n let rest = text.replace(/\\s*<sub>On \\[[^\\n]*\\]\\([^\\n)]*\\)<\\/sub>\\s*$/, \"\").trim();\n let quote: string | undefined;\n const lines = rest.split(/\\r?\\n/);\n let quoted = 0;\n while (quoted < lines.length && /^>( |$)/.test(lines[quoted] ?? \"\")) quoted++;\n if (quoted > 0 && quoted < lines.length && (lines[quoted] ?? \"\").trim() === \"\") {\n quote = lines\n .slice(0, quoted)\n .map((line) => line.replace(/^> ?/, \"\"))\n .join(\"\\n\");\n rest = lines\n .slice(quoted + 1)\n .join(\"\\n\")\n .trim();\n }\n return { body: rest, quote };\n}\n\n/**\n * Joins a summary and sections into one body: the summary first, so it\n * doesn't read as a remark on the last section.\n */\nexport function joinSections(sections: readonly string[], summary: string | undefined): string {\n const held = sections.filter((part) => part.trim() !== \"\");\n // A marker in the summary would be read as one of Ascribe's.\n const lead = (summary ?? \"\").trim().replace(/(<!--\\s*ascribe):/g, `$1${JOINER}:`);\n if (lead === \"\") return held.join(\"\\n\\n\");\n if (held.length === 0) return lead;\n return [lead, SUMMARY_END, ...held].join(\"\\n\\n\");\n}\n"]}
@@ -1,27 +1,6 @@
1
- /** Where a block is written: the anchor grammar's `source` and `via`. */
2
- export interface Anchor {
3
- source: string;
4
- via: string[];
5
- }
6
- /** The words that differ inside a changed block of prose. */
7
- export interface Words {
8
- /** `[start, end)` ranges, in characters, of `now_text`. */
9
- now: [number, number][];
10
- /** `[start, end)` ranges, in characters, of `was_text`. */
11
- was: [number, number][];
12
- now_text: string;
13
- was_text: string;
14
- }
15
- /** One block's change, as `ascribe diff --format json` writes it. */
16
- export interface Change {
17
- kind: "changed" | "added" | "removed" | "moved";
18
- now?: Anchor;
19
- was?: Anchor;
20
- words?: Words;
21
- after?: Anchor;
22
- parent?: Anchor;
23
- text?: string;
24
- }
1
+ import type { Anchor, Change } from "../shapes.js";
2
+ /** A block's change, where it's written, and its words, as `ascribe diff --format json` writes them. */
3
+ export type { Anchor, Change, Words } from "../shapes.js";
25
4
  /** What the page shows: its changes, the page as it will be, or as it was. */
26
5
  export type Show = "changes" | "will" | "was";
27
6
  export interface MarkOptions {
@@ -1,17 +1,4 @@
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";
15
2
  const LABELS = {
16
3
  added: "Added",
17
4
  changed: "Changed",
@@ -34,7 +21,7 @@ const LABEL_INSIDE = new Set([
34
21
  "td",
35
22
  "th",
36
23
  // A tab's panel: before it would put the label among the tab list's panels.
37
- "ascribe-tab",
24
+ ELEMENT_TAB,
38
25
  ]);
39
26
  /** Parses `<path>:<first>-<last>`, splitting at the last `:`. */
40
27
  export function parseSource(source) {
@@ -78,7 +65,7 @@ export function describeSource(source, via) {
78
65
  return text;
79
66
  }
80
67
  function viaOf(element) {
81
- return element.getAttribute("data-ascribe-via") ?? "";
68
+ return element.getAttribute(DATA_VIA) ?? "";
82
69
  }
83
70
  /**
84
71
  * The element of a block: the one with exactly its anchor, or, when there's
@@ -88,8 +75,8 @@ function viaOf(element) {
88
75
  */
89
76
  export function findBlock(root, anchor) {
90
77
  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);
78
+ const anchored = Array.from(root.querySelectorAll(`[${DATA_SOURCE}]`));
79
+ const exact = anchored.find((el) => el.getAttribute(DATA_SOURCE) === anchor.source && viaOf(el) === via);
93
80
  if (exact)
94
81
  return exact;
95
82
  const want = parseSource(anchor.source);
@@ -100,7 +87,7 @@ export function findBlock(root, anchor) {
100
87
  for (const el of anchored) {
101
88
  if (viaOf(el) !== via)
102
89
  continue;
103
- const has = parseSource(el.getAttribute("data-ascribe-source") ?? "");
90
+ const has = parseSource(el.getAttribute(DATA_SOURCE) ?? "");
104
91
  if (!has || has.path !== want.path)
105
92
  continue;
106
93
  if (has.first > want.first || has.last < want.last)
@@ -116,7 +103,7 @@ export function findBlock(root, anchor) {
116
103
  function ui(doc, tag, className, text) {
117
104
  const el = doc.createElement(tag);
118
105
  el.className = className;
119
- el.setAttribute("data-ascribe-ui", "");
106
+ el.setAttribute(DATA_UI, "");
120
107
  if (text !== undefined)
121
108
  el.textContent = text;
122
109
  return el;
@@ -124,13 +111,13 @@ function ui(doc, tag, className, text) {
124
111
  /** A label, inside the block at its start, or just before it. */
125
112
  function addLabel(element, kind) {
126
113
  const doc = element.ownerDocument;
127
- const label = ui(doc, "span", "ascribe-label", LABELS[kind]);
128
- label.setAttribute("data-ascribe-label", kind);
114
+ const label = ui(doc, "span", CLASS_LABEL, LABELS[kind]);
115
+ label.setAttribute(DATA_LABEL, kind);
129
116
  if (LABEL_INSIDE.has(element.localName)) {
130
117
  element.prepend(label);
131
118
  }
132
119
  else {
133
- label.classList.add("ascribe-label-before");
120
+ label.classList.add(CLASS_LABEL_BEFORE);
134
121
  element.before(label);
135
122
  }
136
123
  }
@@ -148,8 +135,8 @@ function contentOf(holder, before) {
148
135
  function copyOf(element) {
149
136
  const copy = element.cloneNode(true);
150
137
  for (const el of [copy, ...Array.from(copy.querySelectorAll("*"))]) {
151
- el.removeAttribute("data-ascribe-source");
152
- el.removeAttribute("data-ascribe-via");
138
+ el.removeAttribute(DATA_SOURCE);
139
+ el.removeAttribute(DATA_VIA);
153
140
  el.removeAttribute("id");
154
141
  }
155
142
  return copy;
@@ -181,7 +168,7 @@ function ownText(element) {
181
168
  for (const child of Array.from(node.childNodes)) {
182
169
  if (child.nodeType === Node.TEXT_NODE)
183
170
  text += child.nodeValue ?? "";
184
- else if (child instanceof Element && !child.hasAttribute("data-ascribe-ui"))
171
+ else if (child instanceof Element && !child.hasAttribute(DATA_UI))
185
172
  walk(child);
186
173
  }
187
174
  };
@@ -205,9 +192,7 @@ class Placer {
205
192
  let previous = this.#last.get(after) ?? after;
206
193
  // Past what the marks put after the block itself: its copy as it
207
194
  // 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) {
195
+ for (let next = previous.nextElementSibling; next && (next.hasAttribute(DATA_WAS_ONLY) || next.classList.contains(CLASS_MOVE_AFTER)); next = next.nextElementSibling) {
211
196
  previous = next;
212
197
  }
213
198
  const el = make(after.parentElement);
@@ -226,7 +211,7 @@ class Placer {
226
211
  else {
227
212
  // First among the container's blocks: before its first anchored
228
213
  // 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"));
214
+ const first = Array.from(container.children).find((c) => c.hasAttribute(DATA_SOURCE) || c.hasAttribute(DATA_UI));
230
215
  if (first)
231
216
  first.before(el);
232
217
  else
@@ -270,8 +255,8 @@ function textMap(element) {
270
255
  }
271
256
  }
272
257
  else if (child instanceof Element &&
273
- !child.hasAttribute("data-ascribe-ui") &&
274
- !child.hasAttribute("data-ascribe-source")) {
258
+ !child.hasAttribute(DATA_UI) &&
259
+ !child.hasAttribute(DATA_SOURCE)) {
275
260
  walk(child);
276
261
  }
277
262
  }
@@ -342,10 +327,10 @@ function markWords(element, words) {
342
327
  edits.sort((a, b) => b.at - a.at || (a.kind === "del" ? -1 : 1));
343
328
  for (const edit of edits) {
344
329
  if (edit.kind === "ins") {
345
- wrapRange(map, offset + edit.at, offset + edit.end, () => ui(doc, "ins", "ascribe-ins"));
330
+ wrapRange(map, offset + edit.at, offset + edit.end, () => ui(doc, "ins", CLASS_INS));
346
331
  }
347
332
  else {
348
- const del = ui(doc, "del", "ascribe-del", edit.text);
333
+ const del = ui(doc, "del", CLASS_DEL, edit.text);
349
334
  insertAt(map, offset + edit.at, del, element);
350
335
  }
351
336
  }
@@ -407,33 +392,33 @@ export function markChanges(root, changes, options = {}) {
407
392
  const was = options.was ?? null;
408
393
  const placer = new Placer(root);
409
394
  const marks = [];
410
- root.classList.add("ascribe-marks");
411
- if (!root.hasAttribute("data-ascribe-show"))
412
- root.setAttribute("data-ascribe-show", "changes");
395
+ root.classList.add(CLASS_MARKS);
396
+ if (!root.hasAttribute(DATA_SHOW))
397
+ root.setAttribute(DATA_SHOW, "changes");
413
398
  let moveCount = 0;
414
399
  for (const change of changes) {
415
400
  switch (change.kind) {
416
401
  case "added":
417
402
  case "changed": {
418
403
  const element = change.now ? findBlock(root, change.now) : null;
419
- if (!element || element.hasAttribute("data-ascribe-change"))
404
+ if (!element || element.hasAttribute(DATA_CHANGE))
420
405
  break;
421
- element.setAttribute("data-ascribe-change", change.kind);
406
+ element.setAttribute(DATA_CHANGE, change.kind);
422
407
  addLabel(element, change.kind);
423
408
  if (change.kind === "changed") {
424
409
  // Where it was, for threads on its old text.
425
410
  if (change.was)
426
- element.setAttribute("data-ascribe-was-source", change.was.source);
411
+ element.setAttribute(DATA_WAS_SOURCE, change.was.source);
427
412
  const before = was && change.was ? findBlock(was, change.was) : null;
428
413
  if (change.words)
429
414
  markWords(element, change.words);
430
415
  if (before) {
431
416
  // As it was, the block as the old page rendered it.
432
417
  const copy = copyOf(before);
433
- copy.setAttribute("data-ascribe-ui", "");
434
- copy.setAttribute("data-ascribe-was-only", "");
418
+ copy.setAttribute(DATA_UI, "");
419
+ copy.setAttribute(DATA_WAS_ONLY, "");
435
420
  element.after(copy);
436
- element.setAttribute("data-ascribe-has-was", "");
421
+ element.setAttribute(DATA_HAS_WAS, "");
437
422
  }
438
423
  }
439
424
  marks.push({ change, element });
@@ -442,23 +427,23 @@ export function markChanges(root, changes, options = {}) {
442
427
  case "removed": {
443
428
  const before = was && change.was ? findBlock(was, change.was) : null;
444
429
  const element = placer.place(change, (container) => {
445
- const holder = ui(doc, holderFor(container), "ascribe-removed");
446
- holder.setAttribute("data-ascribe-change", "removed");
430
+ const holder = ui(doc, holderFor(container), CLASS_REMOVED);
431
+ holder.setAttribute(DATA_CHANGE, "removed");
447
432
  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");
433
+ holder.setAttribute(DATA_WAS_SOURCE, change.was.source);
434
+ const label = ui(doc, "span", CLASS_LABEL, LABELS.removed);
435
+ label.setAttribute(DATA_LABEL, "removed");
436
+ const body = ui(doc, "div", CLASS_REMOVED_BODY);
452
437
  if (before)
453
438
  body.append(...contentOf(holder, before));
454
439
  else
455
440
  body.textContent = change.text ?? "";
456
- const toggle = ui(doc, "button", "ascribe-toggle", "Show");
441
+ const toggle = ui(doc, "button", CLASS_TOGGLE, "Show");
457
442
  toggle.type = "button";
458
443
  toggle.setAttribute("aria-expanded", "false");
459
444
  toggle.addEventListener("click", (event) => {
460
445
  event.stopPropagation();
461
- const open = holder.classList.toggle("ascribe-open");
446
+ const open = holder.classList.toggle(CLASS_OPEN);
462
447
  toggle.textContent = open ? "Collapse" : "Show";
463
448
  toggle.setAttribute("aria-expanded", String(open));
464
449
  });
@@ -470,25 +455,25 @@ export function markChanges(root, changes, options = {}) {
470
455
  }
471
456
  case "moved": {
472
457
  const element = change.now ? findBlock(root, change.now) : null;
473
- if (!element || element.hasAttribute("data-ascribe-change"))
458
+ if (!element || element.hasAttribute(DATA_CHANGE))
474
459
  break;
475
460
  const id = `ascribe-move-${++moveCount}`;
476
461
  const before = was && change.was ? findBlock(was, change.was) : null;
477
462
  const stub = placer.place(change, (container) => {
478
- const holder = ui(doc, holderFor(container), "ascribe-moved-from");
463
+ const holder = ui(doc, holderFor(container), CLASS_MOVED_FROM);
479
464
  holder.id = `${id}-from`;
480
- holder.setAttribute("data-ascribe-change", "moved-from");
465
+ holder.setAttribute(DATA_CHANGE, "moved-from");
481
466
  if (change.was)
482
- holder.setAttribute("data-ascribe-was-source", change.was.source);
483
- const note = ui(doc, "span", "ascribe-move-note");
467
+ holder.setAttribute(DATA_WAS_SOURCE, change.was.source);
468
+ const note = ui(doc, "span", CLASS_MOVE_NOTE);
484
469
  note.append(doc.createTextNode(`A ${blockWord(element)} moved from here `));
485
- const link = ui(doc, "a", "ascribe-move-link");
470
+ const link = ui(doc, "a", CLASS_MOVE_LINK);
486
471
  link.href = `#${id}`;
487
472
  note.append(link);
488
473
  holder.append(note);
489
474
  // As it was, the block itself.
490
- const copy = ui(doc, "div", "ascribe-was-copy");
491
- copy.setAttribute("data-ascribe-was-only", "");
475
+ const copy = ui(doc, "div", CLASS_WAS_COPY);
476
+ copy.setAttribute(DATA_WAS_ONLY, "");
492
477
  if (before)
493
478
  copy.append(...contentOf(holder, before));
494
479
  else
@@ -496,26 +481,26 @@ export function markChanges(root, changes, options = {}) {
496
481
  holder.append(copy);
497
482
  return holder;
498
483
  });
499
- element.setAttribute("data-ascribe-change", "moved");
500
- element.setAttribute("data-ascribe-move", id);
484
+ element.setAttribute(DATA_CHANGE, "moved");
485
+ element.setAttribute(DATA_MOVE, id);
501
486
  if (!element.id)
502
487
  element.id = id;
503
488
  addLabel(element, "moved");
504
- const back = ui(doc, "a", "ascribe-move-link");
489
+ const back = ui(doc, "a", CLASS_MOVE_LINK);
505
490
  back.href = `#${stub.id}`;
506
491
  back.textContent = `from “${headingBefore(root, stub) ?? "the top of the page"}” ↑`;
507
- const forward = stub.querySelector(".ascribe-move-link");
492
+ const forward = stub.querySelector(`.${CLASS_MOVE_LINK}`);
508
493
  if (forward) {
509
494
  forward.href = `#${element.id}`;
510
495
  forward.textContent = `to “${headingBefore(root, element) ?? "the top of the page"}” ↓`;
511
496
  }
512
497
  if (LABEL_INSIDE.has(element.localName)) {
513
- const note = ui(doc, "span", "ascribe-move-note");
498
+ const note = ui(doc, "span", CLASS_MOVE_NOTE);
514
499
  note.append(" ", back);
515
500
  element.append(note);
516
501
  }
517
502
  else {
518
- const holder = ui(doc, "div", "ascribe-move-note ascribe-move-after");
503
+ const holder = ui(doc, "div", `${CLASS_MOVE_NOTE} ${CLASS_MOVE_AFTER}`);
519
504
  holder.append(back);
520
505
  element.after(holder);
521
506
  }
@@ -552,7 +537,7 @@ function hintHidden(root, marks) {
552
537
  const summaries = new Map();
553
538
  for (const { change, element } of marks) {
554
539
  for (let el = element; el && el !== root; el = el.parentElement) {
555
- if (el.localName === "ascribe-tab") {
540
+ if (el.localName === ELEMENT_TAB) {
556
541
  const tab = tabs.get(el) ?? { added: false, count: 0 };
557
542
  if (el === element && change.kind === "added")
558
543
  tab.added = true;
@@ -570,8 +555,8 @@ function hintHidden(root, marks) {
570
555
  const hint = (kind, text) => {
571
556
  // A space before it, not only a margin, so the label reads "Linux 1
572
557
  // change" to a screen reader, not "Linux1 change".
573
- const el = ui(doc, "span", "ascribe-hint", ` ${text}`);
574
- el.setAttribute("data-ascribe-hint", kind);
558
+ const el = ui(doc, "span", CLASS_HINT, ` ${text}`);
559
+ el.setAttribute(DATA_HINT, kind);
575
560
  return el;
576
561
  };
577
562
  const watching = [];
@@ -615,9 +600,9 @@ function summaryOf(element) {
615
600
  */
616
601
  export function tabButton(tab) {
617
602
  const group = tab.parentElement;
618
- if (tab.localName !== "ascribe-tab" || !group)
603
+ if (tab.localName !== ELEMENT_TAB || !group)
619
604
  return null;
620
- const tabs = Array.from(group.children).filter((c) => c.localName === "ascribe-tab");
605
+ const tabs = Array.from(group.children).filter((c) => c.localName === ELEMENT_TAB);
621
606
  const buttons = group.querySelectorAll(':scope > [role="tablist"] > [role="tab"]');
622
607
  return buttons[tabs.indexOf(tab)] ?? null;
623
608
  }
@@ -643,27 +628,27 @@ export function clearMarks(root) {
643
628
  for (const observer of tabWatchers.get(root) ?? [])
644
629
  observer.disconnect();
645
630
  tabWatchers.delete(root);
646
- for (const el of Array.from(root.querySelectorAll("[data-ascribe-ui]"))) {
631
+ for (const el of Array.from(root.querySelectorAll(`[${DATA_UI}]`))) {
647
632
  if (el.localName === "ins")
648
633
  el.replaceWith(...Array.from(el.childNodes));
649
634
  else
650
635
  el.remove();
651
636
  }
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"))
637
+ for (const el of Array.from(root.querySelectorAll(`[${DATA_CHANGE}]`))) {
638
+ el.removeAttribute(DATA_CHANGE);
639
+ el.removeAttribute(DATA_HAS_WAS);
640
+ el.removeAttribute(DATA_WAS_SOURCE);
641
+ if (el.hasAttribute(DATA_MOVE)) {
642
+ if (el.id === el.getAttribute(DATA_MOVE))
658
643
  el.removeAttribute("id");
659
- el.removeAttribute("data-ascribe-move");
644
+ el.removeAttribute(DATA_MOVE);
660
645
  }
661
646
  }
662
647
  root.normalize();
663
648
  }
664
649
  /** Shows the changes, the page as it will be, or the page as it was. */
665
650
  export function setShow(root, show) {
666
- root.setAttribute("data-ascribe-show", show);
651
+ root.setAttribute(DATA_SHOW, show);
667
652
  }
668
653
  /**
669
654
  * Makes `element` visible: opens the `<details>` and selects the tabs it's
@@ -673,7 +658,7 @@ export function reveal(element) {
673
658
  for (let el = element; el; el = el.parentElement) {
674
659
  if (el instanceof HTMLDetailsElement && !el.open)
675
660
  el.open = true;
676
- if (el.localName === "ascribe-tab" && el.hidden)
661
+ if (el.localName === ELEMENT_TAB && el.hidden)
677
662
  tabButton(el)?.click();
678
663
  }
679
664
  }
@@ -684,10 +669,10 @@ export function goTo(element) {
684
669
  if (typeof element.scrollIntoView === "function") {
685
670
  element.scrollIntoView({ block: "center", behavior: "smooth" });
686
671
  }
687
- element.classList.remove("ascribe-flash");
672
+ element.classList.remove(CLASS_FLASH);
688
673
  // Restart the animation.
689
674
  void element.offsetWidth;
690
- element.classList.add("ascribe-flash");
675
+ element.classList.add(CLASS_FLASH);
691
676
  if (!element.hasAttribute("tabindex"))
692
677
  element.setAttribute("tabindex", "-1");
693
678
  element.focus({ preventScroll: true });
@@ -698,13 +683,11 @@ export function goTo(element) {
698
683
  */
699
684
  export function showSources(root) {
700
685
  const doc = root.ownerDocument;
701
- const tip = ui(doc, "span", "ascribe-where");
686
+ const tip = ui(doc, "span", CLASS_WHERE);
702
687
  tip.setAttribute("role", "status");
703
688
  let current = null;
704
689
  const show = (target) => {
705
- const el = target instanceof Element
706
- ? target.closest("[data-ascribe-source], [data-ascribe-was-source]")
707
- : null;
690
+ const el = target instanceof Element ? target.closest(`[${DATA_SOURCE}], [${DATA_WAS_SOURCE}]`) : null;
708
691
  if (el === current)
709
692
  return;
710
693
  current = el;
@@ -713,10 +696,10 @@ export function showSources(root) {
713
696
  current = null;
714
697
  return;
715
698
  }
716
- const source = el.getAttribute("data-ascribe-source");
699
+ const source = el.getAttribute(DATA_SOURCE);
717
700
  tip.textContent = source
718
- ? describeSource(source, el.getAttribute("data-ascribe-via"))
719
- : `was ${describeSource(el.getAttribute("data-ascribe-was-source") ?? "")}`;
701
+ ? describeSource(source, el.getAttribute(DATA_VIA))
702
+ : `was ${describeSource(el.getAttribute(DATA_WAS_SOURCE) ?? "")}`;
720
703
  const box = el.getBoundingClientRect();
721
704
  const host = root.getBoundingClientRect();
722
705
  tip.style.top = `${box.top - host.top}px`;