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