@descent-vtt/spec-brief 0.1.0 → 0.2.1

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 +301 -0
  2. package/README.md +92 -22
  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 +18 -0
  16. package/dist/config.js +66 -11
  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 +27 -23
  40. package/dist/markdown.js +66 -235
  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 +25 -0
  81. package/dist/vendor/spec-core/markdown/links.js +394 -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 +463 -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 +290 -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 +4 -3
  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/glob.js CHANGED
@@ -1,465 +1,136 @@
1
1
  /**
2
- * Globs: parsing, matching, and deciding whether two globs can name the same file.
2
+ * Scopes as globs: spec-core's `path` dialect, read the way spec-brief reads a
3
+ * brief.
3
4
  *
4
- * The dialect is the one people type: `**`, `*`, `?`, `[abc]`, `[!a-z]`,
5
- * `{a,b}` and `\` to escape. A pattern with no glob syntax at all names a
6
- * directory and everything beneath it, so `src/auth` covers
7
- * `src/auth/login.ts` - unless it names a file, one the tree holds or one with
8
- * an extension, which matches only itself. A trailing `/` always means a
9
- * directory. Paths
10
- * are repository-relative and POSIX, and matching is case-sensitive on every
11
- * host, because git's paths are and a result should not depend on who ran it.
12
- * `*` matches a leading dot; a scope that forgot its dotfiles is not a scope
13
- * that excludes them.
5
+ * The syntax, the automaton and the witness search are spec-core's, copied
6
+ * into `src/vendor/` and verified by hash, so the family's tools read one
7
+ * pattern the same way (spec-core ADR-0003). What is decided here is what
8
+ * spec-brief adds around it:
14
9
  *
15
- * Nothing here compiles to a `RegExp`. Matching and intersection are dynamic
16
- * programmes over the pattern and the subject, so a pattern of `*a*a*a*a*b`
17
- * against a long name costs the product of their lengths and cannot backtrack
18
- * into an exponent. The same property is why the sibling tools match their own
19
- * globs this way.
10
+ * - A pattern is relative to the repository root, so a leading `/` is refused
11
+ * rather than rooted at the filesystem; and a list entry's `!` has no list to
12
+ * belong to in a scope, so it is refused rather than read.
13
+ * - Paths compare case-sensitively on every host, as git's do.
14
+ * - A path with no glob syntax is read as the tree reads it: a file the tree
15
+ * holds is that file, a directory it holds is everything beneath it, and a
16
+ * path it does not hold is a file, unless it is written with a trailing `/`.
17
+ * The tree, not the spelling, knows that `Dockerfile` is a file and
18
+ * `docs/v1.2` a directory (ADR-0005, amended 2026-09-26).
19
+ * - A trailing `/**` is at least one segment, so `docs/**` is the directory's
20
+ * contents and a collision witness is always a file.
21
+ *
22
+ * Nothing here compiles to a `RegExp`. Matching costs at most the pattern's
23
+ * size times the path's length, and a question about two scopes that cannot be
24
+ * answered within the search's budget answers `undecided`, never a guess.
20
25
  */
