@effected/markdown 0.2.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/Frontmatter.js +272 -0
- package/FrontmatterResolver.js +273 -0
- package/JsonFrontmatter.js +46 -0
- package/LICENSE +21 -0
- package/Markdown.js +251 -0
- package/MarkdownDiagnostic.js +85 -0
- package/MarkdownDocument.js +248 -0
- package/MarkdownEdit.js +52 -0
- package/MarkdownFormat.js +380 -0
- package/MarkdownNode.js +641 -0
- package/MarkdownVisitor.js +88 -0
- package/Mdast.js +384 -0
- package/README.md +255 -0
- package/TomlFrontmatter.js +44 -0
- package/YamlFrontmatter.js +45 -0
- package/index.d.ts +2190 -0
- package/index.js +15 -0
- package/internal/blockParser.js +419 -0
- package/internal/blockRegistry.js +90 -0
- package/internal/blockTypes.js +27 -0
- package/internal/blocks/atxHeading.js +54 -0
- package/internal/blocks/blockquote.js +40 -0
- package/internal/blocks/code.js +90 -0
- package/internal/blocks/document.js +17 -0
- package/internal/blocks/fencedCode.js +26 -0
- package/internal/blocks/footnoteDefinition.js +84 -0
- package/internal/blocks/frontmatter.js +56 -0
- package/internal/blocks/htmlBlock.js +72 -0
- package/internal/blocks/indentedCode.js +16 -0
- package/internal/blocks/linkReferenceDefinition.js +72 -0
- package/internal/blocks/list.js +159 -0
- package/internal/blocks/paragraph.js +27 -0
- package/internal/blocks/setextHeading.js +24 -0
- package/internal/blocks/table.js +378 -0
- package/internal/blocks/taskListItem.js +36 -0
- package/internal/blocks/thematicBreak.js +35 -0
- package/internal/carriers.js +42 -0
- package/internal/entities.js +26 -0
- package/internal/entityMap.js +7 -0
- package/internal/htmlTags.js +18 -0
- package/internal/inlineNode.js +54 -0
- package/internal/inlineParser.js +403 -0
- package/internal/inlineRegistry.js +68 -0
- package/internal/inlines/autolink.js +36 -0
- package/internal/inlines/autolinkLiteral.js +374 -0
- package/internal/inlines/codeSpan.js +32 -0
- package/internal/inlines/emphasis.js +79 -0
- package/internal/inlines/entity.js +21 -0
- package/internal/inlines/escape.js +34 -0
- package/internal/inlines/footnoteReference.js +63 -0
- package/internal/inlines/lineBreak.js +25 -0
- package/internal/inlines/link.js +212 -0
- package/internal/inlines/rawHtml.js +27 -0
- package/internal/inlines/strikethrough.js +81 -0
- package/internal/inlines/text.js +22 -0
- package/internal/lineIndex.js +76 -0
- package/internal/patterns.js +26 -0
- package/internal/preprocess.js +58 -0
- package/internal/rawInline.js +57 -0
- package/internal/references.js +160 -0
- package/internal/segments.js +36 -0
- package/internal/stringify.js +519 -0
- package/internal/unescape.js +18 -0
- package/package.json +61 -0
- package/tsdoc-metadata.json +11 -0
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
import { appendChild, childrenOf, insertAfter, makeInlineNode, unlink } from "../inlineNode.js";
|
|
2
|
+
|
|
3
|
+
//#region src/internal/inlines/autolinkLiteral.ts
|
|
4
|
+
const C_LOWER_W = 119;
|
|
5
|
+
const C_COLON = 58;
|
|
6
|
+
/** `cmark_isspace`: ASCII only, deliberately — upstream's URL scan uses it. */
|
|
7
|
+
const reAsciiSpace = /^[ \t\n\v\f\r]$/;
|
|
8
|
+
const reAlnum = /^[0-9A-Za-z]$/;
|
|
9
|
+
const reAlpha = /^[A-Za-z]$/;
|
|
10
|
+
const reUnicodeSpace = /^[\t\n\f\r \u00a0\u1680\u2000-\u200a\u202f\u205f\u3000]$/;
|
|
11
|
+
const reAsciiPunctuation = /^[!-/:-@[-`{-~]$/;
|
|
12
|
+
const reUnicodePunctuation = /^\p{P}$/u;
|
|
13
|
+
/** The schemes `sd_autolink_issafe` whitelists, in its order. */
|
|
14
|
+
const SAFE_SCHEMES = [
|
|
15
|
+
"http://",
|
|
16
|
+
"https://",
|
|
17
|
+
"ftp://"
|
|
18
|
+
];
|
|
19
|
+
/** Whether the code point at `index` may appear in a host name. */
|
|
20
|
+
const isValidHostChar = (subject, index) => {
|
|
21
|
+
const code = subject.codePointAt(index);
|
|
22
|
+
if (code === void 0) return false;
|
|
23
|
+
const char = String.fromCodePoint(code);
|
|
24
|
+
if (reUnicodeSpace.test(char)) return false;
|
|
25
|
+
return !(code < 128 ? reAsciiPunctuation.test(char) : reUnicodePunctuation.test(char));
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* `autolink_delim`: pull the link's end back off trailing punctuation.
|
|
29
|
+
*
|
|
30
|
+
* Two rules beyond "drop the trailing mark". Parens are BALANCED rather than
|
|
31
|
+
* simply stripped, so `www.a.com/q_(bar)` keeps its group while `(www.a.com/q)`
|
|
32
|
+
* gives its closer back to the surrounding text. And a trailing `;` is
|
|
33
|
+
* examined for an entity reference: `…&hl;` cuts at the `&`, because the
|
|
34
|
+
* writer will re-escape that run and the link must not swallow it.
|
|
35
|
+
*/
|
|
36
|
+
const autolinkDelim = (subject, base, end) => {
|
|
37
|
+
let linkEnd = end;
|
|
38
|
+
let opening = 0;
|
|
39
|
+
let closing = 0;
|
|
40
|
+
for (let index = 0; index < linkEnd; index += 1) {
|
|
41
|
+
const char = subject.charAt(base + index);
|
|
42
|
+
if (char === "<") {
|
|
43
|
+
linkEnd = index;
|
|
44
|
+
break;
|
|
45
|
+
}
|
|
46
|
+
if (char === "(") opening += 1;
|
|
47
|
+
else if (char === ")") closing += 1;
|
|
48
|
+
}
|
|
49
|
+
while (linkEnd > 0) {
|
|
50
|
+
const char = subject.charAt(base + linkEnd - 1);
|
|
51
|
+
if (char === ")") {
|
|
52
|
+
if (closing <= opening) return linkEnd;
|
|
53
|
+
closing -= 1;
|
|
54
|
+
linkEnd -= 1;
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
if ("?!.,:*_~'\"".includes(char)) {
|
|
58
|
+
linkEnd -= 1;
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
if (char === ";") {
|
|
62
|
+
if (linkEnd < 2) {
|
|
63
|
+
linkEnd -= 1;
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
let newEnd = linkEnd - 2;
|
|
67
|
+
while (newEnd > 0 && reAlpha.test(subject.charAt(base + newEnd))) newEnd -= 1;
|
|
68
|
+
if (newEnd < linkEnd - 2 && subject.charAt(base + newEnd) === "&") linkEnd = newEnd;
|
|
69
|
+
else linkEnd -= 1;
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
return linkEnd;
|
|
73
|
+
}
|
|
74
|
+
return linkEnd;
|
|
75
|
+
};
|
|
76
|
+
/**
|
|
77
|
+
* `check_domain`: how much of `subject` from `base` is a plausible domain.
|
|
78
|
+
*
|
|
79
|
+
* The underscore rule is the interesting one and it is a HOST-name rule, not
|
|
80
|
+
* a domain-name rule: an `_` in either of the last two dot-separated segments
|
|
81
|
+
* disqualifies the whole match (`www.xxx._yyy.zzz` is literal text), while one
|
|
82
|
+
* further left is fine (`www._xxx.yyy.zzz` links). The `np > 10` escape hatch
|
|
83
|
+
* is upstream's fix for GHSA-29g3-96g3-jg6c — a very long segmented run stops
|
|
84
|
+
* being re-examined rather than going quadratic.
|
|
85
|
+
*/
|
|
86
|
+
const checkDomain = (subject, base, size, allowShort) => {
|
|
87
|
+
let index = 1;
|
|
88
|
+
let dots = 0;
|
|
89
|
+
let underscoresPenultimate = 0;
|
|
90
|
+
let underscoresLast = 0;
|
|
91
|
+
for (; index < size - 1; index += 1) {
|
|
92
|
+
if (subject.charAt(base + index) === "\\" && index < size - 2) index += 1;
|
|
93
|
+
const char = subject.charAt(base + index);
|
|
94
|
+
if (char === "_") underscoresLast += 1;
|
|
95
|
+
else if (char === ".") {
|
|
96
|
+
underscoresPenultimate = underscoresLast;
|
|
97
|
+
underscoresLast = 0;
|
|
98
|
+
dots += 1;
|
|
99
|
+
} else if (!isValidHostChar(subject, base + index) && char !== "-") break;
|
|
100
|
+
}
|
|
101
|
+
if ((underscoresPenultimate > 0 || underscoresLast > 0) && dots <= 10) return 0;
|
|
102
|
+
if (allowShort) return index;
|
|
103
|
+
return dots > 0 ? index : 0;
|
|
104
|
+
};
|
|
105
|
+
/** `sd_autolink_issafe`: does the run at `start` open with a whitelisted scheme. */
|
|
106
|
+
const isSafeScheme = (subject, start, length) => SAFE_SCHEMES.some((scheme) => length > scheme.length && subject.slice(start, start + scheme.length).toLowerCase() === scheme && isValidHostChar(subject, start + scheme.length));
|
|
107
|
+
/** Extend a matched prefix to the next space or `<`, as both matchers do. */
|
|
108
|
+
const extendToBoundary = (subject, base, from, size) => {
|
|
109
|
+
let linkEnd = from;
|
|
110
|
+
while (linkEnd < size) {
|
|
111
|
+
const char = subject.charAt(base + linkEnd);
|
|
112
|
+
if (reAsciiSpace.test(char) || char === "<") break;
|
|
113
|
+
linkEnd += 1;
|
|
114
|
+
}
|
|
115
|
+
return linkEnd;
|
|
116
|
+
};
|
|
117
|
+
/** Append a literal-autolink `link` node spanning `[start, end)`. */
|
|
118
|
+
const appendLiteralLink = (scanner, start, end, url) => {
|
|
119
|
+
const text = scanner.subject.slice(start, end);
|
|
120
|
+
const node = makeInlineNode("link", start, end);
|
|
121
|
+
node.data.url = url;
|
|
122
|
+
appendChild(node, makeInlineNode("text", start, end, text));
|
|
123
|
+
scanner.append(node);
|
|
124
|
+
scanner.pos = end;
|
|
125
|
+
};
|
|
126
|
+
/**
|
|
127
|
+
* `www_match`: a `www.` host with no scheme, which links to `http://` + itself.
|
|
128
|
+
*
|
|
129
|
+
* The preceding character is the gate — start of line, whitespace, or one of
|
|
130
|
+
* `*`, `_`, `~`, `(` — which is what keeps `xwww.a.com` from linking.
|
|
131
|
+
*/
|
|
132
|
+
const wwwAutolinkConstruct = {
|
|
133
|
+
name: "wwwAutolink",
|
|
134
|
+
triggers: [C_LOWER_W],
|
|
135
|
+
parse: (scanner) => {
|
|
136
|
+
if (scanner.brackets !== void 0) return false;
|
|
137
|
+
const start = scanner.pos;
|
|
138
|
+
if (start > 0) {
|
|
139
|
+
const before = scanner.subject.charAt(start - 1);
|
|
140
|
+
if (!"*_~(".includes(before) && !reAsciiSpace.test(before)) return false;
|
|
141
|
+
}
|
|
142
|
+
const size = scanner.subject.length - start;
|
|
143
|
+
if (size < 4 || scanner.subject.slice(start, start + 4) !== "www.") return false;
|
|
144
|
+
const domain = checkDomain(scanner.subject, start, size, false);
|
|
145
|
+
if (domain === 0) return false;
|
|
146
|
+
const linkEnd = autolinkDelim(scanner.subject, start, extendToBoundary(scanner.subject, start, domain, size));
|
|
147
|
+
if (linkEnd === 0) return false;
|
|
148
|
+
appendLiteralLink(scanner, start, start + linkEnd, `http://${scanner.subject.slice(start, start + linkEnd)}`);
|
|
149
|
+
return true;
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
/**
|
|
153
|
+
* `url_match`: a bare `http://`, `https://` or `ftp://` URL.
|
|
154
|
+
*
|
|
155
|
+
* The trigger is the `:`, so the scheme itself is already behind the cursor
|
|
156
|
+
* and already emitted as text. Upstream calls `cmark_node_unput` to take it
|
|
157
|
+
* back; `scanner.unputText` is that, and its refusal is a real bail-out —
|
|
158
|
+
* when the scheme's characters did not come from plain text nodes (a decoded
|
|
159
|
+
* entity, say) the run is not the source text it appears to be and no link
|
|
160
|
+
* forms.
|
|
161
|
+
*/
|
|
162
|
+
const urlAutolinkConstruct = {
|
|
163
|
+
name: "urlAutolink",
|
|
164
|
+
triggers: [C_COLON],
|
|
165
|
+
parse: (scanner) => {
|
|
166
|
+
if (scanner.brackets !== void 0) return false;
|
|
167
|
+
const colon = scanner.pos;
|
|
168
|
+
const { subject } = scanner;
|
|
169
|
+
const size = subject.length - colon;
|
|
170
|
+
if (size < 4 || subject.charAt(colon + 1) !== "/" || subject.charAt(colon + 2) !== "/") return false;
|
|
171
|
+
let rewind = 0;
|
|
172
|
+
while (rewind < colon && reAlpha.test(subject.charAt(colon - rewind - 1))) rewind += 1;
|
|
173
|
+
if (!isSafeScheme(subject, colon - rewind, size + rewind)) return false;
|
|
174
|
+
const domain = checkDomain(subject, colon + 3, size - 3, true);
|
|
175
|
+
if (domain === 0) return false;
|
|
176
|
+
const linkEnd = autolinkDelim(subject, colon, extendToBoundary(subject, colon, 3 + domain, size));
|
|
177
|
+
if (linkEnd === 0) return false;
|
|
178
|
+
if (!scanner.unputText(rewind)) return false;
|
|
179
|
+
const start = colon - rewind;
|
|
180
|
+
const end = colon + linkEnd;
|
|
181
|
+
appendLiteralLink(scanner, start, end, subject.slice(start, end));
|
|
182
|
+
return true;
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
/**
|
|
186
|
+
* `validate_protocol`: is `protocol` the run immediately left of the address,
|
|
187
|
+
* and is it itself preceded by a non-alphanumeric?
|
|
188
|
+
*
|
|
189
|
+
* The second half is why `mmmmailto:foo@bar.baz` links only `foo@bar.baz`.
|
|
190
|
+
*/
|
|
191
|
+
const validateProtocol = (protocol, data, at, rewind, maxRewind) => {
|
|
192
|
+
const length = protocol.length;
|
|
193
|
+
if (length > maxRewind - rewind) return false;
|
|
194
|
+
if (data.slice(at - rewind - length, at - rewind) !== protocol) return false;
|
|
195
|
+
if (length === maxRewind - rewind) return true;
|
|
196
|
+
return !reAlnum.test(data.charAt(at - rewind - length - 1));
|
|
197
|
+
};
|
|
198
|
+
/**
|
|
199
|
+
* `postprocess_text`: every email address in one merged run of text.
|
|
200
|
+
*
|
|
201
|
+
* The shape is upstream's, `goto found_at` included — when the forward scan
|
|
202
|
+
* runs into a SECOND `@` the candidate is re-anchored on it rather than
|
|
203
|
+
* abandoned, which is what makes `a@b@c.d` link `b@c.d`. Neither `auto_mailto`
|
|
204
|
+
* nor `np` is reset on that re-entry, because upstream's `goto` jumps past
|
|
205
|
+
* their initializers.
|
|
206
|
+
*/
|
|
207
|
+
const scanEmails = (data) => {
|
|
208
|
+
const matches = [];
|
|
209
|
+
let start = 0;
|
|
210
|
+
let offset = 0;
|
|
211
|
+
let remaining = data.length;
|
|
212
|
+
while (offset < remaining) {
|
|
213
|
+
const at = data.indexOf("@", start + offset);
|
|
214
|
+
if (at === -1) break;
|
|
215
|
+
let maxRewind = at - (start + offset);
|
|
216
|
+
let autoMailto = true;
|
|
217
|
+
let isXmpp = false;
|
|
218
|
+
let rewind = 0;
|
|
219
|
+
let linkEnd = 1;
|
|
220
|
+
let dots = 0;
|
|
221
|
+
let reanchored = true;
|
|
222
|
+
let abandoned = false;
|
|
223
|
+
while (reanchored) {
|
|
224
|
+
reanchored = false;
|
|
225
|
+
for (rewind = 0; rewind < maxRewind; rewind += 1) {
|
|
226
|
+
const char = data.charAt(start + offset + maxRewind - rewind - 1);
|
|
227
|
+
if (reAlnum.test(char) || ".+-_".includes(char)) continue;
|
|
228
|
+
if (char === ":") {
|
|
229
|
+
if (validateProtocol("mailto:", data, start + offset + maxRewind, rewind, maxRewind)) {
|
|
230
|
+
autoMailto = false;
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
if (validateProtocol("xmpp:", data, start + offset + maxRewind, rewind, maxRewind)) {
|
|
234
|
+
autoMailto = false;
|
|
235
|
+
isXmpp = true;
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
break;
|
|
240
|
+
}
|
|
241
|
+
if (rewind === 0) {
|
|
242
|
+
offset += maxRewind + 1;
|
|
243
|
+
abandoned = true;
|
|
244
|
+
break;
|
|
245
|
+
}
|
|
246
|
+
const bound = remaining - offset - maxRewind;
|
|
247
|
+
for (linkEnd = 1; linkEnd < bound; linkEnd += 1) {
|
|
248
|
+
const char = data.charAt(start + offset + maxRewind + linkEnd);
|
|
249
|
+
if (reAlnum.test(char)) continue;
|
|
250
|
+
if (char === "@") {
|
|
251
|
+
offset += maxRewind + 1;
|
|
252
|
+
maxRewind = linkEnd - 1;
|
|
253
|
+
reanchored = true;
|
|
254
|
+
break;
|
|
255
|
+
}
|
|
256
|
+
if (char === "." && linkEnd < bound - 1 && reAlnum.test(data.charAt(start + offset + maxRewind + linkEnd + 1))) {
|
|
257
|
+
dots += 1;
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
if (char === "/" && isXmpp) continue;
|
|
261
|
+
if (char !== "-" && char !== "_") break;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
if (abandoned) continue;
|
|
265
|
+
const last = data.charAt(start + offset + maxRewind + linkEnd - 1);
|
|
266
|
+
if (linkEnd < 2 || dots === 0 || !reAlpha.test(last) && last !== ".") {
|
|
267
|
+
offset += maxRewind + linkEnd;
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
linkEnd = autolinkDelim(data, start + offset + maxRewind, linkEnd);
|
|
271
|
+
if (linkEnd === 0) {
|
|
272
|
+
offset += maxRewind + 1;
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
const from = start + offset + maxRewind - rewind;
|
|
276
|
+
const to = start + offset + maxRewind + linkEnd;
|
|
277
|
+
const text = data.slice(from, to);
|
|
278
|
+
matches.push({
|
|
279
|
+
from,
|
|
280
|
+
to,
|
|
281
|
+
url: autoMailto ? `mailto:${text}` : text
|
|
282
|
+
});
|
|
283
|
+
start += offset + maxRewind + linkEnd;
|
|
284
|
+
remaining -= offset + maxRewind + linkEnd;
|
|
285
|
+
offset = 0;
|
|
286
|
+
}
|
|
287
|
+
return matches;
|
|
288
|
+
};
|
|
289
|
+
/** Replace a run of adjacent text nodes with the same text plus its email links. */
|
|
290
|
+
const linkifyRun = (pieces) => {
|
|
291
|
+
const merged = pieces.map((piece) => piece.node.value).join("");
|
|
292
|
+
const matches = scanEmails(merged);
|
|
293
|
+
if (matches.length === 0) return;
|
|
294
|
+
/**
|
|
295
|
+
* The local content index a merged-run index came from.
|
|
296
|
+
*
|
|
297
|
+
* Exact whenever the contributing node's value is its source characters,
|
|
298
|
+
* which is every node the text fallback produced. A node whose value was
|
|
299
|
+
* DECODED — an entity, a backslash escape — is shorter than the source it
|
|
300
|
+
* spans, so an index inside it clamps to that node's own range rather than
|
|
301
|
+
* running past its end. An email address holds neither, so this is a
|
|
302
|
+
* bound, not a rounding.
|
|
303
|
+
*/
|
|
304
|
+
const localAt = (index) => {
|
|
305
|
+
for (const piece of pieces) {
|
|
306
|
+
const within = index - piece.valueStart;
|
|
307
|
+
if (within <= piece.node.value.length) return piece.node.start + Math.min(within, piece.node.end - piece.node.start);
|
|
308
|
+
}
|
|
309
|
+
const last = pieces[pieces.length - 1];
|
|
310
|
+
return last === void 0 ? 0 : last.node.end;
|
|
311
|
+
};
|
|
312
|
+
const replacements = [];
|
|
313
|
+
let cursor = 0;
|
|
314
|
+
const pushText = (from, to) => {
|
|
315
|
+
if (to <= from) return;
|
|
316
|
+
replacements.push(makeInlineNode("text", localAt(from), localAt(to), merged.slice(from, to)));
|
|
317
|
+
};
|
|
318
|
+
for (const match of matches) {
|
|
319
|
+
pushText(cursor, match.from);
|
|
320
|
+
const start = localAt(match.from);
|
|
321
|
+
const end = localAt(match.to);
|
|
322
|
+
const link = makeInlineNode("link", start, end);
|
|
323
|
+
link.data.url = match.url;
|
|
324
|
+
appendChild(link, makeInlineNode("text", start, end, merged.slice(match.from, match.to)));
|
|
325
|
+
replacements.push(link);
|
|
326
|
+
cursor = match.to;
|
|
327
|
+
}
|
|
328
|
+
pushText(cursor, merged.length);
|
|
329
|
+
const lastPiece = pieces[pieces.length - 1];
|
|
330
|
+
if (lastPiece === void 0) return;
|
|
331
|
+
let tail = lastPiece.node;
|
|
332
|
+
for (const replacement of replacements) {
|
|
333
|
+
insertAfter(tail, replacement);
|
|
334
|
+
tail = replacement;
|
|
335
|
+
}
|
|
336
|
+
for (const piece of pieces) unlink(piece.node);
|
|
337
|
+
};
|
|
338
|
+
/**
|
|
339
|
+
* Turn every bare email address in the finished list into a link.
|
|
340
|
+
*
|
|
341
|
+
* Iterative on purpose: this walks the tree emphasis and strikethrough just
|
|
342
|
+
* built, whose depth the input controls, and the parser's depth cap does not
|
|
343
|
+
* apply until materialization. A tree walk has no reason to recurse.
|
|
344
|
+
*/
|
|
345
|
+
const linkifyEmails = (root) => {
|
|
346
|
+
const pending = [root];
|
|
347
|
+
while (pending.length > 0) {
|
|
348
|
+
const container = pending.pop();
|
|
349
|
+
if (container === void 0) break;
|
|
350
|
+
let pieces = [];
|
|
351
|
+
let valueLength = 0;
|
|
352
|
+
const flush = () => {
|
|
353
|
+
if (pieces.length > 0) linkifyRun(pieces);
|
|
354
|
+
pieces = [];
|
|
355
|
+
valueLength = 0;
|
|
356
|
+
};
|
|
357
|
+
for (const child of childrenOf(container)) {
|
|
358
|
+
if (child.type === "text") {
|
|
359
|
+
pieces.push({
|
|
360
|
+
node: child,
|
|
361
|
+
valueStart: valueLength
|
|
362
|
+
});
|
|
363
|
+
valueLength += child.value.length;
|
|
364
|
+
continue;
|
|
365
|
+
}
|
|
366
|
+
flush();
|
|
367
|
+
if (child.type !== "link" && child.type !== "linkReference") pending.push(child);
|
|
368
|
+
}
|
|
369
|
+
flush();
|
|
370
|
+
}
|
|
371
|
+
};
|
|
372
|
+
|
|
373
|
+
//#endregion
|
|
374
|
+
export { linkifyEmails, urlAutolinkConstruct, wwwAutolinkConstruct };
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { makeInlineNode } from "../inlineNode.js";
|
|
2
|
+
|
|
3
|
+
//#region src/internal/inlines/codeSpan.ts
|
|
4
|
+
const C_BACKTICK = 96;
|
|
5
|
+
const reTicksHere = /^`+/;
|
|
6
|
+
const reNewline = /\n/gm;
|
|
7
|
+
const reNonSpace = /[^ ]/;
|
|
8
|
+
/** A code span. */
|
|
9
|
+
const codeSpanConstruct = {
|
|
10
|
+
name: "codeSpan",
|
|
11
|
+
triggers: [C_BACKTICK],
|
|
12
|
+
parse: (scanner) => {
|
|
13
|
+
const from = scanner.pos;
|
|
14
|
+
const ticks = scanner.match(reTicksHere);
|
|
15
|
+
if (ticks === void 0) return false;
|
|
16
|
+
const afterOpenTicks = scanner.pos;
|
|
17
|
+
const closing = scanner.closingBacktickRun(afterOpenTicks, ticks.length);
|
|
18
|
+
if (closing !== void 0) {
|
|
19
|
+
const contents = scanner.subject.slice(afterOpenTicks, closing).replace(reNewline, " ");
|
|
20
|
+
const stripped = contents.length > 0 && reNonSpace.test(contents) && contents.startsWith(" ") && contents.endsWith(" ") ? contents.slice(1, -1) : contents;
|
|
21
|
+
scanner.pos = closing + ticks.length;
|
|
22
|
+
scanner.append(makeInlineNode("inlineCode", from, scanner.pos, stripped));
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
scanner.pos = afterOpenTicks;
|
|
26
|
+
scanner.appendText(ticks, from, scanner.pos);
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
//#endregion
|
|
32
|
+
export { codeSpanConstruct };
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
//#region src/internal/inlines/emphasis.ts
|
|
2
|
+
const C_ASTERISK = 42;
|
|
3
|
+
const C_UNDERSCORE = 95;
|
|
4
|
+
const rePunctuation = /^[!"#$%&'()*+,\-./:;<=>?@[\]\\^_`{|}~\p{P}\p{S}]/u;
|
|
5
|
+
const reUnicodeWhitespaceChar = /^\s/;
|
|
6
|
+
/**
|
|
7
|
+
* Measure the delimiter run at the cursor and decide whether it can open or
|
|
8
|
+
* close emphasis, per the spec's left- and right-flanking rules. Leaves the
|
|
9
|
+
* cursor where it found it.
|
|
10
|
+
*
|
|
11
|
+
* The `_` arm is the only character-specific rule here, so `~` (which follows
|
|
12
|
+
* `*`'s rules) reuses this as it stands.
|
|
13
|
+
*/
|
|
14
|
+
const scanDelims = (scanner, cc) => {
|
|
15
|
+
const startpos = scanner.pos;
|
|
16
|
+
let numdelims = 0;
|
|
17
|
+
while (scanner.peek() === cc) {
|
|
18
|
+
numdelims += 1;
|
|
19
|
+
scanner.pos += 1;
|
|
20
|
+
}
|
|
21
|
+
if (numdelims === 0) {
|
|
22
|
+
scanner.pos = startpos;
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
const charBefore = startpos === 0 ? "\n" : scanner.subject.charAt(startpos - 1);
|
|
26
|
+
const ccAfter = scanner.peek();
|
|
27
|
+
const charAfter = ccAfter === -1 ? "\n" : String.fromCodePoint(ccAfter);
|
|
28
|
+
const afterIsWhitespace = reUnicodeWhitespaceChar.test(charAfter);
|
|
29
|
+
const afterIsPunctuation = rePunctuation.test(charAfter);
|
|
30
|
+
const beforeIsWhitespace = reUnicodeWhitespaceChar.test(charBefore);
|
|
31
|
+
const beforeIsPunctuation = rePunctuation.test(charBefore);
|
|
32
|
+
const leftFlanking = !afterIsWhitespace && (!afterIsPunctuation || beforeIsWhitespace || beforeIsPunctuation);
|
|
33
|
+
const rightFlanking = !beforeIsWhitespace && (!beforeIsPunctuation || afterIsWhitespace || afterIsPunctuation);
|
|
34
|
+
const canOpen = cc === C_UNDERSCORE ? leftFlanking && (!rightFlanking || beforeIsPunctuation) : leftFlanking;
|
|
35
|
+
const canClose = cc === C_UNDERSCORE ? rightFlanking && (!leftFlanking || afterIsPunctuation) : rightFlanking;
|
|
36
|
+
scanner.pos = startpos;
|
|
37
|
+
return {
|
|
38
|
+
numdelims,
|
|
39
|
+
canOpen,
|
|
40
|
+
canClose
|
|
41
|
+
};
|
|
42
|
+
};
|
|
43
|
+
const markerCharOf = (cc) => cc === C_UNDERSCORE ? "_" : "*";
|
|
44
|
+
/**
|
|
45
|
+
* Consume a delimiter run as literal text and, when it could open or close
|
|
46
|
+
* emphasis, push it onto the delimiter stack for `processEmphasis` to pair up.
|
|
47
|
+
*/
|
|
48
|
+
const handleDelim = (scanner, cc) => {
|
|
49
|
+
const res = scanDelims(scanner, cc);
|
|
50
|
+
if (res === void 0) return false;
|
|
51
|
+
const startpos = scanner.pos;
|
|
52
|
+
scanner.pos += res.numdelims;
|
|
53
|
+
const node = scanner.appendText(scanner.subject.slice(startpos, scanner.pos), startpos, scanner.pos);
|
|
54
|
+
node.data.markerChar = markerCharOf(cc);
|
|
55
|
+
if (res.canOpen || res.canClose) {
|
|
56
|
+
const delimiter = {
|
|
57
|
+
cc,
|
|
58
|
+
numdelims: res.numdelims,
|
|
59
|
+
origdelims: res.numdelims,
|
|
60
|
+
node,
|
|
61
|
+
previous: scanner.delimiters,
|
|
62
|
+
next: void 0,
|
|
63
|
+
canOpen: res.canOpen,
|
|
64
|
+
canClose: res.canClose
|
|
65
|
+
};
|
|
66
|
+
if (delimiter.previous !== void 0) delimiter.previous.next = delimiter;
|
|
67
|
+
scanner.delimiters = delimiter;
|
|
68
|
+
}
|
|
69
|
+
return true;
|
|
70
|
+
};
|
|
71
|
+
/** Emphasis and strong emphasis: `*` and `_` runs. */
|
|
72
|
+
const emphasisConstruct = {
|
|
73
|
+
name: "emphasis",
|
|
74
|
+
triggers: [C_ASTERISK, C_UNDERSCORE],
|
|
75
|
+
parse: (scanner) => handleDelim(scanner, scanner.peek())
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
//#endregion
|
|
79
|
+
export { emphasisConstruct, scanDelims };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { decodeEntity } from "../entities.js";
|
|
2
|
+
import { ENTITY } from "../unescape.js";
|
|
3
|
+
|
|
4
|
+
//#region src/internal/inlines/entity.ts
|
|
5
|
+
const C_AMPERSAND = 38;
|
|
6
|
+
const reEntityHere = new RegExp(`^${ENTITY}`, "i");
|
|
7
|
+
/** A named, decimal or hexadecimal character reference. */
|
|
8
|
+
const entityConstruct = {
|
|
9
|
+
name: "entity",
|
|
10
|
+
triggers: [C_AMPERSAND],
|
|
11
|
+
parse: (scanner) => {
|
|
12
|
+
const from = scanner.pos;
|
|
13
|
+
const matched = scanner.match(reEntityHere);
|
|
14
|
+
if (matched === void 0) return false;
|
|
15
|
+
scanner.appendText(decodeEntity(matched) ?? matched, from, scanner.pos);
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
//#endregion
|
|
21
|
+
export { entityConstruct };
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { ESCAPABLE } from "../unescape.js";
|
|
2
|
+
import { makeInlineNode } from "../inlineNode.js";
|
|
3
|
+
|
|
4
|
+
//#region src/internal/inlines/escape.ts
|
|
5
|
+
const C_BACKSLASH = 92;
|
|
6
|
+
const C_NEWLINE = 10;
|
|
7
|
+
const reEscapable = new RegExp(`^${ESCAPABLE}`);
|
|
8
|
+
/** A backslash escape, or a backslash hard line break. */
|
|
9
|
+
const escapeConstruct = {
|
|
10
|
+
name: "escape",
|
|
11
|
+
triggers: [C_BACKSLASH],
|
|
12
|
+
parse: (scanner) => {
|
|
13
|
+
const from = scanner.pos;
|
|
14
|
+
scanner.pos += 1;
|
|
15
|
+
if (scanner.peek() === C_NEWLINE) {
|
|
16
|
+
scanner.pos += 1;
|
|
17
|
+
const node = makeInlineNode("break", from, scanner.pos);
|
|
18
|
+
node.data.breakStyle = "backslash";
|
|
19
|
+
scanner.append(node);
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
const escaped = scanner.subject.charAt(scanner.pos);
|
|
23
|
+
if (reEscapable.test(escaped)) {
|
|
24
|
+
scanner.pos += 1;
|
|
25
|
+
scanner.appendText(escaped, from, scanner.pos);
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
scanner.appendText("\\", from, scanner.pos);
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
//#endregion
|
|
34
|
+
export { escapeConstruct };
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { normalizeLabelText } from "../references.js";
|
|
2
|
+
import { insertAfter, makeInlineNode, unlink } from "../inlineNode.js";
|
|
3
|
+
import { makeImageOpenConstruct, makeLinkCloseConstruct } from "./link.js";
|
|
4
|
+
|
|
5
|
+
//#region src/internal/inlines/footnoteReference.ts
|
|
6
|
+
const C_CARET = 94;
|
|
7
|
+
/**
|
|
8
|
+
* Whether the bracket that just closed looks like `[^...]` with something
|
|
9
|
+
* between the caret and the `]`.
|
|
10
|
+
*
|
|
11
|
+
* Upstream tests the node list — the node after the opener must be TEXT whose
|
|
12
|
+
* literal starts with `^`, and there must be more content than that caret
|
|
13
|
+
* alone (`literal->len > 1 || opener->inl_text->next->next`). Read off the
|
|
14
|
+
* subject the two tests are the same: `^` triggers no construct, so it always
|
|
15
|
+
* lands in a text node, and "more content" is precisely "the `]` is not the
|
|
16
|
+
* next character".
|
|
17
|
+
*/
|
|
18
|
+
const looksLikeFootnote = (subject, openerIndex, bracketPos) => subject.charCodeAt(openerIndex + 1) === C_CARET && bracketPos > openerIndex + 2;
|
|
19
|
+
/**
|
|
20
|
+
* cmark-gfm's footnote branch: form a reference, or leave the whole span as
|
|
21
|
+
* the literal text upstream's post-pass would have rewritten it to.
|
|
22
|
+
*/
|
|
23
|
+
const footnoteReferenceFallback = (scanner, opener, bracketPos, afterBracket) => {
|
|
24
|
+
if (!looksLikeFootnote(scanner.subject, opener.index, bracketPos)) return false;
|
|
25
|
+
scanner.pos = afterBracket;
|
|
26
|
+
const rawLabel = scanner.subject.slice(opener.index + 2, bracketPos);
|
|
27
|
+
const key = normalizeLabelText(rawLabel);
|
|
28
|
+
const matched = key !== "" && scanner.footnoteLabels.has(key);
|
|
29
|
+
scanner.processEmphasis(opener.previousDelimiter);
|
|
30
|
+
const start = opener.node.start;
|
|
31
|
+
const node = matched ? makeInlineNode("footnoteReference", start, scanner.pos) : makeInlineNode("text", start, scanner.pos, `[^${rawLabel}]`);
|
|
32
|
+
if (matched) {
|
|
33
|
+
node.data.identifier = key.toLowerCase();
|
|
34
|
+
node.data.label = rawLabel;
|
|
35
|
+
}
|
|
36
|
+
insertAfter(opener.node, node);
|
|
37
|
+
let child = node.next;
|
|
38
|
+
while (child !== void 0) {
|
|
39
|
+
const following = child.next;
|
|
40
|
+
unlink(child);
|
|
41
|
+
child = following;
|
|
42
|
+
}
|
|
43
|
+
scanner.removeBracket();
|
|
44
|
+
unlink(opener.node);
|
|
45
|
+
return true;
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* The `]` construct for the `gfm` dialect: CommonMark's, with the footnote
|
|
49
|
+
* branch wired into its no-match seam.
|
|
50
|
+
*/
|
|
51
|
+
const gfmLinkCloseConstruct = makeLinkCloseConstruct(footnoteReferenceFallback);
|
|
52
|
+
/**
|
|
53
|
+
* The `!` construct for the `gfm` dialect: CommonMark's, refusing to open an
|
|
54
|
+
* image on `![^` so the caret can reach the footnote branch.
|
|
55
|
+
*
|
|
56
|
+
* `makeImageOpenConstruct` carries the reason; it is a footnote rule living on
|
|
57
|
+
* the bang, which is why it is re-exported from here rather than left to look
|
|
58
|
+
* like a link concern.
|
|
59
|
+
*/
|
|
60
|
+
const gfmImageOpenConstruct = makeImageOpenConstruct(false);
|
|
61
|
+
|
|
62
|
+
//#endregion
|
|
63
|
+
export { footnoteReferenceFallback, gfmImageOpenConstruct, gfmLinkCloseConstruct };
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { makeInlineNode } from "../inlineNode.js";
|
|
2
|
+
|
|
3
|
+
//#region src/internal/inlines/lineBreak.ts
|
|
4
|
+
const C_NEWLINE = 10;
|
|
5
|
+
const reInitialSpace = /^ */;
|
|
6
|
+
/** A soft or hard line break. */
|
|
7
|
+
const lineBreakConstruct = {
|
|
8
|
+
name: "lineBreak",
|
|
9
|
+
triggers: [C_NEWLINE],
|
|
10
|
+
parse: (scanner) => {
|
|
11
|
+
const from = scanner.pos;
|
|
12
|
+
scanner.pos += 1;
|
|
13
|
+
const trailingSpaces = scanner.trimTrailingSpaces();
|
|
14
|
+
if (trailingSpaces >= 2) {
|
|
15
|
+
const node = makeInlineNode("break", from - trailingSpaces, scanner.pos);
|
|
16
|
+
node.data.breakStyle = "spaces";
|
|
17
|
+
scanner.append(node);
|
|
18
|
+
} else scanner.appendText("\n", from, scanner.pos);
|
|
19
|
+
scanner.match(reInitialSpace);
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
//#endregion
|
|
25
|
+
export { lineBreakConstruct };
|