@descent-vtt/spec-brief 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +264 -0
- package/README.md +90 -20
- package/dist/archive.d.ts +52 -2
- package/dist/archive.js +205 -37
- package/dist/archive.js.map +1 -1
- package/dist/brief.d.ts +2 -0
- package/dist/brief.js +5 -1
- package/dist/brief.js.map +1 -1
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +48 -14
- package/dist/cli.js.map +1 -1
- package/dist/collisions.d.ts +43 -12
- package/dist/collisions.js +90 -58
- package/dist/collisions.js.map +1 -1
- package/dist/config.d.ts +11 -0
- package/dist/config.js +58 -10
- package/dist/config.js.map +1 -1
- package/dist/corpus.d.ts +21 -1
- package/dist/corpus.js +51 -2
- package/dist/corpus.js.map +1 -1
- package/dist/engine.d.ts +33 -3
- package/dist/engine.js +112 -9
- package/dist/engine.js.map +1 -1
- package/dist/frontmatter.d.ts +18 -61
- package/dist/frontmatter.js +31 -300
- package/dist/frontmatter.js.map +1 -1
- package/dist/glob.d.ts +77 -54
- package/dist/glob.js +104 -433
- package/dist/glob.js.map +1 -1
- package/dist/index.d.ts +10 -8
- package/dist/index.js +8 -6
- package/dist/index.js.map +1 -1
- package/dist/links.d.ts +1 -1
- package/dist/links.js +3 -4
- package/dist/links.js.map +1 -1
- package/dist/lint.d.ts +33 -2
- package/dist/lint.js +9 -2
- package/dist/lint.js.map +1 -1
- package/dist/markdown.d.ts +26 -23
- package/dist/markdown.js +60 -237
- package/dist/markdown.js.map +1 -1
- package/dist/plugins.d.ts +14 -0
- package/dist/plugins.js +142 -6
- package/dist/plugins.js.map +1 -1
- package/dist/report.d.ts +40 -3
- package/dist/report.js +227 -11
- package/dist/report.js.map +1 -1
- package/dist/rules.d.ts +22 -4
- package/dist/rules.js +175 -45
- package/dist/rules.js.map +1 -1
- package/dist/scaffold.d.ts +1 -1
- package/dist/scaffold.js +10 -9
- package/dist/scaffold.js.map +1 -1
- package/dist/schedule.d.ts +133 -0
- package/dist/schedule.js +265 -0
- package/dist/schedule.js.map +1 -0
- package/dist/scope.d.ts +52 -0
- package/dist/scope.js +56 -0
- package/dist/scope.js.map +1 -0
- package/dist/text.d.ts +13 -0
- package/dist/text.js +23 -0
- package/dist/text.js.map +1 -1
- package/dist/trigger.d.ts +17 -0
- package/dist/trigger.js +49 -0
- package/dist/trigger.js.map +1 -0
- package/dist/types.d.ts +14 -5
- package/dist/types.js.map +1 -1
- package/dist/vendor/spec-core/markdown/frontmatter.d.ts +103 -0
- package/dist/vendor/spec-core/markdown/frontmatter.js +459 -0
- package/dist/vendor/spec-core/markdown/frontmatter.js.map +1 -0
- package/dist/vendor/spec-core/markdown/headings.d.ts +45 -0
- package/dist/vendor/spec-core/markdown/headings.js +199 -0
- package/dist/vendor/spec-core/markdown/headings.js.map +1 -0
- package/dist/vendor/spec-core/markdown/index.d.ts +5 -0
- package/dist/vendor/spec-core/markdown/index.js +4 -0
- package/dist/vendor/spec-core/markdown/index.js.map +1 -0
- package/dist/vendor/spec-core/markdown/layout.d.ts +39 -0
- package/dist/vendor/spec-core/markdown/layout.js +28 -0
- package/dist/vendor/spec-core/markdown/layout.js.map +1 -0
- package/dist/vendor/spec-core/markdown/links.d.ts +22 -0
- package/dist/vendor/spec-core/markdown/links.js +364 -0
- package/dist/vendor/spec-core/markdown/links.js.map +1 -0
- package/dist/vendor/spec-core/markdown/lists.d.ts +22 -0
- package/dist/vendor/spec-core/markdown/lists.js +127 -0
- package/dist/vendor/spec-core/markdown/lists.js.map +1 -0
- package/dist/vendor/spec-core/markdown/scan.d.ts +36 -0
- package/dist/vendor/spec-core/markdown/scan.js +439 -0
- package/dist/vendor/spec-core/markdown/scan.js.map +1 -0
- package/dist/vendor/spec-core/markdown/syntax.d.ts +75 -0
- package/dist/vendor/spec-core/markdown/syntax.js +181 -0
- package/dist/vendor/spec-core/markdown/syntax.js.map +1 -0
- package/dist/vendor/spec-core/markdown/tables.d.ts +18 -0
- package/dist/vendor/spec-core/markdown/tables.js +86 -0
- package/dist/vendor/spec-core/markdown/tables.js.map +1 -0
- package/dist/vendor/spec-core/markdown/types.d.ts +287 -0
- package/dist/vendor/spec-core/markdown/types.js +9 -0
- package/dist/vendor/spec-core/markdown/types.js.map +1 -0
- package/dist/vendor/spec-core/path/index.d.ts +1 -0
- package/dist/vendor/spec-core/path/index.js +2 -0
- package/dist/vendor/spec-core/path/index.js.map +1 -0
- package/dist/vendor/spec-core/path/posix.d.ts +83 -0
- package/dist/vendor/spec-core/path/posix.js +150 -0
- package/dist/vendor/spec-core/path/posix.js.map +1 -0
- package/dist/vendor/spec-core/pattern/automaton.d.ts +129 -0
- package/dist/vendor/spec-core/pattern/automaton.js +340 -0
- package/dist/vendor/spec-core/pattern/automaton.js.map +1 -0
- package/dist/vendor/spec-core/pattern/charset.d.ts +58 -0
- package/dist/vendor/spec-core/pattern/charset.js +116 -0
- package/dist/vendor/spec-core/pattern/charset.js.map +1 -0
- package/dist/vendor/spec-core/pattern/glob.d.ts +121 -0
- package/dist/vendor/spec-core/pattern/glob.js +481 -0
- package/dist/vendor/spec-core/pattern/glob.js.map +1 -0
- package/dist/vendor/spec-core/pattern/index.d.ts +6 -0
- package/dist/vendor/spec-core/pattern/index.js +4 -0
- package/dist/vendor/spec-core/pattern/index.js.map +1 -0
- package/dist/vendor/spec-core/pattern/regex.d.ts +67 -0
- package/dist/vendor/spec-core/pattern/regex.js +813 -0
- package/dist/vendor/spec-core/pattern/regex.js.map +1 -0
- package/dist/vendor/spec-core/text/index.d.ts +2 -0
- package/dist/vendor/spec-core/text/index.js +2 -0
- package/dist/vendor/spec-core/text/index.js.map +1 -0
- package/dist/vendor/spec-core/text/lines.d.ts +72 -0
- package/dist/vendor/spec-core/text/lines.js +156 -0
- package/dist/vendor/spec-core/text/lines.js.map +1 -0
- package/package.json +3 -2
- package/schema.json +17 -0
- package/src/vendor/spec-core/LICENSE +21 -0
- package/src/vendor/spec-core/README.md +8 -0
package/dist/markdown.js
CHANGED
|
@@ -1,185 +1,70 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* A
|
|
3
|
-
*
|
|
2
|
+
* A brief's Markdown: spec-core's scanner, copied into `src/vendor/spec-core/`,
|
|
3
|
+
* read in spec-brief's dialect.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
5
|
+
* The scanner is exact about what is code and what is a comment, as CommonMark
|
|
6
|
+
* has it, and reports every heading, list item and link it finds. What of that
|
|
7
|
+
* is a brief's structure is decided here (ADR-0001, amended 2026-09-26):
|
|
8
|
+
*
|
|
9
|
+
* - A heading is an ATX heading outside a block quote. Prose over a `---` is a
|
|
10
|
+
* setext heading to a renderer, and a brief that separates its parts that way
|
|
11
|
+
* must not grow a section from it; a heading in a block quote is quoted.
|
|
12
|
+
* - A task is a list item outside a block quote with a box GFM renders: a
|
|
13
|
+
* space, `x` or `X`. The scanner reads `[~]`, `[?]` and others as well,
|
|
14
|
+
* which a brief does not write.
|
|
15
|
+
* - A link destination is one written in the link or in a definition: the
|
|
16
|
+
* inline form and the definition, images included, an image inside a link's
|
|
17
|
+
* text too. A reference is rewritten through its definition, once.
|
|
18
|
+
*
|
|
19
|
+
* Everything here is in 0-based lines of the brief's `lines`, and columns
|
|
20
|
+
* within them.
|
|
13
21
|
*/
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
function blank(length) {
|
|
20
|
-
return ' '.repeat(length);
|
|
22
|
+
import { linesOf, scanMarkdown } from './vendor/spec-core/markdown/index.js';
|
|
23
|
+
import { textOfLines } from './text.js';
|
|
24
|
+
/** Whether a checkbox is one a task is written with. */
|
|
25
|
+
function isBox(checkbox) {
|
|
26
|
+
return checkbox === ' ' || checkbox === 'x' || checkbox === 'X';
|
|
21
27
|
}
|
|
22
28
|
/**
|
|
23
|
-
*
|
|
24
|
-
*
|
|
29
|
+
* The links that write their destination - inline links and images, and
|
|
30
|
+
* definitions - with offsets into the text scanned `at` that offset.
|
|
31
|
+
*
|
|
32
|
+
* A link's text may hold an image, `[](d.md)`, whose
|
|
33
|
+
* destination moves with the brief as the link's does. spec-core reads the
|
|
34
|
+
* link and resumes after it, so its text is read again here. Each text is
|
|
35
|
+
* shorter than the one it came from, so the reading ends.
|
|
25
36
|
*/
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
if (i < from) {
|
|
34
|
-
prose.push(blank(line.length));
|
|
35
|
-
masked.push(blank(line.length));
|
|
36
|
-
continue;
|
|
37
|
-
}
|
|
38
|
-
if (fence !== null) {
|
|
39
|
-
prose.push(line);
|
|
40
|
-
masked.push(blank(line.length));
|
|
41
|
-
const close = FENCE.exec(line);
|
|
42
|
-
if (close !== null) {
|
|
43
|
-
const [, run = '', rest = ''] = close;
|
|
44
|
-
if (run.charAt(0) === fence.char && run.length >= fence.length && rest.trim() === '')
|
|
45
|
-
fence = null;
|
|
46
|
-
}
|
|
47
|
-
continue;
|
|
48
|
-
}
|
|
49
|
-
if (!inComment) {
|
|
50
|
-
const open = FENCE.exec(line);
|
|
51
|
-
const [, run = '', info = ''] = open ?? [];
|
|
52
|
-
if (open !== null && !(run.charAt(0) === '`' && info.includes('`'))) {
|
|
53
|
-
fence = { char: run.charAt(0), length: run.length };
|
|
54
|
-
prose.push(line);
|
|
55
|
-
masked.push(blank(line.length));
|
|
56
|
-
continue;
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
let proseLine = '';
|
|
60
|
-
let maskedLine = '';
|
|
61
|
-
let j = 0;
|
|
62
|
-
while (j < line.length) {
|
|
63
|
-
if (inComment) {
|
|
64
|
-
const close = line.indexOf('-->', j);
|
|
65
|
-
const stop = close < 0 ? line.length : close + 3;
|
|
66
|
-
proseLine += blank(stop - j);
|
|
67
|
-
maskedLine += blank(stop - j);
|
|
68
|
-
inComment = close < 0;
|
|
69
|
-
j = stop;
|
|
70
|
-
continue;
|
|
71
|
-
}
|
|
72
|
-
if (line.startsWith('<!--', j)) {
|
|
73
|
-
inComment = true;
|
|
74
|
-
proseLine += blank(4);
|
|
75
|
-
maskedLine += blank(4);
|
|
76
|
-
j += 4;
|
|
77
|
-
continue;
|
|
78
|
-
}
|
|
79
|
-
if (line.charAt(j) === '`') {
|
|
80
|
-
const run = backtickRun(line, j);
|
|
81
|
-
const close = closingRun(line, j + run, run);
|
|
82
|
-
if (close >= 0) {
|
|
83
|
-
const span = line.slice(j, close + run);
|
|
84
|
-
proseLine += span;
|
|
85
|
-
maskedLine += blank(span.length);
|
|
86
|
-
j = close + run;
|
|
87
|
-
continue;
|
|
88
|
-
}
|
|
89
|
-
proseLine += line.slice(j, j + run);
|
|
90
|
-
maskedLine += line.slice(j, j + run);
|
|
91
|
-
j += run;
|
|
92
|
-
continue;
|
|
93
|
-
}
|
|
94
|
-
proseLine += line.charAt(j);
|
|
95
|
-
maskedLine += line.charAt(j);
|
|
96
|
-
j += 1;
|
|
97
|
-
}
|
|
98
|
-
prose.push(proseLine);
|
|
99
|
-
masked.push(maskedLine);
|
|
100
|
-
}
|
|
101
|
-
const headings = findHeadings(lines, masked);
|
|
102
|
-
return { lines, prose, masked, headings, tasks: findTasks(lines, masked, headings) };
|
|
103
|
-
}
|
|
104
|
-
function backtickRun(line, at) {
|
|
105
|
-
let end = at;
|
|
106
|
-
while (line.charAt(end) === '`')
|
|
107
|
-
end += 1;
|
|
108
|
-
return end - at;
|
|
109
|
-
}
|
|
110
|
-
/** The index of a run of exactly `length` backticks at or after `from`, or -1. */
|
|
111
|
-
function closingRun(line, from, length) {
|
|
112
|
-
let i = from;
|
|
113
|
-
while (i < line.length) {
|
|
114
|
-
if (line.charAt(i) !== '`') {
|
|
115
|
-
i += 1;
|
|
116
|
-
continue;
|
|
117
|
-
}
|
|
118
|
-
const run = backtickRun(line, i);
|
|
119
|
-
if (run === length)
|
|
120
|
-
return i;
|
|
121
|
-
i += run;
|
|
122
|
-
}
|
|
123
|
-
return -1;
|
|
124
|
-
}
|
|
125
|
-
function findHeadings(lines, masked) {
|
|
126
|
-
const headings = [];
|
|
127
|
-
for (let i = 0; i < masked.length; i += 1) {
|
|
128
|
-
const match = HEADING.exec(masked[i]);
|
|
129
|
-
if (!match)
|
|
130
|
-
continue;
|
|
131
|
-
const level = match[1].length;
|
|
132
|
-
const original = lines[i];
|
|
133
|
-
const text = original
|
|
134
|
-
.replace(/^ {0,3}#{1,6}/, '')
|
|
135
|
-
.replace(/[ \t]+#+[ \t]*$/, '')
|
|
136
|
-
.trim();
|
|
137
|
-
headings.push({ line: i, level, text });
|
|
138
|
-
}
|
|
139
|
-
return headings;
|
|
140
|
-
}
|
|
141
|
-
function indentOf(line) {
|
|
142
|
-
return line.length - line.trimStart().length;
|
|
143
|
-
}
|
|
144
|
-
function findTasks(lines, masked, headings) {
|
|
145
|
-
const headingLines = new Set(headings.map((h) => h.line));
|
|
146
|
-
const tasks = [];
|
|
147
|
-
for (let i = 0; i < masked.length; i += 1) {
|
|
148
|
-
const match = TASK.exec(masked[i]);
|
|
149
|
-
if (!match)
|
|
150
|
-
continue;
|
|
151
|
-
const indent = match[1].length;
|
|
152
|
-
const original = TASK.exec(lines[i]);
|
|
153
|
-
const text = (original?.[3] ?? '').trim();
|
|
154
|
-
tasks.push({ line: i, end: itemEnd(lines, masked, headingLines, i, indent), checked: match[2] !== ' ', text });
|
|
155
|
-
}
|
|
156
|
-
return tasks;
|
|
37
|
+
function written(core, at) {
|
|
38
|
+
return core.links.flatMap((link) => {
|
|
39
|
+
const inner = written(scanMarkdown(link.text), at + core.text.indexOf(link.text, link.start));
|
|
40
|
+
if (link.form !== 'inline' && link.form !== 'definition')
|
|
41
|
+
return inner;
|
|
42
|
+
return [{ ...link, targetStart: at + link.targetStart, targetEnd: at + link.targetEnd }, ...inner];
|
|
43
|
+
});
|
|
157
44
|
}
|
|
158
|
-
/**
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
}
|
|
182
|
-
return last + 1;
|
|
45
|
+
/** Scans a brief's lines, which carry no byte-order mark. */
|
|
46
|
+
export function scan(lines) {
|
|
47
|
+
const core = scanMarkdown(textOfLines(lines));
|
|
48
|
+
const unquoted = (line) => core.lines[line - 1].quoteDepth === 0;
|
|
49
|
+
return {
|
|
50
|
+
lines,
|
|
51
|
+
prose: linesOf(core, 'prose'),
|
|
52
|
+
masked: linesOf(core, 'structure'),
|
|
53
|
+
headings: core.headings
|
|
54
|
+
.filter((h) => h.form === 'atx' && unquoted(h.line))
|
|
55
|
+
.map((h) => ({ line: h.line - 1, level: h.level, text: h.text })),
|
|
56
|
+
tasks: core.listItems
|
|
57
|
+
.filter((item) => isBox(item.checkbox) && item.quoteDepth === 0)
|
|
58
|
+
.map((item) => ({ line: item.line - 1, end: item.endLine, checked: item.checkbox !== ' ', text: item.firstLine })),
|
|
59
|
+
links: written(core, 0)
|
|
60
|
+
.map((link) => {
|
|
61
|
+
// A link's text may run over lines; its destination is on the last.
|
|
62
|
+
const at = core.index.positionAt(link.targetStart);
|
|
63
|
+
const start = at.column - 1;
|
|
64
|
+
return { line: at.line - 1, start, end: start + link.targetEnd - link.targetStart, target: link.target };
|
|
65
|
+
})
|
|
66
|
+
.sort((a, b) => a.line - b.line || a.start - b.start),
|
|
67
|
+
};
|
|
183
68
|
}
|
|
184
69
|
/** Sections: every heading below the title, running to the next heading at its level or above. */
|
|
185
70
|
export function sectionsOf(result) {
|
|
@@ -205,68 +90,6 @@ export function sectionsOf(result) {
|
|
|
205
90
|
export function titleOf(result) {
|
|
206
91
|
return result.headings.find((h) => h.level === 1);
|
|
207
92
|
}
|
|
208
|
-
const DEFINITION = /^ {0,3}\[(?!\^)[^\]]+\]:[ \t]*/;
|
|
209
|
-
/** What may follow a definition's destination: nothing, or a title. */
|
|
210
|
-
const TITLE = /^(?:"[^"]*"|'[^']*'|\([^)]*\))?$/;
|
|
211
|
-
/**
|
|
212
|
-
* Link and image destinations outside code and comments: `[text](dest)`,
|
|
213
|
-
* ``, and reference definitions `[label]: dest`.
|
|
214
|
-
*/
|
|
215
|
-
export function linksOf(result) {
|
|
216
|
-
const links = [];
|
|
217
|
-
result.masked.forEach((masked, line) => {
|
|
218
|
-
const original = result.lines[line];
|
|
219
|
-
const definition = DEFINITION.exec(masked);
|
|
220
|
-
if (definition) {
|
|
221
|
-
// `[Note]: this matters` is prose that looks like a definition; a real one
|
|
222
|
-
// has nothing after its destination but an optional title.
|
|
223
|
-
const found = readDestination(masked, original, definition[0].length);
|
|
224
|
-
if (found && TITLE.test(masked.slice(found.end + (masked.charAt(found.end) === '>' ? 1 : 0)).trim())) {
|
|
225
|
-
links.push({ line, ...found });
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
let from = 0;
|
|
229
|
-
for (;;) {
|
|
230
|
-
const at = masked.indexOf('](', from);
|
|
231
|
-
if (at < 0)
|
|
232
|
-
break;
|
|
233
|
-
let start = at + 2;
|
|
234
|
-
while (masked.charAt(start) === ' ' || masked.charAt(start) === '\t')
|
|
235
|
-
start += 1;
|
|
236
|
-
const found = readDestination(masked, original, start);
|
|
237
|
-
if (found)
|
|
238
|
-
links.push({ line, ...found });
|
|
239
|
-
from = at + 2;
|
|
240
|
-
}
|
|
241
|
-
});
|
|
242
|
-
return links;
|
|
243
|
-
}
|
|
244
|
-
function readDestination(masked, original, start) {
|
|
245
|
-
if (masked.charAt(start) === '<') {
|
|
246
|
-
const close = masked.indexOf('>', start + 1);
|
|
247
|
-
if (close < 0)
|
|
248
|
-
return undefined;
|
|
249
|
-
return { start: start + 1, end: close, target: original.slice(start + 1, close) };
|
|
250
|
-
}
|
|
251
|
-
let depth = 0;
|
|
252
|
-
let end = start;
|
|
253
|
-
while (end < masked.length) {
|
|
254
|
-
const ch = masked.charAt(end);
|
|
255
|
-
if (ch === ' ' || ch === '\t')
|
|
256
|
-
break;
|
|
257
|
-
if (ch === '(')
|
|
258
|
-
depth += 1;
|
|
259
|
-
if (ch === ')') {
|
|
260
|
-
if (depth === 0)
|
|
261
|
-
break;
|
|
262
|
-
depth -= 1;
|
|
263
|
-
}
|
|
264
|
-
end += 1;
|
|
265
|
-
}
|
|
266
|
-
if (end === start)
|
|
267
|
-
return undefined;
|
|
268
|
-
return { start, end, target: original.slice(start, end) };
|
|
269
|
-
}
|
|
270
93
|
/** Whether a line carries anything once comments are removed. */
|
|
271
94
|
export function hasContent(proseLine) {
|
|
272
95
|
return proseLine.trim().length > 0;
|
package/dist/markdown.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"markdown.js","sourceRoot":"","sources":["../src/markdown.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * A structural Markdown scanner: code, comments, headings, task items and link\n * destinations, each with its line.\n *\n * Not a CommonMark parser, and it does not need to be. Nothing here renders.\n * What must be exactly right is knowing what is code or a comment, because a\n * heading inside a fenced block is not a section and a template hint inside\n * `<!-- -->` is not content. Everything else is deliberately simple: ATX\n * headings only (a setext underline and a front-matter delimiter are the same\n * three characters), fences at any indentation, and no indented code blocks,\n * because inside a list four spaces of indentation is a continuation far more\n * often than code.\n */\n\nexport interface Heading {\n /** 0-based line. */\n readonly line: number;\n readonly level: number;\n readonly text: string;\n}\n\nexport interface Section {\n readonly heading: Heading;\n /** 0-based line after the section's last line. The body starts on the line after the heading. */\n readonly end: number;\n}\n\nexport interface TaskItem {\n /** 0-based line of the box. */\n readonly line: number;\n /** 0-based line after the item's last continuation line. */\n readonly end: number;\n readonly checked: boolean;\n readonly text: string;\n}\n\nexport interface LinkDestination {\n /** 0-based line. */\n readonly line: number;\n /** Columns of the destination within the line, end exclusive. */\n readonly start: number;\n readonly end: number;\n readonly target: string;\n}\n\nexport interface Scan {\n readonly lines: readonly string[];\n /** Lines with comments blanked and code kept: what counts as content. */\n readonly prose: readonly string[];\n /** Lines with comments, code spans and fenced blocks blanked: what counts as structure. */\n readonly masked: readonly string[];\n readonly headings: readonly Heading[];\n readonly tasks: readonly TaskItem[];\n}\n\n// Any indentation: a fence inside a nested list item is indented with the item.\nconst FENCE = /^[ \\t]*(`{3,}|~{3,})(.*)$/;\nconst HEADING = /^ {0,3}(#{1,6})(?:[ \\t]+|$)/;\nconst TASK = /^([ \\t]*)(?:[-*+]|\\d{1,9}[.)])[ \\t]+\\[([ xX])\\](?:[ \\t]+(.*))?$/;\nconst LIST_ITEM = /^[ \\t]*(?:[-*+]|\\d{1,9}[.)])(?:[ \\t]|$)/;\n\nfunction blank(length: number): string {\n return ' '.repeat(length);\n}\n\n/**\n * Scans `lines` from line `from`; earlier lines (front matter) are blanked in\n * both masks and contribute nothing.\n */\nexport function scan(lines: readonly string[], from = 0): Scan {\n const prose: string[] = [];\n const masked: string[] = [];\n let fence: { readonly char: string; readonly length: number } | null = null;\n let inComment = false;\n\n for (let i = 0; i < lines.length; i += 1) {\n const line = lines[i] as string;\n if (i < from) {\n prose.push(blank(line.length));\n masked.push(blank(line.length));\n continue;\n }\n if (fence !== null) {\n prose.push(line);\n masked.push(blank(line.length));\n const close = FENCE.exec(line);\n if (close !== null) {\n const [, run = '', rest = ''] = close;\n if (run.charAt(0) === fence.char && run.length >= fence.length && rest.trim() === '') fence = null;\n }\n continue;\n }\n if (!inComment) {\n const open = FENCE.exec(line);\n const [, run = '', info = ''] = open ?? [];\n if (open !== null && !(run.charAt(0) === '`' && info.includes('`'))) {\n fence = { char: run.charAt(0), length: run.length };\n prose.push(line);\n masked.push(blank(line.length));\n continue;\n }\n }\n\n let proseLine = '';\n let maskedLine = '';\n let j = 0;\n while (j < line.length) {\n if (inComment) {\n const close = line.indexOf('-->', j);\n const stop = close < 0 ? line.length : close + 3;\n proseLine += blank(stop - j);\n maskedLine += blank(stop - j);\n inComment = close < 0;\n j = stop;\n continue;\n }\n if (line.startsWith('<!--', j)) {\n inComment = true;\n proseLine += blank(4);\n maskedLine += blank(4);\n j += 4;\n continue;\n }\n if (line.charAt(j) === '`') {\n const run = backtickRun(line, j);\n const close = closingRun(line, j + run, run);\n if (close >= 0) {\n const span = line.slice(j, close + run);\n proseLine += span;\n maskedLine += blank(span.length);\n j = close + run;\n continue;\n }\n proseLine += line.slice(j, j + run);\n maskedLine += line.slice(j, j + run);\n j += run;\n continue;\n }\n proseLine += line.charAt(j);\n maskedLine += line.charAt(j);\n j += 1;\n }\n prose.push(proseLine);\n masked.push(maskedLine);\n }\n\n const headings = findHeadings(lines, masked);\n return { lines, prose, masked, headings, tasks: findTasks(lines, masked, headings) };\n}\n\nfunction backtickRun(line: string, at: number): number {\n let end = at;\n while (line.charAt(end) === '`') end += 1;\n return end - at;\n}\n\n/** The index of a run of exactly `length` backticks at or after `from`, or -1. */\nfunction closingRun(line: string, from: number, length: number): number {\n let i = from;\n while (i < line.length) {\n if (line.charAt(i) !== '`') {\n i += 1;\n continue;\n }\n const run = backtickRun(line, i);\n if (run === length) return i;\n i += run;\n }\n return -1;\n}\n\nfunction findHeadings(lines: readonly string[], masked: readonly string[]): Heading[] {\n const headings: Heading[] = [];\n for (let i = 0; i < masked.length; i += 1) {\n const match = HEADING.exec(masked[i] as string);\n if (!match) continue;\n const level = (match[1] as string).length;\n const original = lines[i] as string;\n const text = original\n .replace(/^ {0,3}#{1,6}/, '')\n .replace(/[ \\t]+#+[ \\t]*$/, '')\n .trim();\n headings.push({ line: i, level, text });\n }\n return headings;\n}\n\nfunction indentOf(line: string): number {\n return line.length - line.trimStart().length;\n}\n\nfunction findTasks(lines: readonly string[], masked: readonly string[], headings: readonly Heading[]): TaskItem[] {\n const headingLines = new Set(headings.map((h) => h.line));\n const tasks: TaskItem[] = [];\n for (let i = 0; i < masked.length; i += 1) {\n const match = TASK.exec(masked[i] as string);\n if (!match) continue;\n const indent = (match[1] as string).length;\n const original = TASK.exec(lines[i] as string);\n const text = (original?.[3] ?? '').trim();\n tasks.push({ line: i, end: itemEnd(lines, masked, headingLines, i, indent), checked: match[2] !== ' ', text });\n }\n return tasks;\n}\n\n/**\n * Where the list item starting at `start` ends: after its last line indented\n * past the marker, or its last lazy continuation line - text directly under\n * the item with no blank line between, which CommonMark folds into the item's\n * paragraph.\n */\nfunction itemEnd(\n lines: readonly string[],\n masked: readonly string[],\n headingLines: ReadonlySet<number>,\n start: number,\n indent: number,\n): number {\n let last = start;\n let previousBlank = false;\n for (let k = start + 1; k < lines.length; k += 1) {\n const line = lines[k] as string;\n if (headingLines.has(k)) break;\n if (line.trim() === '') {\n previousBlank = true;\n continue;\n }\n const nested = indentOf(line) > indent;\n const lazy = !previousBlank && !LIST_ITEM.test(masked[k] as string) && !FENCE.test(line) && !/^\\s*>/.test(line);\n if (!nested && !lazy) break;\n last = k;\n previousBlank = false;\n }\n return last + 1;\n}\n\n/** Sections: every heading below the title, running to the next heading at its level or above. */\nexport function sectionsOf(result: Scan): Section[] {\n const sections: Section[] = [];\n const { headings } = result;\n for (let h = 0; h < headings.length; h += 1) {\n const heading = headings[h] as Heading;\n if (heading.level < 2) continue;\n let end = result.lines.length;\n for (let k = h + 1; k < headings.length; k += 1) {\n const next = headings[k] as Heading;\n if (next.level <= heading.level) {\n end = next.line;\n break;\n }\n }\n sections.push({ heading, end });\n }\n return sections;\n}\n\n/** The first level-one heading, which is what a reader takes as the title. */\nexport function titleOf(result: Scan): Heading | undefined {\n return result.headings.find((h) => h.level === 1);\n}\n\nconst DEFINITION = /^ {0,3}\\[(?!\\^)[^\\]]+\\]:[ \\t]*/;\n/** What may follow a definition's destination: nothing, or a title. */\nconst TITLE = /^(?:\"[^\"]*\"|'[^']*'|\\([^)]*\\))?$/;\n\n/**\n * Link and image destinations outside code and comments: `[text](dest)`,\n * ``, and reference definitions `[label]: dest`.\n */\nexport function linksOf(result: Scan): LinkDestination[] {\n const links: LinkDestination[] = [];\n result.masked.forEach((masked, line) => {\n const original = result.lines[line] as string;\n const definition = DEFINITION.exec(masked);\n if (definition) {\n // `[Note]: this matters` is prose that looks like a definition; a real one\n // has nothing after its destination but an optional title.\n const found = readDestination(masked, original, definition[0].length);\n if (found && TITLE.test(masked.slice(found.end + (masked.charAt(found.end) === '>' ? 1 : 0)).trim())) {\n links.push({ line, ...found });\n }\n }\n let from = 0;\n for (;;) {\n const at = masked.indexOf('](', from);\n if (at < 0) break;\n let start = at + 2;\n while (masked.charAt(start) === ' ' || masked.charAt(start) === '\\t') start += 1;\n const found = readDestination(masked, original, start);\n if (found) links.push({ line, ...found });\n from = at + 2;\n }\n });\n return links;\n}\n\nfunction readDestination(\n masked: string,\n original: string,\n start: number,\n): { start: number; end: number; target: string } | undefined {\n if (masked.charAt(start) === '<') {\n const close = masked.indexOf('>', start + 1);\n if (close < 0) return undefined;\n return { start: start + 1, end: close, target: original.slice(start + 1, close) };\n }\n let depth = 0;\n let end = start;\n while (end < masked.length) {\n const ch = masked.charAt(end);\n if (ch === ' ' || ch === '\\t') break;\n if (ch === '(') depth += 1;\n if (ch === ')') {\n if (depth === 0) break;\n depth -= 1;\n }\n end += 1;\n }\n if (end === start) return undefined;\n return { start, end, target: original.slice(start, end) };\n}\n\n/** Whether a line carries anything once comments are removed. */\nexport function hasContent(proseLine: string): boolean {\n return proseLine.trim().length > 0;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"markdown.js","sourceRoot":"","sources":["../src/markdown.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * A brief's Markdown: spec-core's scanner, copied into `src/vendor/spec-core/`,\n * read in spec-brief's dialect.\n *\n * The scanner is exact about what is code and what is a comment, as CommonMark\n * has it, and reports every heading, list item and link it finds. What of that\n * is a brief's structure is decided here (ADR-0001, amended 2026-09-26):\n *\n * - A heading is an ATX heading outside a block quote. Prose over a `---` is a\n * setext heading to a renderer, and a brief that separates its parts that way\n * must not grow a section from it; a heading in a block quote is quoted.\n * - A task is a list item outside a block quote with a box GFM renders: a\n * space, `x` or `X`. The scanner reads `[~]`, `[?]` and others as well,\n * which a brief does not write.\n * - A link destination is one written in the link or in a definition: the\n * inline form and the definition, images included, an image inside a link's\n * text too. A reference is rewritten through its definition, once.\n *\n * Everything here is in 0-based lines of the brief's `lines`, and columns\n * within them.\n */\n\nimport { type Link, linesOf, type MarkdownScan, scanMarkdown, type ScannedLine } from './vendor/spec-core/markdown/index.js';\nimport { textOfLines } from './text.js';\n\nexport interface Heading {\n /** 0-based line. */\n readonly line: number;\n readonly level: number;\n /** As a reader sees it: without a closing sequence or a comment. */\n readonly text: string;\n}\n\nexport interface Section {\n readonly heading: Heading;\n /** 0-based line after the section's last line. The body starts on the line after the heading. */\n readonly end: number;\n}\n\nexport interface TaskItem {\n /** 0-based line of the box. */\n readonly line: number;\n /** 0-based line after the item's last line: its continuations and what is nested under it. */\n readonly end: number;\n readonly checked: boolean;\n readonly text: string;\n}\n\nexport interface LinkDestination {\n /** 0-based line. */\n readonly line: number;\n /** Columns of the destination within the line, end exclusive. */\n readonly start: number;\n readonly end: number;\n readonly target: string;\n}\n\nexport interface Scan {\n readonly lines: readonly string[];\n /** Lines with front matter and comments blanked, and code kept: what counts as content. */\n readonly prose: readonly string[];\n /** Lines with front matter, code, raw-text HTML and comments blanked: what counts as structure. */\n readonly masked: readonly string[];\n readonly headings: readonly Heading[];\n readonly tasks: readonly TaskItem[];\n /** Destinations outside code and comments, in the order of their lines and columns. */\n readonly links: readonly LinkDestination[];\n}\n\n/** Whether a checkbox is one a task is written with. */\nfunction isBox(checkbox: string | null): boolean {\n return checkbox === ' ' || checkbox === 'x' || checkbox === 'X';\n}\n\n/**\n * The links that write their destination - inline links and images, and\n * definitions - with offsets into the text scanned `at` that offset.\n *\n * A link's text may hold an image, `[](d.md)`, whose\n * destination moves with the brief as the link's does. spec-core reads the\n * link and resumes after it, so its text is read again here. Each text is\n * shorter than the one it came from, so the reading ends.\n */\nfunction written(core: MarkdownScan, at: number): Link[] {\n return core.links.flatMap((link) => {\n const inner = written(scanMarkdown(link.text), at + core.text.indexOf(link.text, link.start));\n if (link.form !== 'inline' && link.form !== 'definition') return inner;\n return [{ ...link, targetStart: at + link.targetStart, targetEnd: at + link.targetEnd }, ...inner];\n });\n}\n\n/** Scans a brief's lines, which carry no byte-order mark. */\nexport function scan(lines: readonly string[]): Scan {\n const core = scanMarkdown(textOfLines(lines));\n const unquoted = (line: number): boolean => (core.lines[line - 1] as ScannedLine).quoteDepth === 0;\n return {\n lines,\n prose: linesOf(core, 'prose'),\n masked: linesOf(core, 'structure'),\n headings: core.headings\n .filter((h) => h.form === 'atx' && unquoted(h.line))\n .map((h) => ({ line: h.line - 1, level: h.level, text: h.text })),\n tasks: core.listItems\n .filter((item) => isBox(item.checkbox) && item.quoteDepth === 0)\n .map((item) => ({ line: item.line - 1, end: item.endLine, checked: item.checkbox !== ' ', text: item.firstLine })),\n links: written(core, 0)\n .map((link) => {\n // A link's text may run over lines; its destination is on the last.\n const at = core.index.positionAt(link.targetStart);\n const start = at.column - 1;\n return { line: at.line - 1, start, end: start + link.targetEnd - link.targetStart, target: link.target };\n })\n .sort((a, b) => a.line - b.line || a.start - b.start),\n };\n}\n\n/** Sections: every heading below the title, running to the next heading at its level or above. */\nexport function sectionsOf(result: Scan): Section[] {\n const sections: Section[] = [];\n const { headings } = result;\n for (let h = 0; h < headings.length; h += 1) {\n const heading = headings[h] as Heading;\n if (heading.level < 2) continue;\n let end = result.lines.length;\n for (let k = h + 1; k < headings.length; k += 1) {\n const next = headings[k] as Heading;\n if (next.level <= heading.level) {\n end = next.line;\n break;\n }\n }\n sections.push({ heading, end });\n }\n return sections;\n}\n\n/** The first level-one heading, which is what a reader takes as the title. */\nexport function titleOf(result: Scan): Heading | undefined {\n return result.headings.find((h) => h.level === 1);\n}\n\n/** Whether a line carries anything once comments are removed. */\nexport function hasContent(proseLine: string): boolean {\n return proseLine.trim().length > 0;\n}\n"]}
|
package/dist/plugins.d.ts
CHANGED
|
@@ -9,8 +9,22 @@
|
|
|
9
9
|
* configuration does; the configuration belongs to the repository, and so
|
|
10
10
|
* does that trust.
|
|
11
11
|
*/
|
|
12
|
+
import type { Waiver } from './archive.js';
|
|
12
13
|
import type { PluginReference } from './config.js';
|
|
13
14
|
import type { Plugin } from './lint.js';
|
|
15
|
+
/**
|
|
16
|
+
* The file a package's "exports" gives a subpath (`.` or `./x`) under the
|
|
17
|
+
* conditions of an import, or `null` when it gives none: Node's
|
|
18
|
+
* PACKAGE_EXPORTS_RESOLVE. An exact key wins; then the "*" pattern with the
|
|
19
|
+
* longest prefix, and of those the longest key.
|
|
20
|
+
*/
|
|
21
|
+
export declare function exportsTarget(exports: unknown, subpath: string): string | null;
|
|
14
22
|
/** Checks the shape a module exported, since nothing typed it on the way in. */
|
|
15
23
|
export declare function asPlugin(value: unknown, module: string, options: unknown): Plugin;
|
|
24
|
+
/**
|
|
25
|
+
* Checks what a `waive` hook returned: a list of `{ rule, path, reason }`,
|
|
26
|
+
* each a string. A hook that returns nothing waives nothing: a function that
|
|
27
|
+
* found no ruling and fell off its end has answered, not failed.
|
|
28
|
+
*/
|
|
29
|
+
export declare function asWaivers(value: unknown, plugin: string): Waiver[];
|
|
16
30
|
export declare function loadPlugins(references: readonly PluginReference[], root: string): Promise<Plugin[]>;
|
package/dist/plugins.js
CHANGED
|
@@ -9,16 +9,129 @@
|
|
|
9
9
|
* configuration does; the configuration belongs to the repository, and so
|
|
10
10
|
* does that trust.
|
|
11
11
|
*/
|
|
12
|
+
import { readFile } from 'node:fs/promises';
|
|
12
13
|
import { createRequire } from 'node:module';
|
|
13
|
-
import { isAbsolute, join, resolve } from 'node:path';
|
|
14
|
+
import { dirname, isAbsolute, join, resolve } from 'node:path';
|
|
14
15
|
import { pathToFileURL } from 'node:url';
|
|
15
16
|
import { ConfigError } from './config.js';
|
|
16
17
|
const SEVERITIES = new Set(['off', 'note', 'warning', 'error']);
|
|
17
|
-
|
|
18
|
+
/** The conditions Node matches in "exports" when it imports a module. */
|
|
19
|
+
const IMPORT_CONDITIONS = new Set(['node', 'import', 'module-sync', 'default']);
|
|
20
|
+
/**
|
|
21
|
+
* A target of "exports": a path inside the package, `null` where the package
|
|
22
|
+
* refuses the subpath, `undefined` where no condition matched and a fallback
|
|
23
|
+
* may still. A target that is not a "./" path, or that climbs out of the
|
|
24
|
+
* package or into another, is refused, as Node refuses it: its segments are
|
|
25
|
+
* split on a backslash too, since `join` reads one as a separator on Windows,
|
|
26
|
+
* where "pkg/..\..\outside" would otherwise climb out of node_modules.
|
|
27
|
+
*/
|
|
28
|
+
function exportTarget(value, match) {
|
|
29
|
+
if (typeof value === 'string') {
|
|
30
|
+
if (!value.startsWith('./'))
|
|
31
|
+
return null;
|
|
32
|
+
const path = match === null ? value : value.replaceAll('*', match);
|
|
33
|
+
const parts = path.slice(2).split(/[\\/]/);
|
|
34
|
+
return parts.some((part) => part === '..' || part === '.' || part.toLowerCase() === 'node_modules') ? null : path;
|
|
35
|
+
}
|
|
36
|
+
if (Array.isArray(value)) {
|
|
37
|
+
for (const item of value) {
|
|
38
|
+
const path = exportTarget(item, match);
|
|
39
|
+
if (typeof path === 'string')
|
|
40
|
+
return path;
|
|
41
|
+
}
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
if (typeof value === 'object' && value !== null) {
|
|
45
|
+
for (const [condition, item] of Object.entries(value)) {
|
|
46
|
+
if (!IMPORT_CONDITIONS.has(condition))
|
|
47
|
+
continue;
|
|
48
|
+
const path = exportTarget(item, match);
|
|
49
|
+
if (path !== undefined)
|
|
50
|
+
return path;
|
|
51
|
+
}
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* The file a package's "exports" gives a subpath (`.` or `./x`) under the
|
|
58
|
+
* conditions of an import, or `null` when it gives none: Node's
|
|
59
|
+
* PACKAGE_EXPORTS_RESOLVE. An exact key wins; then the "*" pattern with the
|
|
60
|
+
* longest prefix, and of those the longest key.
|
|
61
|
+
*/
|
|
62
|
+
export function exportsTarget(exports, subpath) {
|
|
63
|
+
const keys = typeof exports === 'object' && exports !== null && !Array.isArray(exports) ? Object.keys(exports) : [];
|
|
64
|
+
const dotted = keys.filter((key) => key.startsWith('.'));
|
|
65
|
+
if (dotted.length === 0)
|
|
66
|
+
return subpath === '.' ? (exportTarget(exports, null) ?? null) : null;
|
|
67
|
+
// Subpaths beside conditions is a package Node refuses to read.
|
|
68
|
+
if (dotted.length !== keys.length)
|
|
69
|
+
return null;
|
|
70
|
+
const map = exports;
|
|
71
|
+
if (!subpath.includes('*') && Object.hasOwn(map, subpath))
|
|
72
|
+
return exportTarget(map[subpath], null) ?? null;
|
|
73
|
+
let best;
|
|
74
|
+
for (const key of dotted) {
|
|
75
|
+
const star = key.indexOf('*');
|
|
76
|
+
if (star < 0 || key.includes('*', star + 1))
|
|
77
|
+
continue;
|
|
78
|
+
const suffix = key.slice(star + 1);
|
|
79
|
+
if (subpath.length < key.length || !subpath.startsWith(key.slice(0, star)) || !subpath.endsWith(suffix))
|
|
80
|
+
continue;
|
|
81
|
+
if (best === undefined || star > best.prefix || (star === best.prefix && key.length > best.key.length)) {
|
|
82
|
+
best = { key, prefix: star, match: subpath.slice(star, subpath.length - suffix.length) };
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return best === undefined ? null : (exportTarget(map[best.key], best.match) ?? null);
|
|
86
|
+
}
|
|
87
|
+
/** A package.json, `undefined` when there is none to read. */
|
|
88
|
+
async function manifest(path) {
|
|
89
|
+
try {
|
|
90
|
+
return JSON.parse(await readFile(path, 'utf8'));
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
if (error.code === 'ENOENT' || error.code === 'ENOTDIR')
|
|
94
|
+
return undefined;
|
|
95
|
+
throw error;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* The file a package specifier names, found from the root the way an import
|
|
100
|
+
* there would find it. `require.resolve` reads "exports" under the require
|
|
101
|
+
* conditions, so a package that exports only for "import" - an ESM-only
|
|
102
|
+
* plugin - could not be found; Node's `import.meta.resolve` resolves only
|
|
103
|
+
* from the module calling it, not from the root. So the nearest
|
|
104
|
+
* node_modules/<name> is found here and its "exports" read under the import
|
|
105
|
+
* conditions; a package with no "exports" resolves as it always has.
|
|
106
|
+
*/
|
|
107
|
+
async function resolvePackage(specifier, root) {
|
|
108
|
+
const slash = specifier.indexOf('/', specifier.startsWith('@') ? specifier.indexOf('/') + 1 : 0);
|
|
109
|
+
const name = slash < 0 ? specifier : specifier.slice(0, slash);
|
|
110
|
+
const subpath = slash < 0 ? '.' : `.${specifier.slice(slash)}`;
|
|
111
|
+
let directory = root;
|
|
112
|
+
// Bounded by the depth of the path: each pass moves one directory up.
|
|
113
|
+
for (let depth = 0; depth < 256; depth += 1) {
|
|
114
|
+
const packageDirectory = join(directory, 'node_modules', name);
|
|
115
|
+
const found = await manifest(join(packageDirectory, 'package.json'));
|
|
116
|
+
if (found !== undefined) {
|
|
117
|
+
if (found['exports'] === undefined || found['exports'] === null)
|
|
118
|
+
break;
|
|
119
|
+
const target = exportsTarget(found['exports'], subpath);
|
|
120
|
+
if (target === null)
|
|
121
|
+
throw new Error(`${name} exports no "${subpath}" for import`);
|
|
122
|
+
return join(packageDirectory, target);
|
|
123
|
+
}
|
|
124
|
+
const parent = dirname(directory);
|
|
125
|
+
if (parent === directory)
|
|
126
|
+
break;
|
|
127
|
+
directory = parent;
|
|
128
|
+
}
|
|
129
|
+
return createRequire(join(root, 'package.json')).resolve(specifier);
|
|
130
|
+
}
|
|
131
|
+
async function specifierUrl(specifier, root) {
|
|
18
132
|
if (specifier.startsWith('.') || isAbsolute(specifier))
|
|
19
133
|
return pathToFileURL(resolve(root, specifier)).href;
|
|
20
|
-
|
|
21
|
-
return pathToFileURL(require.resolve(specifier)).href;
|
|
134
|
+
return pathToFileURL(await resolvePackage(specifier, root)).href;
|
|
22
135
|
}
|
|
23
136
|
function isRule(value) {
|
|
24
137
|
if (typeof value !== 'object' || value === null)
|
|
@@ -40,6 +153,7 @@ export function asPlugin(value, module, options) {
|
|
|
40
153
|
const candidate = value;
|
|
41
154
|
const name = candidate['name'];
|
|
42
155
|
const rules = candidate['rules'];
|
|
156
|
+
const waive = candidate['waive'];
|
|
43
157
|
if (typeof name !== 'string' || !/^[a-z0-9@][a-z0-9@/._-]*$/i.test(name))
|
|
44
158
|
problems.push('"name" must be a plain identifier');
|
|
45
159
|
if (!Array.isArray(rules))
|
|
@@ -50,16 +164,38 @@ export function asPlugin(value, module, options) {
|
|
|
50
164
|
problems.push(`rules[${i}] needs a lower-case "id", a "description", a "severity" and a "check" function`);
|
|
51
165
|
});
|
|
52
166
|
}
|
|
167
|
+
if (waive !== undefined && typeof waive !== 'function')
|
|
168
|
+
problems.push('"waive" must be a function');
|
|
53
169
|
if (problems.length > 0)
|
|
54
170
|
throw new ConfigError(module, problems);
|
|
55
|
-
|
|
171
|
+
const plugin = { name: name, rules: rules, options };
|
|
172
|
+
return waive === undefined ? plugin : { ...plugin, waive: waive };
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Checks what a `waive` hook returned: a list of `{ rule, path, reason }`,
|
|
176
|
+
* each a string. A hook that returns nothing waives nothing: a function that
|
|
177
|
+
* found no ruling and fell off its end has answered, not failed.
|
|
178
|
+
*/
|
|
179
|
+
export function asWaivers(value, plugin) {
|
|
180
|
+
const isWaiver = (item) => {
|
|
181
|
+
if (typeof item !== 'object' || item === null)
|
|
182
|
+
return false;
|
|
183
|
+
const w = item;
|
|
184
|
+
return typeof w['rule'] === 'string' && typeof w['path'] === 'string' && typeof w['reason'] === 'string';
|
|
185
|
+
};
|
|
186
|
+
if (value === undefined)
|
|
187
|
+
return [];
|
|
188
|
+
if (!Array.isArray(value) || !value.every(isWaiver)) {
|
|
189
|
+
throw new ConfigError(`plugin "${plugin}"`, ['"waive" must return a list of { rule, path, reason }, each a string']);
|
|
190
|
+
}
|
|
191
|
+
return value.map((w) => ({ rule: w.rule, path: w.path, reason: w.reason }));
|
|
56
192
|
}
|
|
57
193
|
export async function loadPlugins(references, root) {
|
|
58
194
|
const plugins = [];
|
|
59
195
|
for (const reference of references) {
|
|
60
196
|
let loaded;
|
|
61
197
|
try {
|
|
62
|
-
loaded = (await import(specifierUrl(reference.module, root)));
|
|
198
|
+
loaded = (await import(await specifierUrl(reference.module, root)));
|
|
63
199
|
}
|
|
64
200
|
catch (error) {
|
|
65
201
|
throw new ConfigError(reference.module, [`could not be loaded: ${error.message}`]);
|