@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.
Files changed (65) hide show
  1. package/Frontmatter.js +272 -0
  2. package/FrontmatterResolver.js +273 -0
  3. package/JsonFrontmatter.js +46 -0
  4. package/LICENSE +21 -0
  5. package/Markdown.js +251 -0
  6. package/MarkdownDiagnostic.js +85 -0
  7. package/MarkdownDocument.js +248 -0
  8. package/MarkdownEdit.js +52 -0
  9. package/MarkdownFormat.js +380 -0
  10. package/MarkdownNode.js +641 -0
  11. package/MarkdownVisitor.js +88 -0
  12. package/Mdast.js +384 -0
  13. package/README.md +255 -0
  14. package/TomlFrontmatter.js +44 -0
  15. package/YamlFrontmatter.js +45 -0
  16. package/index.d.ts +2190 -0
  17. package/index.js +15 -0
  18. package/internal/blockParser.js +419 -0
  19. package/internal/blockRegistry.js +90 -0
  20. package/internal/blockTypes.js +27 -0
  21. package/internal/blocks/atxHeading.js +54 -0
  22. package/internal/blocks/blockquote.js +40 -0
  23. package/internal/blocks/code.js +90 -0
  24. package/internal/blocks/document.js +17 -0
  25. package/internal/blocks/fencedCode.js +26 -0
  26. package/internal/blocks/footnoteDefinition.js +84 -0
  27. package/internal/blocks/frontmatter.js +56 -0
  28. package/internal/blocks/htmlBlock.js +72 -0
  29. package/internal/blocks/indentedCode.js +16 -0
  30. package/internal/blocks/linkReferenceDefinition.js +72 -0
  31. package/internal/blocks/list.js +159 -0
  32. package/internal/blocks/paragraph.js +27 -0
  33. package/internal/blocks/setextHeading.js +24 -0
  34. package/internal/blocks/table.js +378 -0
  35. package/internal/blocks/taskListItem.js +36 -0
  36. package/internal/blocks/thematicBreak.js +35 -0
  37. package/internal/carriers.js +42 -0
  38. package/internal/entities.js +26 -0
  39. package/internal/entityMap.js +7 -0
  40. package/internal/htmlTags.js +18 -0
  41. package/internal/inlineNode.js +54 -0
  42. package/internal/inlineParser.js +403 -0
  43. package/internal/inlineRegistry.js +68 -0
  44. package/internal/inlines/autolink.js +36 -0
  45. package/internal/inlines/autolinkLiteral.js +374 -0
  46. package/internal/inlines/codeSpan.js +32 -0
  47. package/internal/inlines/emphasis.js +79 -0
  48. package/internal/inlines/entity.js +21 -0
  49. package/internal/inlines/escape.js +34 -0
  50. package/internal/inlines/footnoteReference.js +63 -0
  51. package/internal/inlines/lineBreak.js +25 -0
  52. package/internal/inlines/link.js +212 -0
  53. package/internal/inlines/rawHtml.js +27 -0
  54. package/internal/inlines/strikethrough.js +81 -0
  55. package/internal/inlines/text.js +22 -0
  56. package/internal/lineIndex.js +76 -0
  57. package/internal/patterns.js +26 -0
  58. package/internal/preprocess.js +58 -0
  59. package/internal/rawInline.js +57 -0
  60. package/internal/references.js +160 -0
  61. package/internal/segments.js +36 -0
  62. package/internal/stringify.js +519 -0
  63. package/internal/unescape.js +18 -0
  64. package/package.json +61 -0
  65. 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 };