@descent-vtt/spec-graph 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. package/CHANGELOG.md +410 -0
  2. package/README.md +102 -17
  3. package/dist/cli.d.ts +2 -2
  4. package/dist/cli.js +38 -19
  5. package/dist/cli.js.map +1 -1
  6. package/dist/directives.d.ts +9 -2
  7. package/dist/directives.js +15 -4
  8. package/dist/directives.js.map +1 -1
  9. package/dist/extract.d.ts +1 -1
  10. package/dist/extract.js +50 -16
  11. package/dist/extract.js.map +1 -1
  12. package/dist/glob.d.ts +44 -44
  13. package/dist/glob.js +152 -116
  14. package/dist/glob.js.map +1 -1
  15. package/dist/index.d.ts +3 -3
  16. package/dist/index.js +4 -2
  17. package/dist/index.js.map +1 -1
  18. package/dist/lifecycle.js +10 -2
  19. package/dist/lifecycle.js.map +1 -1
  20. package/dist/markdown.d.ts +53 -161
  21. package/dist/markdown.js +42 -864
  22. package/dist/markdown.js.map +1 -1
  23. package/dist/report.d.ts +23 -0
  24. package/dist/report.js +80 -0
  25. package/dist/report.js.map +1 -1
  26. package/dist/rules.js +48 -14
  27. package/dist/rules.js.map +1 -1
  28. package/dist/sections.d.ts +1 -1
  29. package/dist/sections.js +7 -3
  30. package/dist/sections.js.map +1 -1
  31. package/dist/select.d.ts +2 -2
  32. package/dist/select.js +3 -3
  33. package/dist/select.js.map +1 -1
  34. package/dist/source.d.ts +6 -19
  35. package/dist/source.js +4 -70
  36. package/dist/source.js.map +1 -1
  37. package/dist/types.d.ts +23 -3
  38. package/dist/types.js +1 -1
  39. package/dist/types.js.map +1 -1
  40. package/dist/vendor/spec-core/markdown/frontmatter.d.ts +103 -0
  41. package/dist/vendor/spec-core/markdown/frontmatter.js +459 -0
  42. package/dist/vendor/spec-core/markdown/frontmatter.js.map +1 -0
  43. package/dist/vendor/spec-core/markdown/headings.d.ts +45 -0
  44. package/dist/vendor/spec-core/markdown/headings.js +199 -0
  45. package/dist/vendor/spec-core/markdown/headings.js.map +1 -0
  46. package/dist/vendor/spec-core/markdown/index.d.ts +5 -0
  47. package/dist/vendor/spec-core/markdown/index.js +4 -0
  48. package/dist/vendor/spec-core/markdown/index.js.map +1 -0
  49. package/dist/vendor/spec-core/markdown/layout.d.ts +39 -0
  50. package/dist/vendor/spec-core/markdown/layout.js +28 -0
  51. package/dist/vendor/spec-core/markdown/layout.js.map +1 -0
  52. package/dist/vendor/spec-core/markdown/links.d.ts +22 -0
  53. package/dist/vendor/spec-core/markdown/links.js +364 -0
  54. package/dist/vendor/spec-core/markdown/links.js.map +1 -0
  55. package/dist/vendor/spec-core/markdown/lists.d.ts +22 -0
  56. package/dist/vendor/spec-core/markdown/lists.js +127 -0
  57. package/dist/vendor/spec-core/markdown/lists.js.map +1 -0
  58. package/dist/vendor/spec-core/markdown/scan.d.ts +36 -0
  59. package/dist/vendor/spec-core/markdown/scan.js +439 -0
  60. package/dist/vendor/spec-core/markdown/scan.js.map +1 -0
  61. package/dist/vendor/spec-core/markdown/syntax.d.ts +75 -0
  62. package/dist/vendor/spec-core/markdown/syntax.js +181 -0
  63. package/dist/vendor/spec-core/markdown/syntax.js.map +1 -0
  64. package/dist/vendor/spec-core/markdown/tables.d.ts +18 -0
  65. package/dist/vendor/spec-core/markdown/tables.js +86 -0
  66. package/dist/vendor/spec-core/markdown/tables.js.map +1 -0
  67. package/dist/vendor/spec-core/markdown/types.d.ts +287 -0
  68. package/dist/vendor/spec-core/markdown/types.js +9 -0
  69. package/dist/vendor/spec-core/markdown/types.js.map +1 -0
  70. package/dist/vendor/spec-core/path/index.d.ts +1 -0
  71. package/dist/vendor/spec-core/path/index.js +2 -0
  72. package/dist/vendor/spec-core/path/index.js.map +1 -0
  73. package/dist/vendor/spec-core/path/posix.d.ts +83 -0
  74. package/dist/vendor/spec-core/path/posix.js +150 -0
  75. package/dist/vendor/spec-core/path/posix.js.map +1 -0
  76. package/dist/vendor/spec-core/pattern/automaton.d.ts +129 -0
  77. package/dist/vendor/spec-core/pattern/automaton.js +340 -0
  78. package/dist/vendor/spec-core/pattern/automaton.js.map +1 -0
  79. package/dist/vendor/spec-core/pattern/charset.d.ts +58 -0
  80. package/dist/vendor/spec-core/pattern/charset.js +116 -0
  81. package/dist/vendor/spec-core/pattern/charset.js.map +1 -0
  82. package/dist/vendor/spec-core/pattern/glob.d.ts +121 -0
  83. package/dist/vendor/spec-core/pattern/glob.js +481 -0
  84. package/dist/vendor/spec-core/pattern/glob.js.map +1 -0
  85. package/dist/vendor/spec-core/pattern/index.d.ts +6 -0
  86. package/dist/vendor/spec-core/pattern/index.js +4 -0
  87. package/dist/vendor/spec-core/pattern/index.js.map +1 -0
  88. package/dist/{regex.d.ts → vendor/spec-core/pattern/regex.d.ts} +20 -29
  89. package/dist/{regex.js → vendor/spec-core/pattern/regex.js} +40 -49
  90. package/dist/vendor/spec-core/pattern/regex.js.map +1 -0
  91. package/dist/vendor/spec-core/text/index.d.ts +2 -0
  92. package/dist/vendor/spec-core/text/index.js +2 -0
  93. package/dist/vendor/spec-core/text/index.js.map +1 -0
  94. package/dist/vendor/spec-core/text/lines.d.ts +72 -0
  95. package/dist/vendor/spec-core/text/lines.js +156 -0
  96. package/dist/vendor/spec-core/text/lines.js.map +1 -0
  97. package/dist/yaml.d.ts +44 -12
  98. package/dist/yaml.js +115 -206
  99. package/dist/yaml.js.map +1 -1
  100. package/package.json +3 -2
  101. package/src/vendor/spec-core/LICENSE +21 -0
  102. package/src/vendor/spec-core/README.md +8 -0
  103. package/dist/regex.js.map +0 -1
