Skip to content

Commit 48c764a

Browse files
D-SketonCopilot
andauthored
fix: Incorrectly ignores single-line code blocks when escaping Swig and HTML comments (#5722)
* fix: Incorrectly ignores single-line code blocks when escaping Swig and HTML comments * fix: raw tag * add more test * add more test * Update test/scripts/hexo/post.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: D-Sketon <2055272094@qq.com> --------- Signed-off-by: D-Sketon <2055272094@qq.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent bc395f7 commit 48c764a

File tree

2 files changed

+721
-16
lines changed

2 files changed

+721
-16
lines changed

lib/hexo/post.ts

Lines changed: 65 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import type { NodeJSLikeCallback, RenderData } from '../types';
1313
const preservedKeys = ['title', 'slug', 'path', 'layout', 'date', 'content'];
1414

1515
const rHexoPostRenderEscape = /<hexoPostRenderCodeBlock>([\s\S]+?)<\/hexoPostRenderCodeBlock>/g;
16-
const rCommentEscape = /(<!--[\s\S]*?-->)/g;
1716
const rSwigTag = /(\{\{.+?\}\})|(\{#.+?#\})|(\{%.+?%\})/s;
1817

1918
const rSwigPlaceHolder = /(?:<|&lt;)!--swig\uFFFC(\d+)--(?:>|&gt;)/g;
@@ -26,6 +25,7 @@ const STATE_SWIG_COMMENT = 2;
2625
const STATE_SWIG_TAG = 3;
2726
const STATE_SWIG_FULL_TAG = 4;
2827
const STATE_PLAINTEXT_COMMENT = 5;
28+
const STATE_INLINE_CODE = 6;
2929

3030
const isNonWhiteSpaceChar = (char: string) => char !== '\r'
3131
&& char !== '\n'
@@ -68,22 +68,15 @@ class PostRenderEscape {
6868
return str.replace(rCommentHolder, PostRenderEscape.restoreContent(this.stored));
6969
}
7070

71-
escapeComments(str: string) {
72-
return str.replace(rCommentEscape, (_, content) => PostRenderEscape.escapeContent(this.stored, 'comment', content));
73-
}
74-
7571
escapeCodeBlocks(str: string) {
7672
return str.replace(rHexoPostRenderEscape, (_, content) => PostRenderEscape.escapeContent(this.stored, 'code', content));
7773
}
7874

79-
/**
80-
* @param {string} str
81-
* @returns string
82-
*/
8375
escapeAllSwigTags(str: string) {
8476
let state = STATE_PLAINTEXT;
8577
let buffer_start = -1;
8678
let plaintext_comment_start = -1;
79+
let inline_code_backtick_count = 0;
8780
let plain_text_start = 0;
8881
let output = '';
8982

@@ -101,7 +94,7 @@ class PostRenderEscape {
10194
let idx = 0;
10295

10396
// for backtracking
104-
const swig_start_idx = [0, 0, 0, 0, 0];
97+
const swig_start_idx = [0, 0, 0, 0, 0, 0, 0];
10598

10699
const flushPlainText = (end: number) => {
107100
if (plain_text_start !== -1 && end > plain_text_start) {
@@ -124,11 +117,25 @@ class PostRenderEscape {
124117
while (idx < length) {
125118
while (idx < length) {
126119
const char = str[idx];
120+
const prev_char = idx > 0 ? str[idx - 1] : '';
127121
const next_char = str[idx + 1];
128122

129-
if (state === STATE_PLAINTEXT) { // From plain text to swig
123+
if (state === STATE_PLAINTEXT) { // From plain text to swig or inline code
130124
ensurePlainTextStart(idx);
131-
if (char === '{') {
125+
// Check for inline code block (backticks)
126+
if (char === '`' && prev_char !== '\\') {
127+
// Count consecutive backticks
128+
let backtick_count = 1;
129+
while (str[idx + backtick_count] === '`') {
130+
backtick_count++;
131+
}
132+
133+
flushPlainText(idx);
134+
state = STATE_INLINE_CODE;
135+
inline_code_backtick_count = backtick_count;
136+
swig_start_idx[state] = idx;
137+
idx += backtick_count - 1; // Skip the counted backticks
138+
} else if (char === '{') {
132139
// check if it is a complete tag {{ }}
133140
if (next_char === '{') {
134141
flushPlainText(idx);
@@ -154,13 +161,49 @@ class PostRenderEscape {
154161
swig_tag_name_end = false;
155162
swig_start_idx[state] = idx;
156163
}
157-
}
158-
if (char === '<' && next_char === '!' && str[idx + 2] === '-' && str[idx + 3] === '-') {
164+
} else if (char === '<' && next_char === '!' && str[idx + 2] === '-' && str[idx + 3] === '-') {
159165
flushPlainText(idx);
160166
state = STATE_PLAINTEXT_COMMENT;
161167
plaintext_comment_start = idx;
162168
idx += 3;
163169
}
170+
} else if (state === STATE_INLINE_CODE) {
171+
const inline_code_start = swig_start_idx[state];
172+
// Check for newline - inline code cannot span multiple lines
173+
if ((char === '\n' && next_char === '\n')
174+
|| (char === '\r' && next_char === '\n' && str[idx + 2] === '\r' && str[idx + 3] === '\n')
175+
|| (char === '\r' && next_char === '\n' && str[idx + 2] === '\n')
176+
|| (char === '\n' && next_char === '\r' && str[idx + 2] === '\n')
177+
) {
178+
// Backtrack: treat the opening backticks as plain text and retry from after them
179+
pushAndReset(str.slice(inline_code_start, inline_code_start + inline_code_backtick_count));
180+
// Reset idx to position right after the opening backticks
181+
idx = inline_code_start + inline_code_backtick_count - 1;
182+
state = STATE_PLAINTEXT;
183+
} else if (char === '{' && next_char === '%' && str.slice(idx).match(/^\{% *raw *%\}/)) {
184+
// we may have raw tag in inline code
185+
const raw_tag_end_match = str.slice(idx).match(/\{% *endraw *%\}/);
186+
if (raw_tag_end_match) {
187+
pushAndReset(str.slice(inline_code_start, idx));
188+
// escape the raw tag content
189+
pushAndReset(PostRenderEscape.escapeContent(this.stored, 'swig', str.slice(idx, idx + raw_tag_end_match.index! + raw_tag_end_match[0].length)));
190+
idx = idx + raw_tag_end_match.index! + raw_tag_end_match[0].length - 1;
191+
swig_start_idx[state] = idx + 1;
192+
}
193+
} else if (char === '`') {
194+
// Count consecutive backticks
195+
let backtick_count = 1;
196+
while (str[idx + backtick_count] === '`') {
197+
backtick_count++;
198+
}
199+
200+
// If the count matches, we found the closing backticks
201+
if (backtick_count === inline_code_backtick_count) {
202+
pushAndReset(str.slice(inline_code_start, idx + backtick_count));
203+
idx += backtick_count - 1; // Skip the counted backticks
204+
state = STATE_PLAINTEXT;
205+
}
206+
}
164207
} else if (state === STATE_SWIG_TAG) {
165208
if (char === '"' || char === '\'') {
166209
if (swig_string_quote === '') {
@@ -269,6 +312,14 @@ class PostRenderEscape {
269312
pushAndReset(PostRenderEscape.escapeContent(this.stored, 'comment', comment));
270313
break;
271314
}
315+
if (state === STATE_INLINE_CODE) {
316+
const inline_code_start = swig_start_idx[state];
317+
pushAndReset(str.slice(inline_code_start, inline_code_start + inline_code_backtick_count));
318+
// Reset idx to position right after the opening backticks
319+
idx = inline_code_start + inline_code_backtick_count;
320+
state = STATE_PLAINTEXT;
321+
continue;
322+
}
272323
// If the swig tag is not closed, then it is a plain text, we need to backtrack
273324
if (state === STATE_SWIG_FULL_TAG) {
274325
pushAndReset(`{%${str.slice(swig_full_tag_start_start, swig_full_tag_start_end)}%`);

0 commit comments

Comments
 (0)