@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.
Files changed (128) hide show
  1. package/CHANGELOG.md +264 -0
  2. package/README.md +90 -20
  3. package/dist/archive.d.ts +52 -2
  4. package/dist/archive.js +205 -37
  5. package/dist/archive.js.map +1 -1
  6. package/dist/brief.d.ts +2 -0
  7. package/dist/brief.js +5 -1
  8. package/dist/brief.js.map +1 -1
  9. package/dist/cli.d.ts +1 -1
  10. package/dist/cli.js +48 -14
  11. package/dist/cli.js.map +1 -1
  12. package/dist/collisions.d.ts +43 -12
  13. package/dist/collisions.js +90 -58
  14. package/dist/collisions.js.map +1 -1
  15. package/dist/config.d.ts +11 -0
  16. package/dist/config.js +58 -10
  17. package/dist/config.js.map +1 -1
  18. package/dist/corpus.d.ts +21 -1
  19. package/dist/corpus.js +51 -2
  20. package/dist/corpus.js.map +1 -1
  21. package/dist/engine.d.ts +33 -3
  22. package/dist/engine.js +112 -9
  23. package/dist/engine.js.map +1 -1
  24. package/dist/frontmatter.d.ts +18 -61
  25. package/dist/frontmatter.js +31 -300
  26. package/dist/frontmatter.js.map +1 -1
  27. package/dist/glob.d.ts +77 -54
  28. package/dist/glob.js +104 -433
  29. package/dist/glob.js.map +1 -1
  30. package/dist/index.d.ts +10 -8
  31. package/dist/index.js +8 -6
  32. package/dist/index.js.map +1 -1
  33. package/dist/links.d.ts +1 -1
  34. package/dist/links.js +3 -4
  35. package/dist/links.js.map +1 -1
  36. package/dist/lint.d.ts +33 -2
  37. package/dist/lint.js +9 -2
  38. package/dist/lint.js.map +1 -1
  39. package/dist/markdown.d.ts +26 -23
  40. package/dist/markdown.js +60 -237
  41. package/dist/markdown.js.map +1 -1
  42. package/dist/plugins.d.ts +14 -0
  43. package/dist/plugins.js +142 -6
  44. package/dist/plugins.js.map +1 -1
  45. package/dist/report.d.ts +40 -3
  46. package/dist/report.js +227 -11
  47. package/dist/report.js.map +1 -1
  48. package/dist/rules.d.ts +22 -4
  49. package/dist/rules.js +175 -45
  50. package/dist/rules.js.map +1 -1
  51. package/dist/scaffold.d.ts +1 -1
  52. package/dist/scaffold.js +10 -9
  53. package/dist/scaffold.js.map +1 -1
  54. package/dist/schedule.d.ts +133 -0
  55. package/dist/schedule.js +265 -0
  56. package/dist/schedule.js.map +1 -0
  57. package/dist/scope.d.ts +52 -0
  58. package/dist/scope.js +56 -0
  59. package/dist/scope.js.map +1 -0
  60. package/dist/text.d.ts +13 -0
  61. package/dist/text.js +23 -0
  62. package/dist/text.js.map +1 -1
  63. package/dist/trigger.d.ts +17 -0
  64. package/dist/trigger.js +49 -0
  65. package/dist/trigger.js.map +1 -0
  66. package/dist/types.d.ts +14 -5
  67. package/dist/types.js.map +1 -1
  68. package/dist/vendor/spec-core/markdown/frontmatter.d.ts +103 -0
  69. package/dist/vendor/spec-core/markdown/frontmatter.js +459 -0
  70. package/dist/vendor/spec-core/markdown/frontmatter.js.map +1 -0
  71. package/dist/vendor/spec-core/markdown/headings.d.ts +45 -0
  72. package/dist/vendor/spec-core/markdown/headings.js +199 -0
  73. package/dist/vendor/spec-core/markdown/headings.js.map +1 -0
  74. package/dist/vendor/spec-core/markdown/index.d.ts +5 -0
  75. package/dist/vendor/spec-core/markdown/index.js +4 -0
  76. package/dist/vendor/spec-core/markdown/index.js.map +1 -0
  77. package/dist/vendor/spec-core/markdown/layout.d.ts +39 -0
  78. package/dist/vendor/spec-core/markdown/layout.js +28 -0
  79. package/dist/vendor/spec-core/markdown/layout.js.map +1 -0
  80. package/dist/vendor/spec-core/markdown/links.d.ts +22 -0
  81. package/dist/vendor/spec-core/markdown/links.js +364 -0
  82. package/dist/vendor/spec-core/markdown/links.js.map +1 -0
  83. package/dist/vendor/spec-core/markdown/lists.d.ts +22 -0
  84. package/dist/vendor/spec-core/markdown/lists.js +127 -0
  85. package/dist/vendor/spec-core/markdown/lists.js.map +1 -0
  86. package/dist/vendor/spec-core/markdown/scan.d.ts +36 -0
  87. package/dist/vendor/spec-core/markdown/scan.js +439 -0
  88. package/dist/vendor/spec-core/markdown/scan.js.map +1 -0
  89. package/dist/vendor/spec-core/markdown/syntax.d.ts +75 -0
  90. package/dist/vendor/spec-core/markdown/syntax.js +181 -0
  91. package/dist/vendor/spec-core/markdown/syntax.js.map +1 -0
  92. package/dist/vendor/spec-core/markdown/tables.d.ts +18 -0
  93. package/dist/vendor/spec-core/markdown/tables.js +86 -0
  94. package/dist/vendor/spec-core/markdown/tables.js.map +1 -0
  95. package/dist/vendor/spec-core/markdown/types.d.ts +287 -0
  96. package/dist/vendor/spec-core/markdown/types.js +9 -0
  97. package/dist/vendor/spec-core/markdown/types.js.map +1 -0
  98. package/dist/vendor/spec-core/path/index.d.ts +1 -0
  99. package/dist/vendor/spec-core/path/index.js +2 -0
  100. package/dist/vendor/spec-core/path/index.js.map +1 -0
  101. package/dist/vendor/spec-core/path/posix.d.ts +83 -0
  102. package/dist/vendor/spec-core/path/posix.js +150 -0
  103. package/dist/vendor/spec-core/path/posix.js.map +1 -0
  104. package/dist/vendor/spec-core/pattern/automaton.d.ts +129 -0
  105. package/dist/vendor/spec-core/pattern/automaton.js +340 -0
  106. package/dist/vendor/spec-core/pattern/automaton.js.map +1 -0
  107. package/dist/vendor/spec-core/pattern/charset.d.ts +58 -0
  108. package/dist/vendor/spec-core/pattern/charset.js +116 -0
  109. package/dist/vendor/spec-core/pattern/charset.js.map +1 -0
  110. package/dist/vendor/spec-core/pattern/glob.d.ts +121 -0
  111. package/dist/vendor/spec-core/pattern/glob.js +481 -0
  112. package/dist/vendor/spec-core/pattern/glob.js.map +1 -0
  113. package/dist/vendor/spec-core/pattern/index.d.ts +6 -0
  114. package/dist/vendor/spec-core/pattern/index.js +4 -0
  115. package/dist/vendor/spec-core/pattern/index.js.map +1 -0
  116. package/dist/vendor/spec-core/pattern/regex.d.ts +67 -0
  117. package/dist/vendor/spec-core/pattern/regex.js +813 -0
  118. package/dist/vendor/spec-core/pattern/regex.js.map +1 -0
  119. package/dist/vendor/spec-core/text/index.d.ts +2 -0
  120. package/dist/vendor/spec-core/text/index.js +2 -0
  121. package/dist/vendor/spec-core/text/index.js.map +1 -0
  122. package/dist/vendor/spec-core/text/lines.d.ts +72 -0
  123. package/dist/vendor/spec-core/text/lines.js +156 -0
  124. package/dist/vendor/spec-core/text/lines.js.map +1 -0
  125. package/package.json +3 -2
  126. package/schema.json +17 -0
  127. package/src/vendor/spec-core/LICENSE +21 -0
  128. package/src/vendor/spec-core/README.md +8 -0
package/dist/markdown.js CHANGED
@@ -1,185 +1,70 @@
1
1
  /**
2
- * A structural Markdown scanner: code, comments, headings, task items and link
3
- * destinations, each with its line.
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
- * 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.
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
- // 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);
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
- * Scans `lines` from line `from`; earlier lines (front matter) are blanked in
24
- * both masks and contribute nothing.
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, `[![diagram](d.png)](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
- 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;
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
- * 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;
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
- * `![alt](dest)`, 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;
@@ -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 * `![alt](dest)`, 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, `[![diagram](d.png)](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
- function specifierUrl(specifier, root) {
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
- const require = createRequire(join(root, 'package.json'));
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
- return { name: name, rules: rules, options };
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}`]);