@descent-vtt/spec-brief 0.1.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 +22 -0
- package/LICENSE +21 -0
- package/README.md +269 -0
- package/bin/spec-brief.js +19 -0
- package/dist/apply.d.ts +26 -0
- package/dist/apply.js +71 -0
- package/dist/apply.js.map +1 -0
- package/dist/archive.d.ts +81 -0
- package/dist/archive.js +333 -0
- package/dist/archive.js.map +1 -0
- package/dist/brief.d.ts +60 -0
- package/dist/brief.js +152 -0
- package/dist/brief.js.map +1 -0
- package/dist/cli.d.ts +35 -0
- package/dist/cli.js +411 -0
- package/dist/cli.js.map +1 -0
- package/dist/collisions.d.ts +50 -0
- package/dist/collisions.js +127 -0
- package/dist/collisions.js.map +1 -0
- package/dist/config.d.ts +94 -0
- package/dist/config.js +353 -0
- package/dist/config.js.map +1 -0
- package/dist/corpus.d.ts +41 -0
- package/dist/corpus.js +154 -0
- package/dist/corpus.js.map +1 -0
- package/dist/engine.d.ts +121 -0
- package/dist/engine.js +276 -0
- package/dist/engine.js.map +1 -0
- package/dist/frontmatter.d.ts +68 -0
- package/dist/frontmatter.js +311 -0
- package/dist/frontmatter.js.map +1 -0
- package/dist/fs.d.ts +59 -0
- package/dist/fs.js +189 -0
- package/dist/fs.js.map +1 -0
- package/dist/git.d.ts +59 -0
- package/dist/git.js +131 -0
- package/dist/git.js.map +1 -0
- package/dist/glob.d.ts +79 -0
- package/dist/glob.js +465 -0
- package/dist/glob.js.map +1 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.js +26 -0
- package/dist/index.js.map +1 -0
- package/dist/integrity.d.ts +11 -0
- package/dist/integrity.js +20 -0
- package/dist/integrity.js.map +1 -0
- package/dist/links.d.ts +38 -0
- package/dist/links.js +142 -0
- package/dist/links.js.map +1 -0
- package/dist/lint.d.ts +38 -0
- package/dist/lint.js +90 -0
- package/dist/lint.js.map +1 -0
- package/dist/markdown.d.ts +65 -0
- package/dist/markdown.js +274 -0
- package/dist/markdown.js.map +1 -0
- package/dist/plugins.d.ts +16 -0
- package/dist/plugins.js +77 -0
- package/dist/plugins.js.map +1 -0
- package/dist/report.d.ts +38 -0
- package/dist/report.js +244 -0
- package/dist/report.js.map +1 -0
- package/dist/rules.d.ts +58 -0
- package/dist/rules.js +448 -0
- package/dist/rules.js.map +1 -0
- package/dist/scaffold.d.ts +25 -0
- package/dist/scaffold.js +81 -0
- package/dist/scaffold.js.map +1 -0
- package/dist/schema.d.ts +47 -0
- package/dist/schema.js +195 -0
- package/dist/schema.js.map +1 -0
- package/dist/text.d.ts +40 -0
- package/dist/text.js +95 -0
- package/dist/text.js.map +1 -0
- package/dist/types.d.ts +30 -0
- package/dist/types.js +5 -0
- package/dist/types.js.map +1 -0
- package/package.json +76 -0
- package/schema.json +321 -0
package/dist/markdown.js
ADDED
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A structural Markdown scanner: code, comments, headings, task items and link
|
|
3
|
+
* destinations, each with its line.
|
|
4
|
+
*
|
|
5
|
+
* Not a CommonMark parser, and it does not need to be. Nothing here renders.
|
|
6
|
+
* What must be exactly right is knowing what is code or a comment, because a
|
|
7
|
+
* heading inside a fenced block is not a section and a template hint inside
|
|
8
|
+
* `<!-- -->` is not content. Everything else is deliberately simple: ATX
|
|
9
|
+
* headings only (a setext underline and a front-matter delimiter are the same
|
|
10
|
+
* three characters), fences at any indentation, and no indented code blocks,
|
|
11
|
+
* because inside a list four spaces of indentation is a continuation far more
|
|
12
|
+
* often than code.
|
|
13
|
+
*/
|
|
14
|
+
// Any indentation: a fence inside a nested list item is indented with the item.
|
|
15
|
+
const FENCE = /^[ \t]*(`{3,}|~{3,})(.*)$/;
|
|
16
|
+
const HEADING = /^ {0,3}(#{1,6})(?:[ \t]+|$)/;
|
|
17
|
+
const TASK = /^([ \t]*)(?:[-*+]|\d{1,9}[.)])[ \t]+\[([ xX])\](?:[ \t]+(.*))?$/;
|
|
18
|
+
const LIST_ITEM = /^[ \t]*(?:[-*+]|\d{1,9}[.)])(?:[ \t]|$)/;
|
|
19
|
+
function blank(length) {
|
|
20
|
+
return ' '.repeat(length);
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Scans `lines` from line `from`; earlier lines (front matter) are blanked in
|
|
24
|
+
* both masks and contribute nothing.
|
|
25
|
+
*/
|
|
26
|
+
export function scan(lines, from = 0) {
|
|
27
|
+
const prose = [];
|
|
28
|
+
const masked = [];
|
|
29
|
+
let fence = null;
|
|
30
|
+
let inComment = false;
|
|
31
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
32
|
+
const line = lines[i];
|
|
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;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Where the list item starting at `start` ends: after its last line indented
|
|
160
|
+
* past the marker, or its last lazy continuation line - text directly under
|
|
161
|
+
* the item with no blank line between, which CommonMark folds into the item's
|
|
162
|
+
* paragraph.
|
|
163
|
+
*/
|
|
164
|
+
function itemEnd(lines, masked, headingLines, start, indent) {
|
|
165
|
+
let last = start;
|
|
166
|
+
let previousBlank = false;
|
|
167
|
+
for (let k = start + 1; k < lines.length; k += 1) {
|
|
168
|
+
const line = lines[k];
|
|
169
|
+
if (headingLines.has(k))
|
|
170
|
+
break;
|
|
171
|
+
if (line.trim() === '') {
|
|
172
|
+
previousBlank = true;
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
const nested = indentOf(line) > indent;
|
|
176
|
+
const lazy = !previousBlank && !LIST_ITEM.test(masked[k]) && !FENCE.test(line) && !/^\s*>/.test(line);
|
|
177
|
+
if (!nested && !lazy)
|
|
178
|
+
break;
|
|
179
|
+
last = k;
|
|
180
|
+
previousBlank = false;
|
|
181
|
+
}
|
|
182
|
+
return last + 1;
|
|
183
|
+
}
|
|
184
|
+
/** Sections: every heading below the title, running to the next heading at its level or above. */
|
|
185
|
+
export function sectionsOf(result) {
|
|
186
|
+
const sections = [];
|
|
187
|
+
const { headings } = result;
|
|
188
|
+
for (let h = 0; h < headings.length; h += 1) {
|
|
189
|
+
const heading = headings[h];
|
|
190
|
+
if (heading.level < 2)
|
|
191
|
+
continue;
|
|
192
|
+
let end = result.lines.length;
|
|
193
|
+
for (let k = h + 1; k < headings.length; k += 1) {
|
|
194
|
+
const next = headings[k];
|
|
195
|
+
if (next.level <= heading.level) {
|
|
196
|
+
end = next.line;
|
|
197
|
+
break;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
sections.push({ heading, end });
|
|
201
|
+
}
|
|
202
|
+
return sections;
|
|
203
|
+
}
|
|
204
|
+
/** The first level-one heading, which is what a reader takes as the title. */
|
|
205
|
+
export function titleOf(result) {
|
|
206
|
+
return result.headings.find((h) => h.level === 1);
|
|
207
|
+
}
|
|
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
|
+
/** Whether a line carries anything once comments are removed. */
|
|
271
|
+
export function hasContent(proseLine) {
|
|
272
|
+
return proseLine.trim().length > 0;
|
|
273
|
+
}
|
|
274
|
+
//# sourceMappingURL=markdown.js.map
|
|
@@ -0,0 +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"]}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Loading rule plugins named by the configuration.
|
|
3
|
+
*
|
|
4
|
+
* spec-brief does not know how another tool signs a departure, records a
|
|
5
|
+
* reproducer or measures a blast radius, and should not: the tool that
|
|
6
|
+
* defines a format is the one that can check it. A plugin is a module whose
|
|
7
|
+
* default export is a plugin object, or a function of the configured options
|
|
8
|
+
* that returns one. Loading one runs its code, exactly as loading an ESLint
|
|
9
|
+
* configuration does; the configuration belongs to the repository, and so
|
|
10
|
+
* does that trust.
|
|
11
|
+
*/
|
|
12
|
+
import type { PluginReference } from './config.js';
|
|
13
|
+
import type { Plugin } from './lint.js';
|
|
14
|
+
/** Checks the shape a module exported, since nothing typed it on the way in. */
|
|
15
|
+
export declare function asPlugin(value: unknown, module: string, options: unknown): Plugin;
|
|
16
|
+
export declare function loadPlugins(references: readonly PluginReference[], root: string): Promise<Plugin[]>;
|
package/dist/plugins.js
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Loading rule plugins named by the configuration.
|
|
3
|
+
*
|
|
4
|
+
* spec-brief does not know how another tool signs a departure, records a
|
|
5
|
+
* reproducer or measures a blast radius, and should not: the tool that
|
|
6
|
+
* defines a format is the one that can check it. A plugin is a module whose
|
|
7
|
+
* default export is a plugin object, or a function of the configured options
|
|
8
|
+
* that returns one. Loading one runs its code, exactly as loading an ESLint
|
|
9
|
+
* configuration does; the configuration belongs to the repository, and so
|
|
10
|
+
* does that trust.
|
|
11
|
+
*/
|
|
12
|
+
import { createRequire } from 'node:module';
|
|
13
|
+
import { isAbsolute, join, resolve } from 'node:path';
|
|
14
|
+
import { pathToFileURL } from 'node:url';
|
|
15
|
+
import { ConfigError } from './config.js';
|
|
16
|
+
const SEVERITIES = new Set(['off', 'note', 'warning', 'error']);
|
|
17
|
+
function specifierUrl(specifier, root) {
|
|
18
|
+
if (specifier.startsWith('.') || isAbsolute(specifier))
|
|
19
|
+
return pathToFileURL(resolve(root, specifier)).href;
|
|
20
|
+
const require = createRequire(join(root, 'package.json'));
|
|
21
|
+
return pathToFileURL(require.resolve(specifier)).href;
|
|
22
|
+
}
|
|
23
|
+
function isRule(value) {
|
|
24
|
+
if (typeof value !== 'object' || value === null)
|
|
25
|
+
return false;
|
|
26
|
+
const rule = value;
|
|
27
|
+
return (typeof rule['id'] === 'string' &&
|
|
28
|
+
/^[a-z0-9][a-z0-9-]*$/.test(rule['id']) &&
|
|
29
|
+
typeof rule['description'] === 'string' &&
|
|
30
|
+
typeof rule['severity'] === 'string' &&
|
|
31
|
+
SEVERITIES.has(rule['severity']) &&
|
|
32
|
+
typeof rule['check'] === 'function');
|
|
33
|
+
}
|
|
34
|
+
/** Checks the shape a module exported, since nothing typed it on the way in. */
|
|
35
|
+
export function asPlugin(value, module, options) {
|
|
36
|
+
const problems = [];
|
|
37
|
+
if (typeof value !== 'object' || value === null) {
|
|
38
|
+
throw new ConfigError(module, ['does not export a plugin: expected an object with "name" and "rules"']);
|
|
39
|
+
}
|
|
40
|
+
const candidate = value;
|
|
41
|
+
const name = candidate['name'];
|
|
42
|
+
const rules = candidate['rules'];
|
|
43
|
+
if (typeof name !== 'string' || !/^[a-z0-9@][a-z0-9@/._-]*$/i.test(name))
|
|
44
|
+
problems.push('"name" must be a plain identifier');
|
|
45
|
+
if (!Array.isArray(rules))
|
|
46
|
+
problems.push('"rules" must be a list');
|
|
47
|
+
else {
|
|
48
|
+
rules.forEach((rule, i) => {
|
|
49
|
+
if (!isRule(rule))
|
|
50
|
+
problems.push(`rules[${i}] needs a lower-case "id", a "description", a "severity" and a "check" function`);
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
if (problems.length > 0)
|
|
54
|
+
throw new ConfigError(module, problems);
|
|
55
|
+
return { name: name, rules: rules, options };
|
|
56
|
+
}
|
|
57
|
+
export async function loadPlugins(references, root) {
|
|
58
|
+
const plugins = [];
|
|
59
|
+
for (const reference of references) {
|
|
60
|
+
let loaded;
|
|
61
|
+
try {
|
|
62
|
+
loaded = (await import(specifierUrl(reference.module, root)));
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
throw new ConfigError(reference.module, [`could not be loaded: ${error.message}`]);
|
|
66
|
+
}
|
|
67
|
+
const exported = loaded['default'] ?? loaded['plugin'];
|
|
68
|
+
const value = typeof exported === 'function' ? await exported(reference.options) : exported;
|
|
69
|
+
plugins.push(asPlugin(value, reference.module, reference.options));
|
|
70
|
+
}
|
|
71
|
+
const names = plugins.map((p) => p.name);
|
|
72
|
+
const repeated = names.filter((n, i) => names.indexOf(n) !== i);
|
|
73
|
+
if (repeated.length > 0)
|
|
74
|
+
throw new ConfigError('plugins', [`two plugins are named "${repeated[0]}"`]);
|
|
75
|
+
return plugins;
|
|
76
|
+
}
|
|
77
|
+
//# sourceMappingURL=plugins.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plugins.js","sourceRoot":"","sources":["../src/plugins.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Loading rule plugins named by the configuration.\n *\n * spec-brief does not know how another tool signs a departure, records a\n * reproducer or measures a blast radius, and should not: the tool that\n * defines a format is the one that can check it. A plugin is a module whose\n * default export is a plugin object, or a function of the configured options\n * that returns one. Loading one runs its code, exactly as loading an ESLint\n * configuration does; the configuration belongs to the repository, and so\n * does that trust.\n */\n\nimport { createRequire } from 'node:module';\nimport { isAbsolute, join, resolve } from 'node:path';\nimport { pathToFileURL } from 'node:url';\n\nimport type { PluginReference } from './config.js';\nimport { ConfigError } from './config.js';\nimport type { Plugin } from './lint.js';\nimport type { Rule } from './rules.js';\n\nconst SEVERITIES = new Set(['off', 'note', 'warning', 'error']);\n\nfunction specifierUrl(specifier: string, root: string): string {\n if (specifier.startsWith('.') || isAbsolute(specifier)) return pathToFileURL(resolve(root, specifier)).href;\n const require = createRequire(join(root, 'package.json'));\n return pathToFileURL(require.resolve(specifier)).href;\n}\n\nfunction isRule(value: unknown): value is Rule {\n if (typeof value !== 'object' || value === null) return false;\n const rule = value as Record<string, unknown>;\n return (\n typeof rule['id'] === 'string' &&\n /^[a-z0-9][a-z0-9-]*$/.test(rule['id']) &&\n typeof rule['description'] === 'string' &&\n typeof rule['severity'] === 'string' &&\n SEVERITIES.has(rule['severity']) &&\n typeof rule['check'] === 'function'\n );\n}\n\n/** Checks the shape a module exported, since nothing typed it on the way in. */\nexport function asPlugin(value: unknown, module: string, options: unknown): Plugin {\n const problems: string[] = [];\n if (typeof value !== 'object' || value === null) {\n throw new ConfigError(module, ['does not export a plugin: expected an object with \"name\" and \"rules\"']);\n }\n const candidate = value as Record<string, unknown>;\n const name = candidate['name'];\n const rules = candidate['rules'];\n if (typeof name !== 'string' || !/^[a-z0-9@][a-z0-9@/._-]*$/i.test(name)) problems.push('\"name\" must be a plain identifier');\n if (!Array.isArray(rules)) problems.push('\"rules\" must be a list');\n else {\n rules.forEach((rule, i) => {\n if (!isRule(rule)) problems.push(`rules[${i}] needs a lower-case \"id\", a \"description\", a \"severity\" and a \"check\" function`);\n });\n }\n if (problems.length > 0) throw new ConfigError(module, problems);\n return { name: name as string, rules: rules as Rule[], options };\n}\n\nexport async function loadPlugins(references: readonly PluginReference[], root: string): Promise<Plugin[]> {\n const plugins: Plugin[] = [];\n for (const reference of references) {\n let loaded: Record<string, unknown>;\n try {\n loaded = (await import(specifierUrl(reference.module, root))) as Record<string, unknown>;\n } catch (error) {\n throw new ConfigError(reference.module, [`could not be loaded: ${(error as Error).message}`]);\n }\n const exported = loaded['default'] ?? loaded['plugin'];\n const value = typeof exported === 'function' ? await (exported as (o: unknown) => unknown)(reference.options) : exported;\n plugins.push(asPlugin(value, reference.module, reference.options));\n }\n const names = plugins.map((p) => p.name);\n const repeated = names.filter((n, i) => names.indexOf(n) !== i);\n if (repeated.length > 0) throw new ConfigError('plugins', [`two plugins are named \"${repeated[0] as string}\"`]);\n return plugins;\n}\n"]}
|
package/dist/report.d.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rendering: findings, lists, the collision matrix and plans, for a person at
|
|
3
|
+
* a terminal or for the machine reading the job.
|
|
4
|
+
*
|
|
5
|
+
* `json` is a document with a version, for orchestrators. `sarif` is SARIF
|
|
6
|
+
* 2.1.0, for code-scanning upload. `github` is workflow commands, which put a
|
|
7
|
+
* finding on the line of the pull request with no upload and no permission.
|
|
8
|
+
*/
|
|
9
|
+
import type { Plan } from './archive.js';
|
|
10
|
+
import type { Brief } from './brief.js';
|
|
11
|
+
import type { CollisionReport } from './collisions.js';
|
|
12
|
+
import type { Finding } from './types.js';
|
|
13
|
+
export type Format = 'pretty' | 'json' | 'sarif' | 'github';
|
|
14
|
+
export declare const FORMATS: readonly Format[];
|
|
15
|
+
/** The version of the JSON documents this tool prints. Bumped when a field changes meaning. */
|
|
16
|
+
export declare const JSON_SCHEMA_VERSION = 1;
|
|
17
|
+
export interface Style {
|
|
18
|
+
readonly color: boolean;
|
|
19
|
+
}
|
|
20
|
+
declare const CODES: Readonly<Record<string, readonly [number, number]>>;
|
|
21
|
+
export declare function paint(style: Style, color: keyof typeof CODES, text: string): string;
|
|
22
|
+
export declare function summaryLine(findings: readonly Finding[]): string;
|
|
23
|
+
export declare function prettyFindings(findings: readonly Finding[], style: Style): string;
|
|
24
|
+
export declare function jsonDocument(command: string, version: string, body: Record<string, unknown>): string;
|
|
25
|
+
export declare function findingJson(f: Finding): Record<string, unknown>;
|
|
26
|
+
export declare function sarif(findings: readonly Finding[], version: string): string;
|
|
27
|
+
export declare function githubCommands(findings: readonly Finding[]): string;
|
|
28
|
+
export declare function briefJson(brief: Brief, extra?: Record<string, unknown>): Record<string, unknown>;
|
|
29
|
+
export interface ListRow {
|
|
30
|
+
readonly brief: Brief;
|
|
31
|
+
readonly ready: boolean;
|
|
32
|
+
readonly waitingOn: readonly string[];
|
|
33
|
+
}
|
|
34
|
+
export declare function prettyList(rows: readonly ListRow[], style: Style): string;
|
|
35
|
+
export declare function prettyMatrix(report: CollisionReport, style: Style): string;
|
|
36
|
+
export declare function prettyPlan(plan: Plan, dryRun: boolean, style: Style): string;
|
|
37
|
+
export declare function planJson(plan: Plan): Record<string, unknown>;
|
|
38
|
+
export {};
|