@oh-my-pi/pi-utils 17.2.8 → 17.2.10
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 +32 -0
- package/dist/types/acp/connection.d.ts +118 -0
- package/dist/types/acp/protocol.d.ts +526 -0
- package/dist/types/acp/schema.d.ts +41 -0
- package/dist/types/acp/stream.d.ts +8 -0
- package/dist/types/acp/transport.d.ts +81 -0
- package/dist/types/acp.d.ts +6 -0
- package/dist/types/browsers.d.ts +68 -0
- package/dist/types/chalk.d.ts +125 -0
- package/dist/types/dates.d.ts +7 -0
- package/dist/types/docx/converter.d.ts +46 -0
- package/dist/types/docx/xml.d.ts +26 -0
- package/dist/types/docx/zip.d.ts +6 -0
- package/dist/types/docx.d.ts +11 -0
- package/dist/types/dom/core.d.ts +431 -0
- package/dist/types/dom/parser.d.ts +7 -0
- package/dist/types/dom/selector.d.ts +5 -0
- package/dist/types/dom.d.ts +5 -0
- package/dist/types/headers.d.ts +34 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/logger/rotating-file.d.ts +18 -0
- package/dist/types/lru.d.ts +46 -0
- package/dist/types/marked/core.d.ts +445 -0
- package/dist/types/marked.d.ts +2 -0
- package/dist/types/postmortem.d.ts +14 -0
- package/dist/types/procmgr.d.ts +16 -0
- package/dist/types/prompt.d.ts +2 -2
- package/dist/types/readability/readability.d.ts +9 -0
- package/dist/types/readability/readerable.d.ts +10 -0
- package/dist/types/readability/types.d.ts +70 -0
- package/dist/types/readability.d.ts +4 -0
- package/dist/types/template.d.ts +62 -0
- package/dist/types/turndown/gfm.d.ts +11 -0
- package/dist/types/turndown/html.d.ts +5 -0
- package/dist/types/turndown/service.d.ts +21 -0
- package/dist/types/turndown/types.d.ts +70 -0
- package/dist/types/turndown.d.ts +4 -0
- package/dist/types/version.d.ts +18 -0
- package/dist/types/vterm/buffer.d.ts +99 -0
- package/dist/types/vterm/terminal.d.ts +44 -0
- package/dist/types/vterm.d.ts +8 -0
- package/dist/types/xml.d.ts +31 -0
- package/package.json +2 -5
- package/src/acp/connection.ts +344 -0
- package/src/acp/protocol.ts +466 -0
- package/src/acp/schema.ts +160 -0
- package/src/acp/stream.ts +82 -0
- package/src/acp/transport.ts +213 -0
- package/src/acp.ts +6 -0
- package/src/browsers.ts +501 -0
- package/src/chalk.ts +312 -0
- package/src/dates.ts +194 -0
- package/src/docx/converter.ts +681 -0
- package/src/docx/xml.ts +166 -0
- package/src/docx/zip.ts +87 -0
- package/src/docx.ts +20 -0
- package/src/dom/core.ts +1254 -0
- package/src/dom/parser.ts +370 -0
- package/src/dom/selector.ts +290 -0
- package/src/dom.ts +33 -0
- package/src/fetch-retry.ts +7 -1
- package/src/frontmatter.ts +1 -1
- package/src/headers.ts +167 -0
- package/src/index.ts +1 -0
- package/src/logger/rotating-file.ts +149 -0
- package/src/logger.ts +12 -28
- package/src/lru.ts +185 -0
- package/src/marked/core.ts +1576 -0
- package/src/marked.ts +2 -0
- package/src/postmortem.ts +44 -1
- package/src/procmgr.ts +21 -4
- package/src/prompt.ts +5 -22
- package/src/readability/readability.ts +533 -0
- package/src/readability/readerable.ts +51 -0
- package/src/readability/types.ts +72 -0
- package/src/readability.ts +11 -0
- package/src/template.ts +586 -0
- package/src/turndown/gfm.ts +106 -0
- package/src/turndown/html.ts +257 -0
- package/src/turndown/service.ts +334 -0
- package/src/turndown/types.ts +81 -0
- package/src/turndown.ts +5 -0
- package/src/version.ts +99 -0
- package/src/vterm/buffer.ts +218 -0
- package/src/vterm/terminal.ts +773 -0
- package/src/vterm.ts +8 -0
- package/src/which.ts +6 -4
- package/src/xml.ts +313 -0
- package/src/winston-daily-rotate-file.d.ts +0 -6
|
@@ -0,0 +1,1576 @@
|
|
|
1
|
+
/** Token shapes emitted by the Markdown lexer. */
|
|
2
|
+
export namespace Tokens {
|
|
3
|
+
/** A block quote. */
|
|
4
|
+
export interface Blockquote {
|
|
5
|
+
type: "blockquote";
|
|
6
|
+
raw: string;
|
|
7
|
+
text: string;
|
|
8
|
+
tokens: Token[];
|
|
9
|
+
}
|
|
10
|
+
/** A hard line break. */
|
|
11
|
+
export interface Br {
|
|
12
|
+
type: "br";
|
|
13
|
+
raw: string;
|
|
14
|
+
}
|
|
15
|
+
/** A task-list checkbox. */
|
|
16
|
+
export interface Checkbox {
|
|
17
|
+
type: "checkbox";
|
|
18
|
+
raw: string;
|
|
19
|
+
checked: boolean;
|
|
20
|
+
}
|
|
21
|
+
/** A code block. */
|
|
22
|
+
export interface Code {
|
|
23
|
+
type: "code";
|
|
24
|
+
raw: string;
|
|
25
|
+
codeBlockStyle?: "indented";
|
|
26
|
+
lang?: string;
|
|
27
|
+
text: string;
|
|
28
|
+
escaped?: boolean;
|
|
29
|
+
}
|
|
30
|
+
/** An inline code span. */
|
|
31
|
+
export interface Codespan {
|
|
32
|
+
type: "codespan";
|
|
33
|
+
raw: string;
|
|
34
|
+
text: string;
|
|
35
|
+
}
|
|
36
|
+
/** A reference-link definition. */
|
|
37
|
+
export interface Def {
|
|
38
|
+
type: "def";
|
|
39
|
+
raw: string;
|
|
40
|
+
tag: string;
|
|
41
|
+
href: string;
|
|
42
|
+
title?: string;
|
|
43
|
+
}
|
|
44
|
+
/** Deleted text. */
|
|
45
|
+
export interface Del {
|
|
46
|
+
type: "del";
|
|
47
|
+
raw: string;
|
|
48
|
+
text: string;
|
|
49
|
+
tokens: Token[];
|
|
50
|
+
}
|
|
51
|
+
/** Emphasized text. */
|
|
52
|
+
export interface Em {
|
|
53
|
+
type: "em";
|
|
54
|
+
raw: string;
|
|
55
|
+
text: string;
|
|
56
|
+
tokens: Token[];
|
|
57
|
+
}
|
|
58
|
+
/** An escaped punctuation character. */
|
|
59
|
+
export interface Escape {
|
|
60
|
+
type: "escape";
|
|
61
|
+
raw: string;
|
|
62
|
+
text: string;
|
|
63
|
+
}
|
|
64
|
+
/** A custom extension token. */
|
|
65
|
+
export interface Generic {
|
|
66
|
+
// Upstream marked deliberately permits arbitrary extension-token fields.
|
|
67
|
+
[key: string]: any;
|
|
68
|
+
type: string;
|
|
69
|
+
raw: string;
|
|
70
|
+
tokens?: Token[];
|
|
71
|
+
}
|
|
72
|
+
/** A heading. */
|
|
73
|
+
export interface Heading {
|
|
74
|
+
type: "heading";
|
|
75
|
+
raw: string;
|
|
76
|
+
depth: number;
|
|
77
|
+
text: string;
|
|
78
|
+
tokens: Token[];
|
|
79
|
+
}
|
|
80
|
+
/** A horizontal rule. */
|
|
81
|
+
export interface Hr {
|
|
82
|
+
type: "hr";
|
|
83
|
+
raw: string;
|
|
84
|
+
}
|
|
85
|
+
/** Raw block or inline HTML. */
|
|
86
|
+
export interface HTML {
|
|
87
|
+
type: "html";
|
|
88
|
+
raw: string;
|
|
89
|
+
pre?: boolean;
|
|
90
|
+
text: string;
|
|
91
|
+
block: boolean;
|
|
92
|
+
inLink?: boolean;
|
|
93
|
+
inRawBlock?: boolean;
|
|
94
|
+
}
|
|
95
|
+
/** An image. */
|
|
96
|
+
export interface Image {
|
|
97
|
+
type: "image";
|
|
98
|
+
raw: string;
|
|
99
|
+
href: string;
|
|
100
|
+
title: string | null;
|
|
101
|
+
text: string;
|
|
102
|
+
tokens: Token[];
|
|
103
|
+
}
|
|
104
|
+
/** A hyperlink. */
|
|
105
|
+
export interface Link {
|
|
106
|
+
type: "link";
|
|
107
|
+
raw: string;
|
|
108
|
+
href: string;
|
|
109
|
+
title?: string | null;
|
|
110
|
+
text: string;
|
|
111
|
+
tokens: Token[];
|
|
112
|
+
}
|
|
113
|
+
/** A list. */
|
|
114
|
+
export interface List {
|
|
115
|
+
type: "list";
|
|
116
|
+
raw: string;
|
|
117
|
+
ordered: boolean;
|
|
118
|
+
start: number | "";
|
|
119
|
+
loose: boolean;
|
|
120
|
+
items: ListItem[];
|
|
121
|
+
}
|
|
122
|
+
/** A list item. */
|
|
123
|
+
export interface ListItem {
|
|
124
|
+
type: "list_item";
|
|
125
|
+
raw: string;
|
|
126
|
+
task: boolean;
|
|
127
|
+
checked?: boolean;
|
|
128
|
+
loose: boolean;
|
|
129
|
+
text: string;
|
|
130
|
+
tokens: Token[];
|
|
131
|
+
}
|
|
132
|
+
/** A paragraph. */
|
|
133
|
+
export interface Paragraph {
|
|
134
|
+
type: "paragraph";
|
|
135
|
+
raw: string;
|
|
136
|
+
pre?: boolean;
|
|
137
|
+
text: string;
|
|
138
|
+
tokens: Token[];
|
|
139
|
+
}
|
|
140
|
+
/** Blank block space. */
|
|
141
|
+
export interface Space {
|
|
142
|
+
type: "space";
|
|
143
|
+
raw: string;
|
|
144
|
+
}
|
|
145
|
+
/** Strongly emphasized text. */
|
|
146
|
+
export interface Strong {
|
|
147
|
+
type: "strong";
|
|
148
|
+
raw: string;
|
|
149
|
+
text: string;
|
|
150
|
+
tokens: Token[];
|
|
151
|
+
}
|
|
152
|
+
/** A GFM table cell. */
|
|
153
|
+
export interface TableCell {
|
|
154
|
+
text: string;
|
|
155
|
+
tokens: Token[];
|
|
156
|
+
header: boolean;
|
|
157
|
+
align: "center" | "left" | "right" | null;
|
|
158
|
+
}
|
|
159
|
+
/** A GFM table. */
|
|
160
|
+
export interface Table {
|
|
161
|
+
type: "table";
|
|
162
|
+
raw: string;
|
|
163
|
+
align: Array<"center" | "left" | "right" | null>;
|
|
164
|
+
header: TableCell[];
|
|
165
|
+
rows: TableCell[][];
|
|
166
|
+
}
|
|
167
|
+
/** Plain text. */
|
|
168
|
+
export interface Text {
|
|
169
|
+
type: "text";
|
|
170
|
+
raw: string;
|
|
171
|
+
text: string;
|
|
172
|
+
tokens?: Token[];
|
|
173
|
+
escaped?: boolean;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
type KnownToken =
|
|
178
|
+
| Tokens.Blockquote
|
|
179
|
+
| Tokens.Br
|
|
180
|
+
| Tokens.Checkbox
|
|
181
|
+
| Tokens.Code
|
|
182
|
+
| Tokens.Codespan
|
|
183
|
+
| Tokens.Def
|
|
184
|
+
| Tokens.Del
|
|
185
|
+
| Tokens.Em
|
|
186
|
+
| Tokens.Escape
|
|
187
|
+
| Tokens.Heading
|
|
188
|
+
| Tokens.Hr
|
|
189
|
+
| Tokens.HTML
|
|
190
|
+
| Tokens.Image
|
|
191
|
+
| Tokens.Link
|
|
192
|
+
| Tokens.List
|
|
193
|
+
| Tokens.ListItem
|
|
194
|
+
| Tokens.Paragraph
|
|
195
|
+
| Tokens.Space
|
|
196
|
+
| Tokens.Strong
|
|
197
|
+
| Tokens.Table
|
|
198
|
+
| Tokens.Text;
|
|
199
|
+
|
|
200
|
+
/** A built-in or extension Markdown token. */
|
|
201
|
+
export type Token = KnownToken | Tokens.Generic;
|
|
202
|
+
|
|
203
|
+
function isKnownToken(token: Token): token is KnownToken {
|
|
204
|
+
switch (token.type) {
|
|
205
|
+
case "blockquote":
|
|
206
|
+
case "br":
|
|
207
|
+
case "checkbox":
|
|
208
|
+
case "code":
|
|
209
|
+
case "codespan":
|
|
210
|
+
case "def":
|
|
211
|
+
case "del":
|
|
212
|
+
case "em":
|
|
213
|
+
case "escape":
|
|
214
|
+
case "heading":
|
|
215
|
+
case "hr":
|
|
216
|
+
case "html":
|
|
217
|
+
case "image":
|
|
218
|
+
case "link":
|
|
219
|
+
case "list":
|
|
220
|
+
case "list_item":
|
|
221
|
+
case "paragraph":
|
|
222
|
+
case "space":
|
|
223
|
+
case "strong":
|
|
224
|
+
case "table":
|
|
225
|
+
case "text":
|
|
226
|
+
return true;
|
|
227
|
+
default:
|
|
228
|
+
return false;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** Reference definitions collected while lexing. */
|
|
233
|
+
export type Links = Record<string, Pick<Tokens.Link | Tokens.Image, "href" | "title">>;
|
|
234
|
+
/** A token array carrying its reference-definition map. */
|
|
235
|
+
export type TokensList = Token[] & { links: Links };
|
|
236
|
+
|
|
237
|
+
/** Context supplied to extension tokenizers. */
|
|
238
|
+
export interface TokenizerThis {
|
|
239
|
+
lexer: Lexer;
|
|
240
|
+
}
|
|
241
|
+
/** A tokenizer extension callback. */
|
|
242
|
+
export type TokenizerExtensionFunction = (
|
|
243
|
+
this: TokenizerThis,
|
|
244
|
+
src: string,
|
|
245
|
+
tokens: Token[] | TokensList,
|
|
246
|
+
) => Tokens.Generic | undefined;
|
|
247
|
+
/** A tokenizer extension start hint. */
|
|
248
|
+
export type TokenizerStartFunction = (this: TokenizerThis, src: string) => number | void;
|
|
249
|
+
/** An inline or block tokenizer extension. */
|
|
250
|
+
export interface TokenizerExtension {
|
|
251
|
+
name: string;
|
|
252
|
+
level: "block" | "inline";
|
|
253
|
+
start?: TokenizerStartFunction;
|
|
254
|
+
tokenizer: TokenizerExtensionFunction;
|
|
255
|
+
childTokens?: string[];
|
|
256
|
+
}
|
|
257
|
+
/** Context supplied to extension renderers. */
|
|
258
|
+
export interface RendererThis {
|
|
259
|
+
parser: Parser;
|
|
260
|
+
}
|
|
261
|
+
/** A custom renderer extension callback. */
|
|
262
|
+
export type RendererExtensionFunction = (this: RendererThis, token: Tokens.Generic) => string | false | undefined;
|
|
263
|
+
/** A named renderer extension. */
|
|
264
|
+
export interface RendererExtension {
|
|
265
|
+
name: string;
|
|
266
|
+
renderer: RendererExtensionFunction;
|
|
267
|
+
}
|
|
268
|
+
/** A combined tokenizer/renderer extension. */
|
|
269
|
+
export type TokenizerAndRendererExtension =
|
|
270
|
+
| TokenizerExtension
|
|
271
|
+
| RendererExtension
|
|
272
|
+
| (TokenizerExtension & RendererExtension);
|
|
273
|
+
|
|
274
|
+
/** Overrides for built-in tokenizer methods. */
|
|
275
|
+
export interface TokenizerObject {
|
|
276
|
+
url?(this: Tokenizer, src: string): Tokens.Link | undefined | false;
|
|
277
|
+
lheading?(this: Tokenizer, src: string): Tokens.Heading | undefined | false;
|
|
278
|
+
del?(this: Tokenizer, src: string, maskedSrc?: string, prevChar?: string): Tokens.Del | undefined | false;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
interface RendererTokenMap {
|
|
282
|
+
space: Tokens.Space;
|
|
283
|
+
html: Tokens.HTML;
|
|
284
|
+
link: Tokens.Link;
|
|
285
|
+
image: Tokens.Image;
|
|
286
|
+
text: Tokens.Text | Tokens.Escape;
|
|
287
|
+
code: Tokens.Code;
|
|
288
|
+
blockquote: Tokens.Blockquote;
|
|
289
|
+
heading: Tokens.Heading;
|
|
290
|
+
hr: Tokens.Hr;
|
|
291
|
+
list: Tokens.List;
|
|
292
|
+
listitem: Tokens.ListItem;
|
|
293
|
+
paragraph: Tokens.Paragraph;
|
|
294
|
+
strong: Tokens.Strong;
|
|
295
|
+
em: Tokens.Em;
|
|
296
|
+
codespan: Tokens.Codespan;
|
|
297
|
+
br: Tokens.Br;
|
|
298
|
+
del: Tokens.Del;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/** Overrides for built-in HTML renderer methods. */
|
|
302
|
+
export type RendererObject = {
|
|
303
|
+
[K in keyof RendererTokenMap]?: (this: Renderer, token: RendererTokenMap[K]) => string | false;
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
/** Options supported by the in-house Markdown implementation. */
|
|
307
|
+
export interface MarkedOptions {
|
|
308
|
+
async?: boolean;
|
|
309
|
+
breaks?: boolean;
|
|
310
|
+
gfm?: boolean;
|
|
311
|
+
pedantic?: boolean;
|
|
312
|
+
silent?: boolean;
|
|
313
|
+
tokenizer?: Tokenizer | TokenizerObject | null;
|
|
314
|
+
/** Built-in tokenizer overrides composed by Marked.use(). */
|
|
315
|
+
tokenizerOverrides?: TokenizerObject;
|
|
316
|
+
renderer?: Renderer | RendererObject | null;
|
|
317
|
+
walkTokens?: ((token: Token) => void | Promise<void>) | null;
|
|
318
|
+
extensions?: ExtensionRegistry | null;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
interface ExtensionRegistry {
|
|
322
|
+
block: TokenizerExtension[];
|
|
323
|
+
inline: TokenizerExtension[];
|
|
324
|
+
renderers: Record<string, RendererExtensionFunction>;
|
|
325
|
+
childTokens: Record<string, string[]>;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/** Options accepted by Marked.use(). */
|
|
329
|
+
export interface MarkedExtension extends Omit<MarkedOptions, "extensions"> {
|
|
330
|
+
extensions?: TokenizerAndRendererExtension[] | null;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const DEFAULTS: MarkedOptions = {
|
|
334
|
+
async: false,
|
|
335
|
+
breaks: false,
|
|
336
|
+
gfm: true,
|
|
337
|
+
pedantic: false,
|
|
338
|
+
silent: false,
|
|
339
|
+
tokenizer: null,
|
|
340
|
+
renderer: null,
|
|
341
|
+
walkTokens: null,
|
|
342
|
+
extensions: null,
|
|
343
|
+
};
|
|
344
|
+
const PUNCTUATION = /[!"#$%&'()*+,\-./:;<=>?@[\\\]^_`{|}~]/;
|
|
345
|
+
|
|
346
|
+
function tokenList(links: Links = {}): TokensList {
|
|
347
|
+
const list = [] as unknown as TokensList;
|
|
348
|
+
list.links = links;
|
|
349
|
+
return list;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function normalizeSource(src: string): string {
|
|
353
|
+
return src.replace(/\r\n|\r/g, "\n").replace(/\t/g, " ");
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function unescapeMarkdown(value: string): string {
|
|
357
|
+
return value.replace(/\\([!"#$%&'()*+,\-./:;<=>?@[\\\]^_`{|}~])/g, "$1");
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function cleanUrl(value: string): string {
|
|
361
|
+
return unescapeMarkdown(value.trim().replace(/^<|>$/g, ""));
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function escapeHtml(value: string, encode = true): string {
|
|
365
|
+
let out = value
|
|
366
|
+
.replace(/&(?!(?:#\d+|#x[\da-f]+|\w+);)/gi, "&")
|
|
367
|
+
.replace(/</g, "<")
|
|
368
|
+
.replace(/>/g, ">")
|
|
369
|
+
.replace(/"/g, """)
|
|
370
|
+
.replace(/'/g, "'");
|
|
371
|
+
if (!encode) out = out.replace(/&(#\d+|#x[\da-f]+|\w+);/gi, "&$1;");
|
|
372
|
+
return out;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function findClosingBracket(src: string, start: number, open: string, close: string): number {
|
|
376
|
+
let depth = 0;
|
|
377
|
+
for (let i = start; i < src.length; i++) {
|
|
378
|
+
if (src[i] === "\\") {
|
|
379
|
+
i++;
|
|
380
|
+
continue;
|
|
381
|
+
}
|
|
382
|
+
if (src[i] === open) depth++;
|
|
383
|
+
else if (src[i] === close) {
|
|
384
|
+
if (depth === 0) return i;
|
|
385
|
+
depth--;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
return -1;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function findDelimiter(src: string, delimiter: string, from: number): number {
|
|
392
|
+
let at = src.indexOf(delimiter, from);
|
|
393
|
+
while (at !== -1) {
|
|
394
|
+
let escapes = 0;
|
|
395
|
+
for (let i = at - 1; i >= 0 && src[i] === "\\"; i--) escapes++;
|
|
396
|
+
if (escapes % 2 === 0) return at;
|
|
397
|
+
at = src.indexOf(delimiter, at + delimiter.length);
|
|
398
|
+
}
|
|
399
|
+
return -1;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function canOpenDelimiter(src: string, index: number, width: number, marker: string, previous = "\n"): boolean {
|
|
403
|
+
const before = index === 0 ? previous : src[index - 1]!;
|
|
404
|
+
if (index === 0 && before === marker) return false;
|
|
405
|
+
const after = src[index + width];
|
|
406
|
+
if (after === undefined || /\s/.test(after)) return false;
|
|
407
|
+
if (marker === "_" && /[\p{L}\p{N}]/u.test(before) && /[\p{L}\p{N}]/u.test(after)) return false;
|
|
408
|
+
return true;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function canCloseDelimiter(src: string, index: number, marker: string): boolean {
|
|
412
|
+
const before = src[index - 1];
|
|
413
|
+
const after = src[index + 1] ?? "\n";
|
|
414
|
+
if (before === undefined || /\s/.test(before)) return false;
|
|
415
|
+
if (marker === "_" && /[\p{L}\p{N}]/u.test(before) && /[\p{L}\p{N}]/u.test(after)) return false;
|
|
416
|
+
return !PUNCTUATION.test(before) || /\s/.test(after) || PUNCTUATION.test(after);
|
|
417
|
+
}
|
|
418
|
+
function inlineHtmlPrefix(src: string): string | undefined {
|
|
419
|
+
if (src.startsWith("<!--")) {
|
|
420
|
+
const end = src.indexOf("-->", 4);
|
|
421
|
+
return end === -1 ? undefined : src.slice(0, end + 3);
|
|
422
|
+
}
|
|
423
|
+
if (!/^<\/?[A-Za-z][A-Za-z0-9-]*(?:\s|\/?>)/.test(src)) return undefined;
|
|
424
|
+
let quote = "";
|
|
425
|
+
for (let i = 1; i < src.length; i++) {
|
|
426
|
+
const char = src[i]!;
|
|
427
|
+
if (quote !== "") {
|
|
428
|
+
if (char === quote) quote = "";
|
|
429
|
+
continue;
|
|
430
|
+
}
|
|
431
|
+
if (char === '"' || char === "'") {
|
|
432
|
+
quote = char;
|
|
433
|
+
continue;
|
|
434
|
+
}
|
|
435
|
+
if (char === ">") return src.slice(0, i + 1);
|
|
436
|
+
}
|
|
437
|
+
return undefined;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function appendText(tokens: Token[], raw: string, text = raw, escaped = false): void {
|
|
441
|
+
if (raw === "") return;
|
|
442
|
+
const previous = tokens.at(-1);
|
|
443
|
+
if (previous?.type === "text" && previous.tokens === undefined && previous.escaped === escaped) {
|
|
444
|
+
previous.raw += raw;
|
|
445
|
+
previous.text += text;
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
tokens.push({ type: "text", raw, text, escaped });
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/** Tokenizes the built-in inline Markdown surface. */
|
|
452
|
+
export class Tokenizer {
|
|
453
|
+
options: MarkedOptions;
|
|
454
|
+
lexer!: Lexer;
|
|
455
|
+
/** Creates a tokenizer with the supplied options. */
|
|
456
|
+
constructor(options: MarkedOptions = {}) {
|
|
457
|
+
this.options = options;
|
|
458
|
+
}
|
|
459
|
+
/** Tokenizes GFM deletion. */
|
|
460
|
+
del(src: string): Tokens.Del | undefined {
|
|
461
|
+
if (!src.startsWith("~~") || /\s/.test(src[2] ?? "")) return undefined;
|
|
462
|
+
const end = findDelimiter(src, "~~", 2);
|
|
463
|
+
if (end < 2 || /\s/.test(src[end - 1] ?? "")) return undefined;
|
|
464
|
+
const text = src.slice(2, end);
|
|
465
|
+
return { type: "del", raw: src.slice(0, end + 2), text, tokens: this.lexer.inlineTokens(text) };
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
function matchLink(src: string, lexer: Lexer): Tokens.Link | Tokens.Image | undefined {
|
|
470
|
+
const image = src.startsWith("![");
|
|
471
|
+
if (!(image || src.startsWith("["))) return undefined;
|
|
472
|
+
const labelStart = image ? 2 : 1;
|
|
473
|
+
const labelEnd = findClosingBracket(src, labelStart, "[", "]");
|
|
474
|
+
if (labelEnd === -1) return undefined;
|
|
475
|
+
const label = src.slice(labelStart, labelEnd);
|
|
476
|
+
if (src[labelEnd + 1] === "(") {
|
|
477
|
+
const destinationEnd = findClosingBracket(src, labelEnd + 2, "(", ")");
|
|
478
|
+
if (destinationEnd === -1) return undefined;
|
|
479
|
+
const inside = src.slice(labelEnd + 2, destinationEnd).trim();
|
|
480
|
+
let href = inside;
|
|
481
|
+
let title: string | null = null;
|
|
482
|
+
const titleMatch = /^(<[^>]*>|\S+?)(?:\s+(?:"([\s\S]*)"|'([\s\S]*)'|\(([\s\S]*)\)))?$/.exec(inside);
|
|
483
|
+
if (!titleMatch) return undefined;
|
|
484
|
+
href = cleanUrl(titleMatch[1]!);
|
|
485
|
+
title = titleMatch[2] ?? titleMatch[3] ?? titleMatch[4] ?? null;
|
|
486
|
+
const raw = src.slice(0, destinationEnd + 1);
|
|
487
|
+
const tokens = lexer.inlineTokens(label);
|
|
488
|
+
return image
|
|
489
|
+
? { type: "image", raw, href, title, text: unescapeMarkdown(label), tokens }
|
|
490
|
+
: { type: "link", raw, href, title, text: label, tokens };
|
|
491
|
+
}
|
|
492
|
+
let rawEnd = labelEnd + 1;
|
|
493
|
+
let ref = label;
|
|
494
|
+
if (src[rawEnd] === "[") {
|
|
495
|
+
const refEnd = findClosingBracket(src, rawEnd + 1, "[", "]");
|
|
496
|
+
if (refEnd === -1) return undefined;
|
|
497
|
+
ref = src.slice(rawEnd + 1, refEnd) || label;
|
|
498
|
+
rawEnd = refEnd + 1;
|
|
499
|
+
}
|
|
500
|
+
const def = lexer.tokens.links[ref.replace(/\s+/g, " ").toLowerCase()];
|
|
501
|
+
if (!def) return undefined;
|
|
502
|
+
const raw = src.slice(0, rawEnd);
|
|
503
|
+
const tokens = lexer.inlineTokens(label);
|
|
504
|
+
return image
|
|
505
|
+
? { type: "image", raw, href: def.href, title: def.title ?? null, text: unescapeMarkdown(label), tokens }
|
|
506
|
+
: { type: "link", raw, href: def.href, title: def.title ?? null, text: label, tokens };
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
function trimBareUrl(candidate: string): string {
|
|
510
|
+
let out = candidate;
|
|
511
|
+
while (/[.,:;!?]$/.test(out)) out = out.slice(0, -1);
|
|
512
|
+
let opens = 0;
|
|
513
|
+
let closes = 0;
|
|
514
|
+
for (const char of out) {
|
|
515
|
+
if (char === "(") opens++;
|
|
516
|
+
else if (char === ")") closes++;
|
|
517
|
+
}
|
|
518
|
+
while (closes > opens && out.endsWith(")")) {
|
|
519
|
+
out = out.slice(0, -1);
|
|
520
|
+
closes--;
|
|
521
|
+
}
|
|
522
|
+
return out;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
function inlineTokens(src: string, lexer: Lexer, output: Token[] = []): Token[] {
|
|
526
|
+
let rest = src;
|
|
527
|
+
while (rest !== "") {
|
|
528
|
+
let custom: Tokens.Generic | undefined;
|
|
529
|
+
for (const extension of lexer.extensions.inline) {
|
|
530
|
+
custom = extension.tokenizer.call({ lexer }, rest, output);
|
|
531
|
+
if (custom?.raw) break;
|
|
532
|
+
}
|
|
533
|
+
if (custom?.raw) {
|
|
534
|
+
output.push(custom);
|
|
535
|
+
rest = rest.slice(custom.raw.length);
|
|
536
|
+
continue;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
const escaped = /^\\([!"#$%&'()*+,\-./:;<=>?@[\\\]^_`{|}~])/.exec(rest);
|
|
540
|
+
if (escaped) {
|
|
541
|
+
output.push({ type: "escape", raw: escaped[0], text: escaped[1]! });
|
|
542
|
+
rest = rest.slice(2);
|
|
543
|
+
continue;
|
|
544
|
+
}
|
|
545
|
+
const br = lexer.options.breaks ? /^(?: {2,}|\\)?\n/.exec(rest) : /^(?: {2,}|\\)\n/.exec(rest);
|
|
546
|
+
if (br) {
|
|
547
|
+
output.push({ type: "br", raw: br[0] });
|
|
548
|
+
rest = rest.slice(br[0].length);
|
|
549
|
+
continue;
|
|
550
|
+
}
|
|
551
|
+
if (rest[0] === "`") {
|
|
552
|
+
const opener = /^`+/.exec(rest)![0];
|
|
553
|
+
const end = findDelimiter(rest, opener, opener.length);
|
|
554
|
+
if (end !== -1) {
|
|
555
|
+
const raw = rest.slice(0, end + opener.length);
|
|
556
|
+
let text = rest.slice(opener.length, end).replace(/\n/g, " ");
|
|
557
|
+
if (/^ .* $/.test(text) && text.trim() !== "") text = text.slice(1, -1);
|
|
558
|
+
output.push({ type: "codespan", raw, text });
|
|
559
|
+
rest = rest.slice(raw.length);
|
|
560
|
+
continue;
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
const auto = /^<((?:https?:\/\/|ftp:\/\/)[^ >]+|[^ <>@]+@[^ <>@]+)>/i.exec(rest);
|
|
564
|
+
if (auto) {
|
|
565
|
+
const text = auto[1]!;
|
|
566
|
+
const href = text.includes("@") && !/^[a-z][a-z+.-]*:\/\//i.test(text) ? `mailto:${text}` : text;
|
|
567
|
+
output.push({ type: "link", raw: auto[0], text, href, tokens: [{ type: "text", raw: text, text }] });
|
|
568
|
+
rest = rest.slice(auto[0].length);
|
|
569
|
+
continue;
|
|
570
|
+
}
|
|
571
|
+
const html = inlineHtmlPrefix(rest);
|
|
572
|
+
if (html) {
|
|
573
|
+
output.push({ type: "html", raw: html, inLink: false, inRawBlock: false, block: false, text: html });
|
|
574
|
+
rest = rest.slice(html.length);
|
|
575
|
+
continue;
|
|
576
|
+
}
|
|
577
|
+
const link = matchLink(rest, lexer);
|
|
578
|
+
if (link) {
|
|
579
|
+
output.push(link);
|
|
580
|
+
rest = rest.slice(link.raw.length);
|
|
581
|
+
continue;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
const marker = rest[0];
|
|
585
|
+
const previous = output.at(-1)?.raw.at(-1) ?? "\n";
|
|
586
|
+
if (
|
|
587
|
+
(marker === "*" || marker === "_") &&
|
|
588
|
+
rest.startsWith(marker.repeat(3)) &&
|
|
589
|
+
canOpenDelimiter(rest, 0, 3, marker, previous)
|
|
590
|
+
) {
|
|
591
|
+
const end = findDelimiter(rest, marker.repeat(3), 3);
|
|
592
|
+
if (end !== -1 && canCloseDelimiter(rest, end, marker)) {
|
|
593
|
+
const raw = rest.slice(0, end + 3);
|
|
594
|
+
const inner = rest.slice(3, end);
|
|
595
|
+
const text = `${marker.repeat(2)}${inner}${marker.repeat(2)}`;
|
|
596
|
+
output.push({ type: "em", raw, text, tokens: lexer.inlineTokens(text) });
|
|
597
|
+
rest = rest.slice(raw.length);
|
|
598
|
+
continue;
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
if (
|
|
602
|
+
(marker === "*" || marker === "_") &&
|
|
603
|
+
canOpenDelimiter(rest, 0, rest[1] === marker ? 2 : 1, marker, previous)
|
|
604
|
+
) {
|
|
605
|
+
const width = rest[1] === marker ? 2 : 1;
|
|
606
|
+
const delimiter = marker.repeat(width);
|
|
607
|
+
let end = findDelimiter(rest, delimiter, width);
|
|
608
|
+
let nested = 0;
|
|
609
|
+
while (end !== -1) {
|
|
610
|
+
if (!canCloseDelimiter(rest, end, marker) && canOpenDelimiter(rest, end, width, marker)) {
|
|
611
|
+
nested++;
|
|
612
|
+
} else if (canCloseDelimiter(rest, end, marker) && nested > 0) {
|
|
613
|
+
nested--;
|
|
614
|
+
} else if (canCloseDelimiter(rest, end, marker)) {
|
|
615
|
+
break;
|
|
616
|
+
}
|
|
617
|
+
end = findDelimiter(rest, delimiter, end + width);
|
|
618
|
+
}
|
|
619
|
+
if (end !== -1) {
|
|
620
|
+
const raw = rest.slice(0, end + width);
|
|
621
|
+
const text = rest.slice(width, end);
|
|
622
|
+
const tokens = lexer.inlineTokens(text);
|
|
623
|
+
output.push(width === 2 ? { type: "strong", raw, text, tokens } : { type: "em", raw, text, tokens });
|
|
624
|
+
rest = rest.slice(raw.length);
|
|
625
|
+
continue;
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
if (rest.startsWith("~~")) {
|
|
629
|
+
let del: Tokens.Del | undefined | false;
|
|
630
|
+
const override = lexer.tokenizerOverrides.del;
|
|
631
|
+
if (override) del = override.call(lexer.tokenizer, rest, rest);
|
|
632
|
+
if (del === false || !override) del = lexer.tokenizer.del(rest);
|
|
633
|
+
if (del) {
|
|
634
|
+
output.push(del);
|
|
635
|
+
rest = rest.slice(del.raw.length);
|
|
636
|
+
continue;
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
let url: Tokens.Link | undefined | false;
|
|
640
|
+
const urlOverride = lexer.tokenizerOverrides.url;
|
|
641
|
+
if (urlOverride) url = urlOverride.call(lexer.tokenizer, rest);
|
|
642
|
+
if (!urlOverride || url === false) {
|
|
643
|
+
const match =
|
|
644
|
+
/^(?:(?:https?:\/\/|ftp:\/\/|www\.)[^\s<]+|[A-Za-z0-9._+-]+@[A-Za-z0-9_-]+(?:\.[A-Za-z0-9_-]+)+)/i.exec(
|
|
645
|
+
rest,
|
|
646
|
+
);
|
|
647
|
+
if (match) {
|
|
648
|
+
const text = trimBareUrl(match[0]);
|
|
649
|
+
const href =
|
|
650
|
+
text.includes("@") && !text.includes("://")
|
|
651
|
+
? `mailto:${text}`
|
|
652
|
+
: text.startsWith("www.")
|
|
653
|
+
? `http://${text}`
|
|
654
|
+
: text;
|
|
655
|
+
url = { type: "link", raw: text, text, href, tokens: [{ type: "text", raw: text, text }] };
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
if (url) {
|
|
659
|
+
output.push(url);
|
|
660
|
+
rest = rest.slice(url.raw.length);
|
|
661
|
+
continue;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
let next = rest.length;
|
|
665
|
+
for (const char of ["\\", "`", "<", "[", "!", "*", "_", "~", "\n"]) {
|
|
666
|
+
const at = rest.indexOf(char, 1);
|
|
667
|
+
if (at !== -1 && at < next) next = at;
|
|
668
|
+
}
|
|
669
|
+
const urlAt = /(?:https?:\/\/|ftp:\/\/|www\.|[A-Za-z0-9._+-]+@)/i.exec(rest.slice(1));
|
|
670
|
+
if (urlAt && urlAt.index + 1 < next) next = urlAt.index + 1;
|
|
671
|
+
const hardBreak = /(?: {2,}|\\)\n/.exec(rest.slice(1));
|
|
672
|
+
if (hardBreak && hardBreak.index + 1 < next) next = hardBreak.index + 1;
|
|
673
|
+
for (const extension of lexer.extensions.inline) {
|
|
674
|
+
const at = extension.start?.call({ lexer }, rest);
|
|
675
|
+
if (typeof at === "number" && at > 0 && at < next) next = at;
|
|
676
|
+
}
|
|
677
|
+
if (next === 0) next = 1;
|
|
678
|
+
appendText(output, rest.slice(0, next));
|
|
679
|
+
rest = rest.slice(next);
|
|
680
|
+
}
|
|
681
|
+
return output;
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
function splitTableRow(line: string): string[] {
|
|
685
|
+
let value = line.trim();
|
|
686
|
+
if (value.startsWith("|")) value = value.slice(1);
|
|
687
|
+
if (value.endsWith("|") && !value.endsWith("\\|")) value = value.slice(0, -1);
|
|
688
|
+
const cells: string[] = [];
|
|
689
|
+
let cell = "";
|
|
690
|
+
for (let i = 0; i < value.length; i++) {
|
|
691
|
+
const char = value[i]!;
|
|
692
|
+
if (char === "|" && value[i - 1] !== "\\") {
|
|
693
|
+
cells.push(cell.trim());
|
|
694
|
+
cell = "";
|
|
695
|
+
} else cell += char;
|
|
696
|
+
}
|
|
697
|
+
cells.push(cell.trim());
|
|
698
|
+
return cells.map(entry => entry.replace(/\\\|/g, "|"));
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
function isFence(line: string): RegExpExecArray | null {
|
|
702
|
+
return /^ {0,3}(`{3,}|~{3,})(.*?)(?:\n|$)$/.exec(line);
|
|
703
|
+
}
|
|
704
|
+
function isHeading(line: string): boolean {
|
|
705
|
+
return /^ {0,3}#{1,6}(?:\s|$)/.test(line);
|
|
706
|
+
}
|
|
707
|
+
function isHr(line: string): boolean {
|
|
708
|
+
return /^ {0,3}((?:\*[ \t]*){3,}|(?:-[ \t]*){3,}|(?:_[ \t]*){3,})(?:\n|$)$/.test(line);
|
|
709
|
+
}
|
|
710
|
+
function isList(line: string): RegExpExecArray | null {
|
|
711
|
+
return /^( {0,3})((?:[*+-])|(?:\d{1,9}[.)]))(?:[ \t]+|(?=\n|$))(.*?)(?:\n|$)$/.exec(line);
|
|
712
|
+
}
|
|
713
|
+
function isBlockquote(line: string): boolean {
|
|
714
|
+
return /^ {0,3}>/.test(line);
|
|
715
|
+
}
|
|
716
|
+
function isDefinition(line: string): boolean {
|
|
717
|
+
return /^ {0,3}\[[^\]]+\]:/.test(line);
|
|
718
|
+
}
|
|
719
|
+
function isHtmlStart(line: string): boolean {
|
|
720
|
+
return /^ {0,3}(?:<!--|<(?:script|pre|style|textarea|address|article|aside|base|basefont|blockquote|body|caption|center|col|colgroup|dd|details|dialog|dir|div|dl|dt|fieldset|figcaption|figure|footer|form|frame|frameset|h[1-6]|head|header|hr|html|iframe|legend|li|link|main|menu|menuitem|nav|noframes|ol|optgroup|option|p|param|search|section|summary|table|tbody|td|tfoot|th|thead|title|tr|track|ul)(?:\s|\/?>))/i.test(
|
|
721
|
+
line,
|
|
722
|
+
);
|
|
723
|
+
}
|
|
724
|
+
function isBlockStart(lines: string[], index: number): boolean {
|
|
725
|
+
const line = lines[index] ?? "";
|
|
726
|
+
if (/^\n+$/.test(line)) return true;
|
|
727
|
+
if (
|
|
728
|
+
isFence(line) ||
|
|
729
|
+
isHeading(line) ||
|
|
730
|
+
isHr(line) ||
|
|
731
|
+
isList(line) ||
|
|
732
|
+
isBlockquote(line) ||
|
|
733
|
+
isDefinition(line) ||
|
|
734
|
+
isHtmlStart(line) ||
|
|
735
|
+
/^ {4}\S/.test(line)
|
|
736
|
+
)
|
|
737
|
+
return true;
|
|
738
|
+
if (index + 1 < lines.length && /^ {0,3}(?:=+|-+)[ \t]*(?:\n|$)$/.test(lines[index + 1]!)) return true;
|
|
739
|
+
if (
|
|
740
|
+
index + 1 < lines.length &&
|
|
741
|
+
line.includes("|") &&
|
|
742
|
+
splitTableRow(lines[index + 1]!).every(c => /^:?-{1,}:?$/.test(c))
|
|
743
|
+
)
|
|
744
|
+
return true;
|
|
745
|
+
return false;
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
function lineArray(src: string): string[] {
|
|
749
|
+
return src.match(/.*(?:\n|$)/g)?.filter((line, index, all) => line !== "" || index < all.length - 1) ?? [];
|
|
750
|
+
}
|
|
751
|
+
function stripFinalNewline(value: string): string {
|
|
752
|
+
return value.endsWith("\n") ? value.slice(0, -1) : value;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
function makeTable(lines: string[], index: number, lexer: Lexer): { token: Tokens.Table; count: number } | undefined {
|
|
756
|
+
if (index + 1 >= lines.length) return undefined;
|
|
757
|
+
const headerValues = splitTableRow(stripFinalNewline(lines[index]!));
|
|
758
|
+
const delimiterValues = splitTableRow(stripFinalNewline(lines[index + 1]!));
|
|
759
|
+
if (
|
|
760
|
+
!lines[index]!.includes("|") ||
|
|
761
|
+
headerValues.length !== delimiterValues.length ||
|
|
762
|
+
!delimiterValues.every(c => /^:?-{1,}:?$/.test(c))
|
|
763
|
+
)
|
|
764
|
+
return undefined;
|
|
765
|
+
const align = delimiterValues.map(cell =>
|
|
766
|
+
cell.startsWith(":") && cell.endsWith(":")
|
|
767
|
+
? ("center" as const)
|
|
768
|
+
: cell.startsWith(":")
|
|
769
|
+
? ("left" as const)
|
|
770
|
+
: cell.endsWith(":")
|
|
771
|
+
? ("right" as const)
|
|
772
|
+
: null,
|
|
773
|
+
);
|
|
774
|
+
let count = 2;
|
|
775
|
+
const body: string[][] = [];
|
|
776
|
+
while (index + count < lines.length && !/^\s*\n?$/.test(lines[index + count]!)) {
|
|
777
|
+
const cells = splitTableRow(stripFinalNewline(lines[index + count]!));
|
|
778
|
+
if (cells.length === 1 && !lines[index + count]!.includes("|")) break;
|
|
779
|
+
while (cells.length < headerValues.length) cells.push("");
|
|
780
|
+
body.push(cells.slice(0, headerValues.length));
|
|
781
|
+
count++;
|
|
782
|
+
}
|
|
783
|
+
const cell = (text: string, column: number, header: boolean): Tokens.TableCell => {
|
|
784
|
+
const tokens: Token[] = [];
|
|
785
|
+
lexer.inline(text, tokens);
|
|
786
|
+
return { text, tokens, header, align: align[column]! };
|
|
787
|
+
};
|
|
788
|
+
return {
|
|
789
|
+
token: {
|
|
790
|
+
type: "table",
|
|
791
|
+
raw: lines.slice(index, index + count).join(""),
|
|
792
|
+
header: headerValues.map((text, column) => cell(text, column, true)),
|
|
793
|
+
align,
|
|
794
|
+
rows: body.map(row => row.map((text, column) => cell(text, column, false))),
|
|
795
|
+
},
|
|
796
|
+
count,
|
|
797
|
+
};
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
function parseList(lines: string[], index: number, lexer: Lexer): { token: Tokens.List; count: number } | undefined {
|
|
801
|
+
const first = isList(lines[index]!);
|
|
802
|
+
if (!first) return undefined;
|
|
803
|
+
const ordered = /^\d/.test(first[2]!);
|
|
804
|
+
const delimiter = ordered ? first[2]!.at(-1)! : first[2]!;
|
|
805
|
+
const start = ordered ? Number.parseInt(first[2]!, 10) : "";
|
|
806
|
+
const rawItems: Array<{ raw: string; text: string; taskRaw?: string; checked?: boolean }> = [];
|
|
807
|
+
let cursor = index;
|
|
808
|
+
while (cursor < lines.length) {
|
|
809
|
+
const match = isList(lines[cursor]!);
|
|
810
|
+
if (
|
|
811
|
+
!match ||
|
|
812
|
+
/^\d/.test(match[2]!) !== ordered ||
|
|
813
|
+
(ordered ? match[2]!.at(-1) !== delimiter : match[2] !== delimiter)
|
|
814
|
+
)
|
|
815
|
+
break;
|
|
816
|
+
const itemStart = cursor;
|
|
817
|
+
const markerWidth = match[1]!.length + match[2]!.length + 1;
|
|
818
|
+
let text = match[3] ?? "";
|
|
819
|
+
if (lines[cursor]!.endsWith("\n")) text += "\n";
|
|
820
|
+
cursor++;
|
|
821
|
+
while (cursor < lines.length) {
|
|
822
|
+
const next = lines[cursor]!;
|
|
823
|
+
const indent = /^ */.exec(next)![0].length;
|
|
824
|
+
if (!/^\s*\n?$/.test(next) && indent <= first[1]!.length && isBlockStart(lines, cursor)) break;
|
|
825
|
+
if (!/^\s/.test(next) && !/^\n$/.test(next)) break;
|
|
826
|
+
if (/^\s*\n$/.test(next)) {
|
|
827
|
+
let lookahead = cursor + 1;
|
|
828
|
+
while (lookahead < lines.length && /^\s*\n$/.test(lines[lookahead]!)) lookahead++;
|
|
829
|
+
if (lookahead < lines.length) {
|
|
830
|
+
// A blank line closes the list unless the next top-level line is a
|
|
831
|
+
// compatible item (same bullet char / ordered delimiter) or indented
|
|
832
|
+
// item content. The blank must stay OUTSIDE the list raw (it becomes
|
|
833
|
+
// a `space` token) so token shape never depends on what follows —
|
|
834
|
+
// real marked does the same, and the TUI's streaming freeze relies
|
|
835
|
+
// on that append-stability.
|
|
836
|
+
const following = lines[lookahead]!;
|
|
837
|
+
const followingIndent = /^ */.exec(following)![0].length;
|
|
838
|
+
if (followingIndent <= first[1]!.length) {
|
|
839
|
+
const followingList = isList(following);
|
|
840
|
+
const compatible =
|
|
841
|
+
followingList !== null &&
|
|
842
|
+
/^\d/.test(followingList[2]!) === ordered &&
|
|
843
|
+
(ordered ? followingList[2]!.at(-1) === delimiter : followingList[2] === delimiter);
|
|
844
|
+
if (!compatible) break;
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
text += next;
|
|
848
|
+
} else {
|
|
849
|
+
const remove = Math.min(indent, markerWidth);
|
|
850
|
+
text += next.slice(remove);
|
|
851
|
+
}
|
|
852
|
+
cursor++;
|
|
853
|
+
if (/\n\s*\n$/.test(text) && cursor < lines.length && !/^\s/.test(lines[cursor]!)) break;
|
|
854
|
+
}
|
|
855
|
+
let raw = lines.slice(itemStart, cursor).join("");
|
|
856
|
+
if (cursor === lines.length || (cursor < lines.length && /^\s*\n$/.test(lines[cursor]!)))
|
|
857
|
+
raw = stripFinalNewline(raw);
|
|
858
|
+
let taskRaw: string | undefined;
|
|
859
|
+
let checked: boolean | undefined;
|
|
860
|
+
const task = /^\[([ xX])\][ \t]+/.exec(text);
|
|
861
|
+
if (task) {
|
|
862
|
+
taskRaw = task[0];
|
|
863
|
+
checked = task[1]!.toLowerCase() === "x";
|
|
864
|
+
text = text.slice(task[0].length);
|
|
865
|
+
}
|
|
866
|
+
rawItems.push({ raw, text: stripFinalNewline(text), taskRaw, checked });
|
|
867
|
+
}
|
|
868
|
+
if (rawItems.length === 0) return undefined;
|
|
869
|
+
const raw = lines.slice(index, cursor).join("");
|
|
870
|
+
const loose = rawItems.some(item => /\n\s*\n/.test(item.raw)) || /\n\s*\n/.test(raw);
|
|
871
|
+
const items = rawItems.map(item => {
|
|
872
|
+
let tokens = lexer.blockTokens(item.text, []);
|
|
873
|
+
if (!loose)
|
|
874
|
+
tokens = tokens.map(token =>
|
|
875
|
+
token.type === "paragraph"
|
|
876
|
+
? ({ type: "text", raw: token.raw, text: token.text, tokens: token.tokens } as Tokens.Text)
|
|
877
|
+
: token,
|
|
878
|
+
);
|
|
879
|
+
if (item.taskRaw !== undefined)
|
|
880
|
+
tokens.unshift({ type: "checkbox", raw: item.taskRaw, checked: item.checked === true });
|
|
881
|
+
const listItem: Tokens.ListItem = {
|
|
882
|
+
type: "list_item",
|
|
883
|
+
raw: item.raw,
|
|
884
|
+
task: item.taskRaw !== undefined,
|
|
885
|
+
loose,
|
|
886
|
+
text: item.text,
|
|
887
|
+
tokens,
|
|
888
|
+
};
|
|
889
|
+
if (item.checked !== undefined) listItem.checked = item.checked;
|
|
890
|
+
return listItem;
|
|
891
|
+
});
|
|
892
|
+
return { token: { type: "list", raw, ordered, start, loose, items }, count: cursor - index };
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
function blockTokens(src: string, lexer: Lexer, output: Token[]): Token[] {
|
|
896
|
+
const lines = lineArray(src);
|
|
897
|
+
let i = 0;
|
|
898
|
+
while (i < lines.length) {
|
|
899
|
+
const remaining = lines.slice(i).join("");
|
|
900
|
+
let custom: Tokens.Generic | undefined;
|
|
901
|
+
for (const extension of lexer.extensions.block) {
|
|
902
|
+
custom = extension.tokenizer.call({ lexer }, remaining, output);
|
|
903
|
+
if (custom?.raw) break;
|
|
904
|
+
}
|
|
905
|
+
if (custom?.raw) {
|
|
906
|
+
output.push(custom);
|
|
907
|
+
let consumed = custom.raw.length;
|
|
908
|
+
while (i < lines.length && consumed > 0) {
|
|
909
|
+
consumed -= lines[i]!.length;
|
|
910
|
+
i++;
|
|
911
|
+
}
|
|
912
|
+
continue;
|
|
913
|
+
}
|
|
914
|
+
const line = lines[i]!;
|
|
915
|
+
if (/^\s*\n$/.test(line)) {
|
|
916
|
+
let raw = "";
|
|
917
|
+
while (i < lines.length && /^\s*\n$/.test(lines[i]!)) raw += lines[i++]!;
|
|
918
|
+
if (output.length > 0) {
|
|
919
|
+
const previous = output.at(-1)!;
|
|
920
|
+
if (previous.raw.endsWith("\n")) previous.raw = previous.raw.slice(0, -1);
|
|
921
|
+
raw = `\n${raw}`;
|
|
922
|
+
}
|
|
923
|
+
if (raw.length > 1) output.push({ type: "space", raw });
|
|
924
|
+
continue;
|
|
925
|
+
}
|
|
926
|
+
const fence = isFence(line);
|
|
927
|
+
if (fence) {
|
|
928
|
+
const marker = fence[1]!;
|
|
929
|
+
let raw = line;
|
|
930
|
+
let text = "";
|
|
931
|
+
i++;
|
|
932
|
+
while (i < lines.length) {
|
|
933
|
+
const next = lines[i++]!;
|
|
934
|
+
if (new RegExp(`^ {0,3}${marker[0]}{${marker.length},}[ \\t]*(?:\\n|$)$`).test(next)) {
|
|
935
|
+
raw += next;
|
|
936
|
+
break;
|
|
937
|
+
}
|
|
938
|
+
raw += next;
|
|
939
|
+
text += next;
|
|
940
|
+
}
|
|
941
|
+
text = stripFinalNewline(text);
|
|
942
|
+
const rawToken = i < lines.length && /^\s*\n$/.test(lines[i]!) ? stripFinalNewline(raw) : raw;
|
|
943
|
+
output.push({ type: "code", raw: rawToken, lang: fence[2]!.trim(), text });
|
|
944
|
+
continue;
|
|
945
|
+
}
|
|
946
|
+
if (/^ {4}/.test(line)) {
|
|
947
|
+
let raw = "";
|
|
948
|
+
let text = "";
|
|
949
|
+
while (i < lines.length && /^ {4}/.test(lines[i]!)) {
|
|
950
|
+
raw += lines[i]!;
|
|
951
|
+
text += lines[i]!.slice(4);
|
|
952
|
+
i++;
|
|
953
|
+
if (i < lines.length && /^\s*\n$/.test(lines[i]!) && i + 1 < lines.length && /^ {4}/.test(lines[i + 1]!)) {
|
|
954
|
+
raw += lines[i]!;
|
|
955
|
+
text += lines[i]!;
|
|
956
|
+
i++;
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
if (i < lines.length && /^\s*\n$/.test(lines[i]!)) {
|
|
960
|
+
raw = stripFinalNewline(raw);
|
|
961
|
+
text = stripFinalNewline(text);
|
|
962
|
+
}
|
|
963
|
+
output.push({ type: "code", raw, codeBlockStyle: "indented", text: text.replace(/\n+$/, "\n") });
|
|
964
|
+
continue;
|
|
965
|
+
}
|
|
966
|
+
const heading = /^ {0,3}(#{1,6})(?:[ \t]+|$)(.*?)(?:\n|$)$/.exec(line);
|
|
967
|
+
if (heading) {
|
|
968
|
+
const text = heading[2]!.replace(/[ \t]+#+[ \t]*$/, "").trim();
|
|
969
|
+
const raw = i + 1 < lines.length && /^\s*\n$/.test(lines[i + 1]!) ? stripFinalNewline(line) : line;
|
|
970
|
+
const tokens: Token[] = [];
|
|
971
|
+
lexer.inline(text, tokens);
|
|
972
|
+
output.push({ type: "heading", raw, depth: heading[1]!.length, text, tokens });
|
|
973
|
+
i++;
|
|
974
|
+
continue;
|
|
975
|
+
}
|
|
976
|
+
if (isHr(line)) {
|
|
977
|
+
output.push({ type: "hr", raw: line });
|
|
978
|
+
i++;
|
|
979
|
+
continue;
|
|
980
|
+
}
|
|
981
|
+
if (isBlockquote(line)) {
|
|
982
|
+
let raw = "";
|
|
983
|
+
let text = "";
|
|
984
|
+
while (i < lines.length) {
|
|
985
|
+
const next = lines[i]!;
|
|
986
|
+
if (isBlockquote(next)) {
|
|
987
|
+
raw += next;
|
|
988
|
+
text += next.replace(/^ {0,3}>[ \t]?/, "");
|
|
989
|
+
i++;
|
|
990
|
+
continue;
|
|
991
|
+
}
|
|
992
|
+
if (!/^\s*\n$/.test(next) && !isBlockStart(lines, i)) {
|
|
993
|
+
raw += next;
|
|
994
|
+
text += next;
|
|
995
|
+
i++;
|
|
996
|
+
continue;
|
|
997
|
+
}
|
|
998
|
+
break;
|
|
999
|
+
}
|
|
1000
|
+
text = stripFinalNewline(text);
|
|
1001
|
+
output.push({ type: "blockquote", raw, tokens: lexer.blockTokens(text, []), text });
|
|
1002
|
+
continue;
|
|
1003
|
+
}
|
|
1004
|
+
const list = parseList(lines, i, lexer);
|
|
1005
|
+
if (list) {
|
|
1006
|
+
output.push(list.token);
|
|
1007
|
+
i += list.count;
|
|
1008
|
+
continue;
|
|
1009
|
+
}
|
|
1010
|
+
if (isHtmlStart(line)) {
|
|
1011
|
+
let raw = line;
|
|
1012
|
+
const tag = /^ {0,3}<([A-Za-z][\w-]*)/.exec(line)?.[1]?.toLowerCase();
|
|
1013
|
+
i++;
|
|
1014
|
+
if (line.trimStart().startsWith("<!--")) {
|
|
1015
|
+
while (!raw.includes("-->") && i < lines.length) raw += lines[i++]!;
|
|
1016
|
+
} else if (tag && !new RegExp(`</${tag}>`, "i").test(raw)) {
|
|
1017
|
+
while (i < lines.length) {
|
|
1018
|
+
raw += lines[i++]!;
|
|
1019
|
+
if (new RegExp(`</${tag}>`, "i").test(raw)) break;
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
const value = i < lines.length && /^\s*\n$/.test(lines[i]!) ? stripFinalNewline(raw) : raw;
|
|
1023
|
+
output.push({
|
|
1024
|
+
type: "html",
|
|
1025
|
+
block: true,
|
|
1026
|
+
raw: value,
|
|
1027
|
+
pre: tag === "pre" || tag === "script" || tag === "style",
|
|
1028
|
+
text: value,
|
|
1029
|
+
});
|
|
1030
|
+
continue;
|
|
1031
|
+
}
|
|
1032
|
+
const def =
|
|
1033
|
+
/^ {0,3}\[([^\]]+)\]:[ \t]*(?:<([^>]+)>|(\S+))(?:[ \t]+(?:"([^"]*)"|'([^']*)'|\(([^)]*)\)))?[ \t]*(?:\n|$)$/.exec(
|
|
1034
|
+
line,
|
|
1035
|
+
);
|
|
1036
|
+
if (def) {
|
|
1037
|
+
const tag = def[1]!.replace(/\s+/g, " ").toLowerCase();
|
|
1038
|
+
const href = cleanUrl(def[2] ?? def[3] ?? "");
|
|
1039
|
+
const title = def[4] ?? def[5] ?? def[6];
|
|
1040
|
+
lexer.tokens.links[tag] = { href, title: title ?? null };
|
|
1041
|
+
const token: Tokens.Def = { type: "def", tag, raw: line, href };
|
|
1042
|
+
if (title !== undefined) token.title = title;
|
|
1043
|
+
output.push(token);
|
|
1044
|
+
i++;
|
|
1045
|
+
continue;
|
|
1046
|
+
}
|
|
1047
|
+
const table = makeTable(lines, i, lexer);
|
|
1048
|
+
if (table) {
|
|
1049
|
+
output.push(table.token);
|
|
1050
|
+
i += table.count;
|
|
1051
|
+
continue;
|
|
1052
|
+
}
|
|
1053
|
+
if (i + 1 < lines.length && /^ {0,3}(=+|-+)[ \t]*(?:\n|$)$/.test(lines[i + 1]!)) {
|
|
1054
|
+
let fallback: Tokens.Heading | undefined | false;
|
|
1055
|
+
const override = lexer.tokenizerOverrides.lheading;
|
|
1056
|
+
if (override) fallback = override.call(lexer.tokenizer, remaining);
|
|
1057
|
+
if (!override || fallback === false) {
|
|
1058
|
+
const raw = line + lines[i + 1]!;
|
|
1059
|
+
const text = stripFinalNewline(line);
|
|
1060
|
+
const tokens: Token[] = [];
|
|
1061
|
+
lexer.inline(text, tokens);
|
|
1062
|
+
output.push({ type: "heading", raw, depth: lines[i + 1]!.trimStart()[0] === "=" ? 1 : 2, text, tokens });
|
|
1063
|
+
i += 2;
|
|
1064
|
+
continue;
|
|
1065
|
+
}
|
|
1066
|
+
if (fallback) {
|
|
1067
|
+
output.push(fallback);
|
|
1068
|
+
i += Math.max(1, lineArray(fallback.raw).length);
|
|
1069
|
+
continue;
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
let raw = line;
|
|
1073
|
+
i++;
|
|
1074
|
+
while (i < lines.length && !isBlockStart(lines, i)) {
|
|
1075
|
+
raw += lines[i++]!;
|
|
1076
|
+
}
|
|
1077
|
+
const text = stripFinalNewline(raw);
|
|
1078
|
+
const tokens: Token[] = [];
|
|
1079
|
+
lexer.inline(text, tokens);
|
|
1080
|
+
output.push({ type: "paragraph", raw, text, tokens });
|
|
1081
|
+
}
|
|
1082
|
+
return output;
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
const BLOCK_RULES = {
|
|
1086
|
+
normal: {
|
|
1087
|
+
blockquote: /^ {0,3}>/,
|
|
1088
|
+
code: /^ {4}/,
|
|
1089
|
+
def: /^ {0,3}\[/,
|
|
1090
|
+
fences: /^ {0,3}(?:```|~~~)/,
|
|
1091
|
+
heading: /^ {0,3}#/,
|
|
1092
|
+
hr: /^ {0,3}(?:\*|_|-)/,
|
|
1093
|
+
html: /^ {0,3}</,
|
|
1094
|
+
lheading: /^/,
|
|
1095
|
+
list: /^ {0,3}(?:[*+-]|\d+[.)])/,
|
|
1096
|
+
newline: /^\n+/,
|
|
1097
|
+
paragraph: /^/,
|
|
1098
|
+
table: /^/,
|
|
1099
|
+
text: /^[^\n]+/,
|
|
1100
|
+
},
|
|
1101
|
+
gfm: {} as Record<string, RegExp>,
|
|
1102
|
+
pedantic: {} as Record<string, RegExp>,
|
|
1103
|
+
};
|
|
1104
|
+
BLOCK_RULES.gfm = { ...BLOCK_RULES.normal };
|
|
1105
|
+
BLOCK_RULES.pedantic = { ...BLOCK_RULES.normal };
|
|
1106
|
+
const INLINE_RULES = {
|
|
1107
|
+
normal: {
|
|
1108
|
+
_backpedal: /^/,
|
|
1109
|
+
anyPunctuation: PUNCTUATION,
|
|
1110
|
+
autolink: /^</,
|
|
1111
|
+
blockSkip: /^/,
|
|
1112
|
+
br: /^(?: {2,}|\\)\n/,
|
|
1113
|
+
code: /^`+/,
|
|
1114
|
+
del: /^~~/,
|
|
1115
|
+
delLDelim: /^~~/,
|
|
1116
|
+
delRDelim: /^~~/,
|
|
1117
|
+
emStrongLDelim: /^[*_]/,
|
|
1118
|
+
emStrongRDelimAst: /\*/,
|
|
1119
|
+
emStrongRDelimUnd: /_/,
|
|
1120
|
+
escape: /^\\/,
|
|
1121
|
+
link: /^!?\[/,
|
|
1122
|
+
nolink: /^\[/,
|
|
1123
|
+
punctuation: PUNCTUATION,
|
|
1124
|
+
reflink: /^\[/,
|
|
1125
|
+
reflinkSearch: /\[/,
|
|
1126
|
+
tag: /^</,
|
|
1127
|
+
text: /^[\s\S]/,
|
|
1128
|
+
url: /^(?:https?:|ftp:|www\.)/,
|
|
1129
|
+
},
|
|
1130
|
+
gfm: {} as Record<string, RegExp>,
|
|
1131
|
+
breaks: {} as Record<string, RegExp>,
|
|
1132
|
+
pedantic: {} as Record<string, RegExp>,
|
|
1133
|
+
};
|
|
1134
|
+
INLINE_RULES.gfm = { ...INLINE_RULES.normal };
|
|
1135
|
+
INLINE_RULES.breaks = { ...INLINE_RULES.normal };
|
|
1136
|
+
INLINE_RULES.pedantic = { ...INLINE_RULES.normal };
|
|
1137
|
+
|
|
1138
|
+
/** Stateful block and inline Markdown lexer. */
|
|
1139
|
+
export class Lexer {
|
|
1140
|
+
tokens: TokensList;
|
|
1141
|
+
options: MarkedOptions;
|
|
1142
|
+
state = { inLink: false, inRawBlock: false, top: true };
|
|
1143
|
+
inlineQueue: Array<{ src: string; tokens: Token[] }> = [];
|
|
1144
|
+
tokenizer: Tokenizer;
|
|
1145
|
+
tokenizerOverrides: TokenizerObject;
|
|
1146
|
+
extensions: ExtensionRegistry;
|
|
1147
|
+
/** Creates a lexer. */
|
|
1148
|
+
constructor(options: MarkedOptions = {}) {
|
|
1149
|
+
this.options = { ...DEFAULTS, ...options };
|
|
1150
|
+
this.tokens = tokenList();
|
|
1151
|
+
this.extensions = options.extensions ?? { block: [], inline: [], renderers: {}, childTokens: {} };
|
|
1152
|
+
this.tokenizer = options.tokenizer instanceof Tokenizer ? options.tokenizer : new Tokenizer(this.options);
|
|
1153
|
+
this.tokenizer.lexer = this;
|
|
1154
|
+
this.tokenizerOverrides =
|
|
1155
|
+
options.tokenizerOverrides ?? (options.tokenizer instanceof Tokenizer ? {} : (options.tokenizer ?? {}));
|
|
1156
|
+
}
|
|
1157
|
+
/** Exposes rule objects for callers that optimize regular expressions. */
|
|
1158
|
+
static get rules() {
|
|
1159
|
+
return { block: BLOCK_RULES, inline: INLINE_RULES };
|
|
1160
|
+
}
|
|
1161
|
+
/** Lexes a complete document. */
|
|
1162
|
+
static lex(src: string, options: MarkedOptions = {}): TokensList {
|
|
1163
|
+
return new Lexer(options).lex(src);
|
|
1164
|
+
}
|
|
1165
|
+
/** Lexes inline Markdown. */
|
|
1166
|
+
static lexInline(src: string, options: MarkedOptions = {}): Token[] {
|
|
1167
|
+
return new Lexer(options).inlineTokens(src);
|
|
1168
|
+
}
|
|
1169
|
+
/** Lexes and resolves a complete document. */
|
|
1170
|
+
lex(src: string): TokensList {
|
|
1171
|
+
this.tokens = tokenList();
|
|
1172
|
+
this.inlineQueue = [];
|
|
1173
|
+
this.blockTokens(normalizeSource(src), this.tokens);
|
|
1174
|
+
for (const queued of this.inlineQueue) this.inlineTokens(queued.src, queued.tokens);
|
|
1175
|
+
this.inlineQueue = [];
|
|
1176
|
+
return this.tokens;
|
|
1177
|
+
}
|
|
1178
|
+
/** Appends block tokens to an output array. */
|
|
1179
|
+
blockTokens(src: string, tokens: Token[] | TokensList = this.tokens): Token[] | TokensList {
|
|
1180
|
+
return blockTokens(src, this, tokens);
|
|
1181
|
+
}
|
|
1182
|
+
/** Queues inline tokenization compatibly with marked. */
|
|
1183
|
+
inline(src: string, tokens: Token[] = []): Token[] {
|
|
1184
|
+
this.inlineQueue.push({ src, tokens });
|
|
1185
|
+
return tokens;
|
|
1186
|
+
}
|
|
1187
|
+
/** Tokenizes inline Markdown immediately. */
|
|
1188
|
+
inlineTokens(src: string, tokens: Token[] = []): Token[] {
|
|
1189
|
+
return inlineTokens(src, this, tokens);
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
/** Default HTML renderer. */
|
|
1194
|
+
export class Renderer {
|
|
1195
|
+
options: MarkedOptions;
|
|
1196
|
+
parser!: Parser;
|
|
1197
|
+
overrides: RendererObject;
|
|
1198
|
+
/** Creates an HTML renderer. */
|
|
1199
|
+
constructor(options: MarkedOptions = {}, overrides: RendererObject = {}) {
|
|
1200
|
+
this.options = options;
|
|
1201
|
+
this.overrides = overrides;
|
|
1202
|
+
}
|
|
1203
|
+
/** Renders blank space. */ space(_token: Tokens.Space): string {
|
|
1204
|
+
return "";
|
|
1205
|
+
}
|
|
1206
|
+
/** Renders a code block. */ code({ text, lang, escaped }: Tokens.Code): string {
|
|
1207
|
+
const code = escaped ? text : escapeHtml(text);
|
|
1208
|
+
const language = (lang ?? "").match(/^\S*/)?.[0] ?? "";
|
|
1209
|
+
return `<pre><code${language ? ` class="language-${escapeHtml(language)}"` : ""}>${code}${text.endsWith("\n") ? "" : "\n"}</code></pre>\n`;
|
|
1210
|
+
}
|
|
1211
|
+
/** Renders a block quote. */ blockquote({ tokens }: Tokens.Blockquote): string {
|
|
1212
|
+
return `<blockquote>\n${this.parser.parse(tokens)}</blockquote>\n`;
|
|
1213
|
+
}
|
|
1214
|
+
/** Passes raw HTML through. */ html({ text }: Tokens.HTML): string {
|
|
1215
|
+
return text;
|
|
1216
|
+
}
|
|
1217
|
+
/** Renders a definition as no output. */ def(_token: Tokens.Def): string {
|
|
1218
|
+
return "";
|
|
1219
|
+
}
|
|
1220
|
+
/** Renders a heading. */ heading({ tokens, depth }: Tokens.Heading): string {
|
|
1221
|
+
return `<h${depth}>${this.parser.parseInline(tokens)}</h${depth}>\n`;
|
|
1222
|
+
}
|
|
1223
|
+
/** Renders a horizontal rule. */ hr(_token: Tokens.Hr): string {
|
|
1224
|
+
return "<hr>\n";
|
|
1225
|
+
}
|
|
1226
|
+
/** Renders a list. */ list(token: Tokens.List): string {
|
|
1227
|
+
const tag = token.ordered ? "ol" : "ul";
|
|
1228
|
+
const start = token.ordered && token.start !== 1 ? ` start="${token.start}"` : "";
|
|
1229
|
+
return `<${tag}${start}>\n${token.items.map(item => this.parser.renderListItem(item)).join("")}</${tag}>\n`;
|
|
1230
|
+
}
|
|
1231
|
+
/** Renders a list item. */ listitem(item: Tokens.ListItem): string {
|
|
1232
|
+
return `<li>${this.parser.parse(item.tokens)}</li>\n`;
|
|
1233
|
+
}
|
|
1234
|
+
/** Renders a checkbox. */ checkbox({ checked }: Tokens.Checkbox): string {
|
|
1235
|
+
return `<input${checked ? ' checked=""' : ""} disabled="" type="checkbox">`;
|
|
1236
|
+
}
|
|
1237
|
+
/** Renders a paragraph. */ paragraph({ tokens }: Tokens.Paragraph): string {
|
|
1238
|
+
return `<p>${this.parser.parseInline(tokens)}</p>\n`;
|
|
1239
|
+
}
|
|
1240
|
+
/** Renders a table. */ table(token: Tokens.Table): string {
|
|
1241
|
+
const row = (cells: Tokens.TableCell[]) => `<tr>\n${cells.map(cell => this.tablecell(cell)).join("")}</tr>\n`;
|
|
1242
|
+
return `<table>\n<thead>\n${row(token.header)}</thead>\n${token.rows.length ? `<tbody>${token.rows.map(row).join("")}</tbody>` : ""}</table>\n`;
|
|
1243
|
+
}
|
|
1244
|
+
/** Renders a table row. */ tablerow(text: string): string {
|
|
1245
|
+
return `<tr>\n${text}</tr>\n`;
|
|
1246
|
+
}
|
|
1247
|
+
/** Renders a table cell. */ tablecell(token: Tokens.TableCell): string {
|
|
1248
|
+
const tag = token.header ? "th" : "td";
|
|
1249
|
+
return `<${tag}${token.align ? ` align="${token.align}"` : ""}>${this.parser.parseInline(token.tokens)}</${tag}>\n`;
|
|
1250
|
+
}
|
|
1251
|
+
/** Renders strong text. */ strong({ tokens }: Tokens.Strong): string {
|
|
1252
|
+
return `<strong>${this.parser.parseInline(tokens)}</strong>`;
|
|
1253
|
+
}
|
|
1254
|
+
/** Renders emphasized text. */ em({ tokens }: Tokens.Em): string {
|
|
1255
|
+
return `<em>${this.parser.parseInline(tokens)}</em>`;
|
|
1256
|
+
}
|
|
1257
|
+
/** Renders inline code. */ codespan({ text }: Tokens.Codespan): string {
|
|
1258
|
+
return `<code>${escapeHtml(text)}</code>`;
|
|
1259
|
+
}
|
|
1260
|
+
/** Renders a line break. */ br(_token: Tokens.Br): string {
|
|
1261
|
+
return "<br>";
|
|
1262
|
+
}
|
|
1263
|
+
/** Renders deleted text. */ del({ tokens }: Tokens.Del): string {
|
|
1264
|
+
return `<del>${this.parser.parseInline(tokens)}</del>`;
|
|
1265
|
+
}
|
|
1266
|
+
/** Renders a link. */ link({ href, title, tokens }: Tokens.Link): string {
|
|
1267
|
+
const titleAttr = title ? ` title="${escapeHtml(title)}"` : "";
|
|
1268
|
+
return `<a href="${escapeHtml(encodeURI(href))}"${titleAttr}>${this.parser.parseInline(tokens)}</a>`;
|
|
1269
|
+
}
|
|
1270
|
+
/** Renders an image. */ image({ href, title, text }: Tokens.Image): string {
|
|
1271
|
+
const titleAttr = title ? ` title="${escapeHtml(title)}"` : "";
|
|
1272
|
+
return `<img src="${escapeHtml(encodeURI(href))}" alt="${escapeHtml(text)}"${titleAttr}>`;
|
|
1273
|
+
}
|
|
1274
|
+
/** Renders plain text. */ text({ text }: Tokens.Text | Tokens.Escape): string {
|
|
1275
|
+
return escapeHtml(text, false);
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
/** Converts token streams into HTML. */
|
|
1280
|
+
export class Parser {
|
|
1281
|
+
options: MarkedOptions;
|
|
1282
|
+
renderer: Renderer;
|
|
1283
|
+
extensions: ExtensionRegistry;
|
|
1284
|
+
/** Creates a parser. */
|
|
1285
|
+
constructor(options: MarkedOptions = {}) {
|
|
1286
|
+
this.options = options;
|
|
1287
|
+
this.extensions = options.extensions ?? { block: [], inline: [], renderers: {}, childTokens: {} };
|
|
1288
|
+
const rendererOption = options.renderer;
|
|
1289
|
+
this.renderer = rendererOption instanceof Renderer ? rendererOption : new Renderer(options, rendererOption ?? {});
|
|
1290
|
+
this.renderer.parser = this;
|
|
1291
|
+
}
|
|
1292
|
+
/** Parses block tokens. */ static parse(tokens: Token[], options: MarkedOptions = {}): string {
|
|
1293
|
+
return new Parser(options).parse(tokens);
|
|
1294
|
+
}
|
|
1295
|
+
/** Parses inline tokens. */ static parseInline(tokens: Token[], options: MarkedOptions = {}): string {
|
|
1296
|
+
return new Parser(options).parseInline(tokens);
|
|
1297
|
+
}
|
|
1298
|
+
/** Parses block tokens with the configured renderer. */
|
|
1299
|
+
parse(tokens: Token[]): string {
|
|
1300
|
+
let out = "";
|
|
1301
|
+
for (const token of tokens) {
|
|
1302
|
+
const extension = this.extensions.renderers[token.type];
|
|
1303
|
+
if (extension) {
|
|
1304
|
+
const rendered = extension.call({ parser: this }, token);
|
|
1305
|
+
if (rendered !== false && rendered !== undefined) {
|
|
1306
|
+
out += rendered;
|
|
1307
|
+
continue;
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
if (!isKnownToken(token)) {
|
|
1311
|
+
out += this.parseInline([token]);
|
|
1312
|
+
continue;
|
|
1313
|
+
}
|
|
1314
|
+
switch (token.type) {
|
|
1315
|
+
case "space":
|
|
1316
|
+
out += this.call("space", token, () => this.renderer.space(token));
|
|
1317
|
+
break;
|
|
1318
|
+
case "checkbox":
|
|
1319
|
+
out += `${this.renderer.checkbox(token)} `;
|
|
1320
|
+
break;
|
|
1321
|
+
case "hr":
|
|
1322
|
+
out += this.call("hr", token, () => this.renderer.hr(token));
|
|
1323
|
+
break;
|
|
1324
|
+
case "heading":
|
|
1325
|
+
out += this.call("heading", token, () => this.renderer.heading(token));
|
|
1326
|
+
break;
|
|
1327
|
+
case "code":
|
|
1328
|
+
out += this.call("code", token, () => this.renderer.code(token));
|
|
1329
|
+
break;
|
|
1330
|
+
case "blockquote":
|
|
1331
|
+
out += this.call("blockquote", token, () => this.renderer.blockquote(token));
|
|
1332
|
+
break;
|
|
1333
|
+
case "html":
|
|
1334
|
+
out += this.call("html", token, () => this.renderer.html(token));
|
|
1335
|
+
break;
|
|
1336
|
+
case "def":
|
|
1337
|
+
out += this.renderer.def(token);
|
|
1338
|
+
break;
|
|
1339
|
+
case "list":
|
|
1340
|
+
out += this.call("list", token, () => this.renderer.list(token));
|
|
1341
|
+
break;
|
|
1342
|
+
case "paragraph":
|
|
1343
|
+
out += this.call("paragraph", token, () => this.renderer.paragraph(token));
|
|
1344
|
+
break;
|
|
1345
|
+
case "table":
|
|
1346
|
+
out += this.renderer.table(token);
|
|
1347
|
+
break;
|
|
1348
|
+
case "text":
|
|
1349
|
+
out += token.tokens ? this.parseInline(token.tokens) : this.renderer.text(token);
|
|
1350
|
+
break;
|
|
1351
|
+
default:
|
|
1352
|
+
out += this.parseInline([token]);
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
return out;
|
|
1356
|
+
}
|
|
1357
|
+
/** Parses inline tokens with the configured renderer. */
|
|
1358
|
+
parseInline(tokens: Token[]): string {
|
|
1359
|
+
let out = "";
|
|
1360
|
+
for (const token of tokens) {
|
|
1361
|
+
const extension = this.extensions.renderers[token.type];
|
|
1362
|
+
if (extension) {
|
|
1363
|
+
const rendered = extension.call({ parser: this }, token);
|
|
1364
|
+
if (rendered !== false && rendered !== undefined) {
|
|
1365
|
+
out += rendered;
|
|
1366
|
+
continue;
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
if (!isKnownToken(token)) {
|
|
1370
|
+
if (token.tokens) out += this.parseInline(token.tokens);
|
|
1371
|
+
else if (typeof token.text === "string") out += escapeHtml(token.text);
|
|
1372
|
+
continue;
|
|
1373
|
+
}
|
|
1374
|
+
switch (token.type) {
|
|
1375
|
+
case "escape":
|
|
1376
|
+
case "text":
|
|
1377
|
+
out += this.call("text", token, () => this.renderer.text(token));
|
|
1378
|
+
break;
|
|
1379
|
+
case "checkbox":
|
|
1380
|
+
out += `${this.renderer.checkbox(token)} `;
|
|
1381
|
+
break;
|
|
1382
|
+
case "html":
|
|
1383
|
+
out += this.call("html", token, () => this.renderer.html(token));
|
|
1384
|
+
break;
|
|
1385
|
+
case "link":
|
|
1386
|
+
out += this.call("link", token, () => this.renderer.link(token));
|
|
1387
|
+
break;
|
|
1388
|
+
case "image":
|
|
1389
|
+
out += this.call("image", token, () => this.renderer.image(token));
|
|
1390
|
+
break;
|
|
1391
|
+
case "strong":
|
|
1392
|
+
out += this.call("strong", token, () => this.renderer.strong(token));
|
|
1393
|
+
break;
|
|
1394
|
+
case "em":
|
|
1395
|
+
out += this.call("em", token, () => this.renderer.em(token));
|
|
1396
|
+
break;
|
|
1397
|
+
case "codespan":
|
|
1398
|
+
out += this.call("codespan", token, () => this.renderer.codespan(token));
|
|
1399
|
+
break;
|
|
1400
|
+
case "br":
|
|
1401
|
+
out += this.call("br", token, () => this.renderer.br(token));
|
|
1402
|
+
break;
|
|
1403
|
+
case "del":
|
|
1404
|
+
out += this.call("del", token, () => this.renderer.del(token));
|
|
1405
|
+
break;
|
|
1406
|
+
default:
|
|
1407
|
+
if ("tokens" in token && token.tokens) out += this.parseInline(token.tokens);
|
|
1408
|
+
else if ("text" in token && typeof token.text === "string") out += escapeHtml(token.text);
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
return out;
|
|
1412
|
+
}
|
|
1413
|
+
/** Renders a list item, including custom renderer overrides. */
|
|
1414
|
+
renderListItem(item: Tokens.ListItem): string {
|
|
1415
|
+
return this.call("listitem", item, () => this.renderer.listitem(item));
|
|
1416
|
+
}
|
|
1417
|
+
call<K extends keyof RendererTokenMap>(name: K, token: RendererTokenMap[K], fallback: () => string): string {
|
|
1418
|
+
const override = this.renderer.overrides[name];
|
|
1419
|
+
if (override) {
|
|
1420
|
+
const value = override.call(this.renderer, token);
|
|
1421
|
+
if (value !== false) return value;
|
|
1422
|
+
}
|
|
1423
|
+
return fallback();
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1427
|
+
function freshExtensions(): ExtensionRegistry {
|
|
1428
|
+
return { block: [], inline: [], renderers: {}, childTokens: {} };
|
|
1429
|
+
}
|
|
1430
|
+
function mergeExtensions(
|
|
1431
|
+
current: ExtensionRegistry | null | undefined,
|
|
1432
|
+
additions: TokenizerAndRendererExtension[] | null | undefined,
|
|
1433
|
+
): ExtensionRegistry {
|
|
1434
|
+
const registry = current ?? freshExtensions();
|
|
1435
|
+
for (const extension of additions ?? []) {
|
|
1436
|
+
if ("tokenizer" in extension) {
|
|
1437
|
+
registry[extension.level].unshift(extension);
|
|
1438
|
+
if (extension.childTokens) registry.childTokens[extension.name] = extension.childTokens;
|
|
1439
|
+
}
|
|
1440
|
+
if ("renderer" in extension) registry.renderers[extension.name] = extension.renderer;
|
|
1441
|
+
}
|
|
1442
|
+
return registry;
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1445
|
+
function walkTokenTree(
|
|
1446
|
+
tokens: Token[],
|
|
1447
|
+
callback: (token: Token) => void | Promise<void>,
|
|
1448
|
+
registry: ExtensionRegistry,
|
|
1449
|
+
): Array<void | Promise<void>> {
|
|
1450
|
+
const values: Array<void | Promise<void>> = [];
|
|
1451
|
+
for (const token of tokens) {
|
|
1452
|
+
values.push(callback(token));
|
|
1453
|
+
const children: Token[][] = [];
|
|
1454
|
+
if ("tokens" in token && token.tokens) children.push(token.tokens);
|
|
1455
|
+
if (isKnownToken(token)) {
|
|
1456
|
+
if (token.type === "list") for (const item of token.items) children.push([item]);
|
|
1457
|
+
if (token.type === "table") {
|
|
1458
|
+
for (const cell of token.header) children.push(cell.tokens);
|
|
1459
|
+
for (const row of token.rows) for (const cell of row) children.push(cell.tokens);
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
for (const key of registry.childTokens[token.type] ?? []) {
|
|
1463
|
+
const value: unknown = Reflect.get(token, key);
|
|
1464
|
+
if (Array.isArray(value)) children.push(value);
|
|
1465
|
+
}
|
|
1466
|
+
for (const child of children) values.push(...walkTokenTree(child, callback, registry));
|
|
1467
|
+
}
|
|
1468
|
+
return values;
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
/** Configurable Markdown lexer and HTML parser. */
|
|
1472
|
+
export class Marked {
|
|
1473
|
+
defaults: MarkedOptions;
|
|
1474
|
+
/** Creates an isolated Marked instance. */
|
|
1475
|
+
constructor(...extensions: MarkedExtension[]) {
|
|
1476
|
+
this.defaults = { ...DEFAULTS, extensions: freshExtensions() };
|
|
1477
|
+
if (extensions.length) this.use(...extensions);
|
|
1478
|
+
}
|
|
1479
|
+
/** Merges default options. */ options(options: MarkedOptions): this {
|
|
1480
|
+
return this.setOptions(options);
|
|
1481
|
+
}
|
|
1482
|
+
/** Merges default options. */ setOptions(options: MarkedOptions): this {
|
|
1483
|
+
this.defaults = { ...this.defaults, ...options, extensions: options.extensions ?? this.defaults.extensions };
|
|
1484
|
+
return this;
|
|
1485
|
+
}
|
|
1486
|
+
/** Adds tokenizer, renderer, and walk-token extensions. */
|
|
1487
|
+
use(...extensions: MarkedExtension[]): this {
|
|
1488
|
+
for (const extension of extensions) {
|
|
1489
|
+
this.defaults.extensions = mergeExtensions(this.defaults.extensions, extension.extensions);
|
|
1490
|
+
if (extension.tokenizer) {
|
|
1491
|
+
if (this.defaults.tokenizer instanceof Tokenizer) {
|
|
1492
|
+
this.defaults.tokenizerOverrides = { ...this.defaults.tokenizerOverrides, ...extension.tokenizer };
|
|
1493
|
+
} else {
|
|
1494
|
+
this.defaults.tokenizer = { ...this.defaults.tokenizer, ...extension.tokenizer };
|
|
1495
|
+
}
|
|
1496
|
+
}
|
|
1497
|
+
if (extension.renderer)
|
|
1498
|
+
this.defaults.renderer = {
|
|
1499
|
+
...(this.defaults.renderer instanceof Renderer ? {} : this.defaults.renderer),
|
|
1500
|
+
...extension.renderer,
|
|
1501
|
+
};
|
|
1502
|
+
if (extension.walkTokens) {
|
|
1503
|
+
const previous = this.defaults.walkTokens;
|
|
1504
|
+
this.defaults.walkTokens = previous
|
|
1505
|
+
? token => {
|
|
1506
|
+
previous(token);
|
|
1507
|
+
return extension.walkTokens?.(token);
|
|
1508
|
+
}
|
|
1509
|
+
: extension.walkTokens;
|
|
1510
|
+
}
|
|
1511
|
+
for (const key of ["async", "breaks", "gfm", "pedantic", "silent"] as const)
|
|
1512
|
+
if (extension[key] !== undefined) this.defaults[key] = extension[key];
|
|
1513
|
+
}
|
|
1514
|
+
return this;
|
|
1515
|
+
}
|
|
1516
|
+
/** Lexes Markdown into tokens. */ lexer(src: string, options: MarkedOptions = {}): TokensList {
|
|
1517
|
+
return Lexer.lex(src, {
|
|
1518
|
+
...this.defaults,
|
|
1519
|
+
...options,
|
|
1520
|
+
extensions: options.extensions ?? this.defaults.extensions,
|
|
1521
|
+
});
|
|
1522
|
+
}
|
|
1523
|
+
/** Parses a token stream to HTML. */ parser(tokens: Token[], options: MarkedOptions = {}): string {
|
|
1524
|
+
return Parser.parse(tokens, {
|
|
1525
|
+
...this.defaults,
|
|
1526
|
+
...options,
|
|
1527
|
+
extensions: options.extensions ?? this.defaults.extensions,
|
|
1528
|
+
});
|
|
1529
|
+
}
|
|
1530
|
+
/** Walks a token tree depth first. */ walkTokens(
|
|
1531
|
+
tokens: Token[] | TokensList,
|
|
1532
|
+
callback: (token: Token) => void | Promise<void>,
|
|
1533
|
+
): Array<void | Promise<void>> {
|
|
1534
|
+
return walkTokenTree(tokens, callback, this.defaults.extensions ?? freshExtensions());
|
|
1535
|
+
}
|
|
1536
|
+
/** Parses Markdown to HTML, synchronously unless async mode is requested. */
|
|
1537
|
+
parse(src: string, options: MarkedOptions & { async: true }): Promise<string>;
|
|
1538
|
+
parse(src: string, options: MarkedOptions & { async: false }): string;
|
|
1539
|
+
parse(src: string, options?: MarkedOptions | null): string | Promise<string>;
|
|
1540
|
+
parse(src: string, options: MarkedOptions | null = null): string | Promise<string> {
|
|
1541
|
+
const merged = { ...this.defaults, ...options, extensions: options?.extensions ?? this.defaults.extensions };
|
|
1542
|
+
const tokens = Lexer.lex(src, merged);
|
|
1543
|
+
const walked = merged.walkTokens
|
|
1544
|
+
? walkTokenTree(tokens, merged.walkTokens, merged.extensions ?? freshExtensions())
|
|
1545
|
+
: [];
|
|
1546
|
+
if (merged.async) return Promise.all(walked).then(() => Parser.parse(tokens, merged));
|
|
1547
|
+
return Parser.parse(tokens, merged);
|
|
1548
|
+
}
|
|
1549
|
+
/** Parses inline Markdown to HTML. */ parseInline(src: string, options: MarkedOptions = {}): string {
|
|
1550
|
+
const merged = { ...this.defaults, ...options, extensions: options.extensions ?? this.defaults.extensions };
|
|
1551
|
+
return Parser.parseInline(Lexer.lexInline(src, merged), merged);
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
|
|
1555
|
+
const shared = new Marked();
|
|
1556
|
+
/** Parses Markdown with a shared default instance. */
|
|
1557
|
+
export function marked(src: string, options: MarkedOptions & { async: true }): Promise<string>;
|
|
1558
|
+
export function marked(src: string, options: MarkedOptions & { async: false }): string;
|
|
1559
|
+
export function marked(src: string, options?: MarkedOptions | null): string | Promise<string>;
|
|
1560
|
+
export function marked(src: string, options: MarkedOptions | null = null): string | Promise<string> {
|
|
1561
|
+
return shared.parse(src, options);
|
|
1562
|
+
}
|
|
1563
|
+
/** Parses Markdown with a shared default instance. */
|
|
1564
|
+
export const parse = marked;
|
|
1565
|
+
/** Lexes Markdown with default options. */
|
|
1566
|
+
export const lexer = Lexer.lex;
|
|
1567
|
+
/** Parses inline Markdown with default options. */
|
|
1568
|
+
export const parseInline = (src: string, options: MarkedOptions = {}): string => shared.parseInline(src, options);
|
|
1569
|
+
/** Parses a token stream with default options. */
|
|
1570
|
+
export const parser = Parser.parse;
|
|
1571
|
+
/** The default option object. */
|
|
1572
|
+
export const defaults = DEFAULTS;
|
|
1573
|
+
/** Returns a fresh copy of the default options. */
|
|
1574
|
+
export function getDefaults(): MarkedOptions {
|
|
1575
|
+
return { ...DEFAULTS };
|
|
1576
|
+
}
|