@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 +1 -1
- package/dist/github/marker.d.ts +0 -2
- package/dist/github/marker.js +1 -1
- package/dist/github/marker.js.map +1 -1
- package/dist/marks/index.d.ts +3 -24
- package/dist/marks/index.js +72 -89
- package/dist/marks/index.js.map +1 -1
- package/dist/names.d.ts +95 -0
- package/dist/names.js +99 -0
- package/dist/names.js.map +1 -0
- package/dist/overlay/overlay.js +17 -16
- package/dist/overlay/overlay.js.map +1 -1
- package/dist/overlay/text.d.ts +0 -2
- package/dist/overlay/text.js +4 -3
- package/dist/overlay/text.js.map +1 -1
- package/dist/place/anchor.d.ts +3 -7
- package/dist/place/anchor.js.map +1 -1
- package/dist/shapes.d.ts +253 -0
- package/dist/shapes.js +9 -0
- package/dist/shapes.js.map +1 -0
- package/package.json +3 -3
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/
|
|
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
|
|
package/dist/github/marker.d.ts
CHANGED
|
@@ -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
|
/**
|
package/dist/github/marker.js
CHANGED
|
@@ -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
|
-
|
|
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 === \"<\" ? \"<\" : `\\\\${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(/\\\\([!-/:-@[-`{-~])|</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 === \"<\" ? \"<\" : `\\\\${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(/\\\\([!-/:-@[-`{-~])|</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"]}
|
package/dist/marks/index.d.ts
CHANGED
|
@@ -1,27 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
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 {
|
package/dist/marks/index.js
CHANGED
|
@@ -1,17 +1,4 @@
|
|
|
1
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
92
|
-
const exact = anchored.find((el) => el.getAttribute(
|
|
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(
|
|
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(
|
|
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",
|
|
128
|
-
label.setAttribute(
|
|
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(
|
|
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(
|
|
152
|
-
el.removeAttribute(
|
|
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(
|
|
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(
|
|
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(
|
|
274
|
-
!child.hasAttribute(
|
|
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",
|
|
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",
|
|
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(
|
|
411
|
-
if (!root.hasAttribute(
|
|
412
|
-
root.setAttribute(
|
|
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(
|
|
404
|
+
if (!element || element.hasAttribute(DATA_CHANGE))
|
|
420
405
|
break;
|
|
421
|
-
element.setAttribute(
|
|
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(
|
|
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(
|
|
434
|
-
copy.setAttribute(
|
|
418
|
+
copy.setAttribute(DATA_UI, "");
|
|
419
|
+
copy.setAttribute(DATA_WAS_ONLY, "");
|
|
435
420
|
element.after(copy);
|
|
436
|
-
element.setAttribute(
|
|
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),
|
|
446
|
-
holder.setAttribute(
|
|
430
|
+
const holder = ui(doc, holderFor(container), CLASS_REMOVED);
|
|
431
|
+
holder.setAttribute(DATA_CHANGE, "removed");
|
|
447
432
|
if (change.was)
|
|
448
|
-
holder.setAttribute(
|
|
449
|
-
const label = ui(doc, "span",
|
|
450
|
-
label.setAttribute(
|
|
451
|
-
const body = ui(doc, "div",
|
|
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",
|
|
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(
|
|
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(
|
|
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),
|
|
463
|
+
const holder = ui(doc, holderFor(container), CLASS_MOVED_FROM);
|
|
479
464
|
holder.id = `${id}-from`;
|
|
480
|
-
holder.setAttribute(
|
|
465
|
+
holder.setAttribute(DATA_CHANGE, "moved-from");
|
|
481
466
|
if (change.was)
|
|
482
|
-
holder.setAttribute(
|
|
483
|
-
const note = ui(doc, "span",
|
|
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",
|
|
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",
|
|
491
|
-
copy.setAttribute(
|
|
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(
|
|
500
|
-
element.setAttribute(
|
|
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",
|
|
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(
|
|
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",
|
|
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",
|
|
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 ===
|
|
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",
|
|
574
|
-
el.setAttribute(
|
|
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 !==
|
|
603
|
+
if (tab.localName !== ELEMENT_TAB || !group)
|
|
619
604
|
return null;
|
|
620
|
-
const tabs = Array.from(group.children).filter((c) => c.localName ===
|
|
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(
|
|
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(
|
|
653
|
-
el.removeAttribute(
|
|
654
|
-
el.removeAttribute(
|
|
655
|
-
el.removeAttribute(
|
|
656
|
-
if (el.hasAttribute(
|
|
657
|
-
if (el.id === el.getAttribute(
|
|
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(
|
|
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(
|
|
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 ===
|
|
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(
|
|
672
|
+
element.classList.remove(CLASS_FLASH);
|
|
688
673
|
// Restart the animation.
|
|
689
674
|
void element.offsetWidth;
|
|
690
|
-
element.classList.add(
|
|
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",
|
|
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(
|
|
699
|
+
const source = el.getAttribute(DATA_SOURCE);
|
|
717
700
|
tip.textContent = source
|
|
718
|
-
? describeSource(source, el.getAttribute(
|
|
719
|
-
: `was ${describeSource(el.getAttribute(
|
|
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`;
|