@tabnas/markdown 0.4.1 → 0.5.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/dist/inline.js ADDED
@@ -0,0 +1,902 @@
1
+ "use strict";
2
+ /* Copyright (c) 2021-2026 Richard Rodger, MIT License */
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.parseInlines = parseInlines;
5
+ exports.parseReference = parseReference;
6
+ // Phase 2 of the CommonMark parse (spec 0.31.2, Appendix A, "Phase 2: inline
7
+ // structure"). The block phase left the raw text of every paragraph and
8
+ // heading in `stringContent`; this module turns that text into inline
9
+ // children and clears it.
10
+ //
11
+ // The shape is the spec's own algorithm: one left-to-right scan over the
12
+ // subject that appends nodes as it goes, plus two auxiliary stacks that are
13
+ // resolved out of band, because neither construct can be decided at the
14
+ // point where its opener is seen:
15
+ //
16
+ // * the delimiter stack (§6.4) holds `*`/`_` (and, under GFM, `~`) runs,
17
+ // and is resolved by `processEmphasis`;
18
+ // * the bracket stack (§6.3) holds `[` and `![`, and is resolved by
19
+ // `parseCloseBracket` when a `]` turns up.
20
+ //
21
+ // Everything else — code spans, autolinks, raw HTML, entities, escapes,
22
+ // breaks — is decided locally at the scan position. That is what produces
23
+ // the precedence §6.3 requires: a code span, autolink or raw HTML tag is
24
+ // consumed whole during the scan, so it binds more tightly than emphasis and
25
+ // more tightly than the brackets of a link label. Conversely brackets bind
26
+ // more tightly than emphasis, because bracket matching happens during the
27
+ // scan while emphasis matching happens afterwards over the delimiter stack.
28
+ const node_ts_1 = require("./node.js");
29
+ const common_ts_1 = require("./common.js");
30
+ const C_NEWLINE = 10;
31
+ const C_SPACE = 32;
32
+ const C_BANG = 33;
33
+ const C_AMPERSAND = 38;
34
+ const C_OPEN_PAREN = 40;
35
+ const C_CLOSE_PAREN = 41;
36
+ const C_ASTERISK = 42;
37
+ const C_COLON = 58;
38
+ const C_LESSTHAN = 60;
39
+ const C_OPEN_BRACKET = 91;
40
+ const C_BACKSLASH = 92;
41
+ const C_CLOSE_BRACKET = 93;
42
+ const C_UNDERSCORE = 95;
43
+ const C_BACKTICK = 96;
44
+ const C_TILDE = 126;
45
+ const C_DEL = 127;
46
+ // --- §6.6 Raw HTML: the tag grammar, spelled out because it is stricter than
47
+ // `<[^>]*>` in every part. Attribute names are XML-restricted-to-ASCII;
48
+ // unquoted attribute values exclude quotes, `=`, `<`, `>`, backtick and all
49
+ // control characters; comments/PIs/declarations/CDATA each have their own
50
+ // terminator. `\s*` stands in for "spaces, tabs, and up to one line ending":
51
+ // a paragraph can never contain a blank line, so a single `\s*` can only ever
52
+ // span one line ending anyway.
53
+ const TAGNAME = '[A-Za-z][A-Za-z0-9-]*';
54
+ const ATTRIBUTENAME = '[a-zA-Z_:][a-zA-Z0-9:._-]*';
55
+ const UNQUOTEDVALUE = "[^\"'=<>`\\x00-\\x20]+";
56
+ const SINGLEQUOTEDVALUE = "'[^']*'";
57
+ const DOUBLEQUOTEDVALUE = '"[^"]*"';
58
+ const ATTRIBUTEVALUE = '(?:' + UNQUOTEDVALUE + '|' + SINGLEQUOTEDVALUE + '|' + DOUBLEQUOTEDVALUE + ')';
59
+ const ATTRIBUTEVALUESPEC = '(?:\\s*=\\s*' + ATTRIBUTEVALUE + ')';
60
+ const ATTRIBUTE = '(?:\\s+' + ATTRIBUTENAME + ATTRIBUTEVALUESPEC + '?)';
61
+ const OPENTAG = '<' + TAGNAME + ATTRIBUTE + '*\\s*/?>';
62
+ const CLOSETAG = '</' + TAGNAME + '\\s*>';
63
+ // §6.6: `<!-->` and `<!--->` are comments in their own right; otherwise the
64
+ // body simply may not contain `-->`, which the lazy quantifier enforces.
65
+ const HTMLCOMMENT = '<!-->|<!--->|<!--[\\s\\S]*?-->';
66
+ const PROCESSINGINSTRUCTION = '[<][?][\\s\\S]*?[?][>]';
67
+ const DECLARATION = '<![A-Za-z][^>]*>';
68
+ const CDATA = '<!\\[CDATA\\[[\\s\\S]*?\\]\\]>';
69
+ const HTMLTAG = '(?:' +
70
+ OPENTAG +
71
+ '|' +
72
+ CLOSETAG +
73
+ '|' +
74
+ HTMLCOMMENT +
75
+ '|' +
76
+ PROCESSINGINSTRUCTION +
77
+ '|' +
78
+ DECLARATION +
79
+ '|' +
80
+ CDATA +
81
+ ')';
82
+ const ESCAPED_CHAR = '\\\\' + common_ts_1.ESCAPABLE;
83
+ // Every regex handed to `match()` is sticky: it must match at `pos`, and
84
+ // `lastIndex` then gives the new position. (The equivalent of anchoring with
85
+ // `^` on a sliced subject, without the slicing.)
86
+ const reHtmlTag = new RegExp(HTMLTAG, 'iy');
87
+ const reEntityHere = new RegExp(common_ts_1.ENTITY, 'iy');
88
+ const reEscapable = new RegExp('^' + common_ts_1.ESCAPABLE);
89
+ const reTicksHere = /`+/y;
90
+ const reTicks = /`+/g;
91
+ // Chunks of ordinary text: everything up to the next character that could
92
+ // start an inline construct. `~` is only special under GFM, hence two.
93
+ const reMain = /[^\n`[\]\\!<&*_]+/y;
94
+ const reMainGfm = /[^\n`[\]\\!<&*_~]+/y;
95
+ /**
96
+ * Maximum `(` nesting inside a bare link destination. The spec describes
97
+ * balanced parentheses without stating a bound; cmark uses 32, and the bound
98
+ * is what keeps an unbalanced run from being quadratic (see
99
+ * parseLinkDestination).
100
+ */
101
+ const MAX_LINK_PAREN_NESTING = 32;
102
+ const reSpnl = / *(?:\n *)?/y;
103
+ const reInitialSpace = / */y;
104
+ const reIndent = / {0,3}/y;
105
+ const reFinalSpace = / *$/;
106
+ const reSpaceAtEndOfLine = / *(?:\n|$)/y;
107
+ const reWhitespaceChar = /[ \t\n\v\f\r]/;
108
+ // §4.7: at most 999 characters between the brackets, brackets inside only if
109
+ // backslash-escaped. `s` so that an escaped line ending counts as an escape.
110
+ const reLinkLabel = /\[(?:[^\\[\]]|\\.){0,1000}\]/sy;
111
+ // §6.3 link destination, pointy-bracket form: no line endings, no unescaped
112
+ // `<` or `>`.
113
+ const reLinkDestinationBraces = /<(?:[^<>\n\\\x00]|\\.)*>/y;
114
+ // §6.3 link title in `"`, `'` or `(...)` delimiters. The escape alternative
115
+ // comes first so that `\"` inside a double-quoted title is consumed as an
116
+ // escape rather than closing the title.
117
+ const reLinkTitle = new RegExp('(?:"(?:' +
118
+ ESCAPED_CHAR +
119
+ '|[^"\\x00])*"' +
120
+ "|'(?:" +
121
+ ESCAPED_CHAR +
122
+ "|[^'\\x00])*'" +
123
+ '|\\((?:' +
124
+ ESCAPED_CHAR +
125
+ '|[^()\\x00])*\\))', 'y');
126
+ // §6.5 autolinks. Scheme: an ASCII letter then 1–31 more of letter/digit/
127
+ // `+`/`.`/`-`, i.e. 2–32 characters in total, case-insensitive.
128
+ const reAutolink = /<[A-Za-z][A-Za-z0-9.+-]{1,31}:[^<>\x00-\x20]*>/y;
129
+ 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;
130
+ function text(literal) {
131
+ const node = new node_ts_1.MdNode('text');
132
+ node.literal = literal;
133
+ return node;
134
+ }
135
+ // §2.1 punctuation is P* ∪ S*, which `isUnicodePunctuation` already covers.
136
+ // `*$*alpha.` is the case that needs the symbol half: with `$` counted as
137
+ // punctuation the closing `*` is not right-flanking, so the line stays
138
+ // literal text.
139
+ const isPunctuation = common_ts_1.isUnicodePunctuation;
140
+ class InlineParser {
141
+ constructor(refmap, options) {
142
+ this.subject = '';
143
+ this.pos = 0;
144
+ this.delimiters = null;
145
+ this.brackets = null;
146
+ this.refmap = refmap;
147
+ this.options = options;
148
+ }
149
+ // --- scanning primitives ---
150
+ /** Character code at `pos`, or -1 at the end of the subject. */
151
+ peek() {
152
+ return this.pos < this.subject.length ? this.subject.charCodeAt(this.pos) : -1;
153
+ }
154
+ /** Match a sticky regex at `pos`; on success advance past it. */
155
+ match(re) {
156
+ re.lastIndex = this.pos;
157
+ const m = re.exec(this.subject);
158
+ if (null === m)
159
+ return null;
160
+ this.pos = re.lastIndex;
161
+ return m[0];
162
+ }
163
+ /** Spaces, and at most one line ending, between the parts of a link. */
164
+ spnl() {
165
+ this.match(reSpnl);
166
+ }
167
+ /**
168
+ * The whole code point ending at `end`, so an astral character is
169
+ * classified as one character by the flanking rules rather than as a lone
170
+ * surrogate. Start of subject counts as a line ending (§6.4).
171
+ */
172
+ charBefore(end) {
173
+ if (end <= 0)
174
+ return '\n';
175
+ const lo = this.subject.charCodeAt(end - 1);
176
+ if (lo >= 0xdc00 && lo <= 0xdfff && end >= 2) {
177
+ const hi = this.subject.charCodeAt(end - 2);
178
+ if (hi >= 0xd800 && hi <= 0xdbff)
179
+ return this.subject.slice(end - 2, end);
180
+ }
181
+ return this.subject.charAt(end - 1);
182
+ }
183
+ /** The whole code point at `i`; end of subject counts as a line ending. */
184
+ charAtPos(i) {
185
+ if (i >= this.subject.length)
186
+ return '\n';
187
+ const cp = this.subject.codePointAt(i);
188
+ return undefined === cp ? '\n' : String.fromCodePoint(cp);
189
+ }
190
+ // --- §6.3 code spans ---
191
+ /**
192
+ * A code span runs from a backtick string to the next backtick string of
193
+ * *exactly* the same length. Unmatched opening backticks are literal text.
194
+ */
195
+ parseBackticks(block) {
196
+ const ticks = this.match(reTicksHere);
197
+ if (null === ticks)
198
+ return false;
199
+ const afterOpenTicks = this.pos;
200
+ reTicks.lastIndex = this.pos;
201
+ let m;
202
+ while (null !== (m = reTicks.exec(this.subject))) {
203
+ this.pos = reTicks.lastIndex;
204
+ if (m[0] === ticks) {
205
+ const node = new node_ts_1.MdNode('code');
206
+ // Line endings become spaces; then one leading and one trailing
207
+ // space are stripped together, but only if the content is not all
208
+ // spaces (which is what lets a code span hold backticks).
209
+ const contents = this.subject
210
+ .slice(afterOpenTicks, this.pos - ticks.length)
211
+ .replace(/\n/g, ' ');
212
+ node.literal =
213
+ contents.length > 2 &&
214
+ ' ' === contents[0] &&
215
+ ' ' === contents[contents.length - 1] &&
216
+ null !== /[^ ]/.exec(contents)
217
+ ? contents.slice(1, contents.length - 1)
218
+ : contents;
219
+ block.appendChild(node);
220
+ return true;
221
+ }
222
+ }
223
+ // No closing run of the same length: the opener is literal text.
224
+ this.pos = afterOpenTicks;
225
+ block.appendChild(text(ticks));
226
+ return true;
227
+ }
228
+ // --- §6.1 backslash escapes, §6.7 hard line breaks ---
229
+ parseBackslash(block) {
230
+ this.pos += 1;
231
+ if (C_NEWLINE === this.peek()) {
232
+ this.pos += 1;
233
+ block.appendChild(new node_ts_1.MdNode('linebreak'));
234
+ this.match(reInitialSpace);
235
+ }
236
+ else if (reEscapable.test(this.subject.charAt(this.pos))) {
237
+ block.appendChild(text(this.subject.charAt(this.pos)));
238
+ this.pos += 1;
239
+ }
240
+ else {
241
+ block.appendChild(text('\\'));
242
+ }
243
+ return true;
244
+ }
245
+ // --- §6.5 autolinks ---
246
+ parseAutolink(block) {
247
+ let m = this.match(reEmailAutolink);
248
+ if (null !== m) {
249
+ const dest = m.slice(1, m.length - 1);
250
+ block.appendChild(this.makeAutolink('mailto:' + dest, dest));
251
+ return true;
252
+ }
253
+ m = this.match(reAutolink);
254
+ if (null !== m) {
255
+ const dest = m.slice(1, m.length - 1);
256
+ block.appendChild(this.makeAutolink(dest, dest));
257
+ return true;
258
+ }
259
+ return false;
260
+ }
261
+ makeAutolink(destination, label) {
262
+ // Autolink text is literal: no backslash escapes, no entity references.
263
+ const node = new node_ts_1.MdNode('link');
264
+ // Not percent-encoded here — see parseLinkDestination.
265
+ node.destination = destination;
266
+ node.title = null;
267
+ node.appendChild(text(label));
268
+ return node;
269
+ }
270
+ // --- §6.6 raw HTML ---
271
+ parseHtmlTag(block) {
272
+ const m = this.match(reHtmlTag);
273
+ if (null === m)
274
+ return false;
275
+ const node = new node_ts_1.MdNode('html_inline');
276
+ node.literal = m;
277
+ block.appendChild(node);
278
+ return true;
279
+ }
280
+ // --- §6.4 emphasis: delimiter runs and flanking ---
281
+ /**
282
+ * Classify the run of `cc` starting at `pos` without consuming it.
283
+ *
284
+ * §6.4: a run is left-flanking iff it is not followed by Unicode
285
+ * whitespace and either not followed by Unicode punctuation or else
286
+ * preceded by whitespace or punctuation; right-flanking is the mirror
287
+ * image. `_` additionally may not open inside a word (rules 2, 4, 6, 8),
288
+ * which is what keeps `snake_case_words` intact; `*` has no such
289
+ * restriction.
290
+ */
291
+ scanDelims(cc) {
292
+ const startpos = this.pos;
293
+ let numdelims = 0;
294
+ while (cc === this.peek()) {
295
+ numdelims += 1;
296
+ this.pos += 1;
297
+ }
298
+ if (0 === numdelims)
299
+ return null;
300
+ const charBefore = this.charBefore(startpos);
301
+ const charAfter = this.charAtPos(this.pos);
302
+ const afterIsWhitespace = (0, common_ts_1.isUnicodeWhitespace)(charAfter);
303
+ const afterIsPunctuation = isPunctuation(charAfter);
304
+ const beforeIsWhitespace = (0, common_ts_1.isUnicodeWhitespace)(charBefore);
305
+ const beforeIsPunctuation = isPunctuation(charBefore);
306
+ const leftFlanking = !afterIsWhitespace &&
307
+ (!afterIsPunctuation || beforeIsWhitespace || beforeIsPunctuation);
308
+ const rightFlanking = !beforeIsWhitespace &&
309
+ (!beforeIsPunctuation || afterIsWhitespace || afterIsPunctuation);
310
+ let canOpen;
311
+ let canClose;
312
+ if (C_UNDERSCORE === cc) {
313
+ canOpen = leftFlanking && (!rightFlanking || beforeIsPunctuation);
314
+ canClose = rightFlanking && (!leftFlanking || afterIsPunctuation);
315
+ }
316
+ else {
317
+ canOpen = leftFlanking;
318
+ canClose = rightFlanking;
319
+ }
320
+ this.pos = startpos;
321
+ return { numdelims, canOpen, canClose };
322
+ }
323
+ /** Emit a delimiter run as text and push it on the delimiter stack. */
324
+ handleDelim(cc, block) {
325
+ const res = this.scanDelims(cc);
326
+ if (null === res)
327
+ return false;
328
+ const numdelims = res.numdelims;
329
+ const startpos = this.pos;
330
+ this.pos += numdelims;
331
+ const node = text(this.subject.slice(startpos, this.pos));
332
+ block.appendChild(node);
333
+ // GFM strikethrough only recognises runs of one or two tildes; longer
334
+ // runs stay literal.
335
+ const stackable = C_TILDE === cc ? numdelims <= 2 : true;
336
+ if ((res.canOpen || res.canClose) && stackable) {
337
+ const delim = {
338
+ cc,
339
+ numdelims,
340
+ origdelims: numdelims,
341
+ node,
342
+ previous: this.delimiters,
343
+ next: null,
344
+ canOpen: res.canOpen,
345
+ canClose: res.canClose,
346
+ };
347
+ if (null !== delim.previous)
348
+ delim.previous.next = delim;
349
+ this.delimiters = delim;
350
+ }
351
+ return true;
352
+ }
353
+ removeDelimiter(delim) {
354
+ if (null !== delim.previous)
355
+ delim.previous.next = delim.next;
356
+ if (null === delim.next) {
357
+ // Top of stack.
358
+ this.delimiters = delim.previous;
359
+ }
360
+ else {
361
+ delim.next.previous = delim.previous;
362
+ }
363
+ }
364
+ /**
365
+ * The spec's "process emphasis" procedure. `stackBottom` is the floor: for
366
+ * the whole-block call it is null, and for the inlines of a link it is the
367
+ * delimiter that was on top when the link's `[` opened.
368
+ */
369
+ processEmphasis(stackBottom) {
370
+ // openers_bottom, keyed by (delimiter char, whether the closer can also
371
+ // open, closer run length mod 3). Once a closer of some class fails to
372
+ // find an opener, no later closer of the same class needs to look past
373
+ // that point either — this is what keeps the search linear.
374
+ const openersBottom = new Map();
375
+ // First closer above stackBottom.
376
+ let closer = this.delimiters;
377
+ while (null !== closer && closer.previous !== stackBottom) {
378
+ closer = closer.previous;
379
+ }
380
+ while (null !== closer) {
381
+ if (!closer.canClose) {
382
+ closer = closer.next;
383
+ continue;
384
+ }
385
+ const closercc = closer.cc;
386
+ const key = closercc + ':' + (closer.canOpen ? 1 : 0) + ':' + (closer.origdelims % 3);
387
+ const bottom = openersBottom.has(key)
388
+ ? openersBottom.get(key)
389
+ : stackBottom;
390
+ let opener = closer.previous;
391
+ let openerFound = false;
392
+ while (null !== opener && opener !== stackBottom && opener !== bottom) {
393
+ if (opener.cc === closercc && opener.canOpen) {
394
+ if (C_TILDE === closercc) {
395
+ // GFM strikethrough: opener and closer runs must be the same
396
+ // length, so `~~x~` is not a match.
397
+ if (opener.numdelims === closer.numdelims) {
398
+ openerFound = true;
399
+ break;
400
+ }
401
+ }
402
+ else {
403
+ // §6.4 rules 9 and 10, the "rule of three": when either side of
404
+ // the pair could both open and close, the two run lengths may
405
+ // not sum to a multiple of 3 — unless both are themselves
406
+ // multiples of 3.
407
+ const oddMatch = (closer.canOpen || opener.canClose) &&
408
+ 0 !== closer.origdelims % 3 &&
409
+ 0 === (opener.origdelims + closer.origdelims) % 3;
410
+ if (!oddMatch) {
411
+ openerFound = true;
412
+ break;
413
+ }
414
+ }
415
+ }
416
+ opener = opener.previous;
417
+ }
418
+ const oldCloser = closer;
419
+ if (openerFound && null !== opener) {
420
+ // Two delimiters make strong, one makes emph; strikethrough always
421
+ // consumes the whole (equal-length) run.
422
+ const useDelims = C_TILDE === closercc
423
+ ? closer.numdelims
424
+ : closer.numdelims >= 2 && opener.numdelims >= 2
425
+ ? 2
426
+ : 1;
427
+ const nodeType = C_TILDE === closercc ? 'del' : 1 === useDelims ? 'emph' : 'strong';
428
+ const openerInl = opener.node;
429
+ const closerInl = closer.node;
430
+ opener.numdelims -= useDelims;
431
+ closer.numdelims -= useDelims;
432
+ openerInl.literal = (openerInl.literal ?? '').slice(0, (openerInl.literal ?? '').length - useDelims);
433
+ closerInl.literal = (closerInl.literal ?? '').slice(0, (closerInl.literal ?? '').length - useDelims);
434
+ // Everything between the two delimiter text nodes becomes the
435
+ // content of the new node.
436
+ const emph = new node_ts_1.MdNode(nodeType);
437
+ let tmp = openerInl.next;
438
+ while (null !== tmp && tmp !== closerInl) {
439
+ const nxt = tmp.next;
440
+ tmp.unlink();
441
+ emph.appendChild(tmp);
442
+ tmp = nxt;
443
+ }
444
+ openerInl.insertAfter(emph);
445
+ // Delimiters between the pair can never match anything now.
446
+ if (opener.next !== closer) {
447
+ opener.next = closer;
448
+ closer.previous = opener;
449
+ }
450
+ if (0 === opener.numdelims) {
451
+ openerInl.unlink();
452
+ this.removeDelimiter(opener);
453
+ }
454
+ if (0 === closer.numdelims) {
455
+ closerInl.unlink();
456
+ const tempstack = closer.next;
457
+ this.removeDelimiter(closer);
458
+ closer = tempstack;
459
+ }
460
+ }
461
+ else {
462
+ closer = closer.next;
463
+ openersBottom.set(key, oldCloser.previous);
464
+ if (!oldCloser.canOpen) {
465
+ // It found no opener and cannot be one: it is inert.
466
+ this.removeDelimiter(oldCloser);
467
+ }
468
+ }
469
+ }
470
+ // Drop everything above the floor.
471
+ while (null !== this.delimiters && this.delimiters !== stackBottom) {
472
+ this.removeDelimiter(this.delimiters);
473
+ }
474
+ }
475
+ // --- §6.3 links and images ---
476
+ /** Link title without its delimiters, unescaped; null if none here. */
477
+ parseLinkTitle() {
478
+ const title = this.match(reLinkTitle);
479
+ if (null === title)
480
+ return null;
481
+ return (0, common_ts_1.unescapeString)(title.slice(1, title.length - 1));
482
+ }
483
+ /**
484
+ * Link destination: either `<...>`, or a bare run with balanced unescaped
485
+ * parentheses and no ASCII control characters or spaces.
486
+ */
487
+ /**
488
+ * A destination is backslash-unescaped and entity-decoded, but **not**
489
+ * percent-encoded: `[l](/ä)` yields `/ä`, not `/%C3%A4`.
490
+ *
491
+ * Encoding is a rendering concern and `html.ts` applies `normalizeURI` when
492
+ * it writes the attribute. Doing it here too would be invisible in the HTML
493
+ * (`normalizeURI` is idempotent over its own output) but would put the
494
+ * encoded form in the public AST, where mdast — and every previous release
495
+ * of this package — promises the decoded one.
496
+ */
497
+ parseLinkDestination() {
498
+ const braced = this.match(reLinkDestinationBraces);
499
+ if (null !== braced) {
500
+ return (0, common_ts_1.unescapeString)(braced.slice(1, braced.length - 1));
501
+ }
502
+ if (C_LESSTHAN === this.peek())
503
+ return null;
504
+ const savepos = this.pos;
505
+ let openparens = 0;
506
+ let c = this.peek();
507
+ while (-1 !== c) {
508
+ if (C_BACKSLASH === c && reEscapable.test(this.subject.charAt(this.pos + 1))) {
509
+ this.pos += 1;
510
+ if (-1 !== this.peek())
511
+ this.pos += 1;
512
+ }
513
+ else if (C_OPEN_PAREN === c) {
514
+ // Bail rather than scan on once nesting passes the limit. Without
515
+ // this, input like `[a](b` repeated makes every `]` scan to end of
516
+ // input looking for parens that never balance, which is quadratic in
517
+ // the document size. cmark caps nesting at the same depth, and no
518
+ // spec example nests more than two deep.
519
+ if (MAX_LINK_PAREN_NESTING <= openparens)
520
+ return null;
521
+ this.pos += 1;
522
+ openparens += 1;
523
+ }
524
+ else if (C_CLOSE_PAREN === c) {
525
+ if (openparens < 1)
526
+ break;
527
+ this.pos += 1;
528
+ openparens -= 1;
529
+ }
530
+ else if (c <= C_SPACE || C_DEL === c) {
531
+ // Spaces and ASCII control characters end the bare form.
532
+ break;
533
+ }
534
+ else {
535
+ this.pos += 1;
536
+ }
537
+ c = this.peek();
538
+ }
539
+ // An empty destination is only legal immediately before the closing
540
+ // paren of an inline link, as in `[link]()`.
541
+ if (this.pos === savepos && C_CLOSE_PAREN !== c)
542
+ return null;
543
+ if (0 !== openparens)
544
+ return null;
545
+ return (0, common_ts_1.unescapeString)(this.subject.slice(savepos, this.pos));
546
+ }
547
+ /** Length of the link label at `pos`, including brackets; 0 if invalid. */
548
+ parseLinkLabel() {
549
+ const m = this.match(reLinkLabel);
550
+ // §4.7: at most 999 characters between the brackets.
551
+ if (null === m || m.length > 1001)
552
+ return 0;
553
+ return m.length;
554
+ }
555
+ addBracket(node, index, image) {
556
+ if (null !== this.brackets)
557
+ this.brackets.bracketAfter = true;
558
+ this.brackets = {
559
+ node,
560
+ previous: this.brackets,
561
+ previousDelimiter: this.delimiters,
562
+ index,
563
+ image,
564
+ active: true,
565
+ bracketAfter: false,
566
+ };
567
+ }
568
+ removeBracket() {
569
+ if (null !== this.brackets)
570
+ this.brackets = this.brackets.previous;
571
+ }
572
+ parseOpenBracket(block) {
573
+ const startpos = this.pos;
574
+ this.pos += 1;
575
+ const node = text('[');
576
+ block.appendChild(node);
577
+ this.addBracket(node, startpos, false);
578
+ return true;
579
+ }
580
+ /** `!` only matters when it introduces an image. */
581
+ parseBang(block) {
582
+ const startpos = this.pos;
583
+ this.pos += 1;
584
+ if (C_OPEN_BRACKET === this.peek()) {
585
+ this.pos += 1;
586
+ const node = text('![');
587
+ block.appendChild(node);
588
+ this.addBracket(node, startpos + 1, true);
589
+ }
590
+ else {
591
+ block.appendChild(text('!'));
592
+ }
593
+ return true;
594
+ }
595
+ /**
596
+ * The spec's "look for link or image" procedure. On a `]`, walk back to
597
+ * the newest bracket opener and try, in order: an inline `(dest "title")`,
598
+ * a full `[label]`, a collapsed `[]` or a shortcut reference. Anything that
599
+ * fails backtracks to just after the `]` and leaves a literal `]`.
600
+ */
601
+ parseCloseBracket(block) {
602
+ this.pos += 1;
603
+ const startpos = this.pos;
604
+ const opener = this.brackets;
605
+ if (null === opener) {
606
+ block.appendChild(text(']'));
607
+ return true;
608
+ }
609
+ if (!opener.active) {
610
+ // Deactivated by an enclosing link: not an opener any more.
611
+ block.appendChild(text(']'));
612
+ this.removeBracket();
613
+ return true;
614
+ }
615
+ const isImage = opener.image;
616
+ let dest = null;
617
+ let title = null;
618
+ let matched = false;
619
+ const savepos = this.pos;
620
+ // Inline link: `](` destination title `)`.
621
+ if (C_OPEN_PAREN === this.peek()) {
622
+ this.pos += 1;
623
+ this.spnl();
624
+ const d = this.parseLinkDestination();
625
+ if (null !== d) {
626
+ this.spnl();
627
+ // A title has to be separated from the destination by whitespace,
628
+ // so `[a](/url"t")` is not a titled link.
629
+ let t = null;
630
+ if (reWhitespaceChar.test(this.subject.charAt(this.pos - 1))) {
631
+ t = this.parseLinkTitle();
632
+ }
633
+ this.spnl();
634
+ if (C_CLOSE_PAREN === this.peek()) {
635
+ this.pos += 1;
636
+ dest = d;
637
+ title = t;
638
+ matched = true;
639
+ }
640
+ }
641
+ if (!matched)
642
+ this.pos = savepos;
643
+ }
644
+ if (!matched) {
645
+ // Reference forms. A second label gives the full form; an empty or
646
+ // absent one means the link text is itself the label (collapsed and
647
+ // shortcut) — which is only possible if that text holds no bracket.
648
+ let reflabel = null;
649
+ const beforelabel = this.pos;
650
+ const n = this.parseLinkLabel();
651
+ if (n > 2) {
652
+ reflabel = this.subject.slice(beforelabel + 1, beforelabel + n - 1);
653
+ }
654
+ else if (!opener.bracketAfter) {
655
+ reflabel = this.subject.slice(opener.index + 1, startpos - 1);
656
+ }
657
+ if (0 === n)
658
+ this.pos = savepos;
659
+ if (null !== reflabel) {
660
+ const link = this.refmap[(0, common_ts_1.normalizeReference)(reflabel)];
661
+ if (undefined !== link) {
662
+ dest = link.destination;
663
+ title = link.title;
664
+ matched = true;
665
+ }
666
+ }
667
+ }
668
+ if (!matched) {
669
+ this.removeBracket();
670
+ this.pos = startpos;
671
+ block.appendChild(text(']'));
672
+ return true;
673
+ }
674
+ const node = new node_ts_1.MdNode(isImage ? 'image' : 'link');
675
+ node.destination = dest;
676
+ node.title = null === title || '' === title ? null : title;
677
+ let tmp = opener.node.next;
678
+ while (null !== tmp) {
679
+ const nxt = tmp.next;
680
+ tmp.unlink();
681
+ node.appendChild(tmp);
682
+ tmp = nxt;
683
+ }
684
+ block.appendChild(node);
685
+ // Emphasis inside the link text resolves now, and only down to the
686
+ // delimiter that was current when the `[` opened.
687
+ this.processEmphasis(opener.previousDelimiter);
688
+ this.removeBracket();
689
+ opener.node.unlink();
690
+ // Links may not contain links (§6.3): every earlier link opener is
691
+ // spent. Image openers survive, since images may nest in links.
692
+ if (!isImage) {
693
+ let b = this.brackets;
694
+ while (null !== b) {
695
+ if (!b.image)
696
+ b.active = false;
697
+ b = b.previous;
698
+ }
699
+ }
700
+ return true;
701
+ }
702
+ // --- §6.2 entities, §6.8/§6.9 breaks and plain text ---
703
+ parseEntity(block) {
704
+ const m = this.match(reEntityHere);
705
+ if (null === m)
706
+ return false;
707
+ block.appendChild(text((0, common_ts_1.decodeEntity)(m)));
708
+ return true;
709
+ }
710
+ parseString(block) {
711
+ const m = this.match(this.options.gfm ? reMainGfm : reMain);
712
+ if (null === m)
713
+ return false;
714
+ block.appendChild(text(m));
715
+ return true;
716
+ }
717
+ /**
718
+ * §6.7/§6.8: two or more spaces before the line ending make a hard break,
719
+ * anything else a soft one. Trailing spaces are dropped either way, as is
720
+ * the indentation of the next line.
721
+ */
722
+ parseNewline(block) {
723
+ this.pos += 1;
724
+ const lastc = block.lastChild;
725
+ if (null !== lastc &&
726
+ 'text' === lastc.type &&
727
+ null !== lastc.literal &&
728
+ ' ' === lastc.literal.charAt(lastc.literal.length - 1)) {
729
+ const hardbreak = ' ' === lastc.literal.charAt(lastc.literal.length - 2);
730
+ lastc.literal = lastc.literal.replace(reFinalSpace, '');
731
+ block.appendChild(new node_ts_1.MdNode(hardbreak ? 'linebreak' : 'softbreak'));
732
+ }
733
+ else {
734
+ block.appendChild(new node_ts_1.MdNode('softbreak'));
735
+ }
736
+ this.match(reInitialSpace);
737
+ return true;
738
+ }
739
+ /** One inline construct at `pos`; false only at the end of the subject. */
740
+ parseInline(block) {
741
+ const c = this.peek();
742
+ if (-1 === c)
743
+ return false;
744
+ let res = false;
745
+ switch (c) {
746
+ case C_NEWLINE:
747
+ res = this.parseNewline(block);
748
+ break;
749
+ case C_BACKSLASH:
750
+ res = this.parseBackslash(block);
751
+ break;
752
+ case C_BACKTICK:
753
+ res = this.parseBackticks(block);
754
+ break;
755
+ case C_ASTERISK:
756
+ case C_UNDERSCORE:
757
+ res = this.handleDelim(c, block);
758
+ break;
759
+ case C_TILDE:
760
+ // Only GFM gives `~` a meaning; otherwise it is ordinary text (and
761
+ // the non-GFM `reMain` lets parseString swallow whole runs of it).
762
+ res = this.options.gfm ? this.handleDelim(c, block) : this.parseString(block);
763
+ break;
764
+ case C_OPEN_BRACKET:
765
+ res = this.parseOpenBracket(block);
766
+ break;
767
+ case C_BANG:
768
+ res = this.parseBang(block);
769
+ break;
770
+ case C_CLOSE_BRACKET:
771
+ res = this.parseCloseBracket(block);
772
+ break;
773
+ case C_LESSTHAN:
774
+ res = this.parseAutolink(block) || this.parseHtmlTag(block);
775
+ break;
776
+ case C_AMPERSAND:
777
+ res = this.parseEntity(block);
778
+ break;
779
+ default:
780
+ res = this.parseString(block);
781
+ break;
782
+ }
783
+ if (!res) {
784
+ // Nothing claimed it: the character is literal text.
785
+ this.pos += 1;
786
+ block.appendChild(text(String.fromCharCode(c)));
787
+ }
788
+ return true;
789
+ }
790
+ /** Parse one paragraph or heading's raw content into inline children. */
791
+ parse(block) {
792
+ // Trimming is what stops trailing spaces at the end of a block from
793
+ // making a hard break, and drops the leading indentation of the first line.
794
+ this.subject = block.stringContent.trim();
795
+ this.pos = 0;
796
+ this.delimiters = null;
797
+ this.brackets = null;
798
+ while (this.parseInline(block))
799
+ ;
800
+ block.stringContent = '';
801
+ this.processEmphasis(null);
802
+ }
803
+ /**
804
+ * §4.7: consume leading link reference definitions. Called by the block
805
+ * phase while finalizing a paragraph, repeatedly, until it returns 0.
806
+ */
807
+ parseReference(s) {
808
+ this.subject = s;
809
+ this.pos = 0;
810
+ const startpos = this.pos;
811
+ // §4.7 allows up to three spaces of indentation before the label. The
812
+ // block phase normally strips a paragraph line's indentation before it
813
+ // gets here, so this usually consumes nothing.
814
+ this.match(reIndent);
815
+ const labelStart = this.pos;
816
+ const matchChars = this.parseLinkLabel();
817
+ if (0 === matchChars)
818
+ return 0;
819
+ const rawlabel = this.subject.slice(labelStart + 1, labelStart + matchChars - 1);
820
+ if (C_COLON === this.peek()) {
821
+ this.pos += 1;
822
+ }
823
+ else {
824
+ this.pos = startpos;
825
+ return 0;
826
+ }
827
+ this.spnl();
828
+ const dest = this.parseLinkDestination();
829
+ if (null === dest) {
830
+ this.pos = startpos;
831
+ return 0;
832
+ }
833
+ const beforetitle = this.pos;
834
+ this.spnl();
835
+ let title = null;
836
+ if (this.pos !== beforetitle) {
837
+ title = this.parseLinkTitle();
838
+ }
839
+ if (null === title) {
840
+ this.pos = beforetitle;
841
+ }
842
+ // Nothing but spaces may follow on the definition's last line.
843
+ let atLineEnd = true;
844
+ if (null === this.match(reSpaceAtEndOfLine)) {
845
+ if (null === title) {
846
+ atLineEnd = false;
847
+ }
848
+ else {
849
+ // What looked like a title is something else; the definition may
850
+ // still be valid without it.
851
+ title = null;
852
+ this.pos = beforetitle;
853
+ atLineEnd = null !== this.match(reSpaceAtEndOfLine);
854
+ }
855
+ }
856
+ if (!atLineEnd) {
857
+ this.pos = startpos;
858
+ return 0;
859
+ }
860
+ const normlabel = (0, common_ts_1.normalizeReference)(rawlabel);
861
+ if ('' === normlabel) {
862
+ // A label must hold at least one non-whitespace character.
863
+ this.pos = startpos;
864
+ return 0;
865
+ }
866
+ // First definition wins.
867
+ if (undefined === this.refmap[normlabel]) {
868
+ this.refmap[normlabel] = { destination: dest, title };
869
+ }
870
+ return this.pos - startpos;
871
+ }
872
+ }
873
+ /**
874
+ * Phase 2 entry point: walk the block tree and parse the string content of
875
+ * every paragraph and heading into inline children.
876
+ */
877
+ function parseInlines(doc, refmap, options) {
878
+ const parser = new InlineParser(refmap, options);
879
+ const walker = doc.walker();
880
+ let ev = walker.next();
881
+ while (null !== ev) {
882
+ const node = ev.node;
883
+ if (!ev.entering && ('paragraph' === node.type || 'heading' === node.type)) {
884
+ parser.parse(node);
885
+ }
886
+ ev = walker.next();
887
+ }
888
+ }
889
+ // Reference definitions are parsed during the block phase, before any
890
+ // InlineParser for the document exists, so this one is kept aside for it.
891
+ // Nothing here depends on the parse options.
892
+ const referenceParser = new InlineParser({}, { gfm: false, breaks: false });
893
+ /**
894
+ * Consume link reference definitions from the front of a paragraph's raw
895
+ * content, adding them to `refmap`. Returns the number of characters
896
+ * consumed, or 0 if `s` does not start with a definition.
897
+ */
898
+ function parseReference(s, refmap) {
899
+ referenceParser.refmap = refmap;
900
+ return referenceParser.parseReference(s);
901
+ }
902
+ //# sourceMappingURL=inline.js.map