diff --git a/lib/extend/tag.js b/lib/extend/tag.js index ecda6a1ead..6eed6e5aa7 100644 --- a/lib/extend/tag.js +++ b/lib/extend/tag.js @@ -4,8 +4,9 @@ const { stripIndent } = require('hexo-util'); const { cyan, magenta, red } = require('chalk'); const { Environment } = require('nunjucks'); const Promise = require('bluebird'); -const placeholder = '\uFFFC'; -const rPlaceholder = /(?:<|<)!--\uFFFC(\d+)--(?:>|>)/g; +const rSwigRawFullBlock = /{% *raw *%}/; +const rCodeTag = /]*>[\s\S]+?<\/code>/g; +const escapeSwigTag = str => str.replace(/{/g, '{').replace(/}/g, '}'); class NunjucksTag { constructor(name, fn) { @@ -134,9 +135,9 @@ const getContext = (lines, errLine, location, type) => { cyan(' === (line number probably different from source) ===') ]; - Array.prototype.push.apply(message, + message.push( // get LINES_OF_CONTEXT lines surrounding `errLine` - getContextLineNums(1, lines.length, errLine, LINES_OF_CONTEXT) + ...getContextLineNums(1, lines.length, errLine, LINES_OF_CONTEXT) .map(lnNum => { const line = ' ' + lnNum + ' | ' + lines[lnNum - 1]; if (lnNum === errLine) { @@ -188,7 +189,7 @@ class Tag { if (typeof fn !== 'function') throw new TypeError('fn must be a function'); if (options == null || typeof options === 'boolean') { - options = {ends: options}; + options = { ends: options }; } let tag; @@ -229,17 +230,20 @@ class Tag { } // Get path of post from source - const source = options.source || ''; - - 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, source)))
-      .then(result => result.replace(rPlaceholder, (_, index) => cache[index]))
+    const { source = '' } = options;
+
+    return Promise.fromCallback(cb => {
+      this.env.renderString(
+        str.replace(rCodeTag, s => {
+          // https://hexo.io/docs/tag-plugins#Raw
+          // https://mozilla.github.io/nunjucks/templating.html#raw
+          // Only escape code block when there is no raw tag included
+          return s.match(rSwigRawFullBlock) ? s : escapeSwigTag(s);
+        }),
+        options,
+        cb
+      );
+    }).catch(err => Promise.reject(formatNunjucksError(err, str, source)))
       .asCallback(callback);
   }
 }
diff --git a/lib/hexo/post.js b/lib/hexo/post.js
index e1739b6dfc..ad4565e00c 100644
--- a/lib/hexo/post.js
+++ b/lib/hexo/post.js
@@ -10,23 +10,14 @@ const { slugize, escapeRegExp } = require('hexo-util');
 const { copyDir, exists, listDir, mkdirs, readFile, rmdir, unlink, writeFile } = require('hexo-fs');
 const yfm = require('hexo-front-matter');
 
-const replaceSwigTag = str => str.replace(/{/g, '\uFFFCleft\uFFFC').replace(/}/g, '\uFFFCright\uFFFC');
-const restoreReplacesSwigTag = str => str.replace(/\uFFFCleft\uFFFC/g, '{').replace(/\uFFFCright\uFFFC/g, '}');
-
 const preservedKeys = ['title', 'slug', 'path', 'layout', 'date', 'content'];
 
 const rPlaceholder = /(?:<|<)!--\uFFFC(\d+)--(?:>|>)/g;
-const rSwigVar = /\{\{[\s\S]*?\}\}/g;
-const rSwigComment = /\{#[\s\S]*?#\}/g;
-const rSwigBlock = /\{%[\s\S]*?%\}/g;
-const rSwigFullBlock = /\{% *(.+?)(?: *| +.*?)%\}[\s\S]+?\{% *end\1 *%\}/g;
-const rSwigRawFullBlock = /{% *raw *%\}[\s\S]+?\{% *endraw *%\}/g;
-const rSwigTagInsideInlineCode = /`.*?{.*?}.*?`/g;
-
-const _escapeContent = (cache, str) => {
-  const placeholder = '\uFFFC';
-  return ``;
-};
+const rSwigVarAndComment = /{[{#][\s\S]+?[}#]}/g;
+const rSwigFullBlock = /{% *(\S+?)(?: *| +.+?)%}[\s\S]+?{% *end\1 *%}/g;
+const rSwigBlock = /{%[\s\S]+?%}/g;
+
+const _escapeContent = (cache, str) => ``;
 
 class PostRenderCache {
   constructor() {
@@ -50,12 +41,9 @@ class PostRenderCache {
 
   escapeAllSwigTags(str) {
     const escape = _str => _escapeContent(this.cache, _str);
-    return str.replace(rSwigRawFullBlock, escape) // Escape {% raw %} first
-      .replace(rSwigTagInsideInlineCode, replaceSwigTag) // Avoid double escaped by marked renderer
-      .replace(rSwigFullBlock, escape)
-      .replace(rSwigBlock, escape)
-      .replace(rSwigComment, '')
-      .replace(rSwigVar, escape);
+    return str.replace(rSwigVarAndComment, escape) // Remove swig comment first to reduce string size being matched next
+      .replace(rSwigFullBlock, escape) // swig full block must escaped before swig block to avoid confliction
+      .replace(rSwigBlock, escape);
   }
 }
 
@@ -277,7 +265,7 @@ class Post {
         toString: true,
         onRenderEnd(content) {
           // Replace cache data with real contents
-          data.content = cacheObj.loadContent(restoreReplacesSwigTag(content));
+          data.content = cacheObj.loadContent(content);
 
           // Return content after replace the placeholders
           if (disableNunjucks) return data.content;
@@ -287,7 +275,6 @@ class Post {
         }
       }, options);
     }).then(content => {
-      // restore { and } inside inline code
       data.content = content;
 
       // Run "after_post_render" filters
diff --git a/test/scripts/hexo/post.js b/test/scripts/hexo/post.js
index ff4febc1e1..67f97c55f0 100644
--- a/test/scripts/hexo/post.js
+++ b/test/scripts/hexo/post.js
@@ -1016,7 +1016,7 @@ describe('Post', () => {
       engine: 'markdown'
     });
 
-    data.content.trim().should.eql(`

In Go’s templates, blocks look like this: ${escapeSwigTag(escapeHTML('{{block "template name" .}} (content) {{end}}'))}.

`); + data.content.trim().should.eql(`

In Go’s templates, blocks look like this: ${escapeSwigTag('{{block "template name" .}} (content) {{end}}')}.

`); }); // test for https://github.com/hexojs/hexo/issues/3346#issuecomment-595497849 @@ -1137,13 +1137,13 @@ describe('Post', () => { }); // indented pullquote - data.content.trim().should.contains('
{% pullquote %}foo foo foo{% endpullquote %}
'); + data.content.trim().should.contains(`
${escapeSwigTag('{% pullquote %}foo foo foo{% endpullquote %}')}
`); data.content.trim().should.contains('

test001

'); // pullquote tag data.content.trim().should.contains('

bar bar bar

\n
'); data.content.trim().should.contains('

test002

'); // indented youtube tag - data.content.trim().should.contains('
{% youtube https://example.com/demo.mp4 %}
'); + data.content.trim().should.contains(`
${escapeSwigTag('{% youtube https://example.com/demo.mp4 %}')}
`); // youtube tag data.content.trim().should.contains('
'); });