Skip to content

Commit b28edcf

Browse files
authored
fix(regression): handle comment nesting in code fence and nunjucks (#5717)
* fix(regression): handle comment nesting in code fence and nunjucks * fix: reset regex lastIndex before escaping comments in backtick code block
1 parent 69f358b commit b28edcf

File tree

3 files changed

+155
-39
lines changed

3 files changed

+155
-39
lines changed

lib/hexo/post.ts

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ const STATE_SWIG_VAR = 1;
2525
const STATE_SWIG_COMMENT = 2;
2626
const STATE_SWIG_TAG = 3;
2727
const STATE_SWIG_FULL_TAG = 4;
28+
const STATE_PLAINTEXT_COMMENT = 5;
2829

2930
const isNonWhiteSpaceChar = (char: string) => char !== '\r'
3031
&& char !== '\n'
@@ -82,6 +83,7 @@ class PostRenderEscape {
8283
escapeAllSwigTags(str: string) {
8384
let state = STATE_PLAINTEXT;
8485
let buffer_start = -1;
86+
let plaintext_comment_start = -1;
8587
let plain_text_start = 0;
8688
let output = '';
8789

@@ -153,6 +155,12 @@ class PostRenderEscape {
153155
swig_start_idx[state] = idx;
154156
}
155157
}
158+
if (char === '<' && next_char === '!' && str[idx + 2] === '-' && str[idx + 3] === '-') {
159+
flushPlainText(idx);
160+
state = STATE_PLAINTEXT_COMMENT;
161+
plaintext_comment_start = idx;
162+
idx += 3;
163+
}
156164
} else if (state === STATE_SWIG_TAG) {
157165
if (char === '"' || char === '\'') {
158166
if (swig_string_quote === '') {
@@ -242,12 +250,25 @@ class PostRenderEscape {
242250
swig_full_tag_end_buffer = '';
243251
}
244252
}
253+
} else if (state === STATE_PLAINTEXT_COMMENT) {
254+
if (char === '-' && next_char === '-' && str[idx + 2] === '>') {
255+
state = STATE_PLAINTEXT;
256+
const comment = str.slice(plaintext_comment_start, idx + 3);
257+
pushAndReset(PostRenderEscape.escapeContent(this.stored, 'comment', comment));
258+
idx += 2;
259+
}
245260
}
246261
idx++;
247262
}
248263
if (state === STATE_PLAINTEXT) {
249264
break;
250265
}
266+
if (state === STATE_PLAINTEXT_COMMENT) {
267+
// Unterminated comment, just push the rest as comment
268+
const comment = str.slice(plaintext_comment_start, length);
269+
pushAndReset(PostRenderEscape.escapeContent(this.stored, 'comment', comment));
270+
break;
271+
}
251272
// If the swig tag is not closed, then it is a plain text, we need to backtrack
252273
if (state === STATE_SWIG_FULL_TAG) {
253274
pushAndReset(`{%${str.slice(swig_full_tag_start_start, swig_full_tag_start_end)}%`);
@@ -513,7 +534,6 @@ class Post {
513534
return ctx.execFilter('before_post_render', data, { context: ctx });
514535
}).then(() => {
515536
// Escape all comments to avoid conflict with Nunjucks and code block
516-
data.content = cacheObj.escapeComments(data.content);
517537
data.content = cacheObj.escapeCodeBlocks(data.content);
518538
// Escape all Nunjucks/Swig tags
519539
let hasSwigTag = true;
@@ -546,8 +566,8 @@ class Post {
546566
}
547567
}, options);
548568
}).then(content => {
549-
data.content = cacheObj.restoreCodeBlocks(content);
550-
data.content = cacheObj.restoreComments(data.content);
569+
data.content = cacheObj.restoreComments(content);
570+
data.content = cacheObj.restoreCodeBlocks(data.content);
551571

552572
// Run "after_post_render" filters
553573
return ctx.execFilter('after_post_render', data, { context: ctx });

lib/plugins/filter/before_post_render/backtick_code_block.ts

Lines changed: 23 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
11
import type { HighlightOptions } from '../../../extend/syntax_highlight';
22
import type Hexo from '../../../hexo';
33
import type { RenderData } from '../../../types';
4-
import assert from 'assert';
54

65
const rBacktick = /^((?:(?:[^\S\r\n]*>){0,3}|[-*+]|[0-9]+\.)[^\S\r\n]*)(`{3,}|~{3,})[^\S\r\n]*((?:.*?[^`\s])?)[^\S\r\n]*\n((?:[\s\S]*?\n)?)(?:(?:[^\S\r\n]*>){0,3}[^\S\r\n]*)\2[^\S\r\n]?(\n+|$)/gm;
76
const rAllOptions = /([^\s]+)\s+(.+?)\s+(https?:\/\/\S+|\/\S+)\s*(.+)?/;
87
const rLangCaption = /([^\s]+)\s*(.+)?/;
98
const rCommentEscape = /(<!--[\s\S]*?-->)/g;
10-
const rCommentHolder = /(?:<|&lt;)!--comment\uFFFC(\d+)--(?:>|&gt;)/g;
119
const rAdditionalOptions = /\s((?:line_number|line_threshold|first_line|wrap|mark|language_attr|highlight):\S+)/g;
1210