@@ -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 type { HtmlComment, ListItem, 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.\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/** Reads every spec-graph directive out of a document's HTML comments. */\nexport function parseDirectives(comments: readonly HtmlComment[]): Directive[] {\n const out: Directive[] = [];\n for (const comment of comments) {\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 const bodyStart = (head.index ?? 0) + (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 return found.value\n .split(/[,;\\s]+/)\n .map((part) => part.trim())\n .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 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, masked } = scanned;\n // The indentation of each line holding nothing but whitespace and comments.\n // Code is masked too, so a fence has to be told apart by its flag.\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 (!line.code && 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.\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 const bodyStart = (head.index ?? 0) + (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 return found.value\n .split(/[,;\\s]+/)\n .map((part) => part.trim())\n .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 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
@@ -58,7 +58,7 @@ export interface ExtractedDocument {
58
58
  * about the graph: the edge the author wrote down is not in it.
59
59
  */
60
60
  readonly misreadKeys: readonly MisreadKey[];
61
- /** Heading slugs, for resolving `#anchor` references into this document. */
61
+ /** Heading slugs and GitHub's anchors, for resolving `#anchor` references into this document. */
62
62
  readonly anchors: ReadonlySet<string>;
63
63
  readonly identity: DocumentIdentity;
64
64
  readonly scanned: ScannedDocument;
package/dist/extract.js CHANGED
@@ -17,11 +17,11 @@
17
17
  import { attr, attrList, bindItemDirectives, parseDirectives } from './directives.js';
18
18
  import { identify, isDocumentTarget, isExternal, looksLikePath, normaliseRef, parsePrefixedRef, withinOneEdit, ID_KEYS, } from './identity.js';
19
19
  import { isStatusHeading, phaseFromPath, phaseOf, STATUS_KEYS, supersessionTargetsIn } from './lifecycle.js';
20
- import { scanMarkdown, slugify } from './markdown.js';
20
+ import { anchorsOf, isMarkdownLine, isOnlyComment, referenceLinks, scanMarkdown, slugify, } from './markdown.js';
21
21
  import { findSpecificationRegions, regionAt } from './sections.js';
22
22
  import { resolveItemState } from './state.js';
23
23
  import { refOf } from './source.js';
24
- import { parseFrontMatter, toRecord, valuesOf } from './yaml.js';
24
+ import { readEntries, toRecord, valuesOf } from './yaml.js';
25
25
  /* -------------------------------------------------------------------------- */
26
26
  /* Vocabulary */
27
27
  /* -------------------------------------------------------------------------- */
@@ -158,6 +158,19 @@ const RELATION_INDEX = new Map(Object.entries(RELATION_KEYS).map(([canonical, re
158
158
  foldRelationKey(canonical),
159
159
  { ...relation, canonical },
160
160
  ]));
161
+ /**
162
+ * Whether a front-matter key is one the graph is built from: a status, an id
163
+ * or an alias, or a relation. Left unread, each changes a verdict - a status
164
+ * is unknown, a node is named by its path, an edge is missing. A title or a
165
+ * description left unread changes a label.
166
+ */
167
+ function buildsGraph(key) {
168
+ return (STATUS_KEYS.includes(key) ||
169
+ ID_KEYS.includes(key) ||
170
+ key === 'alias' ||
171
+ key === 'aliases' ||
172
+ RELATION_INDEX.has(foldRelationKey(key)));
173
+ }
161
174
  /**
162
175
  * Keys a region must answer for itself, so the file's answer cannot shadow one.
163
176
  *
@@ -476,6 +489,17 @@ export function extractDocument(input) {
476
489
  at: at(nul, nul + 1),
477
490
  });
478
491
  }
492
+ // CommonMark reads a comment that opens a line and never closes as running
493
+ // to the end of the document, and so does the scan: nothing after it is a
494
+ // heading, an item or a link. A renderer hides it all, so the author may see
495
+ // it, but the graph would lose it without a word.
496
+ const unclosed = scanned.comments.find((comment) => !comment.closed);
497
+ if (unclosed !== undefined) {
498
+ problems.push({
499
+ message: 'a comment opened here is never closed, so nothing after it is read - close it with -->',
500
+ at: at(unclosed.start, unclosed.start + 4),
501
+ });
502
+ }
479
503
  for (const directive of directives) {
480
504
  for (const unknown of directive.unknownAttributes) {
481
505
  problems.push({
@@ -484,7 +508,13 @@ export function extractDocument(input) {
484
508
  });
485
509
  }
486
510
  }
487
- const entries = scanned.frontMatter ? parseFrontMatter(scanned.frontMatter.raw, scanned.frontMatter.start) : [];
511
+ const { entries, problems: unread } = readEntries(scanned);
512
+ for (const problem of unread) {
513
+ // A line that names no key could have held any of them, and so could a
514
+ // block that was not read at all.
515
+ const lost = problem.unread && (problem.key === null || buildsGraph(problem.key));
516
+ problems.push({ message: problem.message, at: at(problem.start, problem.end), ...(lost ? { unread: true } : {}) });
517
+ }
488
518
  const byKey = new Map();
489
519
  for (const entry of entries)
490
520
  byKey.set(entry.key, entry);
@@ -534,6 +564,7 @@ export function extractDocument(input) {
534
564
  path: input.path,
535
565
  aliases: identity.aliases,
536
566
  phase,
567
+ history: record,
537
568
  rawStatus: status.raw,
538
569
  statusAt: status.at,
539
570
  frontMatter: toRecord(entries),
@@ -587,7 +618,7 @@ export function extractDocument(input) {
587
618
  const ownItems = allItems.filter((item) => item.document === identity.id);
588
619
  const ownReferences = allReferences.filter((reference) => belongsTo(reference, identity.id, allItems));
589
620
  const misreadKeys = misreadRelationKeys(byKey, identity.id, at);
590
- const anchors = new Set(scanned.headings.map((h) => h.slug));
621
+ const anchors = anchorsOf(scanned.headings);
591
622
  const filled = subSpecifications.map((spec) => ({
592
623
  ...spec.extracted,
593
624
  items: allItems.filter((item) => item.document === spec.document.id),
@@ -659,11 +690,12 @@ function buildRegion(context) {
659
690
  // wrote for itself. Being a record is a statement about the file made from
660
691
  // outside it, and it does not stop at a heading.
661
692
  phase: record ? 'record' : declaredPhase !== 'unknown' ? declaredPhase : pathPhase,
693
+ history: record,
662
694
  rawStatus,
663
695
  statusAt,
664
696
  frontMatter: inherited,
665
697
  };
666
- const anchors = new Set(scanned.headings.filter((h) => h.start >= region.start && h.start < region.end).map((h) => h.slug));
698
+ const anchors = anchorsOf(scanned.headings.filter((h) => h.start >= region.start && h.start < region.end));
667
699
  // Relations a table declares by column, typed by the header the author wrote.
668
700
  const references = region.relations.map((relation) => ({
669
701
  kind: relation.kind,
@@ -744,9 +776,9 @@ function statusSectionBody(scanned, headingLine) {
744
776
  for (const line of scanned.lines) {
745
777
  if (line.line <= headingLine)
746
778
  continue;
747
- if (line.blank)
779
+ if (line.blank || isOnlyComment(scanned, line))
748
780
  continue;
749
- if (line.code)
781
+ if (!isMarkdownLine(line))
750
782
  return null;
751
783
  const trimmed = line.content.trim();
752
784
  // A heading immediately after means the section is empty.
@@ -960,7 +992,8 @@ function extractReferences(context) {
960
992
  // so `| ADR-0002 | ... | [ADR-0001](0001.md) |` yields one typed edge rather
961
993
  // than a typed edge and a neutral citation beside it.
962
994
  const linked = new Set();
963
- for (const link of scanned.links) {
995
+ const links = referenceLinks(scanned);
996
+ for (const link of links) {
964
997
  if (link.form === 'definition')
965
998
  continue;
966
999
  if (isClaimed(claimed, link.start))
@@ -972,14 +1005,14 @@ function extractReferences(context) {
972
1005
  continue;
973
1006
  const owner = ownerOf(items, link.start) ?? documentAt(link.start).id;
974
1007
  const section = sectionPathAt(scanned, link.start);
975
- const classified = classifyReference(scanned.masked, link.start, link.end, section);
1008
+ const classified = classifyReference(scanned.masks.structure, link.start, link.end, section);
976
1009
  linked.add(normaliseRef(stripAnchor(target)));
977
1010
  out.push({
978
1011
  kind: classified.kind,
979
1012
  from: owner,
980
1013
  target,
981
1014
  origin: 'link',
982
- declaredAt: at(link.targetStart, link.targetStart + link.target.length),
1015
+ declaredAt: at(link.targetStart, link.targetEnd),
983
1016
  raw: renderLink(link),
984
1017
  inverted: classified.inverted,
985
1018
  opportunistic: false,
@@ -987,7 +1020,7 @@ function extractReferences(context) {
987
1020
  }
988
1021
  // Bare identifiers in prose. Everything already covered by a link is skipped
989
1022
  // so a citation written as `[ADR-7](0007.md)` is not counted twice.
990
- for (const bare of findBareReferences(scanned)) {
1023
+ for (const bare of findBareReferences(scanned, links)) {
991
1024
  const key = normaliseRef(bare.text);
992
1025
  if (linked.has(key))
993
1026
  continue;
@@ -995,7 +1028,7 @@ function extractReferences(context) {
995
1028
  continue;
996
1029
  const owner = ownerOf(items, bare.start) ?? documentAt(bare.start).id;
997
1030
  const section = sectionPathAt(scanned, bare.start);
998
- const classified = classifyReference(scanned.masked, bare.start, bare.end, section);
1031
+ const classified = classifyReference(scanned.masks.structure, bare.start, bare.end, section);
999
1032
  out.push({
1000
1033
  kind: classified.kind,
1001
1034
  from: owner,
@@ -1167,15 +1200,16 @@ function sentenceAfter(text, end) {
1167
1200
  * Link constructs are blanked first. Without that, `[ADR-7](https://x/adr-7)`
1168
1201
  * would yield three references to the same thing - one real and two harvested
1169
1202
  * out of a URL - which is precisely the mis-match that makes hand-rolled regex
1170
- * checks untrustworthy.
1203
+ * checks untrustworthy. Images are not blanked, since they are not references:
1204
+ * an identifier in one's text is prose, as it always was here.
1171
1205
  */
1172
- function findBareReferences(scanned) {
1206
+ function findBareReferences(scanned, links) {
1173
1207
  // Slice-based for the same reason as the scanner's masking: this runs over
1174
1208
  // every document, and a per-character array is the wrong shape for the job.
1175
- const source = scanned.masked;
1209
+ const source = scanned.masks.structure;
1176
1210
  let text = '';
1177
1211
  let cursor = 0;
1178
- for (const link of scanned.links) {
1212
+ for (const link of links) {
1179
1213
  const start = Math.max(link.start, cursor);
1180
1214
  const end = Math.min(link.end, source.length);
1181
1215
  if (end <= start)