From 02d19962152a593beb270563465a093a711d3820 Mon Sep 17 00:00:00 2001 From: ppoffice Date: Wed, 25 Dec 2019 22:51:14 -0500 Subject: [PATCH] refactor(schema): rewrite config generator & add migration --- .gitignore | 2 +- include/common/ConfigGenerator.js | 103 ------------- include/common/ConfigValidator.js | 132 ----------------- include/common/utils.js | 151 -------------------- include/migration/head.js | 1 + include/migration/v2_v3.js | 89 ++++++++++++ include/schema/comment/changyan.json | 2 +- include/schema/comment/disqusjs.json | 9 +- include/schema/comment/gitalk.json | 21 ++- include/schema/comment/gitment.json | 9 +- include/schema/comment/valine.json | 30 ++-- include/schema/common/article.json | 20 ++- include/schema/common/head.json | 9 +- include/schema/common/navbar.json | 3 +- include/schema/common/providers.json | 9 +- include/schema/config.json | 3 +- include/schema/donate/buymeacoffee.json | 2 +- include/schema/donate/paypal.json | 2 +- include/schema/misc/meta.json | 3 +- include/schema/misc/open_graph.json | 39 +++-- include/schema/misc/poly_links.json | 3 +- include/schema/plugin/baidu_analytics.json | 5 +- include/schema/plugin/google_analytics.json | 5 +- include/schema/plugin/hotjar.json | 8 +- include/schema/widget/links.json | 3 +- include/schema/widget/profile.json | 24 ++-- include/schema/widget/subscribe_email.json | 3 +- include/task/check_config.js | 46 ------ include/task/dependencies.js | 23 --- include/task/welcome.js | 10 -- include/util/migrate.js | 110 ++++++++++++++ include/util/schema.js | 132 +++++++++++++---- include/util/yaml.js | 43 ++++++ package.json | 1 + scripts/index.js | 127 ++++++++++++++-- 35 files changed, 610 insertions(+), 572 deletions(-) delete mode 100644 include/common/ConfigGenerator.js delete mode 100644 include/common/ConfigValidator.js delete mode 100644 include/common/utils.js create mode 100644 include/migration/head.js create mode 100644 include/migration/v2_v3.js delete mode 100644 include/task/check_config.js delete mode 100644 include/task/dependencies.js delete mode 100644 include/task/welcome.js create mode 100644 include/util/migrate.js create mode 100644 include/util/yaml.js diff --git a/.gitignore b/.gitignore index d1d7bbc..7a1d212 100644 --- a/.gitignore +++ b/.gitignore @@ -106,5 +106,5 @@ dist # Stores VSCode versions used for testing VSCode extensions .vscode-test -_config.yml +_config.yml* yarn.lock diff --git a/include/common/ConfigGenerator.js b/include/common/ConfigGenerator.js deleted file mode 100644 index 961b09e..0000000 --- a/include/common/ConfigGenerator.js +++ /dev/null @@ -1,103 +0,0 @@ -const yaml = require('js-yaml'); -const Type = require('js-yaml/lib/js-yaml/type'); -const Schema = require('js-yaml/lib/js-yaml/schema'); - -const { is, descriptors } = require('./utils'); -const { doc, type, requires, defaultValue } = descriptors; - -const UNDEFINED = Symbol('undefined'); -// output null as empty in yaml -const YAML_SCHEMA = new Schema({ - include: [ - require('js-yaml/lib/js-yaml/schema/default_full') - ], - implicit: [ - new Type('tag:yaml.org,2002:null', { - kind: 'scalar', - resolve(data) { - if (data === null) { - return true; - } - const max = data.length; - return (max === 1 && data === '~') || - (max === 4 && (data === 'null' || data === 'Null' || data === 'NULL')); - }, - construct: () => null, - predicate: object => object === null, - represent: { - empty: function () { return ''; } - }, - defaultStyle: 'empty' - }) - ] -}); - -function appendDoc(spec, defaults) { - if (defaults === null) { - return null; - } - if (is.array(defaults) && spec.hasOwnProperty('*')) { - return defaults.map(value => appendDoc(spec['*'], value)); - } else if (is.object(defaults)) { - const _defaults = {}; - for (let key in defaults) { - if (spec.hasOwnProperty(key) && spec[key].hasOwnProperty(doc)) { - let i = 0; - for (let line of spec[key][doc].split('\n')) { - _defaults['#' + key + i++] = line; - } - } - _defaults[key] = appendDoc(spec.hasOwnProperty(key) ? spec[key] : {}, defaults[key]); - } - return _defaults; - } - return defaults; -} - -function generate(spec, parentConfig = null) { - if (!is.spec(spec)) { - return UNDEFINED; - } - if (spec.hasOwnProperty(requires) && !spec[requires](parentConfig)) { - return UNDEFINED; - } - if (spec.hasOwnProperty(defaultValue)) { - return appendDoc(spec, spec[defaultValue]); - } - const types = is.array(spec[type]) ? spec[type] : [spec[type]]; - if (types.includes('object')) { - let defaults = UNDEFINED; - for (let key in spec) { - if (key === '*') { - continue; - } - const value = generate(spec[key], defaults); - if (value !== UNDEFINED) { - if (defaults === UNDEFINED) { - defaults = {}; - } - defaults[key] = value; - } - } - return appendDoc(spec, defaults); - } else if (types.includes('array') && spec.hasOwnProperty('*')) { - return [generate(spec['*'], {})]; - } - return UNDEFINED; -} - -class ConfigGenerator { - constructor(spec) { - this.spec = spec; - } - - generate() { - return yaml.safeDump(generate(this.spec), { - indent: 4, - lineWidth: 1024, - schema: YAML_SCHEMA - }).replace(/^(\s*)\'#.*?\':\s*\'*(.*?)\'*$/mg, '$1# $2'); // make comment lines - } -} - -module.exports = ConfigGenerator; diff --git a/include/common/ConfigValidator.js b/include/common/ConfigValidator.js deleted file mode 100644 index d0e89ab..0000000 --- a/include/common/ConfigValidator.js +++ /dev/null @@ -1,132 +0,0 @@ -const { is, descriptors } = require('./utils'); -const { type, required, requires, format, defaultValue } = descriptors; -const { - InvalidSpecError, - MissingRequiredError, - TypeMismatchError, - FormatMismatchError, - VersionMalformedError, - VersionNotFoundError, - VersionMismatchError } = require('./utils').errors; - -function isRequiresSatisfied(spec, config) { - try { - if (!spec.hasOwnProperty(requires) || spec[requires](config) === true) { - return true; - } - } catch (e) { } - return false; -} - -function getConfigType(spec, config) { - const specTypes = is.array(spec[type]) ? spec[type] : [spec[type]]; - for (let specType of specTypes) { - if (is[specType](config)) { - return specType; - } - } - return null; -} - -function hasFormat(spec, config) { - if (!spec.hasOwnProperty(format)) { - return true; - } - return spec[format].test(config); -} - -function validate(spec, config, parentConfig, path) { - if (!is.spec(spec)) { - throw new InvalidSpecError(spec, path); - } - if (!isRequiresSatisfied(spec, parentConfig)) { - return; - } - if (is.undefined(config) || is.null(config)) { - if (spec[required] === true) { - throw new MissingRequiredError(spec, path); - } - return; - } - const type = getConfigType(spec, config); - if (type === null) { - throw new TypeMismatchError(spec, path, config); - } - if (type === 'string') { - if (!hasFormat(spec, config)) { - throw new FormatMismatchError(spec, path, config); - } - } else if (type === 'array' && spec.hasOwnProperty('*')) { - config.forEach((child, i) => validate(spec['*'], child, config, path.concat(`[${i}]`))); - } else if (type === 'object') { - for (let key in spec) { - if (key === '*') { - Object.keys(config).forEach(k => validate(spec['*'], config[k], config, path.concat(k))); - } else { - validate(spec[key], config[key], config, path.concat(key)); - } - } - } -} - -function formatVersion(ver) { - const m = /^(\d)+\.(\d)+\.(\d)+(?:-([0-9A-Za-z-]+))*$/.exec(ver); - if (m === null) { - throw new VersionMalformedError(ver); - } - return { - major: m[1], - minor: m[2], - patch: m[3], - identifier: m.length > 4 ? m[4] : null - }; -} - -function compareVersion(ver1, ver2) { - for (let section of ['major', 'minor', 'patch']) { - if (ver1[section] !== ver2[section]) { - return Math.sign(ver1[section] - ver2[section]); - } - } - const id1 = ver1.hasOwnProperty('identifier') ? ver1.identifier : null; - const id2 = ver2.hasOwnProperty('identifier') ? ver2.identifier : null; - if (id1 === id2) { - return 0; - } - if (id1 === null) { - return 1; - } - if (id2 === null) { - return -1; - } - return id1.localeCompare(id2); -} - -function isBreakingChange(base, ver) { - return base.major !== ver.major || base.minor !== ver.minor; -} - - -function checkVersion(spec, config) { - if (!config.hasOwnProperty('version')) { - throw new VersionNotFoundError(); - } - const configVersion = formatVersion(config.version); - const specVersion = formatVersion(spec.version[defaultValue]); - if (isBreakingChange(specVersion, configVersion)) { - throw new VersionMismatchError(spec.version[defaultValue], config.version, compareVersion(specVersion, configVersion) > 0); - } -} - -class ConfigValidator { - constructor(spec) { - this.spec = spec; - } - - validate(config) { - checkVersion(this.spec, config); - validate(this.spec, config, null, []); - } -} - -module.exports = ConfigValidator; \ No newline at end of file diff --git a/include/common/utils.js b/include/common/utils.js deleted file mode 100644 index 6508f7d..0000000 --- a/include/common/utils.js +++ /dev/null @@ -1,151 +0,0 @@ -const doc = Symbol('@doc'); -const type = Symbol('@type'); -const format = Symbol('@format'); -const required = Symbol('@required'); -const requires = Symbol('@requires'); -const defaultValue = Symbol('@default'); - -const descriptors = { - doc, - type, - format, - requires, - required, - defaultValue -}; - -const is = (() => ({ - number(value) { - return typeof (value) === 'number'; - }, - string(value) { - return typeof (value) === 'string'; - }, - array(value) { - return Array.isArray(value); - }, - boolean(value) { - return typeof (value) === 'boolean'; - }, - object(value) { - return typeof (value) === 'object' && value.constructor == Object; - }, - function(value) { - return typeof (value) === 'function'; - }, - regexp(value) { - return value instanceof RegExp; - }, - undefined(value) { - return typeof (value) === 'undefined'; - }, - null(value) { - return value === null; - }, - spec(value) { - if (!value.hasOwnProperty(type)) { - return false; - } - if (!is.string(value[type]) && !is.array(value[type])) { - return false; - } - if (value.hasOwnProperty(doc) && !is.string(value[doc])) { - return false; - } - if (value.hasOwnProperty(required) && !is.boolean(value[required])) { - return false; - } - if (value.hasOwnProperty(requires) && !is.function(value[requires])) { - return false; - } - if (value.hasOwnProperty(format) && !is.regexp(value[format])) { - return false; - } - return true; - } -}))(); - -class ConfigError extends Error { - constructor(spec, path) { - super(); - this.spec = spec; - this.path = path; - } -} - -class InvalidSpecError extends ConfigError { - constructor(spec, path) { - super(spec, path); - this.message = `The specification '${path.join('.')}' is invalid.`; - } -} - -class MissingRequiredError extends ConfigError { - constructor(spec, path) { - super(spec, path); - this.message = `Configuration file do not have the required '${path.join('.')}' field.`; - } -} - -class TypeMismatchError extends ConfigError { - constructor(spec, path, config) { - super(spec, path); - this.config = config; - this.message = `Configuration '${path.join('.')}' is not one of the '${spec[type]}' type.`; - } -} - -class FormatMismatchError extends ConfigError { - constructor(spec, path, config) { - super(spec, path); - this.config = config; - this.message = `Configuration '${path.join('.')}' do not match the format '${spec[format]}'.`; - } -} - -class VersionError extends Error { -} - -class VersionNotFoundError extends VersionError { - constructor() { - super(`Version number is not found in the configuration file.`); - } -} - -class VersionMalformedError extends VersionError { - constructor(version) { - super(`Version number ${version} is malformed.`); - this.version = version; - } -} - -class VersionMismatchError extends VersionError { - constructor(specVersion, configVersion, isConfigVersionSmaller) { - super(); - this.specVersion = specVersion; - this.configVersion = configVersion; - if (isConfigVersionSmaller) { - this.message = `The configuration version ${configVersion} is far behind the specification version ${specVersion}.`; - } else { - this.message = `The configuration version ${configVersion} is way ahead of the specification version ${specVersion}.`; - } - } -} - -const errors = { - ConfigError, - InvalidSpecError, - MissingRequiredError, - TypeMismatchError, - FormatMismatchError, - VersionError, - VersionMalformedError, - VersionNotFoundError, - VersionMismatchError -} - -module.exports = { - is, - descriptors, - errors -}; \ No newline at end of file diff --git a/include/migration/head.js b/include/migration/head.js new file mode 100644 index 0000000..63f0634 --- /dev/null +++ b/include/migration/head.js @@ -0,0 +1 @@ +module.exports = require('./v2_v3'); diff --git a/include/migration/v2_v3.js b/include/migration/v2_v3.js new file mode 100644 index 0000000..44561c0 --- /dev/null +++ b/include/migration/v2_v3.js @@ -0,0 +1,89 @@ +const logger = require('hexo-log')(); +const deepmerge = require('deepmerge'); +const Migration = require('../util/migrate').Migration; + +module.exports = class extends Migration { + constructor() { + super('3.0.0', null); + } + + doMigrate(config) { + const result = deepmerge({}, config); + result.head = { + favicon: config.favicon || null, + canonical_url: config.canonical_url || null, + open_graph: config.open_graph || null, + meta: config.meta || null, + rss: config.rss || null + }; + delete result.favicon; + delete result.canonical_url; + delete result.open_graph; + delete result.meta; + delete result.rss; + + if (result.search && Object.prototype.hasOwnProperty.call(result.search, 'type')) { + switch (result.search.type) { + case 'google-cse': + result.search.type = 'google_cse'; + break; + } + } + + if (result.comment && Object.prototype.hasOwnProperty.call(result.comment, 'type')) { + switch (result.comment.type) { + case 'changyan': + result.comment.app_id = config.comment.appid; + delete result.comment.appid; + break; + } + } + + if (Array.isArray(result.widgets) && result.widgets.length) { + for (const widget of result.widgets) { + if (Object.prototype.hasOwnProperty.call(widget, 'type')) { + switch (widget.type) { + case 'archive': + widget.type = 'archives'; + break; + case 'category': + widget.type = 'categories'; + break; + case 'tag': + widget.type = 'tags'; + break; + case 'tagcloud': + logger.warn('The tagcloud widget has been removed from Icarus in version 3.0.0.'); + logger.warn('Please remove it from your configuration file.'); + break; + } + } + } + } + + if (result.plugins && typeof result.plugins === 'object') { + for (const name in result.plugins) { + switch (name) { + case 'outdated-browser': + result.plugins.outdated_browser = result.plugins[name]; + delete result.plugins[name]; + break; + case 'back-to-top': + result.plugins.back_to_top = result.plugins[name]; + delete result.plugins[name]; + break; + case 'baidu-analytics': + result.plugins.baidu_analytics = result.plugins[name]; + delete result.plugins[name]; + break; + case 'google-analytics': + result.plugins.google_analytics = result.plugins[name]; + delete result.plugins[name]; + break; + } + } + } + + return result; + } +}; diff --git a/include/schema/comment/changyan.json b/include/schema/comment/changyan.json index a10ee29..b27efc6 100644 --- a/include/schema/comment/changyan.json +++ b/include/schema/comment/changyan.json @@ -12,7 +12,7 @@ "type": "string", "description": "Changyan app ID" }, - "shortname": { + "conf": { "type": "string", "description": "Changyan configuration ID" } diff --git a/include/schema/comment/disqusjs.json b/include/schema/comment/disqusjs.json index 33ebaf5..cc53d01 100644 --- a/include/schema/comment/disqusjs.json +++ b/include/schema/comment/disqusjs.json @@ -34,17 +34,20 @@ }, "admin": { "type": "string", - "description": "Disqus moderator username" + "description": "Disqus moderator username", + "nullable": true }, "admin_label": { "type": "string", "description": "Disqus moderator badge text", - "default": false + "default": false, + "nullable": true }, "nesting": { "type": "integer", "description": "Maximum number of comment nesting level", - "default": 4 + "default": 4, + "nullable": true } }, "required": [ diff --git a/include/schema/comment/gitalk.json b/include/schema/comment/gitalk.json index 1d6986a..231cd5b 100644 --- a/include/schema/comment/gitalk.json +++ b/include/schema/comment/gitalk.json @@ -34,35 +34,42 @@ "per_page": { "type": "number", "description": "Pagination size, with maximum 100", - "default": 10 + "default": 10, + "nullable": true }, "distraction_free_mode": { "type": "boolean", "description": "Facebook-like distraction free mode", - "default": false + "default": false, + "nullable": true }, "pager_direction": { "type": "string", "description": "Comment sorting direction, available values are `last` and `first`", - "default": "last" + "default": "last", + "nullable": true }, "create_issue_manually": { "type": "boolean", "description": "Create GitHub issues manually for each page", - "default": false + "default": false, + "nullable": true }, "proxy": { "type": "string", - "description": "GitHub oauth request reverse proxy for CORS" + "description": "GitHub oauth request reverse proxy for CORS", + "nullable": true }, "flip_move_options": { "type": "object", - "description": "Comment list animation" + "description": "Comment list animation", + "nullable": true }, "enable_hotkey": { "type": "boolean", "description": "Enable hot key (cmd|ctrl + enter) submit comment", - "default": true + "default": true, + "nullable": true } }, "required": [ diff --git a/include/schema/comment/gitment.json b/include/schema/comment/gitment.json index b220fc1..c9fd071 100644 --- a/include/schema/comment/gitment.json +++ b/include/schema/comment/gitment.json @@ -27,17 +27,20 @@ "theme": { "type": "string", "description": "An optional Gitment theme object", - "default": "gitment.defaultTheme" + "default": "gitment.defaultTheme", + "nullable": true }, "per_page": { "type": "number", "description": "An optional number to which comments will be paginated", - "default": 20 + "default": 20, + "nullable": true }, "max_comment_height": { "type": "number", "description": "An optional number to limit comments' max height, over which comments will be folded", - "default": 250 + "default": 250, + "nullable": true } }, "required": [ diff --git a/include/schema/comment/valine.json b/include/schema/comment/valine.json index 46bede8..0ae4ea9 100644 --- a/include/schema/comment/valine.json +++ b/include/schema/comment/valine.json @@ -18,17 +18,20 @@ }, "placeholder": { "type": "string", - "description": "Comment box placeholders" + "description": "Comment box placeholders", + "nullable": true }, "notify": { "type": "boolean", "description": "Enable email notification when someone comments", - "default": false + "default": false, + "nullable": true }, "verify": { "type": "boolean", "description": "Enable verification code service", - "default": false + "default": false, + "nullable": true }, "avatar": { "type": "string", @@ -44,12 +47,14 @@ "hide", "mm" ], - "default": "mm" + "default": "mm", + "nullable": true }, "avatar_force": { "type": "boolean", "description": "Pull the latest avatar upon page visit", - "default": false + "default": false, + "nullable": true }, "meta": { "type": "array", @@ -61,27 +66,32 @@ "nick", "mail", "link" - ] + ], + "nullable": true }, "page_size": { "type": "integer", "description": "Number of comments per page", - "default": 10 + "default": 10, + "nullable": true }, "visitor": { "type": "boolean", "description": "Show visitor count", - "default": false + "default": false, + "nullable": true }, "highlight": { "type": "boolean", "description": "Enable code highlighting", - "default": true + "default": true, + "nullable": true }, "record_ip": { "type": "boolean", "description": "Record reviewer IP address", - "default": false + "default": false, + "nullable": true } }, "required": [ diff --git a/include/schema/common/article.json b/include/schema/common/article.json index 9918a57..140857a 100644 --- a/include/schema/common/article.json +++ b/include/schema/common/article.json @@ -11,12 +11,14 @@ "theme": { "type": "string", "description": "Code highlight themes\nhttps://github.com/highlightjs/highlight.js/tree/master/src/styles", - "default": "atom-one-light" + "default": "atom-one-light", + "nullable": true }, "clipboard": { - "type": "string", + "type": "boolean", "description": "Show copy code button", - "default": true + "default": true, + "nullable": true }, "fold": { "type": "string", @@ -26,19 +28,23 @@ "folded", "unfolded" ], - "default": "unfolded" + "default": "unfolded", + "nullable": true } - } + }, + "nullable": true }, "thumbnail": { "type": "boolean", "description": "Whether to show thumbnail image for every article", - "default": true + "default": true, + "nullable": true }, "readtime": { "type": "boolean", "description": "Whether to show estimated article reading time", - "default": true + "default": true, + "nullable": true } } } \ No newline at end of file diff --git a/include/schema/common/head.json b/include/schema/common/head.json index 5d961a9..da27ad9 100644 --- a/include/schema/common/head.json +++ b/include/schema/common/head.json @@ -7,11 +7,13 @@ "favicon": { "type": "string", "description": "URL or path to the website's icon", - "default": "/img/favicon.svg" + "default": "/img/favicon.svg", + "nullable": true }, "canonical_url": { "type": "string", - "description": "Canonical URL of the current page" + "description": "Canonical URL of the current page", + "nullable": true }, "open_graph": { "$ref": "/misc/open_graph.json" @@ -21,7 +23,8 @@ }, "rss": { "type": "string", - "description": "URL or path to the website's RSS atom.xml" + "description": "URL or path to the website's RSS atom.xml", + "nullable": true } } } \ No newline at end of file diff --git a/include/schema/common/navbar.json b/include/schema/common/navbar.json index 08db6ad..e4e5ab0 100644 --- a/include/schema/common/navbar.json +++ b/include/schema/common/navbar.json @@ -21,7 +21,8 @@ "Tags": "/tags", "About": "/about" } - ] + ], + "nullable": true }, "links": { "$ref": "/misc/poly_links.json", diff --git a/include/schema/common/providers.json b/include/schema/common/providers.json index f8191cc..398003c 100644 --- a/include/schema/common/providers.json +++ b/include/schema/common/providers.json @@ -7,17 +7,20 @@ "cdn": { "type": "string", "description": "Name or URL template of the JavaScript and/or stylesheet CDN provider", - "default": "jsdelivr" + "default": "jsdelivr", + "nullable": true }, "fontcdn": { "type": "string", "description": "Name or URL template of the webfont CDN provider", - "default": "google" + "default": "google", + "nullable": true }, "iconcdn": { "type": "string", "description": "Name or URL of the webfont Icon CDN provider", - "default": "fontawesome" + "default": "fontawesome", + "nullable": true } } } \ No newline at end of file diff --git a/include/schema/config.json b/include/schema/config.json index 625c095..e6268cd 100644 --- a/include/schema/config.json +++ b/include/schema/config.json @@ -6,7 +6,8 @@ "properties": { "version": { "type": "string", - "description": "Version of the configuration file" + "description": "Version of the configuration file", + "default": "3.0.0" }, "logo": { "type": [ diff --git a/include/schema/donate/buymeacoffee.json b/include/schema/donate/buymeacoffee.json index dfc19db..c51a0f7 100644 --- a/include/schema/donate/buymeacoffee.json +++ b/include/schema/donate/buymeacoffee.json @@ -6,7 +6,7 @@ "properties": { "type": { "type": "string", - "const": "patreon" + "const": "buymeacoffee" }, "url": { "type": "string", diff --git a/include/schema/donate/paypal.json b/include/schema/donate/paypal.json index 7801c30..d19a421 100644 --- a/include/schema/donate/paypal.json +++ b/include/schema/donate/paypal.json @@ -23,6 +23,6 @@ "required": [ "type", "business", - "currencyCode" + "currency_code" ] } \ No newline at end of file diff --git a/include/schema/misc/meta.json b/include/schema/misc/meta.json index fa83261..ba24a0d 100644 --- a/include/schema/misc/meta.json +++ b/include/schema/misc/meta.json @@ -6,5 +6,6 @@ "items": { "type": "string", "description": "Meta tag specified in = style\nE.g., name=theme-color;content=#123456 => " - } + }, + "nullable": true } \ No newline at end of file diff --git a/include/schema/misc/open_graph.json b/include/schema/misc/open_graph.json index d08142f..c508531 100644 --- a/include/schema/misc/open_graph.json +++ b/include/schema/misc/open_graph.json @@ -6,16 +6,19 @@ "properties": { "title": { "type": "string", - "description": "Page title (og:title)" + "description": "Page title (og:title)", + "nullable": true }, "type": { "type": "string", "description": "Page type (og:type)", - "default": "blog" + "default": "blog", + "nullable": true }, "url": { "type": "string", - "description": "Page URL (og:url)" + "description": "Page URL (og:url)", + "nullable": true }, "image": { "type": [ @@ -25,39 +28,49 @@ "description": "Page cover (og:image)", "items": { "type": "string" - } + }, + "nullable": true }, "site_name": { "type": "string", - "description": "Site name (og:site_name)" + "description": "Site name (og:site_name)", + "nullable": true }, "description": { "type": "string", - "description": "Page description (og:description)" + "description": "Page description (og:description)", + "nullable": true }, "twitter_card": { "type": "string", - "description": "Twitter card type (twitter:card)" + "description": "Twitter card type (twitter:card)", + "nullable": true }, "twitter_id": { "type": "string", - "description": "Twitter ID (twitter:creator)" + "description": "Twitter ID (twitter:creator)", + "nullable": true }, "twitter_site": { "type": "string", - "description": "Twitter ID (twitter:creator)" + "description": "Twitter ID (twitter:creator)", + "nullable": true }, "google_plus": { "type": "string", - "description": "Google+ profile link (deprecated)" + "description": "Google+ profile link (deprecated)", + "nullable": true }, "fb_admins": { "type": "string", - "description": "Facebook admin ID" + "description": "Facebook admin ID", + "nullable": true }, "fb_app_id": { "type": "string", - "description": "Facebook App ID" + "description": "Facebook App ID", + "nullable": true } - } + }, + "nullable": true } \ No newline at end of file diff --git a/include/schema/misc/poly_links.json b/include/schema/misc/poly_links.json index 4b260dc..0ad425a 100644 --- a/include/schema/misc/poly_links.json +++ b/include/schema/misc/poly_links.json @@ -36,5 +36,6 @@ "icon": "fab fa-github" } } - ] + ], + "nullable": true } \ No newline at end of file diff --git a/include/schema/plugin/baidu_analytics.json b/include/schema/plugin/baidu_analytics.json index 5073ed1..543577f 100644 --- a/include/schema/plugin/baidu_analytics.json +++ b/include/schema/plugin/baidu_analytics.json @@ -5,8 +5,9 @@ "type": "object", "properties": { "tracking_id": { - "type": "object", - "description": "Baidu Analytics tracking ID" + "type": "string", + "description": "Baidu Analytics tracking ID", + "nullable": true } }, "required": [ diff --git a/include/schema/plugin/google_analytics.json b/include/schema/plugin/google_analytics.json index 5cdd790..9846152 100644 --- a/include/schema/plugin/google_analytics.json +++ b/include/schema/plugin/google_analytics.json @@ -5,8 +5,9 @@ "type": "object", "properties": { "tracking_id": { - "type": "object", - "description": "Google Analytics tracking ID" + "type": "string", + "description": "Google Analytics tracking ID", + "nullable": true } }, "required": [ diff --git a/include/schema/plugin/hotjar.json b/include/schema/plugin/hotjar.json index 788c7b8..1ec6d0e 100644 --- a/include/schema/plugin/hotjar.json +++ b/include/schema/plugin/hotjar.json @@ -9,7 +9,11 @@ "string", "number" ], - "description": "Hotjar site id" + "description": "Hotjar site id", + "nullable": true } - } + }, + "required": [ + "site_id" + ] } \ No newline at end of file diff --git a/include/schema/widget/links.json b/include/schema/widget/links.json index f44e946..2088018 100644 --- a/include/schema/widget/links.json +++ b/include/schema/widget/links.json @@ -22,7 +22,8 @@ "Hexo": "https://hexo.io", "Bulma": "https://bulma.io" } - ] + ], + "nullable": true } }, "required": [ diff --git a/include/schema/widget/profile.json b/include/schema/widget/profile.json index 4251f92..6c4bb66 100644 --- a/include/schema/widget/profile.json +++ b/include/schema/widget/profile.json @@ -6,48 +6,56 @@ "properties": { "type": { "type": "string", - "const": "profile" + "const": "profile", + "nullable": true }, "author": { "type": "string", "description": "Author name", "examples": [ "Your name" - ] + ], + "nullable": true }, "author_title": { "type": "string", "description": "Author title", "examples": [ "Your title" - ] + ], + "nullable": true }, "location": { "type": "string", "description": "Author's current location", "examples": [ "Your location" - ] + ], + "nullable": true }, "avatar": { "type": "string", - "description": "URL or path to the avatar image" + "description": "URL or path to the avatar image", + "nullable": true }, "avatar_rounded": { "type": "boolean", "description": "Whether show the rounded avatar image", - "default": false + "default": false, + "nullable": true }, "gravatar": { "type": "string", - "description": "Email address for the Gravatar" + "description": "Email address for the Gravatar", + "nullable": true }, "follow_link": { "type": "string", "description": "URL or path for the follow button", "examples": [ "https://github.com/ppoffice" - ] + ], + "nullable": true }, "social_links": { "$ref": "/misc/poly_links.json", diff --git a/include/schema/widget/subscribe_email.json b/include/schema/widget/subscribe_email.json index 843098e..43b2c8a 100644 --- a/include/schema/widget/subscribe_email.json +++ b/include/schema/widget/subscribe_email.json @@ -10,7 +10,8 @@ }, "description": { "type": "string", - "description": "Hint text under the email input" + "description": "Hint text under the email input", + "nullable": true }, "feedburner_id": { "type": "string", diff --git a/include/task/check_config.js b/include/task/check_config.js deleted file mode 100644 index 79d1f78..0000000 --- a/include/task/check_config.js +++ /dev/null @@ -1,46 +0,0 @@ -const fs = require('fs'); -const util = require('util'); -const path = require('path'); -const logger = require('hexo-log')(); -const yaml = require('js-yaml'); - -const { errors } = require('../common/utils'); -const rootSpec = require('../specs/config.spec'); -const ConfigValidator = require('../common/ConfigValidator'); -const ConfigGenerator = require('../common/ConfigGenerator'); - -const CONFIG_PATH = path.join(__dirname, '../..', '_config.yml'); - -logger.info('Validating the configuration file'); - -if (!fs.existsSync(CONFIG_PATH)) { - const relativePath = path.relative(process.cwd(), CONFIG_PATH); - logger.warn(`${relativePath} is not found. We are creating one for you...`); - fs.writeFileSync(CONFIG_PATH, new ConfigGenerator(rootSpec).generate()); - logger.info(`${relativePath} is created. Please restart Hexo to apply changes.`); - process.exit(0); -} - -const validator = new ConfigValidator(rootSpec); -const config = yaml.safeLoad(fs.readFileSync(CONFIG_PATH)); -try { - validator.validate(config); -} catch (e) { - if (e instanceof errors.ConfigError) { - logger.error(e.message); - if (e.hasOwnProperty('spec')) { - logger.error('The specification of this configuration is:'); - logger.error(util.inspect(e.spec)); - } - if (e.hasOwnProperty('config')) { - logger.error('Configuration value is:'); - logger.error(util.inspect(e.config)); - } - } else if (e instanceof errors.VersionError) { - logger.error(e.message); - logger.warn('To let us create a fresh configuration file for you, please rename or delete the following file:'); - logger.warn(CONFIG_PATH); - } else { - throw e; - } -} diff --git a/include/task/dependencies.js b/include/task/dependencies.js deleted file mode 100644 index 46ac44a..0000000 --- a/include/task/dependencies.js +++ /dev/null @@ -1,23 +0,0 @@ -const logger = require('hexo-log')(); -const packageInfo = require('../../package.json'); - -// FIXME: will not check against package version -function checkDependency(name) { - try { - require.resolve(name); - return true; - } catch (e) { - logger.error(`Package ${name} is not installed.`); - } - return false; -} - -logger.info('Checking if required dependencies are installed...'); -const missingDeps = Object.keys(packageInfo.peerDependencies) - .map(checkDependency) - .some(installed => !installed); -if (missingDeps) { - logger.error('Please install the missing dependencies from the root directory of your Hexo site.'); - /* eslint no-process-exit: "off" */ - process.exit(-1); -} diff --git a/include/task/welcome.js b/include/task/welcome.js deleted file mode 100644 index ea4be5b..0000000 --- a/include/task/welcome.js +++ /dev/null @@ -1,10 +0,0 @@ -const logger = require('hexo-log')(); - -logger.info(`======================================= - ██╗ ██████╗ █████╗ ██████╗ ██╗ ██╗███████╗ - ██║██╔════╝██╔══██╗██╔══██╗██║ ██║██╔════╝ - ██║██║ ███████║██████╔╝██║ ██║███████╗ - ██║██║ ██╔══██║██╔══██╗██║ ██║╚════██║ - ██║╚██████╗██║ ██║██║ ██║╚██████╔╝███████║ - ╚═╝ ╚═════╝╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝ -=============================================`); diff --git a/include/util/migrate.js b/include/util/migrate.js new file mode 100644 index 0000000..d19e6a6 --- /dev/null +++ b/include/util/migrate.js @@ -0,0 +1,110 @@ +const path = require('path'); +const logger = require('hexo-log')(); + +class Version { + constructor(version) { + const ver = version.split('.').map(i => parseInt(i, 10)); + if (ver.length !== 3) { + throw new Error('Malformed version number ' + version); + } + this.major = ver[0]; + this.minor = ver[1]; + this.patch = ver[2]; + } + + toString() { + return `${this.major}.${this.minor}.${this.patch}`; + } +} + +Version.compare = function(a, b) { + if (!(a instanceof Version) || !(b instanceof Version)) { + throw new Error('Cannot compare non-Versions'); + } + if (a.major !== b.major) { + return a.major - b.major; + } + if (a.minor !== b.minor) { + return a.minor - b.minor; + } + if (a.patch !== b.patch) { + return a.patch - b.patch; + } + return 0; +}; + +class Migration { + + /** + * @param {string} version Target version + * @param {string} head File name of the previous migration + */ + constructor(version, head) { + this.version = new Version(version); + this.head = head; + } + + doMigrate(config) { + throw new Error('Not implemented!'); + } + + migrate(config) { + logger.info(`Updating configurations from ${config.version} to ${this.version.toString()}...`); + const result = this.doMigrate(config); + result.version = this.version.toString(); + return result; + } +} + + +class Migrator { + constructor(root) { + this.versions = []; + this.migrations = {}; + + let head = 'head'; + while (head) { + const migration = new(require(path.join(root, head)))(); + if (!(migration instanceof Migration)) { + throw new Error(`Migration ${head} is not a Migration class.`); + } + this.versions.push(migration.version); + this.migrations[migration.version.toString()] = migration; + head = migration.head; + } + + this.versions.sort(Version.compare); + } + + isOudated(version) { + if (!this.versions.length) { + return false; + } + return Version.compare(new Version(version), this.getLatestVersion()) < 0; + } + + getLatestVersion() { + if (!this.versions.length) { + return null; + } + return this.versions[this.versions.length - 1]; + } + + migrate(config, toVersion = null) { + const fVer = new Version(config.version); + const tVer = toVersion ? new Version(toVersion) : this.getLatestVersion(); + // find all migrations whose version is larger than fromVer, smaller or equal to toVer + // and run migrations on the config one by one + return this.versions.filter(ver => Version.compare(ver, fVer) > 0 && Version.compare(ver, tVer) <= 0) + .sort(Version.compare) + .reduce((cfg, ver) => { + const migration = this.migrations[ver.toString()]; + return migration.migrate(cfg); + }, config); + } +} + +Migrator.Version = Version; +Migrator.Migration = Migration; + +module.exports = Migrator; diff --git a/include/util/schema.js b/include/util/schema.js index 1641cb2..8f25557 100644 --- a/include/util/schema.js +++ b/include/util/schema.js @@ -1,6 +1,9 @@ const Ajv = require('ajv'); const path = require('path'); const deepmerge = require('deepmerge'); +const yaml = require('./yaml'); + +const MAGIC = 'c823d4d4'; const PRIMITIVE_DEFAULTS = { 'null': null, @@ -23,16 +26,84 @@ class DefaultValue { this.description = source.description; } if ('value' in source && source.value) { - this.value = deepmerge(this.value, source.value); + if (this.value instanceof DefaultValue) { + this.value.merge(source.value); + } else if (Array.isArray(this.value) && Array.isArray(source.value)) { + this.value.concat(...source.value); + } else if (typeof this.value === 'object' && typeof source.value === 'object') { + for (const key in source.value) { + this.value[key] = source.value[key]; + } + } else { + this.value = deepmerge(this.value, source.value); + } } return this; } - toString() { - return '[DefaultValue]' + JSON.stringify(value); + clone() { + const result = new DefaultValue(this.value, this.description); + if (result.value instanceof DefaultValue) { + result.value = result.value.clone(); + } else if (Array.isArray(result.value)) { + result.value = [].concat(...result.value); + } else if (typeof result.value === 'object') { + result.value = Object.assign({}, result.value); + } + return result; + } + + toCommentedArray() { + return [].concat(...this.value.map(item => { + if (item instanceof DefaultValue) { + if (typeof item.description !== 'string' || !item.description.trim()) { + return [item.toCommented()]; + } + return item.description.split('\n').map((line, i) => { + return MAGIC + i + ': ' + line; + }).concat(item.toCommented()); + } + return [item]; + })); + } + + toCommentedObject() { + if (this.value instanceof DefaultValue) { + return this.value.toCommented(); + } + const result = {}; + for (const key in this.value) { + const item = this.value[key]; + if (item instanceof DefaultValue) { + if (typeof item.description === 'string' && item.description.trim()) { + item.description.split('\n').forEach((line, i) => { + result[MAGIC + key + i] = line; + }); + } + result[key] = item.toCommented(); + } else { + result[key] = item; + } + } + return result; + } + + toCommented() { + if (Array.isArray(this.value)) { + return this.toCommentedArray(); + } else if (typeof this.value === 'object' && this.value !== null) { + return this.toCommentedObject(); + } + return this.value; + } + + toYaml() { + const regex = new RegExp('^(\\s*)(?:-\\s*\\\')?' + MAGIC + '.*?:\\s*\\\'?(.*?)\\\'*$', 'mg'); + return yaml.stringify(this.toCommented()).replace(regex, '$1# $2');// restore comments } } +/* eslint-disable no-use-before-define */ class Schema { constructor(loader, def) { if (!(loader instanceof SchemaLoader)) { @@ -55,37 +126,40 @@ class Schema { getArrayDefaultValue(def) { let value; + const defaultValue = new DefaultValue(null, def.description); if ('items' in def && typeof def.items === 'object') { const items = Object.assign({}, def.items); delete items.oneOf; value = this.getDefaultValue(items); } if ('oneOf' in def.items && Array.isArray(def.items.oneOf)) { - value = def.items.oneOf.map(one => { - if (!value) { + defaultValue.value = def.items.oneOf.map(one => { + if (!(value instanceof DefaultValue)) { return this.getDefaultValue(one); } - return new DefaultValue(value.value, value.description) - .merge(this.getDefaultValue(one)); + return value.clone().merge(this.getDefaultValue(one)); }); + } else { + if (!Array.isArray(value)) { + value = [value]; + } + defaultValue.value = value; } - if (!Array.isArray(value)) { - value = [value]; - } - return new DefaultValue(value, def.description); + return defaultValue; } getObjectDefaultValue(def) { - let value = {}; + const value = {}; if ('properties' in def && typeof def.properties === 'object') { - for (let property in def.properties) { + for (const property in def.properties) { value[property] = this.getDefaultValue(def.properties[property]); } } + const defaultValue = new DefaultValue(value, def.description); if ('oneOf' in def && Array.isArray(def.oneOf) && def.oneOf.length) { - value = deepmerge(value, this.getDefaultValue(def.oneOf[0])); + return defaultValue.merge(this.getDefaultValue(def.oneOf[0])); } - return new DefaultValue(value, def.description); + return defaultValue; } getTypedDefaultValue(def) { @@ -96,9 +170,13 @@ class Schema { } else if (type === 'object') { defaultValue = this.getObjectDefaultValue(def); } else if (type in PRIMITIVE_DEFAULTS) { - defaultValue = new DefaultValue(PRIMITIVE_DEFAULTS[type], def.description); + if ('nullable' in def && def.nullable) { + defaultValue = new DefaultValue(null, def.description); + } else { + defaultValue = new DefaultValue(PRIMITIVE_DEFAULTS[type], def.description); + } } else { - throw new Error(`Cannot get default value for type ${type}`) + throw new Error(`Cannot get default value for type ${type}`); } // referred default value always get overwritten by its parent default value if ('$ref' in def && def.$ref) { @@ -120,10 +198,10 @@ class Schema { def = this.def; } if ('const' in def) { - return new DefaultValue(def['const'], def.description); + return new DefaultValue(def.const, def.description); } if ('default' in def) { - return new DefaultValue(def['default'], def.description); + return new DefaultValue(def.default, def.description); } if ('examples' in def && Array.isArray(def.examples) && def.examples.length) { return new DefaultValue(def.examples[0], def.description); @@ -141,7 +219,7 @@ class Schema { class SchemaLoader { constructor() { this.schemas = {}; - this.ajv = new Ajv(); + this.ajv = new Ajv({ nullable: true }); } getSchema($id) { @@ -153,11 +231,11 @@ class SchemaLoader { throw new Error('The schema definition does not have an $id field'); } this.ajv.addSchema(def); - this.schemas[def['$id']] = new Schema(this, def); + this.schemas[def.$id] = new Schema(this, def); } removeSchema($id) { - this.ajv.removeSchema(def); + this.ajv.removeSchema($id); delete this.schemas[$id]; } @@ -204,7 +282,7 @@ SchemaLoader.load = (rootSchemaDef, resolveDirs = []) => { } catch (e) { continue; } - if (typeof def !== 'object' || def['$id'] !== $ref) { + if (typeof def !== 'object' || def.$id !== $ref) { continue; } loader.addSchema(def); @@ -217,6 +295,10 @@ SchemaLoader.load = (rootSchemaDef, resolveDirs = []) => { traverseObj(rootSchemaDef, '$ref', handler); return loader; -} +}; -module.exports = SchemaLoader; \ No newline at end of file +module.exports = { + Schema, + SchemaLoader, + DefaultValue +}; diff --git a/include/util/yaml.js b/include/util/yaml.js new file mode 100644 index 0000000..8a7050c --- /dev/null +++ b/include/util/yaml.js @@ -0,0 +1,43 @@ +const yaml = require('js-yaml'); +const YamlType = require('js-yaml/lib/js-yaml/type'); +const YamlSchema = require('js-yaml/lib/js-yaml/schema'); + +// output null as empty in yaml +const YAML_SCHEMA = new YamlSchema({ + include: [ + require('js-yaml/lib/js-yaml/schema/default_full') + ], + implicit: [ + new YamlType('tag:yaml.org,2002:null', { + kind: 'scalar', + resolve(data) { + if (data === null) { + return true; + } + const max = data.length; + return (max === 1 && data === '~') + || (max === 4 && (data === 'null' || data === 'Null' || data === 'NULL')); + }, + construct: () => null, + predicate: object => object === null, + represent: { + empty: () => '' + }, + defaultStyle: 'empty' + }) + ] +}); + +module.exports = { + parse(str) { + return yaml.safeLoad(str); + }, + + stringify(object) { + return yaml.safeDump(object, { + indent: 4, + lineWidth: 1024, + schema: YAML_SCHEMA + }); + } +}; diff --git a/package.json b/package.json index 4b76a08..1d51b8c 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "hexo-log": "^1.0.0", "hexo-pagination": "^1.0.0", "hexo-renderer-inferno": "^0.1.1", + "hexo-renderer-stylus": "^1.1.0", "hexo-util": "^1.8.0", "inferno": "^7.3.3", "inferno-create-element": "^7.3.3", diff --git a/scripts/index.js b/scripts/index.js index cba979d..3e1ac5b 100644 --- a/scripts/index.js +++ b/scripts/index.js @@ -1,7 +1,119 @@ +const fs = require('fs'); +const path = require('path'); +const util = require('util'); +const crypto = require('crypto'); +const logger = require('hexo-log')(); +const packageInfo = require('../package.json'); + +/** + * Print welcome message + */ +logger.info(`======================================= + ██╗ ██████╗ █████╗ ██████╗ ██╗ ██╗███████╗ + ██║██╔════╝██╔══██╗██╔══██╗██║ ██║██╔════╝ + ██║██║ ███████║██████╔╝██║ ██║███████╗ + ██║██║ ██╔══██║██╔══██╗██║ ██║╚════██║ + ██║╚██████╗██║ ██║██║ ██║╚██████╔╝███████║ + ╚═╝ ╚═════╝╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝ +=============================================`); + +/** + * Check if all dependencies are installed + */ +// FIXME: will not check against package version +function checkDependency(name) { + try { + require.resolve(name); + return true; + } catch (e) { + logger.error(`Package ${name} is not installed.`); + } + return false; +} + +logger.info('=== Checking package dependencies ==='); +const missingDeps = Object.keys(packageInfo.peerDependencies) + .map(checkDependency) + .some(installed => !installed); +if (missingDeps) { + logger.error('Please install the missing dependencies from the root directory of your Hexo site.'); + /* eslint no-process-exit: "off" */ + process.exit(-1); +} + +/** + * Configuration file checking and migration + */ +if (!process.argv.includes('--icarus-dont-check-config')) { + const SCHEMA_ROOT = path.join(hexo.theme_dir, 'include/schema/'); + const CONFIG_PATH = path.join(hexo.theme_dir, '_config.yml'); + + const yaml = require('../include/util/yaml'); + const { SchemaLoader } = require('../include/util/schema'); + const loader = SchemaLoader.load(require(path.join(SCHEMA_ROOT, 'config.json')), SCHEMA_ROOT); + const schema = loader.getSchema('/config.json'); + logger.info('=== Checking the configuration file ==='); + + // Generate config file if not exist + if (!process.argv.includes('--icarus-dont-generate-config')) { + if (!fs.existsSync(CONFIG_PATH)) { + logger.warn(`${CONFIG_PATH} is not found. We are creating one for you...`); + logger.info('You may add \'--icarus-dont-generate-config\' to prevent creating the configuration file.'); + const defaultValue = schema.getDefaultValue(); + fs.writeFileSync(CONFIG_PATH, defaultValue.toYaml()); + logger.info(`${CONFIG_PATH} created successfully.`); + } + } + + try { + const cfgStr = fs.readFileSync(CONFIG_PATH); + let cfg = yaml.parse(cfgStr); + // Check config version + if (!process.argv.includes('--icarus-dont-upgrade-config')) { + const migrator = new(require('../include/util/migrate'))(path.join(hexo.theme_dir, 'include/migration')); + // Upgrade config + if (migrator.isOudated(cfg.version)) { + logger.info(`Your configuration file is outdated (${cfg.version} < ${migrator.getLatestVersion()}). ` + + 'Trying to upgrade it...'); + // Backup old config + const hash = crypto.createHash('sha256').update(cfgStr).digest('hex'); + const backupPath = CONFIG_PATH + '.' + hash.substring(0, 16); + fs.writeFileSync(backupPath, cfgStr); + logger.info(`Current configurations are written up to ${backupPath}`); + // Migrate config + cfg = migrator.migrate(cfg); + // Save config + fs.writeFileSync(CONFIG_PATH, yaml.stringify(cfg)); + logger.info(`${CONFIG_PATH} upgraded successfully.`); + const defaultValue = schema.getDefaultValue(); + fs.writeFileSync(CONFIG_PATH + '.example', defaultValue.toYaml()); + logger.info(`We also created an example at ${CONFIG_PATH + '.example'} for your reference.`); + } + } + + // Check config file against schemas + const result = schema.validate(cfg); + if (result !== true) { + logger.warn('Configuration file failed one or more checks.'); + logger.warn('Icarus may still run, but you will encounter excepted results.'); + logger.warn('Here is some information for you to correct the configuration file.'); + logger.warn(util.inspect(result)); + } + } catch (e) { + logger.error(e); + logger.error(`Failed to load the configuration file ${CONFIG_PATH}.`); + logger.error('Please add \'--icarus-dont-check-config\' to your Hexo command if you'); + logger.error('wish to skip the config file checking.'); + process.exit(-1); + } +} + + +/** + * Register Hexo extensions + */ +logger.info('=== Patching Hexo ==='); /* global hexo */ -require('../include/task/welcome'); -require('../include/task/dependencies'); -// require('../include/task/check_config'); require('../include/generator/categories')(hexo); require('../include/generator/category')(hexo); require('../include/generator/tags')(hexo); @@ -10,7 +122,9 @@ require('../include/filter/locals')(hexo); require('../include/helper/cdn')(hexo); require('../include/helper/page')(hexo); -// Fix large blog rendering OOM +/** + * Remove Hexo filters that could cause OOM + */ const hooks = [ 'after_render:html', 'after_post_render' @@ -24,8 +138,3 @@ hooks.forEach(hook => { .filter(filter => filters.includes(filter.name)) .forEach(filter => hexo.extend.filter.unregister(hook, filter)); }); - -// Debug helper -hexo.extend.helper.register('console', function() { - console.log(arguments); -});