@tabnas/markdown 0.4.2 → 0.5.1
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/README.md +131 -10
- package/dist/ast.d.ts +113 -0
- package/dist/ast.js +273 -0
- package/dist/ast.js.map +1 -0
- package/dist/block.d.ts +10 -0
- package/dist/block.js +1411 -0
- package/dist/block.js.map +1 -0
- package/dist/common.d.ts +49 -0
- package/dist/common.js +155 -0
- package/dist/common.js.map +1 -0
- package/dist/commonmark.d.ts +14 -0
- package/dist/commonmark.js +31 -0
- package/dist/commonmark.js.map +1 -0
- package/dist/entities.d.ts +1 -0
- package/dist/entities.js +14 -0
- package/dist/entities.js.map +1 -0
- package/dist/html.d.ts +16 -0
- package/dist/html.js +393 -0
- package/dist/html.js.map +1 -0
- package/dist/inline.d.ts +19 -0
- package/dist/inline.js +1331 -0
- package/dist/inline.js.map +1 -0
- package/dist/markdown.d.ts +21 -84
- package/dist/markdown.js +80 -687
- package/dist/markdown.js.map +1 -1
- package/dist/node.d.ts +112 -0
- package/dist/node.js +203 -0
- package/dist/node.js.map +1 -0
- package/dist/options.d.ts +26 -0
- package/dist/options.js +16 -0
- package/dist/options.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +3 -1
- package/src/ast.ts +406 -0
- package/src/block.ts +1579 -0
- package/src/common.ts +166 -0
- package/src/commonmark.ts +34 -0
- package/src/entities.ts +14 -0
- package/src/html.ts +446 -0
- package/src/inline.ts +1495 -0
- package/src/markdown.ts +132 -770
- package/src/node.ts +293 -0
- package/src/options.ts +44 -0
- package/src/tsconfig.json +2 -0
package/dist/inline.js
ADDED
|
@@ -0,0 +1,1331 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/* Copyright (c) 2021-2026 Richard Rodger, MIT License */
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.linkifyAutolinks = linkifyAutolinks;
|
|
5
|
+
exports.parseInlines = parseInlines;
|
|
6
|
+
exports.parseReference = parseReference;
|
|
7
|
+
// Phase 2 of the CommonMark parse (spec 0.31.2, Appendix A, "Phase 2: inline
|
|
8
|
+
// structure"). The block phase left the raw text of every paragraph and
|
|
9
|
+
// heading in `stringContent`; this module turns that text into inline
|
|
10
|
+
// children and clears it.
|
|
11
|
+
//
|
|
12
|
+
// The shape is the spec's own algorithm: one left-to-right scan over the
|
|
13
|
+
// subject that appends nodes as it goes, plus two auxiliary stacks that are
|
|
14
|
+
// resolved out of band, because neither construct can be decided at the
|
|
15
|
+
// point where its opener is seen:
|
|
16
|
+
//
|
|
17
|
+
// * the delimiter stack (§6.4) holds `*`/`_` (and, under GFM, `~`) runs,
|
|
18
|
+
// and is resolved by `processEmphasis`;
|
|
19
|
+
// * the bracket stack (§6.3) holds `[` and `![`, and is resolved by
|
|
20
|
+
// `parseCloseBracket` when a `]` turns up.
|
|
21
|
+
//
|
|
22
|
+
// Everything else — code spans, autolinks, raw HTML, entities, escapes,
|
|
23
|
+
// breaks — is decided locally at the scan position. That is what produces
|
|
24
|
+
// the precedence §6.3 requires: a code span, autolink or raw HTML tag is
|
|
25
|
+
// consumed whole during the scan, so it binds more tightly than emphasis and
|
|
26
|
+
// more tightly than the brackets of a link label. Conversely brackets bind
|
|
27
|
+
// more tightly than emphasis, because bracket matching happens during the
|
|
28
|
+
// scan while emphasis matching happens afterwards over the delimiter stack.
|
|
29
|
+
const node_ts_1 = require("./node.js");
|
|
30
|
+
const common_ts_1 = require("./common.js");
|
|
31
|
+
const C_TAB = 9;
|
|
32
|
+
const C_NEWLINE = 10;
|
|
33
|
+
const C_VTAB = 11;
|
|
34
|
+
const C_FORMFEED = 12;
|
|
35
|
+
const C_CARRIAGE_RETURN = 13;
|
|
36
|
+
const C_SPACE = 32;
|
|
37
|
+
const C_BANG = 33;
|
|
38
|
+
const C_AMPERSAND = 38;
|
|
39
|
+
const C_OPEN_PAREN = 40;
|
|
40
|
+
const C_CLOSE_PAREN = 41;
|
|
41
|
+
const C_ASTERISK = 42;
|
|
42
|
+
const C_PLUS = 43;
|
|
43
|
+
const C_COMMA = 44;
|
|
44
|
+
const C_HYPHEN = 45;
|
|
45
|
+
const C_PERIOD = 46;
|
|
46
|
+
const C_COLON = 58;
|
|
47
|
+
const C_SEMICOLON = 59;
|
|
48
|
+
const C_LESSTHAN = 60;
|
|
49
|
+
const C_QUESTION = 63;
|
|
50
|
+
const C_AT = 64;
|
|
51
|
+
const C_OPEN_BRACKET = 91;
|
|
52
|
+
const C_BACKSLASH = 92;
|
|
53
|
+
const C_CLOSE_BRACKET = 93;
|
|
54
|
+
const C_UNDERSCORE = 95;
|
|
55
|
+
const C_BACKTICK = 96;
|
|
56
|
+
const C_TILDE = 126;
|
|
57
|
+
const C_DEL = 127;
|
|
58
|
+
// --- §6.6 Raw HTML: the tag grammar, spelled out because it is stricter than
|
|
59
|
+
// `<[^>]*>` in every part. Attribute names are XML-restricted-to-ASCII;
|
|
60
|
+
// unquoted attribute values exclude quotes, `=`, `<`, `>`, backtick and all
|
|
61
|
+
// control characters; comments/PIs/declarations/CDATA each have their own
|
|
62
|
+
// terminator. `\s*` stands in for "spaces, tabs, and up to one line ending":
|
|
63
|
+
// a paragraph can never contain a blank line, so a single `\s*` can only ever
|
|
64
|
+
// span one line ending anyway.
|
|
65
|
+
const TAGNAME = '[A-Za-z][A-Za-z0-9-]*';
|
|
66
|
+
const ATTRIBUTENAME = '[a-zA-Z_:][a-zA-Z0-9:._-]*';
|
|
67
|
+
const UNQUOTEDVALUE = "[^\"'=<>`\\x00-\\x20]+";
|
|
68
|
+
const SINGLEQUOTEDVALUE = "'[^']*'";
|
|
69
|
+
const DOUBLEQUOTEDVALUE = '"[^"]*"';
|
|
70
|
+
const ATTRIBUTEVALUE = '(?:' + UNQUOTEDVALUE + '|' + SINGLEQUOTEDVALUE + '|' + DOUBLEQUOTEDVALUE + ')';
|
|
71
|
+
const ATTRIBUTEVALUESPEC = '(?:\\s*=\\s*' + ATTRIBUTEVALUE + ')';
|
|
72
|
+
const ATTRIBUTE = '(?:\\s+' + ATTRIBUTENAME + ATTRIBUTEVALUESPEC + '?)';
|
|
73
|
+
const OPENTAG = '<' + TAGNAME + ATTRIBUTE + '*\\s*/?>';
|
|
74
|
+
const CLOSETAG = '</' + TAGNAME + '\\s*>';
|
|
75
|
+
// §6.6: `<!-->` and `<!--->` are comments in their own right; otherwise the
|
|
76
|
+
// body simply may not contain `-->`, which the lazy quantifier enforces.
|
|
77
|
+
const HTMLCOMMENT = '<!-->|<!--->|<!--[\\s\\S]*?-->';
|
|
78
|
+
const PROCESSINGINSTRUCTION = '[<][?][\\s\\S]*?[?][>]';
|
|
79
|
+
const DECLARATION = '<![A-Za-z][^>]*>';
|
|
80
|
+
const CDATA = '<!\\[CDATA\\[[\\s\\S]*?\\]\\]>';
|
|
81
|
+
const HTMLTAG = '(?:' +
|
|
82
|
+
OPENTAG +
|
|
83
|
+
'|' +
|
|
84
|
+
CLOSETAG +
|
|
85
|
+
'|' +
|
|
86
|
+
HTMLCOMMENT +
|
|
87
|
+
'|' +
|
|
88
|
+
PROCESSINGINSTRUCTION +
|
|
89
|
+
'|' +
|
|
90
|
+
DECLARATION +
|
|
91
|
+
'|' +
|
|
92
|
+
CDATA +
|
|
93
|
+
')';
|
|
94
|
+
const ESCAPED_CHAR = '\\\\' + common_ts_1.ESCAPABLE;
|
|
95
|
+
// Every regex handed to `match()` is sticky: it must match at `pos`, and
|
|
96
|
+
// `lastIndex` then gives the new position. (The equivalent of anchoring with
|
|
97
|
+
// `^` on a sliced subject, without the slicing.)
|
|
98
|
+
const reHtmlTag = new RegExp(HTMLTAG, 'iy');
|
|
99
|
+
const reEntityHere = new RegExp(common_ts_1.ENTITY, 'iy');
|
|
100
|
+
const reEscapable = new RegExp('^' + common_ts_1.ESCAPABLE);
|
|
101
|
+
const reTicksHere = /`+/y;
|
|
102
|
+
const reTicks = /`+/g;
|
|
103
|
+
// Chunks of ordinary text: everything up to the next character that could
|
|
104
|
+
// start an inline construct. `~` is only special under GFM, hence two.
|
|
105
|
+
const reMain = /[^\n`[\]\\!<&*_]+/y;
|
|
106
|
+
const reMainGfm = /[^\n`[\]\\!<&*_~]+/y;
|
|
107
|
+
/**
|
|
108
|
+
* Maximum `(` nesting inside a bare link destination. The spec describes
|
|
109
|
+
* balanced parentheses without stating a bound; cmark uses 32, and the bound
|
|
110
|
+
* is what keeps an unbalanced run from being quadratic (see
|
|
111
|
+
* parseLinkDestination).
|
|
112
|
+
*/
|
|
113
|
+
const MAX_LINK_PAREN_NESTING = 32;
|
|
114
|
+
const reSpnl = / *(?:\n *)?/y;
|
|
115
|
+
const reInitialSpace = / */y;
|
|
116
|
+
const reIndent = / {0,3}/y;
|
|
117
|
+
const reFinalSpace = / *$/;
|
|
118
|
+
const reSpaceAtEndOfLine = / *(?:\n|$)/y;
|
|
119
|
+
const reWhitespaceChar = /[ \t\n\v\f\r]/;
|
|
120
|
+
// §4.7: at most 999 characters between the brackets, brackets inside only if
|
|
121
|
+
// backslash-escaped. `s` so that an escaped line ending counts as an escape.
|
|
122
|
+
const reLinkLabel = /\[(?:[^\\[\]]|\\.){0,1000}\]/sy;
|
|
123
|
+
// §6.3 link destination, pointy-bracket form: no line endings, no unescaped
|
|
124
|
+
// `<` or `>`.
|
|
125
|
+
const reLinkDestinationBraces = /<(?:[^<>\n\\\x00]|\\.)*>/y;
|
|
126
|
+
// §6.3 link title in `"`, `'` or `(...)` delimiters. The escape alternative
|
|
127
|
+
// comes first so that `\"` inside a double-quoted title is consumed as an
|
|
128
|
+
// escape rather than closing the title.
|
|
129
|
+
const reLinkTitle = new RegExp('(?:"(?:' +
|
|
130
|
+
ESCAPED_CHAR +
|
|
131
|
+
'|[^"\\x00])*"' +
|
|
132
|
+
"|'(?:" +
|
|
133
|
+
ESCAPED_CHAR +
|
|
134
|
+
"|[^'\\x00])*'" +
|
|
135
|
+
'|\\((?:' +
|
|
136
|
+
ESCAPED_CHAR +
|
|
137
|
+
'|[^()\\x00])*\\))', 'y');
|
|
138
|
+
// §6.5 autolinks. Scheme: an ASCII letter then 1–31 more of letter/digit/
|
|
139
|
+
// `+`/`.`/`-`, i.e. 2–32 characters in total, case-insensitive.
|
|
140
|
+
const reAutolink = /<[A-Za-z][A-Za-z0-9.+-]{1,31}:[^<>\x00-\x20]*>/y;
|
|
141
|
+
const reEmailAutolink = /<([a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*)>/y;
|
|
142
|
+
function text(literal) {
|
|
143
|
+
const node = new node_ts_1.MdNode('text');
|
|
144
|
+
node.literal = literal;
|
|
145
|
+
return node;
|
|
146
|
+
}
|
|
147
|
+
// §2.1 punctuation is P* ∪ S*, which `isUnicodePunctuation` already covers.
|
|
148
|
+
// `*$*alpha.` is the case that needs the symbol half: with `$` counted as
|
|
149
|
+
// punctuation the closing `*` is not right-flanking, so the line stays
|
|
150
|
+
// literal text.
|
|
151
|
+
const isPunctuation = common_ts_1.isUnicodePunctuation;
|
|
152
|
+
class InlineParser {
|
|
153
|
+
constructor(refmap, options) {
|
|
154
|
+
this.subject = '';
|
|
155
|
+
this.pos = 0;
|
|
156
|
+
this.delimiters = null;
|
|
157
|
+
this.brackets = null;
|
|
158
|
+
this.refmap = refmap;
|
|
159
|
+
this.options = options;
|
|
160
|
+
}
|
|
161
|
+
// --- scanning primitives ---
|
|
162
|
+
/** Character code at `pos`, or -1 at the end of the subject. */
|
|
163
|
+
peek() {
|
|
164
|
+
return this.pos < this.subject.length ? this.subject.charCodeAt(this.pos) : -1;
|
|
165
|
+
}
|
|
166
|
+
/** Match a sticky regex at `pos`; on success advance past it. */
|
|
167
|
+
match(re) {
|
|
168
|
+
re.lastIndex = this.pos;
|
|
169
|
+
const m = re.exec(this.subject);
|
|
170
|
+
if (null === m)
|
|
171
|
+
return null;
|
|
172
|
+
this.pos = re.lastIndex;
|
|
173
|
+
return m[0];
|
|
174
|
+
}
|
|
175
|
+
/** Spaces, and at most one line ending, between the parts of a link. */
|
|
176
|
+
spnl() {
|
|
177
|
+
this.match(reSpnl);
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* The whole code point ending at `end`, so an astral character is
|
|
181
|
+
* classified as one character by the flanking rules rather than as a lone
|
|
182
|
+
* surrogate. Start of subject counts as a line ending (§6.4).
|
|
183
|
+
*/
|
|
184
|
+
charBefore(end) {
|
|
185
|
+
if (end <= 0)
|
|
186
|
+
return '\n';
|
|
187
|
+
const lo = this.subject.charCodeAt(end - 1);
|
|
188
|
+
if (lo >= 0xdc00 && lo <= 0xdfff && end >= 2) {
|
|
189
|
+
const hi = this.subject.charCodeAt(end - 2);
|
|
190
|
+
if (hi >= 0xd800 && hi <= 0xdbff)
|
|
191
|
+
return this.subject.slice(end - 2, end);
|
|
192
|
+
}
|
|
193
|
+
return this.subject.charAt(end - 1);
|
|
194
|
+
}
|
|
195
|
+
/** The whole code point at `i`; end of subject counts as a line ending. */
|
|
196
|
+
charAtPos(i) {
|
|
197
|
+
if (i >= this.subject.length)
|
|
198
|
+
return '\n';
|
|
199
|
+
const cp = this.subject.codePointAt(i);
|
|
200
|
+
return undefined === cp ? '\n' : String.fromCodePoint(cp);
|
|
201
|
+
}
|
|
202
|
+
// --- §6.3 code spans ---
|
|
203
|
+
/**
|
|
204
|
+
* A code span runs from a backtick string to the next backtick string of
|
|
205
|
+
* *exactly* the same length. Unmatched opening backticks are literal text.
|
|
206
|
+
*/
|
|
207
|
+
parseBackticks(block) {
|
|
208
|
+
const ticks = this.match(reTicksHere);
|
|
209
|
+
if (null === ticks)
|
|
210
|
+
return false;
|
|
211
|
+
const afterOpenTicks = this.pos;
|
|
212
|
+
reTicks.lastIndex = this.pos;
|
|
213
|
+
let m;
|
|
214
|
+
while (null !== (m = reTicks.exec(this.subject))) {
|
|
215
|
+
this.pos = reTicks.lastIndex;
|
|
216
|
+
if (m[0] === ticks) {
|
|
217
|
+
const node = new node_ts_1.MdNode('code');
|
|
218
|
+
// Line endings become spaces; then one leading and one trailing
|
|
219
|
+
// space are stripped together, but only if the content is not all
|
|
220
|
+
// spaces (which is what lets a code span hold backticks).
|
|
221
|
+
const contents = this.subject
|
|
222
|
+
.slice(afterOpenTicks, this.pos - ticks.length)
|
|
223
|
+
.replace(/\n/g, ' ');
|
|
224
|
+
node.literal =
|
|
225
|
+
contents.length > 2 &&
|
|
226
|
+
' ' === contents[0] &&
|
|
227
|
+
' ' === contents[contents.length - 1] &&
|
|
228
|
+
null !== /[^ ]/.exec(contents)
|
|
229
|
+
? contents.slice(1, contents.length - 1)
|
|
230
|
+
: contents;
|
|
231
|
+
block.appendChild(node);
|
|
232
|
+
return true;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
// No closing run of the same length: the opener is literal text.
|
|
236
|
+
this.pos = afterOpenTicks;
|
|
237
|
+
block.appendChild(text(ticks));
|
|
238
|
+
return true;
|
|
239
|
+
}
|
|
240
|
+
// --- §6.1 backslash escapes, §6.7 hard line breaks ---
|
|
241
|
+
parseBackslash(block) {
|
|
242
|
+
this.pos += 1;
|
|
243
|
+
if (C_NEWLINE === this.peek()) {
|
|
244
|
+
this.pos += 1;
|
|
245
|
+
block.appendChild(new node_ts_1.MdNode('linebreak'));
|
|
246
|
+
this.match(reInitialSpace);
|
|
247
|
+
}
|
|
248
|
+
else if (reEscapable.test(this.subject.charAt(this.pos))) {
|
|
249
|
+
block.appendChild(text(this.subject.charAt(this.pos)));
|
|
250
|
+
this.pos += 1;
|
|
251
|
+
}
|
|
252
|
+
else {
|
|
253
|
+
block.appendChild(text('\\'));
|
|
254
|
+
}
|
|
255
|
+
return true;
|
|
256
|
+
}
|
|
257
|
+
// --- §6.5 autolinks ---
|
|
258
|
+
parseAutolink(block) {
|
|
259
|
+
let m = this.match(reEmailAutolink);
|
|
260
|
+
if (null !== m) {
|
|
261
|
+
const dest = m.slice(1, m.length - 1);
|
|
262
|
+
block.appendChild(this.makeAutolink('mailto:' + dest, dest));
|
|
263
|
+
return true;
|
|
264
|
+
}
|
|
265
|
+
m = this.match(reAutolink);
|
|
266
|
+
if (null !== m) {
|
|
267
|
+
const dest = m.slice(1, m.length - 1);
|
|
268
|
+
block.appendChild(this.makeAutolink(dest, dest));
|
|
269
|
+
return true;
|
|
270
|
+
}
|
|
271
|
+
return false;
|
|
272
|
+
}
|
|
273
|
+
makeAutolink(destination, label) {
|
|
274
|
+
// Autolink text is literal: no backslash escapes, no entity references.
|
|
275
|
+
const node = new node_ts_1.MdNode('link');
|
|
276
|
+
// Not percent-encoded here — see parseLinkDestination.
|
|
277
|
+
node.destination = destination;
|
|
278
|
+
node.title = null;
|
|
279
|
+
node.appendChild(text(label));
|
|
280
|
+
return node;
|
|
281
|
+
}
|
|
282
|
+
// --- §6.6 raw HTML ---
|
|
283
|
+
parseHtmlTag(block) {
|
|
284
|
+
const m = this.match(reHtmlTag);
|
|
285
|
+
if (null === m)
|
|
286
|
+
return false;
|
|
287
|
+
const node = new node_ts_1.MdNode('html_inline');
|
|
288
|
+
node.literal = m;
|
|
289
|
+
block.appendChild(node);
|
|
290
|
+
return true;
|
|
291
|
+
}
|
|
292
|
+
// --- §6.4 emphasis: delimiter runs and flanking ---
|
|
293
|
+
/**
|
|
294
|
+
* Classify the run of `cc` starting at `pos` without consuming it.
|
|
295
|
+
*
|
|
296
|
+
* §6.4: a run is left-flanking iff it is not followed by Unicode
|
|
297
|
+
* whitespace and either not followed by Unicode punctuation or else
|
|
298
|
+
* preceded by whitespace or punctuation; right-flanking is the mirror
|
|
299
|
+
* image. `_` additionally may not open inside a word (rules 2, 4, 6, 8),
|
|
300
|
+
* which is what keeps `snake_case_words` intact; `*` has no such
|
|
301
|
+
* restriction.
|
|
302
|
+
*/
|
|
303
|
+
scanDelims(cc) {
|
|
304
|
+
const startpos = this.pos;
|
|
305
|
+
let numdelims = 0;
|
|
306
|
+
while (cc === this.peek()) {
|
|
307
|
+
numdelims += 1;
|
|
308
|
+
this.pos += 1;
|
|
309
|
+
}
|
|
310
|
+
if (0 === numdelims)
|
|
311
|
+
return null;
|
|
312
|
+
const charBefore = this.charBefore(startpos);
|
|
313
|
+
const charAfter = this.charAtPos(this.pos);
|
|
314
|
+
const afterIsWhitespace = (0, common_ts_1.isUnicodeWhitespace)(charAfter);
|
|
315
|
+
const afterIsPunctuation = isPunctuation(charAfter);
|
|
316
|
+
const beforeIsWhitespace = (0, common_ts_1.isUnicodeWhitespace)(charBefore);
|
|
317
|
+
const beforeIsPunctuation = isPunctuation(charBefore);
|
|
318
|
+
const leftFlanking = !afterIsWhitespace &&
|
|
319
|
+
(!afterIsPunctuation || beforeIsWhitespace || beforeIsPunctuation);
|
|
320
|
+
const rightFlanking = !beforeIsWhitespace &&
|
|
321
|
+
(!beforeIsPunctuation || afterIsWhitespace || afterIsPunctuation);
|
|
322
|
+
let canOpen;
|
|
323
|
+
let canClose;
|
|
324
|
+
if (C_UNDERSCORE === cc) {
|
|
325
|
+
canOpen = leftFlanking && (!rightFlanking || beforeIsPunctuation);
|
|
326
|
+
canClose = rightFlanking && (!leftFlanking || afterIsPunctuation);
|
|
327
|
+
}
|
|
328
|
+
else {
|
|
329
|
+
canOpen = leftFlanking;
|
|
330
|
+
canClose = rightFlanking;
|
|
331
|
+
}
|
|
332
|
+
this.pos = startpos;
|
|
333
|
+
return { numdelims, canOpen, canClose };
|
|
334
|
+
}
|
|
335
|
+
/** Emit a delimiter run as text and push it on the delimiter stack. */
|
|
336
|
+
handleDelim(cc, block) {
|
|
337
|
+
const res = this.scanDelims(cc);
|
|
338
|
+
if (null === res)
|
|
339
|
+
return false;
|
|
340
|
+
const numdelims = res.numdelims;
|
|
341
|
+
const startpos = this.pos;
|
|
342
|
+
this.pos += numdelims;
|
|
343
|
+
const node = text(this.subject.slice(startpos, this.pos));
|
|
344
|
+
block.appendChild(node);
|
|
345
|
+
// GFM strikethrough only recognises runs of one or two tildes; longer
|
|
346
|
+
// runs stay literal.
|
|
347
|
+
const stackable = C_TILDE === cc ? numdelims <= 2 : true;
|
|
348
|
+
if ((res.canOpen || res.canClose) && stackable) {
|
|
349
|
+
const delim = {
|
|
350
|
+
cc,
|
|
351
|
+
numdelims,
|
|
352
|
+
origdelims: numdelims,
|
|
353
|
+
node,
|
|
354
|
+
previous: this.delimiters,
|
|
355
|
+
next: null,
|
|
356
|
+
canOpen: res.canOpen,
|
|
357
|
+
canClose: res.canClose,
|
|
358
|
+
};
|
|
359
|
+
if (null !== delim.previous)
|
|
360
|
+
delim.previous.next = delim;
|
|
361
|
+
this.delimiters = delim;
|
|
362
|
+
}
|
|
363
|
+
return true;
|
|
364
|
+
}
|
|
365
|
+
removeDelimiter(delim) {
|
|
366
|
+
if (null !== delim.previous)
|
|
367
|
+
delim.previous.next = delim.next;
|
|
368
|
+
if (null === delim.next) {
|
|
369
|
+
// Top of stack.
|
|
370
|
+
this.delimiters = delim.previous;
|
|
371
|
+
}
|
|
372
|
+
else {
|
|
373
|
+
delim.next.previous = delim.previous;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* The spec's "process emphasis" procedure. `stackBottom` is the floor: for
|
|
378
|
+
* the whole-block call it is null, and for the inlines of a link it is the
|
|
379
|
+
* delimiter that was on top when the link's `[` opened.
|
|
380
|
+
*/
|
|
381
|
+
processEmphasis(stackBottom) {
|
|
382
|
+
// openers_bottom, keyed by (delimiter char, whether the closer can also
|
|
383
|
+
// open, closer run length mod 3). Once a closer of some class fails to
|
|
384
|
+
// find an opener, no later closer of the same class needs to look past
|
|
385
|
+
// that point either — this is what keeps the search linear.
|
|
386
|
+
const openersBottom = new Map();
|
|
387
|
+
// First closer above stackBottom.
|
|
388
|
+
let closer = this.delimiters;
|
|
389
|
+
while (null !== closer && closer.previous !== stackBottom) {
|
|
390
|
+
closer = closer.previous;
|
|
391
|
+
}
|
|
392
|
+
while (null !== closer) {
|
|
393
|
+
if (!closer.canClose) {
|
|
394
|
+
closer = closer.next;
|
|
395
|
+
continue;
|
|
396
|
+
}
|
|
397
|
+
const closercc = closer.cc;
|
|
398
|
+
const key = closercc + ':' + (closer.canOpen ? 1 : 0) + ':' + (closer.origdelims % 3);
|
|
399
|
+
const bottom = openersBottom.has(key)
|
|
400
|
+
? openersBottom.get(key)
|
|
401
|
+
: stackBottom;
|
|
402
|
+
let opener = closer.previous;
|
|
403
|
+
let openerFound = false;
|
|
404
|
+
while (null !== opener && opener !== stackBottom && opener !== bottom) {
|
|
405
|
+
if (opener.cc === closercc && opener.canOpen) {
|
|
406
|
+
if (C_TILDE === closercc) {
|
|
407
|
+
// GFM strikethrough: opener and closer runs must be the same
|
|
408
|
+
// length, so `~~x~` is not a match.
|
|
409
|
+
if (opener.numdelims === closer.numdelims) {
|
|
410
|
+
openerFound = true;
|
|
411
|
+
break;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
else {
|
|
415
|
+
// §6.4 rules 9 and 10, the "rule of three": when either side of
|
|
416
|
+
// the pair could both open and close, the two run lengths may
|
|
417
|
+
// not sum to a multiple of 3 — unless both are themselves
|
|
418
|
+
// multiples of 3.
|
|
419
|
+
const oddMatch = (closer.canOpen || opener.canClose) &&
|
|
420
|
+
0 !== closer.origdelims % 3 &&
|
|
421
|
+
0 === (opener.origdelims + closer.origdelims) % 3;
|
|
422
|
+
if (!oddMatch) {
|
|
423
|
+
openerFound = true;
|
|
424
|
+
break;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
opener = opener.previous;
|
|
429
|
+
}
|
|
430
|
+
const oldCloser = closer;
|
|
431
|
+
if (openerFound && null !== opener) {
|
|
432
|
+
// Two delimiters make strong, one makes emph; strikethrough always
|
|
433
|
+
// consumes the whole (equal-length) run.
|
|
434
|
+
const useDelims = C_TILDE === closercc
|
|
435
|
+
? closer.numdelims
|
|
436
|
+
: closer.numdelims >= 2 && opener.numdelims >= 2
|
|
437
|
+
? 2
|
|
438
|
+
: 1;
|
|
439
|
+
const nodeType = C_TILDE === closercc ? 'del' : 1 === useDelims ? 'emph' : 'strong';
|
|
440
|
+
const openerInl = opener.node;
|
|
441
|
+
const closerInl = closer.node;
|
|
442
|
+
opener.numdelims -= useDelims;
|
|
443
|
+
closer.numdelims -= useDelims;
|
|
444
|
+
openerInl.literal = (openerInl.literal ?? '').slice(0, (openerInl.literal ?? '').length - useDelims);
|
|
445
|
+
closerInl.literal = (closerInl.literal ?? '').slice(0, (closerInl.literal ?? '').length - useDelims);
|
|
446
|
+
// Everything between the two delimiter text nodes becomes the
|
|
447
|
+
// content of the new node.
|
|
448
|
+
const emph = new node_ts_1.MdNode(nodeType);
|
|
449
|
+
let tmp = openerInl.next;
|
|
450
|
+
while (null !== tmp && tmp !== closerInl) {
|
|
451
|
+
const nxt = tmp.next;
|
|
452
|
+
tmp.unlink();
|
|
453
|
+
emph.appendChild(tmp);
|
|
454
|
+
tmp = nxt;
|
|
455
|
+
}
|
|
456
|
+
openerInl.insertAfter(emph);
|
|
457
|
+
// Delimiters between the pair can never match anything now.
|
|
458
|
+
if (opener.next !== closer) {
|
|
459
|
+
opener.next = closer;
|
|
460
|
+
closer.previous = opener;
|
|
461
|
+
}
|
|
462
|
+
if (0 === opener.numdelims) {
|
|
463
|
+
openerInl.unlink();
|
|
464
|
+
this.removeDelimiter(opener);
|
|
465
|
+
}
|
|
466
|
+
if (0 === closer.numdelims) {
|
|
467
|
+
closerInl.unlink();
|
|
468
|
+
const tempstack = closer.next;
|
|
469
|
+
this.removeDelimiter(closer);
|
|
470
|
+
closer = tempstack;
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
else {
|
|
474
|
+
closer = closer.next;
|
|
475
|
+
openersBottom.set(key, oldCloser.previous);
|
|
476
|
+
if (!oldCloser.canOpen) {
|
|
477
|
+
// It found no opener and cannot be one: it is inert.
|
|
478
|
+
this.removeDelimiter(oldCloser);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
// Drop everything above the floor.
|
|
483
|
+
while (null !== this.delimiters && this.delimiters !== stackBottom) {
|
|
484
|
+
this.removeDelimiter(this.delimiters);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
// --- §6.3 links and images ---
|
|
488
|
+
/** Link title without its delimiters, unescaped; null if none here. */
|
|
489
|
+
parseLinkTitle() {
|
|
490
|
+
const title = this.match(reLinkTitle);
|
|
491
|
+
if (null === title)
|
|
492
|
+
return null;
|
|
493
|
+
return (0, common_ts_1.unescapeString)(title.slice(1, title.length - 1));
|
|
494
|
+
}
|
|
495
|
+
/**
|
|
496
|
+
* Link destination: either `<...>`, or a bare run with balanced unescaped
|
|
497
|
+
* parentheses and no ASCII control characters or spaces.
|
|
498
|
+
*/
|
|
499
|
+
/**
|
|
500
|
+
* A destination is backslash-unescaped and entity-decoded, but **not**
|
|
501
|
+
* percent-encoded: `[l](/ä)` yields `/ä`, not `/%C3%A4`.
|
|
502
|
+
*
|
|
503
|
+
* Encoding is a rendering concern and `html.ts` applies `normalizeURI` when
|
|
504
|
+
* it writes the attribute. Doing it here too would be invisible in the HTML
|
|
505
|
+
* (`normalizeURI` is idempotent over its own output) but would put the
|
|
506
|
+
* encoded form in the public AST, where mdast — and every previous release
|
|
507
|
+
* of this package — promises the decoded one.
|
|
508
|
+
*/
|
|
509
|
+
parseLinkDestination() {
|
|
510
|
+
const braced = this.match(reLinkDestinationBraces);
|
|
511
|
+
if (null !== braced) {
|
|
512
|
+
return (0, common_ts_1.unescapeString)(braced.slice(1, braced.length - 1));
|
|
513
|
+
}
|
|
514
|
+
if (C_LESSTHAN === this.peek())
|
|
515
|
+
return null;
|
|
516
|
+
const savepos = this.pos;
|
|
517
|
+
let openparens = 0;
|
|
518
|
+
let c = this.peek();
|
|
519
|
+
while (-1 !== c) {
|
|
520
|
+
if (C_BACKSLASH === c && reEscapable.test(this.subject.charAt(this.pos + 1))) {
|
|
521
|
+
this.pos += 1;
|
|
522
|
+
if (-1 !== this.peek())
|
|
523
|
+
this.pos += 1;
|
|
524
|
+
}
|
|
525
|
+
else if (C_OPEN_PAREN === c) {
|
|
526
|
+
// Bail rather than scan on once nesting passes the limit. Without
|
|
527
|
+
// this, input like `[a](b` repeated makes every `]` scan to end of
|
|
528
|
+
// input looking for parens that never balance, which is quadratic in
|
|
529
|
+
// the document size. cmark caps nesting at the same depth, and no
|
|
530
|
+
// spec example nests more than two deep.
|
|
531
|
+
if (MAX_LINK_PAREN_NESTING <= openparens)
|
|
532
|
+
return null;
|
|
533
|
+
this.pos += 1;
|
|
534
|
+
openparens += 1;
|
|
535
|
+
}
|
|
536
|
+
else if (C_CLOSE_PAREN === c) {
|
|
537
|
+
if (openparens < 1)
|
|
538
|
+
break;
|
|
539
|
+
this.pos += 1;
|
|
540
|
+
openparens -= 1;
|
|
541
|
+
}
|
|
542
|
+
else if (c <= C_SPACE || C_DEL === c) {
|
|
543
|
+
// Spaces and ASCII control characters end the bare form.
|
|
544
|
+
break;
|
|
545
|
+
}
|
|
546
|
+
else {
|
|
547
|
+
this.pos += 1;
|
|
548
|
+
}
|
|
549
|
+
c = this.peek();
|
|
550
|
+
}
|
|
551
|
+
// An empty destination is only legal immediately before the closing
|
|
552
|
+
// paren of an inline link, as in `[link]()`.
|
|
553
|
+
if (this.pos === savepos && C_CLOSE_PAREN !== c)
|
|
554
|
+
return null;
|
|
555
|
+
if (0 !== openparens)
|
|
556
|
+
return null;
|
|
557
|
+
return (0, common_ts_1.unescapeString)(this.subject.slice(savepos, this.pos));
|
|
558
|
+
}
|
|
559
|
+
/** Length of the link label at `pos`, including brackets; 0 if invalid. */
|
|
560
|
+
parseLinkLabel() {
|
|
561
|
+
const m = this.match(reLinkLabel);
|
|
562
|
+
// §4.7: at most 999 characters between the brackets.
|
|
563
|
+
if (null === m || m.length > 1001)
|
|
564
|
+
return 0;
|
|
565
|
+
return m.length;
|
|
566
|
+
}
|
|
567
|
+
addBracket(node, index, image) {
|
|
568
|
+
if (null !== this.brackets)
|
|
569
|
+
this.brackets.bracketAfter = true;
|
|
570
|
+
this.brackets = {
|
|
571
|
+
node,
|
|
572
|
+
previous: this.brackets,
|
|
573
|
+
previousDelimiter: this.delimiters,
|
|
574
|
+
index,
|
|
575
|
+
image,
|
|
576
|
+
active: true,
|
|
577
|
+
bracketAfter: false,
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
removeBracket() {
|
|
581
|
+
if (null !== this.brackets)
|
|
582
|
+
this.brackets = this.brackets.previous;
|
|
583
|
+
}
|
|
584
|
+
parseOpenBracket(block) {
|
|
585
|
+
const startpos = this.pos;
|
|
586
|
+
this.pos += 1;
|
|
587
|
+
const node = text('[');
|
|
588
|
+
block.appendChild(node);
|
|
589
|
+
this.addBracket(node, startpos, false);
|
|
590
|
+
return true;
|
|
591
|
+
}
|
|
592
|
+
/** `!` only matters when it introduces an image. */
|
|
593
|
+
parseBang(block) {
|
|
594
|
+
const startpos = this.pos;
|
|
595
|
+
this.pos += 1;
|
|
596
|
+
if (C_OPEN_BRACKET === this.peek()) {
|
|
597
|
+
this.pos += 1;
|
|
598
|
+
const node = text('![');
|
|
599
|
+
block.appendChild(node);
|
|
600
|
+
this.addBracket(node, startpos + 1, true);
|
|
601
|
+
}
|
|
602
|
+
else {
|
|
603
|
+
block.appendChild(text('!'));
|
|
604
|
+
}
|
|
605
|
+
return true;
|
|
606
|
+
}
|
|
607
|
+
/**
|
|
608
|
+
* The spec's "look for link or image" procedure. On a `]`, walk back to
|
|
609
|
+
* the newest bracket opener and try, in order: an inline `(dest "title")`,
|
|
610
|
+
* a full `[label]`, a collapsed `[]` or a shortcut reference. Anything that
|
|
611
|
+
* fails backtracks to just after the `]` and leaves a literal `]`.
|
|
612
|
+
*/
|
|
613
|
+
parseCloseBracket(block) {
|
|
614
|
+
this.pos += 1;
|
|
615
|
+
const startpos = this.pos;
|
|
616
|
+
const opener = this.brackets;
|
|
617
|
+
if (null === opener) {
|
|
618
|
+
block.appendChild(text(']'));
|
|
619
|
+
return true;
|
|
620
|
+
}
|
|
621
|
+
if (!opener.active) {
|
|
622
|
+
// Deactivated by an enclosing link: not an opener any more.
|
|
623
|
+
block.appendChild(text(']'));
|
|
624
|
+
this.removeBracket();
|
|
625
|
+
return true;
|
|
626
|
+
}
|
|
627
|
+
const isImage = opener.image;
|
|
628
|
+
let dest = null;
|
|
629
|
+
let title = null;
|
|
630
|
+
let matched = false;
|
|
631
|
+
const savepos = this.pos;
|
|
632
|
+
// Inline link: `](` destination title `)`.
|
|
633
|
+
if (C_OPEN_PAREN === this.peek()) {
|
|
634
|
+
this.pos += 1;
|
|
635
|
+
this.spnl();
|
|
636
|
+
const d = this.parseLinkDestination();
|
|
637
|
+
if (null !== d) {
|
|
638
|
+
this.spnl();
|
|
639
|
+
// A title has to be separated from the destination by whitespace,
|
|
640
|
+
// so `[a](/url"t")` is not a titled link.
|
|
641
|
+
let t = null;
|
|
642
|
+
if (reWhitespaceChar.test(this.subject.charAt(this.pos - 1))) {
|
|
643
|
+
t = this.parseLinkTitle();
|
|
644
|
+
}
|
|
645
|
+
this.spnl();
|
|
646
|
+
if (C_CLOSE_PAREN === this.peek()) {
|
|
647
|
+
this.pos += 1;
|
|
648
|
+
dest = d;
|
|
649
|
+
title = t;
|
|
650
|
+
matched = true;
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
if (!matched)
|
|
654
|
+
this.pos = savepos;
|
|
655
|
+
}
|
|
656
|
+
if (!matched) {
|
|
657
|
+
// Reference forms. A second label gives the full form; an empty or
|
|
658
|
+
// absent one means the link text is itself the label (collapsed and
|
|
659
|
+
// shortcut) — which is only possible if that text holds no bracket.
|
|
660
|
+
let reflabel = null;
|
|
661
|
+
const beforelabel = this.pos;
|
|
662
|
+
const n = this.parseLinkLabel();
|
|
663
|
+
if (n > 2) {
|
|
664
|
+
reflabel = this.subject.slice(beforelabel + 1, beforelabel + n - 1);
|
|
665
|
+
}
|
|
666
|
+
else if (!opener.bracketAfter) {
|
|
667
|
+
reflabel = this.subject.slice(opener.index + 1, startpos - 1);
|
|
668
|
+
}
|
|
669
|
+
if (0 === n)
|
|
670
|
+
this.pos = savepos;
|
|
671
|
+
if (null !== reflabel) {
|
|
672
|
+
const link = this.refmap[(0, common_ts_1.normalizeReference)(reflabel)];
|
|
673
|
+
if (undefined !== link) {
|
|
674
|
+
dest = link.destination;
|
|
675
|
+
title = link.title;
|
|
676
|
+
matched = true;
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
if (!matched) {
|
|
681
|
+
this.removeBracket();
|
|
682
|
+
this.pos = startpos;
|
|
683
|
+
block.appendChild(text(']'));
|
|
684
|
+
return true;
|
|
685
|
+
}
|
|
686
|
+
const node = new node_ts_1.MdNode(isImage ? 'image' : 'link');
|
|
687
|
+
node.destination = dest;
|
|
688
|
+
node.title = null === title || '' === title ? null : title;
|
|
689
|
+
let tmp = opener.node.next;
|
|
690
|
+
while (null !== tmp) {
|
|
691
|
+
const nxt = tmp.next;
|
|
692
|
+
tmp.unlink();
|
|
693
|
+
node.appendChild(tmp);
|
|
694
|
+
tmp = nxt;
|
|
695
|
+
}
|
|
696
|
+
block.appendChild(node);
|
|
697
|
+
// Emphasis inside the link text resolves now, and only down to the
|
|
698
|
+
// delimiter that was current when the `[` opened.
|
|
699
|
+
this.processEmphasis(opener.previousDelimiter);
|
|
700
|
+
this.removeBracket();
|
|
701
|
+
opener.node.unlink();
|
|
702
|
+
// Links may not contain links (§6.3): every earlier link opener is
|
|
703
|
+
// spent. Image openers survive, since images may nest in links.
|
|
704
|
+
if (!isImage) {
|
|
705
|
+
let b = this.brackets;
|
|
706
|
+
while (null !== b) {
|
|
707
|
+
if (!b.image)
|
|
708
|
+
b.active = false;
|
|
709
|
+
b = b.previous;
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
return true;
|
|
713
|
+
}
|
|
714
|
+
// --- §6.2 entities, §6.8/§6.9 breaks and plain text ---
|
|
715
|
+
parseEntity(block) {
|
|
716
|
+
const m = this.match(reEntityHere);
|
|
717
|
+
if (null === m)
|
|
718
|
+
return false;
|
|
719
|
+
block.appendChild(text((0, common_ts_1.decodeEntity)(m)));
|
|
720
|
+
return true;
|
|
721
|
+
}
|
|
722
|
+
parseString(block) {
|
|
723
|
+
const m = this.match(this.options.gfm ? reMainGfm : reMain);
|
|
724
|
+
if (null === m)
|
|
725
|
+
return false;
|
|
726
|
+
block.appendChild(text(m));
|
|
727
|
+
return true;
|
|
728
|
+
}
|
|
729
|
+
/**
|
|
730
|
+
* §6.7/§6.8: two or more spaces before the line ending make a hard break,
|
|
731
|
+
* anything else a soft one. Trailing spaces are dropped either way, as is
|
|
732
|
+
* the indentation of the next line.
|
|
733
|
+
*/
|
|
734
|
+
parseNewline(block) {
|
|
735
|
+
this.pos += 1;
|
|
736
|
+
const lastc = block.lastChild;
|
|
737
|
+
if (null !== lastc &&
|
|
738
|
+
'text' === lastc.type &&
|
|
739
|
+
null !== lastc.literal &&
|
|
740
|
+
' ' === lastc.literal.charAt(lastc.literal.length - 1)) {
|
|
741
|
+
const hardbreak = ' ' === lastc.literal.charAt(lastc.literal.length - 2);
|
|
742
|
+
lastc.literal = lastc.literal.replace(reFinalSpace, '');
|
|
743
|
+
block.appendChild(new node_ts_1.MdNode(hardbreak ? 'linebreak' : 'softbreak'));
|
|
744
|
+
}
|
|
745
|
+
else {
|
|
746
|
+
block.appendChild(new node_ts_1.MdNode('softbreak'));
|
|
747
|
+
}
|
|
748
|
+
this.match(reInitialSpace);
|
|
749
|
+
return true;
|
|
750
|
+
}
|
|
751
|
+
/** One inline construct at `pos`; false only at the end of the subject. */
|
|
752
|
+
parseInline(block) {
|
|
753
|
+
const c = this.peek();
|
|
754
|
+
if (-1 === c)
|
|
755
|
+
return false;
|
|
756
|
+
let res = false;
|
|
757
|
+
switch (c) {
|
|
758
|
+
case C_NEWLINE:
|
|
759
|
+
res = this.parseNewline(block);
|
|
760
|
+
break;
|
|
761
|
+
case C_BACKSLASH:
|
|
762
|
+
res = this.parseBackslash(block);
|
|
763
|
+
break;
|
|
764
|
+
case C_BACKTICK:
|
|
765
|
+
res = this.parseBackticks(block);
|
|
766
|
+
break;
|
|
767
|
+
case C_ASTERISK:
|
|
768
|
+
case C_UNDERSCORE:
|
|
769
|
+
res = this.handleDelim(c, block);
|
|
770
|
+
break;
|
|
771
|
+
case C_TILDE:
|
|
772
|
+
// Only GFM gives `~` a meaning; otherwise it is ordinary text (and
|
|
773
|
+
// the non-GFM `reMain` lets parseString swallow whole runs of it).
|
|
774
|
+
res = this.options.gfm ? this.handleDelim(c, block) : this.parseString(block);
|
|
775
|
+
break;
|
|
776
|
+
case C_OPEN_BRACKET:
|
|
777
|
+
res = this.parseOpenBracket(block);
|
|
778
|
+
break;
|
|
779
|
+
case C_BANG:
|
|
780
|
+
res = this.parseBang(block);
|
|
781
|
+
break;
|
|
782
|
+
case C_CLOSE_BRACKET:
|
|
783
|
+
res = this.parseCloseBracket(block);
|
|
784
|
+
break;
|
|
785
|
+
case C_LESSTHAN:
|
|
786
|
+
res = this.parseAutolink(block) || this.parseHtmlTag(block);
|
|
787
|
+
break;
|
|
788
|
+
case C_AMPERSAND:
|
|
789
|
+
res = this.parseEntity(block);
|
|
790
|
+
break;
|
|
791
|
+
default:
|
|
792
|
+
res = this.parseString(block);
|
|
793
|
+
break;
|
|
794
|
+
}
|
|
795
|
+
if (!res) {
|
|
796
|
+
// Nothing claimed it: the character is literal text.
|
|
797
|
+
this.pos += 1;
|
|
798
|
+
block.appendChild(text(String.fromCharCode(c)));
|
|
799
|
+
}
|
|
800
|
+
return true;
|
|
801
|
+
}
|
|
802
|
+
/** Parse one paragraph or heading's raw content into inline children. */
|
|
803
|
+
parse(block) {
|
|
804
|
+
// Trimming is what stops trailing spaces at the end of a block from
|
|
805
|
+
// making a hard break, and drops the leading indentation of the first line.
|
|
806
|
+
this.subject = block.stringContent.trim();
|
|
807
|
+
this.pos = 0;
|
|
808
|
+
this.delimiters = null;
|
|
809
|
+
this.brackets = null;
|
|
810
|
+
while (this.parseInline(block))
|
|
811
|
+
;
|
|
812
|
+
block.stringContent = '';
|
|
813
|
+
this.processEmphasis(null);
|
|
814
|
+
}
|
|
815
|
+
/**
|
|
816
|
+
* §4.7: consume leading link reference definitions. Called by the block
|
|
817
|
+
* phase while finalizing a paragraph, repeatedly, until it returns 0.
|
|
818
|
+
*/
|
|
819
|
+
parseReference(s) {
|
|
820
|
+
this.subject = s;
|
|
821
|
+
this.pos = 0;
|
|
822
|
+
const startpos = this.pos;
|
|
823
|
+
// §4.7 allows up to three spaces of indentation before the label. The
|
|
824
|
+
// block phase normally strips a paragraph line's indentation before it
|
|
825
|
+
// gets here, so this usually consumes nothing.
|
|
826
|
+
this.match(reIndent);
|
|
827
|
+
const labelStart = this.pos;
|
|
828
|
+
const matchChars = this.parseLinkLabel();
|
|
829
|
+
if (0 === matchChars)
|
|
830
|
+
return 0;
|
|
831
|
+
const rawlabel = this.subject.slice(labelStart + 1, labelStart + matchChars - 1);
|
|
832
|
+
if (C_COLON === this.peek()) {
|
|
833
|
+
this.pos += 1;
|
|
834
|
+
}
|
|
835
|
+
else {
|
|
836
|
+
this.pos = startpos;
|
|
837
|
+
return 0;
|
|
838
|
+
}
|
|
839
|
+
this.spnl();
|
|
840
|
+
const dest = this.parseLinkDestination();
|
|
841
|
+
if (null === dest) {
|
|
842
|
+
this.pos = startpos;
|
|
843
|
+
return 0;
|
|
844
|
+
}
|
|
845
|
+
const beforetitle = this.pos;
|
|
846
|
+
this.spnl();
|
|
847
|
+
let title = null;
|
|
848
|
+
if (this.pos !== beforetitle) {
|
|
849
|
+
title = this.parseLinkTitle();
|
|
850
|
+
}
|
|
851
|
+
if (null === title) {
|
|
852
|
+
this.pos = beforetitle;
|
|
853
|
+
}
|
|
854
|
+
// Nothing but spaces may follow on the definition's last line.
|
|
855
|
+
let atLineEnd = true;
|
|
856
|
+
if (null === this.match(reSpaceAtEndOfLine)) {
|
|
857
|
+
if (null === title) {
|
|
858
|
+
atLineEnd = false;
|
|
859
|
+
}
|
|
860
|
+
else {
|
|
861
|
+
// What looked like a title is something else; the definition may
|
|
862
|
+
// still be valid without it.
|
|
863
|
+
title = null;
|
|
864
|
+
this.pos = beforetitle;
|
|
865
|
+
atLineEnd = null !== this.match(reSpaceAtEndOfLine);
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
if (!atLineEnd) {
|
|
869
|
+
this.pos = startpos;
|
|
870
|
+
return 0;
|
|
871
|
+
}
|
|
872
|
+
const normlabel = (0, common_ts_1.normalizeReference)(rawlabel);
|
|
873
|
+
if ('' === normlabel) {
|
|
874
|
+
// A label must hold at least one non-whitespace character.
|
|
875
|
+
this.pos = startpos;
|
|
876
|
+
return 0;
|
|
877
|
+
}
|
|
878
|
+
// First definition wins.
|
|
879
|
+
if (undefined === this.refmap[normlabel]) {
|
|
880
|
+
this.refmap[normlabel] = { destination: dest, title };
|
|
881
|
+
}
|
|
882
|
+
return this.pos - startpos;
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
// --- GFM extended autolinks (autolink literals) -----------------------------
|
|
886
|
+
//
|
|
887
|
+
// GFM recognises `www.x.com`, `https://x.com` and `a@x.com` without the `<…>`
|
|
888
|
+
// CommonMark requires. This runs as a post-pass over the finished inline tree
|
|
889
|
+
// rather than as another branch of the scan above, which is how cmark-gfm does
|
|
890
|
+
// it and is the only shape that keeps 652/652 safe: the scanner that decides
|
|
891
|
+
// code spans, raw HTML, emphasis and links is not touched at all, and an
|
|
892
|
+
// autolink can therefore never win against a construct CommonMark says comes
|
|
893
|
+
// first.
|
|
894
|
+
//
|
|
895
|
+
// The pass rewrites `text` nodes into text/link/text runs. It does not descend
|
|
896
|
+
// into `link` (links may not contain links) or `image` (its children are the
|
|
897
|
+
// alt text), and `code`/`html_inline` are leaves it never looks inside.
|
|
898
|
+
/** RFC 1035's limit on a fully-qualified domain name. See `scanDomainEnd`. */
|
|
899
|
+
const MAX_AUTOLINK_DOMAIN = 253;
|
|
900
|
+
/** RFC 5321's limit on the local part of an address, before the `@`. */
|
|
901
|
+
const MAX_EMAIL_LOCAL = 64;
|
|
902
|
+
/** The three schemes the extension recognises without angle brackets. */
|
|
903
|
+
const AUTOLINK_SCHEMES = ['https://', 'http://', 'ftp://'];
|
|
904
|
+
function isAsciiAlnum(c) {
|
|
905
|
+
return (c >= 48 && c <= 57) || (c >= 65 && c <= 90) || (c >= 97 && c <= 122);
|
|
906
|
+
}
|
|
907
|
+
function isAutolinkSpace(c) {
|
|
908
|
+
return (C_SPACE === c ||
|
|
909
|
+
C_TAB === c ||
|
|
910
|
+
C_NEWLINE === c ||
|
|
911
|
+
C_VTAB === c ||
|
|
912
|
+
C_FORMFEED === c ||
|
|
913
|
+
C_CARRIAGE_RETURN === c);
|
|
914
|
+
}
|
|
915
|
+
/** A domain segment holds alphanumerics, `_` and `-`; `.` separates them. */
|
|
916
|
+
function isDomainChar(c) {
|
|
917
|
+
return isAsciiAlnum(c) || C_UNDERSCORE === c || C_HYPHEN === c || C_PERIOD === c;
|
|
918
|
+
}
|
|
919
|
+
/** Before the `@`: alphanumerics and `.`, `-`, `_`, `+`. */
|
|
920
|
+
function isEmailLocalChar(c) {
|
|
921
|
+
return isAsciiAlnum(c) || C_PERIOD === c || C_HYPHEN === c || C_UNDERSCORE === c || C_PLUS === c;
|
|
922
|
+
}
|
|
923
|
+
/**
|
|
924
|
+
* "All such recognized autolinks can only come at the beginning of a line,
|
|
925
|
+
* after whitespace, or any of the delimiting characters `*`, `_`, `~`, `(`."
|
|
926
|
+
*
|
|
927
|
+
* Offset 0 has no preceding character in `s` at all, so the caller answers for
|
|
928
|
+
* it through `atStart` — see `startsAfterDelimiter`.
|
|
929
|
+
*/
|
|
930
|
+
function isAutolinkStart(s, i, atStart) {
|
|
931
|
+
if (0 === i)
|
|
932
|
+
return atStart;
|
|
933
|
+
const c = s.charCodeAt(i - 1);
|
|
934
|
+
return (isAutolinkSpace(c) ||
|
|
935
|
+
C_ASTERISK === c ||
|
|
936
|
+
C_UNDERSCORE === c ||
|
|
937
|
+
C_TILDE === c ||
|
|
938
|
+
C_OPEN_PAREN === c);
|
|
939
|
+
}
|
|
940
|
+
/**
|
|
941
|
+
* Whether offset 0 of a text node may begin an autolink, given the sibling in
|
|
942
|
+
* front of it.
|
|
943
|
+
*
|
|
944
|
+
* The rule above is about the source character immediately before the match,
|
|
945
|
+
* and by the time this pass runs the tree no longer carries source offsets —
|
|
946
|
+
* but the previous sibling's *type* names that character exactly:
|
|
947
|
+
*
|
|
948
|
+
* * `emph` and `strong` end on the `*` or `_` that closed them, and `del` on
|
|
949
|
+
* its second `~`. All three are delimiters the rule allows, so
|
|
950
|
+
* `*a*www.b.com` links.
|
|
951
|
+
* * `code` ends on a backtick, `link` and `image` on `)`, `]` or `>`, and
|
|
952
|
+
* `html_inline` on `>`. None are delimiters, so `` `x`www.a.com ``,
|
|
953
|
+
* `[l](/u)www.a.com` and `<b>www.a.com` do not link.
|
|
954
|
+
* * a `softbreak` or `linebreak` puts offset 0 at the beginning of a line,
|
|
955
|
+
* and so does having no previous sibling — that is the first inline of the
|
|
956
|
+
* block, or of the emphasis span this pass descended into.
|
|
957
|
+
*
|
|
958
|
+
* A `text` node never appears here: `linkifyChildren` merges every run of
|
|
959
|
+
* adjacent text siblings before handing the first of them over.
|
|
960
|
+
*/
|
|
961
|
+
function startsAfterDelimiter(prev) {
|
|
962
|
+
if (null === prev)
|
|
963
|
+
return true;
|
|
964
|
+
const t = prev.type;
|
|
965
|
+
return 'softbreak' === t || 'linebreak' === t || 'emph' === t || 'strong' === t || 'del' === t;
|
|
966
|
+
}
|
|
967
|
+
/** ASCII case-insensitive comparison against an already-lowercase literal. */
|
|
968
|
+
function matchesIgnoreCase(s, at, lower) {
|
|
969
|
+
if (at + lower.length > s.length)
|
|
970
|
+
return false;
|
|
971
|
+
for (let k = 0; k < lower.length; k++) {
|
|
972
|
+
let c = s.charCodeAt(at + k);
|
|
973
|
+
if (c >= 65 && c <= 90)
|
|
974
|
+
c += 32;
|
|
975
|
+
if (c !== lower.charCodeAt(k))
|
|
976
|
+
return false;
|
|
977
|
+
}
|
|
978
|
+
return true;
|
|
979
|
+
}
|
|
980
|
+
/**
|
|
981
|
+
* End of the run of domain characters at `from`, capped at
|
|
982
|
+
* `MAX_AUTOLINK_DOMAIN`.
|
|
983
|
+
*
|
|
984
|
+
* The cap is what keeps this pass linear. `_` both continues a domain and
|
|
985
|
+
* introduces a valid autolink start, so without a bound an input like
|
|
986
|
+
* `'_www.a_b.c'.repeat(n)` gives every underscore a candidate whose domain run
|
|
987
|
+
* reaches the end of the text — quadratic. Anything past the cap is still
|
|
988
|
+
* scanned, just as the path rather than as the domain, and no real domain
|
|
989
|
+
* comes near it.
|
|
990
|
+
*/
|
|
991
|
+
function scanDomainEnd(s, from) {
|
|
992
|
+
const max = Math.min(s.length, from + MAX_AUTOLINK_DOMAIN);
|
|
993
|
+
let i = from;
|
|
994
|
+
while (i < max && isDomainChar(s.charCodeAt(i)))
|
|
995
|
+
i++;
|
|
996
|
+
return i;
|
|
997
|
+
}
|
|
998
|
+
/**
|
|
999
|
+
* A valid domain: at least one period, and no underscore in either of the last
|
|
1000
|
+
* two segments. `from`..`to` must already be a run of domain characters.
|
|
1001
|
+
*/
|
|
1002
|
+
function isValidDomain(s, from, to) {
|
|
1003
|
+
if (to <= from)
|
|
1004
|
+
return false;
|
|
1005
|
+
let periods = 0;
|
|
1006
|
+
let underscoresInLast = 0;
|
|
1007
|
+
let underscoresInPrev = 0;
|
|
1008
|
+
for (let i = from; i < to; i++) {
|
|
1009
|
+
const c = s.charCodeAt(i);
|
|
1010
|
+
if (C_UNDERSCORE === c) {
|
|
1011
|
+
underscoresInLast++;
|
|
1012
|
+
}
|
|
1013
|
+
else if (C_PERIOD === c) {
|
|
1014
|
+
underscoresInPrev = underscoresInLast;
|
|
1015
|
+
underscoresInLast = 0;
|
|
1016
|
+
periods++;
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
return 0 < periods && 0 === underscoresInLast && 0 === underscoresInPrev;
|
|
1020
|
+
}
|
|
1021
|
+
/**
|
|
1022
|
+
* "Extended autolink path validation" — pull the end of the match back off
|
|
1023
|
+
* trailing characters that read as sentence punctuation rather than as part of
|
|
1024
|
+
* a URL. One loop rather than three passes, because dropping a `)` can expose
|
|
1025
|
+
* a `.` and vice versa.
|
|
1026
|
+
*
|
|
1027
|
+
* * `?` `!` `.` `,` `:` `*` `_` `~` are dropped wherever they trail;
|
|
1028
|
+
* * a trailing `)` is dropped only while the match holds more `)` than `(`,
|
|
1029
|
+
* so `(www.a.com/x_(y))` keeps its inner pair and loses the outer one;
|
|
1030
|
+
* * a trailing `;` preceded by something shaped like an entity reference
|
|
1031
|
+
* takes the whole `&…;` with it.
|
|
1032
|
+
*
|
|
1033
|
+
* The parenthesis counts are taken once and then maintained, not recomputed
|
|
1034
|
+
* per drop: a deeply parenthesised URL is otherwise quadratic in its own
|
|
1035
|
+
* length.
|
|
1036
|
+
*/
|
|
1037
|
+
function trimAutolinkEnd(s, start, end) {
|
|
1038
|
+
let opening = -1;
|
|
1039
|
+
let closing = -1;
|
|
1040
|
+
while (start < end) {
|
|
1041
|
+
const c = s.charCodeAt(end - 1);
|
|
1042
|
+
if (C_QUESTION === c ||
|
|
1043
|
+
C_BANG === c ||
|
|
1044
|
+
C_PERIOD === c ||
|
|
1045
|
+
C_COMMA === c ||
|
|
1046
|
+
C_COLON === c ||
|
|
1047
|
+
C_ASTERISK === c ||
|
|
1048
|
+
C_UNDERSCORE === c ||
|
|
1049
|
+
C_TILDE === c) {
|
|
1050
|
+
end--;
|
|
1051
|
+
continue;
|
|
1052
|
+
}
|
|
1053
|
+
if (C_CLOSE_PAREN === c) {
|
|
1054
|
+
if (opening < 0) {
|
|
1055
|
+
opening = 0;
|
|
1056
|
+
closing = 0;
|
|
1057
|
+
for (let i = start; i < end; i++) {
|
|
1058
|
+
const d = s.charCodeAt(i);
|
|
1059
|
+
if (C_OPEN_PAREN === d)
|
|
1060
|
+
opening++;
|
|
1061
|
+
else if (C_CLOSE_PAREN === d)
|
|
1062
|
+
closing++;
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
if (closing <= opening)
|
|
1066
|
+
return end;
|
|
1067
|
+
closing--;
|
|
1068
|
+
end--;
|
|
1069
|
+
continue;
|
|
1070
|
+
}
|
|
1071
|
+
if (C_SEMICOLON === c) {
|
|
1072
|
+
// `&` followed by one or more alphanumerics, then this `;`.
|
|
1073
|
+
let j = end - 2;
|
|
1074
|
+
while (j > start && isAsciiAlnum(s.charCodeAt(j)))
|
|
1075
|
+
j--;
|
|
1076
|
+
if (j < end - 2 && C_AMPERSAND === s.charCodeAt(j)) {
|
|
1077
|
+
end = j;
|
|
1078
|
+
continue;
|
|
1079
|
+
}
|
|
1080
|
+
return end;
|
|
1081
|
+
}
|
|
1082
|
+
return end;
|
|
1083
|
+
}
|
|
1084
|
+
return end;
|
|
1085
|
+
}
|
|
1086
|
+
/**
|
|
1087
|
+
* Every autolink literal in one text run, left to right and non-overlapping.
|
|
1088
|
+
* Returns null when there is nothing to do, which is the overwhelmingly common
|
|
1089
|
+
* case and saves the caller an array and a splice.
|
|
1090
|
+
*/
|
|
1091
|
+
function scanAutolinks(s, atStart) {
|
|
1092
|
+
const len = s.length;
|
|
1093
|
+
let out = null;
|
|
1094
|
+
// The maximal run of non-space, non-`<` characters containing the current
|
|
1095
|
+
// position — "zero or more non-space non-`<` characters may follow" the
|
|
1096
|
+
// domain. Cached because candidates are tried left to right, so each run is
|
|
1097
|
+
// scanned once however many candidates start inside it.
|
|
1098
|
+
let runStart = -1;
|
|
1099
|
+
let runEnd = -1;
|
|
1100
|
+
let i = 0;
|
|
1101
|
+
let lastEnd = 0;
|
|
1102
|
+
while (i < len) {
|
|
1103
|
+
const c = s.charCodeAt(i);
|
|
1104
|
+
// --- email: found by its `@`, then rewound to the start of the local part
|
|
1105
|
+
if (C_AT === c) {
|
|
1106
|
+
// The rewind is bounded to keep this pass linear, and RFC 5321 bounds a
|
|
1107
|
+
// local part in the same place anyway, so the cap costs no real address.
|
|
1108
|
+
const capped = i - MAX_EMAIL_LOCAL;
|
|
1109
|
+
const floor = Math.max(lastEnd, capped);
|
|
1110
|
+
let k = i;
|
|
1111
|
+
while (k > floor && isEmailLocalChar(s.charCodeAt(k - 1)))
|
|
1112
|
+
k--;
|
|
1113
|
+
// Stopping *on* the cap is not the same as finding a boundary. If the
|
|
1114
|
+
// character just outside it still belongs to the local part, the local
|
|
1115
|
+
// part is longer than the cap allows and there is no address here at
|
|
1116
|
+
// all: accepting the match would link the 64-character tail of a longer
|
|
1117
|
+
// run and leave the rest as text, inventing an address the source never
|
|
1118
|
+
// wrote. `k` can only reach `capped` when the cap, rather than `lastEnd`,
|
|
1119
|
+
// is what ended the loop; and `k` of 0 is the start of the string, where
|
|
1120
|
+
// there is no character outside the cap to disqualify anything.
|
|
1121
|
+
if (k === capped && 0 < k && isEmailLocalChar(s.charCodeAt(k - 1))) {
|
|
1122
|
+
i++;
|
|
1123
|
+
continue;
|
|
1124
|
+
}
|
|
1125
|
+
if (k < i && isAutolinkStart(s, k, atStart)) {
|
|
1126
|
+
// After the `@`: alphanumerics, `.`, `-`, `_`; at least one period; a
|
|
1127
|
+
// trailing `.` is not part of the address, and a trailing `-` or `_`
|
|
1128
|
+
// invalidates the whole thing.
|
|
1129
|
+
let end = i + 1;
|
|
1130
|
+
while (end < len && isDomainChar(s.charCodeAt(end)))
|
|
1131
|
+
end++;
|
|
1132
|
+
while (end > i + 1 && C_PERIOD === s.charCodeAt(end - 1))
|
|
1133
|
+
end--;
|
|
1134
|
+
const last = end > i + 1 ? s.charCodeAt(end - 1) : 0;
|
|
1135
|
+
if (end > i + 1 &&
|
|
1136
|
+
C_HYPHEN !== last &&
|
|
1137
|
+
C_UNDERSCORE !== last &&
|
|
1138
|
+
hasPeriod(s, i + 1, end)) {
|
|
1139
|
+
if (null === out)
|
|
1140
|
+
out = [];
|
|
1141
|
+
out.push({ start: k, end, href: 'mailto:' + s.slice(k, end) });
|
|
1142
|
+
lastEnd = end;
|
|
1143
|
+
i = end;
|
|
1144
|
+
continue;
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
i++;
|
|
1148
|
+
continue;
|
|
1149
|
+
}
|
|
1150
|
+
// --- www / scheme: both need a valid start and a valid domain
|
|
1151
|
+
if (!couldStartUrl(c) || !isAutolinkStart(s, i, atStart)) {
|
|
1152
|
+
i++;
|
|
1153
|
+
continue;
|
|
1154
|
+
}
|
|
1155
|
+
let domainStart = -1;
|
|
1156
|
+
let prefix = '';
|
|
1157
|
+
if (matchesIgnoreCase(s, i, 'www.')) {
|
|
1158
|
+
// The domain of a `www.` autolink includes the `www.` itself, so the
|
|
1159
|
+
// required period is already there and `www.foo_bar.com` is rejected on
|
|
1160
|
+
// the same footing as `http://foo_bar.com`.
|
|
1161
|
+
domainStart = i;
|
|
1162
|
+
prefix = 'http://';
|
|
1163
|
+
}
|
|
1164
|
+
else {
|
|
1165
|
+
for (const scheme of AUTOLINK_SCHEMES) {
|
|
1166
|
+
if (matchesIgnoreCase(s, i, scheme)) {
|
|
1167
|
+
domainStart = i + scheme.length;
|
|
1168
|
+
break;
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
if (0 > domainStart) {
|
|
1173
|
+
i++;
|
|
1174
|
+
continue;
|
|
1175
|
+
}
|
|
1176
|
+
const domainEnd = scanDomainEnd(s, domainStart);
|
|
1177
|
+
// Checked before the run scan below, so a candidate that cannot possibly
|
|
1178
|
+
// match costs only its own domain run.
|
|
1179
|
+
if (!isValidDomain(s, domainStart, domainEnd)) {
|
|
1180
|
+
i++;
|
|
1181
|
+
continue;
|
|
1182
|
+
}
|
|
1183
|
+
if (i >= runEnd || i < runStart) {
|
|
1184
|
+
runStart = i;
|
|
1185
|
+
runEnd = i;
|
|
1186
|
+
while (runEnd < len) {
|
|
1187
|
+
const d = s.charCodeAt(runEnd);
|
|
1188
|
+
if (isAutolinkSpace(d) || C_LESSTHAN === d)
|
|
1189
|
+
break;
|
|
1190
|
+
runEnd++;
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
const end = trimAutolinkEnd(s, i, runEnd);
|
|
1194
|
+
// Trailing punctuation can eat back into the domain — `www.commonmark.org.`
|
|
1195
|
+
// loses its last period — so the domain is validated again over what
|
|
1196
|
+
// actually survived.
|
|
1197
|
+
if (end <= domainStart || !isValidDomain(s, domainStart, Math.min(domainEnd, end))) {
|
|
1198
|
+
i++;
|
|
1199
|
+
continue;
|
|
1200
|
+
}
|
|
1201
|
+
if (null === out)
|
|
1202
|
+
out = [];
|
|
1203
|
+
out.push({ start: i, end, href: prefix + s.slice(i, end) });
|
|
1204
|
+
lastEnd = end;
|
|
1205
|
+
i = end;
|
|
1206
|
+
}
|
|
1207
|
+
return out;
|
|
1208
|
+
}
|
|
1209
|
+
/** `www.`, `http://`, `https://` and `ftp://` all start with one of these. */
|
|
1210
|
+
function couldStartUrl(c) {
|
|
1211
|
+
return 119 === c || 87 === c || 104 === c || 72 === c || 102 === c || 70 === c;
|
|
1212
|
+
}
|
|
1213
|
+
function hasPeriod(s, from, to) {
|
|
1214
|
+
for (let i = from; i < to; i++) {
|
|
1215
|
+
if (C_PERIOD === s.charCodeAt(i))
|
|
1216
|
+
return true;
|
|
1217
|
+
}
|
|
1218
|
+
return false;
|
|
1219
|
+
}
|
|
1220
|
+
/** Replace one text node with the text/link/text run its literal implies. */
|
|
1221
|
+
function linkifyTextNode(node) {
|
|
1222
|
+
const s = node.literal ?? '';
|
|
1223
|
+
const matches = scanAutolinks(s, startsAfterDelimiter(node.prev));
|
|
1224
|
+
if (null === matches)
|
|
1225
|
+
return;
|
|
1226
|
+
let at = 0;
|
|
1227
|
+
for (const m of matches) {
|
|
1228
|
+
if (at < m.start)
|
|
1229
|
+
node.insertBefore(text(s.slice(at, m.start)));
|
|
1230
|
+
const link = new node_ts_1.MdNode('link');
|
|
1231
|
+
// Not percent-encoded here, for the same reason as parseLinkDestination:
|
|
1232
|
+
// encoding belongs to the renderer, and the AST promises the decoded form.
|
|
1233
|
+
link.destination = m.href;
|
|
1234
|
+
link.title = null;
|
|
1235
|
+
link.appendChild(text(s.slice(m.start, m.end)));
|
|
1236
|
+
node.insertBefore(link);
|
|
1237
|
+
at = m.end;
|
|
1238
|
+
}
|
|
1239
|
+
if (at < s.length)
|
|
1240
|
+
node.insertBefore(text(s.slice(at)));
|
|
1241
|
+
node.unlink();
|
|
1242
|
+
}
|
|
1243
|
+
/**
|
|
1244
|
+
* Autolink the text children of one container, and report the child containers
|
|
1245
|
+
* still to visit.
|
|
1246
|
+
*/
|
|
1247
|
+
function linkifyChildren(parent, pending) {
|
|
1248
|
+
let child = parent.firstChild;
|
|
1249
|
+
while (null !== child) {
|
|
1250
|
+
if ('text' === child.type) {
|
|
1251
|
+
// Consolidate the run of adjacent text nodes first. The scan above emits
|
|
1252
|
+
// a node per delimiter run and per character it could not claim, so
|
|
1253
|
+
// `a.b-c_d@a.b` arrives as three siblings and the address is only
|
|
1254
|
+
// visible once they are one string. Merging changes nothing downstream:
|
|
1255
|
+
// the renderer writes text literals back to back, and the AST projection
|
|
1256
|
+
// already coalesces adjacent runs.
|
|
1257
|
+
let sibling = child.next;
|
|
1258
|
+
if (null !== sibling && 'text' === sibling.type) {
|
|
1259
|
+
let merged = child.literal ?? '';
|
|
1260
|
+
while (null !== sibling && 'text' === sibling.type) {
|
|
1261
|
+
merged += sibling.literal ?? '';
|
|
1262
|
+
const next = sibling.next;
|
|
1263
|
+
sibling.unlink();
|
|
1264
|
+
sibling = next;
|
|
1265
|
+
}
|
|
1266
|
+
child.literal = merged;
|
|
1267
|
+
}
|
|
1268
|
+
const after = child.next;
|
|
1269
|
+
linkifyTextNode(child);
|
|
1270
|
+
child = after;
|
|
1271
|
+
continue;
|
|
1272
|
+
}
|
|
1273
|
+
// No links inside links (§6.3), and an image's children are its alt text.
|
|
1274
|
+
if ('link' !== child.type && 'image' !== child.type && child.isContainer()) {
|
|
1275
|
+
pending.push(child);
|
|
1276
|
+
}
|
|
1277
|
+
child = child.next;
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
/**
|
|
1281
|
+
* Recognise GFM autolink literals throughout one finished block's inlines.
|
|
1282
|
+
*
|
|
1283
|
+
* Iterative rather than recursive: emphasis nests as deeply as the input says.
|
|
1284
|
+
*/
|
|
1285
|
+
function linkifyAutolinks(block) {
|
|
1286
|
+
const pending = [block];
|
|
1287
|
+
while (0 < pending.length) {
|
|
1288
|
+
linkifyChildren(pending.pop(), pending);
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
/** Blocks whose raw content the block phase left for this phase to parse. */
|
|
1292
|
+
const INLINE_CONTENT_BLOCKS = {
|
|
1293
|
+
paragraph: true,
|
|
1294
|
+
heading: true,
|
|
1295
|
+
// GFM table cells hold inlines and nothing else — "block-level elements
|
|
1296
|
+
// cannot be inserted in a table" — so a cell is parsed exactly as a
|
|
1297
|
+
// paragraph is, autolink post-pass included.
|
|
1298
|
+
table_cell: true,
|
|
1299
|
+
};
|
|
1300
|
+
/**
|
|
1301
|
+
* Phase 2 entry point: walk the block tree and parse the string content of
|
|
1302
|
+
* every paragraph, heading and table cell into inline children.
|
|
1303
|
+
*/
|
|
1304
|
+
function parseInlines(doc, refmap, options) {
|
|
1305
|
+
const parser = new InlineParser(refmap, options);
|
|
1306
|
+
const walker = doc.walker();
|
|
1307
|
+
let ev = walker.next();
|
|
1308
|
+
while (null !== ev) {
|
|
1309
|
+
const node = ev.node;
|
|
1310
|
+
if (!ev.entering && true === INLINE_CONTENT_BLOCKS[node.type]) {
|
|
1311
|
+
parser.parse(node);
|
|
1312
|
+
if (options.gfm)
|
|
1313
|
+
linkifyAutolinks(node);
|
|
1314
|
+
}
|
|
1315
|
+
ev = walker.next();
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
// Reference definitions are parsed during the block phase, before any
|
|
1319
|
+
// InlineParser for the document exists, so this one is kept aside for it.
|
|
1320
|
+
// Nothing here depends on the parse options.
|
|
1321
|
+
const referenceParser = new InlineParser({}, { gfm: false, breaks: false });
|
|
1322
|
+
/**
|
|
1323
|
+
* Consume link reference definitions from the front of a paragraph's raw
|
|
1324
|
+
* content, adding them to `refmap`. Returns the number of characters
|
|
1325
|
+
* consumed, or 0 if `s` does not start with a definition.
|
|
1326
|
+
*/
|
|
1327
|
+
function parseReference(s, refmap) {
|
|
1328
|
+
referenceParser.refmap = refmap;
|
|
1329
|
+
return referenceParser.parseReference(s);
|
|
1330
|
+
}
|
|
1331
|
+
//# sourceMappingURL=inline.js.map
|