Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 16 additions & 16 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@
"html"
],
"devDependencies": {
"@arethetypeswrong/cli": "^0.18.4",
"@arethetypeswrong/cli": "^0.18.5",
"@markedjs/eslint-config": "^1.0.14",
"@markedjs/testutils": "18.0.0-1",
"@semantic-release/commit-analyzer": "^13.0.1",
Expand All @@ -62,14 +62,14 @@
"dts-bundle-generator": "^9.5.1",
"esbuild": "^0.28.1",
"esbuild-plugin-umd-wrapper": "^3.0.0",
"eslint": "^10.6.0",
"eslint": "^10.7.0",
"highlight.js": "^11.11.1",
"markdown-it": "14.3.0",
"marked-highlight": "^2.2.4",
"marked-man": "^2.1.1",
"recheck": "^4.5.0",
"rimraf": "^6.1.3",
"semantic-release": "^25.0.5",
"semantic-release": "^25.0.7",
"titleize": "^4.0.0",
"tslib": "^2.8.1",
"typescript": "6.0.3"
Expand Down
25 changes: 9 additions & 16 deletions src/Lexer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -304,33 +304,26 @@ export class _Lexer<ParserOutput = string, RendererOutput = string> {
this.tokenizer.lexer = this;
// String with links masked to avoid interference with em and strong
let maskedSrc = src;
let match: RegExpExecArray | null = null;

// Mask out reflinks
if (this.tokens.links) {
const links = Object.keys(this.tokens.links);
if (links.length > 0) {
while ((match = this.tokenizer.rules.inline.reflinkSearch.exec(maskedSrc)) !== null) {
if (links.includes(match[0].slice(match[0].lastIndexOf('[') + 1, -1))) {
maskedSrc = maskedSrc.slice(0, match.index)
+ '[' + 'a'.repeat(match[0].length - 2) + ']'
+ maskedSrc.slice(this.tokenizer.rules.inline.reflinkSearch.lastIndex);
}
}
maskedSrc = maskedSrc.replace(this.tokenizer.rules.inline.reflinkSearch, match0 =>
links.includes(match0.slice(match0.lastIndexOf('[') + 1, -1))
? '[' + 'a'.repeat(match0.length - 2) + ']'
: match0);
}
}

// Mask out escaped characters
while ((match = this.tokenizer.rules.inline.anyPunctuation.exec(maskedSrc)) !== null) {
maskedSrc = maskedSrc.slice(0, match.index) + '++' + maskedSrc.slice(this.tokenizer.rules.inline.anyPunctuation.lastIndex);
}
maskedSrc = maskedSrc.replace(this.tokenizer.rules.inline.anyPunctuation, '++');

// Mask out other blocks
let offset;
while ((match = this.tokenizer.rules.inline.blockSkip.exec(maskedSrc)) !== null) {
offset = match[2] ? match[2].length : 0;
maskedSrc = maskedSrc.slice(0, match.index + offset) + '[' + 'a'.repeat(match[0].length - offset - 2) + ']' + maskedSrc.slice(this.tokenizer.rules.inline.blockSkip.lastIndex);
}
maskedSrc = maskedSrc.replace(this.tokenizer.rules.inline.blockSkip, (match0, _link, context) => {
const offset = context ? context.length : 0;
return match0.slice(0, offset) + '[' + 'a'.repeat(match0.length - offset - 2) + ']';
});

// Mask out blocks from extensions
maskedSrc = this.options.hooks?.emStrongMask?.call({ lexer: this }, maskedSrc) ?? maskedSrc;
Expand Down
30 changes: 16 additions & 14 deletions src/rules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ const lheading = edit(lheadingCore)
.replace(/blockCode/g, /(?: {4}| {0,3}\t)/) // indented code blocks can interrupt
.replace(/fences/g, / {0,3}(?:`{3,}|~{3,})/) // fenced code blocks can interrupt
.replace(/blockquote/g, / {0,3}>/) // blockquote can interrupt
.replace(/heading/g, / {0,3}#{1,6}/) // ATX heading can interrupt
.replace(/heading/g, / {0,3}#{1,6}(?:\s|$)/) // ATX heading can interrupt
.replace(/html/g, / {0,3}<[^\n>]+>\n/) // block html can interrupt
.replace(/\|table/g, '') // table not in commonmark
.getRegex();
Expand All @@ -124,11 +124,11 @@ const lheadingGfm = edit(lheadingCore)
.replace(/blockCode/g, /(?: {4}| {0,3}\t)/) // indented code blocks can interrupt
.replace(/fences/g, / {0,3}(?:`{3,}|~{3,})/) // fenced code blocks can interrupt
.replace(/blockquote/g, / {0,3}>/) // blockquote can interrupt
.replace(/heading/g, / {0,3}#{1,6}/) // ATX heading can interrupt
.replace(/heading/g, / {0,3}#{1,6}(?:\s|$)/) // ATX heading can interrupt
.replace(/html/g, / {0,3}<[^\n>]+>\n/) // block html can interrupt
.replace(/table/g, / {0,3}\|?(?:[:\- ]*\|)+[\:\- ]*\n/) // table can interrupt
.getRegex();
const _paragraph = /^([^\n]+(?:\n(?!hr|heading|lheading|blockquote|fences|list|html|table| +\n)[^\n]+)*)/;
const _paragraph = /^([^\n]+(?:\n(?!hr|heading|lheading|blockquote|fences|list|html|table|[ \t]+\n)[^\n]+)*)/;
const blockText = /^[^\n]+/;
const _blockLabel = /(?!\s*\])(?:\\[\s\S]|[^\[\]\\])+/;
const def = edit(/^ {0,3}\[(label)\]: *(?:\n[ \t]*)?([^<\s][^\s]*|<.*?>)(?:(?: +(?:\n[ \t]*)?| *\n[ \t]*)(title))? *(?:\n+|$)/)
Expand All @@ -149,11 +149,11 @@ const _tag = 'address|article|aside|base|basefont|blockquote|body|caption'
const _comment = /<!--(?:-?>|[\s\S]*?(?:-->|$))/;
const html = edit(
'^ {0,3}(?:' // optional indentation
+ '<(script|pre|style|textarea)[\\s>][\\s\\S]*?(?:</\\1>[^\\n]*\\n+|$)' // (1)
+ '<(script|pre|style|textarea)[\\s>][\\s\\S]*?(?:</\\1>[^\\n]*\\n*|$)' // (1)
+ '|comment[^\\n]*(\\n+|$)' // (2)
+ '|<\\?[\\s\\S]*?(?:\\?>[^\\n]*\\n+|$)' // (3)
+ '|<![A-Z][\\s\\S]*?(?:>[^\\n]*\\n+|$)' // (4)
+ '|<!\\[CDATA\\[[\\s\\S]*?(?:\\]\\]>[^\\n]*\\n+|$)' // (5)
+ '|<\\?[\\s\\S]*?(?:\\?>[^\\n]*\\n*|$)' // (3)
+ '|<![A-Z][\\s\\S]*?(?:>[^\\n]*\\n*|$)' // (4)
+ '|<!\\[CDATA\\[[\\s\\S]*?(?:\\]\\]>[^\\n]*\\n*|$)' // (5)
+ '|</?(tag)(?: +|\\n|/?>)[\\s\\S]*?(?:(?:\\n[ \t]*)+\\n|$)' // (6)
+ '|<(?!script|pre|style|textarea)([a-z][\\w-]*)(?:attribute)*? */?>(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n[ \t]*)+\\n|$)' // (7) open tag
+ '|</(?!script|pre|style|textarea)[a-z][\\w-]*\\s*>(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n[ \t]*)+\\n|$)' // (7) closing tag
Expand All @@ -169,16 +169,18 @@ const createParagraph = (listInterrupt: RegExp) => edit(_paragraph)
.replace('|lheading', '') // setext headings don't interrupt commonmark paragraphs
.replace('|table', '')
.replace('blockquote', ' {0,3}>')
.replace('fences', ' {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n')
.replace('fences', ' {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~~~)[^\\n]*\\n')
.replace('list', listInterrupt)
.replace('html', '</?(?:tag)(?: +|\\n|/?>)|<(?:script|pre|style|textarea|!--)')
.replace('tag', _tag) // pars can be interrupted by type (6) html blocks
.getRegex();

// only non-empty lists starting from 1 can interrupt paragraphs
const paragraph = createParagraph(/ {0,3}(?:[*+-]|1[.)])[ \t]+[^ \t\n]/);
// blockquotes can be interrupted by lists starting from any number
const blockquoteParagraph = createParagraph(/ {0,3}(?:[*+-]|\d{1,9}[.)])[ \t]+[^ \t\n]/);
// inside a blockquote a bare list marker (any number) starts a sibling list,
// so it must not be lazily continued as paragraph text (unlike a top level
// paragraph, where an empty list cannot interrupt)
const blockquoteParagraph = createParagraph(/ {0,3}(?:[*+-]|\d{1,9}[.)])(?:[ \t]|\n|$)/);

const blockquote = edit(/^( {0,3}> ?(paragraph|[^\n]*)(?:\n|$))+/)
.replace('paragraph', blockquoteParagraph)
Expand Down Expand Up @@ -218,7 +220,7 @@ const gfmTable = edit(
.replace('heading', ' {0,3}#{1,6}(?:\\s|$)')
.replace('blockquote', ' {0,3}>')
.replace('code', '(?: {4}| {0,3}\t)[^\\n]')
.replace('fences', ' {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n')
.replace('fences', ' {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~~~)[^\\n]*\\n')
.replace('list', ' {0,3}(?:[*+-]|1[.)])[ \\t]') // any bullet ends the table rows
.replace('html', '</?(?:tag)(?: +|\\n|/?>)|<(?:script|pre|style|textarea|!--)')
.replace('tag', _tag) // tables can be interrupted by type (6) html blocks
Expand All @@ -234,7 +236,7 @@ const blockGfm: Record<BlockKeys, RegExp> = {
.replace('|lheading', '') // setext headings don't interrupt commonmark paragraphs
.replace('table', gfmTable) // interrupt paragraphs with table
.replace('blockquote', ' {0,3}>')
.replace('fences', ' {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n')
.replace('fences', ' {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~~~)[^\\n]*\\n')
.replace('list', ' {0,3}(?:[*+-]|1[.)])[ \\t]+[^ \\t\\n]') // only non-empty lists starting from 1 can interrupt
.replace('html', '</?(?:tag)(?: +|\\n|/?>)|<(?:script|pre|style|textarea|!--)')
.replace('tag', _tag) // pars can be interrupted by type (6) html blocks
Expand Down Expand Up @@ -471,11 +473,11 @@ const inlineGfm: Record<InlineKeys, RegExp> = {
delRDelim,
url: edit(/^((?:protocol):\/\/|www\.)(?:[a-zA-Z0-9\-]+\.?)+[^\s<]*|^email/)
.replace('protocol', _caseInsensitiveProtocol)
.replace('email', /[A-Za-z0-9._+-]+(@)[a-zA-Z0-9-_]+(?:\.[a-zA-Z0-9-_]*[a-zA-Z0-9])+(?![-_])/)
.replace('email', /[A-Za-z0-9._+-]{1,64}(@)[a-zA-Z0-9-_]+(?:\.[a-zA-Z0-9-_]*[a-zA-Z0-9])+(?![-_])/)
.getRegex(),
_backpedal: /(?:[^?!.,:;*_'"~()&]+|\([^)]*\)|&(?![a-zA-Z0-9]+;$)|[?!.,:;*_'"~)]+(?!$))+/,
del: /^(~~?)(?=[^\s~])((?:\\[\s\S]|[^\\])*?(?:\\[\s\S]|[^\s~\\]))\1(?=[^~]|$)/,
text: edit(/^([`~]+|[^`~])(?:(?= {2,}\n)|(?=[a-zA-Z0-9.!#$%&'*+\/=?_`{\|}~-]+@)|[\s\S]*?(?:(?=[\\<!\[`*~_]|\b_|protocol:\/\/|www\.|$)|[^ ](?= {2,}\n)|[^a-zA-Z0-9.!#$%&'*+\/=?_`{\|}~-](?=[a-zA-Z0-9.!#$%&'*+\/=?_`{\|}~-]+@)))/)
text: edit(/^(`+|~+|[^`~])(?:(?=[`~])|(?= {2,}\n)|(?=[a-zA-Z0-9.!#$%&'*+\/=?_`{\|}~-]{1,64}@)|[\s\S]*?(?:(?=[\\<!\[`*~_]|\b_|protocol:\/\/|www\.|$)|[^ ](?= {2,}\n)|[^a-zA-Z0-9.!#$%&'*+\/=?_`{\|}~-](?=[a-zA-Z0-9.!#$%&'*+\/=?_`{\|}~-]{1,64}@)))/)
.replace('protocol', _caseInsensitiveProtocol)
.getRegex(),
};
Expand Down
4 changes: 4 additions & 0 deletions test/specs/new/blank_line_with_tabs.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<p>foo</p>
<p>bar</p>
<p>baz</p>
<p>qux</p>
10 changes: 10 additions & 0 deletions test/specs/new/blank_line_with_tabs.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
gfm: false
---
foo

bar

baz

qux
5 changes: 5 additions & 0 deletions test/specs/new/codespan_adjacent_tilde.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<p>~<code>code</code></p>
<p><code>before</code>~<code>after</code></p>
<p>`<del>strike</del></p>
<p>~<code>code ` tick</code></p>
<p><del><code>deleted code</code></del></p>
9 changes: 9 additions & 0 deletions test/specs/new/codespan_adjacent_tilde.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
~`code`

`before`~`after`

`~strike~

~``code ` tick``

~~`deleted code`~~
13 changes: 13 additions & 0 deletions test/specs/new/empty_list_after_blockquote.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<blockquote>
<p>foo</p>
</blockquote>
<ul>
<li></li>
</ul>
<blockquote>
<p>foo
bar</p>
</blockquote>
<ol>
<li></li>
</ol>
6 changes: 6 additions & 0 deletions test/specs/new/empty_list_after_blockquote.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
> foo
-

> foo
> bar
1.
3 changes: 3 additions & 0 deletions test/specs/new/lheading_hash_prefix.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<h2>#1 Goals</h2>
<h1>#hashtag</h1>
<h1>####### seven</h1>
8 changes: 8 additions & 0 deletions test/specs/new/lheading_hash_prefix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#1 Goals
---------

#hashtag
===

####### seven
===
10 changes: 10 additions & 0 deletions test/specs/redos/quadratic_email_autolink.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
module.exports = [
{
markdown: 'a_'.repeat(50000),
html: `<p>${'a_'.repeat(50000)}</p>`,
},
{
markdown: 'a*'.repeat(50000),
html: `<p>${'a<em>a</em>'.repeat(25000)}</p>`,
},
];
4 changes: 4 additions & 0 deletions test/specs/redos/quadratic_html_block_close.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
module.exports = {
markdown: '<!x ' + 'a>'.repeat(50000),
html: '<!x ' + 'a>'.repeat(50000),
};
14 changes: 14 additions & 0 deletions test/specs/redos/quadratic_inline_masking.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
module.exports = [
{
markdown: '\\.'.repeat(100000),
html: `<p>${'.'.repeat(100000)}</p>`,
},
{
markdown: '`x` '.repeat(60000),
html: `<p>${'<code>x</code> '.repeat(60000)}</p>`,
},
{
markdown: '[a]: x\n\n' + '[a] '.repeat(100000),
html: `<p>${'<a href="x">a</a> '.repeat(100000)}</p>`,
},
];
4 changes: 4 additions & 0 deletions test/specs/redos/quadratic_tilde_paragraph_interrupt.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
module.exports = {
markdown: 'intro\n' + '~'.repeat(50000),
html: `<p>intro\n${'~'.repeat(50000)}</p>\n`,
};