@descent-vtt/spec-graph 0.11.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +44 -0
- package/README.md +16 -4
- package/dist/directives.js +1 -2
- package/dist/directives.js.map +1 -1
- package/dist/extract.d.ts +1 -1
- package/dist/extract.js +36 -14
- package/dist/extract.js.map +1 -1
- package/dist/identity.js +28 -22
- package/dist/identity.js.map +1 -1
- package/dist/sections.js +32 -29
- package/dist/sections.js.map +1 -1
- package/dist/select.js +27 -16
- package/dist/select.js.map +1 -1
- package/dist/state.d.ts +12 -0
- package/dist/state.js +45 -23
- package/dist/state.js.map +1 -1
- package/dist/yaml.js +4 -4
- package/dist/yaml.js.map +1 -1
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -11,6 +11,50 @@ the family's
|
|
|
11
11
|
[versioning policy](https://github.com/DescentVTT/spec-core/blob/main/docs/adr/0009-versions-before-1-0.md)
|
|
12
12
|
records.
|
|
13
13
|
|
|
14
|
+
## 0.12.0
|
|
15
|
+
|
|
16
|
+
Names every JavaScript object answers to - `constructor`, `__proto__` - are
|
|
17
|
+
read as the unknown names they are in seven places, three of which could
|
|
18
|
+
leave a run that passed refusing or finding something now.
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
|
|
22
|
+
- A node type named `constructor` is refused as any unknown type is: a
|
|
23
|
+
query or a project rule written with it matched nothing in silence, and is
|
|
24
|
+
now a usage error, exit 2. Upgrading: write `document`, `item` or `*`.
|
|
25
|
+
- A table column headed `Constructor` declares no relation, where it made
|
|
26
|
+
the table a register and each cell an edge of no kind. Upgrading: a table
|
|
27
|
+
that was a register only by that column needs a `Status` column or a
|
|
28
|
+
relation the README lists.
|
|
29
|
+
- A `@spec-edge` whose `kind` names no relation is a `references` edge, and
|
|
30
|
+
`--verbose` lists it as a parse problem; any other word drew an edge of a
|
|
31
|
+
kind no rule knows, and one to its own document ended the check with exit
|
|
32
|
+
2 (ADR-0014). Upgrading: write one of the eight relations.
|
|
33
|
+
|
|
34
|
+
### Fixed
|
|
35
|
+
|
|
36
|
+
- A front-matter key, a `state=` word or a directory named `constructor` or
|
|
37
|
+
`__proto__` no longer ends a query or a check on a TypeError: the key is
|
|
38
|
+
found where it was written, the state is ignored, and a document under
|
|
39
|
+
such a directory keeps its own id.
|
|
40
|
+
|
|
41
|
+
### Documentation
|
|
42
|
+
|
|
43
|
+
- The README's SARIF example pins the actions the workflows run now, and a
|
|
44
|
+
test holds it to them.
|
|
45
|
+
|
|
46
|
+
## 0.11.1
|
|
47
|
+
|
|
48
|
+
Documentation and robustness: the README's SARIF example is pinned as CI
|
|
49
|
+
pins its own, and every scan steps past an empty match, so no table entry
|
|
50
|
+
can hang a run. Nothing a run answers has changed.
|
|
51
|
+
|
|
52
|
+
### Documentation
|
|
53
|
+
|
|
54
|
+
- The README's SARIF example checks out and sets up Node itself, pins each
|
|
55
|
+
action to a commit with its version beside it, and keeps no token in the
|
|
56
|
+
checkout.
|
|
57
|
+
|
|
14
58
|
## 0.11.0
|
|
15
59
|
|
|
16
60
|
spec-graph reads English only again, by the maintainer's decision; the
|
package/README.md
CHANGED
|
@@ -552,6 +552,8 @@ Each takes these attributes. Any other is a parse problem, listed by
|
|
|
552
552
|
|
|
553
553
|
- `reason`, `note`, `owner` and a node's `kind` are for whoever reads the
|
|
554
554
|
source; spec-graph reads nothing from them.
|
|
555
|
+
- An edge's `kind` is one of the relations a selector steps along. Any other
|
|
556
|
+
word is a parse problem too, and the edge is read as `references`.
|
|
555
557
|
- `aliases` splits at commas, semicolons and spaces.
|
|
556
558
|
- A value is quoted with either quote mark, or bare when it holds no space or
|
|
557
559
|
quote, and an attribute with no value is a flag.
|
|
@@ -1061,14 +1063,24 @@ permissions:
|
|
|
1061
1063
|
contents: read
|
|
1062
1064
|
security-events: write
|
|
1063
1065
|
steps:
|
|
1064
|
-
|
|
1066
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
1067
|
+
with:
|
|
1068
|
+
persist-credentials: false
|
|
1069
|
+
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
|
1070
|
+
with:
|
|
1071
|
+
node-version: '24'
|
|
1072
|
+
- run: npm ci
|
|
1065
1073
|
- run: npx spec-graph --format sarif > spec-graph.sarif
|
|
1066
1074
|
continue-on-error: true
|
|
1067
|
-
- uses: github/codeql-action/upload-sarif@v4
|
|
1075
|
+
- uses: github/codeql-action/upload-sarif@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2
|
|
1068
1076
|
with:
|
|
1069
1077
|
sarif_file: spec-graph.sarif
|
|
1070
1078
|
```
|
|
1071
1079
|
|
|
1080
|
+
The actions are pinned to commits, with the version beside each, because a tag
|
|
1081
|
+
can be moved and a commit cannot; Dependabot's `github-actions` updates keep
|
|
1082
|
+
pins like these current.
|
|
1083
|
+
|
|
1072
1084
|
Findings land as comments on the changed lines, and they persist across commits
|
|
1073
1085
|
without drifting, because the `partialFingerprints` spec-graph writes are the
|
|
1074
1086
|
same identity a baseline is keyed on — the rule, the specification, and the
|
|
@@ -1277,9 +1289,9 @@ tools share, whose code is copied into `src/vendor/` and checked by hash rather
|
|
|
1277
1289
|
than installed. So the whole package is auditable in an afternoon — and nothing
|
|
1278
1290
|
in the pipeline can take longer than its input.
|
|
1279
1291
|
|
|
1280
|
-
**Verified, not just covered.** 1,
|
|
1292
|
+
**Verified, not just covered.** 1,918 tests; 99.4% statement and 99.8% line
|
|
1281
1293
|
coverage. Coverage says a line ran, so the suite is also held to a mutation
|
|
1282
|
-
score - 96.
|
|
1294
|
+
score - 96.38% over 8,539 mutants, measured by CI's full sweep, and a build
|
|
1283
1295
|
fails under 93 - because a vocabulary entry or a boundary
|
|
1284
1296
|
condition can be weakened by an ordinary-looking refactor without a single test
|
|
1285
1297
|
going red. [ADR-0007](https://github.com/DescentVTT/spec-graph/blob/main/docs/adr/0007-mutation-testing.md) is straight about what
|
package/dist/directives.js
CHANGED
|
@@ -63,8 +63,7 @@ export function parseDirectives(comments) {
|
|
|
63
63
|
const attributes = new Map();
|
|
64
64
|
const unknown = [];
|
|
65
65
|
const allowed = SCHEMA[directiveName];
|
|
66
|
-
ATTRIBUTE
|
|
67
|
-
for (let m = ATTRIBUTE.exec(body); m !== null; m = ATTRIBUTE.exec(body)) {
|
|
66
|
+
for (const m of body.matchAll(ATTRIBUTE)) {
|
|
68
67
|
const key = m[1].toLowerCase();
|
|
69
68
|
const quoted = m[3] ?? m[4];
|
|
70
69
|
const bare = m[5];
|
package/dist/directives.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"directives.js","sourceRoot":"","sources":["../src/directives.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Explicit annotations, written as HTML comments.\n *\n * Inference covers the overwhelming majority of real documents, but inference\n * that cannot be overridden is a trap: the first time it guesses wrong about a\n * document somebody cannot restructure, the whole tool gets switched off. These\n * directives are the escape hatch, and they are HTML comments so they stay\n * invisible in GitHub, MkDocs, Docusaurus and every other renderer.\n *\n * ```md\n * <!-- @spec-node id=\"ADR-0007\" status=\"accepted\" aliases=\"adr-7, sharding\" -->\n * <!-- @spec-item id=\"shard-key\" state=\"narrowed\" -->\n * <!-- @spec-edge kind=\"delegates-to\" to=\"ADR-0011#scope\" -->\n * <!-- @spec-ignore -->\n * ```\n *\n * A directive always wins over anything inferred, and says so in the report, so\n * an override is visible rather than mysterious.\n */\n\nimport { isMarkdownLine, type HtmlComment, type ListItem, type ScannedDocument } from './markdown.js';\n\nexport type DirectiveName = 'spec-node' | 'spec-item' | 'spec-edge' | 'spec-ignore' | 'spec-history';\n\nconst KNOWN: ReadonlySet<string> = new Set<DirectiveName>([\n 'spec-node',\n 'spec-item',\n 'spec-edge',\n 'spec-ignore',\n 'spec-history',\n]);\n\nexport interface Attribute {\n readonly value: string;\n /** Absolute offset of the value text, quotes excluded. */\n readonly start: number;\n readonly end: number;\n}\n\nexport interface Directive {\n readonly name: DirectiveName;\n readonly attributes: ReadonlyMap<string, Attribute>;\n /** Offset of the enclosing comment. */\n readonly start: number;\n readonly end: number;\n readonly line: number;\n /** Attribute names that were written but are not part of this directive. */\n readonly unknownAttributes: readonly string[];\n}\n\nconst DIRECTIVE_HEAD = /^\\s*@([a-z][a-z0-9-]*)/;\nconst ATTRIBUTE = /([A-Za-z_][A-Za-z0-9_-]*)\\s*(?:=\\s*(\"([^\"]*)\"|'([^']*)'|([^\\s\"'<>]+)))?/g;\n\n/** Attributes each directive understands. Anything else is reported, not ignored. */\nconst SCHEMA: Readonly<Record<DirectiveName, readonly string[]>> = {\n 'spec-node': ['id', 'status', 'title', 'aliases', 'kind'],\n // Takes no attributes: the file either is a record or it is not. A mutant\n // that lists `Stryker was here` lists no name: one never holds a space.\n 'spec-history': [],\n 'spec-item': ['id', 'state', 'title', 'owner', 'note'],\n 'spec-edge': ['kind', 'to', 'from', 'reason'],\n 'spec-ignore': ['reason'],\n};\n\n/**\n * Reads every spec-graph directive out of a document's HTML comments.\n *\n * Only out of a comment that closes. One that opens a line and never does runs\n * to the end of the document, and read as a directive, a stray\n * `<!-- @spec-ignore` would drop the file, and a stray `<!-- @spec-node` would\n * take every `name=value` in the rest of it as an attribute.\n */\nexport function parseDirectives(comments: readonly HtmlComment[]): Directive[] {\n const out: Directive[] = [];\n for (const comment of comments) {\n if (comment.closed === false) continue;\n const head = DIRECTIVE_HEAD.exec(comment.inner);\n if (!head) continue;\n const name = head[1] as string;\n if (!KNOWN.has(name)) continue;\n\n const directiveName = name as DirectiveName;\n // The head is anchored, so it starts where the comment's text does.\n const bodyStart = (head[0] as string).length;\n const body = comment.inner.slice(bodyStart);\n const attributes = new Map<string, Attribute>();\n const unknown: string[] = [];\n const allowed = SCHEMA[directiveName];\n\n ATTRIBUTE.lastIndex = 0;\n for (let m = ATTRIBUTE.exec(body); m !== null; m = ATTRIBUTE.exec(body)) {\n const key = (m[1] as string).toLowerCase();\n const quoted = m[3] ?? m[4];\n const bare = m[5];\n // A bare attribute is a flag: `<!-- @spec-ignore reason -->`.\n const value = quoted ?? bare ?? 'true';\n const raw = m[2];\n const valueOffset =\n raw === undefined\n ? (m.index ?? 0) + (m[0] as string).length\n : (m.index ?? 0) + (m[0] as string).indexOf(raw) + (quoted === undefined ? 0 : 1);\n const start = comment.innerStart + bodyStart + valueOffset;\n\n if (!allowed.includes(key)) unknown.push(key);\n attributes.set(key, { value, start, end: start + value.length });\n }\n\n out.push({\n name: directiveName,\n attributes,\n start: comment.start,\n end: comment.end,\n line: comment.line,\n unknownAttributes: unknown,\n });\n }\n return out;\n}\n\n/** Reads an attribute, trimmed. Returns `null` when absent or blank. */\nexport function attr(directive: Directive, name: string): Attribute | null {\n const found = directive.attributes.get(name);\n if (!found) return null;\n const trimmed = found.value.trim();\n if (trimmed.length === 0) return null;\n const offset = found.value.indexOf(trimmed);\n return { value: trimmed, start: found.start + offset, end: found.start + offset + trimmed.length };\n}\n\n/** Splits a comma- or whitespace-separated attribute into its parts. */\nexport function attrList(directive: Directive, name: string): string[] {\n const found = attr(directive, name);\n if (!found) return [];\n // Whitespace is a separator, so no part holds any to trim. The filter drops\n // what a separator at either end leaves, and would drop what each separator\n // in a run leaves too, so splitting at one at a time is equivalent.\n return found.value.split(/[,;\\s]+/).filter((part) => part.length > 0);\n}\n\n/**\n * Finds the directive that annotates a given region.\n *\n * A `@spec-item` directive may sit on the line above the item, on the same\n * line, or inside the item body. All three read naturally in the source, so all\n * three are accepted, with the closest preceding directive winning.\n *\n * It answers for one region with no view of the others, so a directive in reach\n * of two items is found for both. Extraction uses `bindItemDirectives`.\n */\nexport function directiveFor(\n directives: readonly Directive[],\n name: DirectiveName,\n region: { start: number; end: number },\n /** How far above the region a directive may sit and still apply. */\n lookBehind: number,\n): Directive | null {\n let best: Directive | null = null;\n for (const directive of directives) {\n if (directive.name !== name) continue;\n const inside = directive.start >= region.start && directive.end <= region.end;\n const above = directive.end <= region.start && region.start - directive.end <= lookBehind;\n if (!inside && !above) continue;\n // Two directives never start at one offset, so `>=` reads the same.\n if (best === null || directive.start > best.start) best = directive;\n }\n return best;\n}\n\n/**\n * Pairs each `@spec-item` directive with the one list item it annotates.\n *\n * A directive is written for one item, so it binds to one: the item directly\n * below it, with nothing but blank lines and other comments between, or else\n * the item it is written in, the innermost where items nest. An item that\n * several bind to takes the last one written.\n *\n * A directive alone on its line is in an item only when indented further than\n * that item's marker, which is how CommonMark reads it. Indented under an item,\n * it annotates that item and not the sibling below; flush between two items, it\n * annotates the second.\n */\nexport function bindItemDirectives(\n scanned: ScannedDocument,\n directives: readonly Directive[],\n): Map<ListItem, Directive> {\n const bound = new Map<ListItem, Directive>();\n const wanted = directives.filter((directive) => directive.name === 'spec-item');\n // Most documents carry none. Sparing them the pass over their lines changes\n // how long this takes and nothing else.\n if (wanted.length === 0) return bound;\n\n const { lines, listItems } = scanned;\n const masked = scanned.masks.structure;\n // The indentation of each line holding nothing but whitespace and comments.\n // Code and raw-text HTML are masked too, so they are told apart by their flags.\n const quiet = new Map<number, number>();\n // For every other line, where the run of quiet lines directly above it began,\n // when there was one.\n const quietFrom = new Map<number, number | undefined>();\n let run: number | undefined;\n for (const line of lines) {\n if (isMarkdownLine(line) && masked.slice(line.contentStart, line.end).trim() === '') {\n quiet.set(line.line, line.indent);\n run ??= line.start;\n continue;\n }\n quietFrom.set(line.line, run);\n run = undefined;\n }\n\n // The items whose text the cursor is in, outermost first. Items nest, so one\n // that has ended is always on top. An item ends at the end of a line, the next\n // thing starts on a later one, and no two things start together, so `<` and\n // `<=` read alike in every comparison of offsets below.\n const open: ListItem[] = [];\n const close = (offset: number): void => {\n while (open.length > 0 && (open[open.length - 1] as ListItem).end <= offset) open.pop();\n };\n let next = 0;\n for (const directive of wanted) {\n while (next < listItems.length && (listItems[next] as ListItem).start < directive.start) {\n const item = listItems[next] as ListItem;\n close(item.start);\n open.push(item);\n next += 1;\n }\n close(directive.start);\n\n // Sharing its line with text, a directive is in whatever that text is in.\n const indent = quiet.get(directive.line) ?? Infinity;\n // No deeper than the scanner already went to find where these items end.\n let depth = open.length - 1;\n while (depth >= 0 && (open[depth] as ListItem).indent >= indent) depth -= 1;\n const holder = open[depth];\n\n const below = listItems[next];\n const directlyAbove = below !== undefined && (quietFrom.get(below.line) ?? Infinity) <= directive.start;\n if (directlyAbove && (holder === undefined || below.start < holder.end)) {\n bound.set(below, directive);\n continue;\n }\n // Flush under an item and above no other, it continues the item as any\n // unindented line does.\n const written = holder ?? open[open.length - 1];\n if (written !== undefined) bound.set(written, directive);\n }\n return bound;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"directives.js","sourceRoot":"","sources":["../src/directives.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Explicit annotations, written as HTML comments.\n *\n * Inference covers the overwhelming majority of real documents, but inference\n * that cannot be overridden is a trap: the first time it guesses wrong about a\n * document somebody cannot restructure, the whole tool gets switched off. These\n * directives are the escape hatch, and they are HTML comments so they stay\n * invisible in GitHub, MkDocs, Docusaurus and every other renderer.\n *\n * ```md\n * <!-- @spec-node id=\"ADR-0007\" status=\"accepted\" aliases=\"adr-7, sharding\" -->\n * <!-- @spec-item id=\"shard-key\" state=\"narrowed\" -->\n * <!-- @spec-edge kind=\"delegates-to\" to=\"ADR-0011#scope\" -->\n * <!-- @spec-ignore -->\n * ```\n *\n * A directive always wins over anything inferred, and says so in the report, so\n * an override is visible rather than mysterious.\n */\n\nimport { isMarkdownLine, type HtmlComment, type ListItem, type ScannedDocument } from './markdown.js';\n\nexport type DirectiveName = 'spec-node' | 'spec-item' | 'spec-edge' | 'spec-ignore' | 'spec-history';\n\nconst KNOWN: ReadonlySet<string> = new Set<DirectiveName>([\n 'spec-node',\n 'spec-item',\n 'spec-edge',\n 'spec-ignore',\n 'spec-history',\n]);\n\nexport interface Attribute {\n readonly value: string;\n /** Absolute offset of the value text, quotes excluded. */\n readonly start: number;\n readonly end: number;\n}\n\nexport interface Directive {\n readonly name: DirectiveName;\n readonly attributes: ReadonlyMap<string, Attribute>;\n /** Offset of the enclosing comment. */\n readonly start: number;\n readonly end: number;\n readonly line: number;\n /** Attribute names that were written but are not part of this directive. */\n readonly unknownAttributes: readonly string[];\n}\n\nconst DIRECTIVE_HEAD = /^\\s*@([a-z][a-z0-9-]*)/;\nconst ATTRIBUTE = /([A-Za-z_][A-Za-z0-9_-]*)\\s*(?:=\\s*(\"([^\"]*)\"|'([^']*)'|([^\\s\"'<>]+)))?/g;\n\n/** Attributes each directive understands. Anything else is reported, not ignored. */\nconst SCHEMA: Readonly<Record<DirectiveName, readonly string[]>> = {\n 'spec-node': ['id', 'status', 'title', 'aliases', 'kind'],\n // Takes no attributes: the file either is a record or it is not. A mutant\n // that lists `Stryker was here` lists no name: one never holds a space.\n 'spec-history': [],\n 'spec-item': ['id', 'state', 'title', 'owner', 'note'],\n 'spec-edge': ['kind', 'to', 'from', 'reason'],\n 'spec-ignore': ['reason'],\n};\n\n/**\n * Reads every spec-graph directive out of a document's HTML comments.\n *\n * Only out of a comment that closes. One that opens a line and never does runs\n * to the end of the document, and read as a directive, a stray\n * `<!-- @spec-ignore` would drop the file, and a stray `<!-- @spec-node` would\n * take every `name=value` in the rest of it as an attribute.\n */\nexport function parseDirectives(comments: readonly HtmlComment[]): Directive[] {\n const out: Directive[] = [];\n for (const comment of comments) {\n if (comment.closed === false) continue;\n const head = DIRECTIVE_HEAD.exec(comment.inner);\n if (!head) continue;\n const name = head[1] as string;\n if (!KNOWN.has(name)) continue;\n\n const directiveName = name as DirectiveName;\n // The head is anchored, so it starts where the comment's text does.\n const bodyStart = (head[0] as string).length;\n const body = comment.inner.slice(bodyStart);\n const attributes = new Map<string, Attribute>();\n const unknown: string[] = [];\n const allowed = SCHEMA[directiveName];\n\n for (const m of body.matchAll(ATTRIBUTE)) {\n const key = (m[1] as string).toLowerCase();\n const quoted = m[3] ?? m[4];\n const bare = m[5];\n // A bare attribute is a flag: `<!-- @spec-ignore reason -->`.\n const value = quoted ?? bare ?? 'true';\n const raw = m[2];\n const valueOffset =\n raw === undefined\n ? (m.index ?? 0) + (m[0] as string).length\n : (m.index ?? 0) + (m[0] as string).indexOf(raw) + (quoted === undefined ? 0 : 1);\n const start = comment.innerStart + bodyStart + valueOffset;\n\n if (!allowed.includes(key)) unknown.push(key);\n attributes.set(key, { value, start, end: start + value.length });\n }\n\n out.push({\n name: directiveName,\n attributes,\n start: comment.start,\n end: comment.end,\n line: comment.line,\n unknownAttributes: unknown,\n });\n }\n return out;\n}\n\n/** Reads an attribute, trimmed. Returns `null` when absent or blank. */\nexport function attr(directive: Directive, name: string): Attribute | null {\n const found = directive.attributes.get(name);\n if (!found) return null;\n const trimmed = found.value.trim();\n if (trimmed.length === 0) return null;\n const offset = found.value.indexOf(trimmed);\n return { value: trimmed, start: found.start + offset, end: found.start + offset + trimmed.length };\n}\n\n/** Splits a comma- or whitespace-separated attribute into its parts. */\nexport function attrList(directive: Directive, name: string): string[] {\n const found = attr(directive, name);\n if (!found) return [];\n // Whitespace is a separator, so no part holds any to trim. The filter drops\n // what a separator at either end leaves, and would drop what each separator\n // in a run leaves too, so splitting at one at a time is equivalent.\n return found.value.split(/[,;\\s]+/).filter((part) => part.length > 0);\n}\n\n/**\n * Finds the directive that annotates a given region.\n *\n * A `@spec-item` directive may sit on the line above the item, on the same\n * line, or inside the item body. All three read naturally in the source, so all\n * three are accepted, with the closest preceding directive winning.\n *\n * It answers for one region with no view of the others, so a directive in reach\n * of two items is found for both. Extraction uses `bindItemDirectives`.\n */\nexport function directiveFor(\n directives: readonly Directive[],\n name: DirectiveName,\n region: { start: number; end: number },\n /** How far above the region a directive may sit and still apply. */\n lookBehind: number,\n): Directive | null {\n let best: Directive | null = null;\n for (const directive of directives) {\n if (directive.name !== name) continue;\n const inside = directive.start >= region.start && directive.end <= region.end;\n const above = directive.end <= region.start && region.start - directive.end <= lookBehind;\n if (!inside && !above) continue;\n // Two directives never start at one offset, so `>=` reads the same.\n if (best === null || directive.start > best.start) best = directive;\n }\n return best;\n}\n\n/**\n * Pairs each `@spec-item` directive with the one list item it annotates.\n *\n * A directive is written for one item, so it binds to one: the item directly\n * below it, with nothing but blank lines and other comments between, or else\n * the item it is written in, the innermost where items nest. An item that\n * several bind to takes the last one written.\n *\n * A directive alone on its line is in an item only when indented further than\n * that item's marker, which is how CommonMark reads it. Indented under an item,\n * it annotates that item and not the sibling below; flush between two items, it\n * annotates the second.\n */\nexport function bindItemDirectives(\n scanned: ScannedDocument,\n directives: readonly Directive[],\n): Map<ListItem, Directive> {\n const bound = new Map<ListItem, Directive>();\n const wanted = directives.filter((directive) => directive.name === 'spec-item');\n // Most documents carry none. Sparing them the pass over their lines changes\n // how long this takes and nothing else.\n if (wanted.length === 0) return bound;\n\n const { lines, listItems } = scanned;\n const masked = scanned.masks.structure;\n // The indentation of each line holding nothing but whitespace and comments.\n // Code and raw-text HTML are masked too, so they are told apart by their flags.\n const quiet = new Map<number, number>();\n // For every other line, where the run of quiet lines directly above it began,\n // when there was one.\n const quietFrom = new Map<number, number | undefined>();\n let run: number | undefined;\n for (const line of lines) {\n if (isMarkdownLine(line) && masked.slice(line.contentStart, line.end).trim() === '') {\n quiet.set(line.line, line.indent);\n run ??= line.start;\n continue;\n }\n quietFrom.set(line.line, run);\n run = undefined;\n }\n\n // The items whose text the cursor is in, outermost first. Items nest, so one\n // that has ended is always on top. An item ends at the end of a line, the next\n // thing starts on a later one, and no two things start together, so `<` and\n // `<=` read alike in every comparison of offsets below.\n const open: ListItem[] = [];\n const close = (offset: number): void => {\n while (open.length > 0 && (open[open.length - 1] as ListItem).end <= offset) open.pop();\n };\n let next = 0;\n for (const directive of wanted) {\n while (next < listItems.length && (listItems[next] as ListItem).start < directive.start) {\n const item = listItems[next] as ListItem;\n close(item.start);\n open.push(item);\n next += 1;\n }\n close(directive.start);\n\n // Sharing its line with text, a directive is in whatever that text is in.\n const indent = quiet.get(directive.line) ?? Infinity;\n // No deeper than the scanner already went to find where these items end.\n let depth = open.length - 1;\n while (depth >= 0 && (open[depth] as ListItem).indent >= indent) depth -= 1;\n const holder = open[depth];\n\n const below = listItems[next];\n const directlyAbove = below !== undefined && (quietFrom.get(below.line) ?? Infinity) <= directive.start;\n if (directlyAbove && (holder === undefined || below.start < holder.end)) {\n bound.set(below, directive);\n continue;\n }\n // Flush under an item and above no other, it continues the item as any\n // unindented line does.\n const written = holder ?? open[open.length - 1];\n if (written !== undefined) bound.set(written, directive);\n }\n return bound;\n}\n"]}
|
package/dist/extract.d.ts
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
*/
|
|
17
17
|
import { type DocumentIdentity } from './identity.js';
|
|
18
18
|
import { type ScannedDocument } from './markdown.js';
|
|
19
|
-
import type
|
|
19
|
+
import { type DocumentNode, type EdgeKind, type EdgeOrigin, type ItemNode, type MisreadKey, type ParseProblem, type SourceRef } from './types.js';
|
|
20
20
|
import { type YamlEntry } from './yaml.js';
|
|
21
21
|
/**
|
|
22
22
|
* A relation that has been read but not yet resolved to a target node.
|
package/dist/extract.js
CHANGED
|
@@ -21,6 +21,7 @@ import { anchorsOf, isMarkdownLine, isOnlyComment, referenceLinks, scanMarkdown,
|
|
|
21
21
|
import { findSpecificationRegions, isRegisterTable, opensHeading, regionAt, } from './sections.js';
|
|
22
22
|
import { resolveItemState } from './state.js';
|
|
23
23
|
import { refOf } from './source.js';
|
|
24
|
+
import { EDGE_KINDS, } from './types.js';
|
|
24
25
|
import { readEntries, toRecord, valuesOf } from './yaml.js';
|
|
25
26
|
/* -------------------------------------------------------------------------- */
|
|
26
27
|
/* Vocabulary */
|
|
@@ -513,6 +514,13 @@ export function extractDocument(input) {
|
|
|
513
514
|
at: at(directive.start, directive.end),
|
|
514
515
|
});
|
|
515
516
|
}
|
|
517
|
+
const kind = directive.name === 'spec-edge' ? attr(directive, 'kind') : null;
|
|
518
|
+
if (kind !== null && !isEdgeKind(kind.value)) {
|
|
519
|
+
problems.push({
|
|
520
|
+
message: `unknown relation "${kind.value}" on @spec-edge, read as references - expected one of: ${EDGE_KINDS.join(', ')}`,
|
|
521
|
+
at: at(kind.start, kind.end),
|
|
522
|
+
});
|
|
523
|
+
}
|
|
516
524
|
}
|
|
517
525
|
const { entries, problems: unread } = readEntries(scanned);
|
|
518
526
|
for (const problem of unread) {
|
|
@@ -981,6 +989,17 @@ function summarise(text) {
|
|
|
981
989
|
function isClaimed(claimed, offset) {
|
|
982
990
|
return claimed.some((range) => offset >= range.start && offset < range.end);
|
|
983
991
|
}
|
|
992
|
+
/**
|
|
993
|
+
* Whether a word a `@spec-edge` gives as its kind is a relation.
|
|
994
|
+
*
|
|
995
|
+
* Asked of the list, as a selector's relation is: the word was once taken as
|
|
996
|
+
* it stood, so `kind="blocks"` made an edge of a kind no rule knows, a
|
|
997
|
+
* document with such an edge to itself ended the run, and `kind="constructor"`
|
|
998
|
+
* did not, since the table of kinds is an object and answers to that name.
|
|
999
|
+
*/
|
|
1000
|
+
function isEdgeKind(word) {
|
|
1001
|
+
return EDGE_KINDS.includes(word);
|
|
1002
|
+
}
|
|
984
1003
|
function extractReferences(context) {
|
|
985
1004
|
const { scanned, directives, document, documentAt, items, byKey, status, claimed, file, index } = context;
|
|
986
1005
|
// This list and the one `withoutOverlaps` builds from it are only ever read
|
|
@@ -1035,11 +1054,14 @@ function extractReferences(context) {
|
|
|
1035
1054
|
for (const directive of directives) {
|
|
1036
1055
|
if (directive.name !== 'spec-edge')
|
|
1037
1056
|
continue;
|
|
1038
|
-
const
|
|
1057
|
+
const written = attr(directive, 'kind')?.value;
|
|
1039
1058
|
const to = attr(directive, 'to');
|
|
1040
1059
|
const from = attr(directive, 'from');
|
|
1041
|
-
if (!
|
|
1060
|
+
if (!written)
|
|
1042
1061
|
continue;
|
|
1062
|
+
// A word that names no relation is still a link to somewhere, as a cell
|
|
1063
|
+
// under a column that names none is: a citation, and no more than one.
|
|
1064
|
+
const kind = isEdgeKind(written) ? written : 'references';
|
|
1043
1065
|
const owner = ownerOf(items, directive.start) ?? documentAt(directive.start).id;
|
|
1044
1066
|
if (to) {
|
|
1045
1067
|
out.push({
|
|
@@ -1048,7 +1070,7 @@ function extractReferences(context) {
|
|
|
1048
1070
|
target: to.value,
|
|
1049
1071
|
origin: 'directive',
|
|
1050
1072
|
declaredAt: at(to.start, to.end),
|
|
1051
|
-
raw: `@spec-edge kind="${
|
|
1073
|
+
raw: `@spec-edge kind="${written}" to="${to.value}"`,
|
|
1052
1074
|
inverted: false,
|
|
1053
1075
|
opportunistic: false,
|
|
1054
1076
|
});
|
|
@@ -1060,7 +1082,7 @@ function extractReferences(context) {
|
|
|
1060
1082
|
target: from.value,
|
|
1061
1083
|
origin: 'directive',
|
|
1062
1084
|
declaredAt: at(from.start, from.end),
|
|
1063
|
-
raw: `@spec-edge kind="${
|
|
1085
|
+
raw: `@spec-edge kind="${written}" from="${from.value}"`,
|
|
1064
1086
|
inverted: true,
|
|
1065
1087
|
opportunistic: false,
|
|
1066
1088
|
});
|
|
@@ -1208,12 +1230,15 @@ function ownerOf(items, offset) {
|
|
|
1208
1230
|
export function classifyReference(text, start, end, section) {
|
|
1209
1231
|
const before = sentenceBefore(text, start);
|
|
1210
1232
|
// Left to right, so the last match within the window is the nearest one - and
|
|
1211
|
-
// the nearest governing phrase is the one that governs.
|
|
1233
|
+
// the nearest governing phrase is the one that governs. `matchAll`, not a
|
|
1234
|
+
// loop over `exec`: an empty match leaves `lastIndex` where it began, and
|
|
1235
|
+
// `exec` finds it there again for ever, where `matchAll` steps past it. No
|
|
1236
|
+
// phrase is empty, so the pattern never matches empty; a phrase emptied by
|
|
1237
|
+
// mistake then fails the tests rather than hanging them.
|
|
1212
1238
|
let best = null;
|
|
1213
|
-
VERB_PATTERN
|
|
1214
|
-
for (let m = VERB_PATTERN.exec(before); m !== null; m = VERB_PATTERN.exec(before)) {
|
|
1239
|
+
for (const m of before.matchAll(VERB_PATTERN)) {
|
|
1215
1240
|
const phrase = m[0];
|
|
1216
|
-
const end =
|
|
1241
|
+
const end = m.index + phrase.length;
|
|
1217
1242
|
const distance = before.length - end;
|
|
1218
1243
|
if (distance > VERB_WINDOW)
|
|
1219
1244
|
continue;
|
|
@@ -1279,10 +1304,8 @@ const STATEMENT_BREAK = /[.?!;]\s|[。?!;]|\n\s*\n|\n\s*[-*+>#]|\|/g;
|
|
|
1279
1304
|
function sentenceBefore(text, start) {
|
|
1280
1305
|
const window = text.slice(Math.max(0, start - 160), start);
|
|
1281
1306
|
let cut = 0;
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
cut = (m.index ?? 0) + m[0].length;
|
|
1285
|
-
}
|
|
1307
|
+
for (const m of window.matchAll(STATEMENT_BREAK))
|
|
1308
|
+
cut = m.index + m[0].length;
|
|
1286
1309
|
return window
|
|
1287
1310
|
.slice(cut)
|
|
1288
1311
|
.toLowerCase()
|
|
@@ -1333,8 +1356,7 @@ function findBareReferences(scanned, links) {
|
|
|
1333
1356
|
}
|
|
1334
1357
|
text += source.slice(cursor);
|
|
1335
1358
|
const out = [];
|
|
1336
|
-
BARE_REF
|
|
1337
|
-
for (let m = BARE_REF.exec(text); m !== null; m = BARE_REF.exec(text)) {
|
|
1359
|
+
for (const m of text.matchAll(BARE_REF)) {
|
|
1338
1360
|
const written = m[0];
|
|
1339
1361
|
// The family as resolution will read it, which takes the shortest prefix
|
|
1340
1362
|
// the number allows: `base64` is `base` and 64. Read off this pattern's
|