Skip to content

Commit

Permalink
fix(escapeAllSwigTags): backtrack when tag is incomplete
Browse files Browse the repository at this point in the history
  • Loading branch information
D-Sketon committed Jan 18, 2025
1 parent bcfb030 commit 79bcc98
Show file tree
Hide file tree
Showing 2 changed files with 237 additions and 83 deletions.
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

0 comments on commit 79bcc98

Please sign in to comment.