diff --git a/lib/hexo/post.js b/lib/hexo/post.js index 117669e5ff..e2ebea8b9b 100644 --- a/lib/hexo/post.js +++ b/lib/hexo/post.js @@ -2,6 +2,7 @@ const assert = require('assert'); const moment = require('moment'); +const parse5 = require('parse5'); const Promise = require('bluebird'); const { join, extname, basename } = require('path'); const { magenta } = require('picocolors'); @@ -404,18 +405,50 @@ class Post { text: data.content, path: source, engine: data.engine, - toString: true, - onRenderEnd(content) { - // Replace cache data with real contents - data.content = cacheObj.restoreAllSwigTags(content); - - // Return content after replace the placeholders - if (disableNunjucks) return data.content; - - // Render with Nunjucks - return tag.render(data.content, data); - } + toString: true }, options); + }).then(content => { + // This function restores swig tags in `content` and render them. + if (disableNunjucks) { + // If rendering is disabled, do nothing. + return cacheObj.restoreAllSwigTags(content); + } + // Whether to allow async/concurrent rendering of tags within the post. + // Enabling it can improve performance for slow async tags, but may produce + // wrong results if tags within a post depend on each other. + const async_tags = data.async_tags || false; + if (!async_tags) { + return tag.render(cacheObj.restoreAllSwigTags(content), data); + } + // We'd like to render tags concurrently, so we split `content` + // by top-level HTML nodes that have swig tags into `split_content` array + // (nodes that don't have swig tags don't need to be split). + // Then we render items in `split_content` asynchronously. + const doc = parse5.parseFragment(content); + const split_content = []; + let current = []; // Current list of nodes to be added to split_content. + doc.childNodes.forEach(node => { + const html = parse5.serializeOuter(node); + const restored = cacheObj.restoreAllSwigTags(html); + current.push(restored); + if (html !== restored) { + // Once we encouner a node that has swig tags, merge + // all content we've seen so far and add to `split_content`. + // We don't simply add every node to `split_content`, because + // most nodes don't have swig tags and calling `tag.render` for + // all of them has significant overhead. + split_content.push(current.join('')); + current = []; + } + }); + if (current.length) { + split_content.push(current.join('')); + } + // Render the tags in each top-level node asynchronously. + const results = split_content.map(async content => { + return await tag.render(content, data); + }); + return Promise.all(results).then(x => x.join('')); }).then(content => { data.content = cacheObj.restoreCodeBlocks(content); diff --git a/package.json b/package.json index 09e0b12120..65b0b3020c 100644 --- a/package.json +++ b/package.json @@ -55,6 +55,7 @@ "moment": "^2.29.1", "moment-timezone": "^0.5.34", "nunjucks": "^3.2.3", + "parse5": "^7.0.0", "picocolors": "^1.0.0", "pretty-hrtime": "^1.0.3", "resolve": "^1.22.0", diff --git a/test/scripts/hexo/post.js b/test/scripts/hexo/post.js index 8927708bf4..cadfcaee1c 100644 --- a/test/scripts/hexo/post.js +++ b/test/scripts/hexo/post.js @@ -902,6 +902,36 @@ describe('Post', () => { ].join('\n')); }); + it('render() - multiple tags with async rendering', async () => { + const content = [ + '{% blockquote %}', + 'test1', + '{% quote test2 %}', + 'test3', + '{% endquote %}', + 'test4', + '{% endblockquote %}', + 'ASDF', + '{% quote test5 %}', + 'test6', + '{% endquote %}' + ].join('\n'); + + const data = await post.render(null, { + content, + async_tags: true + }); + data.content.trim().should.eql([ + '

test1

', + '

test3

', + '
', + 'test4
', + 'ASDF', + '

test6

', + '
' + ].join('\n')); + }); + it('render() - shouln\'t break curly brackets', async () => { hexo.config.prismjs.enable = true; hexo.config.highlight.enable = false; @@ -1203,6 +1233,15 @@ describe('Post', () => { }); data.content.trim().should.eql(`

${escapeSwigTag('{{ 1 + 1 }}')} 2

`); + + // Test that the async tags logic recognize the tags correctly. + const data_async = await post.render(null, { + content, + engine: 'markdown', + async_tags: true + }); + + data_async.content.trim().should.eql(`

${escapeSwigTag('{{ 1 + 1 }}')} 2

`); }); // #3543