@oh-my-pi/pi-utils 18.4.9 → 18.4.11
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/CHANGELOG.md +27 -0
- package/dist/types/dirs.d.ts +5 -1
- package/dist/types/marked/core.d.ts +24 -0
- package/dist/types/marked/inline-closers.d.ts +119 -0
- package/dist/types/marked/inline-text.d.ts +70 -0
- package/dist/types/math-delimiters.d.ts +22 -0
- package/package.json +2 -2
- package/src/dirs.ts +68 -9
- package/src/marked/core.ts +234 -166
- package/src/marked/inline-closers.ts +434 -0
- package/src/marked/inline-text.ts +321 -0
- package/src/math-delimiters.ts +121 -19
package/src/marked/core.ts
CHANGED
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CloserIndexes,
|
|
3
|
+
canCloseDelimiter,
|
|
4
|
+
canOpenDelimiter,
|
|
5
|
+
escapedAt,
|
|
6
|
+
InlineClosers,
|
|
7
|
+
PUNCTUATION,
|
|
8
|
+
} from "./inline-closers";
|
|
9
|
+
import { TextRun, TextStops } from "./inline-text";
|
|
10
|
+
|
|
1
11
|
/** Token shapes emitted by the Markdown lexer. */
|
|
2
12
|
export namespace Tokens {
|
|
3
13
|
/** A block quote. */
|
|
@@ -237,6 +247,16 @@ export type TokensList = Token[] & { links: Links };
|
|
|
237
247
|
/** Context supplied to extension tokenizers. */
|
|
238
248
|
export interface TokenizerThis {
|
|
239
249
|
lexer: Lexer;
|
|
250
|
+
/**
|
|
251
|
+
* For an inline tokenizer, the whole inline source, such as a paragraph's text. The `src` it receives is the part
|
|
252
|
+
* of it before `end` that starts at `end - src.length`: inside a link label or the text of emphasis, `end` is where
|
|
253
|
+
* that text ends, right before its closer, and a token there must not reach past it. Inline tokenizers get one
|
|
254
|
+
* context object per inline source, the same for every call while the lexer works through it and the labels and
|
|
255
|
+
* emphasis inside it, so a tokenizer can keep state for it in a `WeakMap`.
|
|
256
|
+
*/
|
|
257
|
+
source?: string;
|
|
258
|
+
/** For an inline tokenizer, where the text the lexer works through ends in `source` (see `source`). */
|
|
259
|
+
end?: number;
|
|
240
260
|
}
|
|
241
261
|
/** A tokenizer extension callback. */
|
|
242
262
|
export type TokenizerExtensionFunction = (
|
|
@@ -246,11 +266,25 @@ export type TokenizerExtensionFunction = (
|
|
|
246
266
|
) => Tokens.Generic | undefined;
|
|
247
267
|
/** A tokenizer extension start hint. */
|
|
248
268
|
export type TokenizerStartFunction = (this: TokenizerThis, src: string) => number | void;
|
|
269
|
+
/**
|
|
270
|
+
* An offset form of a start hint: the first index at or after `from` in `src` where the extension's tokenizer could
|
|
271
|
+
* match, or `undefined` (not -1) when there is none; anything else below `from` throws. The test at each index must
|
|
272
|
+
* read `src` only from that index on, so the answer for `from` is also the answer for every later offset up to it,
|
|
273
|
+
* and must still pass where `src` goes on past its end: for a link label or the text of emphasis, the lexer asks on
|
|
274
|
+
* the inline source it lies in, then asks again on the label or text at the index it got, if that lies inside.
|
|
275
|
+
*/
|
|
276
|
+
export type TokenizerStartFromFunction = (this: TokenizerThis, src: string, from: number) => number | undefined;
|
|
249
277
|
/** An inline or block tokenizer extension. */
|
|
250
278
|
export interface TokenizerExtension {
|
|
251
279
|
name: string;
|
|
252
280
|
level: "block" | "inline";
|
|
253
281
|
start?: TokenizerStartFunction;
|
|
282
|
+
/**
|
|
283
|
+
* Replaces `start` for inline text when present. Inline lexing calls it on the whole inline source and reuses
|
|
284
|
+
* the answer until lexing passes it, inside link labels and emphasis too, instead of calling `start` on the rest
|
|
285
|
+
* of the source at every text step, so a hint that stops at its answer keeps a long paragraph linear.
|
|
286
|
+
*/
|
|
287
|
+
startFrom?: TokenizerStartFromFunction;
|
|
254
288
|
tokenizer: TokenizerExtensionFunction;
|
|
255
289
|
childTokens?: string[];
|
|
256
290
|
}
|
|
@@ -341,7 +375,6 @@ const DEFAULTS: MarkedOptions = {
|
|
|
341
375
|
walkTokens: null,
|
|
342
376
|
extensions: null,
|
|
343
377
|
};
|
|
344
|
-
const PUNCTUATION = /[!"#$%&'()*+,\-./:;<=>?@[\\\]^_`{|}~]/;
|
|
345
378
|
|
|
346
379
|
function tokenList(links: Links = Object.create(null)): TokensList {
|
|
347
380
|
const list = [] as unknown as TokensList;
|
|
@@ -379,80 +412,33 @@ function escapeHtml(value: string, encode = true): string {
|
|
|
379
412
|
return out;
|
|
380
413
|
}
|
|
381
414
|
|
|
382
|
-
function findClosingBracket(src: string, start: number, open: string, close: string): number {
|
|
383
|
-
let depth = 0;
|
|
384
|
-
for (let i = start; i < src.length; i++) {
|
|
385
|
-
if (src[i] === "\\") {
|
|
386
|
-
i++;
|
|
387
|
-
continue;
|
|
388
|
-
}
|
|
389
|
-
if (src[i] === open) depth++;
|
|
390
|
-
else if (src[i] === close) {
|
|
391
|
-
if (depth === 0) return i;
|
|
392
|
-
depth--;
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
return -1;
|
|
396
|
-
}
|
|
397
|
-
|
|
398
415
|
function findDelimiter(src: string, delimiter: string, from: number): number {
|
|
399
416
|
let at = src.indexOf(delimiter, from);
|
|
400
|
-
while (at !== -1)
|
|
401
|
-
|
|
402
|
-
for (let i = at - 1; i >= 0 && src[i] === "\\"; i--) escapes++;
|
|
403
|
-
if (escapes % 2 === 0) return at;
|
|
404
|
-
at = src.indexOf(delimiter, at + delimiter.length);
|
|
405
|
-
}
|
|
406
|
-
return -1;
|
|
417
|
+
while (at !== -1 && escapedAt(src, at)) at = src.indexOf(delimiter, at + delimiter.length);
|
|
418
|
+
return at;
|
|
407
419
|
}
|
|
408
420
|
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
421
|
+
/**
|
|
422
|
+
* The autolink at the start of `src`, a suffix of `closers`' source: "<", a URL or an address, ">". A URL runs from its
|
|
423
|
+
* scheme to the first " " or ">", which must be a ">"; an address holds no " ", "<", ">" or second "@". An address
|
|
424
|
+
* that matches after a scheme ends where the URL does, so only the URL is tried there.
|
|
425
|
+
*/
|
|
426
|
+
function autolinkPrefix(src: string, closers: InlineClosers): string | undefined {
|
|
427
|
+
const scheme = /^<(?:https?:\/\/|ftp:\/\/)/i.exec(src);
|
|
428
|
+
if (!scheme) return /^<[^ <>@]+@[^ <>@]+>/.exec(src)?.[0];
|
|
429
|
+
const close = closers.closeUrlAutolink(src, scheme[0].length);
|
|
430
|
+
return close === -1 ? undefined : src.slice(0, close + 1);
|
|
416
431
|
}
|
|
417
432
|
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
const after = src[index + 1] ?? "\n";
|
|
421
|
-
if (before === undefined || /\s/.test(before)) return false;
|
|
422
|
-
if (marker === "_" && /[\p{L}\p{N}]/u.test(before) && /[\p{L}\p{N}]/u.test(after)) return false;
|
|
423
|
-
return !PUNCTUATION.test(before) || /\s/.test(after) || PUNCTUATION.test(after);
|
|
424
|
-
}
|
|
425
|
-
function inlineHtmlPrefix(src: string): string | undefined {
|
|
433
|
+
/** The inline HTML (a tag or a comment) at the start of `src`, a suffix of `closers`' source, or `undefined`. */
|
|
434
|
+
function inlineHtmlPrefix(src: string, closers: InlineClosers): string | undefined {
|
|
426
435
|
if (src.startsWith("<!--")) {
|
|
427
|
-
const end =
|
|
436
|
+
const end = closers.closeComment(src);
|
|
428
437
|
return end === -1 ? undefined : src.slice(0, end + 3);
|
|
429
438
|
}
|
|
430
439
|
if (!/^<\/?[A-Za-z][A-Za-z0-9-]*(?:\s|\/?>)/.test(src)) return undefined;
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
const char = src[i]!;
|
|
434
|
-
if (quote !== "") {
|
|
435
|
-
if (char === quote) quote = "";
|
|
436
|
-
continue;
|
|
437
|
-
}
|
|
438
|
-
if (char === '"' || char === "'") {
|
|
439
|
-
quote = char;
|
|
440
|
-
continue;
|
|
441
|
-
}
|
|
442
|
-
if (char === ">") return src.slice(0, i + 1);
|
|
443
|
-
}
|
|
444
|
-
return undefined;
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
function appendText(tokens: Token[], raw: string, text = raw, escaped = false): void {
|
|
448
|
-
if (raw === "") return;
|
|
449
|
-
const previous = tokens.at(-1);
|
|
450
|
-
if (previous?.type === "text" && previous.tokens === undefined && previous.escaped === escaped) {
|
|
451
|
-
previous.raw += raw;
|
|
452
|
-
previous.text += text;
|
|
453
|
-
return;
|
|
454
|
-
}
|
|
455
|
-
tokens.push({ type: "text", raw, text, escaped });
|
|
440
|
+
const end = closers.closeTag(src);
|
|
441
|
+
return end === -1 ? undefined : src.slice(0, end + 1);
|
|
456
442
|
}
|
|
457
443
|
|
|
458
444
|
/** Tokenizes the built-in inline Markdown surface. */
|
|
@@ -473,15 +459,60 @@ export class Tokenizer {
|
|
|
473
459
|
}
|
|
474
460
|
}
|
|
475
461
|
|
|
476
|
-
|
|
462
|
+
// A nested inline source (a link label, the text of emphasis) shares the closers, text stops and tokenizer context of
|
|
463
|
+
// the source it lies in, which are offered to the next inline lex on that lexer. They are offered for the call that
|
|
464
|
+
// lexes the nested source through `lexer.inlineTokens`, which a subclass may override; the first lex that call
|
|
465
|
+
// reaches takes them if it lexes exactly that string, or else starts a root source.
|
|
466
|
+
let offered: { lexer: Lexer; nested: NestedMatch; outer: InlineLex } | undefined;
|
|
467
|
+
|
|
468
|
+
/** Starts the lex of `src` on `lexer`: of the nested source offered for it, else of a root source. Clears the offer. */
|
|
469
|
+
function openInlineSource(src: string, lexer: Lexer, output: Token[]): InlineLex {
|
|
470
|
+
const offer = offered;
|
|
471
|
+
offered = undefined;
|
|
472
|
+
const shared = offer?.lexer === lexer && offer.nested.src === src ? offer : undefined;
|
|
473
|
+
const closers = shared ? shared.nested.closers : new InlineClosers(new CloserIndexes(src), src.length);
|
|
474
|
+
return {
|
|
475
|
+
src,
|
|
476
|
+
lexer,
|
|
477
|
+
output,
|
|
478
|
+
closers,
|
|
479
|
+
stops: shared ? shared.outer.stops.nested(src, closers.end) : new TextStops(src, lexer),
|
|
480
|
+
textRun: new TextRun(src),
|
|
481
|
+
context: shared ? shared.outer.context : { lexer, source: src, end: src.length },
|
|
482
|
+
rest: src,
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* Whether any label can resolve in `links`. The map every lexer starts with has no prototype, so it resolves none
|
|
488
|
+
* until a definition is added.
|
|
489
|
+
*/
|
|
490
|
+
function hasDefinitions(links: Links): boolean {
|
|
491
|
+
if (Object.getPrototypeOf(links) !== null) return true;
|
|
492
|
+
for (const _label in links) return true;
|
|
493
|
+
return false;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* A link, image, emphasis or strong emphasis token whose `tokens` are still to be lexed from `src`, a part of the
|
|
498
|
+
* source that shares `closers`. `inlineTokens` lexes it once the rule that matched it has returned.
|
|
499
|
+
*/
|
|
500
|
+
interface NestedMatch {
|
|
501
|
+
token: Tokens.Link | Tokens.Image | Tokens.Em | Tokens.Strong;
|
|
502
|
+
src: string;
|
|
503
|
+
closers: InlineClosers;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/** Matches a link or image at the start of `src`, a suffix of `closers`' source. */
|
|
507
|
+
function matchLink(src: string, lexer: Lexer, closers: InlineClosers): NestedMatch | undefined {
|
|
477
508
|
const image = src.startsWith("![");
|
|
478
509
|
if (!(image || src.startsWith("["))) return undefined;
|
|
479
510
|
const labelStart = image ? 2 : 1;
|
|
480
|
-
const labelEnd =
|
|
511
|
+
const labelEnd = closers.closeSquare(src, labelStart);
|
|
481
512
|
if (labelEnd === -1) return undefined;
|
|
482
513
|
const label = src.slice(labelStart, labelEnd);
|
|
483
514
|
if (src[labelEnd + 1] === "(") {
|
|
484
|
-
const destinationEnd =
|
|
515
|
+
const destinationEnd = closers.closeRound(src, labelEnd + 2);
|
|
485
516
|
if (destinationEnd === -1) return undefined;
|
|
486
517
|
const inside = src.slice(labelEnd + 2, destinationEnd).trim();
|
|
487
518
|
let href = inside;
|
|
@@ -491,31 +522,92 @@ function matchLink(src: string, lexer: Lexer): Tokens.Link | Tokens.Image | unde
|
|
|
491
522
|
href = cleanUrl(titleMatch[1]!);
|
|
492
523
|
title = titleMatch[2] ?? titleMatch[3] ?? titleMatch[4] ?? null;
|
|
493
524
|
const raw = src.slice(0, destinationEnd + 1);
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
:
|
|
525
|
+
// An image's text is its label unescaped. Nested images each unescape their label, so a label without an escape
|
|
526
|
+
// is taken as it is instead of read again.
|
|
527
|
+
return {
|
|
528
|
+
token: image
|
|
529
|
+
? {
|
|
530
|
+
type: "image",
|
|
531
|
+
raw,
|
|
532
|
+
href,
|
|
533
|
+
title,
|
|
534
|
+
text: closers.escapeWithin(src, labelStart, labelEnd) ? unescapeMarkdown(label) : label,
|
|
535
|
+
tokens: [],
|
|
536
|
+
}
|
|
537
|
+
: { type: "link", raw, href, title, text: label, tokens: [] },
|
|
538
|
+
src: label,
|
|
539
|
+
closers: closers.nested(src, labelEnd),
|
|
540
|
+
};
|
|
498
541
|
}
|
|
499
542
|
let rawEnd = labelEnd + 1;
|
|
500
|
-
let
|
|
543
|
+
let refStart = labelStart;
|
|
544
|
+
let refEnd = labelEnd;
|
|
501
545
|
if (src[rawEnd] === "[") {
|
|
502
|
-
const
|
|
503
|
-
if (
|
|
504
|
-
|
|
505
|
-
|
|
546
|
+
const end = closers.closeSquare(src, rawEnd + 1);
|
|
547
|
+
if (end === -1) return undefined;
|
|
548
|
+
if (end > rawEnd + 1) {
|
|
549
|
+
refStart = rawEnd + 1;
|
|
550
|
+
refEnd = end;
|
|
551
|
+
}
|
|
552
|
+
rawEnd = end + 1;
|
|
506
553
|
}
|
|
507
|
-
|
|
554
|
+
// Definition labels come from `[^\]]+`, so a reference holding an unescaped "]" names none. Skip normalizing it,
|
|
555
|
+
// and every reference while there are no definitions.
|
|
556
|
+
const links = lexer.tokens.links;
|
|
557
|
+
if (!hasDefinitions(links) || closers.squareCloserWithin(src, refStart, refEnd)) return undefined;
|
|
558
|
+
const def = links[src.slice(refStart, refEnd).replace(/\s+/g, " ").toLowerCase()];
|
|
508
559
|
if (!def) return undefined;
|
|
509
560
|
const raw = src.slice(0, rawEnd);
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
561
|
+
return {
|
|
562
|
+
token: image
|
|
563
|
+
? {
|
|
564
|
+
type: "image",
|
|
565
|
+
raw,
|
|
566
|
+
href: def.href,
|
|
567
|
+
title: def.title ?? null,
|
|
568
|
+
text: closers.escapeWithin(src, labelStart, labelEnd) ? unescapeMarkdown(label) : label,
|
|
569
|
+
tokens: [],
|
|
570
|
+
}
|
|
571
|
+
: { type: "link", raw, href: def.href, title: def.title ?? null, text: label, tokens: [] },
|
|
572
|
+
src: label,
|
|
573
|
+
closers: closers.nested(src, labelEnd),
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
/** Matches emphasis or strong emphasis at the start of `rest`, a suffix of `closers`' source, lexed after `output`. */
|
|
578
|
+
function matchEmphasis(rest: string, output: Token[], closers: InlineClosers): NestedMatch | undefined {
|
|
579
|
+
const marker = rest[0];
|
|
580
|
+
const previous = output.at(-1)?.raw.at(-1) ?? "\n";
|
|
581
|
+
if (marker !== "*" && marker !== "_") return undefined;
|
|
582
|
+
if (rest.startsWith(marker.repeat(3)) && canOpenDelimiter(rest, 0, 3, marker, previous)) {
|
|
583
|
+
const end = findDelimiter(rest, marker.repeat(3), 3);
|
|
584
|
+
if (end !== -1 && canCloseDelimiter(rest, end, marker)) {
|
|
585
|
+
// The text between the delimiters inside the outer two markers of each: `**inner**`.
|
|
586
|
+
const text = rest.slice(1, end + 2);
|
|
587
|
+
return {
|
|
588
|
+
token: { type: "em", raw: rest.slice(0, end + 3), text, tokens: [] },
|
|
589
|
+
src: text,
|
|
590
|
+
closers: closers.nested(rest, end + 2),
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
const width = rest[1] === marker ? 2 : 1;
|
|
595
|
+
if (!canOpenDelimiter(rest, 0, width, marker, previous)) return undefined;
|
|
596
|
+
const end = closers.closeEmphasis(rest, marker, width);
|
|
597
|
+
if (end === -1) return undefined;
|
|
598
|
+
const raw = rest.slice(0, end + width);
|
|
599
|
+
const text = rest.slice(width, end);
|
|
600
|
+
return {
|
|
601
|
+
token: width === 2 ? { type: "strong", raw, text, tokens: [] } : { type: "em", raw, text, tokens: [] },
|
|
602
|
+
src: text,
|
|
603
|
+
closers: closers.nested(rest, end),
|
|
604
|
+
};
|
|
514
605
|
}
|
|
515
606
|
|
|
516
607
|
function trimBareUrl(candidate: string): string {
|
|
517
|
-
let
|
|
518
|
-
while (
|
|
608
|
+
let end = candidate.length;
|
|
609
|
+
while (end > 0 && ".,:;!?".includes(candidate[end - 1]!)) end--;
|
|
610
|
+
let out = candidate.slice(0, end);
|
|
519
611
|
let opens = 0;
|
|
520
612
|
let closes = 0;
|
|
521
613
|
for (const char of out) {
|
|
@@ -529,12 +621,43 @@ function trimBareUrl(candidate: string): string {
|
|
|
529
621
|
return out;
|
|
530
622
|
}
|
|
531
623
|
|
|
532
|
-
|
|
533
|
-
|
|
624
|
+
/** The lex of one inline source, carried across the nested sources lexed inside it. */
|
|
625
|
+
interface InlineLex {
|
|
626
|
+
readonly src: string;
|
|
627
|
+
readonly lexer: Lexer;
|
|
628
|
+
readonly output: Token[];
|
|
629
|
+
readonly closers: InlineClosers;
|
|
630
|
+
readonly stops: TextStops;
|
|
631
|
+
readonly textRun: TextRun;
|
|
632
|
+
readonly context: TokenizerThis;
|
|
633
|
+
/** The part of `src` not lexed yet, always a suffix of it. */
|
|
634
|
+
rest: string;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
// Each level of nested links or emphasis keeps only this loop's small frame on the stack while the level inside it
|
|
638
|
+
// is lexed: the rules run in `lexToNested`, which returns each link, image or emphasis before its content is lexed.
|
|
639
|
+
function inlineTokens(src: string, lexer: Lexer, output: Token[]): Token[] {
|
|
640
|
+
const lex = openInlineSource(src, lexer, output);
|
|
641
|
+
for (let nested = lexToNested(lex); nested; nested = lexToNested(lex)) {
|
|
642
|
+
offered = { lexer, nested, outer: lex };
|
|
643
|
+
nested.token.tokens = lexer.inlineTokens(nested.src);
|
|
644
|
+
// An override that returned without lexing the text leaves its offer to no later lex of an equal string.
|
|
645
|
+
offered = undefined;
|
|
646
|
+
output.push(nested.token);
|
|
647
|
+
}
|
|
648
|
+
return output;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
/** Lexes `lex.rest` up to the next link, image or emphasis, which it returns with `lex.rest` past it, or to its end. */
|
|
652
|
+
function lexToNested(lex: InlineLex): NestedMatch | undefined {
|
|
653
|
+
const { src, lexer, output, closers, stops, textRun, context } = lex;
|
|
654
|
+
// The context is shared with the sources nested in this one, which move its end while they are lexed.
|
|
655
|
+
context.end = closers.end;
|
|
656
|
+
let rest = lex.rest;
|
|
534
657
|
while (rest !== "") {
|
|
535
658
|
let custom: Tokens.Generic | undefined;
|
|
536
659
|
for (const extension of lexer.extensions.inline) {
|
|
537
|
-
custom = extension.tokenizer.call(
|
|
660
|
+
custom = extension.tokenizer.call(context, rest, output);
|
|
538
661
|
if (custom?.raw) break;
|
|
539
662
|
}
|
|
540
663
|
if (custom?.raw) {
|
|
@@ -556,81 +679,35 @@ function inlineTokens(src: string, lexer: Lexer, output: Token[] = []): Token[]
|
|
|
556
679
|
continue;
|
|
557
680
|
}
|
|
558
681
|
if (rest[0] === "`") {
|
|
559
|
-
const
|
|
560
|
-
const end =
|
|
682
|
+
const width = closers.codeOpenerWidth(rest);
|
|
683
|
+
const end = closers.closeCode(rest, width);
|
|
561
684
|
if (end !== -1) {
|
|
562
|
-
const raw = rest.slice(0, end +
|
|
563
|
-
let text = rest.slice(
|
|
685
|
+
const raw = rest.slice(0, end + width);
|
|
686
|
+
let text = rest.slice(width, end).replace(/\n/g, " ");
|
|
564
687
|
if (/^ .* $/.test(text) && text.trim() !== "") text = text.slice(1, -1);
|
|
565
688
|
output.push({ type: "codespan", raw, text });
|
|
566
689
|
rest = rest.slice(raw.length);
|
|
567
690
|
continue;
|
|
568
691
|
}
|
|
569
692
|
}
|
|
570
|
-
const auto =
|
|
693
|
+
const auto = autolinkPrefix(rest, closers);
|
|
571
694
|
if (auto) {
|
|
572
|
-
const text = auto
|
|
695
|
+
const text = auto.slice(1, -1);
|
|
573
696
|
const href = text.includes("@") && !/^[a-z][a-z+.-]*:\/\//i.test(text) ? `mailto:${text}` : text;
|
|
574
|
-
output.push({ type: "link", raw: auto
|
|
575
|
-
rest = rest.slice(auto
|
|
697
|
+
output.push({ type: "link", raw: auto, text, href, tokens: [{ type: "text", raw: text, text }] });
|
|
698
|
+
rest = rest.slice(auto.length);
|
|
576
699
|
continue;
|
|
577
700
|
}
|
|
578
|
-
const html = inlineHtmlPrefix(rest);
|
|
701
|
+
const html = inlineHtmlPrefix(rest, closers);
|
|
579
702
|
if (html) {
|
|
580
703
|
output.push({ type: "html", raw: html, inLink: false, inRawBlock: false, block: false, text: html });
|
|
581
704
|
rest = rest.slice(html.length);
|
|
582
705
|
continue;
|
|
583
706
|
}
|
|
584
|
-
const
|
|
585
|
-
if (
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
continue;
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
const marker = rest[0];
|
|
592
|
-
const previous = output.at(-1)?.raw.at(-1) ?? "\n";
|
|
593
|
-
if (
|
|
594
|
-
(marker === "*" || marker === "_") &&
|
|
595
|
-
rest.startsWith(marker.repeat(3)) &&
|
|
596
|
-
canOpenDelimiter(rest, 0, 3, marker, previous)
|
|
597
|
-
) {
|
|
598
|
-
const end = findDelimiter(rest, marker.repeat(3), 3);
|
|
599
|
-
if (end !== -1 && canCloseDelimiter(rest, end, marker)) {
|
|
600
|
-
const raw = rest.slice(0, end + 3);
|
|
601
|
-
const inner = rest.slice(3, end);
|
|
602
|
-
const text = `${marker.repeat(2)}${inner}${marker.repeat(2)}`;
|
|
603
|
-
output.push({ type: "em", raw, text, tokens: lexer.inlineTokens(text) });
|
|
604
|
-
rest = rest.slice(raw.length);
|
|
605
|
-
continue;
|
|
606
|
-
}
|
|
607
|
-
}
|
|
608
|
-
if (
|
|
609
|
-
(marker === "*" || marker === "_") &&
|
|
610
|
-
canOpenDelimiter(rest, 0, rest[1] === marker ? 2 : 1, marker, previous)
|
|
611
|
-
) {
|
|
612
|
-
const width = rest[1] === marker ? 2 : 1;
|
|
613
|
-
const delimiter = marker.repeat(width);
|
|
614
|
-
let end = findDelimiter(rest, delimiter, width);
|
|
615
|
-
let nested = 0;
|
|
616
|
-
while (end !== -1) {
|
|
617
|
-
if (!canCloseDelimiter(rest, end, marker) && canOpenDelimiter(rest, end, width, marker)) {
|
|
618
|
-
nested++;
|
|
619
|
-
} else if (canCloseDelimiter(rest, end, marker) && nested > 0) {
|
|
620
|
-
nested--;
|
|
621
|
-
} else if (canCloseDelimiter(rest, end, marker)) {
|
|
622
|
-
break;
|
|
623
|
-
}
|
|
624
|
-
end = findDelimiter(rest, delimiter, end + width);
|
|
625
|
-
}
|
|
626
|
-
if (end !== -1) {
|
|
627
|
-
const raw = rest.slice(0, end + width);
|
|
628
|
-
const text = rest.slice(width, end);
|
|
629
|
-
const tokens = lexer.inlineTokens(text);
|
|
630
|
-
output.push(width === 2 ? { type: "strong", raw, text, tokens } : { type: "em", raw, text, tokens });
|
|
631
|
-
rest = rest.slice(raw.length);
|
|
632
|
-
continue;
|
|
633
|
-
}
|
|
707
|
+
const nested = matchLink(rest, lexer, closers) ?? matchEmphasis(rest, output, closers);
|
|
708
|
+
if (nested) {
|
|
709
|
+
lex.rest = rest.slice(nested.token.raw.length);
|
|
710
|
+
return nested;
|
|
634
711
|
}
|
|
635
712
|
if (rest.startsWith("~~")) {
|
|
636
713
|
let del: Tokens.Del | undefined | false;
|
|
@@ -643,14 +720,17 @@ function inlineTokens(src: string, lexer: Lexer, output: Token[] = []): Token[]
|
|
|
643
720
|
continue;
|
|
644
721
|
}
|
|
645
722
|
}
|
|
723
|
+
// `rest` is always a suffix of `src`, so its offset follows from the lengths.
|
|
724
|
+
const pos = src.length - rest.length;
|
|
646
725
|
let url: Tokens.Link | undefined | false;
|
|
647
726
|
const urlOverride = lexer.tokenizerOverrides.url;
|
|
648
727
|
if (urlOverride) url = urlOverride.call(lexer.tokenizer, rest);
|
|
649
728
|
if (!urlOverride || url === false) {
|
|
650
|
-
const match =
|
|
651
|
-
/^(?:(?:https?:\/\/|ftp:\/\/|www\.)[^\s<]+|[A-Za-z0-9._+-]+@[A-Za-z0-9_-]+(?:\.[A-Za-z0-9_-]+)+)/i.exec(
|
|
652
|
-
|
|
653
|
-
|
|
729
|
+
const match = stops.bareUrlCanStart(pos)
|
|
730
|
+
? /^(?:(?:https?:\/\/|ftp:\/\/|www\.)[^\s<]+|[A-Za-z0-9._+-]+@[A-Za-z0-9_-]+(?:\.[A-Za-z0-9_-]+)+)/i.exec(
|
|
731
|
+
rest,
|
|
732
|
+
)
|
|
733
|
+
: null;
|
|
654
734
|
if (match) {
|
|
655
735
|
const text = trimBareUrl(match[0]);
|
|
656
736
|
const href =
|
|
@@ -668,24 +748,12 @@ function inlineTokens(src: string, lexer: Lexer, output: Token[] = []): Token[]
|
|
|
668
748
|
continue;
|
|
669
749
|
}
|
|
670
750
|
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
const at = rest.indexOf(char, 1);
|
|
674
|
-
if (at !== -1 && at < next) next = at;
|
|
675
|
-
}
|
|
676
|
-
const urlAt = /(?:https?:\/\/|ftp:\/\/|www\.|[A-Za-z0-9._+-]+@)/i.exec(rest.slice(1));
|
|
677
|
-
if (urlAt && urlAt.index + 1 < next) next = urlAt.index + 1;
|
|
678
|
-
const hardBreak = /(?: {2,}|\\)\n/.exec(rest.slice(1));
|
|
679
|
-
if (hardBreak && hardBreak.index + 1 < next) next = hardBreak.index + 1;
|
|
680
|
-
for (const extension of lexer.extensions.inline) {
|
|
681
|
-
const at = extension.start?.call({ lexer }, rest);
|
|
682
|
-
if (typeof at === "number" && at > 0 && at < next) next = at;
|
|
683
|
-
}
|
|
684
|
-
if (next === 0) next = 1;
|
|
685
|
-
appendText(output, rest.slice(0, next));
|
|
751
|
+
const next = stops.textLength(rest);
|
|
752
|
+
textRun.append(output, pos, pos + next);
|
|
686
753
|
rest = rest.slice(next);
|
|
687
754
|
}
|
|
688
|
-
|
|
755
|
+
lex.rest = rest;
|
|
756
|
+
return undefined;
|
|
689
757
|
}
|
|
690
758
|
|
|
691
759
|
function splitTableRow(line: string): string[] {
|