@tabnas/markdown 0.6.1 → 0.7.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/dist/inline.js CHANGED
@@ -1,8 +1,25 @@
1
1
  "use strict";
2
2
  /* Copyright (c) 2021-2026 Richard Rodger, MIT License */
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.InlineParser = void 0;
5
+ exports.codePointBefore = codePointBefore;
6
+ exports.codePointAt = codePointAt;
7
+ exports.skipInitialSpaces = skipInitialSpaces;
8
+ exports.matchSpnl = matchSpnl;
9
+ exports.scanCodeSpan = scanCodeSpan;
10
+ exports.scanEscape = scanEscape;
11
+ exports.scanAngleAutolink = scanAngleAutolink;
12
+ exports.scanHtmlTag = scanHtmlTag;
13
+ exports.scanEntity = scanEntity;
14
+ exports.scanDelimiterRun = scanDelimiterRun;
15
+ exports.classifyBreak = classifyBreak;
16
+ exports.scanLinkTitle = scanLinkTitle;
17
+ exports.scanLinkDestination = scanLinkDestination;
18
+ exports.scanLinkLabel = scanLinkLabel;
19
+ exports.scanInlineLinkTail = scanInlineLinkTail;
4
20
  exports.linkifyAutolinks = linkifyAutolinks;
5
21
  exports.parseInlines = parseInlines;
22
+ exports.forEachInlineBlock = forEachInlineBlock;
6
23
  exports.parseReference = parseReference;
7
24
  // Phase 2 of the CommonMark parse (spec 0.31.2, Appendix A, "Phase 2: inline
8
25
  // structure"). The block phase left the raw text of every paragraph and
