@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/block.d.ts +257 -0
- package/dist/block.js +213 -68
- package/dist/block.js.map +1 -1
- package/dist/engine-block.d.ts +69 -0
- package/dist/engine-block.js +99 -0
- package/dist/engine-block.js.map +1 -0
- package/dist/engine-inline.d.ts +25 -0
- package/dist/engine-inline.js +237 -0
- package/dist/engine-inline.js.map +1 -0
- package/dist/inline.d.ts +250 -0
- package/dist/inline.js +395 -215
- package/dist/inline.js.map +1 -1
- package/dist/markdown.d.ts +2 -3
- package/dist/markdown.js +91 -63
- package/dist/markdown.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +5 -5
- package/src/block.ts +224 -75
- package/src/engine-block.ts +151 -0
- package/src/engine-inline.ts +284 -0
- package/src/inline.ts +422 -218
- package/src/markdown.ts +95 -65
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
|
-
|
|
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
|
-
|
|
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
|
|
209
|
-
if (null ===
|
|
489
|
+
const scan = scanCodeSpan(this.subject, this.pos);
|
|
490
|
+
if (null === scan)
|
|
210
491
|
return false;
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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
|
|
243
|
-
|
|
244
|
-
|
|
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
|
-
|
|
260
|
-
if (null
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
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
|
|
285
|
-
if (null ===
|
|
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 =
|
|
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
|
-
|
|
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
|
|
491
|
-
if (null ===
|
|
705
|
+
const scan = scanLinkTitle(this.subject, this.pos);
|
|
706
|
+
if (null === scan)
|
|
492
707
|
return null;
|
|
493
|
-
|
|
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
|
|
511
|
-
if (null
|
|
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
|
-
|
|
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
|
|
562
|
-
|
|
563
|
-
if (null === m || m.length > 1001)
|
|
726
|
+
const scan = scanLinkLabel(this.subject, this.pos);
|
|
727
|
+
if (null === scan)
|
|
564
728
|
return 0;
|
|
565
|
-
|
|
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
|
-
|
|
634
|
-
|
|
635
|
-
this.
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
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
|
|
717
|
-
if (null ===
|
|
867
|
+
const scan = scanEntity(this.subject, this.pos);
|
|
868
|
+
if (null === scan)
|
|
718
869
|
return false;
|
|
719
|
-
|
|
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
|
-
|
|
738
|
-
|
|
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(
|
|
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.
|
|
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
|
-
|
|
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
|
-
/**
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1491
|
+
fn(node);
|
|
1312
1492
|
if (options.gfm)
|
|
1313
1493
|
linkifyAutolinks(node);
|
|
1314
1494
|
}
|