@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 +63 -0
- package/README.md +2 -2
- package/dist/glob.d.ts +7 -2
- package/dist/glob.js +8 -12
- package/dist/glob.js.map +1 -1
- package/dist/markdown.d.ts +4 -3
- package/dist/markdown.js +19 -16
- package/dist/markdown.js.map +1 -1
- package/dist/vendor/spec-core/markdown/links.d.ts +2 -2
- package/dist/vendor/spec-core/markdown/links.js +92 -26
- package/dist/vendor/spec-core/markdown/links.js.map +1 -1
- package/dist/vendor/spec-core/markdown/scan.js +1 -1
- package/dist/vendor/spec-core/markdown/scan.js.map +1 -1
- package/dist/vendor/spec-core/markdown/tables.js +3 -0
- package/dist/vendor/spec-core/markdown/tables.js.map +1 -1
- package/dist/vendor/spec-core/pattern/automaton.d.ts +15 -2
- package/dist/vendor/spec-core/pattern/automaton.js +21 -2
- package/dist/vendor/spec-core/pattern/automaton.js.map +1 -1
- package/dist/vendor/spec-core/pattern/charset.d.ts +5 -1
- package/dist/vendor/spec-core/pattern/charset.js +8 -1
- package/dist/vendor/spec-core/pattern/charset.js.map +1 -1
- package/dist/vendor/spec-core/pattern/glob.d.ts +14 -3
- package/dist/vendor/spec-core/pattern/glob.js +55 -16
- package/dist/vendor/spec-core/pattern/glob.js.map +1 -1
- package/dist/vendor/spec-core/text/lines.js +10 -0
- package/dist/vendor/spec-core/text/lines.js.map +1 -1
- package/package.json +1 -1
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
|
|
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)`, `` - 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)`, `` - 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
|
-
/**
|
|
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 {
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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":"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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.\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"]}
|
package/dist/markdown.d.ts
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
|
|
19
|
-
*
|
|
20
|
-
*
|
|
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
|
|
19
|
-
*
|
|
20
|
-
*
|
|
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
|
|
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
|
* `[](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
|
|
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
|
-
*
|
|
44
|
-
* CommonMark
|
|
45
|
-
*
|
|
46
|
-
*
|
|
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.
|
|
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:
|
|
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);
|
package/dist/markdown.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"markdown.js","sourceRoot":"","sources":["../src/markdown.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * A 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 * `[](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":"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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, 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 * `[](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
|
-
|
|
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
|
|
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 +
|
|
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:
|
|
188
|
-
* pairs across a blank line, a heading, or the
|
|
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
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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 {
|