@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 +62 -0
- package/README.md +1 -1
- package/dist/markdown.d.ts +5 -2
- package/dist/markdown.js +26 -17
- package/dist/markdown.js.map +1 -1
- package/dist/vendor/spec-core/markdown/index.d.ts +1 -1
- package/dist/vendor/spec-core/markdown/index.js.map +1 -1
- package/dist/vendor/spec-core/markdown/links.d.ts +8 -2
- package/dist/vendor/spec-core/markdown/links.js +132 -37
- package/dist/vendor/spec-core/markdown/links.js.map +1 -1
- package/dist/vendor/spec-core/markdown/scan.js +24 -14
- package/dist/vendor/spec-core/markdown/scan.js.map +1 -1
- package/dist/vendor/spec-core/markdown/syntax.d.ts +2 -0
- package/dist/vendor/spec-core/markdown/syntax.js +6 -1
- package/dist/vendor/spec-core/markdown/syntax.js.map +1 -1
- package/dist/vendor/spec-core/markdown/types.d.ts +23 -0
- package/dist/vendor/spec-core/markdown/types.js.map +1 -1
- package/dist/vendor/spec-core/pattern/glob.d.ts +2 -1
- package/dist/vendor/spec-core/pattern/glob.js +61 -2
- package/dist/vendor/spec-core/pattern/glob.js.map +1 -1
- package/package.json +5 -2
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)`, `` - one inside a link's text too - and a definition, `[label]: dest`, through which a reference is rewritten once.
|
|
203
|
+
A brief is read as CommonMark reads it wherever that decides what is code or a comment - fenced and indented code, `<pre>`, `<script>`, `<style>` and `<textarea>` blocks, code spans, HTML comments - and nothing inside those is structure. A section, and the title, is an ATX heading, `## Name`, outside a block quote: prose over a `---` line is prose and a rule, and a heading quoted from another document is not one of the brief's sections. A task is a list item outside a block quote with `[ ]`, `[x]` or `[X]`. The links archival rewrites are the ones that write a destination: `[text](dest)`, `` - one inside a link's text too - and a definition, `[label]: dest`, through which a reference is rewritten once. A definition is one where CommonMark reads it, opening a paragraph or under another: the shape of one under a paragraph's text, or in alt text, is text and stays as written.
|
|
204
204
|
|
|
205
205
|
## Rules
|
|
206
206
|
|
package/dist/markdown.d.ts
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
|
|
17
|
-
* a link
|
|
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
|
|
17
|
-
* a link
|
|
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
|
|
35
|
+
* `found`.
|
|
33
36
|
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
* after the link
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
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
|
+
* `[](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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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:
|
|
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);
|
package/dist/markdown.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"markdown.js","sourceRoot":"","sources":["../src/markdown.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * A brief's Markdown: spec-core's scanner, copied into `src/vendor/spec-core/`,\n * read in spec-brief's dialect.\n *\n * The scanner is exact about what is code and what is a comment, as CommonMark\n * has it, and reports every heading, list item and link it finds. What of that\n * is a brief's structure is decided here (ADR-0001, amended 2026-09-26):\n *\n * - A heading is an ATX heading outside a block quote. Prose over a `---` is a\n * setext heading to a renderer, and a brief that separates its parts that way\n * must not grow a section from it; a heading in a block quote is quoted.\n * - A task is a list item outside a block quote with a box GFM renders: a\n * space, `x` or `X`. The scanner reads `[~]`, `[?]` and others as well,\n * which a brief does not write.\n * - A link destination is one written in the link or in a definition: the\n * inline form and the definition, images included, an image 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, `[](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 * `[](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
|
-
|
|
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
|
|
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 +
|
|
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
|
|
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
|
-
|
|
124
|
-
|
|
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
|
|
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
|
|
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:
|
|
171
|
-
* pairs across a blank line, a heading, or the
|
|
239
|
+
* Markdown lines, none blank or a definition, each continuing the one above:
|
|
240
|
+
* a bracket never pairs across a blank line, a heading, a definition, or the
|
|
241
|
+
* start of another block.
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
355
|
-
|
|
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) {
|