@descent-vtt/spec-brief 0.2.2 → 0.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,68 @@
2
2
 
3
3
  Notable changes, newest first. Versions follow [semver](https://semver.org).
4
4
 
5
+ ## 0.2.4
6
+
7
+ spec-core at 65ef842. Archival rewrites a link reference definition only
8
+ where CommonMark reads one, so text shaped like a definition - under a
9
+ paragraph's text, in alt text, in a wiki link's text - stays as written.
10
+
11
+ ### Changed
12
+
13
+ - spec-core at 65ef842. Its Markdown scanner reads a link reference
14
+ definition as CommonMark does. A definition cannot interrupt a paragraph:
15
+ `[r]: r.md` on the line under a paragraph's text, a lazy line in a block
16
+ quote or a list item included, is that text, and `archive` and
17
+ `unarchive` leave it as written, where they rewrote `r.md`. A label holds
18
+ no unescaped bracket and at most 999 characters: `[[r]: r.md](z.md)` is a
19
+ link to `z.md`, which they rewrite, where they rewrote `r.md](z.md)` as
20
+ the destination of a definition, and `[a\]b]: x.md` is a definition,
21
+ whose `x.md` they now rewrite.
22
+ - References follow the definitions. With `[r]: r.md` written under text,
23
+ `[x][r](y.md)` is a link to `y.md`, which archival now rewrites. A second
24
+ bracket holding a bracket is no label, so with `[r]` defined,
25
+ `[x [r][a[b]c] y](o.md)` holds the reference `[r]`, and `(o.md)` is text
26
+ and stays as written, as around any link inside a link.
27
+
28
+ ### Fixed
29
+
30
+ - Text shaped like a definition at the start of an image's alt text or a
31
+ wiki link's text, `![[r]: r.md](z.png)` or `[[[r]: r.md]]`, is no longer
32
+ rewritten by `archive`. spec-brief reads those texts again on their own,
33
+ where such text opens a document and reads as a definition; CommonMark
34
+ reads none there. `z.png` is rewritten as before.
35
+
36
+ ## 0.2.3
37
+
38
+ spec-core at 119345e. A link inside a link now counts only the inner one, as
39
+ CommonMark and a renderer read it, so archival leaves the outer destination
40
+ as written. The refusal of `**` inside a name suggests patterns built from
41
+ the one written.
42
+
43
+ ### Changed
44
+
45
+ - spec-core at 119345e. Its Markdown scanner reads a link inside a link's
46
+ text as CommonMark does: in `[a [b](inner.md) c](outer.md)` the inner pair
47
+ is the link, and the outer brackets and `(outer.md)` are text. `archive`
48
+ and `unarchive` rewrite `inner.md` and leave `(outer.md)` as written,
49
+ where they rewrote both; a renderer links nothing to `outer.md`, so it
50
+ does not move with the brief. spec-brief no longer reads a link's text
51
+ again to find the inner link. It still reads an image's alt text and a
52
+ wiki link's text, which the scanner leaves unread, and keeps each
53
+ destination once.
54
+ - The `glob` error for `**` inside a name suggests the pattern written,
55
+ spelled both ways it may have meant: `docs/**.md` is told `docs/**/*.md`
56
+ for any depth or `docs/*.md` for one level, and `src/a**` `src/a*/**` or
57
+ `src/a*`, where every such pattern was told `docs/**/*.md` or `*.md`. The
58
+ message's first clause is as it was.
59
+
60
+ ### Fixed
61
+
62
+ - Text shaped like a reference definition at the start of a link's text,
63
+ `see [[r]: r.md](z.md)`, is no longer rewritten by `archive`. spec-brief
64
+ read the link's text on its own, where that text starts a line and reads
65
+ as a definition.
66
+
5
67
  ## 0.2.2
6
68
 
7
69
  A packaging fix: the tarball no longer carries spec-core's internal README.
package/README.md CHANGED
@@ -200,7 +200,7 @@ A repository whose briefs have eight ordered sections, `proposed` and `archived`
200
200
 
201
201
  Section names compare without case, typographic quotes, emphasis, a leading number or a trailing colon, and a heading may add a qualifier after a separator: `## 2. Commander’s Intent:` fills `Commander's Intent`, and `## Invariants (must hold)` fills `Invariants`.
202
202
 
203
- A brief is read as CommonMark reads it wherever that decides what is code or a comment - fenced and indented code, `<pre>`, `<script>`, `<style>` and `<textarea>` blocks, code spans, HTML comments - and nothing inside those is structure. A section, and the title, is an ATX heading, `## Name`, outside a block quote: prose over a `---` line is prose and a rule, and a heading quoted from another document is not one of the brief's sections. A task is a list item outside a block quote with `[ ]`, `[x]` or `[X]`. The links archival rewrites are the ones that write a destination: `[text](dest)`, `![alt](dest)` - one inside a link's text too - and a definition, `[label]: dest`, through which a reference is rewritten once.
203
+ A brief is read as CommonMark reads it wherever that decides what is code or a comment - fenced and indented code, `<pre>`, `<script>`, `<style>` and `<textarea>` blocks, code spans, HTML comments - and nothing inside those is structure. A section, and the title, is an ATX heading, `## Name`, outside a block quote: prose over a `---` line is prose and a rule, and a heading quoted from another document is not one of the brief's sections. A task is a list item outside a block quote with `[ ]`, `[x]` or `[X]`. The links archival rewrites are the ones that write a destination: `[text](dest)`, `![alt](dest)` - one inside a link's text too - and a definition, `[label]: dest`, through which a reference is rewritten once. A definition is one where CommonMark reads it, opening a paragraph or under another: the shape of one under a paragraph's text, or in alt text, is text and stays as written.
204
204
 
205
205
  ## Rules
206
206
 
@@ -13,8 +13,11 @@
13
13
  * space, `x` or `X`. The scanner reads `[~]`, `[?]` and others as well,
14
14
  * which a brief does not write.
15
15
  * - A link destination is one written in the link or in a definition: the
16
- * inline form and the definition, images included, an image or a link inside
17
- * a link's text too, each once. A reference is rewritten through its
16
+ * inline form and the definition, images included, an image inside a link's
17
+ * text, and a link or an image in an image's alt text or a wiki link's text
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
18
21
  * definition, once.
19
22
  *
20
23
  * Everything here is in 0-based lines of the brief's `lines`, and columns
package/dist/markdown.js CHANGED
@@ -13,8 +13,11 @@
13
13
  * space, `x` or `X`. The scanner reads `[~]`, `[?]` and others as well,
14
14
  * which a brief does not write.
15
15
  * - A link destination is one written in the link or in a definition: the
16
- * inline form and the definition, images included, an image or a link inside
17
- * a link's text too, each once. A reference is rewritten through its
16
+ * inline form and the definition, images included, an image inside a link's
17
+ * text, and a link or an image in an image's alt text or a wiki link's text
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
18
21
  * definition, once.
19
22
  *
20
23
  * Everything here is in 0-based lines of the brief's `lines`, and columns
@@ -29,23 +32,29 @@ function isBox(checkbox) {
29
32
  /**
30
33
  * The links that write their destination - inline links and images, and
31
34
  * definitions - with offsets into the text scanned `at` that offset, added to
32
- * `found` by the offset of the destination.
35
+ * `found`.
33
36
  *
34
- * A link's text may hold an image, `[![diagram](d.png)](d.md)`, whose
35
- * destination moves with the brief as the link's does. spec-core lists it
36
- * after the link, and reads nothing else there: not a link in the text,
37
- * `[a [b](b.md)](c.md)`, which CommonMark takes as the link where spec-core
38
- * takes the outer pair, nor an image in an image's alt text or a wiki link's.
39
- * So each text is read again here, which finds spec-core's image a second
40
- * time. It is one destination, kept once, because two rewrites of the same
41
- * columns would write the second over the first. Each text is shorter than
42
- * the one it came from, so the reading ends.
37
+ * spec-core reads a link's text as CommonMark does: a link inside it is the
38
+ * link, so `[a [b](b.md)](c.md)` gives `b.md` alone, and an image inside it,
39
+ * `[![diagram](d.png)](d.md)`, is listed after the link. It reads nothing in
40
+ * an image's alt text or a wiki link's text, where a destination still moves
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.
46
+ *
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.
43
51
  */
44
- function written(core, at, found) {
52
+ function written(core, at, inText, found) {
45
53
  for (const link of core.links) {
46
- written(scanMarkdown(link.text), at + core.text.indexOf(link.text, link.start), found);
47
- if (link.form === 'inline' || link.form === 'definition') {
48
- found.set(at + link.targetStart, { ...link, targetStart: at + link.targetStart, targetEnd: at + link.targetEnd });
54
+ if (link.image || link.form === 'wiki')
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 });
49
58
  }
50
59
  }
51
60
  return found;
@@ -64,7 +73,7 @@ export function scan(lines) {
64
73
  tasks: core.listItems
65
74
  .filter((item) => isBox(item.checkbox) && item.quoteDepth === 0)
66
75
  .map((item) => ({ line: item.line - 1, end: item.endLine, checked: item.checkbox !== ' ', text: item.firstLine })),
67
- links: [...written(core, 0, new Map()).values()]
76
+ links: written(core, 0, false, [])
68
77
  .map((link) => {
69
78
  // A link's text may run over lines; its destination is on the last.
70
79
  const at = core.index.positionAt(link.targetStart);
@@ -1 +1 @@
1
- {"version":3,"file":"markdown.js","sourceRoot":"","sources":["../src/markdown.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * A brief's Markdown: spec-core's scanner, copied into `src/vendor/spec-core/`,\n * read in spec-brief's dialect.\n *\n * The scanner is exact about what is code and what is a comment, as CommonMark\n * has it, and reports every heading, list item and link it finds. What of that\n * is a brief's structure is decided here (ADR-0001, amended 2026-09-26):\n *\n * - A heading is an ATX heading outside a block quote. Prose over a `---` is a\n * setext heading to a renderer, and a brief that separates its parts that way\n * must not grow a section from it; a heading in a block quote is quoted.\n * - A task is a list item outside a block quote with a box GFM renders: a\n * space, `x` or `X`. The scanner reads `[~]`, `[?]` and others as well,\n * which a brief does not write.\n * - A link destination is one written in the link or in a definition: the\n * inline form and the definition, images included, an image or a link inside\n * a link's text too, each once. 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` by the offset of the destination.\n *\n * A link's text may hold an image, `[![diagram](d.png)](d.md)`, whose\n * destination moves with the brief as the link's does. spec-core lists it\n * after the link, and reads nothing else there: not a link in the text,\n * `[a [b](b.md)](c.md)`, which CommonMark takes as the link where spec-core\n * takes the outer pair, nor an image in an image's alt text or a wiki link's.\n * So each text is read again here, which finds spec-core's image a second\n * time. It is one destination, kept once, because two rewrites of the same\n * columns would write the second over the first. Each text is shorter than\n * the one it came from, so the reading ends.\n */\nfunction written(core: MarkdownScan, at: number, found: Map<number, Link>): Map<number, Link> {\n for (const link of core.links) {\n 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 * `[![diagram](d.png)](d.md)`, is listed after the link. It reads nothing in\n * an image's alt text or a wiki link's text, where a destination still moves\n * with the brief, so those two are read again here, as `inText`. Each is\n * shorter than the text it came from, so the reading ends. Nor does spec-core\n * list a definition inside one, since a bracket pairs across no definition's\n * line, so each destination is found once, and no two rewrites meet on the\n * same columns.\n *\n * Read on its own, such a text is a document, where `[r]: r.md` at its start\n * is a definition. CommonMark reads none inside a paragraph's text, and a\n * renderer shows `![[r]: r.md](z.png)` with that alt text as written, so a\n * definition found there writes no destination.\n */\nfunction written(core: MarkdownScan, at: number, inText: boolean, found: Link[]): Link[] {\n for (const link of core.links) {\n if (link.image || link.form === 'wiki') written(scanMarkdown(link.text), at + core.text.indexOf(link.text, link.start), true, found);\n if (link.form === 'inline' || (link.form === 'definition' && !inText)) {\n found.push({ ...link, targetStart: at + link.targetStart, targetEnd: at + link.targetEnd });\n }\n }\n return found;\n}\n\n/** Scans a brief's lines, which carry no byte-order mark. */\nexport function scan(lines: readonly string[]): Scan {\n const core = scanMarkdown(textOfLines(lines));\n const unquoted = (line: number): boolean => (core.lines[line - 1] as ScannedLine).quoteDepth === 0;\n return {\n lines,\n prose: linesOf(core, 'prose'),\n masked: linesOf(core, 'structure'),\n headings: core.headings\n .filter((h) => h.form === 'atx' && unquoted(h.line))\n .map((h) => ({ line: h.line - 1, level: h.level, text: h.text })),\n tasks: core.listItems\n .filter((item) => isBox(item.checkbox) && item.quoteDepth === 0)\n .map((item) => ({ line: item.line - 1, end: item.endLine, checked: item.checkbox !== ' ', text: item.firstLine })),\n links: written(core, 0, false, [])\n .map((link) => {\n // A link's text may run over lines; its destination is on the last.\n const at = core.index.positionAt(link.targetStart);\n const start = at.column - 1;\n return { line: at.line - 1, start, end: start + link.targetEnd - link.targetStart, target: link.target };\n })\n .sort((a, b) => a.line - b.line || a.start - b.start),\n };\n}\n\n/** Sections: every heading below the title, running to the next heading at its level or above. */\nexport function sectionsOf(result: Scan): Section[] {\n const sections: Section[] = [];\n const { headings } = result;\n for (let h = 0; h < headings.length; h += 1) {\n const heading = headings[h] as Heading;\n if (heading.level < 2) continue;\n let end = result.lines.length;\n for (let k = h + 1; k < headings.length; k += 1) {\n const next = headings[k] as Heading;\n if (next.level <= heading.level) {\n end = next.line;\n break;\n }\n }\n sections.push({ heading, end });\n }\n return sections;\n}\n\n/** The first level-one heading, which is what a reader takes as the title. */\nexport function titleOf(result: Scan): Heading | undefined {\n return result.headings.find((h) => h.level === 1);\n}\n\n/** Whether a line carries anything once comments are removed. */\nexport function hasContent(proseLine: string): boolean {\n return proseLine.trim().length > 0;\n}\n"]}
@@ -2,4 +2,4 @@ export { linesOf, scanMarkdown } from './scan.js';
2
2
  export { sectionsOf, slugify, titleOf } from './headings.js';
3
3
  export { findEntry, frontMatterCloses, frontMatterKind, isNull, keyName, parseInline, readFrontMatter, removeEntry, renderScalar, setEntry, } from './frontmatter.js';
4
4
  export type { FrontMatter, FrontMatterEntry, FrontMatterProblem, ReadOptions, YamlScalar, YamlValue, } from './frontmatter.js';
5
- export type { Block, BlockKind, FrontMatterBlock, FrontMatterKind, Heading, HeadingForm, HtmlComment, Link, LinkForm, ListItem, MarkdownScan, MaskKind, Masks, ScannedLine, Section, Table, TableCell, TableRow, } from './types.js';
5
+ export type { Block, BlockKind, FrontMatterBlock, FrontMatterKind, Heading, HeadingForm, HtmlComment, Link, LinkForm, ListItem, MarkdownScan, MaskKind, Masks, ScannedLine, Section, Table, TableCell, TableRow, UnclosedFrontMatter, } from './types.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../src/vendor/spec-core/markdown/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAClD,OAAO,EAAE,UAAU,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AAC7D,OAAO,EACL,SAAS,EACT,iBAAiB,EACjB,eAAe,EACf,MAAM,EACN,OAAO,EACP,WAAW,EACX,eAAe,EACf,WAAW,EACX,YAAY,EACZ,QAAQ,GACT,MAAM,kBAAkB,CAAC","sourcesContent":["export { linesOf, scanMarkdown } from './scan.js';\nexport { sectionsOf, slugify, titleOf } from './headings.js';\nexport {\n findEntry,\n frontMatterCloses,\n frontMatterKind,\n isNull,\n keyName,\n parseInline,\n readFrontMatter,\n removeEntry,\n renderScalar,\n setEntry,\n} from './frontmatter.js';\nexport type {\n FrontMatter,\n FrontMatterEntry,\n FrontMatterProblem,\n ReadOptions,\n YamlScalar,\n YamlValue,\n} from './frontmatter.js';\nexport type {\n Block,\n BlockKind,\n FrontMatterBlock,\n FrontMatterKind,\n Heading,\n HeadingForm,\n HtmlComment,\n Link,\n LinkForm,\n ListItem,\n MarkdownScan,\n MaskKind,\n Masks,\n ScannedLine,\n Section,\n Table,\n TableCell,\n TableRow,\n} from './types.js';\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../src/vendor/spec-core/markdown/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAClD,OAAO,EAAE,UAAU,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AAC7D,OAAO,EACL,SAAS,EACT,iBAAiB,EACjB,eAAe,EACf,MAAM,EACN,OAAO,EACP,WAAW,EACX,eAAe,EACf,WAAW,EACX,YAAY,EACZ,QAAQ,GACT,MAAM,kBAAkB,CAAC","sourcesContent":["export { linesOf, scanMarkdown } from './scan.js';\nexport { sectionsOf, slugify, titleOf } from './headings.js';\nexport {\n findEntry,\n frontMatterCloses,\n frontMatterKind,\n isNull,\n keyName,\n parseInline,\n readFrontMatter,\n removeEntry,\n renderScalar,\n setEntry,\n} from './frontmatter.js';\nexport type {\n FrontMatter,\n FrontMatterEntry,\n FrontMatterProblem,\n ReadOptions,\n YamlScalar,\n YamlValue,\n} from './frontmatter.js';\nexport type {\n Block,\n BlockKind,\n FrontMatterBlock,\n FrontMatterKind,\n Heading,\n HeadingForm,\n HtmlComment,\n Link,\n LinkForm,\n ListItem,\n MarkdownScan,\n MaskKind,\n Masks,\n ScannedLine,\n Section,\n Table,\n TableCell,\n TableRow,\n UnclosedFrontMatter,\n} from './types.js';\n"]}
@@ -10,6 +10,12 @@
10
10
  * badge wrapped in a link has two destinations - and for nothing else; each
11
11
  * is listed after the link it lies in.
12
12
  *
13
+ * Links may not contain links, as CommonMark has it: in
14
+ * `[a [b](inner.md) c](outer.md)` the inner pair is the link, and the outer
15
+ * brackets and `(outer.md)` are text. So before a paragraph is read its pairs
16
+ * are read innermost first, as CommonMark reads them, to learn which `[`
17
+ * holds a link at any depth.
18
+ *
13
19
  * Brackets are paired once per paragraph with a stack, and the tables that
14
20
  * say where a destination or a title ends are built once per paragraph, so
15
21
  * no bracket searches the text again for its partner. A document of nothing
@@ -21,5 +27,5 @@
21
27
  * move a bound onto those places change no answer, and are equivalent.
22
28
  */
23
29
  import { type Layout } from './layout.js';
24
- import type { Link } from './types.js';
25
- 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[];
@@ -10,6 +10,12 @@
10
10
  * badge wrapped in a link has two destinations - and for nothing else; each
11
11
  * is listed after the link it lies in.
12
12
  *
13
+ * Links may not contain links, as CommonMark has it: in
14
+ * `[a [b](inner.md) c](outer.md)` the inner pair is the link, and the outer
15
+ * brackets and `(outer.md)` are text. So before a paragraph is read its pairs
16
+ * are read innermost first, as CommonMark reads them, to learn which `[`
17
+ * holds a link at any depth.
18
+ *
13
19
  * Brackets are paired once per paragraph with a stack, and the tables that
14
20
  * say where a destination or a title ends are built once per paragraph, so
15
21
  * no bracket searches the text again for its partner. A document of nothing
@@ -21,7 +27,8 @@
21
27
  * move a bound onto those places change no answer, and are equivalent.
22
28
  */
23
29
  import { inRanges, mergeRanges } from '../text/index.js';
24
- import { isMarkdown, isSpaceOrTab, visibleLead } from './layout.js';
30
+ import { isMarkdown, isSpaceOrTab, leadOffset, visibleLead } from './layout.js';
31
+ import { atxLevel, isThematicBreak, listMarker } from './syntax.js';
25
32
  const BANG = 33;
26
33
  const DOUBLE_QUOTE = 34;
27
34
  const SINGLE_QUOTE = 39;
@@ -36,13 +43,17 @@ const LF = 10;
36
43
  const CR = 13;
37
44
  /** CommonMark: a link label holds at most 999 characters. */
38
45
  const MAX_LABEL = 999;
39
- const DEFINITION = /^ {0,3}\[(?!\^)([^\]]+)\]:[ \t]*(<[^<>]*>|\S+)/;
46
+ /**
47
+ * A label holds no bracket but an escaped one. Its two alternatives never
48
+ * start on the same character, so a label is read without backtracking.
49
+ */
50
+ const DEFINITION = /^ {0,3}\[(?!\^)((?:[^[\]\\]|\\.)+)\]:[ \t]*(<[^<>]*>|\S+)/;
40
51
  /** What may follow a definition's destination: nothing, or a title after whitespace. */
41
52
  const TITLE_ONLY = /^(?:[ \t]+(?:"[^"]*"|'[^']*'|\([^)]*\)))?[ \t]*$/;
42
53
  const AUTOLINK = /<((?:https?|ftp|mailto):[^<>\s]+)>/g;
43
- export function findLinks(layout) {
54
+ export function findLinks(layout, headings) {
44
55
  const definitions = new Map();
45
- const defined = readDefinitions(layout, definitions);
56
+ const defined = readDefinitions(layout, headings, definitions);
46
57
  const bracketed = readBrackets(layout, definitions, new Set(defined.map((d) => d.line)));
47
58
  // `<https://x>` as a link's destination or text is part of that link.
48
59
  const taken = mergeRanges([...defined, ...bracketed]);
@@ -64,20 +75,31 @@ function normalizeLabel(label) {
64
75
  *
65
76
  * `[Note]: this matters` is prose that looks like one, so a definition has
66
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.
67
82
  */
68
- function readDefinitions(layout, definitions) {
69
- const { text, structure } = layout;
83
+ function readDefinitions(layout, headings, definitions) {
84
+ const { text, structure, lines, continues } = layout;
85
+ const closed = closingLines(layout, headings);
70
86
  const out = [];
71
- for (const line of layout.lines) {
87
+ for (const line of lines) {
72
88
  if (!isMarkdown(line) || !visibleLead(layout, line))
73
89
  continue;
74
90
  const masked = structure.slice(line.contentStart, line.end);
75
91
  const match = DEFINITION.exec(masked);
76
92
  if (match === null || !TITLE_ONLY.test(masked.slice(match[0].length)))
77
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;
78
100
  const start = line.contentStart + match[0].indexOf('[');
79
- const label = text.slice(start + 1, start + 1 + match[1].length).trim();
80
- if (label.length === 0)
101
+ const label = text.slice(start + 1, start + 1 + between).trim();
102
+ if (label.length === 0 || between > MAX_LABEL)
81
103
  continue;
82
104
  const written = match[2];
83
105
  const end = line.contentStart + match[0].length;
@@ -92,6 +114,40 @@ function readDefinitions(layout, definitions) {
92
114
  }
93
115
  return out;
94
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
+ }
95
151
  function readAutolinks(layout) {
96
152
  const out = [];
97
153
  for (const match of layout.structure.matchAll(AUTOLINK)) {
@@ -112,26 +168,39 @@ function readAutolinks(layout) {
112
168
  }
113
169
  return out;
114
170
  }
115
- /** Pairs `[` with `]` inside a paragraph, innermost first, skipping escaped ones. */
116
171
  function pairBrackets(structure, from, to) {
117
- const pairs = new Map();
172
+ const brackets = { pairs: new Map(), outer: new Map(), before: new Map(), images: new Set(), nested: new Set() };
173
+ const { pairs } = brackets;
118
174
  const open = [];
175
+ let escaped = -1;
119
176
  for (let at = from; at < to; at += 1) {
120
177
  const ch = structure.charCodeAt(at);
121
- if (ch === BACKSLASH)
178
+ if (ch === BACKSLASH) {
179
+ escaped = at + 1;
122
180
  at += 1;
123
- else if (ch === OPEN_BRACKET)
124
- open.push(at);
181
+ }
182
+ else if (ch === OPEN_BRACKET) {
183
+ open.push({ at, before: pairs.size });
184
+ if (structure.charCodeAt(at - 1) === BANG && escaped !== at - 1)
185
+ brackets.images.add(at);
186
+ }
125
187
  else if (ch === CLOSE_BRACKET) {
126
188
  // A `]` with nothing to close pairs with nothing; recorded under
127
189
  // `undefined` it would not be found either, so this test changes no
128
- // answer, only what the map holds.
190
+ // answer, only what the maps hold.
129
191
  const opened = open.pop();
130
- if (opened !== undefined)
131
- pairs.set(opened, at);
192
+ if (opened !== undefined) {
193
+ pairs.set(opened.at, at);
194
+ brackets.before.set(opened.at, opened.before);
195
+ const around = open[open.length - 1];
196
+ if (around !== undefined) {
197
+ brackets.outer.set(opened.at, around.at);
198
+ brackets.nested.add(around.at);
199
+ }
200
+ }
132
201
  }
133
202
  }
134
- return pairs;
203
+ return brackets;
135
204
  }
136
205
  function endsOf(structure, from, to) {
137
206
  const length = to - from;
@@ -167,8 +236,9 @@ function endsOf(structure, from, to) {
167
236
  }
168
237
  /**
169
238
  * The bracket forms, a paragraph at a time. A paragraph here is a run of
170
- * Markdown lines, none blank, each continuing the one above: a bracket never
171
- * pairs across a blank line, a heading, or the start of another block.
239
+ * Markdown lines, none blank or a definition, each continuing the one above:
240
+ * a bracket never pairs across a blank line, a heading, a definition, or the
241
+ * start of another block.
172
242
  */
173
243
  function readBrackets(layout, definitions, defined) {
174
244
  const { lines, continues, structure, text } = layout;
@@ -178,10 +248,13 @@ function readBrackets(layout, definitions, defined) {
178
248
  let doubleClose = -2;
179
249
  const lookup = (from, to) => to - from > MAX_LABEL ? undefined : definitions.get(normalizeLabel(text.slice(from, to)));
180
250
  // A blank or code line let into a paragraph would bring only characters the
181
- // structure mask blanks, so this test shapes the runs and changes no link.
251
+ // structure mask blanks, so those two tests shape the runs and change no
252
+ // link. A definition's line is no paragraph's: it holds a label, a
253
+ // destination and a title, none of them a link, and the text after it is a
254
+ // paragraph of its own, whose brackets pair with nothing before it.
182
255
  const inParagraph = (i) => {
183
256
  const line = lines[i];
184
- return line !== undefined && isMarkdown(line) && !line.blank;
257
+ return line !== undefined && isMarkdown(line) && !line.blank && !defined.has(i + 1);
185
258
  };
186
259
  for (let first = 0; first < lines.length; first += 1) {
187
260
  if (!inParagraph(first))
@@ -191,7 +264,10 @@ function readBrackets(layout, definitions, defined) {
191
264
  last += 1;
192
265
  const from = lines[first].start;
193
266
  const to = lines[last].end;
194
- const pairs = pairBrackets(structure, from, to);
267
+ const brackets = pairBrackets(structure, from, to);
268
+ const { pairs } = brackets;
269
+ // Each `[` with a link inside it, at any depth.
270
+ const holding = new Set();
195
271
  let ends = null;
196
272
  // Spaces and tabs, and at most one line ending inside the paragraph. The
197
273
  // character at `to` ends the paragraph's last line and is none of these.
@@ -300,7 +376,8 @@ function readBrackets(layout, definitions, defined) {
300
376
  };
301
377
  const bracket = (at, start, image, line) => {
302
378
  const close = pairs.get(at);
303
- if (close === undefined)
379
+ // A link holding a link is text; an image holding one is still an image.
380
+ if (close === undefined || (!image && holding.has(at)))
304
381
  return null;
305
382
  const label = () => text.slice(at + 1, close).trim();
306
383
  if (structure.charCodeAt(close + 1) === OPEN_PAREN) {
@@ -311,11 +388,14 @@ function readBrackets(layout, definitions, defined) {
311
388
  }
312
389
  }
313
390
  else {
314
- // Only a `[` is paired, so a second label follows exactly when this finds one.
391
+ // Only a `[` is paired, so a second bracket follows exactly when this
392
+ // finds one. It is a label unless it holds a bracket, which it does
393
+ // when a pair lies inside it, or more than 999 characters; one that is
394
+ // not leaves the first bracket to be read as a shortcut.
315
395
  const refClose = pairs.get(close + 1);
316
- if (refClose !== undefined) {
396
+ if (refClose !== undefined && refClose - close - 2 <= MAX_LABEL && !brackets.nested.has(close + 1)) {
317
397
  // `[text][]` is read through its text; `[text][label]` through its label.
318
- const collapsed = refClose - close - 2 <= MAX_LABEL && text.slice(close + 2, refClose).trim() === '';
398
+ const collapsed = text.slice(close + 2, refClose).trim() === '';
319
399
  const found = collapsed ? lookup(at + 1, close) : lookup(close + 2, refClose);
320
400
  if (found === undefined)
321
401
  return null;
@@ -329,10 +409,32 @@ function readBrackets(layout, definitions, defined) {
329
409
  return null;
330
410
  return { form: 'shortcut', image, text: label(), label: label(), start, end: close + 1, line: line.line, ...found };
331
411
  };
332
- let escaped = -1;
412
+ const lineAt = (at) => lines[layout.index.positionAt(at).line - 1];
413
+ // CommonMark reads each pair at its `]`, so the pairs inside a `[` are
414
+ // read before it, and a link among them makes it text, and every `[`
415
+ // open around it: the pairs come innermost first, and fill `holding` on
416
+ // the way out. An image among them does not; a link may hold one. What a
417
+ // link or an image reads past its `]` - a destination, a title, a second
418
+ // label - is not read for links again: `reach` is the furthest any of the
419
+ // first so many pairs read to, and a `[` short of it opens nothing.
420
+ const reach = [from];
421
+ for (const at of pairs.keys()) {
422
+ const line = lineAt(at);
423
+ const image = brackets.images.has(at);
424
+ const passed = reach[brackets.before.get(at)] > at;
425
+ const link = passed ? null : bracket(at, at, image, line);
426
+ reach.push(Math.max(reach[reach.length - 1], link === null ? from : link.end));
427
+ const around = brackets.outer.get(at);
428
+ if (around !== undefined && (holding.has(at) || (link !== null && !image)))
429
+ holding.add(around);
430
+ }
333
431
  let at = from;
334
432
  // Inside a link's text, up to its `]`, where CommonMark reads an image -
335
433
  // a badge wrapped in a link - but no other link; then on past the link.
434
+ // `textEnd` is -1 or a `]` that closes a `[`, so never 0, and the `]` is
435
+ // read as no link. Leaving one step later, past it, resumes at the same
436
+ // place, or ends the paragraph where a shortcut link ended it anyway: the
437
+ // test below reads the same with `textEnd > 0` or `at > textEnd`.
336
438
  let textEnd = -1;
337
439
  let resume = -1;
338
440
  while (at < to) {
@@ -343,7 +445,6 @@ function readBrackets(layout, definitions, defined) {
343
445
  }
344
446
  const ch = structure.charCodeAt(at);
345
447
  if (ch === BACKSLASH) {
346
- escaped = at + 1;
347
448
  at += 2;
348
449
  continue;
349
450
  }
@@ -351,14 +452,8 @@ function readBrackets(layout, definitions, defined) {
351
452
  at += 1;
352
453
  continue;
353
454
  }
354
- const line = lines[layout.index.positionAt(at).line - 1];
355
- // A definition's line holds its label, its destination and its title,
356
- // and none of them is a link.
357
- if (defined.has(line.line)) {
358
- at = line.end;
359
- continue;
360
- }
361
- const image = structure.charCodeAt(at - 1) === BANG && escaped !== at - 1;
455
+ const line = lineAt(at);
456
+ const image = brackets.images.has(at);
362
457
  const start = image ? at - 1 : at;
363
458
  const link = wiki(at, start, image, line) ?? bracket(at, start, image, line);
364
459
  if (textEnd >= 0) {