@@ -149,6 +166,281 @@ function text(literal) {
149
166
  // punctuation the closing `*` is not right-flanking, so the line stays
150
167
  // literal text.
151
168
  const isPunctuation = common_ts_1.isUnicodePunctuation;
169
+ // --- pure recognizers -------------------------------------------------------
170
+ //
171
+ // Recognition split from node building: each function inspects `subject` at
172
+ // `pos` and reports what it found, moving nothing and touching no tree. The
173
+ // scanner methods below delegate to them, and they are exported so any other
174
+ // driver over the same syntax shares this exact recognition logic instead of
175
+ // reimplementing it.
176
+ /** Match a sticky regex at `pos`; the match text, or null. */
177
+ function stickyAt(re, subject, pos) {
178
+ re.lastIndex = pos;
179
+ const m = re.exec(subject);
180
+ return null === m ? null : m[0];
181
+ }
182
+ /**
183
+ * The whole code point ending at `end`, so an astral character is classified
184
+ * as one character by the flanking rules rather than as a lone surrogate.
185
+ * Start of subject counts as a line ending (§6.4).
186
+ */
187
+ function codePointBefore(subject, end) {
188
+ if (end <= 0)
189
+ return '\n';
190
+ const lo = subject.charCodeAt(end - 1);
191
+ if (lo >= 0xdc00 && lo <= 0xdfff && end >= 2) {
192
+ const hi = subject.charCodeAt(end - 2);
193
+ if (hi >= 0xd800 && hi <= 0xdbff)
194
+ return subject.slice(end - 2, end);
195
+ }
196
+ return subject.charAt(end - 1);
197
+ }
198
+ /** The whole code point at `i`; end of subject counts as a line ending. */
199
+ function codePointAt(subject, i) {
200
+ if (i >= subject.length)
201
+ return '\n';
202
+ const cp = subject.codePointAt(i);
203
+ return undefined === cp ? '\n' : String.fromCodePoint(cp);
204
+ }
205
+ /** End of the run of spaces starting at `pos`. */
206
+ function skipInitialSpaces(subject, pos) {
207
+ reInitialSpace.lastIndex = pos;
208
+ reInitialSpace.exec(subject);
209
+ return reInitialSpace.lastIndex;
210
+ }
211
+ /** End of "spaces, and at most one line ending" starting at `pos` (§6.3). */
212
+ function matchSpnl(subject, pos) {
213
+ reSpnl.lastIndex = pos;
214
+ reSpnl.exec(subject);
215
+ return reSpnl.lastIndex;
216
+ }
217
+ /**
218
+ * §6.3: a code span runs from a backtick string to the next backtick string
219
+ * of *exactly* the same length. `closed: false` reports an unmatched opener,
220
+ * which is literal text; null means no backtick run starts at `pos`.
221
+ */
222
+ function scanCodeSpan(subject, pos) {
223
+ const ticks = stickyAt(reTicksHere, subject, pos);
224
+ if (null === ticks)
225
+ return null;
226
+ const afterOpenTicks = pos + ticks.length;
227
+ reTicks.lastIndex = afterOpenTicks;
228
+ let m;
229
+ while (null !== (m = reTicks.exec(subject))) {
230
+ if (m[0] === ticks) {
231
+ const end = reTicks.lastIndex;
232
+ // Line endings become spaces; then one leading and one trailing space
233
+ // are stripped together, but only if the content is not all spaces
234
+ // (which is what lets a code span hold backticks).
235
+ const contents = subject.slice(afterOpenTicks, end - ticks.length).replace(/\n/g, ' ');
236
+ const literal = contents.length > 2 &&
237
+ ' ' === contents[0] &&
238
+ ' ' === contents[contents.length - 1] &&
239
+ null !== /[^ ]/.exec(contents)
240
+ ? contents.slice(1, contents.length - 1)
241
+ : contents;
242
+ return { closed: true, end, literal };
243
+ }
244
+ }
245
+ return { closed: false, end: afterOpenTicks, ticks };
246
+ }
247
+ /**
248
+ * §6.1/§6.7: a backslash escape, with `pos` at the backslash. Always
249
+ * succeeds: a backslash before anything unescapable is a literal backslash.
250
+ * The `linebreak` form consumes the following line's leading spaces.
251
+ */
252
+ function scanEscape(subject, pos) {
253
+ const next = pos + 1;
254
+ if (next < subject.length && C_NEWLINE === subject.charCodeAt(next)) {
255
+ return { kind: 'linebreak', literal: '', end: skipInitialSpaces(subject, next + 1) };
256
+ }
257
+ const ch = subject.charAt(next);
258
+ if (reEscapable.test(ch))
259
+ return { kind: 'char', literal: ch, end: next + 1 };
260
+ return { kind: 'literal', literal: '\\', end: next };
261
+ }
262
+ /** §6.5: an email or URI autolink in angle brackets. */
263
+ function scanAngleAutolink(subject, pos) {
264
+ let m = stickyAt(reEmailAutolink, subject, pos);
265
+ if (null !== m) {
266
+ const dest = m.slice(1, m.length - 1);
267
+ return { dest: 'mailto:' + dest, label: dest, end: pos + m.length };
268
+ }
269
+ m = stickyAt(reAutolink, subject, pos);
270
+ if (null !== m) {
271
+ const dest = m.slice(1, m.length - 1);
272
+ return { dest, label: dest, end: pos + m.length };
273
+ }
274
+ return null;
275
+ }
276
+ /** §6.6: a raw HTML tag (open, close, comment, PI, declaration or CDATA). */
277
+ function scanHtmlTag(subject, pos) {
278
+ const m = stickyAt(reHtmlTag, subject, pos);
279
+ return null === m ? null : { raw: m, end: pos + m.length };
280
+ }
281
+ /** §6.2: an entity or numeric character reference, decoded. */
282
+ function scanEntity(subject, pos) {
283
+ const m = stickyAt(reEntityHere, subject, pos);
284
+ return null === m ? null : { literal: (0, common_ts_1.decodeEntity)(m), end: pos + m.length };
285
+ }
286
+ /**
287
+ * §6.4: classify the run of `cc` starting at `pos` without consuming it.
288
+ *
289
+ * A run is left-flanking iff it is not followed by Unicode whitespace and
290
+ * either not followed by Unicode punctuation or else preceded by whitespace
291
+ * or punctuation; right-flanking is the mirror image. `_` additionally may
292
+ * not open inside a word (rules 2, 4, 6, 8), which is what keeps
293
+ * `snake_case_words` intact; `*` has no such restriction.
294
+ */
295
+ function scanDelimiterRun(subject, pos, cc) {
296
+ let numdelims = 0;
297
+ let p = pos;
298
+ while (p < subject.length && cc === subject.charCodeAt(p)) {
299
+ numdelims += 1;
300
+ p += 1;
301
+ }
302
+ if (0 === numdelims)
303
+ return null;
304
+ const charBefore = codePointBefore(subject, pos);
305
+ const charAfter = codePointAt(subject, p);
306
+ const afterIsWhitespace = (0, common_ts_1.isUnicodeWhitespace)(charAfter);
307
+ const afterIsPunctuation = isPunctuation(charAfter);
308
+ const beforeIsWhitespace = (0, common_ts_1.isUnicodeWhitespace)(charBefore);
309
+ const beforeIsPunctuation = isPunctuation(charBefore);
310
+ const leftFlanking = !afterIsWhitespace &&
311
+ (!afterIsPunctuation || beforeIsWhitespace || beforeIsPunctuation);
312
+ const rightFlanking = !beforeIsWhitespace &&
313
+ (!beforeIsPunctuation || afterIsWhitespace || afterIsPunctuation);
314
+ let canOpen;
315
+ let canClose;
316
+ if (C_UNDERSCORE === cc) {
317
+ canOpen = leftFlanking && (!rightFlanking || beforeIsPunctuation);
318
+ canClose = rightFlanking && (!leftFlanking || afterIsPunctuation);
319
+ }
320
+ else {
321
+ canOpen = leftFlanking;
322
+ canClose = rightFlanking;
323
+ }
324
+ return { numdelims, canOpen, canClose };
325
+ }
326
+ /**
327
+ * §6.7/§6.8: how a line ending resolves, given the literal of the text node
328
+ * immediately before it. Two or more trailing spaces make a hard break;
329
+ * `trim` says the trailing spaces must be dropped from that node either way.
330
+ */
331
+ function classifyBreak(prevTextLiteral) {
332
+ if (null === prevTextLiteral ||
333
+ ' ' !== prevTextLiteral.charAt(prevTextLiteral.length - 1)) {
334
+ return { hard: false, trim: false };
335
+ }
336
+ return { hard: ' ' === prevTextLiteral.charAt(prevTextLiteral.length - 2), trim: true };
337
+ }
338
+ /** §6.3: a link title at `pos`, without its delimiters, unescaped. */
339
+ function scanLinkTitle(subject, pos) {
340
+ const m = stickyAt(reLinkTitle, subject, pos);
341
+ if (null === m)
342
+ return null;
343
+ return { title: (0, common_ts_1.unescapeString)(m.slice(1, m.length - 1)), end: pos + m.length };
344
+ }
345
+ /**
346
+ * §6.3: a link destination at `pos` — either `<...>`, or a bare run with
347
+ * balanced unescaped parentheses and no ASCII control characters or spaces.
348
+ *
349
+ * The destination is backslash-unescaped and entity-decoded, but **not**
350
+ * percent-encoded: `[l](/ä)` yields `/ä`, not `/%C3%A4`. Encoding is a
351
+ * rendering concern and `html.ts` applies `normalizeURI` when it writes the
352
+ * attribute; the public AST promises the decoded form.
353
+ */
354
+ function scanLinkDestination(subject, pos) {
355
+ const braced = stickyAt(reLinkDestinationBraces, subject, pos);
356
+ if (null !== braced) {
357
+ return { dest: (0, common_ts_1.unescapeString)(braced.slice(1, braced.length - 1)), end: pos + braced.length };
358
+ }
359
+ if (pos < subject.length && C_LESSTHAN === subject.charCodeAt(pos))
360
+ return null;
361
+ let p = pos;
362
+ let openparens = 0;
363
+ let c = p < subject.length ? subject.charCodeAt(p) : -1;
364
+ while (-1 !== c) {
365
+ if (C_BACKSLASH === c && reEscapable.test(subject.charAt(p + 1))) {
366
+ p += 1;
367
+ if (p < subject.length)
368
+ p += 1;
369
+ }
370
+ else if (C_OPEN_PAREN === c) {
371
+ // Bail rather than scan on once nesting passes the limit. Without
372
+ // this, input like `[a](b` repeated makes every `]` scan to end of
373
+ // input looking for parens that never balance, which is quadratic in
374
+ // the document size. cmark caps nesting at the same depth, and no
375
+ // spec example nests more than two deep.
376
+ if (MAX_LINK_PAREN_NESTING <= openparens)
377
+ return null;
378
+ p += 1;
379
+ openparens += 1;
380
+ }
381
+ else if (C_CLOSE_PAREN === c) {
382
+ if (openparens < 1)
383
+ break;
384
+ p += 1;
385
+ openparens -= 1;
386
+ }
387
+ else if (c <= C_SPACE || C_DEL === c) {
388
+ // Spaces and ASCII control characters end the bare form.
389
+ break;
390
+ }
391
+ else {
392
+ p += 1;
393
+ }
394
+ c = p < subject.length ? subject.charCodeAt(p) : -1;
395
+ }
396
+ // An empty destination is only legal immediately before the closing
397
+ // paren of an inline link, as in `[link]()`.
398
+ if (p === pos && C_CLOSE_PAREN !== c)
399
+ return null;
400
+ if (0 !== openparens)
401
+ return null;
402
+ return { dest: (0, common_ts_1.unescapeString)(subject.slice(pos, p)), end: p };
403
+ }
404
+ /**
405
+ * §4.7/§6.3: a link label at `pos`, including brackets. `rawLength` is what
406
+ * was consumed; `length` is 0 when the label is too long to be valid (§4.7:
407
+ * at most 999 characters between the brackets).
408
+ */
409
+ function scanLinkLabel(subject, pos) {
410
+ const m = stickyAt(reLinkLabel, subject, pos);
411
+ if (null === m)
412
+ return null;
413
+ return { rawLength: m.length, length: m.length > 1001 ? 0 : m.length };
414
+ }
415
+ /**
416
+ * §6.3: the `(destination "title")` tail of an inline link, with `pos` just
417
+ * after the `]`. Null unless the whole tail — through the closing paren —
418
+ * is present.
419
+ */
420
+ function scanInlineLinkTail(subject, pos) {
421
+ if (pos >= subject.length || C_OPEN_PAREN !== subject.charCodeAt(pos))
422
+ return null;
423
+ let p = matchSpnl(subject, pos + 1);
424
+ const d = scanLinkDestination(subject, p);
425
+ if (null === d)
426
+ return null;
427
+ p = matchSpnl(subject, d.end);
428
+ let title = null;
429
+ // A title has to be separated from the destination by whitespace, so
430
+ // `[a](/url"t")` is not a titled link.
431
+ if (reWhitespaceChar.test(subject.charAt(p - 1))) {
432
+ const t = scanLinkTitle(subject, p);
433
+ if (null !== t) {
434
+ title = t.title;
435
+ p = t.end;
436
+ }
437
+ }
438
+ p = matchSpnl(subject, p);
439
+ if (p < subject.length && C_CLOSE_PAREN === subject.charCodeAt(p)) {
440
+ return { dest: d.dest, title, end: p + 1 };
441
+ }
442
+ return null;
443
+ }
152
444
  class InlineParser {
153
445
  constructor(refmap, options) {
154
446
  this.subject = '';
@@ -182,22 +474,11 @@ class InlineParser {
182
474
  * surrogate. Start of subject counts as a line ending (§6.4).
183
475
  */
184
476
  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);
477
+ return codePointBefore(this.subject, end);
194
478
  }
195
479
  /** The whole code point at `i`; end of subject counts as a line ending. */
196
480
  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);
481
+ return codePointAt(this.subject, i);
201
482
  }
