@descent-vtt/spec-brief 0.2.3 → 0.2.5

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 CHANGED
@@ -2,6 +2,69 @@
2
2
 
3
3
  Notable changes, newest first. Versions follow [semver](https://semver.org).
4
4
 
5
+ ## 0.2.5
6
+
7
+ spec-core at f9ce375. A trailing `/` on a brace alternative means that
8
+ directory's contents, as one written alone does: `{src/newmod/,lib}` scopes
9
+ what `src/newmod/` and `lib` scope, so a collision inside a directory the
10
+ round creates is no longer missed.
11
+
12
+ ### Changed
13
+
14
+ - spec-core at f9ce375. Its glob reads a trailing `/` on a brace
15
+ alternative as it reads one on the whole pattern. In a scope,
16
+ `{src/newmod/,lib}` covers `src/newmod/index.ts` and not `src/newmod`,
17
+ whatever the tree holds. `src/newmod` was a literal read from the tree,
18
+ and while the tree did not hold the directory, or with no tree, it
19
+ covered only a file of that name: `matrix` missed a collision with a
20
+ brief writing `src/newmod/index.ts`, which it now reports, and `schedule`
21
+ now keeps the two briefs apart. A directory the tree holds reads as
22
+ before.
23
+ - `literal-read-as-file` no longer names what a trailing `/` says is a
24
+ directory: `{src/newmod/,lib}` in a tree holding neither names `lib`
25
+ alone, where it named `src/newmod` and `lib`. A pattern left with no
26
+ bare name to place, `{src/newmod/,docs/new.md}` in a tree holding
27
+ neither, is `glob-matches-nothing`'s, as `src/newmod/` alone is, where it
28
+ was noted for `src/newmod`.
29
+ - `archive`, which reads a literal as either, no longer takes a changed file
30
+ at `build` itself as inside `{build/,Makefile}`, protected or in scope, as
31
+ it never took one inside `build/`.
32
+ - In the library, `parseGlob(...).glob.literals` leaves out an alternative
33
+ written with a trailing `/`, and the base `globBases` gives for
34
+ `{src/newmod/,lib}` is `src/newmod` whatever the tree holds, where it was
35
+ `src` while the tree did not hold the directory.
36
+
37
+ ## 0.2.4
38
+
39
+ spec-core at 65ef842. Archival rewrites a link reference definition only
40
+ where CommonMark reads one, so text shaped like a definition - under a
41
+ paragraph's text, in alt text, in a wiki link's text - stays as written.
42
+
43
+ ### Changed
44
+
45
+ - spec-core at 65ef842. Its Markdown scanner reads a link reference
46
+ definition as CommonMark does. A definition cannot interrupt a paragraph:
47
+ `[r]: r.md` on the line under a paragraph's text, a lazy line in a block
48
+ quote or a list item included, is that text, and `archive` and
49
+ `unarchive` leave it as written, where they rewrote `r.md`. A label holds
50
+ no unescaped bracket and at most 999 characters: `[[r]: r.md](z.md)` is a
51
+ link to `z.md`, which they rewrite, where they rewrote `r.md](z.md)` as
52
+ the destination of a definition, and `[a\]b]: x.md` is a definition,
53
+ whose `x.md` they now rewrite.
54
+ - References follow the definitions. With `[r]: r.md` written under text,
55
+ `[x][r](y.md)` is a link to `y.md`, which archival now rewrites. A second
56
+ bracket holding a bracket is no label, so with `[r]` defined,
57
+ `[x [r][a[b]c] y](o.md)` holds the reference `[r]`, and `(o.md)` is text
58
+ and stays as written, as around any link inside a link.
59
+
60
+ ### Fixed
61
+
62
+ - Text shaped like a definition at the start of an image's alt text or a
63
+ wiki link's text, `![[r]: r.md](z.png)` or `[[[r]: r.md]]`, is no longer
64
+ rewritten by `archive`. spec-brief reads those texts again on their own,
65
+ where such text opens a document and reads as a definition; CommonMark
66
+ reads none there. `z.png` is rewritten as before.
67
+
5
68
  ## 0.2.3
6
69
 
7
70
  spec-core at 119345e. A link inside a link now counts only the inner one, as
package/README.md CHANGED
@@ -52,7 +52,7 @@ Work put off for later is a brief too, with `status: deferred` and a `trigger` n
52
52
 
53
53
  The id comes from the file name (`012_rotate-session-tokens.md`) or from an `id` field. `affectedFiles` is the scope the round may write; `protectedFiles` is what it is not empowered to change, and archival refuses a round that changed it. Protection wins: what a round may write is `affectedFiles` less `protectedFiles`, so "all of `src/` but the schema" is `affectedFiles: [src/**]` with `protectedFiles: [src/db/schema.ts]`.
54
54
 
55
- Scopes are globs in spec-core's `path` dialect, the one every spec-\* tool reads: `*`, `?`, `[a-z]` and `{a,b}` within a name, `**` for any number of directories, compared case-sensitively on every host. `**` is a whole segment: `docs/**.md` is refused, since tools read it three ways - write `docs/**/*.md` for any depth, or `docs/*.md` for one level. Parentheses are literal, `C++(notes).md`, unless a group holds a `|`: an extended glob such as `+(a|b)` is refused, and written `{a,b}`. A trailing `/**` or `/` is a directory's contents - at least one name below it, never the directory itself. A path with no glob syntax is read from the tree: a file the tree holds is that file, a directory it holds is everything beneath it, and a path it does not hold yet is a file unless it ends in `/`. Write `src/newmod/` for a directory the round creates; `lint` notes a bare `src/newmod` it cannot place.
55
+ Scopes are globs in spec-core's `path` dialect, the one every spec-\* tool reads: `*`, `?`, `[a-z]` and `{a,b}` within a name, `**` for any number of directories, compared case-sensitively on every host. `**` is a whole segment: `docs/**.md` is refused, since tools read it three ways - write `docs/**/*.md` for any depth, or `docs/*.md` for one level. Parentheses are literal, `C++(notes).md`, unless a group holds a `|`: an extended glob such as `+(a|b)` is refused, and written `{a,b}`. A trailing `/**` or `/` is a directory's contents - at least one name below it, never the directory itself - on a brace alternative as on the whole pattern: `{src/,lib}` is `src/` or `lib`. A path with no glob syntax is read from the tree: a file the tree holds is that file, a directory it holds is everything beneath it, and a path it does not hold yet is a file unless it ends in `/`. Write `src/newmod/` for a directory the round creates; `lint` notes a bare `src/newmod` it cannot place.
56
56
 
57
57
  ## Commands
58
58
 
@@ -200,7 +200,7 @@ A repository whose briefs have eight ordered sections, `proposed` and `archived`
200
200
 
201
201
  Section names compare without case, typographic quotes, emphasis, a leading number or a trailing colon, and a heading may add a qualifier after a separator: `## 2. Commander’s Intent:` fills `Commander's Intent`, and `## Invariants (must hold)` fills `Invariants`.
202
202
 
203
- A brief is read as CommonMark reads it wherever that decides what is code or a comment - fenced and indented code, `<pre>`, `<script>`, `<style>` and `<textarea>` blocks, code spans, HTML comments - and nothing inside those is structure. A section, and the title, is an ATX heading, `## Name`, outside a block quote: prose over a `---` line is prose and a rule, and a heading quoted from another document is not one of the brief's sections. A task is a list item outside a block quote with `[ ]`, `[x]` or `[X]`. The links archival rewrites are the ones that write a destination: `[text](dest)`, `![alt](dest)` - one inside a link's text too - and a definition, `[label]: dest`, through which a reference is rewritten once.
203
+ A brief is read as CommonMark reads it wherever that decides what is code or a comment - fenced and indented code, `<pre>`, `<script>`, `<style>` and `<textarea>` blocks, code spans, HTML comments - and nothing inside those is structure. A section, and the title, is an ATX heading, `## Name`, outside a block quote: prose over a `---` line is prose and a rule, and a heading quoted from another document is not one of the brief's sections. A task is a list item outside a block quote with `[ ]`, `[x]` or `[X]`. The links archival rewrites are the ones that write a destination: `[text](dest)`, `![alt](dest)` - one inside a link's text too - and a definition, `[label]: dest`, through which a reference is rewritten once. A definition is one where CommonMark reads it, opening a paragraph or under another: the shape of one under a paragraph's text, or in alt text, is text and stays as written.
204
204
 
205
205
  ## Rules
206
206
 
package/dist/glob.d.ts CHANGED
@@ -17,7 +17,9 @@
17
17
  * The tree, not the spelling, knows that `Dockerfile` is a file and
18
18
  * `docs/v1.2` a directory (ADR-0005, amended 2026-09-26).
19
19
  * - A trailing `/**` is at least one segment, so `docs/**` is the directory's
20
- * contents and a collision witness is always a file.
20
+ * contents and a collision witness is always a file. A trailing `/` means
21
+ * the same on a brace alternative as on the whole pattern: `{src/,lib}` is
22
+ * `src/` or `lib`, and the tree is asked about `lib` alone.
21
23
  *
22
24
  * Nothing here compiles to a `RegExp`. Matching costs at most the pattern's
23
25
  * size times the path's length, and a question about two scopes that cannot be
@@ -34,7 +36,10 @@ export interface Glob {
34
36
  readonly source: string;
35
37
  /** The compiled pattern: spec-core's `path` dialect, case-sensitive. */
36
38
  readonly compiled: CoreGlob;
37
- /** Each path the pattern names with no glob syntax, one per brace alternative; empty when it has none. */
39
+ /**
40
+ * Each path the pattern names with no glob syntax and no trailing `/`, one
41
+ * per brace alternative; empty when it has none.
42
+ */
38
43
  readonly literals: readonly Literal[];
39
44
  }
40
45
  export type GlobParse = {
package/dist/glob.js CHANGED
@@ -17,13 +17,15 @@
17
17
  * The tree, not the spelling, knows that `Dockerfile` is a file and
18
18
  * `docs/v1.2` a directory (ADR-0005, amended 2026-09-26).
19
19
  * - A trailing `/**` is at least one segment, so `docs/**` is the directory's
20
- * contents and a collision witness is always a file.
20
+ * contents and a collision witness is always a file. A trailing `/` means
21
+ * the same on a brace alternative as on the whole pattern: `{src/,lib}` is
22
+ * `src/` or `lib`, and the tree is asked about `lib` alone.
21
23
  *
22
24
  * Nothing here compiles to a `RegExp`. Matching costs at most the pattern's
23
25
  * size times the path's length, and a question about two scopes that cannot be
24
26
  * answered within the search's budget answers `undecided`, never a guess.
25
27
  */
26
- import { AutomatonTooLarge, globCovers as coreCovers, globWitness as coreWitness, isGlobSyntax, parseGlob as coreParse, WITNESS_BUDGET, } from './vendor/spec-core/pattern/index.js';
28
+ import { globCovers as coreCovers, globWitness as coreWitness, isGlobSyntax, parseGlob as coreParse, WITNESS_BUDGET, } from './vendor/spec-core/pattern/index.js';
27
29
  import { segments } from './vendor/spec-core/path/index.js';
28
30
  export { isGlobSyntax, WITNESS_BUDGET };
29
31
  /** Parses a scope pattern, or says why it cannot. */
@@ -40,16 +42,10 @@ export function parseGlob(source, options = {}) {
40
42
  literals.push({ path, reading });
41
43
  return reading;
42
44
  };
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;
46
- }
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;
52
- }
45
+ // A pattern too large to compile is refused by the core, with its reason,
46
+ // as a malformed one is; what the reading throws is a defect, and passes on.
47
+ const parsed = coreParse(pattern, { dialect: 'path', caseSensitive: true, literal });
48
+ return parsed.ok ? { ok: true, glob: { source, compiled: parsed.glob, literals } } : parsed;
53
49
  }
