pantsdown 2.2.6 → 2.2.8

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/README.md CHANGED
@@ -165,3 +165,5 @@ console.log(html);
165
165
 
166
166
  Pantsdown is based on [Marked](https://github.com/markedjs/marked). Without their hard work,
167
167
  Pantsdown would not exist.
168
+
169
+ Last synced with Marked [v18.0.7](https://github.com/markedjs/marked/releases/tag/v18.0.7).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pantsdown",
3
- "version": "2.2.6",
3
+ "version": "2.2.8",
4
4
  "description": "Markdown to 'GitHub HTML' parser",
5
5
  "license": "MIT",
6
6
  "author": "wallpants",
@@ -8,6 +8,10 @@
8
8
  "type": "git",
9
9
  "url": "git://github.com/wallpants/pantsdown.git"
10
10
  },
11
+ "files": [
12
+ "src",
13
+ "!src/**/*.test.ts"
14
+ ],
11
15
  "type": "module",
12
16
  "exports": {
13
17
  "./*": "./*",
@@ -26,33 +30,31 @@
26
30
  "commit": "cz",
27
31
  "format": "oxfmt .",
28
32
  "typecheck": "tsc",
29
- "lint": "eslint .",
33
+ "lint": "oxlint .",
30
34
  "check": "bun run typecheck && bun run lint",
31
- "docs:build": "bun run docs/build.ts"
35
+ "docs:build": "bun run docs/build.ts",
36
+ "prepare": "husky"
32
37
  },
33
38
  "dependencies": {
34
39
  "github-slugger": "^2.0.0",
35
40
  "highlight.js": "^11.11.1",
36
- "katex": "^0.17.0"
41
+ "katex": "^0.18.0"
37
42
  },
38
43
  "devDependencies": {
39
- "@commitlint/config-conventional": "^21.0.2",
40
- "@commitlint/cz-commitlint": "^21.0.2",
41
- "@happy-dom/global-registrator": "^20.9.0",
44
+ "@commitlint/cli": "^21.2.1",
45
+ "@commitlint/config-conventional": "^21.2.0",
46
+ "@commitlint/cz-commitlint": "^21.2.0",
47
+ "@happy-dom/global-registrator": "^20.11.0",
42
48
  "@types/bun": "^1.3.14",
43
49
  "@types/katex": "^0.16.8",
44
- "@typescript-eslint/eslint-plugin": "^8.60.0",
45
- "@typescript-eslint/parser": "^8.60.0",
46
- "commitizen": "^4.3.1",
47
- "commitlint": "^21.0.2",
48
- "eslint": "^9.39.4",
49
- "eslint-import-resolver-typescript": "^4.4.4",
50
- "eslint-plugin-import-x": "^4.16.2",
50
+ "commitizen": "^4.3.2",
51
+ "husky": "^9.1.7",
51
52
  "inquirer": "^12.11.1",
52
- "oxfmt": "^0.52.0",
53
- "semantic-release": "^25.0.3",
54
- "typescript": "6.0.3",
55
- "typescript-eslint": "^8.60.0"
53
+ "oxfmt": "^0.59.0",
54
+ "oxlint": "^1.73.0",
55
+ "oxlint-tsgolint": "^0.25.0",
56
+ "semantic-release": "^25.0.8",
57
+ "typescript": "7.0.2"
56
58
  },
57
59
  "commitlint": {
58
60
  "extends": [
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[] = [];
@@ -31,8 +32,13 @@ export class Lexer {
31
32
  this.tokens = [];
32
33
  this.links = {};
33
34
  this.line = 1;
35
+ this.state = {
36
+ inLink: false,
37
+ inRawBlock: false,
38
+ top: true,
39
+ };
34
40
 
35
- src = src.replace(/\r\n|\r/g, "\n");
41
+ src = src.replace(other.carriageReturn, "\n");
36
42
 
37
43
  this.blockTokens(src, this.tokens);
38
44
 
@@ -70,17 +76,19 @@ export class Lexer {
70
76
  /**
71
77
  * Lexing
72
78
  */
73
- blockTokens(src: string, tokens: Token[]): Token[] {
74
- src = src.replace(/^( *)(\t+)/gm, (_, leading: string, tabs: string) => {
75
- return leading + " ".repeat(tabs.length);
76
- });
77
-
79
+ blockTokens(src: string, tokens: Token[], lastParagraphClipped = false): Token[] {
78
80
  let token: Token | undefined;
79
81
  let lastToken: Token | undefined;
80
82
  let cutSrc;
81
- let lastParagraphClipped;
82
83
 
84
+ let srcLength = Infinity;
83
85
  while (src) {
86
+ if (src.length < srcLength) {
87
+ srcLength = src.length;
88
+ } else {
89
+ this.infiniteLoopError(src.charCodeAt(0));
90
+ }
91
+
84
92
  // newline
85
93
  if ((token = this.tokenizer.space(src))) {
86
94
  src = src.substring(token.raw.length);
@@ -101,7 +109,7 @@ export class Lexer {
101
109
  lastToken = tokens[tokens.length - 1];
102
110
  // An indented code block cannot interrupt a paragraph.
103
111
  if (lastToken && (lastToken.type === "paragraph" || lastToken.type === "text")) {
104
- lastToken.raw += "\n" + token.raw;
112
+ lastToken.raw += (lastToken.raw.endsWith("\n") ? "" : "\n") + token.raw;
105
113
  lastToken.text += "\n" + token.text;
106
114
  const lastInline = this.inlineQueue[this.inlineQueue.length - 1];
107
115
  if (lastInline) lastInline.src = lastToken.text;
@@ -172,7 +180,7 @@ export class Lexer {
172
180
  src = src.substring(token.raw.length);
173
181
  lastToken = tokens[tokens.length - 1];
174
182
  if (lastToken && (lastToken.type === "paragraph" || lastToken.type === "text")) {
175
- lastToken.raw += "\n" + token.raw;
183
+ lastToken.raw += (lastToken.raw.endsWith("\n") ? "" : "\n") + token.raw;
176
184
  lastToken.text += "\n" + token.raw;
177
185
  const lastInline = this.inlineQueue[this.inlineQueue.length - 1];
178
186
  if (lastInline) lastInline.src = lastToken.text;
@@ -205,7 +213,7 @@ export class Lexer {
205
213
  if (this.state.top && (token = this.tokenizer.paragraph(cutSrc))) {
206
214
  lastToken = tokens[tokens.length - 1];
207
215
  if (lastParagraphClipped && lastToken?.type === "paragraph") {
208
- lastToken.raw += "\n" + token.raw;
216
+ lastToken.raw += (lastToken.raw.endsWith("\n") ? "" : "\n") + token.raw;
209
217
  lastToken.text += "\n" + token.text;
210
218
  this.inlineQueue.pop();
211
219
  const lastInline = this.inlineQueue[this.inlineQueue.length - 1];
@@ -223,7 +231,7 @@ export class Lexer {
223
231
  src = src.substring(token.raw.length);
224
232
  lastToken = tokens[tokens.length - 1];
225
233
  if (lastToken?.type === "text") {
226
- lastToken.raw += "\n" + token.raw;
234
+ lastToken.raw += (lastToken.raw.endsWith("\n") ? "" : "\n") + token.raw;
227
235
  lastToken.text += "\n" + token.text;
228
236
  this.inlineQueue.pop();
229
237
  const lastInline = this.inlineQueue[this.inlineQueue.length - 1];
@@ -235,8 +243,7 @@ export class Lexer {
235
243
  }
236
244
 
237
245
  if (src) {
238
- const errMsg = "Infinite loop on byte: " + String(src.charCodeAt(0));
239
- throw new Error(errMsg);
246
+ this.infiniteLoopError(src.charCodeAt(0));
240
247
  }
241
248
  }
242
249
 
@@ -257,42 +264,34 @@ export class Lexer {
257
264
 
258
265
  // String with links masked to avoid interference with em and strong
259
266
  let maskedSrc = src;
260
- let match;
261
267
  let keepPrevChar, prevChar;
262
268
 
263
269
  // Mask out reflinks
264
270
  const links = Object.keys(this.links);
265
271
  if (links.length > 0) {
266
- while ((match = inline.reflinkSearch.exec(maskedSrc)) != null) {
267
- if (links.includes(match[0].slice(match[0].lastIndexOf("[") + 1, -1))) {
268
- maskedSrc =
269
- maskedSrc.slice(0, match.index) +
270
- "[" +
271
- "a".repeat(match[0].length - 2) +
272
- "]" +
273
- maskedSrc.slice(inline.reflinkSearch.lastIndex);
274
- }
275
- }
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
+ );
276
277
  }
277
- // Mask out other blocks
278
- while ((match = inline.blockSkip.exec(maskedSrc)) != null) {
279
- maskedSrc =
280
- maskedSrc.slice(0, match.index) +
281
- "[" +
282
- "a".repeat(match[0].length - 2) +
283
- "]" +
284
- maskedSrc.slice(inline.blockSkip.lastIndex);
285
- }
286
-
287
278
  // Mask out escaped characters
288
- while ((match = inline.anyPunctuation.exec(maskedSrc)) != null) {
289
- maskedSrc =
290
- maskedSrc.slice(0, match.index) +
291
- "++" +
292
- maskedSrc.slice(inline.anyPunctuation.lastIndex);
293
- }
279
+ maskedSrc = maskedSrc.replace(inline.anyPunctuation, "++");
280
+
281
+ // Mask out other blocks
282
+ maskedSrc = maskedSrc.replace(
283
+ inline.blockSkip,
284
+ (match0) => "[" + "a".repeat(match0.length - 2) + "]",
285
+ );
294
286
 
287
+ let srcLength = Infinity;
295
288
  while (src) {
289
+ if (src.length < srcLength) {
290
+ srcLength = src.length;
291
+ } else {
292
+ this.infiniteLoopError(src.charCodeAt(0));
293
+ }
294
+
296
295
  if (!keepPrevChar) {
297
296
  prevChar = "";
298
297
  }
@@ -315,13 +314,7 @@ export class Lexer {
315
314
  // tag
316
315
  if ((token = this.tokenizer.tag(src))) {
317
316
  src = src.substring(token.raw.length);
318
- lastToken = tokens[tokens.length - 1];
319
- if (lastToken && token.type === "text" && lastToken.type === "text") {
320
- lastToken.raw += token.raw;
321
- lastToken.text += token.text;
322
- } else {
323
- tokens.push(token);
324
- }
317
+ tokens.push(token);
325
318
  continue;
326
319
  }
327
320
 
@@ -374,7 +367,7 @@ export class Lexer {
374
367
  }
375
368
 
376
369
  // del (gfm)
377
- if ((token = this.tokenizer.del(src))) {
370
+ if ((token = this.tokenizer.del(src, maskedSrc, prevChar))) {
378
371
  src = src.substring(token.raw.length);
379
372
  tokens.push(token);
380
373
  continue;
@@ -415,11 +408,14 @@ export class Lexer {
415
408
  }
416
409
 
417
410
  if (src) {
418
- const errMsg = "Infinite loop on byte: " + String(src.charCodeAt(0));
419
- throw new Error(errMsg);
411
+ this.infiniteLoopError(src.charCodeAt(0));
420
412
  }
421
413
  }
422
414
 
423
415
  return tokens;
424
416
  }
417
+
418
+ private infiniteLoopError(byte: number): never {
419
+ throw new Error("Infinite loop on byte: " + String(byte));
420
+ }
425
421
  }
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: {