Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(escapeAllSwigTags): backtrack when tag is incomplete #5618

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
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
228 changes: 145 additions & 83 deletions lib/hexo/post.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,112 +80,174 @@ class PostRenderEscape {
let swig_tag_name_end = false;
let swig_tag_name = '';
let swig_full_tag_start_buffer = '';
// current we just consider one level of string quote
let swig_string_quote = '';

const { length } = str;

for (let idx = 0; idx < length; idx++) {
const char = str[idx];
const next_char = str[idx + 1];
let idx = 0;

if (state === STATE_PLAINTEXT) { // From plain text to swig
if (char === '{') {
// check if it is a complete tag {{ }}
if (next_char === '{') {
state = STATE_SWIG_VAR;
idx++;
} else if (next_char === '#') {
state = STATE_SWIG_COMMENT;
idx++;
} else if (next_char === '%') {
state = STATE_SWIG_TAG;
idx++;
swig_tag_name = '';
swig_full_tag_start_buffer = '';
swig_tag_name_begin = false; // Mark if it is the first non white space char in the swig tag
swig_tag_name_end = false;
// for backtracking
const swig_start_idx = {
[STATE_SWIG_VAR]: 0,
[STATE_SWIG_COMMENT]: 0,
[STATE_SWIG_TAG]: 0,
[STATE_SWIG_FULL_TAG]: 0
};

while (idx < length) {
while (idx < length) {
const char = str[idx];
const next_char = str[idx + 1];

if (state === STATE_PLAINTEXT) { // From plain text to swig
if (char === '{') {
// check if it is a complete tag {{ }}
if (next_char === '{') {
state = STATE_SWIG_VAR;
idx++;
swig_start_idx[state] = idx;
} else if (next_char === '#') {
state = STATE_SWIG_COMMENT;
idx++;
swig_start_idx[state] = idx;
} else if (next_char === '%') {
state = STATE_SWIG_TAG;
idx++;
swig_tag_name = '';
swig_full_tag_start_buffer = '';
swig_tag_name_begin = false; // Mark if it is the first non white space char in the swig tag
swig_tag_name_end = false;
swig_start_idx[state] = idx;
} else {
output += char;
}
} else {
output += char;
}
} else {
output += char;
}
} else if (state === STATE_SWIG_TAG) {
if (char === '%' && next_char === '}') { // From swig back to plain text
idx++;
if (swig_tag_name !== '' && str.includes(`end${swig_tag_name}`)) {
state = STATE_SWIG_FULL_TAG;
} else {
} else if (state === STATE_SWIG_TAG) {
if (char === '"' || char === '\'') {
if (swig_string_quote === '') {
swig_string_quote = char;
} else if (swig_string_quote === char) {
swig_string_quote = '';
}
}
// {% } or {% %
if (((char !== '%' && next_char === '}') || (char === '%' && next_char !== '}')) && swig_string_quote === '') {
// From swig back to plain text
swig_tag_name = '';
state = STATE_PLAINTEXT;
output += PostRenderEscape.escapeContent(this.stored, 'swig', `{%${buffer}%}`);
}

buffer = '';
} else {
buffer = buffer + char;
swig_full_tag_start_buffer = swig_full_tag_start_buffer + char;

if (isNonWhiteSpaceChar(char)) {
if (!swig_tag_name_begin && !swig_tag_name_end) {
swig_tag_name_begin = true;
output += `{%${buffer}${char}`;
buffer = '';
} else if (char === '%' && next_char === '}' && swig_string_quote === '') { // From swig back to plain text
idx++;
if (swig_tag_name !== '' && str.includes(`end${swig_tag_name}`)) {
state = STATE_SWIG_FULL_TAG;
swig_start_idx[state] = idx;
} else {
swig_tag_name = '';
state = STATE_PLAINTEXT;
output += PostRenderEscape.escapeContent(this.stored, 'swig', `{%${buffer}%}`);
}

if (swig_tag_name_begin) {
swig_tag_name += char;
}
buffer = '';
} else {
if (swig_tag_name_begin === true) {
swig_tag_name_begin = false;
swig_tag_name_end = true;
buffer = buffer + char;
swig_full_tag_start_buffer = swig_full_tag_start_buffer + char;

if (isNonWhiteSpaceChar(char)) {
if (!swig_tag_name_begin && !swig_tag_name_end) {
swig_tag_name_begin = true;
}

if (swig_tag_name_begin) {
swig_tag_name += char;
}
} else {
if (swig_tag_name_begin === true) {
swig_tag_name_begin = false;
swig_tag_name_end = true;
}
}
}
}
} else if (state === STATE_SWIG_VAR) {
if (char === '}' && next_char === '}') {
idx++;
state = STATE_PLAINTEXT;
output += PostRenderEscape.escapeContent(this.stored, 'swig', `{{${buffer}}}`);
buffer = '';
} else {
buffer = buffer + char;
}
} else if (state === STATE_SWIG_COMMENT) { // From swig back to plain text
if (char === '#' && next_char === '}') {
idx++;
state = STATE_PLAINTEXT;
buffer = '';
}
} else if (state === STATE_SWIG_FULL_TAG) {
if (char === '{' && next_char === '%') {
let swig_full_tag_end_buffer = '';

let _idx = idx + 2;
for (; _idx < length; _idx++) {
const _char = str[_idx];
const _next_char = str[_idx + 1];

if (_char === '%' && _next_char === '}') {
_idx++;
break;
} else if (state === STATE_SWIG_VAR) {
if (char === '"' || char === '\'') {
if (swig_string_quote === '') {
swig_string_quote = char;
} else if (swig_string_quote === char) {
swig_string_quote = '';
}

swig_full_tag_end_buffer = swig_full_tag_end_buffer + _char;
}

if (swig_full_tag_end_buffer.includes(`end${swig_tag_name}`)) {
// {{ }
if (char === '}' && next_char !== '}' && swig_string_quote === '') {
// From swig back to plain text
state = STATE_PLAINTEXT;
output += `{{${buffer}${char}`;
buffer = '';
} else if (char === '}' && next_char === '}' && swig_string_quote === '') {
idx++;
state = STATE_PLAINTEXT;
output += PostRenderEscape.escapeContent(this.stored, 'swig', `{{${buffer}}}`);
buffer = '';
} else {
buffer = buffer + char;
}
} else if (state === STATE_SWIG_COMMENT) { // From swig back to plain text
if (char === '#' && next_char === '}') {
idx++;
state = STATE_PLAINTEXT;
output += PostRenderEscape.escapeContent(this.stored, 'swig', `{%${swig_full_tag_start_buffer}%}${buffer}{%${swig_full_tag_end_buffer}%}`);
idx = _idx;
swig_full_tag_start_buffer = '';
swig_full_tag_end_buffer = '';
buffer = '';
}
} else if (state === STATE_SWIG_FULL_TAG) {
if (char === '{' && next_char === '%') {
let swig_full_tag_end_buffer = '';
let swig_full_tag_found = false;

let _idx = idx + 2;
for (; _idx < length; _idx++) {
const _char = str[_idx];
const _next_char = str[_idx + 1];

if (_char === '%' && _next_char === '}') {
_idx++;
swig_full_tag_found = true;
break;
}

swig_full_tag_end_buffer = swig_full_tag_end_buffer + _char;
}

if (swig_full_tag_found && swig_full_tag_end_buffer.includes(`end${swig_tag_name}`)) {
state = STATE_PLAINTEXT;
output += PostRenderEscape.escapeContent(this.stored, 'swig', `{%${swig_full_tag_start_buffer}%}${buffer}{%${swig_full_tag_end_buffer}%}`);
idx = _idx;
swig_full_tag_start_buffer = '';
swig_full_tag_end_buffer = '';
buffer = '';
} else {
buffer += char;
}
} else {
buffer += char;
}
} else {
buffer += char;
}
idx++;
}
if (state === STATE_PLAINTEXT) {
break;
}
// If the swig tag is not closed, then it is a plain text, we need to backtrack
idx = swig_start_idx[state];
buffer = '';
swig_string_quote = '';
if (state === STATE_SWIG_FULL_TAG) {
output += `{%${swig_full_tag_start_buffer}%`;
} else {
output += '{';
}
swig_full_tag_start_buffer = '';
state = STATE_PLAINTEXT;
}

return output;
Expand Down
92 changes: 92 additions & 0 deletions test/scripts/hexo/post.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1423,6 +1423,98 @@ describe('Post', () => {
data.content.should.not.contains('&#96;'); // `
});

it('render() - should support quotes in tags', async () => {
let content = '{{ "{{ }" }}';
let data = await post.render('', {
content,
engine: 'markdown'
});
data.content.should.eql('{{ }');

content = '{% blockquote "{% }" %}test{% endblockquote %}';
data = await post.render('', {
content,
engine: 'markdown'
});
data.content.should.eql('<blockquote><p>test</p>\n<footer><strong>{% }</strong></footer></blockquote>');
});

it('render() - dont escape incomplete tags with complete tags', async () => {
// lost one character
let content = '{{ 1 }} \n `{% "%}" }` 22222';
let data = await post.render('', {
content,
engine: 'markdown'
});
data.content.should.contains('&#123;% &quot;%&#125;&quot; &#125;');
data.content.should.contains('1');
data.content.should.contains('22222');

content = '{{ 1 }} \n `{% "%}" %` 22222';
data = await post.render('', {
content,
engine: 'markdown'
});
data.content.should.contains('&#123;% &quot;%&#125;&quot; %');
data.content.should.contains('1');
data.content.should.contains('22222');

content = '{{ 1 }} \n `{# }` 22222';
data = await post.render('', {
content,
engine: 'markdown'
});
data.content.should.contains('&#123;# &#125;');
data.content.should.contains('1');
data.content.should.contains('22222');

content = '{{ 1 }} \n `{{ "}}" }` 22222';
data = await post.render('', {
content,
engine: 'markdown'
});
data.content.should.contains('&#123;&#123; &quot;&#125;&#125;&quot; &#125;');
data.content.should.contains('1');
data.content.should.contains('22222');

content = '{{ 1 }} \n `{{ %}` 22222';
data = await post.render('', {
content,
engine: 'markdown'
});
data.content.should.contains('&#123;&#123; %&#125;');
data.content.should.contains('1');
data.content.should.contains('22222');

// lost two characters
content = '{{ 1 }} \n `{#` \n 22222';
data = await post.render('', {
content,
engine: 'markdown'
});
data.content.should.contains('&#123;#');
data.content.should.contains('1');
data.content.should.contains('22222');

content = '{{ 1 }} \n `{%` \n 22222';
data = await post.render('', {
content,
engine: 'markdown'
});
data.content.should.contains('&#123;%');
data.content.should.contains('1');
data.content.should.contains('22222');

content = '{{ 1 }} \n `{{ ` 22222';
data = await post.render('', {
content,
engine: 'markdown'
});
data.content.should.contains('1');
data.content.should.contains('&#123;&#123; ');
data.content.should.contains('22222');
});

it('render() - incomplete tags throw error', async () => {
const content = 'nunjucks should throw {# } error';

Expand Down
Loading