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/renderer.ts CHANGED
@@ -2,7 +2,9 @@ import GithubSlugger from "github-slugger";
2
2
  import hljs from "highlight.js";
3
3
  import { renderToString } from "katex";
4
4
  import { type Pantsdown } from "./pantsdown.ts";
5
+ import { type Parser } from "./parser.ts";
5
6
  import { inline } from "./rules/inline.ts";
7
+ import { other } from "./rules/other.ts";
6
8
  import { type HTMLAttrs, type SourceMap, type Tokens } from "./types.ts";
7
9
  import {
8
10
  addGithubImageStyles,
@@ -11,6 +13,7 @@ import {
11
13
  fixHtmlLocalImageHref,
12
14
  fixLocalImageHref,
13
15
  getHtmlElementText,
16
+ hashString,
14
17
  injectHtmlAttributes,
15
18
  } from "./utils.ts";
16
19
 
@@ -19,20 +22,33 @@ import {
19
22
  */
20
23
  export class Renderer {
21
24
  private pantsdown: Pantsdown;
25
+ parser!: Parser; // set by the parser
22
26
  slugger = new GithubSlugger();
27
+ mermaidCounts = new Map<string, number>();
23
28
 
24
29
  constructor(pantsdown: Pantsdown) {
25
30
  this.pantsdown = pantsdown;
26
31
  }
27
32
 
28
- code(code: string, infostring: string | undefined, sourceMap: SourceMap): string {
29
- const lang = /^\S*/.exec(infostring ?? "")?.[0];
30
- code = code.replace(/\n$/, "");
33
+ space(_token: Tokens["Space"]): string {
34
+ return "";
35
+ }
36
+
37
+ code({ text, lang: infostring, sourceMap }: Tokens["Code"]): string {
38
+ const lang = other.notSpaceStart.exec(infostring ?? "")?.[0];
39
+ let code = text.replace(other.endingNewline, "");
31
40
 
32
41
  const attrs: HTMLAttrs = [];
33
42
 
34
43
  if (lang === "mermaid") {
35
44
  attrs.push(["class", "mermaid-container mermaid"]);
45
+ // stable identity across re-renders — the pan/zoom script keys
46
+ // stashed transforms on it; the occurrence index disambiguates
47
+ // byte-identical diagrams in the same document
48
+ const hash = hashString(code);
49
+ const count = this.mermaidCounts.get(hash) ?? 0;
50
+ this.mermaidCounts.set(hash, count + 1);
51
+ attrs.push(["data-mermaid-key", `${hash}-${count}`]);
36
52
  // escape so diagram source can't be parsed as HTML;
37
53
  // mermaid reads textContent, which undoes the escaping
38
54
  const result = `<pre style="position: relative;">` + escape(code, true) + "\n" + `</pre>`;
@@ -40,7 +56,7 @@ export class Renderer {
40
56
  }
41
57
 
42
58
  if (lang === "math") {
43
- return this.latexBlock(code, sourceMap);
59
+ return this.latexBlock({ text: code, sourceMap });
44
60
  }
45
61
 
46
62
  const language = lang && hljs.getLanguage(lang) ? lang : "plaintext";
@@ -51,51 +67,70 @@ export class Renderer {
51
67
  return injectHtmlAttributes(result, attrs, sourceMap);
52
68
  }
53
69
 
54
- alert(body: string, token: Tokens["Alert"]): string {
70
+ alert(token: Tokens["Alert"]): string {
71
+ const body = this.parser.parse(token.tokens);
55
72
  const result = `<div><span>${token.icon + token.variant}</span>${body}</div>\n`;
56
73
  return injectHtmlAttributes(result, [
57
74
  ["class", `markdown-alert markdown-alert-${token.variant.toLowerCase()}`],
58
75
  ]);
59
76
  }
60
77
 
61
- blockquote(quote: string): string {
62
- return `<blockquote>\n${quote}</blockquote>\n`;
78
+ blockquote({ tokens }: Tokens["Blockquote"]): string {
79
+ const body = this.parser.parse(tokens);
80
+ return `<blockquote>\n${body}</blockquote>\n`;
63
81
  }
64
82
 
65
- html(html: string, _block: boolean, sourceMap?: SourceMap): string {
66
- let result = fixHtmlLocalImageHref(html, this.pantsdown.config);
83
+ html(token: Tokens["HTML"] | Tokens["Tag"]): string {
84
+ let result = fixHtmlLocalImageHref(token.text, this.pantsdown.config);
67
85
  result = addGithubImageStyles(result);
68
86
 
69
87
  const attrs: HTMLAttrs = [];
70
88
 
71
89
  if (this.pantsdown.config.renderer.detailsTagDefaultOpen) {
72
- const tag = inline.tag.exec(html);
90
+ const tag = inline.tag.exec(token.text);
73
91
  if (tag?.[0] === "<details>") {
74
92
  attrs.push(["open", ""]);
75
93
  }
76
94
  }
77
95
 
78
- return injectHtmlAttributes(result, attrs, sourceMap);
96
+ return injectHtmlAttributes(
97
+ result,
98
+ attrs,
99
+ "sourceMap" in token ? token.sourceMap : undefined,
100
+ );
79
101
  }
80
102
 
81
- heading(text: string, level: number, sourceMap: SourceMap): string {
103
+ heading({ tokens, depth, sourceMap }: Tokens["Heading"]): string {
104
+ const text = this.parser.parseInline(tokens);
82
105
  const elementText = getHtmlElementText(text);
83
106
  const slug = this.slugger.slug(elementText);
84
- let result = `<h${level}>`;
107
+ let result = `<h${depth}>`;
85
108
  // span with negative top to add some offset when scrolling to #slug
86
109
  result += `<span style="position: absolute; top: -50px;" id="${slug}"></span>`;
87
110
  result += `${text}<a class="anchor octicon-link" href="#${slug}"></a>`;
88
- result += `</h${level}>\n`;
111
+ result += `</h${depth}>\n`;
89
112
  return injectHtmlAttributes(result, [["style", "position: relative;"]], sourceMap);
90
113
  }
91
114
 
92
- hr(sourceMap: SourceMap): string {
115
+ hr({ sourceMap }: Tokens["Hr"]): string {
93
116
  return injectHtmlAttributes(`<hr>\n`, [], sourceMap);
94
117
  }
95
118
 
96
- list(body: string, ordered: boolean, start: number | "", classes: string[] = []): string {
119
+ list(token: Tokens["List"]): string {
120
+ const ordered = token.ordered;
121
+ const start = token.start;
122
+
123
+ let body = "";
124
+ let containsTaskList = false;
125
+ for (const item of token.items) {
126
+ if (item.task) containsTaskList = true;
127
+ body += this.listitem(item);
128
+ }
129
+
97
130
  const type = ordered ? "ol" : "ul";
98
- const attrs: HTMLAttrs = [["class", classes.join(" ")]];
131
+ const listClasses: string[] = [];
132
+ if (containsTaskList) listClasses.push("contains-task-list");
133
+ const attrs: HTMLAttrs = [["class", listClasses.join(" ")]];
99
134
  if (ordered && start && start !== 1) {
100
135
  attrs.push(["start", String(start)]);
101
136
  }
@@ -103,49 +138,75 @@ export class Renderer {
103
138
  return injectHtmlAttributes(result, attrs);
104
139
  }
105
140
 
106
- listitem(text: string, task: boolean, _checked: boolean, sourceMap: SourceMap): string {
107
- const attrs: [string, string][] = [];
108
- if (task) attrs.push(["class", "task-list-item"]);
141
+ listitem(item: Tokens["ListItem"]): string {
142
+ const attrs: HTMLAttrs = [];
143
+ if (item.task) attrs.push(["class", "task-list-item"]);
144
+ const text = this.parser.parse(item.tokens, false);
109
145
  const result = `<li>${text}</li>\n`;
110
- return injectHtmlAttributes(result, attrs, sourceMap);
146
+ return injectHtmlAttributes(result, attrs, item.sourceMap);
111
147
  }
112
148
 
113
- checkbox(checked: boolean, classes: string[] = []): string {
149
+ checkbox({ checked }: Tokens["Checkbox"]): string {
114
150
  const result = "<input>";
115
151
  const attrs: HTMLAttrs = [
116
152
  ["disabled", ""],
117
153
  ["type", "checkbox"],
118
- ["class", classes.join(" ")],
154
+ ["class", "task-list-item-checkbox"],
119
155
  ];
120
156
  if (checked) attrs.push(["checked", ""]);
121
- return injectHtmlAttributes(result, attrs);
157
+ return injectHtmlAttributes(result, attrs) + " ";
122
158
  }
123
159
 
124
- paragraph(text: string, sourceMap: SourceMap): string {
125
- const result = `<p>${text}</p>\n`;
160
+ paragraph({ tokens, sourceMap }: Tokens["Paragraph"]): string {
161
+ const result = `<p>${this.parser.parseInline(tokens)}</p>\n`;
126
162
  return injectHtmlAttributes(result, [], sourceMap);
127
163
  }
128
164
 
129
- table(header: string, body: string): string {
165
+ table(token: Tokens["Table"]): string {
166
+ const sourceMapLineStart = token.sourceMap?.[0];
167
+
168
+ let header = "";
169
+ let cell = "";
170
+ for (const headerCell of token.header) {
171
+ cell += this.tablecell(headerCell);
172
+ }
173
+ header += this.tablerow({ text: cell, sourceMapStart: sourceMapLineStart });
174
+
175
+ let body = "";
176
+ for (let j = 0, rowsLen = token.rows.length; j < rowsLen; j++) {
177
+ const row = token.rows[j]!;
178
+
179
+ cell = "";
180
+ for (const rowCell of row) {
181
+ cell += this.tablecell(rowCell);
182
+ }
183
+
184
+ body += this.tablerow({
185
+ text: cell,
186
+ sourceMapStart: sourceMapLineStart ? sourceMapLineStart + 2 + j : undefined,
187
+ });
188
+ }
130
189
  if (body) body = `<tbody>${body}</tbody>`;
190
+
131
191
  return "<table>\n" + "<thead>\n" + header + "</thead>\n" + body + "</table>\n";
132
192
  }
133
193
 
134
- tablerow(content: string, sourceMapStart: number | undefined): string {
194
+ tablerow({
195
+ text,
196
+ sourceMapStart,
197
+ }: {
198
+ text: string;
199
+ sourceMapStart?: number | undefined;
200
+ }): string {
135
201
  const sourceMap: SourceMap = sourceMapStart ? [sourceMapStart, sourceMapStart] : undefined;
136
- return injectHtmlAttributes(`<tr>\n${content}</tr>\n`, [], sourceMap);
202
+ return injectHtmlAttributes(`<tr>\n${text}</tr>\n`, [], sourceMap);
137
203
  }
138
204
 
139
- tablecell(
140
- content: string,
141
- flags: {
142
- header: boolean;
143
- align: "center" | "left" | "right" | null;
144
- },
145
- ): string {
146
- const type = flags.header ? "th" : "td";
205
+ tablecell(token: Tokens["TableCell"]): string {
206
+ const content = this.parser.parseInline(token.tokens);
207
+ const type = token.header ? "th" : "td";
147
208
  const attrs: HTMLAttrs = [];
148
- if (flags.align) attrs.push(["align", flags.align]);
209
+ if (token.align) attrs.push(["align", token.align]);
149
210
  const result = `<${type}>` + content + `</${type}>\n`;
150
211
  return injectHtmlAttributes(result, attrs);
151
212
  }
@@ -153,46 +214,48 @@ export class Renderer {
153
214
  /**
154
215
  * span level renderer
155
216
  */
156
- strong(text: string): string {
157
- return `<strong>${text}</strong>`;
217
+ strong({ tokens }: Tokens["Strong"]): string {
218
+ return `<strong>${this.parser.parseInline(tokens)}</strong>`;
158
219
  }
159
220
 
160
- em(text: string): string {
161
- return `<em>${text}</em>`;
221
+ em({ tokens }: Tokens["Em"]): string {
222
+ return `<em>${this.parser.parseInline(tokens)}</em>`;
162
223
  }
163
224
 
164
- codespan(text: string): string {
165
- return `<code>${text}</code>`;
225
+ codespan({ text }: Tokens["Codespan"]): string {
226
+ return `<code>${escape(text, true)}</code>`;
166
227
  }
167
228
 
168
- br(): string {
229
+ br(_token: Tokens["Br"]): string {
169
230
  return "<br>";
170
231
  }
171
232
 
172
- del(text: string): string {
173
- return `<del>${text}</del>`;
233
+ del({ tokens }: Tokens["Del"]): string {
234
+ return `<del>${this.parser.parseInline(tokens)}</del>`;
174
235
  }
175
236
 
176
- link(href: string, title: string | null | undefined, text: string): string {
237
+ link({ href, title, tokens }: Tokens["Link"]): string {
238
+ const text = this.parser.parseInline(tokens);
177
239
  const cleanHref = cleanUrl(href);
178
240
  if (cleanHref === null) {
179
241
  return text;
180
242
  }
181
243
  const attrs: HTMLAttrs = [["href", cleanHref]];
182
- if (title) attrs.push(["title", title]);
244
+ if (title) attrs.push(["title", escape(title)]);
183
245
  return injectHtmlAttributes(`<a>${text}</a>`, attrs);
184
246
  }
185
247
 
186
- image(href: string, title: string | null, text: string): string {
248
+ image({ href, title, tokens }: Tokens["Image"]): string {
249
+ const text = this.parser.parseInline(tokens, this.parser.textRenderer);
187
250
  const cleanHref = cleanUrl(href);
188
251
  if (cleanHref === null) {
189
- return text;
252
+ return escape(text);
190
253
  }
191
254
  const attrs: HTMLAttrs = [
192
255
  ["src", fixLocalImageHref(cleanHref, this.pantsdown.config)],
193
- ["alt", text],
256
+ ["alt", escape(text)],
194
257
  ];
195
- if (title) attrs.push(["title", title]);
258
+ if (title) attrs.push(["title", escape(title)]);
196
259
  return injectHtmlAttributes("<img>", attrs);
197
260
  }
198
261
 
@@ -204,9 +267,19 @@ export class Renderer {
204
267
  }" data-footnote-ref aria-describedby="footnote-label">${token.label}</a></sup>`;
205
268
  }
206
269
 
207
- footnotes(token: Tokens["Footnotes"], body: string) {
270
+ footnotes(token: Tokens["Footnotes"]) {
208
271
  if (!token.items.length) return "";
209
272
 
273
+ const body = token.items.reduce((acc, { label, content, sourceMap }) => {
274
+ let footnoteItem = `<li id="footnote-${encodeURIComponent(label)}">\n`;
275
+ footnoteItem += this.parser.parse(content);
276
+ footnoteItem += "</li>\n";
277
+
278
+ footnoteItem = injectHtmlAttributes(footnoteItem, [], sourceMap);
279
+
280
+ return acc + footnoteItem;
281
+ }, "");
282
+
210
283
  let footnotesHTML = '<section class="footnotes" data-footnotes>\n';
211
284
  footnotesHTML += `<ol>\n${body}\n</ol>\n`;
212
285
  footnotesHTML += "</section>\n";
@@ -214,13 +287,17 @@ export class Renderer {
214
287
  return footnotesHTML;
215
288
  }
216
289
 
217
- text(text: string): string {
218
- return text;
290
+ text(token: Tokens["Text"] | Tokens["Escape"] | Tokens["Tag"]): string {
291
+ return "tokens" in token
292
+ ? this.parser.parseInline(token.tokens)
293
+ : "escaped" in token && token.escaped
294
+ ? token.text
295
+ : escape(token.text);
219
296
  }
220
297
 
221
- latexBlock(latex: string, sourceMap: SourceMap): string {
298
+ latexBlock({ text, sourceMap }: Pick<Tokens["LatexBlock"], "text" | "sourceMap">): string {
222
299
  try {
223
- const rendered = renderToString(latex, {
300
+ const rendered = renderToString(text, {
224
301
  displayMode: true,
225
302
  throwOnError: false,
226
303
  output: "html",
@@ -229,16 +306,16 @@ export class Renderer {
229
306
  return injectHtmlAttributes(`<div class="katex-block">${rendered}</div>\n`, [], sourceMap);
230
307
  } catch {
231
308
  return injectHtmlAttributes(
232
- `<div class="katex-block katex-error"><code>${escape(latex)}</code></div>\n`,
309
+ `<div class="katex-block katex-error"><code>${escape(text)}</code></div>\n`,
233
310
  [],
234
311
  sourceMap,
235
312
  );
236
313
  }
237
314
  }
238
315
 
239
- latexInline(latex: string): string {
316
+ latexInline({ text }: Tokens["LatexInline"]): string {
240
317
  try {
241
- const rendered = renderToString(latex, {
318
+ const rendered = renderToString(text, {
242
319
  displayMode: false,
243
320
  throwOnError: false,
244
321
  output: "html",
@@ -246,7 +323,7 @@ export class Renderer {
246
323
  });
247
324
  return `<span class="katex-inline">${rendered}</span>`;
248
325
  } catch {
249
- return `<span class="katex-inline katex-error"><code>${escape(latex)}</code></span>`;
326
+ return `<span class="katex-inline katex-error"><code>${escape(text)}</code></span>`;
250
327
  }
251
328
  }
252
329
  }
@@ -19,7 +19,7 @@ type BlockRuleNames =
19
19
  | "paragraph"
20
20
  | "latexBlock";
21
21
 
22
- export const label = /(?!\s*\])(?:\\.|[^\[\]\\])+/;
22
+ export const label = /(?!\s*\])(?:\\[\s\S]|[^\[\]\\])+/;
23
23
 
24
24
  const tag =
25
25
  "address|article|aside|base|basefont|blockquote|body|caption" +
@@ -32,19 +32,19 @@ const tag =
32
32
  const title = /(?:"(?:\\"?|[^"\\])*"|'[^'\n]*(?:\n[^'\n]+)*\n?'|\([^()]*\))/;
33
33
 
34
34
  const block_def = edit(
35
- /^ {0,3}\[(label)\]: *(?:\n *)?([^<\s][^\s]*|<.*?>)(?:(?: +(?:\n *)?| *\n *)(title))? *(?:\n+|$)/,
35
+ /^ {0,3}\[(label)\]: *(?:\n[ \t]*)?([^<\s][^\s]*|<.*?>)(?:(?: +(?:\n[ \t]*)?| *\n[ \t]*)(title))? *(?:\n+|$)/,
36
36
  )
37
37
  .replace("label", label)
38
38
  .replace("title", title)
39
39
  .getRegex();
40
40
 
41
- const block_bullet = /(?:[*+-]|\d{1,9}[.)])/;
41
+ const block_bullet = / {0,3}(?:[*+-]|\d{1,9}[.)])/;
42
42
 
43
43
  const block_listItemStart = edit(/^( *)(bull) */)
44
44
  .replace("bull", block_bullet)
45
45
  .getRegex();
46
46
 
47
- const block_list = edit(/^( {0,3}bull)([ \t][^\n]+?)?(?:\n|$)/)
47
+ const block_list = edit(/^(bull)([ \t][^\n]*?)?(?:\n|$)/)
48
48
  .replace(/bull/g, block_bullet)
49
49
  .replace("hr", "\\n+(?=\\1?(?:(?:- *){3,}|(?:_ *){3,}|(?:\\* *){3,})(?:\\n+|$))")
50
50
  .replace("def", "\\n+(?=" + block_def.source + ")")
@@ -54,14 +54,14 @@ export const comment = /<!--(?:-?>|[\s\S]*?(?:-->|$))/;
54
54
 
55
55
  const block_html = edit(
56
56
  "^ {0,3}(?:" + // optional indentation
57
- "<(script|pre|style|textarea)[\\s>][\\s\\S]*?(?:</\\1>[^\\n]*\\n+|$)" + // (1)
57
+ "<(script|pre|style|textarea)[\\s>][\\s\\S]*?(?:</\\1>[^\\n]*\\n*|$)" + // (1)
58
58
  "|comment[^\\n]*(\\n+|$)" + // (2)
59
- "|<\\?[\\s\\S]*?(?:\\?>\\n*|$)" + // (3)
60
- "|<![A-Z][\\s\\S]*?(?:>\\n*|$)" + // (4)
61
- "|<!\\[CDATA\\[[\\s\\S]*?(?:\\]\\]>\\n*|$)" + // (5)
62
- "|</?(tag)(?: +|\\n|/?>)[\\s\\S]*?(?:(?:\\n *)+\\n|$)" + // (6)
63
- "|<(?!script|pre|style|textarea)([a-z][\\w-]*)(?:attribute)*? */?>(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n *)+\\n|$)" + // (7) open tag
64
- "|</(?!script|pre|style|textarea)[a-z][\\w-]*\\s*>(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n *)+\\n|$)" + // (7) closing tag
59
+ "|<\\?[\\s\\S]*?(?:\\?>[^\\n]*\\n*|$)" + // (3)
60
+ "|<![A-Z][\\s\\S]*?(?:>[^\\n]*\\n*|$)" + // (4)
61
+ "|<!\\[CDATA\\[[\\s\\S]*?(?:\\]\\]>[^\\n]*\\n*|$)" + // (5)
62
+ "|</?(tag)(?: +|\\n|/?>)[\\s\\S]*?(?:(?:\\n[ \\t]*)+\\n|$)" + // (6)
63
+ "|<(?!script|pre|style|textarea)([a-z][\\w-]*)(?:attribute)*? */?>(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n[ \\t]*)+\\n|$)" + // (7) open tag
64
+ "|</(?!script|pre|style|textarea)[a-z][\\w-]*\\s*>(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n[ \\t]*)+\\n|$)" + // (7) closing tag
65
65
  ")",
66
66
  "i",
67
67
  )
@@ -70,16 +70,17 @@ const block_html = edit(
70
70
  .replace("attribute", / +[a-zA-Z:_][\w.:-]*(?: *= *"[^"\n]*"| *= *'[^'\n]*'| *= *[^\s"'=<>`]+)?/)
71
71
  .getRegex();
72
72
 
73
- // const block_lheading = edit(/^(?!bull )((?:.|\n(?!\s*?\n|bull ))+?)\n {0,3}(=+|-+) *(?:\n+|$)/)
73
+ // upstream's lheadingGfm variant (we are GFM-only; the commonmark variant drops |table)
74
74
  const block_lheading = edit(
75
- /^(?!bull |blockCode|fences|blockquote|heading|html)((?:.|\n(?!\s*?\n|bull |blockCode|fences|blockquote|heading|html))+?)\n {0,3}(=+|-+) *(?:\n+|$)/,
75
+ /^(?!bull |blockCode|fences|blockquote|heading|html|table)((?:.|\n(?!\s*?\n|bull |blockCode|fences|blockquote|heading|html|table))+?)\n {0,3}(=+|-+) *(?:\n+|$)/,
76
76
  )
77
77
  .replace(/bull/g, block_bullet) // lists can interrupt
78
- .replace(/blockCode/g, / {4}/) // indented code blocks can interrupt
78
+ .replace(/blockCode/g, /(?: {4}| {0,3}\t)/) // indented code blocks can interrupt
79
79
  .replace(/fences/g, / {0,3}(?:`{3,}|~{3,})/) // fenced code blocks can interrupt
80
80
  .replace(/blockquote/g, / {0,3}>/) // blockquote can interrupt
81
- .replace(/heading/g, / {0,3}#{1,6}/) // ATX heading can interrupt
81
+ .replace(/heading/g, / {0,3}#{1,6}(?:\s|$)/) // ATX heading can interrupt
82
82
  .replace(/html/g, / {0,3}<[^\n>]+>\n/) // block html can interrupt
83
+ .replace(/table/g, / {0,3}\|?(?:[:\- ]*\|)+[\:\- ]*\n/) // table can interrupt
83
84
  .getRegex();
84
85
 
85
86
  const block_hr = /^ {0,3}((?:-[\t ]*){3,}|(?:_[ \t]*){3,}|(?:\*[ \t]*){3,})(?:\n+|$)/;
@@ -92,34 +93,40 @@ const block_table = edit(
92
93
  .replace("hr", block_hr)
93
94
  .replace("heading", " {0,3}#{1,6}(?:\\s|$)")
94
95
  .replace("blockquote", " {0,3}>")
95
- .replace("code", " {4}[^\\n]")
96
- .replace("fences", " {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n")
97
- .replace("list", " {0,3}(?:[*+-]|1[.)]) ") // only lists starting from 1 can interrupt
96
+ .replace("code", "(?: {4}| {0,3}\\t)[^\\n]")
97
+ .replace("fences", " {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~~~)[^\\n]*\\n")
98
+ .replace("list", " {0,3}(?:[*+-]|1[.)])[ \\t]") // any bullet ends the table rows
98
99
  .replace("html", "</?(?:tag)(?: +|\\n|/?>)|<(?:script|pre|style|textarea|!--)")
99
100
  .replace("tag", tag) // tables can be interrupted by type (6) html blocks
100
101
  .getRegex();
101
102
 
102
- const block_paragraph = edit(
103
- /^([^\n]+(?:\n(?!hr|heading|lheading|blockquote|fences|list|html|table| +\n)[^\n]+)*)/,
104
- )
105
- .replace("hr", block_hr)
106
- .replace("heading", " {0,3}#{1,6}(?:\\s|$)")
107
- .replace("|lheading", "") // setext headings don't interrupt commonmark paragraphs
108
- .replace("table", block_table) // interrupt paragraphs with table
109
- .replace("blockquote", " {0,3}>")
110
- .replace("fences", " {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n")
111
- .replace("list", " {0,3}(?:[*+-]|1[.)]) ") // only lists starting from 1 can interrupt
112
- .replace("html", "</?(?:tag)(?: +|\\n|/?>)|<(?:script|pre|style|textarea|!--)")
113
- .replace("tag", tag) // pars can be interrupted by type (6) html blocks
114
- .getRegex();
103
+ const createParagraph = (listInterrupt: string) =>
104
+ edit(/^([^\n]+(?:\n(?!hr|heading|lheading|blockquote|fences|list|html|table|[ \t]+\n)[^\n]+)*)/)
105
+ .replace("hr", block_hr)
106
+ .replace("heading", " {0,3}#{1,6}(?:\\s|$)")
107
+ .replace("|lheading", "") // setext headings don't interrupt commonmark paragraphs
108
+ .replace("table", block_table) // interrupt paragraphs with table
109
+ .replace("blockquote", " {0,3}>")
110
+ .replace("fences", " {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~~~)[^\\n]*\\n")
111
+ .replace("list", listInterrupt)
112
+ .replace("html", "</?(?:tag)(?: +|\\n|/?>)|<(?:script|pre|style|textarea|!--)")
113
+ .replace("tag", tag) // pars can be interrupted by type (6) html blocks
114
+ .getRegex();
115
+
116
+ // only non-empty lists starting from 1 can interrupt paragraphs
117
+ const block_paragraph = createParagraph(" {0,3}(?:[*+-]|1[.)])[ \\t]+[^ \\t\\n]");
118
+ // inside a blockquote a bare list marker (any number) starts a sibling list,
119
+ // so it must not be lazily continued as paragraph text (unlike a top level
120
+ // paragraph, where an empty list cannot interrupt)
121
+ const block_blockquoteParagraph = createParagraph(" {0,3}(?:[*+-]|\\d{1,9}[.)])(?:[ \\t]|\\n|$)");
115
122
 
116
123
  const block_blockquote = edit(/^( {0,3}> ?(paragraph|[^\n]*)(?:\n|$))+/)
117
- .replace("paragraph", block_paragraph)
124
+ .replace("paragraph", block_blockquoteParagraph)
118
125
  .getRegex();
119
126
 
120
127
  export const block: Record<BlockRuleNames, RegExp> = {
121
- newline: /^(?: *(?:\n|$))+/,
122
- code: /^( {4}[^\n]+(?:\n(?: *(?:\n|$))*)?)+/,
128
+ newline: /^(?:[ \t]*(?:\n|$))+/,
129
+ code: /^((?: {4}| {0,3}\t)[^\n]+(?:\n(?:[ \t]*(?:\n|$))*)?)+/,
123
130
  fences:
124
131
  /^ {0,3}(`{3,}(?=[^`\n]*(?:\n|$))|~{3,})([^\n]*)(?:\n|$)(?:|([\s\S]*?)(?:\n|$))(?: {0,3}\1[~`]* *(?=\n|$)|$)/,
125
132
  hr: block_hr,