diff --git a/lib/extend/tag.js b/lib/extend/tag.js index 31f9f2aa5b..5fc396ff50 100644 --- a/lib/extend/tag.js +++ b/lib/extend/tag.js @@ -4,8 +4,6 @@ const stripIndent = require('strip-indent'); const { cyan } = require('chalk'); const nunjucks = require('nunjucks'); const Promise = require('bluebird'); -const placeholder = '\uFFFC'; -const rPlaceholder = /(?:<|<)!--\uFFFC(\d+)--(?:>|>)/g; class NunjucksTag { constructor(name, fn) { @@ -77,7 +75,7 @@ class NunjucksBlock extends NunjucksTag { } run(context, args, body) { - return this._run(context, args, trimBody(body)); + return this._run(context, args, trimBody(body)) + '\n'; } } @@ -111,7 +109,7 @@ class NunjucksAsyncBlock extends NunjucksBlock { body = () => result || ''; this._run(context, args, trimBody(body)).then(result => { - callback(err, result); + callback(err, `${result}\n`); }); }); } @@ -228,15 +226,8 @@ class Tag { options = {}; } - const cache = []; - - const escapeContent = str => ``; - - str = str.replace(/
[\s\S]*?<\/code><\/pre>/gm, escapeContent); - return Promise.fromCallback(cb => { this.env.renderString(str, options, cb); }) - .catch(err => Promise.reject(formatNunjucksError(err, str))) - .then(result => result.replace(rPlaceholder, (_, index) => cache[index])); + .catch(err => Promise.reject(formatNunjucksError(err, str))); } } diff --git a/lib/hexo/post.js b/lib/hexo/post.js index 1d62074eca..09b590691e 100644 --- a/lib/hexo/post.js +++ b/lib/hexo/post.js @@ -1,6 +1,5 @@ 'use strict'; -const assert = require('assert'); const moment = require('moment'); const Promise = require('bluebird'); const { join, extname } = require('path'); @@ -12,47 +11,6 @@ const yfm = require('hexo-front-matter'); const preservedKeys = ['title', 'slug', 'path', 'layout', 'date', 'content']; -const _escapeContent = (cache, str) => { - const placeholder = '\uFFFC'; - return ``; -}; - -class PostRenderCache { - constructor() { - this.cache = []; - } - - escapeContent(str) { - const rEscapeContent = / ]*)>([\s\S]*?)<\/escape>/g; - return str.replace(rEscapeContent, (_, content) => _escapeContent(this.cache, content)); - } - - loadContent(str) { - const rPlaceholder = /(?:<|<)!--\uFFFC(\d+)--(?:>|>)/g; - const restored = str.replace(rPlaceholder, (_, index) => { - assert(this.cache[index]); - const value = this.cache[index]; - this.cache[index] = null; - return value; - }); - if (restored === str) return restored; - return this.loadContent(restored); // self-recursive for nexted escaping - } - - escapeAllSwigTags(str) { - const rSwigVar = /\{\{[\s\S]*?\}\}/g; - const rSwigComment = /\{#[\s\S]*?#\}/g; - const rSwigBlock = /\{%[\s\S]*?%\}/g; - const rSwigFullBlock = /\{% *(.+?)(?: *| +.*?)%\}[\s\S]+?\{% *end\1 *%\}/g; - - const escape = _str => _escapeContent(this.cache, _str); - return str.replace(rSwigFullBlock, escape) - .replace(rSwigBlock, escape) - .replace(rSwigComment, '') - .replace(rSwigVar, escape); - } -} - const prepareFrontMatter = data => { for (const [key, item] of Object.entries(data)) { if (moment.isMoment(item)) { @@ -96,7 +54,7 @@ class Post { const ctx = this.context; const { config } = ctx; - data.slug = slugize((data.slug || data.title).toString(), {transform: config.filename_case}); + data.slug = slugize((data.slug || data.title).toString(), { transform: config.filename_case }); data.layout = (data.layout || config.default_layout).toLowerCase(); data.date = data.date ? moment(data.date) : moment(); @@ -135,7 +93,7 @@ class Post { let yfmSplit; return this._getScaffold(data.layout).then(scaffold => { - const frontMatter = prepareFrontMatter({...data}); + const frontMatter = prepareFrontMatter({ ...data }); yfmSplit = yfm.split(scaffold); return tag.render(yfmSplit.data, frontMatter); @@ -183,7 +141,7 @@ class Post { const ctx = this.context; const { config } = ctx; const draftDir = join(ctx.source_dir, '_drafts'); - const slug = slugize(data.slug.toString(), {transform: config.filename_case}); + const slug = slugize(data.slug.toString(), { transform: config.filename_case }); data.slug = slug; const regex = new RegExp(`^${escapeRegExp(slug)}(?:[^\\/\\\\]+)`); let src = ''; @@ -248,52 +206,45 @@ class Post { // disable Nunjucks when the renderer spcify that. const disableNunjucks = ext && ctx.render.renderer.get(ext) && !!ctx.render.renderer.get(ext).disableNunjucks; - const cacheObj = new PostRenderCache(); - return promise.then(content => { data.content = content; // Run "before_post_render" filters - return ctx.execFilter('before_post_render', data, {context: ctx}); + return ctx.execFilter('before_post_render', data, { context: ctx }); }).then(() => { - data.content = cacheObj.escapeContent(data.content); - if (isSwig) { - // Render with Nunjucks if this is a swig file + // Render with Nunjucks if the post is a swig file return tag.render(data.content, data); } - // Escape all Swig tags - if (!disableNunjucks) { - data.content = cacheObj.escapeAllSwigTags(data.content); - } - const options = data.markdown || {}; if (!config.highlight.enable) options.highlight = null; - ctx.log.debug('Rendering post: %s', magenta(source)); - // Render with markdown or other renderer - return ctx.render.render({ - text: data.content, + + const promise = text => ctx.render.render({ + text, path: source, engine: data.engine, toString: true, onRenderEnd(content) { - // Replace cache data with real contents - data.content = cacheObj.loadContent(content); - - // Return content after replace the placeholders - if (disableNunjucks) return data.content; - - // Render with Nunjucks - return tag.render(data.content, data); + data.content = content.replace(//g, ''); + return data.content; } }, options); + + if (!disableNunjucks) { + return tag.render(data.content, data).then(promise); + } + + // Nunjucks is disabled + return promise(data.content); + }).then(content => { data.content = content; // Run "after_post_render" filters - return ctx.execFilter('after_post_render', data, {context: ctx}); + return ctx.execFilter('after_post_render', data, { context: ctx }); }).asCallback(callback); } } diff --git a/lib/plugins/filter/before_post_render/backtick_code_block.js b/lib/plugins/filter/before_post_render/backtick_code_block.js index a97af39210..8d6c51df93 100644 --- a/lib/plugins/filter/before_post_render/backtick_code_block.js +++ b/lib/plugins/filter/before_post_render/backtick_code_block.js @@ -63,7 +63,7 @@ function backtickCodeBlock(data) { .replace(/{/g, '{') .replace(/}/g, '}'); - return `${start} ${content} ${end}`; + return `${start}${end}`; }); } diff --git a/test/fixtures/post_render.js b/test/fixtures/post_render.js index c6f7f067a0..3943683b5c 100644 --- a/test/fixtures/post_render.js +++ b/test/fixtures/post_render.js @@ -7,7 +7,7 @@ const code = [ ' sleep()' ].join('\n'); -const content = [ +exports.content = [ '# Title', '``` python', code, @@ -24,7 +24,19 @@ const content = [ '{% endquote %}' ].join('\n'); -exports.content = content; +exports.content_for_issue_3346 = [ + '# Title', + '```', + '{% test1 %}', + '{{ test2 }}', + '```', + 'some content', + '', + '## Another title', + '{% blockquote %}', + 'quote content', + '{% endblockquote %}' +].join('\n'); exports.expected = [ 'Title
', @@ -33,7 +45,7 @@ exports.expected = [ 'Another title
', '', '\n\n', + '\n\n\n', 'quote content
\n', - '' ].join(''); @@ -50,3 +62,13 @@ exports.expected_disable_nunjucks = [ 'quote contentquote content
\n', '
', '{% endquote %}' ].join(''); + +exports.expected_for_issue_3346 = [ + 'Title
', + highlight('{% test1 %}\n{{ test2 }}').replace(/{/g, '{').replace(/}/g, '}'), // Escaped by backtick_code_block + '\nsome content
\n', + 'Another title
', + '', + '' +].join(''); diff --git a/test/scripts/extend/tag.js b/test/scripts/extend/tag.js index 193ee64890..ff28e44e6b 100644 --- a/test/scripts/extend/tag.js +++ b/test/scripts/extend/tag.js @@ -37,7 +37,7 @@ describe('Tag', () => { ].join('\n'); const result = await tag.render(str); - result.should.eql('foo bar test content'); + result.should.eql('foo bar test content\n'); }); it('register() - async block', async () => { @@ -52,7 +52,7 @@ describe('Tag', () => { ].join('\n'); const result = await tag.render(str); - result.should.eql('foo bar test content'); + result.should.eql('foo bar test content\n'); }); it('register() - nested test', async () => { @@ -111,7 +111,7 @@ describe('Tag', () => { ].join('\n'); const result = await tag.render(str); - result.should.eql('test content'); + result.should.eql('test content\n'); }); it('register() - async callback', async () => { diff --git a/test/scripts/filters/backtick_code_block.js b/test/scripts/filters/backtick_code_block.js index 247e0dbd75..5f1f0bbfd9 100644 --- a/test/scripts/filters/backtick_code_block.js +++ b/test/scripts/filters/backtick_code_block.js @@ -67,7 +67,7 @@ describe('Backtick code block', () => { }; codeBlock(data); - data.content.should.eql('quote content
\n', + '' + highlight(code, {lang: 'js'}) + ' '); + data.content.should.eql(''); }); it('without language name', () => { @@ -82,7 +82,7 @@ describe('Backtick code block', () => { const expected = highlight(code); codeBlock(data); - data.content.should.eql('' + expected + ' '); + data.content.should.eql(''); }); it('without language name - ignore tab character', () => { @@ -97,7 +97,7 @@ describe('Backtick code block', () => { const expected = highlight(code); codeBlock(data); - data.content.should.eql('' + expected + ' '); + data.content.should.eql(''); }); it('title', () => { @@ -115,7 +115,7 @@ describe('Backtick code block', () => { }); codeBlock(data); - data.content.should.eql('' + expected + ' '); + data.content.should.eql(''); }); it('url', () => { @@ -133,7 +133,7 @@ describe('Backtick code block', () => { }); codeBlock(data); - data.content.should.eql('' + expected + ' '); + data.content.should.eql(''); }); it('link text', () => { @@ -151,7 +151,7 @@ describe('Backtick code block', () => { }); codeBlock(data); - data.content.should.eql('' + expected + ' '); + data.content.should.eql(''); }); it('indent', () => { @@ -171,7 +171,7 @@ describe('Backtick code block', () => { }); codeBlock(data); - data.content.should.eql('' + expected + ' '); + data.content.should.eql(''); }); it('line number false', () => { @@ -191,7 +191,7 @@ describe('Backtick code block', () => { }); codeBlock(data); - data.content.should.eql('' + expected + ' '); + data.content.should.eql(''); }); it('line number false, don`t first_line_number always1', () => { @@ -212,7 +212,7 @@ describe('Backtick code block', () => { }); codeBlock(data); - data.content.should.eql('' + expected + ' '); + data.content.should.eql(''); }); it('line number false, don`t care first_line_number inilne', () => { @@ -233,7 +233,7 @@ describe('Backtick code block', () => { }); codeBlock(data); - data.content.should.eql('' + expected + ' '); + data.content.should.eql(''); }); it('line number true', () => { @@ -253,7 +253,7 @@ describe('Backtick code block', () => { }); codeBlock(data); - data.content.should.eql('' + expected + ' '); + data.content.should.eql(''); }); it('line number, first_line_number always1, js=', () => { @@ -275,7 +275,7 @@ describe('Backtick code block', () => { }); codeBlock(data); - data.content.should.eql('' + expected + ' '); + data.content.should.eql(''); }); it('line number, first_line_number inline, js', () => { @@ -297,7 +297,7 @@ describe('Backtick code block', () => { }); codeBlock(data); - data.content.should.eql('' + expected + ' '); + data.content.should.eql(''); }); it('line number, first_line_number inline, js=1', () => { @@ -319,7 +319,7 @@ describe('Backtick code block', () => { }); codeBlock(data); - data.content.should.eql('' + expected + ' '); + data.content.should.eql(''); }); it('line number, first_line_number inline, js=2', () => { @@ -341,7 +341,7 @@ describe('Backtick code block', () => { }); codeBlock(data); - data.content.should.eql('' + expected + ' '); + data.content.should.eql(''); }); it('tab replace', () => { @@ -367,7 +367,7 @@ describe('Backtick code block', () => { }); codeBlock(data); - data.content.should.eql('' + expected + ' '); + data.content.should.eql(''); }); it('wrap', () => { @@ -382,7 +382,7 @@ describe('Backtick code block', () => { }; codeBlock(data); - data.content.should.eql('' + highlight(code, { lang: 'js', wrap: false }) + ' '); + data.content.should.eql(''); hexo.config.highlight.wrap = true; }); diff --git a/test/scripts/hexo/post.js b/test/scripts/hexo/post.js index 846c3ef3f5..8678c18dbb 100644 --- a/test/scripts/hexo/post.js +++ b/test/scripts/hexo/post.js @@ -645,7 +645,7 @@ describe('Post', () => { }).then(data => { data.content.trim().should.eql([ highlighted, - '', + '\n', highlighted ].join('\n')); }); @@ -910,7 +910,7 @@ describe('Post', () => { engine: 'markdown' }).then(data => { data.content.trim().should.eql([ - '\n\n', + 'content1
\n\n\n\n', 'content1
\nThis is a following paragraph
\n', '' ].join('')); @@ -932,11 +932,59 @@ describe('Post', () => { engine: 'markdown' }).then(data => { data.content.trim().should.eql([ - 'content2
\n\n\n', + 'content1
\n\n\n\n', 'content1
\nThis is a following paragraph
\n', '' ].join('')); }); }); + // test for Issue #3346 + it('render() - swig tag inside backtick code block', () => { + const content = fixture.content_for_issue_3346; + + return post.render(null, { + content, + engine: 'markdown' + }).then(data => { + data.content.trim().should.eql(fixture.expected_for_issue_3346); + }); + }); + + // test for https://github.com/hexojs/hexo/pull/4171#issuecomment-594412367 + it('render() - markdown content right after swig tag', async () => { + const content = [ + '{% pullquote warning %}', + 'Text', + '{% endpullquote %}', + '# Title 0', + '{% pullquote warning %}', + 'Text', + '{% endpullquote %}', + '{% pullquote warning %}', + 'Text', + '{% endpullquote %}', + '# Title 1', + '{% pullquote warning %}', + 'Text', + '{% endpullquote %}', + '{% pullquote warning %}Text{% endpullquote %}', + '# Title 2', + '{% pullquote warning %}Text{% endpullquote %}', + '{% pullquote warning %}Text{% endpullquote %}', + '# Title 3', + '{% pullquote warning %}Text{% endpullquote %}' + ].join('\n'); + + const data = await post.render(null, { + content, + engine: 'markdown' + }); + + // We only to make sure markdown content is rendered correctly + data.content.trim().should.include('content2
\nTitle 0
'); + data.content.trim().should.include('Title 1
'); + data.content.trim().should.include('Title 2
'); + data.content.trim().should.include('Title 3
'); + }); });