diff --git a/lib/hexo/post.ts b/lib/hexo/post.ts index 9418039e4..3d1497970 100644 --- a/lib/hexo/post.ts +++ b/lib/hexo/post.ts @@ -16,6 +16,7 @@ const rHexoPostRenderEscape = /([\s\S]+?)<\/hexoPostRen const rSwigTag = /(\{\{.+?\}\})|(\{#.+?#\})|(\{%.+?%\})/s; const rSwigPlaceHolder = /(?:<|<)!--swig\uFFFC(\d+)--(?:>|>)/g; +const rInlineSwigPlaceHolder = /(?:\uFFFC|&#(?:xFFFC|xfffc|65532);)swig(\d+)(?:\uFFFC|&#(?:xFFFC|xfffc|65532);)/g; const rCodeBlockPlaceHolder = /(?:<|<)!--code\uFFFC(\d+)--(?:>|>)/g; const rCommentHolder = /(?:<|<)!--comment\uFFFC(\d+)--(?:>|>)/g; @@ -42,8 +43,9 @@ class PostRenderEscape { this.stored = []; } - static escapeContent(cache: string[], flag: string, str: string) { - return ``; + static escapeContent(cache: string[], flag: string, str: string, inline = false) { + const idx = cache.push(str) - 1; + return inline ? `\uFFFC${flag}${idx}\uFFFC` : ``; } static restoreContent(cache: string[]) { @@ -56,8 +58,9 @@ class PostRenderEscape { } restoreAllSwigTags(str: string) { - const restored = str.replace(rSwigPlaceHolder, PostRenderEscape.restoreContent(this.stored)); - return restored; + str = str.replace(rSwigPlaceHolder, PostRenderEscape.restoreContent(this.stored)); + str = str.replace(rInlineSwigPlaceHolder, PostRenderEscape.restoreContent(this.stored)); + return str; } restoreCodeBlocks(str: string) { @@ -114,6 +117,16 @@ class PostRenderEscape { plain_text_start = -1; }; + // Inline placeholder avoids triggering CommonMark HTML block (type 2), + // which would prevent Markdown processing on the rest of the line. + const hasTrailingContentOnLine = (startIdx: number): boolean => { + for (let j = startIdx; j < length; j++) { + if (str[j] === '\n' || str[j] === '\r') return false; + if (isNonWhiteSpaceChar(str[j])) return true; + } + return false; + }; + while (idx < length) { while (idx < length) { const char = str[idx]; @@ -224,7 +237,8 @@ class PostRenderEscape { swig_tag_name = ''; state = STATE_PLAINTEXT; // since we have already move idx to next char of '}', so here is idx -1 - pushAndReset(PostRenderEscape.escapeContent(this.stored, 'swig', `{%${str.slice(buffer_start, idx - 1)}%}`)); + const swigTagStr = `{%${str.slice(buffer_start, idx - 1)}%}`; + pushAndReset(PostRenderEscape.escapeContent(this.stored, 'swig', swigTagStr, hasTrailingContentOnLine(idx + 1))); } } else { @@ -257,9 +271,10 @@ class PostRenderEscape { state = STATE_PLAINTEXT; pushAndReset(`{{${str.slice(buffer_start, idx)}${char}`); } else if (char === '}' && next_char === '}' && swig_string_quote === '') { - pushAndReset(PostRenderEscape.escapeContent(this.stored, 'swig', `{{${str.slice(buffer_start, idx)}}}`)); + const swigVarStr = `{{${str.slice(buffer_start, idx)}}}`; idx++; state = STATE_PLAINTEXT; + pushAndReset(PostRenderEscape.escapeContent(this.stored, 'swig', swigVarStr, hasTrailingContentOnLine(idx + 1))); } } else if (state === STATE_SWIG_COMMENT) { // From swig back to plain text if (char === '#' && next_char === '}') { @@ -288,7 +303,8 @@ class PostRenderEscape { if (swig_full_tag_found && swig_full_tag_end_buffer.includes(`end${swig_tag_name}`)) { state = STATE_PLAINTEXT; - pushAndReset(PostRenderEscape.escapeContent(this.stored, 'swig', `{%${str.slice(swig_full_tag_start_start, swig_full_tag_start_end)}%}${str.slice(buffer_start, idx)}{%${swig_full_tag_end_buffer}%}`)); + const swigFullTagStr = `{%${str.slice(swig_full_tag_start_start, swig_full_tag_start_end)}%}${str.slice(buffer_start, idx)}{%${swig_full_tag_end_buffer}%}`; + pushAndReset(PostRenderEscape.escapeContent(this.stored, 'swig', swigFullTagStr, hasTrailingContentOnLine(_idx + 1))); idx = _idx; swig_full_tag_end_buffer = ''; } diff --git a/test/scripts/hexo/post.ts b/test/scripts/hexo/post.ts index 3e43ad01e..eaab998dd 100644 --- a/test/scripts/hexo/post.ts +++ b/test/scripts/hexo/post.ts @@ -2423,4 +2423,148 @@ describe('Post', () => { data.content.trim().should.include('c'); }); }); + + describe('inline swig placeholder', () => { + it('render() - markdown link on same line as swig tag at beginning of line', async () => { + hexo.extend.tag.register('inlineTestTag', () => 'test'); + + const content = '{% inlineTestTag %} text with [link](https://example.com) more'; + const data = await post.render('', { + content, + engine: 'markdown' + }); + + data.content.trim().should.include('>link'); + data.content.trim().should.include('example.com'); + data.content.trim().should.include('test'); + + hexo.extend.tag.unregister('inlineTestTag'); + }); + + it('render() - swig tag alone on line should not be wrapped in

', async () => { + hexo.extend.tag.register('blockTestTag', () => '

block content
'); + + const content = '{% blockTestTag %}\n\nparagraph text'; + const data = await post.render('', { + content, + engine: 'markdown' + }); + + data.content.trim().should.include('
block content
'); + data.content.trim().should.include('

paragraph text

'); + data.content.should.not.include('

'); + + hexo.extend.tag.unregister('blockTestTag'); + }); + + it('render() - multiple swig tags on same line with markdown', async () => { + hexo.extend.tag.register('multiTestTag', args => `${args[0]}`); + + const content = '{% multiTestTag a %} [link](https://example.com) {% multiTestTag b %}'; + const data = await post.render('', { + content, + engine: 'markdown' + }); + + data.content.trim().should.include('>link'); + data.content.trim().should.include('example.com'); + data.content.trim().should.include('a'); + data.content.trim().should.include('b'); + + hexo.extend.tag.unregister('multiTestTag'); + }); + + it('render() - full block tag should still work as block', async () => { + const content = [ + '{% blockquote %}', + 'quote text', + '{% endblockquote %}', + '', + 'paragraph text' + ].join('\n'); + + const data = await post.render('', { + content, + engine: 'markdown' + }); + + data.content.trim().should.include('
'); + data.content.trim().should.include('

paragraph text

'); + }); + + it('render() - markdown link on same line as block tag closing', async () => { + const content = '{% blockquote %}quote{% endblockquote %} [link](https://example.com)'; + const data = await post.render('', { + content, + engine: 'markdown' + }); + + data.content.trim().should.include('
'); + data.content.trim().should.include('>link'); + }); + + it('render() - variable tag with trailing markdown link should use inline format', async () => { + const content = '{{ "var-output" }} text [link](https://example.com) after'; + + const data = await post.render('', { + content, + engine: 'markdown' + }); + + data.content.trim().should.include('>link'); + data.content.trim().should.include('example.com'); + data.content.trim().should.include('var-output'); + }); + + it('render() - variable tag alone on line should use block format', async () => { + const content = '{{ "block-var" }}\n\nparagraph text'; + + const data = await post.render('', { + content, + engine: 'markdown' + }); + + data.content.trim().should.include('block-var'); + data.content.trim().should.include('

paragraph text

'); + data.content.should.not.include('

block-var

'); + }); + + it('render() - swig tag with trailing whitespace only should use block format', async () => { + hexo.extend.tag.register('wsTestTag', () => '
ws content
'); + + const content = '{% wsTestTag %} \n\nparagraph text'; + + const data = await post.render('', { + content, + engine: 'markdown' + }); + + data.content.trim().should.include('
ws content
'); + data.content.trim().should.include('

paragraph text

'); + data.content.should.not.include('

'); + + hexo.extend.tag.unregister('wsTestTag'); + }); + + it('render() - block format on first line does not prevent inline format on next line', async () => { + hexo.extend.tag.register('blockLineTag', () => '
block
'); + hexo.extend.tag.register('inlineLineTag', () => 'inline'); + + const content = '{% blockLineTag %}\n{% inlineLineTag %} [link](https://example.com)'; + + const data = await post.render('', { + content, + engine: 'markdown' + }); + + data.content.trim().should.include('
block
'); + data.content.trim().should.include('inline'); + data.content.trim().should.include('>link'); + data.content.should.not.include('

'); + + hexo.extend.tag.unregister('blockLineTag'); + hexo.extend.tag.unregister('inlineLineTag'); + }); + + }); });