Skip to content
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
27 changes: 21 additions & 6 deletions lib/hexo/post.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ const rHexoPostRenderEscape = /<hexoPostRenderCodeBlock>([\s\S]+?)<\/hexoPostRen
const rSwigTag = /(\{\{.+?\}\})|(\{#.+?#\})|(\{%.+?%\})/s;

const rSwigPlaceHolder = /(?:<|&lt;)!--swig\uFFFC(\d+)--(?:>|&gt;)/g;
const rInlineSwigPlaceHolder = /(?:\uFFFC|&#(?:xFFFC|xfffc|65532);)swig(\d+)(?:\uFFFC|&#(?:xFFFC|xfffc|65532);)/g;
const rCodeBlockPlaceHolder = /(?:<|&lt;)!--code\uFFFC(\d+)--(?:>|&gt;)/g;
const rCommentHolder = /(?:<|&lt;)!--comment\uFFFC(\d+)--(?:>|&gt;)/g;

Expand All @@ -42,8 +43,9 @@ class PostRenderEscape {
this.stored = [];
}

static escapeContent(cache: string[], flag: string, str: string) {
return `<!--${flag}\uFFFC${cache.push(str) - 1}-->`;
static escapeContent(cache: string[], flag: string, str: string, inline = false) {
const idx = cache.push(str) - 1;
return inline ? `\uFFFC${flag}${idx}\uFFFC` : `<!--${flag}\uFFFC${idx}-->`;
}

static restoreContent(cache: string[]) {
Expand All @@ -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) {
Expand Down Expand Up @@ -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];
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 === '}') {
Expand Down
133 changes: 133 additions & 0 deletions test/scripts/hexo/post.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2423,4 +2423,137 @@ describe('Post', () => {
data.content.trim().should.include('<code>c</code>');
});
});

describe('inline swig placeholder', () => {
it('render() - markdown link on same line as swig tag at beginning of line', async () => {
hexo.extend.tag.register('inlineTestTag', () => '<span>test</span>');

const content = '{% inlineTestTag %} text with [link](https://example.com) more';
const data = await post.render('', {
content,
engine: 'markdown'
});

data.content.trim().should.include('>link</a>');
data.content.trim().should.include('example.com');
data.content.trim().should.include('<span>test</span>');

hexo.extend.tag.unregister('inlineTestTag');
});

it('render() - swig tag alone on line should not be wrapped in <p>', async () => {
hexo.extend.tag.register('blockTestTag', () => '<div>block content</div>');

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

data.content.trim().should.include('<div>block content</div>');
data.content.trim().should.include('<p>paragraph text</p>');
data.content.should.not.include('<p><div>');

hexo.extend.tag.unregister('blockTestTag');
});

it('render() - multiple swig tags on same line with markdown', async () => {
hexo.extend.tag.register('multiTestTag', args => `<span>${args[0]}</span>`);

const content = '{% multiTestTag a %} [link](https://example.com) {% multiTestTag b %}';
const data = await post.render('', {
content,
engine: 'markdown'
});

data.content.trim().should.include('>link</a>');
data.content.trim().should.include('example.com');
data.content.trim().should.include('<span>a</span>');
data.content.trim().should.include('<span>b</span>');

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('<blockquote>');
data.content.trim().should.include('<p>paragraph text</p>');
});

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</a>');
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('<p>paragraph text</p>');
data.content.should.not.include('<p>block-var</p>');
});

it('render() - swig tag with trailing whitespace only should use block format', async () => {
hexo.extend.tag.register('wsTestTag', () => '<div>ws content</div>');

const content = '{% wsTestTag %} \n\nparagraph text';

const data = await post.render('', {
content,
engine: 'markdown'
});

data.content.trim().should.include('<div>ws content</div>');
data.content.trim().should.include('<p>paragraph text</p>');
data.content.should.not.include('<p><div>');

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', () => '<div>block</div>');
hexo.extend.tag.register('inlineLineTag', () => '<span>inline</span>');

const content = '{% blockLineTag %}\n{% inlineLineTag %} [link](https://example.com)';

const data = await post.render('', {
content,
engine: 'markdown'
});

data.content.trim().should.include('<div>block</div>');
data.content.trim().should.include('<span>inline</span>');
data.content.trim().should.include('>link</a>');
data.content.should.not.include('<p><div>');

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

});
});
Loading