202
483
  // --- §6.3 code spans ---
203
484
  /**
@@ -205,70 +486,41 @@ class InlineParser {
205
486
  * *exactly* the same length. Unmatched opening backticks are literal text.
206
487
  */
207
488
  parseBackticks(block) {
208
- const ticks = this.match(reTicksHere);
209
- if (null === ticks)
489
+ const scan = scanCodeSpan(this.subject, this.pos);
490
+ if (null === scan)
210
491
  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
- }
492
+ this.pos = scan.end;
493
+ if (scan.closed) {
494
+ const node = new node_ts_1.MdNode('code');
495
+ node.literal = scan.literal;
496
+ block.appendChild(node);
497
+ }
498
+ else {
499
+ // No closing run of the same length: the opener is literal text.
500
+ block.appendChild(text(scan.ticks));
234
501
  }
235
- // No closing run of the same length: the opener is literal text.
236
- this.pos = afterOpenTicks;
237
- block.appendChild(text(ticks));
238
502
  return true;
239
503
  }
240
504
  // --- §6.1 backslash escapes, §6.7 hard line breaks ---
241
505
  parseBackslash(block) {
242
- this.pos += 1;
243
- if (C_NEWLINE === this.peek()) {
244
- this.pos += 1;
506
+ const scan = scanEscape(this.subject, this.pos);
507
+ this.pos = scan.end;
508
+ if ('linebreak' === scan.kind) {
245
509
  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
510
  }
252
511
  else {
253
- block.appendChild(text('\\'));
512
+ block.appendChild(text(scan.literal));
254
513
  }
255
514
  return true;
256
515
  }
257
516
  // --- §6.5 autolinks ---
258
517
  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;
518
+ const scan = scanAngleAutolink(this.subject, this.pos);
519
+ if (null === scan)
520
+ return false;
521
+ this.pos = scan.end;
522
+ block.appendChild(this.makeAutolink(scan.dest, scan.label));
523
+ return true;
272
524
  }
