doc-detective 4.38.0 → 4.41.4
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 +4 -4
- package/dist/agents/adapters/claude-code.d.ts.map +1 -1
- package/dist/agents/adapters/claude-code.js +1 -2
- package/dist/agents/adapters/claude-code.js.map +1 -1
- package/dist/agents/adapters/codex.d.ts.map +1 -1
- package/dist/agents/adapters/codex.js +3 -4
- package/dist/agents/adapters/codex.js.map +1 -1
- package/dist/agents/adapters/copilot-cli.d.ts.map +1 -1
- package/dist/agents/adapters/copilot-cli.js +1 -2
- package/dist/agents/adapters/copilot-cli.js.map +1 -1
- package/dist/agents/adapters/gemini-cli.d.ts.map +1 -1
- package/dist/agents/adapters/gemini-cli.js +1 -2
- package/dist/agents/adapters/gemini-cli.js.map +1 -1
- package/dist/agents/adapters/opencode.d.ts.map +1 -1
- package/dist/agents/adapters/opencode.js +1 -2
- package/dist/agents/adapters/opencode.js.map +1 -1
- package/dist/agents/adapters/qwen-code.d.ts.map +1 -1
- package/dist/agents/adapters/qwen-code.js +1 -2
- package/dist/agents/adapters/qwen-code.js.map +1 -1
- package/dist/agents/fetcher.d.ts.map +1 -1
- package/dist/agents/fetcher.js +1 -2
- package/dist/agents/fetcher.js.map +1 -1
- package/dist/common/src/ajv.d.ts +30 -0
- package/dist/common/src/ajv.d.ts.map +1 -0
- package/dist/common/src/ajv.js +131 -0
- package/dist/common/src/ajv.js.map +1 -0
- package/dist/common/src/detectTests.d.ts +2 -18
- package/dist/common/src/detectTests.d.ts.map +1 -1
- package/dist/common/src/detectTests.js +8 -77
- package/dist/common/src/detectTests.js.map +1 -1
- package/dist/common/src/schemas/referenced.d.ts +12 -0
- package/dist/common/src/schemas/referenced.d.ts.map +1 -0
- package/dist/common/src/schemas/referenced.js +10 -0
- package/dist/common/src/schemas/referenced.js.map +1 -0
- package/dist/common/src/schemas/schemas.json +2 -2
- package/dist/common/src/schemas/schemasReferenced.json +14871 -0
- package/dist/common/src/schemas/validators.cjs +509 -0
- package/dist/common/src/schemas/validatorsCheck.cjs +2 -0
- package/dist/common/src/textUtils.d.ts +25 -0
- package/dist/common/src/textUtils.d.ts.map +1 -0
- package/dist/common/src/textUtils.js +84 -0
- package/dist/common/src/textUtils.js.map +1 -0
- package/dist/common/src/types/generated/annotate_v3.d.ts.map +1 -1
- package/dist/common/src/types/generated/annotation_v3.d.ts.map +1 -1
- package/dist/common/src/types/generated/click_v3.d.ts +153 -0
- package/dist/common/src/types/generated/click_v3.d.ts.map +1 -1
- package/dist/common/src/types/generated/config_v3.d.ts +13753 -207
- package/dist/common/src/types/generated/config_v3.d.ts.map +1 -1
- package/dist/common/src/types/generated/dragAndDrop_v3.d.ts +72 -0
- package/dist/common/src/types/generated/dragAndDrop_v3.d.ts.map +1 -1
- package/dist/common/src/types/generated/find_v3.d.ts +632 -0
- package/dist/common/src/types/generated/find_v3.d.ts.map +1 -1
- package/dist/common/src/types/generated/goTo_v3.d.ts +31 -0
- package/dist/common/src/types/generated/goTo_v3.d.ts.map +1 -1
- package/dist/common/src/types/generated/httpRequest_v3.d.ts +181 -0
- package/dist/common/src/types/generated/httpRequest_v3.d.ts.map +1 -1
- package/dist/common/src/types/generated/loadCookie_v3.d.ts +30 -0
- package/dist/common/src/types/generated/loadCookie_v3.d.ts.map +1 -1
- package/dist/common/src/types/generated/report_v3.d.ts +28234 -321
- package/dist/common/src/types/generated/report_v3.d.ts.map +1 -1
- package/dist/common/src/types/generated/resolvedTests_v3.d.ts +42281 -822
- package/dist/common/src/types/generated/resolvedTests_v3.d.ts.map +1 -1
- package/dist/common/src/types/generated/saveCookie_v3.d.ts +35 -0
- package/dist/common/src/types/generated/saveCookie_v3.d.ts.map +1 -1
- package/dist/common/src/types/generated/screenshot_v3.d.ts +45 -0
- package/dist/common/src/types/generated/screenshot_v3.d.ts.map +1 -1
- package/dist/common/src/types/generated/spec_v3.d.ts +28188 -275
- package/dist/common/src/types/generated/spec_v3.d.ts.map +1 -1
- package/dist/common/src/types/generated/startSurface_v3.d.ts +63 -1
- package/dist/common/src/types/generated/startSurface_v3.d.ts.map +1 -1
- package/dist/common/src/types/generated/step_v3.d.ts +5186 -523
- package/dist/common/src/types/generated/step_v3.d.ts.map +1 -1
- package/dist/common/src/types/generated/test_v3.d.ts +13268 -3582
- package/dist/common/src/types/generated/test_v3.d.ts.map +1 -1
- package/dist/common/src/types/generated/type_v3.d.ts +62 -0
- package/dist/common/src/types/generated/type_v3.d.ts.map +1 -1
- package/dist/common/src/types/generated/waitUntil_v3.d.ts +62 -0
- package/dist/common/src/types/generated/waitUntil_v3.d.ts.map +1 -1
- package/dist/common/src/validate.d.ts +1 -1
- package/dist/common/src/validate.d.ts.map +1 -1
- package/dist/common/src/validate.js +29 -75
- package/dist/common/src/validate.js.map +1 -1
- package/dist/core/appium.d.ts +3 -2
- package/dist/core/appium.d.ts.map +1 -1
- package/dist/core/appium.js +63 -89
- package/dist/core/appium.js.map +1 -1
- package/dist/core/appiumReuse.d.ts +43 -0
- package/dist/core/appiumReuse.d.ts.map +1 -0
- package/dist/core/appiumReuse.js +226 -0
- package/dist/core/appiumReuse.js.map +1 -0
- package/dist/core/config.d.ts +10 -2
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +37 -7
- package/dist/core/config.js.map +1 -1
- package/dist/core/detectTests.d.ts.map +1 -1
- package/dist/core/detectTests.js +51 -17
- package/dist/core/detectTests.js.map +1 -1
- package/dist/core/files.d.ts.map +1 -1
- package/dist/core/files.js +3 -4
- package/dist/core/files.js.map +1 -1
- package/dist/core/index.d.ts +2 -2
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +132 -21
- package/dist/core/index.js.map +1 -1
- package/dist/core/integrations/heretto.d.ts +1 -1
- package/dist/core/integrations/heretto.d.ts.map +1 -1
- package/dist/core/integrations/heretto.js +7 -6
- package/dist/core/integrations/heretto.js.map +1 -1
- package/dist/core/lazyYaml.d.ts +2 -0
- package/dist/core/lazyYaml.d.ts.map +1 -0
- package/dist/core/lazyYaml.js +13 -0
- package/dist/core/lazyYaml.js.map +1 -0
- package/dist/core/resolveTests.js +1 -1
- package/dist/core/resolveTests.js.map +1 -1
- package/dist/core/telem.d.ts +1 -1
- package/dist/core/telem.d.ts.map +1 -1
- package/dist/core/telem.js +2 -11
- package/dist/core/telem.js.map +1 -1
- package/dist/core/telemLite.d.ts +10 -0
- package/dist/core/telemLite.d.ts.map +1 -0
- package/dist/core/telemLite.js +101 -0
- package/dist/core/telemLite.js.map +1 -0
- package/dist/core/tests/androidEmulator.d.ts +67 -4
- package/dist/core/tests/androidEmulator.d.ts.map +1 -1
- package/dist/core/tests/androidEmulator.js +366 -37
- package/dist/core/tests/androidEmulator.js.map +1 -1
- package/dist/core/tests/appSurface.d.ts +25 -6
- package/dist/core/tests/appSurface.d.ts.map +1 -1
- package/dist/core/tests/appSurface.js +88 -52
- package/dist/core/tests/appSurface.js.map +1 -1
- package/dist/core/tests/browserSessions.d.ts +2 -1
- package/dist/core/tests/browserSessions.d.ts.map +1 -1
- package/dist/core/tests/browserSessions.js +7 -1
- package/dist/core/tests/browserSessions.js.map +1 -1
- package/dist/core/tests/checkLink.d.ts +1 -0
- package/dist/core/tests/checkLink.d.ts.map +1 -1
- package/dist/core/tests/checkLink.js +14 -5
- package/dist/core/tests/checkLink.js.map +1 -1
- package/dist/core/tests/closeSurface.d.ts.map +1 -1
- package/dist/core/tests/closeSurface.js +4 -3
- package/dist/core/tests/closeSurface.js.map +1 -1
- package/dist/core/tests/findStrategies.d.ts.map +1 -1
- package/dist/core/tests/findStrategies.js +153 -116
- package/dist/core/tests/findStrategies.js.map +1 -1
- package/dist/core/tests/geckoDriver.d.ts +16 -0
- package/dist/core/tests/geckoDriver.d.ts.map +1 -0
- package/dist/core/tests/geckoDriver.js +127 -0
- package/dist/core/tests/geckoDriver.js.map +1 -0
- package/dist/core/tests/goTo.d.ts.map +1 -1
- package/dist/core/tests/goTo.js +12 -33
- package/dist/core/tests/goTo.js.map +1 -1
- package/dist/core/tests/httpRequest.d.ts.map +1 -1
- package/dist/core/tests/httpRequest.js +4 -3
- package/dist/core/tests/httpRequest.js.map +1 -1
- package/dist/core/tests/iosSimulator.d.ts +4 -1
- package/dist/core/tests/iosSimulator.d.ts.map +1 -1
- package/dist/core/tests/iosSimulator.js +49 -1
- package/dist/core/tests/iosSimulator.js.map +1 -1
- package/dist/core/tests/mobileBrowser.d.ts +2 -1
- package/dist/core/tests/mobileBrowser.d.ts.map +1 -1
- package/dist/core/tests/mobileBrowser.js +5 -2
- package/dist/core/tests/mobileBrowser.js.map +1 -1
- package/dist/core/tests/runCode.d.ts +9 -1
- package/dist/core/tests/runCode.d.ts.map +1 -1
- package/dist/core/tests/runCode.js +58 -2
- package/dist/core/tests/runCode.js.map +1 -1
- package/dist/core/tests/saveScreenshot.d.ts +18 -2
- package/dist/core/tests/saveScreenshot.d.ts.map +1 -1
- package/dist/core/tests/saveScreenshot.js +159 -4
- package/dist/core/tests/saveScreenshot.js.map +1 -1
- package/dist/core/tests/startRecording.d.ts.map +1 -1
- package/dist/core/tests/startRecording.js +6 -4
- package/dist/core/tests/startRecording.js.map +1 -1
- package/dist/core/tests/startSurface.d.ts.map +1 -1
- package/dist/core/tests/startSurface.js +8 -2
- package/dist/core/tests/startSurface.js.map +1 -1
- package/dist/core/tests.d.ts +21 -7
- package/dist/core/tests.d.ts.map +1 -1
- package/dist/core/tests.js +372 -121
- package/dist/core/tests.js.map +1 -1
- package/dist/core/utils.d.ts +68 -3
- package/dist/core/utils.d.ts.map +1 -1
- package/dist/core/utils.js +551 -18
- package/dist/core/utils.js.map +1 -1
- package/dist/core/verification.d.ts +17 -0
- package/dist/core/verification.d.ts.map +1 -0
- package/dist/core/verification.js +850 -0
- package/dist/core/verification.js.map +1 -0
- package/dist/debug/appium.js +2 -2
- package/dist/debug/appium.js.map +1 -1
- package/dist/debug/findings.js +2 -2
- package/dist/debug/findings.js.map +1 -1
- package/dist/debug/tools.d.ts +8 -0
- package/dist/debug/tools.d.ts.map +1 -1
- package/dist/debug/tools.js +60 -22
- package/dist/debug/tools.js.map +1 -1
- package/dist/hints/context.d.ts.map +1 -1
- package/dist/hints/context.js +2 -2
- package/dist/hints/context.js.map +1 -1
- package/dist/index.cjs +25777 -4
- package/dist/reporters/htmlReporter.js +1 -1
- package/dist/runtime/androidInstaller.d.ts +45 -0
- package/dist/runtime/androidInstaller.d.ts.map +1 -1
- package/dist/runtime/androidInstaller.js +88 -5
- package/dist/runtime/androidInstaller.js.map +1 -1
- package/dist/runtime/browsers.d.ts +87 -7
- package/dist/runtime/browsers.d.ts.map +1 -1
- package/dist/runtime/browsers.js +308 -41
- package/dist/runtime/browsers.js.map +1 -1
- package/dist/runtime/cacheDir.d.ts +1 -1
- package/dist/runtime/cacheDir.d.ts.map +1 -1
- package/dist/runtime/cacheDir.js +5 -2
- package/dist/runtime/cacheDir.js.map +1 -1
- package/dist/runtime/heavyDeps.d.ts.map +1 -1
- package/dist/runtime/heavyDeps.js.map +1 -1
- package/dist/runtime/installCommand.d.ts.map +1 -1
- package/dist/runtime/installCommand.js +23 -0
- package/dist/runtime/installCommand.js.map +1 -1
- package/dist/runtime/loader.d.ts +13 -0
- package/dist/runtime/loader.d.ts.map +1 -1
- package/dist/runtime/loader.js +50 -0
- package/dist/runtime/loader.js.map +1 -1
- package/dist/runtime/wdaProducts.d.ts +12 -1
- package/dist/runtime/wdaProducts.d.ts.map +1 -1
- package/dist/runtime/wdaProducts.js +27 -2
- package/dist/runtime/wdaProducts.js.map +1 -1
- package/dist/runtime/windowsBash.d.ts +6 -1
- package/dist/runtime/windowsBash.d.ts.map +1 -1
- package/dist/runtime/windowsBash.js +45 -4
- package/dist/runtime/windowsBash.js.map +1 -1
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +19 -5
- package/dist/utils.js.map +1 -1
- package/package.json +43 -41
|
@@ -0,0 +1,850 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import { requireYaml } from "./lazyYaml.js";
|
|
3
|
+
// ===========================================================================
|
|
4
|
+
// Verification metadata write-back (ADR 01140).
|
|
5
|
+
//
|
|
6
|
+
// A page opts in by listing a `verified-by` entry with `tool: doc-detective`
|
|
7
|
+
// and a `check` naming a test or spec id, either in its front matter or in an
|
|
8
|
+
// inline `meta` statement. When that check passes, Doc Detective fills the
|
|
9
|
+
// entry's `version` and `passed`, and sets `last-verified` to the oldest
|
|
10
|
+
// `passed` across the entries once every entry has one. Other tools' entries
|
|
11
|
+
// are never touched, and nothing is written on any other result.
|
|
12
|
+
//
|
|
13
|
+
// The keys are manni's shared vocabulary (manni:stewardship 1.2.0, proposal
|
|
14
|
+
// 0098), and the inline form is manni's `meta` statement (proposal 0099). The
|
|
15
|
+
// statement scanner, anchoring and pin below port manni's own rules
|
|
16
|
+
// (src/shared/statements.ts and src/meta/core/statements.ts), so a pin written
|
|
17
|
+
// here is the pin `manni meta derive` writes. test/verification-fixtures/ holds
|
|
18
|
+
// pages pinned by manni, and the tests check this port against them.
|
|
19
|
+
// ===========================================================================
|
|
20
|
+
export const VERIFIED_TOOL = "doc-detective";
|
|
21
|
+
const TOOL = VERIFIED_TOOL;
|
|
22
|
+
// Apply edits made against one text, in order; edits at one offset keep the
|
|
23
|
+
// order they were made in.
|
|
24
|
+
// Precondition: edits never overlap. Every caller edits disjoint node ranges or
|
|
25
|
+
// inserts at a point; an overlapping edit would drop the text between them, so
|
|
26
|
+
// it throws instead, and the writer's per-file guard leaves the file untouched.
|
|
27
|
+
function applyEdits(text, edits) {
|
|
28
|
+
const sorted = edits.map((e, i) => ({ ...e, i })).sort((a, b) => a.start - b.start || a.i - b.i);
|
|
29
|
+
let out = "";
|
|
30
|
+
let pos = 0;
|
|
31
|
+
for (const e of sorted) {
|
|
32
|
+
if (e.start < pos)
|
|
33
|
+
throw new Error(`overlapping edits at offset ${e.start}`);
|
|
34
|
+
out += text.slice(pos, e.start) + e.text;
|
|
35
|
+
pos = e.end;
|
|
36
|
+
}
|
|
37
|
+
return out + text.slice(pos);
|
|
38
|
+
}
|
|
39
|
+
function isRecord(value) {
|
|
40
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
41
|
+
}
|
|
42
|
+
// The oldest of the entries' pass dates, or undefined when any entry has none.
|
|
43
|
+
function oldest(values) {
|
|
44
|
+
let best;
|
|
45
|
+
let bestTime = Infinity;
|
|
46
|
+
for (const value of values) {
|
|
47
|
+
if (value === undefined)
|
|
48
|
+
return undefined;
|
|
49
|
+
const time = Date.parse(value);
|
|
50
|
+
if (Number.isNaN(time))
|
|
51
|
+
return undefined;
|
|
52
|
+
if (time < bestTime) {
|
|
53
|
+
best = value;
|
|
54
|
+
bestTime = time;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return best;
|
|
58
|
+
}
|
|
59
|
+
// Fill every doc-detective entry whose check passed. Returns whether any did,
|
|
60
|
+
// and each entry's pass date afterwards, read through `passedOf`.
|
|
61
|
+
function fillEntries(entries, read, fill, opts) {
|
|
62
|
+
let any = false;
|
|
63
|
+
const passed = entries.map((entry) => {
|
|
64
|
+
const e = read(entry);
|
|
65
|
+
if (!e)
|
|
66
|
+
return undefined;
|
|
67
|
+
if (e.tool !== TOOL || typeof e.check !== "string")
|
|
68
|
+
return e.passed;
|
|
69
|
+
const result = opts.resolve(e.check);
|
|
70
|
+
if (result === null)
|
|
71
|
+
opts.onUnknown?.(e.check);
|
|
72
|
+
if (result !== "PASS")
|
|
73
|
+
return e.passed;
|
|
74
|
+
fill(entry);
|
|
75
|
+
any = true;
|
|
76
|
+
return opts.today;
|
|
77
|
+
});
|
|
78
|
+
return { any, passed };
|
|
79
|
+
}
|
|
80
|
+
// --------------------------------------------------------------------------
|
|
81
|
+
// Front matter
|
|
82
|
+
// --------------------------------------------------------------------------
|
|
83
|
+
const FRONT_MATTER = /^\uFEFF?---[ \t]*\r?\n([\s\S]*?)^---[ \t]*$/m;
|
|
84
|
+
function frontMatter(content) {
|
|
85
|
+
const fm = FRONT_MATTER.exec(content);
|
|
86
|
+
if (!fm || fm.index !== 0)
|
|
87
|
+
return undefined;
|
|
88
|
+
const start = fm[0].indexOf("\n") + 1;
|
|
89
|
+
return { start, src: fm[1], end: fm[0].length };
|
|
90
|
+
}
|
|
91
|
+
// Set `key` on a YAML map as a text edit: replace the value in place (keeping
|
|
92
|
+
// its quotes), or add the key at the end of the map, in its style.
|
|
93
|
+
function setKey(src, map, key, value, eol, edits) {
|
|
94
|
+
const YAML = requireYaml();
|
|
95
|
+
const pair = map.items.find((p) => YAML.isScalar(p.key) && p.key.value === key);
|
|
96
|
+
if (pair) {
|
|
97
|
+
const v = pair.value;
|
|
98
|
+
if (!YAML.isScalar(v) || !v.range || String(v.value) === value)
|
|
99
|
+
return;
|
|
100
|
+
const quote = src[v.range[0]];
|
|
101
|
+
const text = quote === '"' || quote === "'" ? `${quote}${value}${quote}` : value;
|
|
102
|
+
// A key with no value (`passed:`) has a zero-length value range right after
|
|
103
|
+
// its colon, so the value needs a separating space.
|
|
104
|
+
const gap = v.range[0] === v.range[1] && !/\s/.test(src[v.range[0] - 1] ?? "") ? " " : "";
|
|
105
|
+
edits.push({ start: v.range[0], end: v.range[1], text: `${gap}${text}` });
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
if (map.flow) {
|
|
109
|
+
const close = src.lastIndexOf("}", map.range[1] - 1);
|
|
110
|
+
edits.push({ start: close, end: close, text: `${map.items.length > 0 ? ", " : ""}${key}: ${value}` });
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
const { at, indent } = blockEnd(src, map);
|
|
114
|
+
edits.push({ start: at, end: at, text: `${eol}${indent}${key}: ${value}` });
|
|
115
|
+
}
|
|
116
|
+
// Where a block map or sequence's last line ends (past any trailing comment,
|
|
117
|
+
// before its line break), and the indent its items start at. Callers pass only
|
|
118
|
+
// block-style nodes, which always hold an item: an empty one parses as flow
|
|
119
|
+
// (`[]`, `{}`) or null, and both take other paths.
|
|
120
|
+
function blockEnd(src, node) {
|
|
121
|
+
const YAML = requireYaml();
|
|
122
|
+
const last = node.items[node.items.length - 1];
|
|
123
|
+
const tail = YAML.isPair(last) ? (last.value?.range ? last.value : last.key) : last;
|
|
124
|
+
let end = tail.range[1];
|
|
125
|
+
while (end > 0 && /\s/.test(src[end - 1]))
|
|
126
|
+
end--;
|
|
127
|
+
const nl = src.indexOf("\n", end);
|
|
128
|
+
const at = nl === -1 ? src.length : src[nl - 1] === "\r" ? nl - 1 : nl;
|
|
129
|
+
const column = node.range[0] - (src.lastIndexOf("\n", node.range[0] - 1) + 1);
|
|
130
|
+
return { at, indent: " ".repeat(column) };
|
|
131
|
+
}
|
|
132
|
+
// `verification.write: true`: give a passing page with no Doc Detective entry
|
|
133
|
+
// a `verified-by` entry naming its spec, for the fill to complete in the same
|
|
134
|
+
// run. The entry goes at the end of an existing list, or of the front matter,
|
|
135
|
+
// in its style; a page without front matter gets a block. A page that already
|
|
136
|
+
// names doc-detective, or whose front matter isn't a YAML map, is unchanged.
|
|
137
|
+
export function declarePage(content, specId) {
|
|
138
|
+
const check = /^[A-Za-z0-9._/-]+$/.test(specId) ? specId : JSON.stringify(specId);
|
|
139
|
+
const fm = frontMatter(content);
|
|
140
|
+
const eol = (fm ? content.slice(0, fm.end) : content).includes("\r\n") ? "\r\n" : "\n";
|
|
141
|
+
const item = (indent) => `${indent}- tool: ${TOOL}${eol}${indent} check: ${check}`;
|
|
142
|
+
if (!fm) {
|
|
143
|
+
const bom = content.charCodeAt(0) === 0xfeff ? "\uFEFF" : "";
|
|
144
|
+
return `${bom}---${eol}verified-by:${eol}${item(" ")}${eol}---${eol}${content.slice(bom.length)}`;
|
|
145
|
+
}
|
|
146
|
+
const YAML = requireYaml();
|
|
147
|
+
const doc = YAML.parseDocument(fm.src);
|
|
148
|
+
if (doc.errors.length > 0)
|
|
149
|
+
return content;
|
|
150
|
+
const root = doc.contents;
|
|
151
|
+
const edits = [];
|
|
152
|
+
if (root === null) {
|
|
153
|
+
edits.push({ start: 0, end: 0, text: `verified-by:${eol}${item(" ")}${eol}` });
|
|
154
|
+
}
|
|
155
|
+
else if (!YAML.isMap(root)) {
|
|
156
|
+
return content;
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
const list = root.get("verified-by", true);
|
|
160
|
+
if (list === undefined) {
|
|
161
|
+
if (root.flow) {
|
|
162
|
+
const close = fm.src.lastIndexOf("}", root.range[1] - 1);
|
|
163
|
+
const text = `${root.items.length > 0 ? ", " : ""}verified-by: [{tool: ${TOOL}, check: ${check}}]`;
|
|
164
|
+
edits.push({ start: close, end: close, text });
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
const { at, indent } = blockEnd(fm.src, root);
|
|
168
|
+
edits.push({ start: at, end: at, text: `${eol}${indent}verified-by:${eol}${item(`${indent} `)}` });
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
else if (!YAML.isSeq(list)) {
|
|
172
|
+
return content;
|
|
173
|
+
}
|
|
174
|
+
else if (list.items.some((i) => YAML.isMap(i) && i.get("tool") === TOOL)) {
|
|
175
|
+
return content;
|
|
176
|
+
}
|
|
177
|
+
else if (list.flow) {
|
|
178
|
+
const close = fm.src.lastIndexOf("]", list.range[1] - 1);
|
|
179
|
+
const text = `${list.items.length > 0 ? ", " : ""}{tool: ${TOOL}, check: ${check}}`;
|
|
180
|
+
edits.push({ start: close, end: close, text });
|
|
181
|
+
}
|
|
182
|
+
else {
|
|
183
|
+
const { at, indent } = blockEnd(fm.src, list);
|
|
184
|
+
edits.push({ start: at, end: at, text: `${eol}${item(indent)}` });
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return content.slice(0, fm.start) + applyEdits(fm.src, edits) + content.slice(fm.start + fm.src.length);
|
|
188
|
+
}
|
|
189
|
+
function writeFrontMatter(content, opts) {
|
|
190
|
+
const fm = frontMatter(content);
|
|
191
|
+
if (!fm || !fm.src.includes("verified-by"))
|
|
192
|
+
return content;
|
|
193
|
+
const YAML = requireYaml();
|
|
194
|
+
const doc = YAML.parseDocument(fm.src);
|
|
195
|
+
if (doc.errors.length > 0 || !YAML.isMap(doc.contents))
|
|
196
|
+
return content;
|
|
197
|
+
const root = doc.contents;
|
|
198
|
+
const list = root.get("verified-by", true);
|
|
199
|
+
if (!YAML.isSeq(list))
|
|
200
|
+
return content;
|
|
201
|
+
const eol = fm.src.includes("\r\n") ? "\r\n" : "\n";
|
|
202
|
+
const edits = [];
|
|
203
|
+
const { any, passed } = fillEntries(list.items, (item) => {
|
|
204
|
+
if (!YAML.isMap(item))
|
|
205
|
+
return undefined;
|
|
206
|
+
const p = item.get("passed", true);
|
|
207
|
+
return { tool: item.get("tool"), check: item.get("check"), passed: YAML.isScalar(p) ? String(p.value) : undefined };
|
|
208
|
+
}, (item) => {
|
|
209
|
+
// Keys the entry already has go first. An empty `passed:` that ends the
|
|
210
|
+
// entry shares its offset with the line a new key is appended on, and its
|
|
211
|
+
// value must land before that line.
|
|
212
|
+
const values = [
|
|
213
|
+
["version", opts.version],
|
|
214
|
+
["passed", opts.today],
|
|
215
|
+
];
|
|
216
|
+
values.sort(([a], [b]) => Number(!item.has(a)) - Number(!item.has(b)));
|
|
217
|
+
for (const [key, value] of values)
|
|
218
|
+
setKey(fm.src, item, key, value, eol, edits);
|
|
219
|
+
}, opts);
|
|
220
|
+
if (!any)
|
|
221
|
+
return content;
|
|
222
|
+
const last = oldest(passed);
|
|
223
|
+
if (last !== undefined)
|
|
224
|
+
setKey(fm.src, root, "last-verified", last, eol, edits);
|
|
225
|
+
if (edits.length === 0)
|
|
226
|
+
return content;
|
|
227
|
+
return content.slice(0, fm.start) + applyEdits(fm.src, edits) + content.slice(fm.start + fm.src.length);
|
|
228
|
+
}
|
|
229
|
+
// --------------------------------------------------------------------------
|
|
230
|
+
// Inline `meta` statements: a port of manni's scanner, anchoring and pin.
|
|
231
|
+
// --------------------------------------------------------------------------
|
|
232
|
+
// The citations hashing rule: lines joined with LF, one BOM stripped, CRLF
|
|
233
|
+
// folded to LF, no trailing LF, as `sha256-<hex>`.
|
|
234
|
+
export function pinOf(lines) {
|
|
235
|
+
let text = lines.join("\n");
|
|
236
|
+
if (text.charCodeAt(0) === 0xfeff)
|
|
237
|
+
text = text.slice(1);
|
|
238
|
+
text = text.replace(/\r\n/g, "\n");
|
|
239
|
+
return `sha256-${crypto.createHash("sha256").update(text, "utf8").digest("hex")}`;
|
|
240
|
+
}
|
|
241
|
+
const HTML_COMMENT = { open: "<!--", close: "-->" };
|
|
242
|
+
const JSX_COMMENT = { open: "{/*", close: "*/}" };
|
|
243
|
+
const LINK_REFERENCE = { open: "[comment]: # (", close: ")" };
|
|
244
|
+
const PI_FORM = { open: "<?manni", close: "?>" };
|
|
245
|
+
const FORMS = {
|
|
246
|
+
markdown: [HTML_COMMENT, JSX_COMMENT, LINK_REFERENCE],
|
|
247
|
+
html: [HTML_COMMENT],
|
|
248
|
+
asciidoc: [{ open: "// (", close: ")" }],
|
|
249
|
+
dita: [PI_FORM, HTML_COMMENT],
|
|
250
|
+
};
|
|
251
|
+
// cite's marker forms, which also count as marker-only lines when anchoring.
|
|
252
|
+
const CITE_FORMS = { ...FORMS, dita: [HTML_COMMENT] };
|
|
253
|
+
const isLineForm = (form) => form.close === ")";
|
|
254
|
+
function lineEnd(content, pos) {
|
|
255
|
+
const nl = content.indexOf("\n", pos);
|
|
256
|
+
return nl === -1 ? content.length : nl;
|
|
257
|
+
}
|
|
258
|
+
function lineText(content, pos, end) {
|
|
259
|
+
const text = content.slice(pos, end);
|
|
260
|
+
return text.endsWith("\r") ? text.slice(0, -1) : text;
|
|
261
|
+
}
|
|
262
|
+
function beforeCr(content, end) {
|
|
263
|
+
return end > 0 && content.charCodeAt(end - 1) === 13 ? end - 1 : end;
|
|
264
|
+
}
|
|
265
|
+
function nextLineStart(content, offset) {
|
|
266
|
+
if (offset === 0 || content.charCodeAt(offset - 1) === 10)
|
|
267
|
+
return offset;
|
|
268
|
+
return lineEnd(content, offset) + 1;
|
|
269
|
+
}
|
|
270
|
+
function lineAt(content, offset) {
|
|
271
|
+
let line = 1;
|
|
272
|
+
const stop = Math.min(offset, content.length);
|
|
273
|
+
for (let at = content.indexOf("\n"); at !== -1 && at < stop; at = content.indexOf("\n", at + 1))
|
|
274
|
+
line++;
|
|
275
|
+
return line;
|
|
276
|
+
}
|
|
277
|
+
function offsetOfLine(content, line) {
|
|
278
|
+
let pos = 0;
|
|
279
|
+
for (let n = 1; n < line; n++) {
|
|
280
|
+
const nl = content.indexOf("\n", pos);
|
|
281
|
+
if (nl === -1)
|
|
282
|
+
return content.length;
|
|
283
|
+
pos = nl + 1;
|
|
284
|
+
}
|
|
285
|
+
return pos;
|
|
286
|
+
}
|
|
287
|
+
const isCite = (inner) => inner === "cite" || (inner.startsWith("cite") && /\s/.test(inner.charAt(4)));
|
|
288
|
+
const isMetaKeyword = (inner) => inner === "meta" || (inner.startsWith("meta") && /\s/.test(inner.charAt(4)));
|
|
289
|
+
function isMarkerLine(line, format) {
|
|
290
|
+
const text = line.trim();
|
|
291
|
+
const cite = (CITE_FORMS[format] || []).some((form) => {
|
|
292
|
+
if (text.length < form.open.length + form.close.length || !text.startsWith(form.open))
|
|
293
|
+
return false;
|
|
294
|
+
const closeAt = text.indexOf(form.close, form.open.length);
|
|
295
|
+
if (closeAt !== text.length - form.close.length)
|
|
296
|
+
return false;
|
|
297
|
+
return isCite(text.slice(form.open.length, closeAt).trim());
|
|
298
|
+
});
|
|
299
|
+
if (cite)
|
|
300
|
+
return true;
|
|
301
|
+
return (FORMS[format] || []).some((form) => {
|
|
302
|
+
if (text.length < form.open.length + form.close.length)
|
|
303
|
+
return false;
|
|
304
|
+
if (!text.startsWith(form.open) || !text.endsWith(form.close))
|
|
305
|
+
return false;
|
|
306
|
+
const closeAt = isLineForm(form) ? text.length - form.close.length : text.indexOf(form.close, form.open.length);
|
|
307
|
+
if (closeAt !== text.length - form.close.length)
|
|
308
|
+
return false;
|
|
309
|
+
return isMetaKeyword(text.slice(form.open.length, closeAt).trim());
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
const ATX_HEADING = /^[ \t]*#{1,6}([ \t]|$)/;
|
|
313
|
+
const ASCIIDOC_TITLE = /^={1,6}[ \t]/;
|
|
314
|
+
const ADORNMENT = /^[ \t]*([-=~^"'*+#_:<>])(?:[ \t]*\1){2,}[ \t]*$/;
|
|
315
|
+
const TAG_LINE = /^[ \t]*<\/?[A-Za-z][^<>]*>[ \t]*$/;
|
|
316
|
+
function isBoundLine(line, format) {
|
|
317
|
+
if (ADORNMENT.test(line))
|
|
318
|
+
return true;
|
|
319
|
+
if (format === "markdown")
|
|
320
|
+
return ATX_HEADING.test(line) || TAG_LINE.test(line);
|
|
321
|
+
if (format === "html")
|
|
322
|
+
return TAG_LINE.test(line);
|
|
323
|
+
if (format === "asciidoc")
|
|
324
|
+
return ASCIIDOC_TITLE.test(line);
|
|
325
|
+
return false;
|
|
326
|
+
}
|
|
327
|
+
// manni's one block rule (`src/shared/code-regions.ts`), which anchoring and
|
|
328
|
+
// the code skip both read. A Markdown fence opens after any indent and closes
|
|
329
|
+
// on a bare run of its character at least as long. An AsciiDoc delimited
|
|
330
|
+
// block closes only on an exact repeat of its opener line. Its content is
|
|
331
|
+
// code in a verbatim block (`----`, `....`, `++++`, `////`) and prose in a
|
|
332
|
+
// compound one (`====`, `****`, `____`).
|
|
333
|
+
const ANY_FENCE = /^(?:[ \t]*(?:`{3,}|~{3,})|-{4,})/;
|
|
334
|
+
const FENCE_OPENER = /^[ \t]*(`{3,}|~{3,})/;
|
|
335
|
+
const ASCIIDOC_DELIMITER = /^([-.+/=*_])\1{3,}[ \t]*$/;
|
|
336
|
+
const VERBATIM = new Set(["-", ".", "+", "/"]);
|
|
337
|
+
// A line a two-line title can be, so a `=` line under it is its underline.
|
|
338
|
+
const TITLE_TEXT = /^[^\s./[]/;
|
|
339
|
+
function asciidocBlock(line, above) {
|
|
340
|
+
const char = ASCIIDOC_DELIMITER.exec(line)?.[1];
|
|
341
|
+
if (char === undefined)
|
|
342
|
+
return undefined;
|
|
343
|
+
if (char === "=" && above !== undefined && TITLE_TEXT.test(above)) {
|
|
344
|
+
if (Math.abs(above.trimEnd().length - line.trimEnd().length) <= 2)
|
|
345
|
+
return undefined;
|
|
346
|
+
}
|
|
347
|
+
return VERBATIM.has(char) ? "verbatim" : "compound";
|
|
348
|
+
}
|
|
349
|
+
function closesFence(line, open) {
|
|
350
|
+
const trimmed = line.trim();
|
|
351
|
+
let n = 0;
|
|
352
|
+
while (trimmed.charAt(n) === open.charAt(0))
|
|
353
|
+
n++;
|
|
354
|
+
return n >= open.length && n === trimmed.length;
|
|
355
|
+
}
|
|
356
|
+
// What closes the block `line` opens, or undefined when it opens none. The
|
|
357
|
+
// code skip reads verbatim blocks only; anchoring passes `compound` to read
|
|
358
|
+
// every block, and `above` to tell a title's `=` underline from a block.
|
|
359
|
+
function blockCloser(line, format, options = {}) {
|
|
360
|
+
if (format === "markdown") {
|
|
361
|
+
const run = FENCE_OPENER.exec(line)?.[1];
|
|
362
|
+
return run === undefined ? undefined : (l) => closesFence(l, run);
|
|
363
|
+
}
|
|
364
|
+
if (format === "asciidoc") {
|
|
365
|
+
const kind = asciidocBlock(line, options.above);
|
|
366
|
+
if (kind === undefined || (kind === "compound" && options.compound !== true))
|
|
367
|
+
return undefined;
|
|
368
|
+
const open = line.trimEnd();
|
|
369
|
+
return (l) => l.trimEnd() === open;
|
|
370
|
+
}
|
|
371
|
+
return undefined;
|
|
372
|
+
}
|
|
373
|
+
// Whether a line ends a paragraph as a fence or a block delimiter.
|
|
374
|
+
function isFenceLine(line, format, above) {
|
|
375
|
+
return ANY_FENCE.test(line) || (format === "asciidoc" && asciidocBlock(line, above) !== undefined);
|
|
376
|
+
}
|
|
377
|
+
function lineBefore(content, pos) {
|
|
378
|
+
if (pos === 0)
|
|
379
|
+
return undefined;
|
|
380
|
+
const start = content.lastIndexOf("\n", pos - 2) + 1;
|
|
381
|
+
return lineText(content, start, pos - 1);
|
|
382
|
+
}
|
|
383
|
+
function paragraphAfter(content, offset, format) {
|
|
384
|
+
let pos = offset;
|
|
385
|
+
const firstEnd = lineEnd(content, pos);
|
|
386
|
+
if (lineText(content, pos, firstEnd).trim() === "")
|
|
387
|
+
pos = firstEnd + 1;
|
|
388
|
+
while (pos < content.length) {
|
|
389
|
+
const end = lineEnd(content, pos);
|
|
390
|
+
const text = lineText(content, pos, end);
|
|
391
|
+
if (text.trim() === "" || isMarkerLine(text, format)) {
|
|
392
|
+
pos = end + 1;
|
|
393
|
+
continue;
|
|
394
|
+
}
|
|
395
|
+
if (isFenceLine(text, format, lineBefore(content, pos)))
|
|
396
|
+
return undefined;
|
|
397
|
+
const start = pos;
|
|
398
|
+
let last = end;
|
|
399
|
+
let cursor = isBoundLine(text, format) ? content.length : end + 1;
|
|
400
|
+
while (cursor < content.length) {
|
|
401
|
+
const e = lineEnd(content, cursor);
|
|
402
|
+
const t = lineText(content, cursor, e);
|
|
403
|
+
if (t.trim() === "" || isFenceLine(t, format) || isMarkerLine(t, format) || isBoundLine(t, format))
|
|
404
|
+
break;
|
|
405
|
+
last = e;
|
|
406
|
+
cursor = e + 1;
|
|
407
|
+
}
|
|
408
|
+
return { end: beforeCr(content, last), line: lineAt(content, start) };
|
|
409
|
+
}
|
|
410
|
+
return undefined;
|
|
411
|
+
}
|
|
412
|
+
// The block opening on `line`, delimiters included: a fence at any indent, or
|
|
413
|
+
// any AsciiDoc delimited block. Undefined when it opens none or never closes.
|
|
414
|
+
function fenceSpanAt(content, line, format) {
|
|
415
|
+
const pos = offsetOfLine(content, line);
|
|
416
|
+
const above = lineBefore(content, pos);
|
|
417
|
+
const closes = blockCloser(lineText(content, pos, lineEnd(content, pos)), format, { above, compound: true });
|
|
418
|
+
if (closes === undefined)
|
|
419
|
+
return undefined;
|
|
420
|
+
let cursor = Math.min(lineEnd(content, pos) + 1, content.length);
|
|
421
|
+
while (cursor < content.length) {
|
|
422
|
+
const e = lineEnd(content, cursor);
|
|
423
|
+
if (closes(lineText(content, cursor, e)))
|
|
424
|
+
return { start: line, end: lineAt(content, cursor) };
|
|
425
|
+
cursor = e + 1;
|
|
426
|
+
}
|
|
427
|
+
return undefined;
|
|
428
|
+
}
|
|
429
|
+
// The lines a statement ending at `after` anchors: the rest of its own line
|
|
430
|
+
// when that carries text, else the paragraph or fenced block that follows.
|
|
431
|
+
function anchoredLines(content, after, format) {
|
|
432
|
+
const restEnd = lineEnd(content, after);
|
|
433
|
+
if (content.slice(after, restEnd).trim() !== "") {
|
|
434
|
+
const line = lineAt(content, after);
|
|
435
|
+
return { start: line, end: line };
|
|
436
|
+
}
|
|
437
|
+
const paragraph = paragraphAfter(content, after, format);
|
|
438
|
+
if (paragraph !== undefined)
|
|
439
|
+
return { start: paragraph.line, end: lineAt(content, paragraph.end) };
|
|
440
|
+
let pos = nextLineStart(content, after);
|
|
441
|
+
while (pos < content.length) {
|
|
442
|
+
const end = lineEnd(content, pos);
|
|
443
|
+
const text = lineText(content, pos, end);
|
|
444
|
+
if (text.trim() !== "" && !isMarkerLine(text, format))
|
|
445
|
+
return fenceSpanAt(content, lineAt(content, pos), format);
|
|
446
|
+
pos = end + 1;
|
|
447
|
+
}
|
|
448
|
+
return undefined;
|
|
449
|
+
}
|
|
450
|
+
// Verbatim blocks and backtick spans, where no statement is read.
|
|
451
|
+
function codeRegions(text, format) {
|
|
452
|
+
const spans = format === "markdown" || format === "asciidoc";
|
|
453
|
+
const out = [];
|
|
454
|
+
if (!spans)
|
|
455
|
+
return out;
|
|
456
|
+
let open;
|
|
457
|
+
let pos = 0;
|
|
458
|
+
while (pos < text.length) {
|
|
459
|
+
const nl = text.indexOf("\n", pos);
|
|
460
|
+
const end = nl === -1 ? text.length : nl;
|
|
461
|
+
const line = lineText(text, pos, end);
|
|
462
|
+
if (open) {
|
|
463
|
+
if (open.closes(line)) {
|
|
464
|
+
out.push({ start: open.start, end });
|
|
465
|
+
open = undefined;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
else {
|
|
469
|
+
const closes = blockCloser(line, format);
|
|
470
|
+
if (closes !== undefined)
|
|
471
|
+
open = { closes, start: pos };
|
|
472
|
+
else
|
|
473
|
+
spanRegions(line, pos, out);
|
|
474
|
+
}
|
|
475
|
+
pos = end + 1;
|
|
476
|
+
}
|
|
477
|
+
if (open)
|
|
478
|
+
out.push({ start: open.start, end: text.length });
|
|
479
|
+
return out;
|
|
480
|
+
}
|
|
481
|
+
function spanRegions(line, lineStart, out) {
|
|
482
|
+
const run = (i) => {
|
|
483
|
+
let n = 0;
|
|
484
|
+
while (line.charCodeAt(i + n) === 96)
|
|
485
|
+
n++;
|
|
486
|
+
return n;
|
|
487
|
+
};
|
|
488
|
+
let i = 0;
|
|
489
|
+
while (i < line.length) {
|
|
490
|
+
const n = run(i);
|
|
491
|
+
if (n === 0) {
|
|
492
|
+
i++;
|
|
493
|
+
continue;
|
|
494
|
+
}
|
|
495
|
+
let j = i + n;
|
|
496
|
+
let closer = -1;
|
|
497
|
+
while (j < line.length) {
|
|
498
|
+
const m = run(j);
|
|
499
|
+
if (m === 0)
|
|
500
|
+
j++;
|
|
501
|
+
else if (m === n) {
|
|
502
|
+
closer = j;
|
|
503
|
+
break;
|
|
504
|
+
}
|
|
505
|
+
else
|
|
506
|
+
j += m;
|
|
507
|
+
}
|
|
508
|
+
if (closer === -1) {
|
|
509
|
+
i += n;
|
|
510
|
+
continue;
|
|
511
|
+
}
|
|
512
|
+
out.push({ start: lineStart + i, end: lineStart + closer + n });
|
|
513
|
+
i = closer + n;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
const PIN_TOKEN = /^sha256-[0-9a-f]{64}$/;
|
|
517
|
+
function parseStatements(content, from, format) {
|
|
518
|
+
const body = content.slice(from);
|
|
519
|
+
const code = codeRegions(body, format);
|
|
520
|
+
const out = [];
|
|
521
|
+
for (const form of FORMS[format] || []) {
|
|
522
|
+
let cursor = 0;
|
|
523
|
+
for (;;) {
|
|
524
|
+
const at = body.indexOf(form.open, cursor);
|
|
525
|
+
if (at === -1)
|
|
526
|
+
break;
|
|
527
|
+
const region = code.find((r) => at >= r.start && at < r.end);
|
|
528
|
+
if (region) {
|
|
529
|
+
cursor = region.end;
|
|
530
|
+
continue;
|
|
531
|
+
}
|
|
532
|
+
const innerStart = at + form.open.length;
|
|
533
|
+
const closeAt = isLineForm(form) ? body.lastIndexOf(form.close, lineEnd(body, at)) : body.indexOf(form.close, innerStart);
|
|
534
|
+
if (closeAt === -1 || closeAt < innerStart) {
|
|
535
|
+
cursor = innerStart;
|
|
536
|
+
continue;
|
|
537
|
+
}
|
|
538
|
+
cursor = closeAt + form.close.length;
|
|
539
|
+
const inner = body.slice(innerStart, closeAt);
|
|
540
|
+
if (!isMetaKeyword(inner.trim()))
|
|
541
|
+
continue;
|
|
542
|
+
const lead = /^\s*meta\s*/.exec(inner)?.[0] ?? "";
|
|
543
|
+
const rest = inner.slice(lead.length);
|
|
544
|
+
const word = /^\S+/.exec(rest)?.[0] ?? "";
|
|
545
|
+
const pinned = PIN_TOKEN.test(word);
|
|
546
|
+
const payload = (pinned ? rest.slice(word.length) : rest).trim();
|
|
547
|
+
out.push({
|
|
548
|
+
line: lineAt(content, from + at),
|
|
549
|
+
start: from + at,
|
|
550
|
+
end: from + cursor,
|
|
551
|
+
pinAt: from + innerStart + lead.length,
|
|
552
|
+
closeAt: from + closeAt,
|
|
553
|
+
pin: pinned ? word : undefined,
|
|
554
|
+
payload,
|
|
555
|
+
broken: isLineForm(form) && payload.includes(")")
|
|
556
|
+
? `its line comment ends at the first ")"; use a block comment form`
|
|
557
|
+
: format === "dita" && form === HTML_COMMENT && payload.includes("--")
|
|
558
|
+
? `an XML comment can't hold "--"; use <?manni meta …?>`
|
|
559
|
+
: "",
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
return out.sort((a, b) => a.start - b.start);
|
|
564
|
+
}
|
|
565
|
+
// DITA's `<data name="manni-meta" value='…'/>` carriers, read as manni's
|
|
566
|
+
// `dataStatement` reads them: the tag's own `name` and `value` attributes,
|
|
567
|
+
// parsed whole so `data-name` or text inside another value never counts, the
|
|
568
|
+
// value's entities decoded, an optional pin, then the payload.
|
|
569
|
+
const ATTRIBUTE = /([^\s=/>"']+)\s*=\s*(?:"([^"]*)"|'([^']*)')/g;
|
|
570
|
+
// A start tag's attributes, first occurrence wins; `at` is the value's first
|
|
571
|
+
// character in `tag`, just past its opening quote.
|
|
572
|
+
function attributesOf(tag) {
|
|
573
|
+
const out = new Map();
|
|
574
|
+
const from = /^<[^\s/>]*/.exec(tag)?.[0].length ?? 0;
|
|
575
|
+
for (const m of tag.slice(from).matchAll(ATTRIBUTE)) {
|
|
576
|
+
const [whole, name = "", double, single] = m;
|
|
577
|
+
const value = double ?? single ?? "";
|
|
578
|
+
if (!out.has(name))
|
|
579
|
+
out.set(name, { value, at: from + m.index + whole.length - value.length - 1 });
|
|
580
|
+
}
|
|
581
|
+
return out;
|
|
582
|
+
}
|
|
583
|
+
const isCarrier = (tag) => attributesOf(tag).get("name")?.value === "manni-meta";
|
|
584
|
+
function decodeEntities(text) {
|
|
585
|
+
return text.replace(/&(#x[0-9a-fA-F]+|#[0-9]+|quot|apos|lt|gt|amp);/g, (_m, e) => {
|
|
586
|
+
if (e.startsWith("#x"))
|
|
587
|
+
return String.fromCodePoint(Number.parseInt(e.slice(2), 16));
|
|
588
|
+
if (e.startsWith("#"))
|
|
589
|
+
return String.fromCodePoint(Number.parseInt(e.slice(1), 10));
|
|
590
|
+
return { quot: '"', apos: "'", lt: "<", gt: ">", amp: "&" }[e] ?? "";
|
|
591
|
+
});
|
|
592
|
+
}
|
|
593
|
+
function dataStatements(content) {
|
|
594
|
+
const out = [];
|
|
595
|
+
for (let at = content.indexOf("<data"); at !== -1; at = content.indexOf("<data", at + 5)) {
|
|
596
|
+
if (!/[\s/>]/.test(content.charAt(at + 5)))
|
|
597
|
+
continue; // `<data`, not `<datalist`
|
|
598
|
+
const tag = startTagEnd(content, at);
|
|
599
|
+
if (!tag)
|
|
600
|
+
continue;
|
|
601
|
+
const attributes = attributesOf(content.slice(at, tag.end));
|
|
602
|
+
const value = attributes.get("value");
|
|
603
|
+
if (attributes.get("name")?.value !== "manni-meta" || value === undefined)
|
|
604
|
+
continue;
|
|
605
|
+
const open = at + value.at;
|
|
606
|
+
const close = open + value.value.length;
|
|
607
|
+
const raw = value.value;
|
|
608
|
+
const lead = /^\s*/.exec(raw)?.[0] ?? "";
|
|
609
|
+
const rest = decodeEntities(raw.slice(lead.length));
|
|
610
|
+
const word = /^\S+/.exec(rest)?.[0] ?? "";
|
|
611
|
+
const pinned = PIN_TOKEN.test(word);
|
|
612
|
+
out.push({
|
|
613
|
+
line: lineAt(content, at),
|
|
614
|
+
start: at,
|
|
615
|
+
end: tag.end,
|
|
616
|
+
pinAt: open + lead.length,
|
|
617
|
+
closeAt: close,
|
|
618
|
+
pin: pinned ? word : undefined,
|
|
619
|
+
payload: (pinned ? rest.slice(word.length) : rest).trim(),
|
|
620
|
+
broken: "",
|
|
621
|
+
quote: content.charAt(open - 1),
|
|
622
|
+
});
|
|
623
|
+
}
|
|
624
|
+
return out;
|
|
625
|
+
}
|
|
626
|
+
// Text for an XML attribute value quoted with `quote`.
|
|
627
|
+
function escapeAttribute(text, quote) {
|
|
628
|
+
return text
|
|
629
|
+
.replace(/&/g, "&")
|
|
630
|
+
.replace(/</g, "<")
|
|
631
|
+
.replace(quote === "'" ? /'/g : /"/g, quote === "'" ? "'" : """);
|
|
632
|
+
}
|
|
633
|
+
function startTagEnd(source, at) {
|
|
634
|
+
let quote;
|
|
635
|
+
for (let i = at + 1; i < source.length; i++) {
|
|
636
|
+
const ch = source[i];
|
|
637
|
+
if (quote !== undefined) {
|
|
638
|
+
if (ch === quote)
|
|
639
|
+
quote = undefined;
|
|
640
|
+
}
|
|
641
|
+
else if (ch === '"' || ch === "'")
|
|
642
|
+
quote = ch;
|
|
643
|
+
else if (ch === ">")
|
|
644
|
+
return { end: i + 1, selfClosing: source[i - 1] === "/" };
|
|
645
|
+
}
|
|
646
|
+
return undefined;
|
|
647
|
+
}
|
|
648
|
+
function scanXml(content, statements) {
|
|
649
|
+
const root = { kind: "element", name: "#document", start: 0, end: content.length, children: [] };
|
|
650
|
+
let current = root;
|
|
651
|
+
let i = 0;
|
|
652
|
+
const push = (node) => {
|
|
653
|
+
node.parent = current;
|
|
654
|
+
current.children.push(node);
|
|
655
|
+
};
|
|
656
|
+
const skipTo = (needle, from) => {
|
|
657
|
+
const at = content.indexOf(needle, from);
|
|
658
|
+
return at === -1 ? content.length : at + needle.length;
|
|
659
|
+
};
|
|
660
|
+
while (i < content.length) {
|
|
661
|
+
const lt = content.indexOf("<", i);
|
|
662
|
+
if (lt === -1 || lt > i) {
|
|
663
|
+
const end = lt === -1 ? content.length : lt;
|
|
664
|
+
push({ kind: "text", name: "#text", start: i, end, children: [], blank: content.slice(i, end).trim() === "" });
|
|
665
|
+
i = end;
|
|
666
|
+
continue;
|
|
667
|
+
}
|
|
668
|
+
let end;
|
|
669
|
+
if (content.startsWith("<!--", i))
|
|
670
|
+
end = skipTo("-->", i + 4);
|
|
671
|
+
else if (content.startsWith("<![CDATA[", i)) {
|
|
672
|
+
end = skipTo("]]>", i + 9);
|
|
673
|
+
push({ kind: "text", name: "#cdata", start: i, end, children: [] });
|
|
674
|
+
i = end;
|
|
675
|
+
continue;
|
|
676
|
+
}
|
|
677
|
+
else if (content.startsWith("<?", i))
|
|
678
|
+
end = skipTo("?>", i + 2);
|
|
679
|
+
else if (content.startsWith("<!", i)) {
|
|
680
|
+
const bracket = content.indexOf("[", i);
|
|
681
|
+
const gt = content.indexOf(">", i);
|
|
682
|
+
end = bracket !== -1 && gt !== -1 && bracket < gt ? skipTo(">", skipTo("]", bracket)) : skipTo(">", i);
|
|
683
|
+
}
|
|
684
|
+
else if (content.startsWith("</", i)) {
|
|
685
|
+
end = skipTo(">", i);
|
|
686
|
+
current.end = end;
|
|
687
|
+
current = current.parent ?? root;
|
|
688
|
+
i = end;
|
|
689
|
+
continue;
|
|
690
|
+
}
|
|
691
|
+
else {
|
|
692
|
+
const tag = startTagEnd(content, i);
|
|
693
|
+
const name = /^<([^\s/>]+)/.exec(content.slice(i, i + 256))?.[1];
|
|
694
|
+
if (tag === undefined || name === undefined) {
|
|
695
|
+
i += 1;
|
|
696
|
+
continue;
|
|
697
|
+
}
|
|
698
|
+
const node = { kind: "element", name, start: i, end: tag.end, children: [] };
|
|
699
|
+
// DITA's <data name="manni-meta"> carrier is a statement too.
|
|
700
|
+
if (name === "data" && isCarrier(content.slice(i, tag.end)))
|
|
701
|
+
node.statement = true;
|
|
702
|
+
push(node);
|
|
703
|
+
if (!tag.selfClosing) {
|
|
704
|
+
node.end = content.length;
|
|
705
|
+
current = node;
|
|
706
|
+
}
|
|
707
|
+
i = tag.end;
|
|
708
|
+
continue;
|
|
709
|
+
}
|
|
710
|
+
push({ kind: "other", name: "#other", start: i, end, children: [], statement: statements.has(i) });
|
|
711
|
+
i = end;
|
|
712
|
+
}
|
|
713
|
+
return root;
|
|
714
|
+
}
|
|
715
|
+
function* walk(node) {
|
|
716
|
+
yield node;
|
|
717
|
+
for (const child of node.children)
|
|
718
|
+
yield* walk(child);
|
|
719
|
+
}
|
|
720
|
+
function xmlAnchors(content, statements) {
|
|
721
|
+
const byStart = new Map(statements.map((s) => [s.start, s]));
|
|
722
|
+
const tree = scanXml(content, new Set(byStart.keys()));
|
|
723
|
+
const spans = [...walk(tree)].filter((n) => n.statement === true).map(({ start, end }) => ({ start, end }));
|
|
724
|
+
const ignorable = (n) => n.kind === "other" || n.blank === true || n.statement === true;
|
|
725
|
+
const titleOf = (n) => n.children.find((c) => c.kind === "element" && c.name === "title");
|
|
726
|
+
const linesOf = (n) => ({ start: lineAt(content, n.start), end: lineAt(content, Math.max(n.start, n.end - 1)) });
|
|
727
|
+
const out = new Map();
|
|
728
|
+
for (const node of walk(tree)) {
|
|
729
|
+
// An instruction or comment statement, or a `<data>` carrier.
|
|
730
|
+
const s = node.statement ? byStart.get(node.start) : undefined;
|
|
731
|
+
if (!s)
|
|
732
|
+
continue;
|
|
733
|
+
const parent = node.parent ?? tree;
|
|
734
|
+
const index = parent.children.indexOf(node);
|
|
735
|
+
const firstChild = parent !== tree && parent.children.slice(0, index).every(ignorable);
|
|
736
|
+
const next = parent.children.slice(index + 1).find((c) => c.kind === "element" && c.statement !== true);
|
|
737
|
+
const owner = firstChild ? parent : next?.name === "title" ? parent : undefined;
|
|
738
|
+
const title = owner === undefined || owner === tree ? undefined : titleOf(owner);
|
|
739
|
+
if (title)
|
|
740
|
+
out.set(s, linesOf(title));
|
|
741
|
+
else if (firstChild)
|
|
742
|
+
out.set(s, linesOf(parent));
|
|
743
|
+
else if (next)
|
|
744
|
+
out.set(s, linesOf(next));
|
|
745
|
+
}
|
|
746
|
+
return { anchors: out, spans };
|
|
747
|
+
}
|
|
748
|
+
// The anchored lines as a pin hashes them, by manni's `anchoredText`
|
|
749
|
+
// (proposal 0101). Every statement inside the lines is cut out first, the
|
|
750
|
+
// anchoring one and any other, so a tool writing into a statement never
|
|
751
|
+
// changes a pin over it. `spans` are every statement on the page, in document
|
|
752
|
+
// order. A line that held a statement and is blank after the cut is dropped,
|
|
753
|
+
// so moving a statement onto or off its own line keeps the pin. A line blank
|
|
754
|
+
// before the cut stays.
|
|
755
|
+
function anchoredText(content, anchor, spans) {
|
|
756
|
+
const from = Math.max(offsetOfLine(content, anchor.start), content.charCodeAt(0) === 0xfeff ? 1 : 0);
|
|
757
|
+
const nl = content.indexOf("\n", offsetOfLine(content, anchor.end));
|
|
758
|
+
const to = nl === -1 ? content.length : nl;
|
|
759
|
+
let text = "";
|
|
760
|
+
const cuts = [];
|
|
761
|
+
let at = from;
|
|
762
|
+
for (const s of spans) {
|
|
763
|
+
const end = Math.min(s.end, to);
|
|
764
|
+
if (end <= at || s.start >= to)
|
|
765
|
+
continue;
|
|
766
|
+
text += content.slice(at, Math.max(s.start, at));
|
|
767
|
+
cuts.push(text.length);
|
|
768
|
+
at = end;
|
|
769
|
+
}
|
|
770
|
+
text += content.slice(at, to);
|
|
771
|
+
const parts = text.split("\n");
|
|
772
|
+
const out = [];
|
|
773
|
+
let offset = 0;
|
|
774
|
+
parts.forEach((part, i) => {
|
|
775
|
+
const cut = cuts.some((c) => c >= offset && c <= offset + part.length);
|
|
776
|
+
offset += part.length + 1;
|
|
777
|
+
// A CR before an LF is folded, as manni's splitLines folds CRLF.
|
|
778
|
+
const line = (i < parts.length - 1 || to < content.length) && part.endsWith("\r") ? part.slice(0, -1) : part;
|
|
779
|
+
if (!(cut && line.trim() === ""))
|
|
780
|
+
out.push(line);
|
|
781
|
+
});
|
|
782
|
+
return out;
|
|
783
|
+
}
|
|
784
|
+
function writeStatements(content, format, opts) {
|
|
785
|
+
if (!FORMS[format])
|
|
786
|
+
return content;
|
|
787
|
+
const from = format === "markdown" || format === "asciidoc" ? (frontMatter(content)?.end ?? 0) : 0;
|
|
788
|
+
const statements = parseStatements(content, from, format);
|
|
789
|
+
if (format === "dita")
|
|
790
|
+
statements.push(...dataStatements(content));
|
|
791
|
+
if (statements.length === 0)
|
|
792
|
+
return content;
|
|
793
|
+
const { anchors, spans } = format === "dita"
|
|
794
|
+
? xmlAnchors(content, statements)
|
|
795
|
+
: {
|
|
796
|
+
anchors: new Map(statements.map((s) => [s, anchoredLines(content, s.end, format)])),
|
|
797
|
+
spans: statements.map(({ start, end }) => ({ start, end })),
|
|
798
|
+
};
|
|
799
|
+
const edits = [];
|
|
800
|
+
for (const s of statements) {
|
|
801
|
+
const anchor = anchors.get(s);
|
|
802
|
+
if (s.broken) {
|
|
803
|
+
// Only ours is worth a word: another tool's statement isn't ours to fix.
|
|
804
|
+
if (s.payload.includes(TOOL))
|
|
805
|
+
opts.onSkipped?.(s.line, s.broken);
|
|
806
|
+
continue;
|
|
807
|
+
}
|
|
808
|
+
if (!anchor) {
|
|
809
|
+
if (s.payload.includes(TOOL))
|
|
810
|
+
opts.onSkipped?.(s.line, "it anchors nothing: no paragraph, block or element follows it");
|
|
811
|
+
continue;
|
|
812
|
+
}
|
|
813
|
+
let data;
|
|
814
|
+
try {
|
|
815
|
+
data = JSON.parse(s.payload);
|
|
816
|
+
}
|
|
817
|
+
catch {
|
|
818
|
+
continue;
|
|
819
|
+
}
|
|
820
|
+
if (!isRecord(data) || !Array.isArray(data["verified-by"]))
|
|
821
|
+
continue;
|
|
822
|
+
const before = JSON.stringify(data);
|
|
823
|
+
const { any, passed } = fillEntries(data["verified-by"], (e) => (isRecord(e) ? { tool: e.tool, check: e.check, passed: typeof e.passed === "string" ? e.passed : undefined } : undefined), (e) => {
|
|
824
|
+
const entry = e;
|
|
825
|
+
entry.version = opts.version;
|
|
826
|
+
entry.passed = opts.today;
|
|
827
|
+
}, opts);
|
|
828
|
+
if (!any)
|
|
829
|
+
continue;
|
|
830
|
+
const last = oldest(passed);
|
|
831
|
+
if (last !== undefined)
|
|
832
|
+
data["last-verified"] = last;
|
|
833
|
+
const pin = pinOf(anchoredText(content, anchor, spans));
|
|
834
|
+
const json = JSON.stringify(data);
|
|
835
|
+
// A same-day re-run fills the same values over an unchanged block: skip
|
|
836
|
+
// it, so the file stays byte-identical.
|
|
837
|
+
if (pin === s.pin && json === before)
|
|
838
|
+
continue;
|
|
839
|
+
const trailing = /\s*$/.exec(content.slice(s.pinAt, s.closeAt))?.[0] ?? "";
|
|
840
|
+
const text = `${pin} ${json}`;
|
|
841
|
+
edits.push({ start: s.pinAt, end: s.closeAt, text: `${s.quote ? escapeAttribute(text, s.quote) : text}${trailing}` });
|
|
842
|
+
}
|
|
843
|
+
return edits.length === 0 ? content : applyEdits(content, edits);
|
|
844
|
+
}
|
|
845
|
+
// Fill Doc Detective's `verified-by` entries in a page's front matter and its
|
|
846
|
+
// inline `meta` statements. Returns the page unchanged when nothing passed.
|
|
847
|
+
export function applyVerification(content, format, opts) {
|
|
848
|
+
return writeStatements(writeFrontMatter(content, opts), format, opts);
|
|
849
|
+
}
|
|
850
|
+
//# sourceMappingURL=verification.js.map
|