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/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,
@@ -19,26 +21,33 @@ import {
19
21
  */
20
22
  export class Renderer {
21
23
  private pantsdown: Pantsdown;
24
+ parser!: Parser; // set by the parser
22
25
  slugger = new GithubSlugger();
23
26
 
24
27
  constructor(pantsdown: Pantsdown) {
25
28
  this.pantsdown = pantsdown;
26
29
  }
27
30
 
28
- code(code: string, infostring: string | undefined, sourceMap: SourceMap): string {
29
- const lang = /^\S*/.exec(infostring ?? "")?.[0];
30
- code = code.replace(/\n$/, "");
31
+ space(_token: Tokens["Space"]): string {
32
+ return "";
33
+ }
34
+
35
+ code({ text, lang: infostring, sourceMap }: Tokens["Code"]): string {
36
+ const lang = other.notSpaceStart.exec(infostring ?? "")?.[0];
37
+ let code = text.replace(other.endingNewline, "");
31
38
 
32
39
  const attrs: HTMLAttrs = [];
33
40
 
34
41
  if (lang === "mermaid") {
35
42
  attrs.push(["class", "mermaid-container mermaid"]);
36
- const result = `<pre style="position: relative;">` + code + "\n" + `</pre>`;
43
+ // escape so diagram source can't be parsed as HTML;
44
+ // mermaid reads textContent, which undoes the escaping
45
+ const result = `<pre style="position: relative;">` + escape(code, true) + "\n" + `</pre>`;
37
46
  return injectHtmlAttributes(result, attrs, sourceMap);
38
47
  }
39
48
 
40
49
  if (lang === "math") {
41
- return this.latexBlock(code, sourceMap);
50
+ return this.latexBlock({ text: code, sourceMap });
42
51
  }
43
52
 
44
53
  const language = lang && hljs.getLanguage(lang) ? lang : "plaintext";
@@ -49,51 +58,70 @@ export class Renderer {
49
58
  return injectHtmlAttributes(result, attrs, sourceMap);
50
59
  }
51
60
 
52
- alert(body: string, token: Tokens["Alert"]): string {
61
+ alert(token: Tokens["Alert"]): string {
62
+ const body = this.parser.parse(token.tokens);
53
63
  const result = `<div><span>${token.icon + token.variant}</span>${body}</div>\n`;
54
64
  return injectHtmlAttributes(result, [
55
65
  ["class", `markdown-alert markdown-alert-${token.variant.toLowerCase()}`],
56
66
  ]);
57
67
  }
58
68
 
59
- blockquote(quote: string): string {
60
- return `<blockquote>\n${quote}</blockquote>\n`;
69
+ blockquote({ tokens }: Tokens["Blockquote"]): string {
70
+ const body = this.parser.parse(tokens);
71
+ return `<blockquote>\n${body}</blockquote>\n`;
61
72
  }
62
73
 
63
- html(html: string, _block: boolean, sourceMap?: SourceMap): string {
64
- let result = fixHtmlLocalImageHref(html, this.pantsdown.config);
74
+ html(token: Tokens["HTML"] | Tokens["Tag"]): string {
75
+ let result = fixHtmlLocalImageHref(token.text, this.pantsdown.config);
65
76
  result = addGithubImageStyles(result);
66
77
 
67
78
  const attrs: HTMLAttrs = [];
68
79
 
69
80
  if (this.pantsdown.config.renderer.detailsTagDefaultOpen) {
70
- const tag = inline.tag.exec(html);
81
+ const tag = inline.tag.exec(token.text);
71
82
  if (tag?.[0] === "<details>") {
72
83
  attrs.push(["open", ""]);
73
84
  }
74
85
  }
75
86
 
76
- return injectHtmlAttributes(result, attrs, sourceMap);
87
+ return injectHtmlAttributes(
88
+ result,
89
+ attrs,
90
+ "sourceMap" in token ? token.sourceMap : undefined,
91
+ );
77
92
  }
78
93
 
79
- heading(text: string, level: number, sourceMap: SourceMap): string {
94
+ heading({ tokens, depth, sourceMap }: Tokens["Heading"]): string {
95
+ const text = this.parser.parseInline(tokens);
80
96
  const elementText = getHtmlElementText(text);
81
97
  const slug = this.slugger.slug(elementText);
82
- let result = `<h${level}>`;
98
+ let result = `<h${depth}>`;
83
99
  // span with negative top to add some offset when scrolling to #slug
84
100
  result += `<span style="position: absolute; top: -50px;" id="${slug}"></span>`;
85
101
  result += `${text}<a class="anchor octicon-link" href="#${slug}"></a>`;
86
- result += `</h${level}>\n`;
102
+ result += `</h${depth}>\n`;
87
103
  return injectHtmlAttributes(result, [["style", "position: relative;"]], sourceMap);
88
104
  }
89
105
 
90
- hr(sourceMap: SourceMap): string {
106
+ hr({ sourceMap }: Tokens["Hr"]): string {
91
107
  return injectHtmlAttributes(`<hr>\n`, [], sourceMap);
92
108
  }
93
109
 
94
- list(body: string, ordered: boolean, start: number | "", classes: string[] = []): string {
110
+ list(token: Tokens["List"]): string {
111
+ const ordered = token.ordered;
112
+ const start = token.start;
113
+
114
+ let body = "";
115
+ let containsTaskList = false;
116
+ for (const item of token.items) {
117
+ if (item.task) containsTaskList = true;
118
+ body += this.listitem(item);
119
+ }
120
+
95
121
  const type = ordered ? "ol" : "ul";
96
- const attrs: HTMLAttrs = [["class", classes.join(" ")]];
122
+ const listClasses: string[] = [];
123
+ if (containsTaskList) listClasses.push("contains-task-list");
124
+ const attrs: HTMLAttrs = [["class", listClasses.join(" ")]];
97
125
  if (ordered && start && start !== 1) {
98
126
  attrs.push(["start", String(start)]);
99
127
  }
@@ -101,49 +129,75 @@ export class Renderer {
101
129
  return injectHtmlAttributes(result, attrs);
102
130
  }
103
131
 
104
- listitem(text: string, task: boolean, _checked: boolean, sourceMap: SourceMap): string {
105
- const attrs: [string, string][] = [];
106
- if (task) attrs.push(["class", "task-list-item"]);
132
+ listitem(item: Tokens["ListItem"]): string {
133
+ const attrs: HTMLAttrs = [];
134
+ if (item.task) attrs.push(["class", "task-list-item"]);
135
+ const text = this.parser.parse(item.tokens, false);
107
136
  const result = `<li>${text}</li>\n`;
108
- return injectHtmlAttributes(result, attrs, sourceMap);
137
+ return injectHtmlAttributes(result, attrs, item.sourceMap);
109
138
  }
110
139
 
111
- checkbox(checked: boolean, classes: string[] = []): string {
140
+ checkbox({ checked }: Tokens["Checkbox"]): string {
112
141
  const result = "<input>";
113
142
  const attrs: HTMLAttrs = [
114
143
  ["disabled", ""],
115
144
  ["type", "checkbox"],
116
- ["class", classes.join(" ")],
145
+ ["class", "task-list-item-checkbox"],
117
146
  ];
118
147
  if (checked) attrs.push(["checked", ""]);
119
- return injectHtmlAttributes(result, attrs);
148
+ return injectHtmlAttributes(result, attrs) + " ";
120
149
  }
121
150
 
122
- paragraph(text: string, sourceMap: SourceMap): string {
123
- const result = `<p>${text}</p>\n`;
151
+ paragraph({ tokens, sourceMap }: Tokens["Paragraph"]): string {
152
+ const result = `<p>${this.parser.parseInline(tokens)}</p>\n`;
124
153
  return injectHtmlAttributes(result, [], sourceMap);
125
154
  }
126
155
 
127
- table(header: string, body: string): string {
156
+ table(token: Tokens["Table"]): string {
157
+ const sourceMapLineStart = token.sourceMap?.[0];
158
+
159
+ let header = "";
160
+ let cell = "";
161
+ for (const headerCell of token.header) {
162
+ cell += this.tablecell(headerCell);
163
+ }
164
+ header += this.tablerow({ text: cell, sourceMapStart: sourceMapLineStart });
165
+
166
+ let body = "";
167
+ for (let j = 0, rowsLen = token.rows.length; j < rowsLen; j++) {
168
+ const row = token.rows[j]!;
169
+
170
+ cell = "";
171
+ for (const rowCell of row) {
172
+ cell += this.tablecell(rowCell);
173
+ }
174
+
175
+ body += this.tablerow({
176
+ text: cell,
177
+ sourceMapStart: sourceMapLineStart ? sourceMapLineStart + 2 + j : undefined,
178
+ });
179
+ }
128
180
  if (body) body = `<tbody>${body}</tbody>`;
181
+
129
182
  return "<table>\n" + "<thead>\n" + header + "</thead>\n" + body + "</table>\n";
130
183
  }
131
184
 
132
- tablerow(content: string, sourceMapStart: number | undefined): string {
185
+ tablerow({
186
+ text,
187
+ sourceMapStart,
188
+ }: {
189
+ text: string;
190
+ sourceMapStart?: number | undefined;
191
+ }): string {
133
192
  const sourceMap: SourceMap = sourceMapStart ? [sourceMapStart, sourceMapStart] : undefined;
134
- return injectHtmlAttributes(`<tr>\n${content}</tr>\n`, [], sourceMap);
193
+ return injectHtmlAttributes(`<tr>\n${text}</tr>\n`, [], sourceMap);
135
194
  }
136
195
 
137
- tablecell(
138
- content: string,
139
- flags: {
140
- header: boolean;
141
- align: "center" | "left" | "right" | null;
142
- },
143
- ): string {
144
- const type = flags.header ? "th" : "td";
196
+ tablecell(token: Tokens["TableCell"]): string {
197
+ const content = this.parser.parseInline(token.tokens);
198
+ const type = token.header ? "th" : "td";
145
199
  const attrs: HTMLAttrs = [];
146
- if (flags.align) attrs.push(["align", flags.align]);
200
+ if (token.align) attrs.push(["align", token.align]);
147
201
  const result = `<${type}>` + content + `</${type}>\n`;
148
202
  return injectHtmlAttributes(result, attrs);
149
203
  }
@@ -151,46 +205,48 @@ export class Renderer {
151
205
  /**
152
206
  * span level renderer
153
207
  */
154
- strong(text: string): string {
155
- return `<strong>${text}</strong>`;
208
+ strong({ tokens }: Tokens["Strong"]): string {
209
+ return `<strong>${this.parser.parseInline(tokens)}</strong>`;
156
210
  }
157
211
 
158
- em(text: string): string {
159
- return `<em>${text}</em>`;
212
+ em({ tokens }: Tokens["Em"]): string {
213
+ return `<em>${this.parser.parseInline(tokens)}</em>`;
160
214
  }
161
215
 
162
- codespan(text: string): string {
163
- return `<code>${text}</code>`;
216
+ codespan({ text }: Tokens["Codespan"]): string {
217
+ return `<code>${escape(text, true)}</code>`;
164
218
  }
165
219
 
166
- br(): string {
220
+ br(_token: Tokens["Br"]): string {
167
221
  return "<br>";
168
222
  }
169
223
 
170
- del(text: string): string {
171
- return `<del>${text}</del>`;
224
+ del({ tokens }: Tokens["Del"]): string {
225
+ return `<del>${this.parser.parseInline(tokens)}</del>`;
172
226
  }
173
227
 
174
- link(href: string, title: string | null | undefined, text: string): string {
228
+ link({ href, title, tokens }: Tokens["Link"]): string {
229
+ const text = this.parser.parseInline(tokens);
175
230
  const cleanHref = cleanUrl(href);
176
231
  if (cleanHref === null) {
177
232
  return text;
178
233
  }
179
234
  const attrs: HTMLAttrs = [["href", cleanHref]];
180
- if (title) attrs.push(["title", title]);
235
+ if (title) attrs.push(["title", escape(title)]);
181
236
  return injectHtmlAttributes(`<a>${text}</a>`, attrs);
182
237
  }
183
238
 
184
- image(href: string, title: string | null, text: string): string {
239
+ image({ href, title, tokens }: Tokens["Image"]): string {
240
+ const text = this.parser.parseInline(tokens, this.parser.textRenderer);
185
241
  const cleanHref = cleanUrl(href);
186
242
  if (cleanHref === null) {
187
- return text;
243
+ return escape(text);
188
244
  }
189
245
  const attrs: HTMLAttrs = [
190
246
  ["src", fixLocalImageHref(cleanHref, this.pantsdown.config)],
191
- ["alt", text],
247
+ ["alt", escape(text)],
192
248
  ];
193
- if (title) attrs.push(["title", title]);
249
+ if (title) attrs.push(["title", escape(title)]);
194
250
  return injectHtmlAttributes("<img>", attrs);
195
251
  }
196
252
 
@@ -202,9 +258,19 @@ export class Renderer {
202
258
  }" data-footnote-ref aria-describedby="footnote-label">${token.label}</a></sup>`;
203
259
  }
204
260
 
205
- footnotes(token: Tokens["Footnotes"], body: string) {
261
+ footnotes(token: Tokens["Footnotes"]) {
206
262
  if (!token.items.length) return "";
207
263
 
264
+ const body = token.items.reduce((acc, { label, content, sourceMap }) => {
265
+ let footnoteItem = `<li id="footnote-${encodeURIComponent(label)}">\n`;
266
+ footnoteItem += this.parser.parse(content);
267
+ footnoteItem += "</li>\n";
268
+
269
+ footnoteItem = injectHtmlAttributes(footnoteItem, [], sourceMap);
270
+
271
+ return acc + footnoteItem;
272
+ }, "");
273
+
208
274
  let footnotesHTML = '<section class="footnotes" data-footnotes>\n';
209
275
  footnotesHTML += `<ol>\n${body}\n</ol>\n`;
210
276
  footnotesHTML += "</section>\n";
@@ -212,13 +278,17 @@ export class Renderer {
212
278
  return footnotesHTML;
213
279
  }
214
280
 
215
- text(text: string): string {
216
- return text;
281
+ text(token: Tokens["Text"] | Tokens["Escape"] | Tokens["Tag"]): string {
282
+ return "tokens" in token
283
+ ? this.parser.parseInline(token.tokens)
284
+ : "escaped" in token && token.escaped
285
+ ? token.text
286
+ : escape(token.text);
217
287
  }
218
288
 
219
- latexBlock(latex: string, sourceMap: SourceMap): string {
289
+ latexBlock({ text, sourceMap }: Pick<Tokens["LatexBlock"], "text" | "sourceMap">): string {
220
290
  try {
221
- const rendered = renderToString(latex, {
291
+ const rendered = renderToString(text, {
222
292
  displayMode: true,
223
293
  throwOnError: false,
224
294
  output: "html",
@@ -227,16 +297,16 @@ export class Renderer {
227
297
  return injectHtmlAttributes(`<div class="katex-block">${rendered}</div>\n`, [], sourceMap);
228
298
  } catch {
229
299
  return injectHtmlAttributes(
230
- `<div class="katex-block katex-error"><code>${escape(latex)}</code></div>\n`,
300
+ `<div class="katex-block katex-error"><code>${escape(text)}</code></div>\n`,
231
301
  [],
232
302
  sourceMap,
233
303
  );
234
304
  }
235
305
  }
236
306
 
237
- latexInline(latex: string): string {
307
+ latexInline({ text }: Tokens["LatexInline"]): string {
238
308
  try {
239
- const rendered = renderToString(latex, {
309
+ const rendered = renderToString(text, {
240
310
  displayMode: false,
241
311
  throwOnError: false,
242
312
  output: "html",
@@ -244,7 +314,7 @@ export class Renderer {
244
314
  });
245
315
  return `<span class="katex-inline">${rendered}</span>`;
246
316
  } catch {
247
- return `<span class="katex-inline katex-error"><code>${escape(latex)}</code></span>`;
317
+ return `<span class="katex-inline katex-error"><code>${escape(text)}</code></span>`;
248
318
  }
249
319
  }
250
320
  }
@@ -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,