273
525
  makeAutolink(destination, label) {
274
526
  // Autolink text is literal: no backslash escapes, no entity references.
@@ -281,56 +533,19 @@ class InlineParser {
281
533
  }
282
534
  // --- §6.6 raw HTML ---
283
535
  parseHtmlTag(block) {
284
- const m = this.match(reHtmlTag);
285
- if (null === m)
536
+ const scan = scanHtmlTag(this.subject, this.pos);
537
+ if (null === scan)
286
538
  return false;
539
+ this.pos = scan.end;
287
540
  const node = new node_ts_1.MdNode('html_inline');
288
- node.literal = m;
541
+ node.literal = scan.raw;
289
542
  block.appendChild(node);
290
543
  return true;
291
544
  }
292
545
  // --- §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
- */
546
+ /** Classify the run of `cc` starting at `pos` without consuming it. */
303
547
  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 };
548
+ return scanDelimiterRun(this.subject, this.pos, cc);
334
549
  }
335
550
  /** Emit a delimiter run as text and push it on the delimiter stack. */
336
551
  handleDelim(cc, block) {
@@ -487,82 +702,34 @@ class InlineParser {
487
702
  // --- §6.3 links and images ---
488
703
  /** Link title without its delimiters, unescaped; null if none here. */
489
704
  parseLinkTitle() {
490
- const title = this.match(reLinkTitle);
491
- if (null === title)
705
+ const scan = scanLinkTitle(this.subject, this.pos);
706
+ if (null === scan)
492
707
  return null;
493
- return (0, common_ts_1.unescapeString)(title.slice(1, title.length - 1));
708
+ this.pos = scan.end;
709
+ return scan.title;
494
710
  }
495
711
  /**
496
712
  * 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.
713
+ * parentheses and no ASCII control characters or spaces. See
714
+ * `scanLinkDestination` for why the value is decoded but not
715
+ * percent-encoded.
508
716
  */
509
717
  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)
718
+ const scan = scanLinkDestination(this.subject, this.pos);
719
+ if (null === scan)
556
720
  return null;
557
- return (0, common_ts_1.unescapeString)(this.subject.slice(savepos, this.pos));
721
+ this.pos = scan.end;
722
+ return scan.dest;
558
723
  }
