Skip to content

Commit

Permalink
Merge branch 'master' of github.com:jehy/hexo into private-posts
Browse files Browse the repository at this point in the history
  • Loading branch information
jehy committed Jul 29, 2020
2 parents d8ffa70 + 1489074 commit 6f15cc6
Show file tree
Hide file tree
Showing 15 changed files with 118 additions and 137 deletions.
45 changes: 26 additions & 19 deletions 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 = /(?:<|&lt;)!--\uFFFC(\d+)--(?:>|&gt;)/g;
const rSwigRawFullBlock = /{% *raw *%}/;
const rCodeTag = /<code[^<>]*>[\s\S]+?<\/code>/g;
const escapeSwigTag = str => str.replace(/{/g, '&#123;').replace(/}/g, '&#125;');

class NunjucksTag {
constructor(name, fn) {
Expand Down Expand Up @@ -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) {
Expand All @@ -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';
Expand All @@ -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;
Expand Down Expand Up @@ -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 => `<!--${placeholder}${cache.push(str) - 1}-->`;

str = str.replace(/<pre><code.*?>[\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);
}
}
Expand Down
50 changes: 18 additions & 32 deletions lib/hexo/post.js
Expand Up @@ -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, '&#123;').replace(/\uFFFCright\uFFFC/g, '&#125;');

const { parse: yfmParse, split: yfmSplit, stringify: yfmStringify } = require('hexo-front-matter');
const preservedKeys = ['title', 'slug', 'path', 'layout', 'date', 'content'];

const rPlaceholder = /(?:<|&lt;)!--\uFFFC(\d+)--(?:>|&gt;)/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 `<!--${placeholder}${cache.push(str) - 1}-->`;
};
const rSwigVarAndComment = /{[{#][\s\S]+?[}#]}/g;
const rSwigFullBlock = /{% *(\S+?)(?: *| +.+?)%}[\s\S]+?{% *end\1 *%}/g;
const rSwigBlock = /{%[\s\S]+?%}/g;

const _escapeContent = (cache, str) => `<!--\uFFFC${cache.push(str) - 1}-->`;

class PostRenderCache {
constructor() {
Expand All @@ -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);
}
}

Expand Down Expand Up @@ -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
Expand All @@ -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}`;
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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;
Expand All @@ -287,7 +274,6 @@ class Post {
}
}, options);
}).then(content => {
// restore { and } inside inline code
data.content = content;

// Run "after_post_render" filters
Expand Down
2 changes: 1 addition & 1 deletion lib/plugins/filter/after_post_render/excerpt.js
@@ -1,6 +1,6 @@
'use strict';

const rExcerpt = /<!--+\s*more\s*--+>/i;
const rExcerpt = /<!-- ?more ?-->/i;

function excerptFilter(data) {
const { content } = data;
Expand Down
12 changes: 8 additions & 4 deletions lib/plugins/filter/after_post_render/external_link.js
Expand Up @@ -2,6 +2,10 @@

const { isExternalLink } = require('hexo-util');
let EXTERNAL_LINK_POST_ENABLED = true;
const rATag = /<a(?:\s+?|\s+?[^<>]+\s+?)?href=["']([^<>"']+)["'][^<>]*>/gi;
const rTargetAttr = /target=/i;
const rRelAttr = /rel=/i;
const rRelStrAttr = /rel=["']([^<>"']*)["']/i;

function externalLinkFilter(data) {
if (!EXTERNAL_LINK_POST_ENABLED) return;
Expand All @@ -13,11 +17,11 @@ function externalLinkFilter(data) {
return;
}

data.content = data.content.replace(/<a\s+(?:[^<>]+\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=');
Expand Down
12 changes: 8 additions & 4 deletions lib/plugins/filter/after_render/external_link.js
Expand Up @@ -3,6 +3,10 @@
const { isExternalLink } = require('hexo-util');

let EXTERNAL_LINK_SITE_ENABLED = true;
const rATag = /<a(?:\s+?|\s+?[^<>]+\s+?)?href=["']([^<>"']+)["'][^<>]*>/gi;
const rTargetAttr = /target=/i;
const rRelAttr = /rel=/i;
const rRelStrAttr = /rel=["']([^<>"']*)["']/i;

function externalLinkFilter(data) {
if (!EXTERNAL_LINK_SITE_ENABLED) return;
Expand All @@ -14,11 +18,11 @@ function externalLinkFilter(data) {
return;
}

return data.replace(/<a\s+(?:[^<>]+\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=');
Expand Down
4 changes: 2 additions & 2 deletions lib/plugins/filter/after_render/meta_generator.js
Expand Up @@ -7,14 +7,14 @@ function hexoMetaGeneratorInject(data) {
if (!NEED_INJECT) return;

if (!this.config.meta_generator
|| data.match(/<meta\s+(?:[^<>]+\s)?name=['|"]?generator['|"]?/i)) {
|| data.match(/<meta\s+(?:[^<>/]+\s)?name=['"]generator['"]/i)) {
NEED_INJECT = false;
return;
}

META_GENERATOR_TAG = META_GENERATOR_TAG || `<meta name="generator" content="Hexo ${this.version}">`;

return data.replace(/<head>(?!<\/head>).+?<\/head>/s, str => str.replace('</head>', `${META_GENERATOR_TAG}</head>`));
return data.replace('</head>', `${META_GENERATOR_TAG}</head>`);
}

module.exports = hexoMetaGeneratorInject;
2 changes: 1 addition & 1 deletion lib/plugins/processor/asset.js
Expand Up @@ -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');

Expand Down
2 changes: 1 addition & 1 deletion lib/plugins/processor/post.js
Expand Up @@ -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');
Expand Down
2 changes: 1 addition & 1 deletion 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) => {
Expand Down
8 changes: 4 additions & 4 deletions 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": {
Expand Down Expand Up @@ -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",
Expand Down
22 changes: 14 additions & 8 deletions test/benchmark.js
Expand Up @@ -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' }
Expand Down Expand Up @@ -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');
Expand All @@ -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');
Expand All @@ -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'),
Expand Down

0 comments on commit 6f15cc6

Please sign in to comment.