diff --git a/lib/extend/tag.js b/lib/extend/tag.js index 3ad227dc0b..6eed6e5aa7 100644 --- a/lib/extend/tag.js +++ b/lib/extend/tag.js @@ -1,11 +1,12 @@ 'use strict'; const { stripIndent } = require('hexo-util'); -const { cyan } = require('chalk'); +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) { @@ -129,14 +130,14 @@ const LINES_OF_CONTEXT = 5; const getContext = (lines, errLine, location, type) => { const message = [ - location + ' ' + type, + location + ' ' + red(type), cyan(' ===== Context Dump ====='), 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) { @@ -158,14 +159,14 @@ const getContext = (lines, errLine, location, type) => { * @param {string} str string input for Nunjucks * @return {Error} New error object with embedded context */ -const formatNunjucksError = (err, input) => { +const formatNunjucksError = (err, input, source = '') => { const match = err.message.match(/Line (\d+), Column \d+/); if (!match) return err; const errLine = parseInt(match[1], 10); if (isNaN(errLine)) return err; // trim useless info from Nunjucks Error - const splited = err.message.replace('(unknown path)', '').split('\n'); + const splited = err.message.replace('(unknown path)', source ? magenta(source) : '').split('\n'); const e = new Error(); e.name = 'Nunjucks Error'; @@ -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; @@ -222,21 +223,27 @@ class Tag { if (env.hasExtension(name)) env.removeExtension(name); } - render(str, options, callback) { + render(str, options = {}, callback) { if (!callback && typeof options === 'function') { callback = options; 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]))
+    // Get path of post from source
+    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..6508e8fec4 100644
--- a/lib/hexo/post.js
+++ b/lib/hexo/post.js
@@ -8,25 +8,15 @@ const { magenta } = require('chalk');
 const { load } = require('js-yaml');
 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 { parse: yfmParse, split: yfmSplit, stringify: yfmStringify } = require('hexo-front-matter');
 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 +40,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);
   }
 }
 
@@ -140,15 +127,15 @@ class Post {
 
   _renderScaffold(data) {
     const { tag } = this.context.extend;
-    let yfmSplit;
+    let splited;
 
     return this._getScaffold(data.layout).then(scaffold => {
       const frontMatter = prepareFrontMatter({ ...data });
-      yfmSplit = yfm.split(scaffold);
+      splited = yfmSplit(scaffold);
 
-      return tag.render(yfmSplit.data, frontMatter);
+      return tag.render(splited.data, frontMatter);
     }).then(frontMatter => {
-      const { separator } = yfmSplit;
+      const { separator } = splited;
       const jsonMode = separator.startsWith(';');
 
       // Parse front-matter
@@ -163,14 +150,14 @@ class Post {
 
       let content = '';
       // Prepend the separator
-      if (yfmSplit.prefixSeparator) content += `${separator}\n`;
+      if (splited.prefixSeparator) content += `${separator}\n`;
 
-      content += yfm.stringify(obj, {
+      content += yfmStringify(obj, {
         mode: jsonMode ? 'json' : ''
       });
 
       // Concat content
-      content += yfmSplit.content;
+      content += splited.content;
 
       if (data.content) {
         content += `\n${data.content}`;
@@ -209,7 +196,7 @@ class Post {
       return readFile(src);
     }).then(content => {
       // Create post
-      Object.assign(data, yfm(content));
+      Object.assign(data, yfmParse(content));
       data.content = data._content;
       delete data._content;
 
@@ -277,7 +264,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 +274,6 @@ class Post {
         }
       }, options);
     }).then(content => {
-      // restore { and } inside inline code
       data.content = content;
 
       // Run "after_post_render" filters
diff --git a/lib/plugins/filter/after_post_render/excerpt.js b/lib/plugins/filter/after_post_render/excerpt.js
index d82c078586..a33f44c6f2 100644
--- a/lib/plugins/filter/after_post_render/excerpt.js
+++ b/lib/plugins/filter/after_post_render/excerpt.js
@@ -1,6 +1,6 @@
 'use strict';
 
-const rExcerpt = //i;
 
 function excerptFilter(data) {
   const { content } = data;
diff --git a/lib/plugins/filter/after_post_render/external_link.js b/lib/plugins/filter/after_post_render/external_link.js
index 196b2d8b82..02c7e6fd99 100644
--- a/lib/plugins/filter/after_post_render/external_link.js
+++ b/lib/plugins/filter/after_post_render/external_link.js
@@ -2,6 +2,10 @@
 
 const { isExternalLink } = require('hexo-util');
 let EXTERNAL_LINK_POST_ENABLED = true;
+const rATag = /]+\s+?)?href=["']([^<>"']+)["'][^<>]*>/gi;
+const rTargetAttr = /target=/i;
+const rRelAttr = /rel=/i;
+const rRelStrAttr = /rel=["']([^<>"']*)["']/i;
 
 function externalLinkFilter(data) {
   if (!EXTERNAL_LINK_POST_ENABLED) return;
@@ -13,11 +17,11 @@ function externalLinkFilter(data) {
     return;
   }
 
-  data.content = data.content.replace(/]+\s)?href=["']([^<>"']+)["'][^<>]*>/gi, (str, href) => {
-    if (/target=/gi.test(str) || !isExternalLink(href, url, external_link.exclude)) return str;
+  data.content = data.content.replace(rATag, (str, href) => {
+    if (!isExternalLink(href, url, external_link.exclude) || rTargetAttr.test(str)) return str;
 
-    if (/rel=/gi.test(str)) {
-      str = str.replace(/rel="(.*?)"/gi, (relStr, rel) => {
+    if (rRelAttr.test(str)) {
+      str = str.replace(rRelStrAttr, (relStr, rel) => {
         return rel.includes('noopenner') ? relStr : `rel="${rel} noopener"`;
       });
       return str.replace('href=', 'target="_blank" href=');
diff --git a/lib/plugins/filter/after_render/external_link.js b/lib/plugins/filter/after_render/external_link.js
index 22d302f3c9..f742a82902 100644
--- a/lib/plugins/filter/after_render/external_link.js
+++ b/lib/plugins/filter/after_render/external_link.js
@@ -3,6 +3,10 @@
 const { isExternalLink } = require('hexo-util');
 
 let EXTERNAL_LINK_SITE_ENABLED = true;
+const rATag = /]+\s+?)?href=["']([^<>"']+)["'][^<>]*>/gi;
+const rTargetAttr = /target=/i;
+const rRelAttr = /rel=/i;
+const rRelStrAttr = /rel=["']([^<>"']*)["']/i;
 
 function externalLinkFilter(data) {
   if (!EXTERNAL_LINK_SITE_ENABLED) return;
@@ -14,11 +18,11 @@ function externalLinkFilter(data) {
     return;
   }
 
-  return data.replace(/]+\s)?href=["']([^<>"']+)["'][^<>]*>/gi, (str, href) => {
-    if (/target=/i.test(str) || !isExternalLink(href, url, external_link.exclude)) return str;
+  return data.replace(rATag, (str, href) => {
+    if (!isExternalLink(href, url, external_link.exclude) || rTargetAttr.test(str)) return str;
 
-    if (/rel=/i.test(str)) {
-      str = str.replace(/rel="(.*?)"/gi, (relStr, rel) => {
+    if (rRelAttr.test(str)) {
+      str = str.replace(rRelStrAttr, (relStr, rel) => {
         return rel.includes('noopenner') ? relStr : `rel="${rel} noopener"`;
       });
       return str.replace('href=', 'target="_blank" href=');
diff --git a/lib/plugins/filter/after_render/meta_generator.js b/lib/plugins/filter/after_render/meta_generator.js
index e0ccfd5c85..d5e4f2c2f3 100644
--- a/lib/plugins/filter/after_render/meta_generator.js
+++ b/lib/plugins/filter/after_render/meta_generator.js
@@ -7,14 +7,14 @@ function hexoMetaGeneratorInject(data) {
   if (!NEED_INJECT) return;
 
   if (!this.config.meta_generator
-    || data.match(/]+\s)?name=['|"]?generator['|"]?/i)) {
+    || data.match(//]+\s)?name=['"]generator['"]/i)) {
     NEED_INJECT = false;
     return;
   }
 
   META_GENERATOR_TAG = META_GENERATOR_TAG || ``;
 
-  return data.replace(/(?!<\/head>).+?<\/head>/s, str => str.replace('', `${META_GENERATOR_TAG}`));
+  return data.replace('', `${META_GENERATOR_TAG}`);
 }
 
 module.exports = hexoMetaGeneratorInject;
diff --git a/lib/plugins/processor/asset.js b/lib/plugins/processor/asset.js
index 16cb0e2630..97482474b7 100644
--- a/lib/plugins/processor/asset.js
+++ b/lib/plugins/processor/asset.js
@@ -2,7 +2,7 @@
 
 const { timezone, toDate, isExcludedFile, isMatch } = require('./common');
 const Promise = require('bluebird');
-const yfm = require('hexo-front-matter');
+const { parse: yfm } = require('hexo-front-matter');
 const { extname, relative } = require('path');
 const { Pattern } = require('hexo-util');
 
diff --git a/lib/plugins/processor/post.js b/lib/plugins/processor/post.js
index ff51e12ba5..4faa872162 100644
--- a/lib/plugins/processor/post.js
+++ b/lib/plugins/processor/post.js
@@ -2,7 +2,7 @@
 
 const { toDate, timezone, isExcludedFile, isTmpFile, isHiddenFile, isMatch } = require('./common');
 const Promise = require('bluebird');
-const yfm = require('hexo-front-matter');
+const { parse: yfm } = require('hexo-front-matter');
 const { extname, join } = require('path');
 const { stat, listDir } = require('hexo-fs');
 const { slugize, Pattern, Permalink } = require('hexo-util');
diff --git a/lib/theme/view.js b/lib/theme/view.js
index 3afa8e5c5f..6c0439bc48 100644
--- a/lib/theme/view.js
+++ b/lib/theme/view.js
@@ -1,7 +1,7 @@
 'use strict';
 
 const { dirname, extname, join } = require('path');
-const yfm = require('hexo-front-matter');
+const { parse: yfm } = require('hexo-front-matter');
 const Promise = require('bluebird');
 
 const assignIn = (target, ...sources) => {
diff --git a/package.json b/package.json
index 7cec4fe468..0257626f91 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "hexo",
-  "version": "4.2.0",
+  "version": "5.0.0",
   "description": "A fast, simple & powerful blog framework, powered by Node.js.",
   "main": "lib/hexo",
   "bin": {
@@ -43,12 +43,12 @@
     "archy": "^1.0.0",
     "bluebird": "^3.5.2",
     "chalk": "^4.0.0",
-    "hexo-cli": "^3.0.0",
-    "hexo-front-matter": "^1.0.0",
+    "hexo-cli": "^4.0.0",
+    "hexo-front-matter": "^2.0.0",
     "hexo-fs": "^3.1.0",
     "hexo-i18n": "^1.0.0",
     "hexo-log": "^2.0.0",
-    "hexo-util": "^2.0.0",
+    "hexo-util": "^2.2.0",
     "js-yaml": "^3.12.0",
     "micromatch": "^4.0.2",
     "moment": "^2.22.2",
diff --git a/test/benchmark.js b/test/benchmark.js
index 8ad9ac8020..6916e69a9a 100644
--- a/test/benchmark.js
+++ b/test/benchmark.js
@@ -10,6 +10,7 @@ const { red } = require('chalk');
 const hooks = [
   { regex: /Hexo version/, tag: 'hexo-begin' },
   { regex: /Start processing/, tag: 'processing' },
+  { regex: /Rendering post/, tag: 'render-post' },
   { regex: /Files loaded/, tag: 'file-loaded' },
   { regex: /generated in/, tag: 'generated' },
   { regex: /Database saved/, tag: 'database-saved' }
@@ -61,7 +62,8 @@ async function run_benchmark(name) {
 
     hexo.on('close', () => {
       performance.measure('Load Plugin/Scripts/Database', 'hexo-begin', 'processing');
-      performance.measure('Process Source', 'processing', 'file-loaded');
+      performance.measure('Process Source', 'processing', 'render-post');
+      performance.measure('Render Posts', 'render-post', 'file-loaded');
       performance.measure('Render Files', 'file-loaded', 'generated');
       performance.measure('Save Database', 'generated', 'database-saved');
       performance.measure('Total time', 'hexo-begin', 'database-saved');
@@ -83,16 +85,17 @@ async function gitClone(repo, dir, depth = 1) {
 
 async function init() {
   if (await exists(testDir)) {
-    log.info(`"${testDir}" already exists, deleting`);
-    await rmdir(testDir);
+    log.info(`"${testDir}" already exists. Skipping benchmark environment setup.`);
+  } else {
+    log.info('Setting up a dummy hexo site with 500 posts');
+    await gitClone('https://github.com/hexojs/hexo-theme-unit-test.git', testDir);
+    await gitClone('https://github.com/hexojs/hexo-theme-landscape', resolve(testDir, 'themes', 'landscape'));
+    await gitClone('https://github.com/SukkaLab/hexo-many-posts.git', resolve(testDir, 'source', '_posts', 'hexo-many-posts'));
   }
 
-  log.info('Setting up a dummy hexo site with 500 posts');
-  await gitClone('https://github.com/hexojs/hexo-theme-unit-test.git', testDir);
-  await gitClone('https://github.com/hexojs/hexo-theme-landscape', resolve(testDir, 'themes', 'landscape'));
-  await gitClone('https://github.com/SukkaLab/hexo-many-posts.git', resolve(testDir, 'source', '_posts', 'hexo-many-posts'));
-
   log.info('Installing dependencies');
+  // Always re-install dependencies
+  if (await exists(resolve(testDir, 'node_modules'))) await rmdir(resolve(testDir, 'node_modules'));
   await spawnAsync(npmScript, ['install', '--silent'], { cwd: testDir });
 
   log.info('Replacing hexo');
@@ -104,6 +107,9 @@ async function init() {
       resolve(testDir, 'node_modules', 'hexo'),
       resolve(__dirname, '..')
     ]);
+
+    await rmdir(resolve(testDir, 'node_modules', 'hexo-cli'));
+
     await spawnAsync('cmd', [
       '/s', '/c', 'mklink', '/D',
       resolve(testDir, 'node_modules', 'hexo-cli'),
diff --git a/test/scripts/extend/tag_errors.js b/test/scripts/extend/tag_errors.js
index 5f408e4548..1b616f2cfc 100644
--- a/test/scripts/extend/tag_errors.js
+++ b/test/scripts/extend/tag_errors.js
@@ -102,4 +102,26 @@ describe('Tag Errors', () => {
       assertNunjucksError(err, 2, 'expected variable end');
     }
   });
+
+  it('source file path', async () => {
+    const source = '_posts/hello-world.md';
+    const tag = new Tag();
+
+    tag.register('test',
+      (args, content) => {},
+      { ends: true });
+
+    const body = [
+      '{% test %}',
+      '  {{docker ps -aq | map docker inspect -f "{{.Name}} {{.Mounts}}"}}',
+      '{% endtest %}'
+    ].join('\n');
+
+    try {
+      // Add { source } as option
+      await tag.render(body, { source });
+    } catch (err) {
+      err.message.should.contains(source);
+    }
+  });
 });
diff --git a/test/scripts/filters/excerpt.js b/test/scripts/filters/excerpt.js
index e3e9efe39c..db5272121a 100644
--- a/test/scripts/filters/excerpt.js
+++ b/test/scripts/filters/excerpt.js
@@ -23,36 +23,14 @@ describe('Excerpt', () => {
   });
 
   it('with ', () => {
+    const _moreCases = [
+      '',
+      '',
+      '',
+      ''
+    ];
 
-    _moreCases().forEach(_test);
-
-    function _moreCases() {
-      const template = '';
-      // see: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp#Special_characters_meaning_in_regular_expressions
-      const spaces = ' \f\n\r\t\v\u00a0\u1680\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u2028\u2029\u202f\u205f\u3000\ufeff';
-      const cases = [];
-      let more, lead, tail, s, e;
-
-      for (let i = 0; i < spaces.length; ++i) {
-        lead = spaces[i];
-        for (let k = 0; k < spaces.length; ++k) {
-          tail = spaces[k];
-          s = '';
-          for (let m = 0; m < 3; ++m) {
-            e = '';
-            for (let n = 0; n < 3; ++n) {
-              more = template.replace('{{lead}}', s).replace('{{tail}}', e);
-              cases.push(more);
-              e += tail;
-            }
-
-            s += lead;
-          }
-        }
-      }
-
-      return cases;
-    }
+    _moreCases.forEach(moreCase => _test(moreCase));
 
     function _test(more) {
       const content = [
diff --git a/test/scripts/filters/meta_generator.js b/test/scripts/filters/meta_generator.js
index 5df65a86c4..7d6078cedf 100644
--- a/test/scripts/filters/meta_generator.js
+++ b/test/scripts/filters/meta_generator.js
@@ -37,32 +37,6 @@ describe('Meta Generator', () => {
     should.not.exist(metaGenerator(''));
   });
 
-  it('ignore empty head tag', () => {
-    const content = '';
-    hexo.config.meta_generator = true;
-    const result = metaGenerator(content);
-
-    const $ = cheerio.load(result);
-    $('meta[name="generator"]').should.have.lengthOf(1);
-
-    const expected = '';
-    result.should.eql(expected);
-  });
-
-  it('apply to first non-empty head tag only', () => {
-    const content = '';
-    hexo.config.meta_generator = true;
-    const result = metaGenerator(content);
-
-    const $ = cheerio.load(result);
-    $('meta[name="generator"]').should.have.lengthOf(1);
-
-    const expected = '';
-    result.should.eql(expected);
-  });
-
   // Test for Issue #3777
   it('multi-line head', () => {
     const content = '\n\n';
diff --git a/test/scripts/hexo/post.js b/test/scripts/hexo/post.js
index ff4febc1e1..de412cd06b 100644
--- a/test/scripts/hexo/post.js
+++ b/test/scripts/hexo/post.js
@@ -6,7 +6,7 @@ const Promise = require('bluebird');
 const { readFile, mkdirs, unlink, rmdir, writeFile, exists, stat, listDir } = require('hexo-fs');
 const { highlight, escapeHTML } = require('hexo-util');
 const { spy, useFakeTimers } = require('sinon');
-const frontMatter = require('hexo-front-matter');
+const { parse: yfm } = require('hexo-front-matter');
 const fixture = require('../../fixtures/post_render');
 const escapeSwigTag = str => str.replace(/{/g, '{').replace(/}/g, '}');
 
@@ -573,7 +573,7 @@ describe('Post', () => {
   }).then(data => post.publish({
     slug: 'foo'
   })).then(data => {
-    const meta = frontMatter(data.content);
+    const meta = yfm(data.content);
     meta.tags.should.eql(['tag', 'test']);
     return unlink(data.path);
   }));
@@ -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('
'); });