54
50
  /**
55
51
  * Whether a path matches. The path is read as a repository path: empty and
package/dist/glob.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"glob.js","sourceRoot":"","sources":["../src/glob.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,OAAO,EACL,iBAAiB,EACjB,UAAU,IAAI,UAAU,EACxB,WAAW,IAAI,WAAW,EAC1B,YAAY,EACZ,SAAS,IAAI,SAAS,EAGtB,cAAc,GAEf,MAAM,qCAAqC,CAAC;AAC7C,OAAO,EAAE,QAAQ,EAAE,MAAM,kCAAkC,CAAC;AAE5D,OAAO,EAAE,YAAY,EAAuB,cAAc,EAAgB,CAAC;AA4B3E,qDAAqD;AACrD,MAAM,UAAU,SAAS,CAAC,MAAc,EAAE,OAAO,GAAiB,EAAE;IAClE,MAAM,OAAO,GAAG,MAAM,CAAC,IAAI,EAAE,CAAC;IAC9B,IAAI,OAAO,CAAC,UAAU,CAAC,GAAG,CAAC;QAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,iEAAiE,EAAE,CAAC;IAC5H,IAAI,OAAO,CAAC,UAAU,CAAC,GAAG,CAAC;QAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,wEAAwE,EAAE,CAAC;IACnI,MAAM,KAAK,GAAG,OAAO,CAAC,OAAO,IAAI,MAAM,CAAC;IACxC,MAAM,QAAQ,GAAc,EAAE,CAAC;IAC/B,MAAM,OAAO,GAAG,CAAC,IAAY,EAAkB,EAAE;QAC/C,MAAM,OAAO,GAAG,OAAO,KAAK,KAAK,UAAU,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC;QAClE,QAAQ,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,CAAC;QACjC,OAAO,OAAO,CAAC;IACjB,CAAC,CAAC;IACF,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,SAAS,CAAC,OAAO,EAAE,EAAE,OAAO,EAAE,MAAM,EAAE,aAAa,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,CAAC;QACrF,OAAO,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,CAAC,IAAI,EAAE,QAAQ,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC;IAC9F,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,mFAAmF;QACnF,IAAI,KAAK,YAAY,iBAAiB;YAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,CAAC,OAAO,EAAE,CAAC;QACnF,MAAM,KAAK,CAAC;IACd,CAAC;AACH,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,SAAS,CAAC,IAAU,EAAE,IAAY;IAChD,MAAM,SAAS,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAC3C,OAAO,SAAS,KAAK,EAAE,IAAI,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC;AAC5D,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,WAAW,CAAC,OAAwB,EAAE,OAAO,GAAoB,EAAE,EAAE,MAAM,GAAW,cAAc;IAClH,OAAO,WAAW,CAChB,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,EAC9B,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,EAC9B,MAAM,CACP,CAAC;AACJ,CAAC;AAED,uFAAuF;AACvF,MAAM,UAAU,UAAU,CAAC,KAAsB,EAAE,KAAW,EAAE,MAAM,GAAW,cAAc;IAC7F,OAAO,UAAU,CACf,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,EAC5B,KAAK,CAAC,QAAQ,EACd,MAAM,CACP,CAAC;AACJ,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,cAAc,CAAC,CAAO,EAAE,CAAO,EAAE,MAAM,GAAW,cAAc;IAC9E,MAAM,OAAO,GAAG,WAAW,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,CAAC,CAAC;IAChD,IAAI,OAAO,CAAC,IAAI,KAAK,WAAW,EAAE,CAAC;QACjC,MAAM,IAAI,KAAK,CAAC,YAAY,CAAC,CAAC,MAAM,UAAU,CAAC,CAAC,MAAM,gDAAgD,CAAC,CAAC;IAC1G,CAAC;IACD,OAAO,OAAO,CAAC,IAAI,KAAK,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC;AACxD,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,SAAS,CAAC,IAAU;IAClC,OAAO,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC;AAC7B,CAAC;AAED,yFAAyF;AACzF,MAAM,UAAU,QAAQ,CAAC,IAAU;IACjC,4DAA4D;IAC5D,OAAO,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAW,CAAC;AAC1C,CAAC;AAED,sGAAsG;AACtG,MAAM,UAAU,YAAY,CAAC,IAAY;IACvC,OAAO,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAChC,CAAC;AAQD,MAAM,KAAK,GAAG,IAAI,OAAO,EAA2B,CAAC;AAErD,kGAAkG;AAClG,MAAM,UAAU,MAAM,CAAC,KAAwB;IAC7C,MAAM,KAAK,GAAG,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;IAC/B,IAAI,KAAK,KAAK,SAAS;QAAE,OAAO,KAAK,CAAC;IACtC,MAAM,WAAW,GAAG,IAAI,GAAG,EAAU,CAAC;IACtC,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,KAAK,IAAI,KAAK,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,KAAK,KAAK,CAAC,CAAC,EAAE,KAAK,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,KAAK,GAAG,CAAC,CAAC;YAAE,WAAW,CAAC,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC;IAChI,CAAC;IACD,MAAM,IAAI,GAAG,EAAE,KAAK,EAAE,IAAI,GAAG,CAAC,KAAK,CAAC,EAAE,WAAW,EAAE,CAAC;IACpD,KAAK,CAAC,GAAG,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;IACvB,OAAO,IAAI,CAAC;AACd,CAAC;AAED,sEAAsE;AACtE,MAAM,UAAU,IAAI,CAAC,IAAU,EAAE,IAAY;IAC3C,IAAI,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC;QAAE,OAAO,MAAM,CAAC;IACxC,OAAO,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,IAAI,CAAC;AACzD,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,SAAS,CAAC,KAA+B;IACvD,IAAI,KAAK,KAAK,IAAI;QAAE,OAAO,GAAG,EAAE,CAAC,MAAM,CAAC;IACxC,MAAM,IAAI,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC;IAC3B,OAAO,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,KAAK,WAAW,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC;AAC7E,CAAC","sourcesContent":["/**\n * Scopes as globs: spec-core's `path` dialect, read the way spec-brief reads a\n * brief.\n *\n * The syntax, the automaton and the witness search are spec-core's, copied\n * into `src/vendor/` and verified by hash, so the family's tools read one\n * pattern the same way (spec-core ADR-0003). What is decided here is what\n * spec-brief adds around it:\n *\n * - A pattern is relative to the repository root, so a leading `/` is refused\n * rather than rooted at the filesystem; and a list entry's `!` has no list to\n * belong to in a scope, so it is refused rather than read.\n * - Paths compare case-sensitively on every host, as git's do.\n * - A path with no glob syntax is read as the tree reads it: a file the tree\n * holds is that file, a directory it holds is everything beneath it, and a\n * path it does not hold is a file, unless it is written with a trailing `/`.\n * The tree, not the spelling, knows that `Dockerfile` is a file and\n * `docs/v1.2` a directory (ADR-0005, amended 2026-09-26).\n * - A trailing `/**` is at least one segment, so `docs/**` is the directory's\n * contents and a collision witness is always a file.\n *\n * Nothing here compiles to a `RegExp`. Matching costs at most the pattern's\n * size times the path's length, and a question about two scopes that cannot be\n * answered within the search's budget answers `undecided`, never a guess.\n */\n\nimport {\n AutomatonTooLarge,\n globCovers as coreCovers,\n globWitness as coreWitness,\n isGlobSyntax,\n parseGlob as coreParse,\n type Glob as CoreGlob,\n type LiteralReading,\n WITNESS_BUDGET,\n type Witness,\n} from './vendor/spec-core/pattern/index.js';\nimport { segments } from './vendor/spec-core/path/index.js';\n\nexport { isGlobSyntax, type LiteralReading, WITNESS_BUDGET, type Witness };\n\n/** A literal path a pattern names, and how it was read. */\nexport interface Literal {\n readonly path: string;\n readonly reading: LiteralReading;\n}\n\nexport interface Glob {\n readonly source: string;\n /** The compiled pattern: spec-core's `path` dialect, case-sensitive. */\n readonly compiled: CoreGlob;\n /** Each path the pattern names with no glob syntax, one per brace alternative; empty when it has none. */\n readonly literals: readonly Literal[];\n}\n\nexport type GlobParse = { readonly ok: true; readonly glob: Glob } | { readonly ok: false; readonly error: string };\n\nexport interface ParseOptions {\n /**\n * How a path with no glob syntax is read: `file`, that path alone;\n * `directory`, everything beneath it; `either`, both. A function is asked\n * per literal path, as {@link readingIn} answers for a tree. `file` when\n * unset: a path nothing says is a directory is not read as one.\n */\n readonly literal?: LiteralReading | ((path: string) => LiteralReading) | undefined;\n}\n\n/** Parses a scope pattern, or says why it cannot. */\nexport function parseGlob(source: string, options: ParseOptions = {}): GlobParse {\n const pattern = source.trim();\n if (pattern.startsWith('!')) return { ok: false, error: 'negated patterns are not supported; narrow the positive pattern' };\n if (pattern.startsWith('/')) return { ok: false, error: 'a pattern is relative to the repository root and cannot start with \"/\"' };\n const given = options.literal ?? 'file';\n const literals: Literal[] = [];\n const literal = (path: string): LiteralReading => {\n const reading = typeof given === 'function' ? given(path) : given;\n literals.push({ path, reading });\n return reading;\n };\n try {\n const parsed = coreParse(pattern, { dialect: 'path', caseSensitive: true, literal });\n return parsed.ok ? { ok: true, glob: { source, compiled: parsed.glob, literals } } : parsed;\n } catch (error) {\n // Too many states is a pattern nobody meant; anything else is a defect to surface.\n if (error instanceof AutomatonTooLarge) return { ok: false, error: error.message };\n throw error;\n }\n}\n\n/**\n * Whether a path matches. The path is read as a repository path: empty and\n * `.` segments are dropped, so `./src//a.ts` is `src/a.ts`.\n */\nexport function matchGlob(glob: Glob, path: string): boolean {\n const canonical = segments(path).join('/');\n return canonical !== '' && glob.compiled.match(canonical);\n}\n\n/**\n * A shortest path every glob in `include` matches and none in `exclude` does,\n * `none` as a proof that there is no such path, or `undecided` when the search\n * met its budget first.\n */\nexport function globWitness(include: readonly Glob[], exclude: readonly Glob[] = [], budget: number = WITNESS_BUDGET): Witness {\n return coreWitness(\n include.map((g) => g.compiled),\n exclude.map((g) => g.compiled),\n budget,\n );\n}\n\n/** Whether every path `inner` matches is matched by one of `outer`, or `undecided`. */\nexport function globCovers(outer: readonly Glob[], inner: Glob, budget: number = WITNESS_BUDGET): boolean | 'undecided' {\n return coreCovers(\n outer.map((g) => g.compiled),\n inner.compiled,\n budget,\n );\n}\n\n/**\n * A path both globs match, or `null` when there is none: the 0.1 interface,\n * kept for the library's callers. It cannot say `undecided`, so a search that\n * meets its budget throws; {@link globWitness} answers in three ways.\n */\nexport function intersectGlobs(a: Glob, b: Glob, budget: number = WITNESS_BUDGET): string | null {\n const witness = globWitness([a, b], [], budget);\n if (witness.kind === 'undecided') {\n throw new Error(`whether \"${a.source}\" and \"${b.source}\" meet is undecided within the search's budget`);\n }\n return witness.kind === 'found' ? witness.path : null;\n}\n\n/**\n * The directories a walk must enter to find every match, one per brace\n * alternative: the leading literal segments, or a literal file's directory.\n * `''` for an alternative that can match at the root.\n */\nexport function globBases(glob: Glob): readonly string[] {\n return glob.compiled.bases;\n}\n\n/** The first of {@link globBases}: the 0.1 interface, kept for the library's callers. */\nexport function globBase(glob: Glob): string {\n // One base per alternative, and a pattern has at least one.\n return glob.compiled.bases[0] as string;\n}\n\n/** A name with an extension, `login.ts` or `.eslintrc.json`, and not a dot-name such as `.github`. */\nexport function hasExtension(name: string): boolean {\n return /.\\.[^.]+$/.test(name);\n}\n\n/** The files a tree holds, and every directory above one. */\nexport interface Tree {\n readonly files: ReadonlySet<string>;\n readonly directories: ReadonlySet<string>;\n}\n\nconst trees = new WeakMap<readonly string[], Tree>();\n\n/** The tree a list of files describes. Built once per list, which every brief of a run shares. */\nexport function treeOf(files: readonly string[]): Tree {\n const known = trees.get(files);\n if (known !== undefined) return known;\n const directories = new Set<string>();\n for (const file of files) {\n for (let slash = file.indexOf('/'); slash !== -1; slash = file.indexOf('/', slash + 1)) directories.add(file.slice(0, slash));\n }\n const tree = { files: new Set(files), directories };\n trees.set(files, tree);\n return tree;\n}\n\n/** What the tree holds at a path: a file, a directory, or nothing. */\nexport function held(tree: Tree, path: string): 'file' | 'directory' | null {\n if (tree.files.has(path)) return 'file';\n return tree.directories.has(path) ? 'directory' : null;\n}\n\n/**\n * How a tree reads a literal path: a directory it holds is a directory, and\n * anything else is a file. Without a tree nothing is known to be a directory,\n * so every literal is a file; a trailing `/` says otherwise, and never reaches\n * this question.\n */\nexport function readingIn(files: readonly string[] | null): (path: string) => LiteralReading {\n if (files === null) return () => 'file';\n const tree = treeOf(files);\n return (path) => (held(tree, path) === 'directory' ? 'directory' : 'file');\n}\n"]}
1
+ {"version":3,"file":"glob.js","sourceRoot":"","sources":["../src/glob.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Scopes as globs: spec-core's `path` dialect, read the way spec-brief reads a\n * brief.\n *\n * The syntax, the automaton and the witness search are spec-core's, copied\n * into `src/vendor/` and verified by hash, so the family's tools read one\n * pattern the same way (spec-core ADR-0003). What is decided here is what\n * spec-brief adds around it:\n *\n * - A pattern is relative to the repository root, so a leading `/` is refused\n * rather than rooted at the filesystem; and a list entry's `!` has no list to\n * belong to in a scope, so it is refused rather than read.\n * - Paths compare case-sensitively on every host, as git's do.\n * - A path with no glob syntax is read as the tree reads it: a file the tree\n * holds is that file, a directory it holds is everything beneath it, and a\n * path it does not hold is a file, unless it is written with a trailing `/`.\n * The tree, not the spelling, knows that `Dockerfile` is a file and\n * `docs/v1.2` a directory (ADR-0005, amended 2026-09-26).\n * - A trailing `/**` is at least one segment, so `docs/**` is the directory's\n * contents and a collision witness is always a file. A trailing `/` means\n * the same on a brace alternative as on the whole pattern: `{src/,lib}` is\n * `src/` or `lib`, and the tree is asked about `lib` alone.\n *\n * Nothing here compiles to a `RegExp`. Matching costs at most the pattern's\n * size times the path's length, and a question about two scopes that cannot be\n * answered within the search's budget answers `undecided`, never a guess.\n */\n\nimport {\n globCovers as coreCovers,\n globWitness as coreWitness,\n isGlobSyntax,\n parseGlob as coreParse,\n type Glob as CoreGlob,\n type LiteralReading,\n WITNESS_BUDGET,\n type Witness,\n} from './vendor/spec-core/pattern/index.js';\nimport { segments } from './vendor/spec-core/path/index.js';\n\nexport { isGlobSyntax, type LiteralReading, WITNESS_BUDGET, type Witness };\n\n/** A literal path a pattern names, and how it was read. */\nexport interface Literal {\n readonly path: string;\n readonly reading: LiteralReading;\n}\n\nexport interface Glob {\n readonly source: string;\n /** The compiled pattern: spec-core's `path` dialect, case-sensitive. */\n readonly compiled: CoreGlob;\n /**\n * Each path the pattern names with no glob syntax and no trailing `/`, one\n * per brace alternative; empty when it has none.\n */\n readonly literals: readonly Literal[];\n}\n\nexport type GlobParse = { readonly ok: true; readonly glob: Glob } | { readonly ok: false; readonly error: string };\n\nexport interface ParseOptions {\n /**\n * How a path with no glob syntax is read: `file`, that path alone;\n * `directory`, everything beneath it; `either`, both. A function is asked\n * per literal path, as {@link readingIn} answers for a tree. `file` when\n * unset: a path nothing says is a directory is not read as one.\n */\n readonly literal?: LiteralReading | ((path: string) => LiteralReading) | undefined;\n}\n\n/** Parses a scope pattern, or says why it cannot. */\nexport function parseGlob(source: string, options: ParseOptions = {}): GlobParse {\n const pattern = source.trim();\n if (pattern.startsWith('!')) return { ok: false, error: 'negated patterns are not supported; narrow the positive pattern' };\n if (pattern.startsWith('/')) return { ok: false, error: 'a pattern is relative to the repository root and cannot start with \"/\"' };\n const given = options.literal ?? 'file';\n const literals: Literal[] = [];\n const literal = (path: string): LiteralReading => {\n const reading = typeof given === 'function' ? given(path) : given;\n literals.push({ path, reading });\n return reading;\n };\n // A pattern too large to compile is refused by the core, with its reason,\n // as a malformed one is; what the reading throws is a defect, and passes on.\n const parsed = coreParse(pattern, { dialect: 'path', caseSensitive: true, literal });\n return parsed.ok ? { ok: true, glob: { source, compiled: parsed.glob, literals } } : parsed;\n}\n\n/**\n * Whether a path matches. The path is read as a repository path: empty and\n * `.` segments are dropped, so `./src//a.ts` is `src/a.ts`.\n */\nexport function matchGlob(glob: Glob, path: string): boolean {\n const canonical = segments(path).join('/');\n return canonical !== '' && glob.compiled.match(canonical);\n}\n\n/**\n * A shortest path every glob in `include` matches and none in `exclude` does,\n * `none` as a proof that there is no such path, or `undecided` when the search\n * met its budget first.\n */\nexport function globWitness(include: readonly Glob[], exclude: readonly Glob[] = [], budget: number = WITNESS_BUDGET): Witness {\n return coreWitness(\n include.map((g) => g.compiled),\n exclude.map((g) => g.compiled),\n budget,\n );\n}\n\n/** Whether every path `inner` matches is matched by one of `outer`, or `undecided`. */\nexport function globCovers(outer: readonly Glob[], inner: Glob, budget: number = WITNESS_BUDGET): boolean | 'undecided' {\n return coreCovers(\n outer.map((g) => g.compiled),\n inner.compiled,\n budget,\n );\n}\n\n/**\n * A path both globs match, or `null` when there is none: the 0.1 interface,\n * kept for the library's callers. It cannot say `undecided`, so a search that\n * meets its budget throws; {@link globWitness} answers in three ways.\n */\nexport function intersectGlobs(a: Glob, b: Glob, budget: number = WITNESS_BUDGET): string | null {\n const witness = globWitness([a, b], [], budget);\n if (witness.kind === 'undecided') {\n throw new Error(`whether \"${a.source}\" and \"${b.source}\" meet is undecided within the search's budget`);\n }\n return witness.kind === 'found' ? witness.path : null;\n}\n\n/**\n * The directories a walk must enter to find every match, one per brace\n * alternative: the leading literal segments, or a literal file's directory.\n * `''` for an alternative that can match at the root.\n */\nexport function globBases(glob: Glob): readonly string[] {\n return glob.compiled.bases;\n}\n\n/** The first of {@link globBases}: the 0.1 interface, kept for the library's callers. */\nexport function globBase(glob: Glob): string {\n // One base per alternative, and a pattern has at least one.\n return glob.compiled.bases[0] as string;\n}\n\n/** A name with an extension, `login.ts` or `.eslintrc.json`, and not a dot-name such as `.github`. */\nexport function hasExtension(name: string): boolean {\n return /.\\.[^.]+$/.test(name);\n}\n\n/** The files a tree holds, and every directory above one. */\nexport interface Tree {\n readonly files: ReadonlySet<string>;\n readonly directories: ReadonlySet<string>;\n}\n\nconst trees = new WeakMap<readonly string[], Tree>();\n\n/** The tree a list of files describes. Built once per list, which every brief of a run shares. */\nexport function treeOf(files: readonly string[]): Tree {\n const known = trees.get(files);\n if (known !== undefined) return known;\n const directories = new Set<string>();\n for (const file of files) {\n for (let slash = file.indexOf('/'); slash !== -1; slash = file.indexOf('/', slash + 1)) directories.add(file.slice(0, slash));\n }\n const tree = { files: new Set(files), directories };\n trees.set(files, tree);\n return tree;\n}\n\n/** What the tree holds at a path: a file, a directory, or nothing. */\nexport function held(tree: Tree, path: string): 'file' | 'directory' | null {\n if (tree.files.has(path)) return 'file';\n return tree.directories.has(path) ? 'directory' : null;\n}\n\n/**\n * How a tree reads a literal path: a directory it holds is a directory, and\n * anything else is a file. Without a tree nothing is known to be a directory,\n * so every literal is a file; a trailing `/` says otherwise, and never reaches\n * this question.\n */\nexport function readingIn(files: readonly string[] | null): (path: string) => LiteralReading {\n if (files === null) return () => 'file';\n const tree = treeOf(files);\n return (path) => (held(tree, path) === 'directory' ? 'directory' : 'file');\n}\n"]}
@@ -15,9 +15,10 @@
15
15
  * - A link destination is one written in the link or in a definition: the
16
16
  * inline form and the definition, images included, an image inside a link's
17
17
  * text, and a link or an image in an image's alt text or a wiki link's text
18
- * too, each once. A link inside a link's text is the link, as CommonMark
19
- * reads it, and the brackets around it are text. A reference is rewritten
20
- * through its definition, once.
18
+ * too, each once, but no definition there, where CommonMark reads none. A
19
+ * link inside a link's text is the link, as CommonMark reads it, and the
20
+ * brackets around it are text. A reference is rewritten through its
21
+ * definition, once.
21
22
  *
22
23
  * Everything here is in 0-based lines of the brief's `lines`, and columns
23
24
  * within them.
package/dist/markdown.js CHANGED
@@ -15,9 +15,10 @@
15
15
  * - A link destination is one written in the link or in a definition: the
16
16
  * inline form and the definition, images included, an image inside a link's
17
17
  * text, and a link or an image in an image's alt text or a wiki link's text
18
- * too, each once. A link inside a link's text is the link, as CommonMark
19
- * reads it, and the brackets around it are text. A reference is rewritten
20
- * through its definition, once.
18
+ * too, each once, but no definition there, where CommonMark reads none. A
19
+ * link inside a link's text is the link, as CommonMark reads it, and the
20
+ * brackets around it are text. A reference is rewritten through its
21
+ * definition, once.
21
22
  *
22
23
  * Everything here is in 0-based lines of the brief's `lines`, and columns
23
24
  * within them.
@@ -31,27 +32,29 @@ function isBox(checkbox) {
31
32
  /**
32
33
  * The links that write their destination - inline links and images, and
33
34
  * definitions - with offsets into the text scanned `at` that offset, added to
34
- * `found` by the offset of the destination.
35
+ * `found`.
35
36
  *
36
37
  * spec-core reads a link's text as CommonMark does: a link inside it is the
37
38
  * link, so `[a [b](b.md)](c.md)` gives `b.md` alone, and an image inside it,
38
39
  * `[![diagram](d.png)](d.md)`, is listed after the link. It reads nothing in
39
40
  * an image's alt text or a wiki link's text, where a destination still moves
40
- * with the brief, so those two are read again here. Each is shorter than the
41
- * text it came from, so the reading ends.
41
+ * with the brief, so those two are read again here, as `inText`. Each is
42
+ * shorter than the text it came from, so the reading ends. Nor does spec-core
43
+ * list a definition inside one, since a bracket pairs across no definition's
44
+ * line, so each destination is found once, and no two rewrites meet on the
45
+ * same columns.
42
46
  *
43
- * spec-core also reads a definition at the start of any line, where
44
- * CommonMark lets none interrupt a paragraph, and so lists one that lies in
45
- * an image's alt text written over lines. Read again, the alt text gives it a
46
- * second time. It is one destination, kept once, because two rewrites of the
47
- * same columns would write the second over the first.
47
+ * Read on its own, such a text is a document, where `[r]: r.md` at its start
48
+ * is a definition. CommonMark reads none inside a paragraph's text, and a
49
+ * renderer shows `![[r]: r.md](z.png)` with that alt text as written, so a
50
+ * definition found there writes no destination.
48
51
  */
49
- function written(core, at, found) {
52
+ function written(core, at, inText, found) {
50
53
  for (const link of core.links) {
51
54
  if (link.image || link.form === 'wiki')
52
- written(scanMarkdown(link.text), at + core.text.indexOf(link.text, link.start), found);
53
- if (link.form === 'inline' || link.form === 'definition') {
54
- found.set(at + link.targetStart, { ...link, targetStart: at + link.targetStart, targetEnd: at + link.targetEnd });
55
+ written(scanMarkdown(link.text), at + core.text.indexOf(link.text, link.start), true, found);
56
+ if (link.form === 'inline' || (link.form === 'definition' && !inText)) {
57
+ found.push({ ...link, targetStart: at + link.targetStart, targetEnd: at + link.targetEnd });
55
58
  }
56
59
  }
57
60
  return found;
@@ -70,7 +73,7 @@ export function scan(lines) {
70
73
  tasks: core.listItems
71
74
  .filter((item) => isBox(item.checkbox) && item.quoteDepth === 0)
72
75
  .map((item) => ({ line: item.line - 1, end: item.endLine, checked: item.checkbox !== ' ', text: item.firstLine })),
73
- links: [...written(core, 0, new Map()).values()]
76
+ links: written(core, 0, false, [])
74
77
  .map((link) => {
75
78
  // A link's text may run over lines; its destination is on the last.
76
79
  const at = core.index.positionAt(link.targetStart);
@@ -1 +1 @@
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, and a link or an image in an image's alt text or a wiki link's text\n * too, each once. A link inside a link's text is the link, as CommonMark\n * reads it, and the brackets around it are text. A reference is rewritten\n * 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, each once, 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, added to\n * `found` by the offset of the destination.\n *\n * spec-core reads a link's text as CommonMark does: a link inside it is the\n * link, so `[a [b](b.md)](c.md)` gives `b.md` alone, and an image inside it,\n * `[![diagram](d.png)](d.md)`, is listed after the link. It reads nothing in\n * an image's alt text or a wiki link's text, where a destination still moves\n * with the brief, so those two are read again here. Each is shorter than the\n * text it came from, so the reading ends.\n *\n * spec-core also reads a definition at the start of any line, where\n * CommonMark lets none interrupt a paragraph, and so lists one that lies in\n * an image's alt text written over lines. Read again, the alt text gives it a\n * second time. It is one destination, kept once, because two rewrites of the\n * same columns would write the second over the first.\n */\nfunction written(core: MarkdownScan, at: number, found: Map<number, Link>): Map<number, Link> {\n for (const link of core.links) {\n if (link.image || link.form === 'wiki') written(scanMarkdown(link.text), at + core.text.indexOf(link.text, link.start), found);\n if (link.form === 'inline' || link.form === 'definition') {\n found.set(at + link.targetStart, { ...link, targetStart: at + link.targetStart, targetEnd: at + link.targetEnd });\n }\n }\n return found;\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, new Map()).values()]\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"]}
1
+ {"version":3,"file":"markdown.js","sourceRoot":"","sources":["../src/markdown.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,OAAO,EAAa,OAAO,EAAqB,YAAY,EAAoB,MAAM,sCAAsC,CAAC;AAC7H,OAAO,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AA8CxC,wDAAwD;AACxD,SAAS,KAAK,CAAC,QAAuB;IACpC,OAAO,QAAQ,KAAK,GAAG,IAAI,QAAQ,KAAK,GAAG,IAAI,QAAQ,KAAK,GAAG,CAAC;AAClE,CAAC;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,SAAS,OAAO,CAAC,IAAkB,EAAE,EAAU,EAAE,MAAe,EAAE,KAAa;IAC7E,KAAK,MAAM,IAAI,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC;QAC9B,IAAI,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,IAAI,KAAK,MAAM;YAAE,OAAO,CAAC,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,EAAE,GAAG,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,KAAK,CAAC,EAAE,IAAI,EAAE,KAAK,CAAC,CAAC;QACrI,IAAI,IAAI,CAAC,IAAI,KAAK,QAAQ,IAAI,CAAC,IAAI,CAAC,IAAI,KAAK,YAAY,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC;YACtE,KAAK,CAAC,IAAI,CAAC,EAAE,GAAG,IAAI,EAAE,WAAW,EAAE,EAAE,GAAG,IAAI,CAAC,WAAW,EAAE,SAAS,EAAE,EAAE,GAAG,IAAI,CAAC,SAAS,EAAE,CAAC,CAAC;QAC9F,CAAC;IACH,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,6DAA6D;AAC7D,MAAM,UAAU,IAAI,CAAC,KAAwB;IAC3C,MAAM,IAAI,GAAG,YAAY,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC,CAAC;IAC9C,MAAM,QAAQ,GAAG,CAAC,IAAY,EAAW,EAAE,CAAE,IAAI,CAAC,KAAK,CAAC,IAAI,GAAG,CAAC,CAAiB,CAAC,UAAU,KAAK,CAAC,CAAC;IACnG,OAAO;QACL,KAAK;QACL,KAAK,EAAE,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC;QAC7B,MAAM,EAAE,OAAO,CAAC,IAAI,EAAE,WAAW,CAAC;QAClC,QAAQ,EAAE,IAAI,CAAC,QAAQ;aACpB,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,KAAK,IAAI,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;aACnD,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,GAAG,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;QACnE,KAAK,EAAE,IAAI,CAAC,SAAS;aAClB,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,IAAI,CAAC,UAAU,KAAK,CAAC,CAAC;aAC/D,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,GAAG,CAAC,EAAE,GAAG,EAAE,IAAI,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,CAAC,QAAQ,KAAK,GAAG,EAAE,IAAI,EAAE,IAAI,CAAC,SAAS,EAAE,CAAC,CAAC;QACpH,KAAK,EAAE,OAAO,CAAC,IAAI,EAAE,CAAC,EAAE,KAAK,EAAE,EAAE,CAAC;aAC/B,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE;YACZ,oEAAoE;YACpE,MAAM,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;YACnD,MAAM,KAAK,GAAG,EAAE,CAAC,MAAM,GAAG,CAAC,CAAC;YAC5B,OAAO,EAAE,IAAI,EAAE,EAAE,CAAC,IAAI,GAAG,CAAC,EAAE,KAAK,EAAE,GAAG,EAAE,KAAK,GAAG,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC,WAAW,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,CAAC;QAC3G,CAAC,CAAC;aACD,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,KAAK,CAAC;KACxD,CAAC;AACJ,CAAC;AAED,kGAAkG;AAClG,MAAM,UAAU,UAAU,CAAC,MAAY;IACrC,MAAM,QAAQ,GAAc,EAAE,CAAC;IAC/B,MAAM,EAAE,QAAQ,EAAE,GAAG,MAAM,CAAC;IAC5B,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,QAAQ,CAAC,MAAM,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC;QAC5C,MAAM,OAAO,GAAG,QAAQ,CAAC,CAAC,CAAY,CAAC;QACvC,IAAI,OAAO,CAAC,KAAK,GAAG,CAAC;YAAE,SAAS;QAChC,IAAI,GAAG,GAAG,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC;QAC9B,KAAK,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,QAAQ,CAAC,MAAM,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC;YAChD,MAAM,IAAI,GAAG,QAAQ,CAAC,CAAC,CAAY,CAAC;YACpC,IAAI,IAAI,CAAC,KAAK,IAAI,OAAO,CAAC,KAAK,EAAE,CAAC;gBAChC,GAAG,GAAG,IAAI,CAAC,IAAI,CAAC;gBAChB,MAAM;YACR,CAAC;QACH,CAAC;QACD,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC,CAAC;IAClC,CAAC;IACD,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED,8EAA8E;AAC9E,MAAM,UAAU,OAAO,CAAC,MAAY;IAClC,OAAO,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,KAAK,CAAC,CAAC,CAAC;AACpD,CAAC;AAED,iEAAiE;AACjE,MAAM,UAAU,UAAU,CAAC,SAAiB;IAC1C,OAAO,SAAS,CAAC,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC,CAAC;AACrC,CAAC","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, and a link or an image in an image's alt text or a wiki link's text\n * too, each once, but no definition there, where CommonMark reads none. A\n * link inside a link's text is the link, as CommonMark reads it, and the\n * brackets around it are text. A reference is rewritten through its\n * 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, each once, 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, added to\n * `found`.\n *\n * spec-core reads a link's text as CommonMark does: a link inside it is the\n * link, so `[a [b](b.md)](c.md)` gives `b.md` alone, and an image inside it,\n * `[![diagram](d.png)](d.md)`, is listed after the link. It reads nothing in\n * an image's alt text or a wiki link's text, where a destination still moves\n * with the brief, so those two are read again here, as `inText`. Each is\n * shorter than the text it came from, so the reading ends. Nor does spec-core\n * list a definition inside one, since a bracket pairs across no definition's\n * line, so each destination is found once, and no two rewrites meet on the\n * same columns.\n *\n * Read on its own, such a text is a document, where `[r]: r.md` at its start\n * is a definition. CommonMark reads none inside a paragraph's text, and a\n * renderer shows `![[r]: r.md](z.png)` with that alt text as written, so a\n * definition found there writes no destination.\n */\nfunction written(core: MarkdownScan, at: number, inText: boolean, found: Link[]): Link[] {\n for (const link of core.links) {\n if (link.image || link.form === 'wiki') written(scanMarkdown(link.text), at + core.text.indexOf(link.text, link.start), true, found);\n if (link.form === 'inline' || (link.form === 'definition' && !inText)) {\n found.push({ ...link, targetStart: at + link.targetStart, targetEnd: at + link.targetEnd });\n }\n }\n return found;\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, false, [])\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"]}
@@ -27,5 +27,5 @@
27
27
  * move a bound onto those places change no answer, and are equivalent.
28
28
  */
29
29
  import { type Layout } from './layout.js';
30
- import type { Link } from './types.js';
31
- export declare function findLinks(layout: Layout): Link[];
30
+ import type { Heading, Link } from './types.js';
31
+ export declare function findLinks(layout: Layout, headings: readonly Heading[]): Link[];
@@ -27,7 +27,8 @@
27
27
  * move a bound onto those places change no answer, and are equivalent.
28
28
  */
29
29
  import { inRanges, mergeRanges } from '../text/index.js';
30
- import { isMarkdown, isSpaceOrTab, visibleLead } from './layout.js';
30
+ import { isMarkdown, isSpaceOrTab, leadOffset, visibleLead } from './layout.js';
31
+ import { atxLevel, isThematicBreak, listMarker } from './syntax.js';
31
32
  const BANG = 33;
32
33
  const DOUBLE_QUOTE = 34;
33
34
  const SINGLE_QUOTE = 39;
@@ -42,13 +43,17 @@ const LF = 10;
42
43
  const CR = 13;
43
44
  /** CommonMark: a link label holds at most 999 characters. */
44
45
  const MAX_LABEL = 999;
45
- const DEFINITION = /^ {0,3}\[(?!\^)([^\]]+)\]:[ \t]*(<[^<>]*>|\S+)/;
46
+ /**
47
+ * A label holds no bracket but an escaped one. Its two alternatives never
48
+ * start on the same character, so a label is read without backtracking.
49
+ */
50
+ const DEFINITION = /^ {0,3}\[(?!\^)((?:[^[\]\\]|\\.)+)\]:[ \t]*(<[^<>]*>|\S+)/;
46
51
  /** What may follow a definition's destination: nothing, or a title after whitespace. */
47
52
  const TITLE_ONLY = /^(?:[ \t]+(?:"[^"]*"|'[^']*'|\([^)]*\)))?[ \t]*$/;
48
53
  const AUTOLINK = /<((?:https?|ftp|mailto):[^<>\s]+)>/g;
49
- export function findLinks(layout) {
54
+ export function findLinks(layout, headings) {
50
55
  const definitions = new Map();
51
- const defined = readDefinitions(layout, definitions);
56
+ const defined = readDefinitions(layout, headings, definitions);
52
57
  const bracketed = readBrackets(layout, definitions, new Set(defined.map((d) => d.line)));
53
58
  // `<https://x>` as a link's destination or text is part of that link.
54
59
  const taken = mergeRanges([...defined, ...bracketed]);
@@ -70,20 +75,31 @@ function normalizeLabel(label) {
70
75
  *
71
76
  * `[Note]: this matters` is prose that looks like one, so a definition has
72
77
  * nothing after its destination but a title. `[^1]: text` is a footnote.
78
+ *
79
+ * A definition cannot interrupt a paragraph, as CommonMark has it: one
80
+ * written on the line under a paragraph's text is that text. It opens a
81
+ * paragraph, or follows a definition that did.
73
82
  */
74
- function readDefinitions(layout, definitions) {
75
- const { text, structure } = layout;
83
+ function readDefinitions(layout, headings, definitions) {
84
+ const { text, structure, lines, continues } = layout;
85
+ const closed = closingLines(layout, headings);
76
86
  const out = [];
77
- for (const line of layout.lines) {
87
+ for (const line of lines) {
78
88
  if (!isMarkdown(line) || !visibleLead(layout, line))
79
89
  continue;
80
90
  const masked = structure.slice(line.contentStart, line.end);
81
91
  const match = DEFINITION.exec(masked);
82
92
  if (match === null || !TITLE_ONLY.test(masked.slice(match[0].length)))
83
93
  continue;
94
+ // Under a definition the paragraph holds nothing else yet. `continues`
95
+ // holds no first line, so there is a line above whenever it is asked about.
96
+ const follows = out[out.length - 1]?.line === line.line - 1;
97
+ if (!follows && continues.has(line.line) && paragraphOpen(lines[line.line - 2], closed))
98
+ continue;
99
+ const between = match[1].length;
84
100
  const start = line.contentStart + match[0].indexOf('[');
85
- const label = text.slice(start + 1, start + 1 + match[1].length).trim();
86
- if (label.length === 0)
101
+ const label = text.slice(start + 1, start + 1 + between).trim();
102
+ if (label.length === 0 || between > MAX_LABEL)
87
103
  continue;
88
104
  const written = match[2];
89
105
  const end = line.contentStart + match[0].length;
@@ -98,6 +114,40 @@ function readDefinitions(layout, definitions) {
98
114
  }
99
115
  return out;
100
116
  }
117
+ /**
118
+ * The lines after which no paragraph is open, though the line under one
119
+ * would go on with a paragraph written there: the last line of a heading,
120
+ * and of an HTML block a comment opens. Such a block starts where a comment
121
+ * opens a line, after at most three columns, and ends on the line the
122
+ * comment closes on.
123
+ */
124
+ function closingLines(layout, headings) {
125
+ const closed = new Set();
126
+ for (const heading of headings)
127
+ closed.add(heading.endLine);
128
+ for (const comment of layout.comments) {
129
+ const opened = layout.lines[comment.line - 1];
130
+ if (opened.indent <= 3 && comment.start === leadOffset(opened))
131
+ closed.add(layout.index.positionAt(comment.end).line);
132
+ }
133
+ return closed;
134
+ }
135
+ /**
136
+ * Whether a paragraph is open after a line, for the line under it to go on
137
+ * with. None is after a blank line, code, a thematic break or a closing
138
+ * line, and none is open in a list item whose marker has nothing after it,
139
+ * or a heading or a thematic break.
140
+ */
141
+ function paragraphOpen(line, closed) {
142
+ if (!isMarkdown(line) || line.blank || closed.has(line.line) || isThematicBreak(line.content))
143
+ return false;
144
+ const marker = listMarker(line.content);
145
+ if (marker === null)
146
+ return true;
147
+ // The marker takes every space after it, so what is left is empty or text.
148
+ const rest = line.content.slice(marker.offset + marker.width);
149
+ return rest.length > 0 && atxLevel(rest) === 0 && !isThematicBreak(rest);
150
+ }
101
151
  function readAutolinks(layout) {
102
152
  const out = [];
103
153
  for (const match of layout.structure.matchAll(AUTOLINK)) {
@@ -119,7 +169,7 @@ function readAutolinks(layout) {
119
169
  return out;
120
170
  }
121
171
  function pairBrackets(structure, from, to) {
122
- const brackets = { pairs: new Map(), outer: new Map(), before: new Map(), images: new Set() };
172
+ const brackets = { pairs: new Map(), outer: new Map(), before: new Map(), images: new Set(), nested: new Set() };
123
173
  const { pairs } = brackets;
124
174
  const open = [];
125
175
  let escaped = -1;
@@ -143,8 +193,10 @@ function pairBrackets(structure, from, to) {
143
193
  pairs.set(opened.at, at);
144
194
  brackets.before.set(opened.at, opened.before);
145
195
  const around = open[open.length - 1];
146
- if (around !== undefined)
196
+ if (around !== undefined) {
147
197
  brackets.outer.set(opened.at, around.at);
198
+ brackets.nested.add(around.at);
199
+ }
148
200
  }
149
201
  }
150
202
  }
@@ -184,8 +236,9 @@ function endsOf(structure, from, to) {
184
236
  }
185
237
  /**
186
238
  * The bracket forms, a paragraph at a time. A paragraph here is a run of
187
- * Markdown lines, none blank, each continuing the one above: a bracket never
188
- * pairs across a blank line, a heading, or the start of another block.
239
+ * Markdown lines, none blank or a definition, each continuing the one above:
240
+ * a bracket never pairs across a blank line, a heading, a definition, or the
241
+ * start of another block.
189
242
  */
190
243
  function readBrackets(layout, definitions, defined) {
191
244
  const { lines, continues, structure, text } = layout;
@@ -195,10 +248,16 @@ function readBrackets(layout, definitions, defined) {
195
248
  let doubleClose = -2;
196
249
  const lookup = (from, to) => to - from > MAX_LABEL ? undefined : definitions.get(normalizeLabel(text.slice(from, to)));
197
250
  // A blank or code line let into a paragraph would bring only characters the
198
- // structure mask blanks, so this test shapes the runs and changes no link.
251
+ // structure mask blanks, so those two tests shape the runs and change no
252
+ // link. A definition's line is no paragraph's: it holds a label, a
253
+ // destination and a title, none of them a link, and the text after it is a
254
+ // paragraph of its own, whose brackets pair with nothing before it. Past
255
+ // the last line there is no line, and `continues` holds nothing there, so
256
+ // no run reaches past it: the loop below could take one step more, and the
257
+ // test for a line that is not there could go.
199
258
  const inParagraph = (i) => {
200
259
  const line = lines[i];
201
- return line !== undefined && isMarkdown(line) && !line.blank;
260
+ return line !== undefined && isMarkdown(line) && !line.blank && !defined.has(i + 1);
202
261
  };
203
262
  for (let first = 0; first < lines.length; first += 1) {
204
263
  if (!inParagraph(first))
@@ -332,11 +391,16 @@ function readBrackets(layout, definitions, defined) {
332
391
  }
333
392
  }
334
393
  else {
335
- // Only a `[` is paired, so a second label follows exactly when this finds one.
394
+ // Only a `[` is paired, so a second bracket follows exactly when this
395
+ // finds one. It is a label unless it holds a bracket, which it does
396
+ // when a pair lies inside it, or more than 999 characters; one that is
397
+ // not leaves the first bracket to be read as a shortcut.
336
398
  const refClose = pairs.get(close + 1);
337
- if (refClose !== undefined) {
399
+ // With no second bracket the length is NaN and the comparison false,
400
+ // so the first test only narrows the type.
401
+ if (refClose !== undefined && refClose - close - 2 <= MAX_LABEL && !brackets.nested.has(close + 1)) {
338
402
  // `[text][]` is read through its text; `[text][label]` through its label.
339
- const collapsed = refClose - close - 2 <= MAX_LABEL && text.slice(close + 2, refClose).trim() === '';
403
+ const collapsed = text.slice(close + 2, refClose).trim() === '';
340
404
  const found = collapsed ? lookup(at + 1, close) : lookup(close + 2, refClose);
341
405
  if (found === undefined)
342
406
  return null;
@@ -363,9 +427,11 @@ function readBrackets(layout, definitions, defined) {
363
427
  const line = lineAt(at);
364
428
  const image = brackets.images.has(at);
365
429
  const passed = reach[brackets.before.get(at)] > at;
366
- const link = passed || defined.has(line.line) ? null : bracket(at, at, image, line);
430
+ const link = passed ? null : bracket(at, at, image, line);
367
431
  reach.push(Math.max(reach[reach.length - 1], link === null ? from : link.end));
368
432
  const around = brackets.outer.get(at);
433
+ // `holding` is asked only about offsets, so the first test only
434
+ // narrows the type.
369
435
  if (around !== undefined && (holding.has(at) || (link !== null && !image)))
370
436
  holding.add(around);
371
437
  }
@@ -375,7 +441,10 @@ function readBrackets(layout, definitions, defined) {
375
441
  // `textEnd` is -1 or a `]` that closes a `[`, so never 0, and the `]` is
376
442
  // read as no link. Leaving one step later, past it, resumes at the same
377
443
  // place, or ends the paragraph where a shortcut link ended it anyway: the
378
- // test below reads the same with `textEnd > 0` or `at > textEnd`.
444
+ // test below reads the same with `textEnd > 0` or `at > textEnd`, and so
445
+ // does the one further into the loop with `textEnd > 0`. `resume` is read
446
+ // only while `textEnd` is set, and the two are set together, so its first
447
+ // value is never read.
379
448
  let textEnd = -1;
380
449
  let resume = -1;
381
450
  while (at < to) {
@@ -394,12 +463,6 @@ function readBrackets(layout, definitions, defined) {
394
463
  continue;
395
464
  }
396
465
  const line = lineAt(at);
397
- // A definition's line holds its label, its destination and its title,
398
- // and none of them is a link.
399
- if (defined.has(line.line)) {
400
- at = line.end;
401
- continue;
402
- }
403
466
  const image = brackets.images.has(at);
404
467
  const start = image ? at - 1 : at;
405
468
  const link = wiki(at, start, image, line) ?? bracket(at, start, image, line);
@@ -422,6 +485,9 @@ function readBrackets(layout, definitions, defined) {
422
485
  if (!link.image && link.form !== 'wiki') {
423
486
  textEnd = pairs.get(at);
424
487
  resume = link.end;
488
+ // Into the text. A step back instead reads the character before
489
+ // the `[`, which opens nothing there - it opened no link before
490
+ // this one, or it is escaped - and comes back here.
425
491
  at += 1;
426
492
  }
427
493
  else {