diff --git a/lib/hexo/post.ts b/lib/hexo/post.ts index 2efeae4dca..52c8efd35e 100644 --- a/lib/hexo/post.ts +++ b/lib/hexo/post.ts @@ -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; diff --git a/test/scripts/hexo/post.ts b/test/scripts/hexo/post.ts index 79a4328ce7..f2987f6f12 100644 --- a/test/scripts/hexo/post.ts +++ b/test/scripts/hexo/post.ts @@ -1423,6 +1423,98 @@ describe('Post', () => { data.content.should.not.contains('`'); // ` }); + 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('

test

\n
'); + }); + + 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('{% "%}" }'); + 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('{% "%}" %'); + 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('{# }'); + 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('{{ "}}" }'); + 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('{{ %}'); + 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('{#'); + 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('{%'); + 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('{{ '); + data.content.should.contains('22222'); + }); + it('render() - incomplete tags throw error', async () => { const content = 'nunjucks should throw {# } error';