21
- /** More alternatives than this is a pattern nobody meant. */
22
- export const MAX_ALTERNATIVES = 256;
23
- const SLASH = 0x2f;
24
- const GLOBSTAR = { kind: 'globstar' };
25
- /** A name with an extension, `login.ts` or `.eslintrc.json`, and not a dot-directory such as `.github`. */
26
- function hasExtension(name) {
27
- return /.\.[^.]+$/.test(name);
28
- }
26
+ import { AutomatonTooLarge, globCovers as coreCovers, globWitness as coreWitness, isGlobSyntax, parseGlob as coreParse, WITNESS_BUDGET, } from './vendor/spec-core/pattern/index.js';
27
+ import { segments } from './vendor/spec-core/path/index.js';
28
+ export { isGlobSyntax, WITNESS_BUDGET };
29
+ /** Parses a scope pattern, or says why it cannot. */
29
30
  export function parseGlob(source, options = {}) {
30
- let pattern = source.trim();
31
- if (pattern.length === 0)
32
- return { ok: false, error: 'the pattern is empty' };
31
+ const pattern = source.trim();
33
32
  if (pattern.startsWith('!'))
34
33
  return { ok: false, error: 'negated patterns are not supported; narrow the positive pattern' };
35
34
  if (pattern.startsWith('/'))
36
35
  return { ok: false, error: 'a pattern is relative to the repository root and cannot start with "/"' };
37
- if (/(?:^|[^\\])[@+!?*]\(/.test(pattern))
38
- return { ok: false, error: 'extended globs such as "+(a|b)" are not supported' };
39
- // A leading "./" needs no stripping: "." segments are dropped below.
40
- if (pattern.endsWith('/'))
41
- pattern = `${pattern}**`;
42
- const expanded = expandBraces(pattern);
43
- if (typeof expanded === 'string')
44
- return { ok: false, error: expanded };
45
- const alternatives = [];
46
- for (const alternative of expanded) {
47
- const segments = [];
48
- for (const raw of alternative.split('/')) {
49
- if (raw === '' || raw === '.')
50
- continue;
51
- if (raw === '..')
52
- return { ok: false, error: 'a pattern cannot leave the repository with ".."' };
53
- if (raw === '**') {
54
- if (segments[segments.length - 1]?.kind !== 'globstar')
55
- segments.push(GLOBSTAR);
56
- continue;
57
- }
58
- const tokens = tokenize(raw);
59
- if (typeof tokens === 'string')
60
- return { ok: false, error: tokens };
61
- segments.push({ kind: 'pattern', tokens, literal: tokens.every((t) => t.kind === 'literal') });
62
- }
63
- if (segments.length === 0)
64
- return { ok: false, error: 'the pattern names no path' };
65
- // A path with no glob syntax names a directory and everything beneath it,
66
- // unless it names a file: one the tree has, or one with an extension.
67
- // Treating a file as a directory would let "src/a.ts" overlap
68
- // "**/session.ts" through a "src/a.ts/session.ts" nobody can create.
69
- if (segments.every((s) => s.kind === 'pattern' && s.literal)) {
70
- const path = segments
71
- .map((s) => (s.kind === 'pattern' ? s.tokens.map((t) => (t.kind === 'literal' ? t.char : '')).join('') : ''))
72
- .join('/');
73
- const last = path.slice(path.lastIndexOf('/') + 1);
74
- if (options.isFile?.(path) !== true && !hasExtension(last))
75
- segments.push(GLOBSTAR);
76
- }
77
- alternatives.push(segments);
78
- }
79
- return { ok: true, glob: { source, alternatives } };
80
- }
81
- /** Expands `{a,b}` groups, innermost last, or says why the pattern is malformed. */
82
- function expandBraces(pattern) {
83
- let open = -1;
84
- let depth = 0;
85
- for (let i = 0; i < pattern.length; i += 1) {
86
- const ch = pattern.charAt(i);
87
- if (ch === '\\') {
88
- i += 1;
89
- continue;
90
- }
91
- if (ch === '[') {
92
- const close = classEnd(pattern, i);
93
- if (close > 0)
94
- i = close;
95
- continue;
96
- }
97
- if (ch === '{') {
98
- if (depth === 0)
99
- open = i;
100
- depth += 1;
101
- }
102
- else if (ch === '}') {
103
- if (depth === 0)
104
- return 'a "}" closes no "{"';
105
- depth -= 1;
106
- if (depth === 0) {
107
- const options = splitTopLevel(pattern.slice(open + 1, i));
108
- const prefix = pattern.slice(0, open);
109
- const suffix = pattern.slice(i + 1);
110
- const results = [];
111
- for (const option of options) {
112
- const expanded = expandBraces(`${prefix}${option}${suffix}`);
113
- if (typeof expanded === 'string')
114
- return expanded;
115
- results.push(...expanded);
116
- if (results.length > MAX_ALTERNATIVES)
117
- return `the braces expand to more than ${MAX_ALTERNATIVES} patterns`;
118
- }
119
- return results;
120
- }
121
- }
122
- }
123
- if (depth > 0)
124
- return 'a "{" is never closed';
125
- return [pattern];
126
- }
127
- function splitTopLevel(body) {
128
- const parts = [];
129
- let depth = 0;
130
- let start = 0;
131
- for (let i = 0; i < body.length; i += 1) {
132
- const ch = body.charAt(i);
133
- if (ch === '\\')
134
- i += 1;
135
- else if (ch === '{')
136
- depth += 1;
137
- else if (ch === '}')
138
- depth -= 1;
139
- else if (ch === ',' && depth === 0) {
140
- parts.push(body.slice(start, i));
141
- start = i + 1;
142
- }
143
- }
144
- parts.push(body.slice(start));
145
- return parts;
146
- }
147
- /** The index of the `]` closing a class opened at `open`, or -1. */
148
- function classEnd(pattern, open) {
149
- let i = open + 1;
150
- if (pattern.charAt(i) === '!' || pattern.charAt(i) === '^')
151
- i += 1;
152
- if (pattern.charAt(i) === ']')
153
- i += 1;
154
- for (; i < pattern.length; i += 1) {
155
- const ch = pattern.charAt(i);
156
- if (ch === '/')
157
- return -1;
158
- if (ch === ']')
159
- return i;
160
- }
161
- return -1;
162
- }
163
- function tokenize(segment) {
164
- const tokens = [];
165
- const chars = Array.from(segment);
166
- for (let i = 0; i < chars.length; i += 1) {
167
- const ch = chars[i];
168
- if (ch === '\\') {
169
- const next = chars[i + 1];
170
- // A backslash before a letter, a digit or nothing is a Windows separator, not an escape.
171
- if (next === undefined || /[A-Za-z0-9]/.test(next))
172
- return '"\\" escapes glob syntax; separate directories with "/"';
173
- tokens.push({ kind: 'literal', char: next });
174
- i += 1;
175
- }
176
- else if (ch === '*') {
177
- if (tokens[tokens.length - 1]?.kind !== 'star')
178
- tokens.push({ kind: 'star' });
179
- }
180
- else if (ch === '?') {
181
- tokens.push({ kind: 'any' });
182
- }
183
- else if (ch === '[') {
184
- const parsed = readClass(chars, i);
185
- if (typeof parsed === 'string')
186
- return parsed;
187
- tokens.push(parsed.token);
188
- i = parsed.end;
189
- }
190
- else {
191
- tokens.push({ kind: 'literal', char: ch });
192
- }
193
- }
194
- return tokens;
195
- }
196
- function readClass(chars, open) {
197
- let i = open + 1;
198
- let negated = false;
199
- if (chars[i] === '!' || chars[i] === '^') {
200
- negated = true;
201
- i += 1;
202
- }
203
- const ranges = [];
204
- let first = true;
205
- for (; i < chars.length; i += 1) {
206
- const ch = chars[i];
207
- // Never empty: the first character is always a member, even "]".
208
- if (ch === ']' && !first)
209
- return { token: { kind: 'class', negated, ranges }, end: i };
210
- first = false;
211
- let lo = ch;
212
- if (ch === '\\' && chars[i + 1] !== undefined) {
213
- i += 1;
214
- lo = chars[i];
215
- }
216
- let hi = lo;
217
- if (chars[i + 1] === '-' && chars[i + 2] !== undefined && chars[i + 2] !== ']') {
218
- hi = chars[i + 2];
219
- i += 2;
220
- }
221
- const from = lo.codePointAt(0);
222
- const to = hi.codePointAt(0);
223
- if (to < from)
224
- return `the range "${lo}-${hi}" runs backwards`;
225
- ranges.push([from, to]);
226
- }
227
- return 'a "[" is never closed';
228
- }
229
- function splitPath(path) {
230
- return path.split('/').filter((s) => s !== '' && s !== '.');
231
- }
232
- /** Membership of a class. A slash is never asked about: paths are matched a segment at a time. */
233
- function inClass(token, point) {
234
- const inside = token.ranges.some(([lo, hi]) => point >= lo && point <= hi);
235
- return token.negated ? !inside : inside;
236
- }
237
- function charMatches(token, char) {
238
- switch (token.kind) {
239
- case 'literal':
240
- return token.char === char;
241
- case 'any':
242
- return true;
243
- case 'class':
244
- return inClass(token, char.codePointAt(0));
245
- }
246
- }
247
- /** Whether one segment of a path matches a pattern segment's tokens. */
248
- function tokensMatch(tokens, subject) {
249
- const chars = Array.from(subject);
250
- // next[j]: tokens[i+1..] match chars[j..]; filled from the end of both.
251
- let next = new Array(chars.length + 1).fill(false);
252
- next[chars.length] = true;
253
- for (let i = tokens.length - 1; i >= 0; i -= 1) {
254
- const token = tokens[i];
255
- const row = new Array(chars.length + 1).fill(false);
256
- for (let j = chars.length; j >= 0; j -= 1) {
257
- row[j] =
258
- token.kind === 'star'
259
- ? next[j] || (j < chars.length && row[j + 1])
260
- : j < chars.length && charMatches(token, chars[j]) && next[j + 1];
261
- }
262
- next = row;
36
+ const given = options.literal ?? 'file';
37
+ const literals = [];
38
+ const literal = (path) => {
39
+ const reading = typeof given === 'function' ? given(path) : given;
40
+ literals.push({ path, reading });
41
+ return reading;
42
+ };
43
+ try {
44
+ const parsed = coreParse(pattern, { dialect: 'path', caseSensitive: true, literal });
45
+ return parsed.ok ? { ok: true, glob: { source, compiled: parsed.glob, literals } } : parsed;
263
46
  }
264
- return next[0];
265
- }
266
- function segmentsMatch(segments, path) {
267
- let next = new Array(path.length + 1).fill(false);
268
- next[path.length] = true;
269
- for (let i = segments.length - 1; i >= 0; i -= 1) {
270
- const segment = segments[i];
271
- const row = new Array(path.length + 1).fill(false);
272
- for (let j = path.length; j >= 0; j -= 1) {
273
- row[j] =
274
- segment.kind === 'globstar'
275
- ? next[j] || (j < path.length && row[j + 1])
276
- : j < path.length && tokensMatch(segment.tokens, path[j]) && next[j + 1];
277
- }
278
- next = row;
47
+ catch (error) {
48
+ // Too many states is a pattern nobody meant; anything else is a defect to surface.
49
+ if (error instanceof AutomatonTooLarge)
50
+ return { ok: false, error: error.message };
51
+ throw error;
279
52
  }
280
- return next[0];
281
53
  }
54
+ /**
55
+ * Whether a path matches. The path is read as a repository path: empty and
56
+ * `.` segments are dropped, so `./src//a.ts` is `src/a.ts`.
57
+ */
282
58
  export function matchGlob(glob, path) {
283
- const segments = splitPath(path);
284
- if (segments.length === 0)
285
- return false;
286
- return glob.alternatives.some((alternative) => segmentsMatch(alternative, segments));
59
+ const canonical = segments(path).join('/');
60
+ return canonical !== '' && glob.compiled.match(canonical);
287
61
  }
288
62
  /**
289
- * A path both globs match, or `null` when there is none. The answer is exact
290
- * for the dialect above: a `null` means no file can be in both scopes, and a
291
- * path is a witness a reader can check by eye.
63
+ * A shortest path every glob in `include` matches and none in `exclude` does,
64
+ * `none` as a proof that there is no such path, or `undecided` when the search
65
+ * met its budget first.
292
66
  */
293
- export function intersectGlobs(a, b) {
294
- for (const left of a.alternatives) {
295
- for (const right of b.alternatives) {
296
- const witness = intersectSegments(left, right);
297
- if (witness !== null)
298
- return witness.length === 0 ? 'x' : witness.join('/');
299
- }
300
- }
301
- return null;
67
+ export function globWitness(include, exclude = [], budget = WITNESS_BUDGET) {
68
+ return coreWitness(include.map((g) => g.compiled), exclude.map((g) => g.compiled), budget);
302
69
  }
303
- const ANY_SEGMENT = [{ kind: 'star' }];
304
- function intersectSegments(p, q) {
305
- const memo = new Map();
306
- const width = q.length + 1;
307
- const visit = (i, j) => {
308
- const key = i * width + j;
309
- const cached = memo.get(key);
310
- if (cached !== undefined)
311
- return cached;
312
- const result = step(i, j);
313
- memo.set(key, result);
314
- return result;
315
- };
316
- const prepend = (head, tail) => head === null || tail === null ? null : [head, ...tail];
317
- const step = (i, j) => {
318
- if (i === p.length && j === q.length)
319
- return [];
320
- const x = p[i];
321
- const y = q[j];
322
- if (x?.kind === 'globstar') {
323
- const skip = visit(i + 1, j);
324
- if (skip !== null)
325
- return skip;
326
- if (y === undefined)
327
- return null;
328
- if (y.kind === 'globstar')
329
- return visit(i, j + 1);
330
- return prepend(intersectTokens(y.tokens, ANY_SEGMENT), visit(i, j + 1));
331
- }
332
- if (y?.kind === 'globstar') {
333
- const skip = visit(i, j + 1);
334
- if (skip !== null)
335
- return skip;
336
- if (x === undefined)
337
- return null;
338
- return prepend(intersectTokens(x.tokens, ANY_SEGMENT), visit(i + 1, j));
339
- }
340
- if (x === undefined || y === undefined)
341
- return null;
342
- const head = intersectTokens(x.tokens, y.tokens);
343
- return head === null ? null : prepend(head, visit(i + 1, j + 1));
344
- };
345
- return visit(0, 0);
70
+ /** Whether every path `inner` matches is matched by one of `outer`, or `undecided`. */
71
+ export function globCovers(outer, inner, budget = WITNESS_BUDGET) {
72
+ return coreCovers(outer.map((g) => g.compiled), inner.compiled, budget);
346
73
  }
347
74
  /**
348
- * A non-empty string both token sequences match, or `null`.
349
- *
350
- * Complete by a shortest-witness argument: in a shortest common string, no
351
- * character is absorbed by a star on both sides at once (dropping it would
352
- * leave a shorter one), so every character advances at least one sequence,
353
- * and the five moves below are all the ways that can happen.
75
+ * A path both globs match, or `null` when there is none: the 0.1 interface,
76
+ * kept for the library's callers. It cannot say `undecided`, so a search that
77
+ * meets its budget throws; {@link globWitness} answers in three ways.
354
78
  */
355
- export function intersectTokens(x, y) {
356
- const memo = new Map();
357
- const width = y.length + 1;
358
- const visit = (i, j) => {
359
- const key = i * width + j;
360
- const cached = memo.get(key);
361
- if (cached !== undefined)
362
- return cached;
363
- const result = step(i, j);
364
- memo.set(key, result);
365
- return result;
366
- };
367
- const prepend = (head, tail) => head === null || tail === null ? null : head + tail;
368
- const step = (i, j) => {
369
- if (i === x.length && j === y.length)
370
- return '';
371
- const a = x[i];
372
- const b = y[j];
373
- if (a?.kind === 'star') {
374
- const skip = visit(i + 1, j);
375
- if (skip !== null)
376
- return skip;
377
- if (b === undefined)
378
- return null;
379
- if (b.kind === 'star')
380
- return visit(i, j + 1);
381
- return prepend(witnessChar(b), visit(i, j + 1));
382
- }
383
- if (b?.kind === 'star') {
384
- const skip = visit(i, j + 1);
385
- if (skip !== null)
386
- return skip;
387
- if (a === undefined)
388
- return null;
389
- return prepend(witnessChar(a), visit(i + 1, j));
390
- }
391
- if (a === undefined || b === undefined)
392
- return null;
393
- return prepend(meet(a, b), visit(i + 1, j + 1));
394
- };
395
- const witness = visit(0, 0);
396
- // Empty only when both sides are nothing but stars, and then any name will do.
397
- return witness === '' ? 'x' : witness;
398
- }
399
- function accepts(token, point) {
400
- if (point === SLASH)
401
- return false;
402
- switch (token.kind) {
403
- case 'literal':
404
- return token.char.codePointAt(0) === point;
405
- case 'any':
406
- return true;
407
- case 'class':
408
- return inClass(token, point);
79
+ export function intersectGlobs(a, b, budget = WITNESS_BUDGET) {
80
+ const witness = globWitness([a, b], [], budget);
81
+ if (witness.kind === 'undecided') {
82
+ throw new Error(`whether "${a.source}" and "${b.source}" meet is undecided within the search's budget`);
409
83
  }
84
+ return witness.kind === 'found' ? witness.path : null;
410
85
  }
411
86
  /**
412
- * Every point where the set of characters all the tokens accept could begin.
413
- *
414
- * Each token accepts a union of intervals: a literal one point, a positive
415
- * class its ranges, a negated class and `?` everything outside theirs. Less the
416
- * slash. The least character of an intersection of such sets is the left end
417
- * of one of its intervals, so it is one of these points, and checking them
418
- * all decides the intersection exactly. `0` is also the point after the
419
- * slash, and readable characters go first, so that a witness is one a person
420
- * would type.
87
+ * The directories a walk must enter to find every match, one per brace
88
+ * alternative: the leading literal segments, or a literal file's directory.
89
+ * `''` for an alternative that can match at the root.
421
90
  */
422
- function candidates(tokens) {
423
- const points = [0x78, 0x61, 0x30, 0x5f, 0x2d, 0x21, 0];
424
- for (const token of tokens) {
425
- if (token.kind === 'literal')
426
- points.push(token.char.codePointAt(0));
427
- if (token.kind === 'class') {
428
- for (const [lo, hi] of token.ranges)
429
- points.push(lo, hi + 1);
430
- }
431
- }
432
- return points;
91
+ export function globBases(glob) {
92
+ return glob.compiled.bases;
433
93
  }
434
- /** A character every token accepts, or `null` when there is none. */
435
- function pick(tokens) {
436
- for (const point of candidates(tokens)) {
437
- if (point <= 0x10ffff && tokens.every((t) => accepts(t, point)))
438
- return String.fromCodePoint(point);
439
- }
440
- return null;
94
+ /** The first of {@link globBases}: the 0.1 interface, kept for the library's callers. */
95
+ export function globBase(glob) {
96
+ // One base per alternative, and a pattern has at least one.
97
+ return glob.compiled.bases[0];
441
98
  }
442
- function witnessChar(token) {
443
- return pick([token]);
99
+ /** A name with an extension, `login.ts` or `.eslintrc.json`, and not a dot-name such as `.github`. */
100
+ export function hasExtension(name) {
101
+ return /.\.[^.]+$/.test(name);
102
+ }
103
+ const trees = new WeakMap();
104
+ /** The tree a list of files describes. Built once per list, which every brief of a run shares. */
105
+ export function treeOf(files) {
106
+ const known = trees.get(files);
107
+ if (known !== undefined)
108
+ return known;
109
+ const directories = new Set();
110
+ for (const file of files) {
111
+ for (let slash = file.indexOf('/'); slash !== -1; slash = file.indexOf('/', slash + 1))
112
+ directories.add(file.slice(0, slash));
113
+ }
114
+ const tree = { files: new Set(files), directories };
115
+ trees.set(files, tree);
116
+ return tree;
444
117
  }
445
- function meet(a, b) {
446
- return pick([a, b]);
118
+ /** What the tree holds at a path: a file, a directory, or nothing. */
119
+ export function held(tree, path) {
120
+ if (tree.files.has(path))
121
+ return 'file';
122
+ return tree.directories.has(path) ? 'directory' : null;
447
123
  }
448
124
  /**
449
- * The directory a glob is rooted in: its leading literal segments. A pattern
450
- * that names a file is rooted in that file's directory.
125
+ * How a tree reads a literal path: a directory it holds is a directory, and
126
+ * anything else is a file. Without a tree nothing is known to be a directory,
127
+ * so every literal is a file; a trailing `/` says otherwise, and never reaches
128
+ * this question.
451
129
  */
452
- export function globBase(glob) {
453
- const first = glob.alternatives[0];
454
- const literal = [];
455
- for (const segment of first) {
456
- if (segment.kind !== 'pattern' || !segment.literal)
457
- break;
458
- literal.push(segment.tokens.map((t) => (t.kind === 'literal' ? t.char : '')).join(''));
459
- }
460
- // A literal file path is rooted in its directory; a literal directory in itself.
461
- if (literal.length === first.length)
462
- literal.pop();
463
- return literal.join('/');
130
+ export function readingIn(files) {
131
+ if (files === null)
132
+ return () => 'file';
133
+ const tree = treeOf(files);
134
+ return (path) => (held(tree, path) === 'directory' ? 'directory' : 'file');
464
135
  }
465
136
  //# sourceMappingURL=glob.js.map