1311
const escapeSwigTag = (str: string) => str.replace(/{/g, '&#123;').replace(/}/g, '&#125;');
@@ -83,44 +81,34 @@ function parseArgs(args: string) {
8381
};
8482
}
8583

86-
class CodeBlockEscape {
87-
public stored: string[];
88-
89-
constructor() {
90-
this.stored = [];
91-
}
92-
93-
static escapeContent(cache: string[], flag: string, str: string) {
94-
return `<!--${flag}\uFFFC${cache.push(str) - 1}-->`;
95-
}
96-
97-
static restoreContent(cache: string[]) {
98-
return (_: string, index: number) => {
99-
assert(cache[index]);
100-
const value = cache[index];
101-
cache[index] = null;
102-
return value;
103-
};
104-
}
105-
106-
restoreComments(str: string) {
107-
return str.replace(rCommentHolder, CodeBlockEscape.restoreContent(this.stored));
108-
}
109-
110-
escapeComments(str: string) {
111-
return str.replace(rCommentEscape, (_, content) => CodeBlockEscape.escapeContent(this.stored, 'comment', content));
112-
}
113-
114-
}
115-
11684
export = (ctx: Hexo): (data: RenderData) => void => {
11785
return function backtickCodeBlock(data: RenderData): void {
11886
const dataContent = data.content;
11987

12088
if ((!dataContent.includes('```') && !dataContent.includes('~~~')) || !ctx.extend.highlight.query(ctx.config.syntax_highlighter)) return;
121-
const cacheObj = new CodeBlockEscape();
122-
data.content = cacheObj.escapeComments(data.content);
123-
data.content = data.content.replace(rBacktick, ($0, start, $2, _args, _content, end) => {
89+
// get all comment starts and ends
90+
const commentStarts = [];
91+
const commentEnds = [];
92+
let match: RegExpExecArray | null;
93+
rCommentEscape.lastIndex = 0;
94+
while ((match = rCommentEscape.exec(dataContent)) !== null) {
95+
commentStarts.push(match.index);
96+
commentEnds.push(match.index + match[0].length);
97+
}
98+
// notice that commentStarts and commentEnds are sorted, and commentStarts[i] < commentEnds[i], commentEnds[i] <= commentStarts[i+1]
99+
let commentIndex = 0;
100+
data.content = data.content.replace(rBacktick, ($0, start, $2, _args, _content, end, matchIndex) => {
101+
// get the start and end of the code block
102+
const codeBlockStart = matchIndex;
103+
const codeBlockEnd = matchIndex + $0.length;
104+
// check if the code block is nested in a comment
105+
while (commentIndex < commentStarts.length && commentEnds[commentIndex] <= codeBlockStart) {
106+
commentIndex++;
107+
}
108+
if (commentIndex < commentStarts.length && commentStarts[commentIndex] < codeBlockStart && commentEnds[commentIndex] > codeBlockEnd) {
109+
// the code block is nested in a comment, return escaped content directly
110+
return escapeSwigTag($0);
111+
}
124112
let content = _content.replace(/\n$/, '');
125113

126114
// neither highlight or prismjs is enabled, return escaped content directly.
@@ -182,6 +170,5 @@ export = (ctx: Hexo): (data: RenderData) => void => {
182170
+ '</hexoPostRenderCodeBlock>'
183171
+ end;
184172
});
185-
data.content = cacheObj.restoreComments(data.content);
186173
};
187174
};

test/scripts/hexo/post.ts

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1631,6 +1631,89 @@ describe('Post', () => {
16311631
].join('\n'));
16321632
});
16331633

1634+
it('render() - incomplete comments', async () => {
1635+
const content = [
1636+
'foo',
1637+
'<!--',
1638+
'test',
1639+
'{% raw %}',
1640+
'bar',
1641+
'{% endraw %}'
1642+
].join('\n');
1643+
1644+
const data = await post.render('', {
1645+
content,
1646+
engine: 'markdown'
1647+
});
1648+
1649+
data.content.should.eql([
1650+
'<p>foo</p>',
1651+
'<!--',
1652+
'test',
1653+
'{% raw %}',
1654+
'bar',
1655+
'{% endraw %}'
1656+
].join('\n'));
1657+
});
1658+
1659+
// https://github.com/hexojs/hexo/issues/5716
1660+
it('render() - comments nesting in nunjucks', async () => {
1661+
const tagSpy = spy();
1662+
hexo.extend.tag.register('testTag', (args, content) => {
1663+
tagSpy(args, content);
1664+
return '';
1665+
}, {
1666+
ends: true
1667+
});
1668+
let content = [
1669+
'{% testTag %}',
1670+
'foo',
1671+
'<!--',
1672+
'test',
1673+
'-->',
1674+
'bar',
1675+
'{% endtestTag %}'
1676+
].join('\n');
1677+
1678+
let data = await post.render('', {
1679+
content,
1680+
engine: 'markdown'
1681+
});
1682+
1683+
data.content.should.eql('');
1684+
tagSpy.calledOnce.should.be.true;
1685+
tagSpy.firstCall.args[1].should.eql([
1686+
'foo',
1687+
'<!--',
1688+
'test',
1689+
'-->',
1690+
'bar'
1691+
].join('\n'));
1692+
1693+
content = [
1694+
'{% testTag %}',
1695+
'foo',
1696+
'<!-- test -->',
1697+
'bar',
1698+
'{% endtestTag %}'
1699+
].join('\n');
1700+
1701+
data = await post.render('', {
1702+
content,
1703+
engine: 'markdown'
1704+
});
1705+
1706+
data.content.should.eql('');
1707+
tagSpy.calledTwice.should.be.true;
1708+
tagSpy.secondCall.args[1].should.eql([
1709+
'foo',
1710+
'<!-- test -->',
1711+
'bar'
1712+
].join('\n'));
1713+
1714+
hexo.extend.tag.unregister('testTag');
1715+
});
1716+
16341717
// https://github.com/hexojs/hexo/issues/5433
16351718
it('render() - code fence nesting in comments', async () => {
16361719
const code = 'alert("Hello world")';
@@ -1660,4 +1743,30 @@ describe('Post', () => {
16601743
''
16611744
].join('\n'));
16621745
});
1746+
1747+
// https://github.com/hexojs/hexo/issues/5715
1748+
it('render() - comment nesting in code fence', async () => {
1749+
const code = 'alert("Hello world")';
1750+
const content = [
1751+
'foo',
1752+
'```',
1753+
'<!--',
1754+
code,
1755+
'-->',
1756+
'```',
1757+
'bar'
1758+
].join('\n');
1759+
1760+
const data = await post.render('', {
1761+
content,
1762+
engine: 'markdown'
1763+
});
1764+
1765+
data.content.should.eql([
1766+
'<p>foo</p>',
1767+
'<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">&lt;!--</span><br><span class="line">alert(&quot;Hello world&quot;)</span><br><span class="line">--&gt;</span><br></pre></td></tr></table></figure>',
1768+
'<p>bar</p>',
1769+
''
1770+
].join('\n'));
1771+
});
16631772
});

0 commit comments

Comments
 (0)