@descent-vtt/spec-graph 0.7.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.
- package/CHANGELOG.md +477 -0
- package/README.md +109 -17
- package/dist/cli.d.ts +2 -2
- package/dist/cli.js +38 -19
- package/dist/cli.js.map +1 -1
- package/dist/directives.d.ts +9 -2
- package/dist/directives.js +15 -4
- package/dist/directives.js.map +1 -1
- package/dist/extract.d.ts +1 -1
- package/dist/extract.js +50 -16
- package/dist/extract.js.map +1 -1
- package/dist/glob.d.ts +44 -44
- package/dist/glob.js +152 -116
- package/dist/glob.js.map +1 -1
- package/dist/identity.d.ts +12 -4
- package/dist/identity.js +32 -8
- package/dist/identity.js.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.js +4 -2
- package/dist/index.js.map +1 -1
- package/dist/lifecycle.js +10 -2
- package/dist/lifecycle.js.map +1 -1
- package/dist/markdown.d.ts +53 -161
- package/dist/markdown.js +42 -864
- package/dist/markdown.js.map +1 -1
- package/dist/report.d.ts +23 -0
- package/dist/report.js +80 -0
- package/dist/report.js.map +1 -1
- package/dist/rules.js +48 -14
- package/dist/rules.js.map +1 -1
- package/dist/sections.d.ts +1 -1
- package/dist/sections.js +18 -4
- package/dist/sections.js.map +1 -1
- package/dist/select.d.ts +2 -2
- package/dist/select.js +3 -3
- package/dist/select.js.map +1 -1
- package/dist/source.d.ts +6 -19
- package/dist/source.js +4 -70
- package/dist/source.js.map +1 -1
- package/dist/types.d.ts +23 -3
- package/dist/types.js +1 -1
- package/dist/types.js.map +1 -1
- package/dist/vendor/spec-core/markdown/frontmatter.d.ts +103 -0
- package/dist/vendor/spec-core/markdown/frontmatter.js +459 -0
- package/dist/vendor/spec-core/markdown/frontmatter.js.map +1 -0
- package/dist/vendor/spec-core/markdown/headings.d.ts +45 -0
- package/dist/vendor/spec-core/markdown/headings.js +199 -0
- package/dist/vendor/spec-core/markdown/headings.js.map +1 -0
- package/dist/vendor/spec-core/markdown/index.d.ts +5 -0
- package/dist/vendor/spec-core/markdown/index.js +4 -0
- package/dist/vendor/spec-core/markdown/index.js.map +1 -0
- package/dist/vendor/spec-core/markdown/layout.d.ts +39 -0
- package/dist/vendor/spec-core/markdown/layout.js +28 -0
- package/dist/vendor/spec-core/markdown/layout.js.map +1 -0
- package/dist/vendor/spec-core/markdown/links.d.ts +22 -0
- package/dist/vendor/spec-core/markdown/links.js +364 -0
- package/dist/vendor/spec-core/markdown/links.js.map +1 -0
- package/dist/vendor/spec-core/markdown/lists.d.ts +22 -0
- package/dist/vendor/spec-core/markdown/lists.js +127 -0
- package/dist/vendor/spec-core/markdown/lists.js.map +1 -0
- package/dist/vendor/spec-core/markdown/scan.d.ts +36 -0
- package/dist/vendor/spec-core/markdown/scan.js +439 -0
- package/dist/vendor/spec-core/markdown/scan.js.map +1 -0
- package/dist/vendor/spec-core/markdown/syntax.d.ts +75 -0
- package/dist/vendor/spec-core/markdown/syntax.js +181 -0
- package/dist/vendor/spec-core/markdown/syntax.js.map +1 -0
- package/dist/vendor/spec-core/markdown/tables.d.ts +18 -0
- package/dist/vendor/spec-core/markdown/tables.js +86 -0
- package/dist/vendor/spec-core/markdown/tables.js.map +1 -0
- package/dist/vendor/spec-core/markdown/types.d.ts +287 -0
- package/dist/vendor/spec-core/markdown/types.js +9 -0
- package/dist/vendor/spec-core/markdown/types.js.map +1 -0
- package/dist/vendor/spec-core/path/index.d.ts +1 -0
- package/dist/vendor/spec-core/path/index.js +2 -0
- package/dist/vendor/spec-core/path/index.js.map +1 -0
- package/dist/vendor/spec-core/path/posix.d.ts +83 -0
- package/dist/vendor/spec-core/path/posix.js +150 -0
- package/dist/vendor/spec-core/path/posix.js.map +1 -0
- package/dist/vendor/spec-core/pattern/automaton.d.ts +129 -0
- package/dist/vendor/spec-core/pattern/automaton.js +340 -0
- package/dist/vendor/spec-core/pattern/automaton.js.map +1 -0
- package/dist/vendor/spec-core/pattern/charset.d.ts +58 -0
- package/dist/vendor/spec-core/pattern/charset.js +116 -0
- package/dist/vendor/spec-core/pattern/charset.js.map +1 -0
- package/dist/vendor/spec-core/pattern/glob.d.ts +121 -0
- package/dist/vendor/spec-core/pattern/glob.js +481 -0
- package/dist/vendor/spec-core/pattern/glob.js.map +1 -0
- package/dist/vendor/spec-core/pattern/index.d.ts +6 -0
- package/dist/vendor/spec-core/pattern/index.js +4 -0
- package/dist/vendor/spec-core/pattern/index.js.map +1 -0
- package/dist/{regex.d.ts → vendor/spec-core/pattern/regex.d.ts} +20 -29
- package/dist/{regex.js → vendor/spec-core/pattern/regex.js} +40 -49
- package/dist/vendor/spec-core/pattern/regex.js.map +1 -0
- package/dist/vendor/spec-core/text/index.d.ts +2 -0
- package/dist/vendor/spec-core/text/index.js +2 -0
- package/dist/vendor/spec-core/text/index.js.map +1 -0
- package/dist/vendor/spec-core/text/lines.d.ts +72 -0
- package/dist/vendor/spec-core/text/lines.js +156 -0
- package/dist/vendor/spec-core/text/lines.js.map +1 -0
- package/dist/yaml.d.ts +44 -12
- package/dist/yaml.js +115 -206
- package/dist/yaml.js.map +1 -1
- package/package.json +3 -2
- package/src/vendor/spec-core/LICENSE +21 -0
- package/src/vendor/spec-core/README.md +8 -0
- package/dist/regex.js.map +0 -1
package/dist/markdown.js
CHANGED
|
@@ -1,887 +1,65 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* What spec-graph reads out of a Markdown document.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
4
|
+
* The scan is spec-core's, copied into `src/vendor/spec-core/markdown/`: one
|
|
5
|
+
* pass that is exact about code and comments, as CommonMark is, and simple about
|
|
6
|
+
* the rest (ADR-0001). It started as this file, and the guarantees it was
|
|
7
|
+
* written for came with it: code never counts, items are blocks and not lines,
|
|
8
|
+
* and everything carries absolute offsets.
|
|
9
9
|
*
|
|
10
|
-
* What
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* - **Code never counts.** Fenced blocks, indented blocks and inline spans are
|
|
14
|
-
* masked before a single link is extracted, so an example in a code sample
|
|
15
|
-
* never becomes a graph edge.
|
|
16
|
-
* - **Items are blocks, not lines.** A list item owns its wrapped continuation
|
|
17
|
-
* lines and its nested content, so a resolution written on the second line of
|
|
18
|
-
* a bullet is found instead of missed.
|
|
19
|
-
* - **Everything carries absolute offsets.** No re-scanning, no drift between
|
|
20
|
-
* the text a rule matched and the position it reports.
|
|
10
|
+
* What stays here is what spec-graph decides about the scan, which no other
|
|
11
|
+
* tool has to agree with: which links cite something, which anchors a heading
|
|
12
|
+
* answers to, and which lines are Markdown at all.
|
|
21
13
|
*/
|
|
22
|
-
|
|
23
|
-
/* -------------------------------------------------------------------------- */
|
|
24
|
-
/* Entry point */
|
|
25
|
-
/* -------------------------------------------------------------------------- */
|
|
26
|
-
export function scanMarkdown(source) {
|
|
27
|
-
const text = source.charCodeAt(0) === 0xfeff ? source.slice(1) : source;
|
|
28
|
-
const index = createLineIndex(text);
|
|
29
|
-
const frontMatter = readFrontMatter(text, index);
|
|
30
|
-
const bodyStart = frontMatter ? frontMatter.bodyStart : 0;
|
|
31
|
-
const lines = scanLines(text, index, bodyStart);
|
|
32
|
-
// Raw-text HTML joins the code ranges rather than sitting beside them: what
|
|
33
|
-
// both have in common is that their content is not Markdown, and every reader
|
|
34
|
-
// downstream - comments, links, tables - already asks that one question.
|
|
35
|
-
const codeRanges = mergeRanges([...collectCodeRanges(lines, bodyStart), ...collectRawTextHtml(lines, bodyStart)]);
|
|
36
|
-
const comments = scanComments(text, index, bodyStart, codeRanges);
|
|
37
|
-
const inlineCode = scanInlineCode(text, bodyStart, codeRanges, comments);
|
|
38
|
-
const maskRanges = [
|
|
39
|
-
...(frontMatter ? [{ start: 0, end: frontMatter.bodyStart }] : []),
|
|
40
|
-
...codeRanges,
|
|
41
|
-
...inlineCode,
|
|
42
|
-
...comments.map((c) => ({ start: c.start, end: c.end })),
|
|
43
|
-
];
|
|
44
|
-
const masked = applyMask(text, maskRanges);
|
|
45
|
-
const sortedMask = mergeRanges(maskRanges);
|
|
46
|
-
const headings = scanHeadings(lines);
|
|
47
|
-
const listItems = scanListItems(lines, text, masked);
|
|
48
|
-
const links = scanLinks(masked, text, index);
|
|
49
|
-
const tables = scanTables(lines, text, masked);
|
|
50
|
-
return {
|
|
51
|
-
text,
|
|
52
|
-
index,
|
|
53
|
-
frontMatter,
|
|
54
|
-
bodyStart,
|
|
55
|
-
lines,
|
|
56
|
-
headings,
|
|
57
|
-
listItems,
|
|
58
|
-
comments,
|
|
59
|
-
links,
|
|
60
|
-
tables,
|
|
61
|
-
masked,
|
|
62
|
-
isMasked: (offset) => containsOffset(sortedMask, offset),
|
|
63
|
-
};
|
|
64
|
-
}
|
|
65
|
-
/* -------------------------------------------------------------------------- */
|
|
66
|
-
/* Front matter */
|
|
67
|
-
/* -------------------------------------------------------------------------- */
|
|
68
|
-
const FENCE_FM = /^(-{3,}|\+{3,})[ \t]*$/;
|
|
69
|
-
function readFrontMatter(text, index) {
|
|
70
|
-
if (index.lineCount === 0)
|
|
71
|
-
return null;
|
|
72
|
-
const first = index.lineText(1);
|
|
73
|
-
const open = FENCE_FM.exec(first);
|
|
74
|
-
if (!open)
|
|
75
|
-
return null;
|
|
76
|
-
const closer = open[1].startsWith('-') ? /^(-{3,}|\.{3,})[ \t]*$/ : /^\+{3,}[ \t]*$/;
|
|
77
|
-
for (let line = 2; line <= index.lineCount; line += 1) {
|
|
78
|
-
if (closer.test(index.lineText(line))) {
|
|
79
|
-
const start = index.lineStart(2);
|
|
80
|
-
const end = index.lineStart(line);
|
|
81
|
-
const bodyStart = line < index.lineCount ? index.lineStart(line + 1) : text.length;
|
|
82
|
-
return { raw: text.slice(start, end), start, end, bodyStart };
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
// An unterminated opener is a horizontal rule, not front matter.
|
|
86
|
-
return null;
|
|
87
|
-
}
|
|
88
|
-
/* -------------------------------------------------------------------------- */
|
|
89
|
-
/* Line classification */
|
|
90
|
-
/* -------------------------------------------------------------------------- */
|
|
91
|
-
const LIST_MARKER = /^([ \t]*)([-*+]|\d{1,9}[.)])([ \t]+|$)/;
|
|
92
|
-
const ATX_HEADING = /^ {0,3}(#{1,6})(?:[ \t]+(.*?))?[ \t]*(?:#+[ \t]*)?$/;
|
|
93
|
-
const SETEXT_UNDERLINE = /^ {0,3}(=+|-+)[ \t]*$/;
|
|
94
|
-
const THEMATIC_BREAK = /^ {0,3}(?:(?:\*[ \t]*){3,}|(?:-[ \t]*){3,}|(?:_[ \t]*){3,})$/;
|
|
95
|
-
const FENCE_OPEN = /^([ \t]*)(`{3,}|~{3,})(.*)$/;
|
|
96
|
-
/** Counts indentation in columns, expanding tabs to the next multiple of four. */
|
|
97
|
-
function measureIndent(line) {
|
|
98
|
-
let width = 0;
|
|
99
|
-
for (const ch of line) {
|
|
100
|
-
if (ch === ' ')
|
|
101
|
-
width += 1;
|
|
102
|
-
else if (ch === '\t')
|
|
103
|
-
width += 4 - (width % 4);
|
|
104
|
-
else
|
|
105
|
-
break;
|
|
106
|
-
}
|
|
107
|
-
return width;
|
|
108
|
-
}
|
|
109
|
-
/** Strips `>` block-quote markers, returning the offset and text of the content. */
|
|
110
|
-
function stripQuotes(raw, lineStart) {
|
|
111
|
-
let i = 0;
|
|
112
|
-
let depth = 0;
|
|
113
|
-
for (;;) {
|
|
114
|
-
let j = i;
|
|
115
|
-
let spaces = 0;
|
|
116
|
-
while (j < raw.length && (raw[j] === ' ' || raw[j] === '\t') && spaces < 3) {
|
|
117
|
-
spaces += 1;
|
|
118
|
-
j += 1;
|
|
119
|
-
}
|
|
120
|
-
if (raw[j] !== '>')
|
|
121
|
-
break;
|
|
122
|
-
j += 1;
|
|
123
|
-
if (raw[j] === ' ')
|
|
124
|
-
j += 1;
|
|
125
|
-
depth += 1;
|
|
126
|
-
i = j;
|
|
127
|
-
}
|
|
128
|
-
return { contentStart: lineStart + i, content: raw.slice(i), depth };
|
|
129
|
-
}
|
|
130
|
-
function scanLines(text, index, bodyStart) {
|
|
131
|
-
const out = [];
|
|
132
|
-
const firstLine = index.positionAt(bodyStart).line;
|
|
133
|
-
let fence = null;
|
|
134
|
-
let inList = false;
|
|
135
|
-
let listIndent = 0;
|
|
136
|
-
let previousBlank = true;
|
|
137
|
-
let indentedCode = false;
|
|
138
|
-
for (let line = firstLine; line <= index.lineCount; line += 1) {
|
|
139
|
-
const start = index.lineStart(line);
|
|
140
|
-
const end = index.lineEnd(line);
|
|
141
|
-
if (end < bodyStart)
|
|
142
|
-
continue;
|
|
143
|
-
const raw = text.slice(start, end);
|
|
144
|
-
const { contentStart, content, depth } = stripQuotes(raw, start);
|
|
145
|
-
const indent = measureIndent(content);
|
|
146
|
-
const blank = content.trim().length === 0;
|
|
147
|
-
let code = false;
|
|
148
|
-
if (fence) {
|
|
149
|
-
code = true;
|
|
150
|
-
const close = FENCE_OPEN.exec(content);
|
|
151
|
-
if (close &&
|
|
152
|
-
close[2][0] === fence.char &&
|
|
153
|
-
close[2].length >= fence.length &&
|
|
154
|
-
close[3].trim().length === 0 &&
|
|
155
|
-
measureIndent(close[1]) <= fence.indent + 3) {
|
|
156
|
-
fence = null;
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
else {
|
|
160
|
-
const open = FENCE_OPEN.exec(content);
|
|
161
|
-
if (open && open[3].indexOf('`') === -1) {
|
|
162
|
-
fence = {
|
|
163
|
-
char: open[2][0],
|
|
164
|
-
length: open[2].length,
|
|
165
|
-
indent: measureIndent(open[1]),
|
|
166
|
-
};
|
|
167
|
-
code = true;
|
|
168
|
-
indentedCode = false;
|
|
169
|
-
}
|
|
170
|
-
else if (blank) {
|
|
171
|
-
code = indentedCode;
|
|
172
|
-
}
|
|
173
|
-
else {
|
|
174
|
-
// Indented code is only recognised outside list containers. Inside a
|
|
175
|
-
// list, four-space indentation is ordinary continuation far more often
|
|
176
|
-
// than it is a code block, and masking a continuation would silently
|
|
177
|
-
// drop the links and resolutions written there.
|
|
178
|
-
if (!inList && indent >= 4 && (previousBlank || indentedCode)) {
|
|
179
|
-
code = true;
|
|
180
|
-
indentedCode = true;
|
|
181
|
-
}
|
|
182
|
-
else {
|
|
183
|
-
indentedCode = false;
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
if (!code && !blank) {
|
|
188
|
-
const marker = LIST_MARKER.exec(content);
|
|
189
|
-
if (marker) {
|
|
190
|
-
inList = true;
|
|
191
|
-
listIndent = indent;
|
|
192
|
-
}
|
|
193
|
-
else if (inList && indent === 0 && previousBlank) {
|
|
194
|
-
inList = false;
|
|
195
|
-
listIndent = 0;
|
|
196
|
-
}
|
|
197
|
-
else if (inList && indent <= listIndent && previousBlank && ATX_HEADING.test(content)) {
|
|
198
|
-
inList = false;
|
|
199
|
-
listIndent = 0;
|
|
200
|
-
}
|
|
201
|
-
if (ATX_HEADING.test(content) && indent < 4) {
|
|
202
|
-
inList = false;
|
|
203
|
-
listIndent = 0;
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
out.push({ line, start, end, contentStart, content, indent, blank, quoteDepth: depth, code });
|
|
207
|
-
previousBlank = blank;
|
|
208
|
-
}
|
|
209
|
-
return out;
|
|
210
|
-
}
|
|
14
|
+
export { scanMarkdown, slugify } from './vendor/spec-core/markdown/index.js';
|
|
211
15
|
/**
|
|
212
|
-
*
|
|
16
|
+
* Whether a line's content is Markdown.
|
|
213
17
|
*
|
|
214
|
-
*
|
|
215
|
-
*
|
|
216
|
-
*
|
|
217
|
-
* to annotate a document puts `<!-- @spec-node id="..." -->` inside a script
|
|
218
|
-
* sample or a `<pre>` block, and reading that as a directive lets a worked
|
|
219
|
-
* example rename the document it appears in.
|
|
220
|
-
*
|
|
221
|
-
* `<div>` and `<details>` are deliberately not on the list. Their content *is*
|
|
222
|
-
* Markdown, and a decision written inside a collapsed `<details>` section is
|
|
223
|
-
* still a decision.
|
|
18
|
+
* Code, the four raw-text HTML elements and front matter are not, for the same
|
|
19
|
+
* reason: a heading, a status or an item written in one is an example or data,
|
|
20
|
+
* and never a statement the document makes.
|
|
224
21
|
*/
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
function collectRawTextHtml(lines, bodyStart) {
|
|
228
|
-
const ranges = [];
|
|
229
|
-
let open = null;
|
|
230
|
-
for (const line of lines) {
|
|
231
|
-
if (line.code)
|
|
232
|
-
continue;
|
|
233
|
-
if (open === null) {
|
|
234
|
-
if (!RAW_TEXT_OPEN.test(line.content.trimStart()))
|
|
235
|
-
continue;
|
|
236
|
-
open = { start: Math.max(line.start, bodyStart), end: line.end };
|
|
237
|
-
}
|
|
238
|
-
else {
|
|
239
|
-
open.end = line.end;
|
|
240
|
-
}
|
|
241
|
-
// The close tag ends the block on the line that carries it, whichever of
|
|
242
|
-
// the four it names. An unclosed block runs to the end of the document,
|
|
243
|
-
// which is what a browser does with it too.
|
|
244
|
-
if (RAW_TEXT_CLOSE.test(line.content)) {
|
|
245
|
-
ranges.push(open);
|
|
246
|
-
open = null;
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
if (open)
|
|
250
|
-
ranges.push(open);
|
|
251
|
-
return ranges;
|
|
252
|
-
}
|
|
253
|
-
function collectCodeRanges(lines, bodyStart) {
|
|
254
|
-
const ranges = [];
|
|
255
|
-
let open = null;
|
|
256
|
-
for (const line of lines) {
|
|
257
|
-
if (line.code) {
|
|
258
|
-
const start = Math.max(line.start, bodyStart);
|
|
259
|
-
if (open && open.end >= line.start - 2)
|
|
260
|
-
open.end = line.end;
|
|
261
|
-
else {
|
|
262
|
-
if (open)
|
|
263
|
-
ranges.push(open);
|
|
264
|
-
open = { start, end: line.end };
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
else if (open) {
|
|
268
|
-
ranges.push(open);
|
|
269
|
-
open = null;
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
if (open)
|
|
273
|
-
ranges.push(open);
|
|
274
|
-
return ranges;
|
|
275
|
-
}
|
|
276
|
-
/* -------------------------------------------------------------------------- */
|
|
277
|
-
/* Headings */
|
|
278
|
-
/* -------------------------------------------------------------------------- */
|
|
279
|
-
export function slugify(text) {
|
|
280
|
-
return text
|
|
281
|
-
.trim()
|
|
282
|
-
.toLowerCase()
|
|
283
|
-
.replace(/[`*_~[\]()<>#!|]/g, '')
|
|
284
|
-
.replace(/[^\p{L}\p{N}\s-]/gu, '')
|
|
285
|
-
.trim()
|
|
286
|
-
// GitHub hyphenates every whitespace character, so a run of two spaces left
|
|
287
|
-
// behind by stripped punctuation becomes two hyphens, not one.
|
|
288
|
-
.replace(/\s/g, '-');
|
|
289
|
-
}
|
|
290
|
-
function scanHeadings(lines) {
|
|
291
|
-
const out = [];
|
|
292
|
-
for (let i = 0; i < lines.length; i += 1) {
|
|
293
|
-
const line = lines[i];
|
|
294
|
-
if (line.code || line.blank)
|
|
295
|
-
continue;
|
|
296
|
-
const atx = ATX_HEADING.exec(line.content);
|
|
297
|
-
if (atx && line.indent < 4) {
|
|
298
|
-
const body = (atx[2] ?? '').trim();
|
|
299
|
-
out.push({
|
|
300
|
-
level: atx[1].length,
|
|
301
|
-
text: body,
|
|
302
|
-
slug: slugify(body),
|
|
303
|
-
start: line.contentStart,
|
|
304
|
-
end: line.end,
|
|
305
|
-
line: line.line,
|
|
306
|
-
});
|
|
307
|
-
continue;
|
|
308
|
-
}
|
|
309
|
-
// Setext: an underline of `=` or `-` directly below a paragraph line. The
|
|
310
|
-
// preceding line must not itself be a heading, a list item or a fence.
|
|
311
|
-
const next = lines[i + 1];
|
|
312
|
-
if (!next || next.code || next.blank)
|
|
313
|
-
continue;
|
|
314
|
-
const under = SETEXT_UNDERLINE.exec(next.content);
|
|
315
|
-
if (!under)
|
|
316
|
-
continue;
|
|
317
|
-
if (LIST_MARKER.test(line.content) || THEMATIC_BREAK.test(line.content))
|
|
318
|
-
continue;
|
|
319
|
-
if (line.indent >= 4)
|
|
320
|
-
continue;
|
|
321
|
-
const body = line.content.trim();
|
|
322
|
-
if (body.length === 0)
|
|
323
|
-
continue;
|
|
324
|
-
out.push({
|
|
325
|
-
level: under[1][0] === '=' ? 1 : 2,
|
|
326
|
-
text: body,
|
|
327
|
-
slug: slugify(body),
|
|
328
|
-
start: line.contentStart,
|
|
329
|
-
end: next.end,
|
|
330
|
-
line: line.line,
|
|
331
|
-
});
|
|
332
|
-
}
|
|
333
|
-
return out;
|
|
334
|
-
}
|
|
335
|
-
/* -------------------------------------------------------------------------- */
|
|
336
|
-
/* List items */
|
|
337
|
-
/* -------------------------------------------------------------------------- */
|
|
338
|
-
const CHECKBOX = /^\[([ xX~\-?!*/+])\](?=[ \t]|$)/;
|
|
339
|
-
function scanListItems(lines, text, masked) {
|
|
340
|
-
const out = [];
|
|
341
|
-
// Indents of the list items currently enclosing the cursor, outermost first.
|
|
342
|
-
const stack = [];
|
|
343
|
-
for (let i = 0; i < lines.length; i += 1) {
|
|
344
|
-
const line = lines[i];
|
|
345
|
-
if (line.code || line.blank)
|
|
346
|
-
continue;
|
|
347
|
-
const marker = LIST_MARKER.exec(line.content);
|
|
348
|
-
if (!marker)
|
|
349
|
-
continue;
|
|
350
|
-
const indent = measureIndent(marker[1]);
|
|
351
|
-
while (stack.length > 0 && indent <= stack[stack.length - 1])
|
|
352
|
-
stack.pop();
|
|
353
|
-
const depth = stack.length;
|
|
354
|
-
stack.push(indent);
|
|
355
|
-
const markerStart = line.contentStart + marker[1].length;
|
|
356
|
-
let textStart = markerStart + marker[2].length + marker[3].length;
|
|
357
|
-
const rest = text.slice(textStart, line.end);
|
|
358
|
-
const box = CHECKBOX.exec(rest);
|
|
359
|
-
let checkbox = null;
|
|
360
|
-
let checkboxStart = null;
|
|
361
|
-
if (box) {
|
|
362
|
-
checkbox = box[1];
|
|
363
|
-
checkboxStart = textStart;
|
|
364
|
-
textStart += box[0].length;
|
|
365
|
-
while (text[textStart] === ' ' || text[textStart] === '\t')
|
|
366
|
-
textStart += 1;
|
|
367
|
-
}
|
|
368
|
-
const end = findItemEnd(lines, i, indent);
|
|
369
|
-
const firstLine = text.slice(textStart, line.end).trim();
|
|
370
|
-
const body = text.slice(textStart, end);
|
|
371
|
-
out.push({
|
|
372
|
-
start: line.contentStart,
|
|
373
|
-
end,
|
|
374
|
-
markerStart,
|
|
375
|
-
marker: marker[2],
|
|
376
|
-
textStart,
|
|
377
|
-
checkbox,
|
|
378
|
-
checkboxStart,
|
|
379
|
-
firstLine,
|
|
380
|
-
body,
|
|
381
|
-
maskedBody: masked.slice(textStart, end),
|
|
382
|
-
line: line.line,
|
|
383
|
-
depth,
|
|
384
|
-
indent,
|
|
385
|
-
});
|
|
386
|
-
}
|
|
387
|
-
return out;
|
|
22
|
+
export function isMarkdownLine(line) {
|
|
23
|
+
return !line.code && !line.html && !line.frontMatter;
|
|
388
24
|
}
|
|
389
25
|
/**
|
|
390
|
-
*
|
|
26
|
+
* Whether a line holds a comment and nothing else.
|
|
391
27
|
*
|
|
392
|
-
*
|
|
393
|
-
*
|
|
394
|
-
*
|
|
28
|
+
* For a reader that takes the first line of a section as its value, which is
|
|
29
|
+
* what a status section is. A template's hint written above the value - `<!--
|
|
30
|
+
* proposed | accepted -->` - is not the value, and neither is a directive. A
|
|
31
|
+
* line that goes on past its comment, `<!-- hint --> Accepted`, is still read.
|
|
395
32
|
*/
|
|
396
|
-
function
|
|
397
|
-
|
|
398
|
-
let sawBlank = false;
|
|
399
|
-
for (let i = startIndex + 1; i < lines.length; i += 1) {
|
|
400
|
-
const line = lines[i];
|
|
401
|
-
if (line.blank) {
|
|
402
|
-
sawBlank = true;
|
|
403
|
-
continue;
|
|
404
|
-
}
|
|
405
|
-
if (!line.code) {
|
|
406
|
-
if (line.indent < 4 && ATX_HEADING.test(line.content))
|
|
407
|
-
break;
|
|
408
|
-
if (line.indent <= indent) {
|
|
409
|
-
if (sawBlank)
|
|
410
|
-
break;
|
|
411
|
-
if (LIST_MARKER.test(line.content))
|
|
412
|
-
break;
|
|
413
|
-
if (THEMATIC_BREAK.test(line.content))
|
|
414
|
-
break;
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
if (line.indent <= indent && sawBlank)
|
|
418
|
-
break;
|
|
419
|
-
end = line.end;
|
|
420
|
-
sawBlank = false;
|
|
421
|
-
}
|
|
422
|
-
return end;
|
|
33
|
+
export function isOnlyComment(scanned, line) {
|
|
34
|
+
return line.comment && scanned.masks.structure.slice(line.contentStart, line.end).trim().length === 0;
|
|
423
35
|
}
|
|
424
|
-
/* -------------------------------------------------------------------------- */
|
|
425
|
-
/* Tables */
|
|
426
|
-
/* -------------------------------------------------------------------------- */
|
|
427
|
-
/** `| :--- | ---: | :-: |` - the row that makes the line above it a header. */
|
|
428
|
-
const TABLE_DELIMITER = /^\|?(?:\s*:?-+:?\s*\|)+\s*:?-*:?\s*\|?$/;
|
|
429
36
|
/**
|
|
430
|
-
*
|
|
431
|
-
*
|
|
432
|
-
* Cell offsets are the point: a register kept as a table needs findings that
|
|
433
|
-
* name the cell declaring the relation, not the row and not the file.
|
|
37
|
+
* The links that cite something.
|
|
434
38
|
*
|
|
435
|
-
*
|
|
436
|
-
*
|
|
39
|
+
* An image shows a file and cites nothing: a diagram of a decision is not a
|
|
40
|
+
* dependency on it, and a missing picture is a broken page, not a broken
|
|
41
|
+
* relation. So images are not references, as they never were. A wiki embed,
|
|
42
|
+
* `![[0007-sharding]]`, is the exception - it transcludes the note it names,
|
|
43
|
+
* which is the strongest citation a wiki can write - and spec-graph has always
|
|
44
|
+
* read it as one.
|
|
437
45
|
*/
|
|
438
|
-
function
|
|
439
|
-
|
|
440
|
-
for (let i = 0; i < lines.length - 1; i += 1) {
|
|
441
|
-
const header = lines[i];
|
|
442
|
-
const delimiter = lines[i + 1];
|
|
443
|
-
if (header.code || header.blank || delimiter.code)
|
|
444
|
-
continue;
|
|
445
|
-
if (!header.content.includes('|'))
|
|
446
|
-
continue;
|
|
447
|
-
if (!TABLE_DELIMITER.test(delimiter.content.trim()))
|
|
448
|
-
continue;
|
|
449
|
-
const headers = splitRow(header, text, masked);
|
|
450
|
-
if (headers.length === 0)
|
|
451
|
-
continue;
|
|
452
|
-
const rows = [];
|
|
453
|
-
let end = delimiter.end;
|
|
454
|
-
for (let j = i + 2; j < lines.length; j += 1) {
|
|
455
|
-
const line = lines[j];
|
|
456
|
-
if (line.blank || line.code || !line.content.includes('|'))
|
|
457
|
-
break;
|
|
458
|
-
const cells = splitRow(line, text, masked);
|
|
459
|
-
if (cells.length === 0)
|
|
460
|
-
break;
|
|
461
|
-
rows.push({ cells, start: line.contentStart, end: line.end, line: line.line });
|
|
462
|
-
end = line.end;
|
|
463
|
-
}
|
|
464
|
-
out.push({ start: header.contentStart, end, headers, rows });
|
|
465
|
-
i += rows.length + 1;
|
|
466
|
-
}
|
|
467
|
-
return out;
|
|
468
|
-
}
|
|
469
|
-
/** Splits one row into cells, keeping each cell's offset in the source. */
|
|
470
|
-
function splitRow(line, text, masked) {
|
|
471
|
-
const from = line.contentStart;
|
|
472
|
-
const to = line.end;
|
|
473
|
-
const bounds = [];
|
|
474
|
-
for (let i = from; i < to; i += 1) {
|
|
475
|
-
if (masked[i] !== '|')
|
|
476
|
-
continue;
|
|
477
|
-
// A pipe escaped with a backslash is content, not a column edge.
|
|
478
|
-
if (i > from && text[i - 1] === '\\')
|
|
479
|
-
continue;
|
|
480
|
-
bounds.push(i);
|
|
481
|
-
}
|
|
482
|
-
if (bounds.length === 0)
|
|
483
|
-
return [];
|
|
484
|
-
const cells = [];
|
|
485
|
-
// A leading pipe opens the first cell; without one the row starts at `from`.
|
|
486
|
-
let cursor = bounds[0] === from ? from + 1 : from;
|
|
487
|
-
for (const bound of bounds) {
|
|
488
|
-
if (bound < cursor)
|
|
489
|
-
continue;
|
|
490
|
-
cells.push(makeCell(text, cursor, bound));
|
|
491
|
-
cursor = bound + 1;
|
|
492
|
-
}
|
|
493
|
-
// Trailing content after the last pipe is a final cell unless the row ended
|
|
494
|
-
// with a border pipe.
|
|
495
|
-
if (cursor < to && text.slice(cursor, to).trim().length > 0)
|
|
496
|
-
cells.push(makeCell(text, cursor, to));
|
|
497
|
-
return cells;
|
|
498
|
-
}
|
|
499
|
-
function makeCell(text, start, end) {
|
|
500
|
-
const raw = text.slice(start, end);
|
|
501
|
-
const leading = raw.length - raw.trimStart().length;
|
|
502
|
-
const trimmed = raw.trim();
|
|
503
|
-
return { text: trimmed, start: start + leading, end: start + leading + trimmed.length };
|
|
504
|
-
}
|
|
505
|
-
/* -------------------------------------------------------------------------- */
|
|
506
|
-
/* HTML comments */
|
|
507
|
-
/* -------------------------------------------------------------------------- */
|
|
508
|
-
function scanComments(text, index, bodyStart, code) {
|
|
509
|
-
const out = [];
|
|
510
|
-
const merged = mergeRanges(code);
|
|
511
|
-
let from = bodyStart;
|
|
512
|
-
for (;;) {
|
|
513
|
-
const open = text.indexOf('<!--', from);
|
|
514
|
-
if (open === -1)
|
|
515
|
-
break;
|
|
516
|
-
if (containsOffset(merged, open)) {
|
|
517
|
-
from = open + 4;
|
|
518
|
-
continue;
|
|
519
|
-
}
|
|
520
|
-
const close = text.indexOf('-->', open + 4);
|
|
521
|
-
if (close === -1)
|
|
522
|
-
break;
|
|
523
|
-
out.push({
|
|
524
|
-
start: open,
|
|
525
|
-
end: close + 3,
|
|
526
|
-
inner: text.slice(open + 4, close),
|
|
527
|
-
innerStart: open + 4,
|
|
528
|
-
line: index.positionAt(open).line,
|
|
529
|
-
});
|
|
530
|
-
from = close + 3;
|
|
531
|
-
}
|
|
532
|
-
return out;
|
|
533
|
-
}
|
|
534
|
-
/* -------------------------------------------------------------------------- */
|
|
535
|
-
/* Inline code */
|
|
536
|
-
/* -------------------------------------------------------------------------- */
|
|
537
|
-
function scanInlineCode(text, bodyStart, code, comments) {
|
|
538
|
-
const blocked = mergeRanges([...code, ...comments.map((c) => ({ start: c.start, end: c.end }))]);
|
|
539
|
-
const out = [];
|
|
540
|
-
let i = bodyStart;
|
|
541
|
-
while (i < text.length) {
|
|
542
|
-
if (text[i] !== '`') {
|
|
543
|
-
i += 1;
|
|
544
|
-
continue;
|
|
545
|
-
}
|
|
546
|
-
if (containsOffset(blocked, i)) {
|
|
547
|
-
i += 1;
|
|
548
|
-
continue;
|
|
549
|
-
}
|
|
550
|
-
let runStart = i;
|
|
551
|
-
while (text[i] === '`')
|
|
552
|
-
i += 1;
|
|
553
|
-
const runLength = i - runStart;
|
|
554
|
-
// An escaped backtick does not open a span.
|
|
555
|
-
if (runStart > 0 && text[runStart - 1] === '\\')
|
|
556
|
-
continue;
|
|
557
|
-
let j = i;
|
|
558
|
-
let closed = -1;
|
|
559
|
-
while (j < text.length) {
|
|
560
|
-
if (text[j] !== '`') {
|
|
561
|
-
j += 1;
|
|
562
|
-
continue;
|
|
563
|
-
}
|
|
564
|
-
const closeStart = j;
|
|
565
|
-
while (text[j] === '`')
|
|
566
|
-
j += 1;
|
|
567
|
-
if (j - closeStart === runLength && !containsOffset(blocked, closeStart)) {
|
|
568
|
-
closed = j;
|
|
569
|
-
break;
|
|
570
|
-
}
|
|
571
|
-
}
|
|
572
|
-
if (closed === -1)
|
|
573
|
-
continue;
|
|
574
|
-
out.push({ start: runStart, end: closed });
|
|
575
|
-
i = closed;
|
|
576
|
-
}
|
|
577
|
-
return out;
|
|
578
|
-
}
|
|
579
|
-
/* -------------------------------------------------------------------------- */
|
|
580
|
-
/* Masking */
|
|
581
|
-
/* -------------------------------------------------------------------------- */
|
|
582
|
-
function mergeRanges(ranges) {
|
|
583
|
-
if (ranges.length === 0)
|
|
584
|
-
return [];
|
|
585
|
-
const sorted = [...ranges].sort((a, b) => a.start - b.start || a.end - b.end);
|
|
586
|
-
const out = [{ ...sorted[0] }];
|
|
587
|
-
for (let i = 1; i < sorted.length; i += 1) {
|
|
588
|
-
const next = sorted[i];
|
|
589
|
-
const last = out[out.length - 1];
|
|
590
|
-
if (next.start <= last.end)
|
|
591
|
-
last.end = Math.max(last.end, next.end);
|
|
592
|
-
else
|
|
593
|
-
out.push({ ...next });
|
|
594
|
-
}
|
|
595
|
-
return out;
|
|
596
|
-
}
|
|
597
|
-
function containsOffset(sorted, offset) {
|
|
598
|
-
let lo = 0;
|
|
599
|
-
let hi = sorted.length - 1;
|
|
600
|
-
while (lo <= hi) {
|
|
601
|
-
const mid = (lo + hi) >> 1;
|
|
602
|
-
const range = sorted[mid];
|
|
603
|
-
if (offset < range.start)
|
|
604
|
-
hi = mid - 1;
|
|
605
|
-
else if (offset >= range.end)
|
|
606
|
-
lo = mid + 1;
|
|
607
|
-
else
|
|
608
|
-
return true;
|
|
609
|
-
}
|
|
610
|
-
return false;
|
|
46
|
+
export function referenceLinks(scanned) {
|
|
47
|
+
return scanned.links.filter((link) => !link.image || link.form === 'wiki');
|
|
611
48
|
}
|
|
612
49
|
/**
|
|
613
|
-
*
|
|
50
|
+
* The anchors a set of headings answers to.
|
|
614
51
|
*
|
|
615
|
-
*
|
|
616
|
-
*
|
|
617
|
-
*
|
|
52
|
+
* Each heading's slug, and the anchor GitHub gives it, which is the slug with
|
|
53
|
+
* `-1`, `-2` and so on for the second and later headings that slug alike. A
|
|
54
|
+
* link written against the rendered page uses the anchor; one written by hand
|
|
55
|
+
* uses the slug, and for any heading but a repeat the two are one.
|
|
618
56
|
*/
|
|
619
|
-
function
|
|
620
|
-
const
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
// byte of every document, and a `split('')`/`join('')` round trip allocates an
|
|
625
|
-
// array entry per character - on a large corpus that was the single most
|
|
626
|
-
// expensive thing the scanner did.
|
|
627
|
-
let out = '';
|
|
628
|
-
let cursor = 0;
|
|
629
|
-
for (const range of merged) {
|
|
630
|
-
const start = Math.max(range.start, cursor);
|
|
631
|
-
const end = Math.min(range.end, text.length);
|
|
632
|
-
if (end <= start)
|
|
633
|
-
continue;
|
|
634
|
-
out += text.slice(cursor, start);
|
|
635
|
-
// The character class is deliberately not Unicode-aware: a surrogate pair
|
|
636
|
-
// must become two spaces so that every later offset still lines up.
|
|
637
|
-
out += text.slice(start, end).replace(NON_TERMINATOR, ' ');
|
|
638
|
-
cursor = end;
|
|
639
|
-
}
|
|
640
|
-
return out + text.slice(cursor);
|
|
641
|
-
}
|
|
642
|
-
const NON_TERMINATOR = /[^\n\r]/g;
|
|
643
|
-
/* -------------------------------------------------------------------------- */
|
|
644
|
-
/* Links */
|
|
645
|
-
/* -------------------------------------------------------------------------- */
|
|
646
|
-
const DEFINITION = /^ {0,3}\[([^\]\n]+)\]:[ \t]*(<[^>\n]*>|\S+)/;
|
|
647
|
-
const AUTOLINK = /<((?:https?|ftp|mailto):[^>\s]+)>/g;
|
|
648
|
-
/** How far a bracket construct may run before we call it unbalanced prose. */
|
|
649
|
-
const MAX_LINK_SPAN = 4096;
|
|
650
|
-
function scanLinks(masked, text, index) {
|
|
651
|
-
const out = [];
|
|
652
|
-
const definitions = new Map();
|
|
653
|
-
const definitionRanges = [];
|
|
654
|
-
// Pass 1: reference definitions. These are the destination table the other
|
|
655
|
-
// two written-with-a-label forms are read through, which is why the whole
|
|
656
|
-
// document is swept for them before a single use is looked at: a definition
|
|
657
|
-
// is conventionally written at the foot of the file, long after the links
|
|
658
|
-
// that use it.
|
|
659
|
-
for (let line = 1; line <= index.lineCount; line += 1) {
|
|
660
|
-
const start = index.lineStart(line);
|
|
661
|
-
const content = masked.slice(start, index.lineEnd(line));
|
|
662
|
-
const match = DEFINITION.exec(content);
|
|
663
|
-
if (!match)
|
|
664
|
-
continue;
|
|
665
|
-
const label = match[1].trim().toLowerCase();
|
|
666
|
-
const rawTarget = match[2];
|
|
667
|
-
const targetStart = start + content.indexOf(rawTarget, match[1].length);
|
|
668
|
-
const target = unwrapAngle(rawTarget);
|
|
669
|
-
// First definition wins, as in CommonMark.
|
|
670
|
-
if (!definitions.has(label))
|
|
671
|
-
definitions.set(label, { target, targetStart });
|
|
672
|
-
definitionRanges.push({ start, end: start + match[0].length });
|
|
673
|
-
out.push({
|
|
674
|
-
text: match[1].trim(),
|
|
675
|
-
target,
|
|
676
|
-
start,
|
|
677
|
-
end: start + match[0].length,
|
|
678
|
-
targetStart,
|
|
679
|
-
label: match[1].trim(),
|
|
680
|
-
form: 'definition',
|
|
681
|
-
line,
|
|
682
|
-
});
|
|
683
|
-
}
|
|
684
|
-
// Pass 2: autolinks.
|
|
685
|
-
AUTOLINK.lastIndex = 0;
|
|
686
|
-
for (let m = AUTOLINK.exec(masked); m !== null; m = AUTOLINK.exec(masked)) {
|
|
687
|
-
const start = m.index;
|
|
688
|
-
out.push({
|
|
689
|
-
text: '',
|
|
690
|
-
target: m[1],
|
|
691
|
-
start,
|
|
692
|
-
end: start + m[0].length,
|
|
693
|
-
targetStart: start + 1,
|
|
694
|
-
label: null,
|
|
695
|
-
form: 'autolink',
|
|
696
|
-
line: index.positionAt(start).line,
|
|
697
|
-
});
|
|
57
|
+
export function anchorsOf(headings) {
|
|
58
|
+
const out = new Set();
|
|
59
|
+
for (const heading of headings) {
|
|
60
|
+
out.add(heading.slug);
|
|
61
|
+
out.add(heading.anchor);
|
|
698
62
|
}
|
|
699
|
-
// Pass 3: bracket constructs, left to right.
|
|
700
|
-
const definedAt = mergeRanges(definitionRanges);
|
|
701
|
-
let i = 0;
|
|
702
|
-
while (i < masked.length) {
|
|
703
|
-
const ch = masked[i];
|
|
704
|
-
if (ch !== '[') {
|
|
705
|
-
i += 1;
|
|
706
|
-
continue;
|
|
707
|
-
}
|
|
708
|
-
// The label of a definition is not itself a shortcut link to anything.
|
|
709
|
-
if (containsOffset(definedAt, i)) {
|
|
710
|
-
i += 1;
|
|
711
|
-
continue;
|
|
712
|
-
}
|
|
713
|
-
const isImage = i > 0 && masked[i - 1] === '!';
|
|
714
|
-
if (masked[i + 1] === '[') {
|
|
715
|
-
const close = masked.indexOf(']]', i + 2);
|
|
716
|
-
if (close !== -1 && close - i <= MAX_LINK_SPAN && masked.slice(i + 2, close).indexOf('\n') === -1) {
|
|
717
|
-
const target = text.slice(i + 2, close).trim();
|
|
718
|
-
if (target.length > 0) {
|
|
719
|
-
out.push({
|
|
720
|
-
text: target,
|
|
721
|
-
target: wikiTarget(target),
|
|
722
|
-
start: i,
|
|
723
|
-
end: close + 2,
|
|
724
|
-
targetStart: i + 2,
|
|
725
|
-
label: null,
|
|
726
|
-
form: 'wiki',
|
|
727
|
-
line: index.positionAt(i).line,
|
|
728
|
-
});
|
|
729
|
-
i = close + 2;
|
|
730
|
-
continue;
|
|
731
|
-
}
|
|
732
|
-
}
|
|
733
|
-
}
|
|
734
|
-
const labelEnd = matchBracket(masked, i);
|
|
735
|
-
if (labelEnd === -1) {
|
|
736
|
-
i += 1;
|
|
737
|
-
continue;
|
|
738
|
-
}
|
|
739
|
-
const label = text.slice(i + 1, labelEnd);
|
|
740
|
-
const after = masked[labelEnd + 1];
|
|
741
|
-
if (after === '(') {
|
|
742
|
-
const dest = readDestination(masked, labelEnd + 2);
|
|
743
|
-
if (dest) {
|
|
744
|
-
if (!isImage) {
|
|
745
|
-
out.push({
|
|
746
|
-
text: label.trim(),
|
|
747
|
-
target: unwrapAngle(text.slice(dest.start, dest.end).trim()),
|
|
748
|
-
start: isImage ? i - 1 : i,
|
|
749
|
-
end: dest.close + 1,
|
|
750
|
-
targetStart: dest.start,
|
|
751
|
-
label: null,
|
|
752
|
-
form: 'inline',
|
|
753
|
-
line: index.positionAt(i).line,
|
|
754
|
-
});
|
|
755
|
-
}
|
|
756
|
-
i = dest.close + 1;
|
|
757
|
-
continue;
|
|
758
|
-
}
|
|
759
|
-
}
|
|
760
|
-
else if (after === '[') {
|
|
761
|
-
const refEnd = matchBracket(masked, labelEnd + 1);
|
|
762
|
-
if (refEnd !== -1) {
|
|
763
|
-
const ref = text.slice(labelEnd + 2, refEnd).trim();
|
|
764
|
-
const written = ref.length > 0 ? ref : label.trim();
|
|
765
|
-
const defined = definitions.get(written.toLowerCase());
|
|
766
|
-
// A label with no definition is not a link. Every renderer prints
|
|
767
|
-
// `[design][one]` verbatim when nothing says what `one` is, so reading
|
|
768
|
-
// it as a citation invents a reference the author never made - and
|
|
769
|
-
// then reports it as broken.
|
|
770
|
-
if (!isImage && defined) {
|
|
771
|
-
out.push({
|
|
772
|
-
text: label.trim(),
|
|
773
|
-
target: defined.target,
|
|
774
|
-
start: i,
|
|
775
|
-
end: refEnd + 1,
|
|
776
|
-
targetStart: defined.targetStart,
|
|
777
|
-
label: written,
|
|
778
|
-
form: 'reference',
|
|
779
|
-
line: index.positionAt(i).line,
|
|
780
|
-
});
|
|
781
|
-
}
|
|
782
|
-
i = refEnd + 1;
|
|
783
|
-
continue;
|
|
784
|
-
}
|
|
785
|
-
}
|
|
786
|
-
else if (!isImage) {
|
|
787
|
-
const written = label.trim();
|
|
788
|
-
const defined = definitions.get(written.toLowerCase());
|
|
789
|
-
if (defined) {
|
|
790
|
-
out.push({
|
|
791
|
-
text: written,
|
|
792
|
-
target: defined.target,
|
|
793
|
-
start: i,
|
|
794
|
-
end: labelEnd + 1,
|
|
795
|
-
targetStart: defined.targetStart,
|
|
796
|
-
label: written,
|
|
797
|
-
form: 'shortcut',
|
|
798
|
-
line: index.positionAt(i).line,
|
|
799
|
-
});
|
|
800
|
-
i = labelEnd + 1;
|
|
801
|
-
continue;
|
|
802
|
-
}
|
|
803
|
-
}
|
|
804
|
-
i = labelEnd + 1;
|
|
805
|
-
}
|
|
806
|
-
out.sort((a, b) => a.start - b.start);
|
|
807
63
|
return out;
|
|
808
64
|
}
|
|
809
|
-
/** Finds the `]` matching the `[` at `open`, honouring nesting and escapes. */
|
|
810
|
-
function matchBracket(text, open) {
|
|
811
|
-
let depth = 0;
|
|
812
|
-
const limit = Math.min(text.length, open + MAX_LINK_SPAN);
|
|
813
|
-
for (let i = open; i < limit; i += 1) {
|
|
814
|
-
const ch = text[i];
|
|
815
|
-
if (ch === '\\') {
|
|
816
|
-
i += 1;
|
|
817
|
-
continue;
|
|
818
|
-
}
|
|
819
|
-
if (ch === '\n' && text[i + 1] === '\n')
|
|
820
|
-
return -1;
|
|
821
|
-
if (ch === '[')
|
|
822
|
-
depth += 1;
|
|
823
|
-
else if (ch === ']') {
|
|
824
|
-
depth -= 1;
|
|
825
|
-
if (depth === 0)
|
|
826
|
-
return i;
|
|
827
|
-
}
|
|
828
|
-
}
|
|
829
|
-
return -1;
|
|
830
|
-
}
|
|
831
|
-
/** Reads a link destination starting just after `(`. */
|
|
832
|
-
function readDestination(text, from) {
|
|
833
|
-
let i = from;
|
|
834
|
-
const limit = Math.min(text.length, from + MAX_LINK_SPAN);
|
|
835
|
-
while (i < limit && (text[i] === ' ' || text[i] === '\t' || text[i] === '\n'))
|
|
836
|
-
i += 1;
|
|
837
|
-
const start = i;
|
|
838
|
-
if (text[i] === '<') {
|
|
839
|
-
const close = text.indexOf('>', i + 1);
|
|
840
|
-
if (close === -1 || close > limit)
|
|
841
|
-
return null;
|
|
842
|
-
const paren = text.indexOf(')', close);
|
|
843
|
-
if (paren === -1 || paren > limit)
|
|
844
|
-
return null;
|
|
845
|
-
return { start, end: close + 1, close: paren };
|
|
846
|
-
}
|
|
847
|
-
let depth = 0;
|
|
848
|
-
while (i < limit) {
|
|
849
|
-
const ch = text[i];
|
|
850
|
-
if (ch === '\\') {
|
|
851
|
-
i += 2;
|
|
852
|
-
continue;
|
|
853
|
-
}
|
|
854
|
-
if (ch === '(')
|
|
855
|
-
depth += 1;
|
|
856
|
-
else if (ch === ')') {
|
|
857
|
-
if (depth === 0)
|
|
858
|
-
break;
|
|
859
|
-
depth -= 1;
|
|
860
|
-
}
|
|
861
|
-
else if (ch === ' ' || ch === '\t' || ch === '\n')
|
|
862
|
-
break;
|
|
863
|
-
i += 1;
|
|
864
|
-
}
|
|
865
|
-
const end = i;
|
|
866
|
-
// Skip an optional title, then require the closing paren.
|
|
867
|
-
let j = i;
|
|
868
|
-
while (j < limit && text[j] !== ')') {
|
|
869
|
-
if (text[j] === '\n' && text[j + 1] === '\n')
|
|
870
|
-
return null;
|
|
871
|
-
j += 1;
|
|
872
|
-
}
|
|
873
|
-
if (j >= limit || text[j] !== ')')
|
|
874
|
-
return null;
|
|
875
|
-
if (end === start)
|
|
876
|
-
return null;
|
|
877
|
-
return { start, end, close: j };
|
|
878
|
-
}
|
|
879
|
-
function unwrapAngle(value) {
|
|
880
|
-
return value.startsWith('<') && value.endsWith('>') ? value.slice(1, -1) : value;
|
|
881
|
-
}
|
|
882
|
-
/** `[[0007-thing|Display]]` and `[[0007-thing]]` both target `0007-thing`. */
|
|
883
|
-
function wikiTarget(raw) {
|
|
884
|
-
const pipe = raw.indexOf('|');
|
|
885
|
-
return (pipe === -1 ? raw : raw.slice(0, pipe)).trim();
|
|
886
|
-
}
|
|
887
65
|
//# sourceMappingURL=markdown.js.map
|