pantsdown 2.2.7 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/lexer.ts CHANGED
@@ -1,10 +1,11 @@
1
1
  import { inline } from "./rules/inline.ts";
2
+ import { other } from "./rules/other.ts";
2
3
  import { Tokenizer } from "./tokenizer.ts";
3
4
  import { type Links, type SourceMap, type Token, type Tokens } from "./types.ts";
4
5
 
5
6
  export class Lexer {
6
7
  private tokenizer: Tokenizer;
7
- private inlineQueue: { src: string; tokens: Token[] }[];
8
+ inlineQueue: { src: string; tokens: Token[] }[];
8
9
  private links: Links = {};
9
10
 
10
11
  tokens: Token[] = [];
@@ -37,7 +38,7 @@ export class Lexer {
37
38
  top: true,
38
39
  };
39
40
 
40
- src = src.replace(/\r\n|\r/g, "\n");
41
+ src = src.replace(other.carriageReturn, "\n");
41
42
 
42
43
  this.blockTokens(src, this.tokens);
43
44
 
@@ -75,17 +76,19 @@ export class Lexer {
75
76
  /**
76
77
  * Lexing
77
78
  */
78
- blockTokens(src: string, tokens: Token[]): Token[] {
79
- src = src.replace(/^( *)(\t+)/gm, (_, leading: string, tabs: string) => {
80
- return leading + " ".repeat(tabs.length);
81
- });
82
-
79
+ blockTokens(src: string, tokens: Token[], lastParagraphClipped = false): Token[] {
83
80
  let token: Token | undefined;
84
81
  let lastToken: Token | undefined;
85
82
  let cutSrc;
86
- let lastParagraphClipped;
87
83
 
84
+ let srcLength = Infinity;
88
85
  while (src) {
86
+ if (src.length < srcLength) {
87
+ srcLength = src.length;
88
+ } else {
89
+ this.infiniteLoopError(src.charCodeAt(0));
90
+ }
91
+
89
92
  // newline
90
93
  if ((token = this.tokenizer.space(src))) {
91
94
  src = src.substring(token.raw.length);
@@ -106,7 +109,7 @@ export class Lexer {
106
109
  lastToken = tokens[tokens.length - 1];
107
110
  // An indented code block cannot interrupt a paragraph.
108
111
  if (lastToken && (lastToken.type === "paragraph" || lastToken.type === "text")) {
109
- lastToken.raw += "\n" + token.raw;
112
+ lastToken.raw += (lastToken.raw.endsWith("\n") ? "" : "\n") + token.raw;
110
113
  lastToken.text += "\n" + token.text;
111
114
  const lastInline = this.inlineQueue[this.inlineQueue.length - 1];
112
115
  if (lastInline) lastInline.src = lastToken.text;
@@ -177,7 +180,7 @@ export class Lexer {
177
180
  src = src.substring(token.raw.length);
178
181
  lastToken = tokens[tokens.length - 1];
179
182
  if (lastToken && (lastToken.type === "paragraph" || lastToken.type === "text")) {
180
- lastToken.raw += "\n" + token.raw;
183
+ lastToken.raw += (lastToken.raw.endsWith("\n") ? "" : "\n") + token.raw;
181
184
  lastToken.text += "\n" + token.raw;
182
185
  const lastInline = this.inlineQueue[this.inlineQueue.length - 1];
183
186
  if (lastInline) lastInline.src = lastToken.text;
@@ -210,7 +213,7 @@ export class Lexer {
210
213
  if (this.state.top && (token = this.tokenizer.paragraph(cutSrc))) {
211
214
  lastToken = tokens[tokens.length - 1];
212
215
  if (lastParagraphClipped && lastToken?.type === "paragraph") {
213
- lastToken.raw += "\n" + token.raw;
216
+ lastToken.raw += (lastToken.raw.endsWith("\n") ? "" : "\n") + token.raw;
214
217
  lastToken.text += "\n" + token.text;
215
218
  this.inlineQueue.pop();
216
219
  const lastInline = this.inlineQueue[this.inlineQueue.length - 1];
@@ -228,7 +231,7 @@ export class Lexer {
228
231
  src = src.substring(token.raw.length);
229
232
  lastToken = tokens[tokens.length - 1];
230
233
  if (lastToken?.type === "text") {
231
- lastToken.raw += "\n" + token.raw;
234
+ lastToken.raw += (lastToken.raw.endsWith("\n") ? "" : "\n") + token.raw;
232
235
  lastToken.text += "\n" + token.text;
233
236
  this.inlineQueue.pop();
234
237
  const lastInline = this.inlineQueue[this.inlineQueue.length - 1];
@@ -240,8 +243,7 @@ export class Lexer {
240
243
  }
241
244
 
242
245
  if (src) {
243
- const errMsg = "Infinite loop on byte: " + String(src.charCodeAt(0));
244
- throw new Error(errMsg);
246
+ this.infiniteLoopError(src.charCodeAt(0));
245
247
  }
246
248
  }
247
249
 
@@ -262,42 +264,34 @@ export class Lexer {
262
264
 
263
265
  // String with links masked to avoid interference with em and strong
264
266
  let maskedSrc = src;
265
- let match;
266
267
  let keepPrevChar, prevChar;
267
268
 
268
269
  // Mask out reflinks
269
270
  const links = Object.keys(this.links);
270
271
  if (links.length > 0) {
271
- while ((match = inline.reflinkSearch.exec(maskedSrc)) != null) {
272
- if (links.includes(match[0].slice(match[0].lastIndexOf("[") + 1, -1))) {
273
- maskedSrc =
274
- maskedSrc.slice(0, match.index) +
275
- "[" +
276
- "a".repeat(match[0].length - 2) +
277
- "]" +
278
- maskedSrc.slice(inline.reflinkSearch.lastIndex);
279
- }
280
- }
281
- }
282
- // Mask out other blocks
283
- while ((match = inline.blockSkip.exec(maskedSrc)) != null) {
284
- maskedSrc =
285
- maskedSrc.slice(0, match.index) +
286
- "[" +
287
- "a".repeat(match[0].length - 2) +
288
- "]" +
289
- maskedSrc.slice(inline.blockSkip.lastIndex);
272
+ maskedSrc = maskedSrc.replace(inline.reflinkSearch, (match0) =>
273
+ links.includes(match0.slice(match0.lastIndexOf("[") + 1, -1))
274
+ ? "[" + "a".repeat(match0.length - 2) + "]"
275
+ : match0,
276
+ );
290
277
  }
291
-
292
278
  // Mask out escaped characters
293
- while ((match = inline.anyPunctuation.exec(maskedSrc)) != null) {
294
- maskedSrc =
295
- maskedSrc.slice(0, match.index) +
296
- "++" +
297
- maskedSrc.slice(inline.anyPunctuation.lastIndex);
298
- }
279
+ maskedSrc = maskedSrc.replace(inline.anyPunctuation, "++");
299
280
 
281
+ // Mask out other blocks
282
+ maskedSrc = maskedSrc.replace(
283
+ inline.blockSkip,
284
+ (match0) => "[" + "a".repeat(match0.length - 2) + "]",
285
+ );
286
+
287
+ let srcLength = Infinity;
300
288
  while (src) {
289
+ if (src.length < srcLength) {
290
+ srcLength = src.length;
291
+ } else {
292
+ this.infiniteLoopError(src.charCodeAt(0));
293
+ }
294
+
301
295
  if (!keepPrevChar) {
302
296
  prevChar = "";
303
297
  }
@@ -320,13 +314,7 @@ export class Lexer {
320
314
  // tag
321
315
  if ((token = this.tokenizer.tag(src))) {
322
316
  src = src.substring(token.raw.length);
323
- lastToken = tokens[tokens.length - 1];
324
- if (lastToken && token.type === "text" && lastToken.type === "text") {
325
- lastToken.raw += token.raw;
326
- lastToken.text += token.text;
327
- } else {
328
- tokens.push(token);
329
- }
317
+ tokens.push(token);
330
318
  continue;
331
319
  }
332
320
 
@@ -379,7 +367,7 @@ export class Lexer {
379
367
  }
380
368
 
381
369
  // del (gfm)
382
- if ((token = this.tokenizer.del(src))) {
370
+ if ((token = this.tokenizer.del(src, maskedSrc, prevChar))) {
383
371
  src = src.substring(token.raw.length);
384
372
  tokens.push(token);
385
373
  continue;
@@ -420,11 +408,14 @@ export class Lexer {
420
408
  }
421
409
 
422
410
  if (src) {
423
- const errMsg = "Infinite loop on byte: " + String(src.charCodeAt(0));
424
- throw new Error(errMsg);
411
+ this.infiniteLoopError(src.charCodeAt(0));
425
412
  }
426
413
  }
427
414
 
428
415
  return tokens;
429
416
  }
417
+
418
+ private infiniteLoopError(byte: number): never {
419
+ throw new Error("Infinite loop on byte: " + String(byte));
420
+ }
430
421
  }
package/src/pantsdown.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import GithubSlugger from "github-slugger";
2
- import { javascript } from "./javascript.ts";
2
+ import { getJavascript } from "./javascript.ts";
3
3
  import { Lexer } from "./lexer.ts";
4
4
  import { Parser } from "./parser.ts";
5
5
  import { type PantsdownConfig, type PartialPantsdownConfig } from "./types.ts";
@@ -9,6 +9,15 @@ const defaultConfig: PantsdownConfig = {
9
9
  relativeImageUrlPrefix: "",
10
10
  absoluteImageUrlPrefix: undefined,
11
11
  detailsTagDefaultOpen: false,
12
+ mermaid: {
13
+ buttons: {
14
+ zoom: true,
15
+ reset: true,
16
+ arrows: true,
17
+ popover: true,
18
+ },
19
+ mouseActions: true,
20
+ },
12
21
  },
13
22
  };
14
23
 
@@ -28,10 +37,19 @@ export class Pantsdown {
28
37
  * The object you provide will be deeply merged into current config.
29
38
  */
30
39
  setConfig(config: PartialPantsdownConfig) {
40
+ const { mermaid, ...renderer } = config.renderer ?? {};
31
41
  this.config = {
32
42
  renderer: {
33
43
  ...this.config.renderer,
34
- ...config.renderer,
44
+ ...renderer,
45
+ mermaid: {
46
+ ...this.config.renderer.mermaid,
47
+ ...mermaid,
48
+ buttons: {
49
+ ...this.config.renderer.mermaid.buttons,
50
+ ...mermaid?.buttons,
51
+ },
52
+ },
35
53
  },
36
54
  };
37
55
  }
@@ -43,12 +61,13 @@ export class Pantsdown {
43
61
  // re-init slugger to avoid slug count from incorrectly incrementing
44
62
  // from previosly slugged headings
45
63
  this.parser.renderer.slugger = new GithubSlugger();
64
+ this.parser.renderer.mermaidCounts = new Map();
46
65
 
47
66
  const tokens = this.lexer.lex(src);
48
67
  const html = this.parser.parse(tokens);
49
68
  return {
50
69
  html,
51
- javascript,
70
+ javascript: getJavascript(this.config),
52
71
  };
53
72
  }
54
73
  }
package/src/parser.ts CHANGED
@@ -1,16 +1,19 @@
1
1
  import { type Pantsdown } from "./pantsdown.ts";
2
2
  import { Renderer } from "./renderer.ts";
3
+ import { TextRenderer } from "./text-renderer.ts";
3
4
  import { type Token, type Tokens } from "./types.ts";
4
- import { injectHtmlAttributes } from "./utils.ts";
5
5
 
6
6
  /**
7
7
  * Parsing & Compiling
8
8
  */
9
9
  export class Parser {
10
10
  renderer: Renderer;
11
+ textRenderer: TextRenderer;
11
12
 
12
13
  constructor(pantsdown: Pantsdown) {
13
14
  this.renderer = new Renderer(pantsdown);
15
+ this.renderer.parser = this;
16
+ this.textRenderer = new TextRenderer();
14
17
  }
15
18
 
16
19
  /**
@@ -24,156 +27,75 @@ export class Parser {
24
27
 
25
28
  switch (token.type) {
26
29
  case "space": {
30
+ out += this.renderer.space(token);
27
31
  continue;
28
32
  }
29
33
  case "hr": {
30
- out += this.renderer.hr(token.sourceMap);
34
+ out += this.renderer.hr(token);
31
35
  continue;
32
36
  }
33
37
  case "heading": {
34
- out += this.renderer.heading(
35
- this.parseInline(token.tokens),
36
- token.depth,
37
- token.sourceMap,
38
- );
38
+ out += this.renderer.heading(token);
39
39
  continue;
40
40
  }
41
41
  case "code": {
42
- out += this.renderer.code(token.text, token.lang, token.sourceMap);
42
+ out += this.renderer.code(token);
43
43
  continue;
44
44
  }
45
45
  case "table": {
46
- let header = "";
47
-
48
- // header
49
- let cell = "";
50
- for (let j = 0, len = token.header.length; j < len; j++) {
51
- cell += this.renderer.tablecell(this.parseInline(token.header[j]!.tokens), {
52
- header: true,
53
- align: token.align[j]!,
54
- });
55
- }
56
- const sourceMapLineStart = token.sourceMap?.[0];
57
- header += this.renderer.tablerow(cell, sourceMapLineStart);
58
-
59
- let body = "";
60
- for (let j = 0, rowsLen = token.rows.length; j < rowsLen; j++) {
61
- const row = token.rows[j]!;
62
-
63
- cell = "";
64
- for (let k = 0, rowLen = row.length; k < rowLen; k++) {
65
- cell += this.renderer.tablecell(this.parseInline(row[k]!.tokens), {
66
- header: false,
67
- align: token.align[k]!,
68
- });
69
- }
70
-
71
- body += this.renderer.tablerow(
72
- cell,
73
- sourceMapLineStart ? sourceMapLineStart + 2 + j : undefined,
74
- );
75
- }
76
- out += this.renderer.table(header, body);
46
+ out += this.renderer.table(token);
77
47
  continue;
78
48
  }
79
49
  case "alert": {
80
- const body = this.parse(token.tokens);
81
- out += this.renderer.alert(body, token);
50
+ out += this.renderer.alert(token);
82
51
  continue;
83
52
  }
84
53
  case "blockquote": {
85
- const body = this.parse(token.tokens);
86
- out += this.renderer.blockquote(body);
54
+ out += this.renderer.blockquote(token);
87
55
  continue;
88
56
  }
89
57
  case "list": {
90
- const ordered = token.ordered;
91
- const start = token.start;
92
- const loose = token.loose;
93
- let containsTaskList = false;
94
-
95
- let body = "";
96
- for (let j = 0, itemsLen = token.items.length; j < itemsLen; j++) {
97
- const item = token.items[j]!;
98
- const checked = item.checked;
99
- const task = item.task;
100
-
101
- let itemBody = "";
102
- if (item.task) {
103
- containsTaskList = true;
104
- const checkbox = this.renderer.checkbox(Boolean(checked), [
105
- "task-list-item-checkbox",
106
- ]);
107
- if (loose) {
108
- if (item.tokens.length > 0 && item.tokens[0]!.type === "paragraph") {
109
- item.tokens[0].text = checkbox + " " + item.tokens[0].text;
110
- if (
111
- item.tokens[0].tokens.length > 0 &&
112
- item.tokens[0].tokens[0]!.type === "text"
113
- ) {
114
- item.tokens[0].tokens[0]!.text =
115
- checkbox + " " + item.tokens[0].tokens[0]!.text;
116
- }
117
- } else {
118
- item.tokens.unshift({
119
- type: "text",
120
- text: checkbox + " ",
121
- } as Tokens["Text"]);
122
- }
123
- } else {
124
- itemBody += checkbox + " ";
125
- }
126
- }
127
-
128
- itemBody += this.parse(item.tokens, loose);
129
- body += this.renderer.listitem(itemBody, task, Boolean(checked), item.sourceMap);
130
- }
131
-
132
- const listClasses: string[] = [];
133
- if (containsTaskList) listClasses.push("contains-task-list");
134
- out += this.renderer.list(body, ordered, start, listClasses);
58
+ out += this.renderer.list(token);
59
+ continue;
60
+ }
61
+ case "checkbox": {
62
+ out += this.renderer.checkbox(token);
135
63
  continue;
136
64
  }
137
65
  case "html": {
138
- out += this.renderer.html(
139
- token.text,
140
- token.block,
141
- "sourceMap" in token ? token.sourceMap : undefined,
142
- );
66
+ out += this.renderer.html(token);
143
67
  continue;
144
68
  }
145
69
  case "footnotes": {
146
- const body = token.items.reduce((acc, { label, content, sourceMap }) => {
147
- let footnoteItem = `<li id="footnote-${encodeURIComponent(label)}">\n`;
148
- footnoteItem += this.parse(content);
149
- footnoteItem += "</li>\n";
150
-
151
- footnoteItem = injectHtmlAttributes(footnoteItem, [], sourceMap);
152
-
153
- return acc + footnoteItem;
154
- }, "");
155
-
156
- out += this.renderer.footnotes(token, body);
70
+ out += this.renderer.footnotes(token);
157
71
  continue;
158
72
  }
159
73
  case "paragraph": {
160
- out += this.renderer.paragraph(this.parseInline(token.tokens), token.sourceMap);
74
+ out += this.renderer.paragraph(token);
161
75
  continue;
162
76
  }
163
77
  case "text": {
164
- let textToken = token as Tokens["Text"];
165
- let body = textToken.tokens ? this.parseInline(textToken.tokens) : textToken.text;
78
+ let textToken: Tokens["Text"] = token;
79
+ let body = this.renderer.text(textToken);
166
80
  while (i + 1 < tokens.length && tokens[i + 1]?.type === "text") {
167
81
  textToken = tokens[++i] as Tokens["Text"];
168
- body +=
169
- "\n" +
170
- (textToken.tokens ? this.parseInline(textToken.tokens) : textToken.text);
82
+ body += "\n" + this.renderer.text(textToken);
83
+ }
84
+ if (top) {
85
+ out += this.renderer.paragraph({
86
+ type: "paragraph",
87
+ raw: body,
88
+ text: body,
89
+ tokens: [{ type: "text", raw: body, text: body, escaped: true }],
90
+ sourceMap: textToken.sourceMap,
91
+ });
92
+ } else {
93
+ out += body;
171
94
  }
172
- out += top ? this.renderer.paragraph(body, textToken.sourceMap) : body;
173
95
  continue;
174
96
  }
175
97
  case "latexBlock": {
176
- out += this.renderer.latexBlock(token.text, token.sourceMap);
98
+ out += this.renderer.latexBlock(token);
177
99
  continue;
178
100
  }
179
101
 
@@ -190,7 +112,7 @@ export class Parser {
190
112
  /**
191
113
  * Parse Inline Tokens
192
114
  */
193
- parseInline(tokens: Token[]): string {
115
+ parseInline(tokens: Token[], renderer: Renderer | TextRenderer = this.renderer): string {
194
116
  let out = "";
195
117
 
196
118
  for (let i = 0, len = tokens.length; i < len; i++) {
@@ -198,51 +120,55 @@ export class Parser {
198
120
 
199
121
  switch (token.type) {
200
122
  case "escape": {
201
- out += this.renderer.text(token.text);
123
+ out += renderer.text(token);
202
124
  break;
203
125
  }
204
126
  case "html": {
205
- out += this.renderer.html(token.text, false);
127
+ out += renderer.html(token);
206
128
  break;
207
129
  }
208
130
  case "link": {
209
- out += this.renderer.link(token.href, token.title, this.parseInline(token.tokens));
131
+ out += renderer.link(token);
210
132
  break;
211
133
  }
212
134
  case "image": {
213
- out += this.renderer.image(token.href, token.title, token.text);
135
+ out += renderer.image(token);
136
+ break;
137
+ }
138
+ case "checkbox": {
139
+ out += renderer.checkbox(token);
214
140
  break;
215
141
  }
216
142
  case "strong": {
217
- out += this.renderer.strong(this.parseInline(token.tokens));
143
+ out += renderer.strong(token);
218
144
  break;
219
145
  }
220
146
  case "em": {
221
- out += this.renderer.em(this.parseInline(token.tokens));
147
+ out += renderer.em(token);
222
148
  break;
223
149
  }
224
150
  case "footnoteRef": {
225
- out += this.renderer.footnoteRef(token);
151
+ out += renderer.footnoteRef(token);
226
152
  break;
227
153
  }
228
154
  case "codespan": {
229
- out += this.renderer.codespan(token.text);
155
+ out += renderer.codespan(token);
230
156
  break;
231
157
  }
232
158
  case "br": {
233
- out += this.renderer.br();
159
+ out += renderer.br(token);
234
160
  break;
235
161
  }
236
162
  case "del": {
237
- out += this.renderer.del(this.parseInline(token.tokens));
163
+ out += renderer.del(token);
238
164
  break;
239
165
  }
240
166
  case "text": {
241
- out += this.renderer.text(token.text);
167
+ out += renderer.text(token);
242
168
  break;
243
169
  }
244
170
  case "latexInline": {
245
- out += this.renderer.latexInline(token.text);
171
+ out += renderer.latexInline(token);
246
172
  break;
247
173
  }
248
174
  default: {