559
724
  /** Length of the link label at `pos`, including brackets; 0 if invalid. */
560
725
  parseLinkLabel() {
561
- const m = this.match(reLinkLabel);
562
- // §4.7: at most 999 characters between the brackets.
563
- if (null === m || m.length > 1001)
726
+ const scan = scanLinkLabel(this.subject, this.pos);
727
+ if (null === scan)
564
728
  return 0;
565
- return m.length;
729
+ // The raw match is consumed even when it is too long to be a valid
730
+ // label; callers that need the position back reset it themselves.
731
+ this.pos += scan.rawLength;
732
+ return scan.length;
566
733
  }
567
734
  addBracket(node, index, image) {
568
735
  if (null !== this.brackets)
@@ -630,28 +797,12 @@ class InlineParser {
630
797
  let matched = false;
631
798
  const savepos = this.pos;
632
799
  // 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;
800
+ const tail = scanInlineLinkTail(this.subject, this.pos);
801
+ if (null !== tail) {
802
+ this.pos = tail.end;
803
+ dest = tail.dest;
804
+ title = tail.title;
805
+ matched = true;
655
806
  }
656
807
  if (!matched) {
657
808
  // Reference forms. A second label gives the full form; an empty or
@@ -713,10 +864,11 @@ class InlineParser {
713
864
  }
714
865
  // --- §6.2 entities, §6.8/§6.9 breaks and plain text ---
715
866
  parseEntity(block) {
716
- const m = this.match(reEntityHere);
717
- if (null === m)
867
+ const scan = scanEntity(this.subject, this.pos);
868
+ if (null === scan)
718
869
  return false;
719
- block.appendChild(text((0, common_ts_1.decodeEntity)(m)));
870
+ this.pos = scan.end;
871
+ block.appendChild(text(scan.literal));
720
872
  return true;
721
873
  }
722
874
  parseString(block) {
@@ -734,18 +886,15 @@ class InlineParser {
734
886
  parseNewline(block) {
735
887
  this.pos += 1;
736
888
  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);
889
+ const brk = classifyBreak(null !== lastc && 'text' === lastc.type ? lastc.literal : null);
890
+ if (brk.trim && null !== lastc && null !== lastc.literal) {
742
891
  lastc.literal = lastc.literal.replace(reFinalSpace, '');
743
- block.appendChild(new node_ts_1.MdNode(hardbreak ? 'linebreak' : 'softbreak'));
892
+ block.appendChild(new node_ts_1.MdNode(brk.hard ? 'linebreak' : 'softbreak'));
744
893
  }
745
894
  else {
746
895
  block.appendChild(new node_ts_1.MdNode('softbreak'));
747
896
  }
748
- this.match(reInitialSpace);
897
+ this.pos = skipInitialSpaces(this.subject, this.pos);
749
898
  return true;
750
899
  }
751
900
  /** One inline construct at `pos`; false only at the end of the subject. */
@@ -792,26 +941,45 @@ class InlineParser {
792
941
  res = this.parseString(block);
793
942
  break;
794
943
  }
795
- if (!res) {
796
- // Nothing claimed it: the character is literal text.
797
- this.pos += 1;
798
- block.appendChild(text(String.fromCharCode(c)));
799
- }
944
+ if (!res)
945
+ this.parseLiteralChar(block);
800
946
  return true;
801
947
  }
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.
948
+ /** Nothing claimed the character at `pos`: consume it as literal text. */
949
+ parseLiteralChar(block) {
950
+ const c = this.peek();
951
+ if (-1 === c)
952
+ return false;
953
+ this.pos += 1;
954
+ block.appendChild(text(String.fromCharCode(c)));
955
+ return true;
956
+ }
957
+ /**
958
+ * Reset for one block's raw content; false when the trimmed subject is
959
+ * empty. Trimming is what stops trailing spaces at the end of a block
960
+ * from making a hard break, and drops the leading indentation of the
961
+ * first line.
962
+ */
963
+ beginBlock(block) {
806
964
  this.subject = block.stringContent.trim();
807
965
  this.pos = 0;
808
966
  this.delimiters = null;
809
967
  this.brackets = null;
810
- while (this.parseInline(block))
811
- ;
968
+ return '' !== this.subject;
969
+ }
970
+ /** The tail of a block's inline parse: clear the raw content, resolve
971
+ * emphasis over the whole delimiter stack. */
972
+ finishBlock(block) {
812
973
  block.stringContent = '';
813
974
  this.processEmphasis(null);
814
975
  }
976
+ /** Parse one paragraph or heading's raw content into inline children. */
977
+ parse(block) {
978
+ this.beginBlock(block);
979
+ while (this.parseInline(block))
980
+ ;
981
+ this.finishBlock(block);
982
+ }
815
983
  /**
816
984
  * §4.7: consume leading link reference definitions. Called by the block
817
985
  * phase while finalizing a paragraph, repeatedly, until it returns 0.
@@ -882,6 +1050,7 @@ class InlineParser {
882
1050
  return this.pos - startpos;
883
1051
  }
884
1052
  }
1053
+ exports.InlineParser = InlineParser;
885
1054
  // --- GFM extended autolinks (autolink literals) -----------------------------
886
1055
  //
887
1056
  // GFM recognises `www.x.com`, `https://x.com` and `a@x.com` without the `<…>`
@@ -1303,12 +1472,23 @@ const INLINE_CONTENT_BLOCKS = {
1303
1472
  */
1304
1473
  function parseInlines(doc, refmap, options) {
1305
1474
  const parser = new InlineParser(refmap, options);
1475
+ forEachInlineBlock(doc, options, (node) => parser.parse(node));
1476
+ }
1477
+ /**
1478
+ * Walk the finished block tree, handing every inline content block
1479
+ * (paragraphs, headings, table cells) to `fn` and running the GFM
1480
+ * autolink-literal post-pass after it. This is the one iteration both
1481
+ * inline drivers share — the hand scanner above and the engine path — so
1482
+ * which blocks get inline parsing, and when linkify runs, is decided
1483
+ * exactly once.
1484
+ */
1485
+ function forEachInlineBlock(doc, options, fn) {
1306
1486
  const walker = doc.walker();
1307
1487
  let ev = walker.next();
1308
1488
  while (null !== ev) {
1309
1489
  const node = ev.node;
1310
1490
  if (!ev.entering && true === INLINE_CONTENT_BLOCKS[node.type]) {
1311
- parser.parse(node);
1491
+ fn(node);
1312
1492
  if (options.gfm)
1313
1493
  linkifyAutolinks(node);
1314
1494
  }