From c56a3c46c436a9c98a088d28076dba95f544c4a2 Mon Sep 17 00:00:00 2001 From: ppoffice Date: Fri, 19 Oct 2018 00:53:09 -0400 Subject: [PATCH] feat(script): add config checking specs --- _config.yml.example | 95 ------- includes/helpers/cdn.js | 24 +- includes/helpers/config.js | 19 +- includes/helpers/layout.js | 8 +- includes/specs/_config.yml.js | 473 +++++++++++++++++++++++++++++++++ includes/specs/common.js | 24 ++ includes/tasks/check_config.js | 260 +++++++++++++++++- includes/tasks/check_deps.js | 12 +- layout/comment/valine.ejs | 6 +- layout/common/footer.ejs | 2 +- layout/common/head.ejs | 12 +- layout/common/navbar.ejs | 2 +- layout/widget/recent_posts.ejs | 2 +- package.json | 2 +- 14 files changed, 806 insertions(+), 135 deletions(-) delete mode 100644 _config.yml.example create mode 100644 includes/specs/_config.yml.js create mode 100644 includes/specs/common.js diff --git a/_config.yml.example b/_config.yml.example deleted file mode 100644 index a01feb6..0000000 --- a/_config.yml.example +++ /dev/null @@ -1,95 +0,0 @@ -# Website's icon url. -favicon: /favicon.png - -# Open Graph metadata (https://hexo.io/docs/helpers.html#open-graph) -open_graph: - fb_app_id: - fb_admins: - twitter_id: - google_plus: - -rss: - -# Website's logo shown on the left of the navigation bar. -logo: - -# Navigation bar menu links. -menu: - Home: / - Archives: /archives - Categories: /categories - Tags: /tags - About: /about - -article: - highlight: atom-one-light - thumbnail: true - -search: - type: insight - -comment: - type: disqus - shortname: hexo-theme-icarus - -share: - type: sharethis - install_url: - -widgets: - - type: profile - position: left - author: PPOffice - author_title: Web Developer - location: Earth, Solar System - avatar: - gravatar: - follow_link: http://github.com/ppoffice - social_links: - Github: - icon: fab fa-github - url: http://github.com/ppoffice - Facebook: - icon: fab fa-facebook - url: http://facebook.com - Twitter: - icon: fab fa-twitter - url: http://twitter.com - Dribbble: - icon: fab fa-dribbble - url: http://dribbble.com - RSS: - icon: fas fa-rss - url: / - - type: toc - position: left - - type: links - position: left - links: - Hexo: https://hexo.io - Github: https://github.com/ppoffice - - type: category - position: left - - type: tagcloud - position: left - - type: recent_posts - position: right - - type: archive - position: right - - type: tag - position: right - -plugins: - gallery: true - outdated-browser: true - animejs: true - mathjax: true - google-analytics: - tracking_id: - baidu-analytics: - tracking_id: - -providers: - cdn: - fontcdn: - iconcdn: \ No newline at end of file diff --git a/includes/helpers/cdn.js b/includes/helpers/cdn.js index c45e701..14f1b61 100644 --- a/includes/helpers/cdn.js +++ b/includes/helpers/cdn.js @@ -1,6 +1,6 @@ /** * CDN static file resolvers. - * + * * @example * <%- cdn(package, version, filename) %> * <%- fontcdn(fontName) %> @@ -23,9 +23,9 @@ const icon_providers = { }; module.exports = function (hexo) { - hexo.extend.helper.register('cdn', function (package, version, filename, provider = 'cdnjs') { - provider = hexo.extend.helper.get('get_config').bind(this)('providers.cdn', provider); - if (provider != null && cdn_providers.hasOwnProperty(provider)) { + hexo.extend.helper.register('cdn', function (package, version, filename) { + let provider = hexo.extend.helper.get('get_config').bind(this)('providers.cdn'); + if (provider !== null && cdn_providers.hasOwnProperty(provider)) { provider = cdn_providers[provider]; } return provider.replace(/\${\s*package\s*}/gi, package) @@ -33,18 +33,22 @@ module.exports = function (hexo) { .replace(/\${\s*filename\s*}/gi, filename); }); - hexo.extend.helper.register('fontcdn', function (fontName, provider = 'google') { - provider = hexo.extend.helper.get('get_config').bind(this)('providers.font', provider); - if (provider != null && font_providers.hasOwnProperty(provider)) { + hexo.extend.helper.register('fontcdn', function (fontName) { + let provider = hexo.extend.helper.get('get_config').bind(this)('providers.fontcdn'); + if (provider !== null && font_providers.hasOwnProperty(provider)) { provider = font_providers[provider]; } return provider.replace(/\${\s*fontname\s*}/gi, fontName); }); - hexo.extend.helper.register('iconcdn', function (provider = 'fontawesome') { - provider = hexo.extend.helper.get('get_config').bind(this)('providers.icon', provider); - if (provider != null && icon_providers.hasOwnProperty(provider)) { + hexo.extend.helper.register('iconcdn', function (provider = null) { + if (provider !== null && icon_providers.hasOwnProperty(provider)) { provider = icon_providers[provider]; + } else { + provider = hexo.extend.helper.get('get_config').bind(this)('providers.iconcdn'); + if (provider !== null && icon_providers.hasOwnProperty(provider)) { + provider = icon_providers[provider]; + } } return provider; }); diff --git a/includes/helpers/config.js b/includes/helpers/config.js index 558edda..c0f1839 100644 --- a/includes/helpers/config.js +++ b/includes/helpers/config.js @@ -1,12 +1,15 @@ /** * Theme configuration helpers. - * + * * @description Test if a configuration is set or fetch its value. If `exclude_page` is set, the helpers will * not look up configurations in the current page's front matter. * @example * <%- has_config(config_name, exclude_page) %> * <%- get_config(config_name, default_value, exclude_page) %> */ +const specs = require('../specs/_config.yml'); +const descriptors = require('../specs/common').descriptor; + module.exports = function (hexo) { function readProperty(object, path) { const paths = path.split('.'); @@ -19,10 +22,18 @@ module.exports = function (hexo) { return object; } - hexo.extend.helper.register('get_config', function (configName, defaultValue = null, excludePage = false) { + hexo.extend.helper.register('get_config', function (configName, defaultValue = undefined, excludePage = false) { const value = readProperty(Object.assign({}, this.config, hexo.theme.config, !excludePage ? this.page : {}), configName); - return value == null ? defaultValue : value; + if (value === null) { + if (typeof(defaultValue) !== 'undefined') { + return defaultValue; + } else { + const property = readProperty(specs, configName); + return property === null ? null : property[descriptors.defaultValue]; + } + } + return value; }); hexo.extend.helper.register('has_config', function (configName, excludePage = false) { @@ -32,6 +43,6 @@ module.exports = function (hexo) { hexo.extend.helper.register('get_config_from_obj', function (object, configName, defaultValue = null) { const value = readProperty(object, configName); - return value == null ? defaultValue : value; + return value === null ? defaultValue : value; }); } \ No newline at end of file diff --git a/includes/helpers/layout.js b/includes/helpers/layout.js index e92df26..226cc01 100644 --- a/includes/helpers/layout.js +++ b/includes/helpers/layout.js @@ -1,6 +1,6 @@ /** * Helper functions for controlling layout. - * + * * @example * <%- get_widgets(position) %> * <%- has_column() %> @@ -8,7 +8,11 @@ */ module.exports = function (hexo) { hexo.extend.helper.register('get_widgets', function (position) { - const widgets = hexo.extend.helper.get('get_config').bind(this)('widgets', []); + const hasWidgets = hexo.extend.helper.get('has_config').bind(this)('widgets'); + if (!hasWidgets) { + return []; + } + const widgets = hexo.extend.helper.get('get_config').bind(this)('widgets'); return widgets.filter(widget => widget.hasOwnProperty('position') && widget.position === position); }); diff --git a/includes/specs/_config.yml.js b/includes/specs/_config.yml.js new file mode 100644 index 0000000..421d11e --- /dev/null +++ b/includes/specs/_config.yml.js @@ -0,0 +1,473 @@ +const { version } = require('../../package.json'); +const { type, required, defaultValue, description, condition } = require('./common').descriptor; +const desc = description; + +const IconLink = { + [type]: 'object', + [desc]: 'Link icon settings', + '*': { + [type]: ['string', 'object'], + [desc]: 'Path or URL to the menu item, and/or link icon class names', + icon: { + [required]: true, + [type]: 'string', + [desc]: 'Link icon class names' + }, + url: { + [required]: true, + [type]: 'string', + [desc]: 'Path or URL to the menu item' + } + } +}; + +const DEFAULT_WIDGETS = [ + { + type: 'profile', + position: 'left', + author: 'Your name', + author_title: 'Your title', + location: 'Your location', + avatar: null, + gravatar: null, + follow_link: 'http://github.com/ppoffice', + social_links: { + Github: { + icon: 'fab fa-github', + url: 'http://github.com/ppoffice' + }, + Facebook: { + icon: 'fab fa-facebook', + url: 'http://facebook.com' + }, + Twitter: { + icon: 'fab fa-twitter', + url: 'http://twitter.com' + }, + Dribbble: { + icon: 'fab fa-dribbble', + url: 'http://dribbble.com' + }, + RSS: { + icon: 'fas fa-rss', + url: '/' + } + } + }, + { + type: 'toc', + position: 'left' + }, + { + type: 'links', + position: 'left', + links: { + Hexo: 'https://hexo.io', + Github: 'https://github.com/ppoffice' + } + }, + { + type: 'category', + position: 'left' + }, + { + type: 'tagcloud', + position: 'left' + }, + { + type: 'recent_posts', + position: 'right' + }, + { + type: 'archive', + position: 'right' + }, + { + type: 'tag', + position: 'right' + } +]; + +module.exports = { + [type]: 'object', + [desc]: 'Root of the configuration file', + [required]: true, + version: { + [type]: 'string', + [desc]: 'Version of the Icarus theme that is currently used', + [required]: true, + [defaultValue]: version + }, + favicon: { + [type]: 'string', + [desc]: 'Path or URL to the website\'s icon', + [defaultValue]: null + }, + open_graph: { + [type]: 'object', + [desc]: 'Open Graph metadata (https://hexo.io/docs/helpers.html#open-graph)', + fb_app_id: { + [type]: 'string', + [desc]: 'Facebook App ID', + [defaultValue]: null + }, + fb_admins: { + [type]: 'string', + [desc]: 'Facebook Admin ID', + [defaultValue]: null + }, + twitter_id: { + [type]: 'string', + [desc]: 'Twitter ID', + [defaultValue]: null + }, + twitter_site: { + [type]: 'string', + [desc]: 'Twitter site', + [defaultValue]: null + }, + google_plus: { + [type]: 'string', + [desc]: 'Google+ profile link', + [defaultValue]: null + } + }, + rss: { + [type]: 'string', + [desc]: 'Path or URL to RSS atom.xml', + [defaultValue]: null + }, + logo: { + [type]: ['string', 'object'], + [defaultValue]: '/images/logo.svg', + [desc]: 'Path or URL to the website\'s logo to be shown on the left of the navigation bar or footer', + text: { + [type]: 'string', + [desc]: 'Text to be shown in place of the logo image' + } + }, + navbar: { + [type]: 'object', + [desc]: 'Navigation bar link settings', + menu: { + [type]: 'object', + [desc]: 'Navigation bar menu links', + [defaultValue]: { + Home: '/', + Archives: '/archives', + Categories: '/categories', + Tags: '/tags', + About: '/about' + }, + '*': { + [type]: 'string', + [desc]: 'Path or URL to the menu item' + } + }, + links: { + ...IconLink, + [desc]: 'Navigation bar links to be shown on the right', + [defaultValue]: { + 'Download on GitHub': { + icon: 'fab fa-github', + url: 'http://github.com/ppoffice/hexo-theme-icarus' + } + } + } + }, + footer: { + [type]: 'object', + [description]: 'Footer section link settings', + links: { + ...IconLink, + [desc]: 'Links to be shown on the right of the footer section', + [defaultValue]: { + 'Creative Commons': { + icon: 'fab fa-creative-commons', + url: 'https://creativecommons.org/' + }, + 'Attribution 4.0 International': { + icon: 'fab fa-creative-commons-by', + url: 'https://creativecommons.org/licenses/by/4.0/' + }, + 'Download on GitHub': { + icon: 'fab fa-github', + url: 'http://github.com/ppoffice/hexo-theme-icarus' + } + } + } + }, + article: { + [type]: 'object', + [desc]: 'Article display settings', + highlight: { + [type]: 'string', + [desc]: 'Code highlight theme (https://github.com/highlightjs/highlight.js/tree/master/src/styles)', + [defaultValue]: 'atom-one-light' + }, + thumbnail: { + [type]: 'boolean', + [desc]: 'Whether to show article thumbnail images', + [defaultValue]: true + }, + readtime: { + [type]: 'boolean', + [desc]: 'Whether to show estimate article reading time', + [defaultValue]: true + } + }, + search: { + [type]: 'object', + [desc]: 'Search plugin settings (http://ppoffice.github.io/hexo-theme-icarus/categories/Configuration/Search-Plugins)', + type: { + [type]: 'string', + [desc]: 'Name of the search plugin', + [defaultValue]: 'insight' + }, + cx: { + [type]: 'string', + [desc]: 'Google CSE cx value', + [required]: true, + [condition]: parent => parent.type === 'google-cse' + } + }, + comment: { + [type]: 'object', + [desc]: 'Comment plugin settings (http://ppoffice.github.io/hexo-theme-icarus/categories/Configuration/Comment-Plugins)', + type: { + [type]: 'string', + [desc]: 'Name of the comment plugin', + [defaultValue]: null + }, + appid: { + [type]: 'string', + [desc]: 'Changyan comment app ID', + [required]: true, + [condition]: parent => parent.type === 'changyan' + }, + conf: { + [type]: 'string', + [desc]: 'Changyan comment configuration ID', + [required]: true, + [condition]: parent => parent.type === 'changyan' + }, + shortname: { + [type]: 'string', + [desc]: 'Disqus shortname', + [required]: true, + [condition]: parent => parent.type === 'disqus' + }, + owner: { + [type]: 'string', + [desc]: 'Your GitHub ID', + [required]: true, + [condition]: parent => parent.type === 'gitment' + }, + repo: { + [type]: 'string', + [desc]: 'The repo to store comments', + [required]: true, + [condition]: parent => parent.type === 'gitment' + }, + client_id: { + [type]: 'string', + [desc]: 'Your client ID', + [required]: true, + [condition]: parent => parent.type === 'gitment' + }, + client_secret: { + [type]: 'string', + [desc]: 'Your client secret', + [required]: true, + [condition]: parent => parent.type === 'gitment' + }, + url: { + [type]: 'string', + [desc]: 'URL to your Isso comment service', + [required]: true, + [condition]: parent => parent.type === 'isso' + }, + uid: { + [type]: 'string', + [desc]: 'LiveRe comment service UID', + [required]: true, + [condition]: parent => parent.type === 'livere' + }, + app_id: { + [type]: 'boolean', + [desc]: 'LeanCloud APP ID', + [required]: true, + [condition]: parent => parent.type === 'valine' + }, + app_key: { + [type]: 'boolean', + [desc]: 'LeanCloud APP key', + [required]: true, + [condition]: parent => parent.type === 'valine' + }, + notify: { + [type]: 'boolean', + [desc]: 'Enable email notification when someone comments', + [defaultValue]: false, + [condition]: parent => parent.type === 'valine' + }, + verify: { + [type]: 'boolean', + [desc]: 'Enable verification code service', + [defaultValue]: false, + [condition]: parent => parent.type === 'valine' + }, + placeholder: { + [type]: 'boolean', + [desc]: 'Placeholder text in the comment box', + [defaultValue]: 'Say something...', + [condition]: parent => parent.type === 'valine' + } + }, + share: { + [type]: 'object', + [desc]: 'Share plugin settings (http://ppoffice.github.io/hexo-theme-icarus/categories/Configuration/Share-Plugins)', + type: { + [type]: 'string', + [desc]: 'Share plugin name', + [defaultValue]: null + }, + install_url: { + [type]: 'string', + [desc]: 'URL to the share plugin script provided by share plugin service provider', + [required]: true, + [condition]: parent => parent.type === 'sharethis' || parent.type === 'addthis' + } + }, + widgets: { + [type]: 'array', + [desc]: 'Sidebar widget settings', + [defaultValue]: DEFAULT_WIDGETS, + '*': { + [type]: 'object', + [desc]: 'Single widget settings', + type: { + [type]: 'string', + [desc]: 'Widget name', + [required]: true, + [defaultValue]: 'profile' + }, + position: { + [type]: 'string', + [desc]: 'Where should the widget be placed, left or right', + [required]: true, + [defaultValue]: 'left' + }, + author: { + [type]: 'string', + [desc]: 'Author name to be shown in the profile widget', + [condition]: parent => parent.type === 'profile', + [defaultValue]: 'Your name' + }, + author_title: { + [type]: 'string', + [desc]: 'Title of the author to be shown in the profile widget', + [condition]: parent => parent.type === 'profile', + [defaultValue]: 'Your title' + }, + location: { + [type]: 'string', + [desc]: 'Author\'s current location to be shown in the profile widget', + [condition]: parent => parent.type === 'profile', + [defaultValue]: 'Your location' + }, + avatar: { + [type]: 'string', + [desc]: 'Path or URL to the avatar to be shown in the profile widget', + [condition]: parent => parent.type === 'profile', + [defaultValue]: '/images/avatar.png' + }, + gravatar: { + [type]: 'string', + [desc]: 'Email address for the Gravatar to be shown in the profile widget', + [condition]: parent => parent.type === 'profile' + }, + follow_link: { + [type]: 'string', + [desc]: 'Path or URL for the follow button', + [condition]: parent => parent.type === 'profile' + }, + social_links: { + ...IconLink, + [desc]: 'Links to be shown on the bottom of the profile widget', + [condition]: parent => parent.type === 'profile' + }, + links: { + [type]: 'object', + [desc]: 'Links to be shown in the links widget', + [condition]: parent => parent.type === 'links', + '*': { + [type]: 'string', + [desc]: 'Path or URL to the link', + [required]: true + } + } + } + }, + plugins: { + [type]: 'object', + [desc]: 'Other plugin settings', + gallery: { + [type]: 'boolean', + [desc]: 'Enable the lightGallery and Justified Gallery plugins', + [defaultValue]: true + }, + 'outdated-browser': { + [type]: 'boolean', + [desc]: 'Enable the Outdated Browser plugin', + [defaultValue]: true + }, + animejs: { + [type]: 'boolean', + [desc]: 'Enable page animations', + [defaultValue]: true + }, + mathjax: { + [type]: 'boolean', + [desc]: 'Enable the MathJax plugin', + [defaultValue]: true + }, + 'google-analytics': { + [type]: ['boolean', 'object'], + [desc]: 'Google Analytics plugin settings (http://ppoffice.github.io/hexo-theme-icarus/2018/01/01/plugin/Analytics/#Google-Analytics)', + tracking_id: { + [type]: 'string', + [desc]: 'Google Analytics tracking id', + [defaultValue]: null + } + }, + 'baidu-analytics': { + [type]: ['boolean', 'object'], + [desc]: 'Baidu Analytics plugin settings (http://ppoffice.github.io/hexo-theme-icarus/2018/01/01/plugin/Analytics/#Baidu-Analytics)', + tracking_id: { + [type]: 'string', + [desc]: 'Baidu Analytics tracking id', + [defaultValue]: null + } + } + }, + providers: { + [type]: 'object', + [desc]: 'CDN provider settings', + cdn: { + [type]: 'string', + [desc]: 'Name or URL of the JavaScript and/or stylesheet CDN provider', + [defaultValue]: 'cdnjs' + }, + fontcdn: { + [type]: 'string', + [desc]: 'Name or URL of the webfont CDN provider', + [defaultValue]: 'google' + }, + iconcdn: { + [type]: 'string', + [desc]: 'Name or URL of the webfont Icon CDN provider', + [defaultValue]: 'fontawesome' + } + } +}; \ No newline at end of file diff --git a/includes/specs/common.js b/includes/specs/common.js new file mode 100644 index 0000000..f829d67 --- /dev/null +++ b/includes/specs/common.js @@ -0,0 +1,24 @@ +module.exports = { + projectName: 'Icarus', + is: { + 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; + } + }, + descriptor: { + type: Symbol('@type'), + required: Symbol('@required'), + description: Symbol('@description'), + defaultValue: Symbol('@defaultValue'), + condition: Symbol('@condition'), + } +}; \ No newline at end of file diff --git a/includes/tasks/check_config.js b/includes/tasks/check_config.js index e15ed1e..984c800 100644 --- a/includes/tasks/check_config.js +++ b/includes/tasks/check_config.js @@ -1,10 +1,260 @@ const fs = require('fs'); const path = require('path'); +const yaml = require('js-yaml'); const logger = require('hexo-log')(); +const Schema = require('js-yaml/lib/js-yaml/schema'); +const Type = require('js-yaml/lib/js-yaml/type'); -const conf = path.join(__dirname, '../..', '_config.yml'); +const rootSpec = require('../specs/_config.yml'); +const { projectName, is } = require('../specs/common'); +const { type, required, condition, defaultValue, description } = require('../specs/common').descriptor; -logger.info('Checking if the configuration file exists'); -if (!fs.existsSync(conf)) { - logger.warn(`${conf} is not found. Please create one from the template _config.yml.example.`) -} \ No newline at end of file +const UNDEFINED = Symbol('undefined'); +const CONFIG_PATH = path.join(__dirname, '../..', '_config.yml'); +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 toPlainObject(object, depth) { + if (object === null || (!is.object(object) && !is.array(object))) { + return object; + } + if (depth <= 0) { + return is.array(object) ? '[Array]' : '[Object]'; + } + if (is.array(object)) { + const result = []; + for (let child of object) { + result.push(toPlainObject(child, depth - 1)); + } + return result; + } + const result = {}; + for (let key in object) { + result[key] = toPlainObject(object[key], depth - 1); + } + for (let key of Object.getOwnPropertySymbols(object)) { + result[key.toString()] = toPlainObject(object[key], depth - 1); + } + return result; +} + +function isValidSpec(spec) { + if (!spec.hasOwnProperty(type)) { + return false; + } + return true; +} + +function checkPrecondition(spec, parameter) { + if (!spec.hasOwnProperty(condition)) { + return true; + } + try { + if (spec[condition](parameter) === true) { + return true; + } + } catch (e) { } + return false; +} + +function createDefaultConfig(spec, parentConfig = null) { + if (!isValidSpec(spec)) { + return UNDEFINED; + } + if (!checkPrecondition(spec, parentConfig)) { + return UNDEFINED; + } + if (spec.hasOwnProperty(defaultValue)) { + return 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 (typeof (key) === 'symbol' || key === '*') { + continue; + } + const value = createDefaultConfig(spec[key], defaults); + if (value !== UNDEFINED) { + if (defaults === UNDEFINED) { + defaults = {}; + } + if (spec[key].hasOwnProperty(description)) { + defaults['#' + key] = spec[key][description]; + } + defaults[key] = value; + } + } + return defaults; + } else if (types.includes('array') && spec.hasOwnProperty('*')) { + return [createDefaultConfig(spec['*'], {})]; + } + return UNDEFINED; +} + +function dumpConfig(config, path) { + const configYaml = yaml.safeDump(config, { + indent: 4, + lineWidth: 1024, + schema: YAML_SCHEMA + }).replace(/^(\s*)\'#.*?\':\s*\'*(.*?)\'*$/mg, '$1# $2'); + fs.writeFileSync(path, configYaml); +} + +function validateConfigVersion(config, spec) { + function getMajorVersion(version) { + try { + return parseInt(version.split('.')[0]); + } catch (e) { + logger.error(`Configuration version number ${version} is malformed.`); + } + return null; + } + if (!config.hasOwnProperty('version')) { + logger.error('Failed to get the version number of the configuration file.'); + logger.warn('You are probably using a previous version of confiugration.'); + logger.warn(`Please be noted that it may not work in the newer versions of ${projectName}.`); + return false; + } + const specMajorVersion = getMajorVersion(spec.version[defaultValue]); + const configMajorVersion = getMajorVersion(config.version); + if (configMajorVersion === null || specMajorVersion === null) { + return false; + } + if (configMajorVersion < specMajorVersion) { + logger.warn('You are using a previous version of confiugration.'); + logger.warn(`Please be noted that it may not work in the newer versions of ${projectName}.`); + return false; + } + if (configMajorVersion > specMajorVersion) { + logger.warn('You are probably using a more recent version of confiugration.'); + logger.warn(`Please be noted that it may not work in the previous versions of ${projectName}.`); + return false; + } + return true; +} + +function validateConfigType(config, specTypes) { + specTypes = is.array(specTypes) ? specTypes : [specTypes]; + for (let specType of specTypes) { + if (is[specType](config)) { + return specType; + } + } + logger.error(`Config ${toPlainObject(config, 2)} do not match types ${specTypes}`); + return null; +} + +const INVALID_SPEC = Symbol(); +const MISSING_REQUIRED = Symbol(); +const INVALID_TYPE = Symbol(); + +function validateConfigAndWarn(config, spec, parentConfig, configPath = []) { + const result = validateConfig(config, spec, parentConfig, configPath); + if (result !== true) { + const pathString = configPath.join('.'); + const specTypes = is.array(spec[type]) ? spec[type] : [spec[type]]; + switch(result) { + case INVALID_SPEC: + logger.error(`Invalid specification! The specification '${pathString}' does not have a [type] field:`); + logger.error('The specification of this configuration is:'); + logger.error(JSON.stringify(toPlainObject(spec, 2), null, 4)); + break; + case MISSING_REQUIRED: + logger.error(`Configuration '${pathString}' in required by the specification but is missing from the configuration!`); + logger.error('The specification of this configuration is:'); + logger.error(JSON.stringify(toPlainObject(spec, 2), null, 4)); + break; + case INVALID_TYPE: + logger.error(`Type mismatch! Configuration '${pathString}' is not the '${specTypes.join(' or ')}' type.`); + logger.error('The configuration value is:'); + logger.error(JSON.stringify(toPlainObject(config, 2), null, 4)); + logger.error('The specification of this configuration is:'); + logger.error(JSON.stringify(toPlainObject(spec, 2), null, 4)); + break; + } + } + return result; +} + +function validateConfig(config, spec, parentConfig = null, configPath = []) { + if (!isValidSpec(spec)) { + return INVALID_SPEC; + } + if (!checkPrecondition(spec, parentConfig)) { + return true; + } + if (typeof(config) === 'undefined' || config === null) { + if (spec[required] === true) { + return MISSING_REQUIRED; + } + return true; + } + const configType = validateConfigType(config, spec[type]); + if (configType === null) { + return INVALID_TYPE; + } + if (configType === 'array' && spec.hasOwnProperty('*')) { + for (let i = 0; i < config.length; i++) { + if (!validateConfigAndWarn(config[i], spec['*'], config, configPath.concat(`[${i}]`))) { + return false; + } + } + } else if (configType === 'object') { + for (let key in spec) { + if (key === '*') { + for (let configKey in config) { + if (!validateConfigAndWarn(config[configKey], spec['*'], config, configPath.concat(configKey))) { + return false; + } + } + } else { + if (!validateConfigAndWarn(config[key], spec[key], config, configPath.concat(key))) { + return false; + } + } + } + } + return true; +} + +logger.info('Checking if the configuration file exists...'); + +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...`); + dumpConfig(createDefaultConfig(rootSpec), CONFIG_PATH); + logger.info(`${relativePath} is created. Please restart Hexo to apply changes.`); + process.exit(0); +} + +logger.info('Validating the configuration file...'); +const config = yaml.safeLoad(fs.readFileSync(CONFIG_PATH)); +if (!validateConfigVersion(config, rootSpec)) { + logger.info(`To let ${projectName} create a fresh configuration file for you, please delete or rename the following file:`); + logger.info(CONFIG_PATH); +} else { + validateConfigAndWarn(config, rootSpec); +} diff --git a/includes/tasks/check_deps.js b/includes/tasks/check_deps.js index 0e5027f..ec2b87e 100644 --- a/includes/tasks/check_deps.js +++ b/includes/tasks/check_deps.js @@ -12,13 +12,13 @@ function checkDependency(name) { logger.info('Checking dependencies'); const missingDeps = [ - // 'moment', - // 'lodash', - // 'cheerio', - // 'js-yaml', - // 'highlight.js', - // 'hexo-util', + 'js-yaml', + 'moment', + 'lodash', + 'cheerio', + 'hexo-util', 'hexo-log', + 'hexo-pagination', 'hexo-generator-archive', 'hexo-generator-category', 'hexo-generator-index', diff --git a/layout/comment/valine.ejs b/layout/comment/valine.ejs index c3af5f7..657b335 100644 --- a/layout/comment/valine.ejs +++ b/layout/comment/valine.ejs @@ -9,11 +9,11 @@ <% } %> \ No newline at end of file diff --git a/layout/common/footer.ejs b/layout/common/footer.ejs index 3b6e11a..634571c 100644 --- a/layout/common/footer.ejs +++ b/layout/common/footer.ejs @@ -6,7 +6,7 @@ <% if (has_config('logo.text') && get_config('logo.text')) { %> <%= get_config('logo.text') %> <% } else { %> - + <% } %>

diff --git a/layout/common/head.ejs b/layout/common/head.ejs index 1f39311..26c4fcc 100644 --- a/layout/common/head.ejs +++ b/layout/common/head.ejs @@ -5,11 +5,11 @@ <% if (has_config('open_graph')) { %> <%- open_graph({ - twitter_id: get_config('open_graph.twitter_id', ''), - twitter_site: get_config('open_graph.twitter_site', ''), - google_plus: get_config('open_graph.google_plus', ''), - fb_admins: get_config('open_graph.fb_admins', ''), - fb_app_id: get_config('open_graph.fb_app_id', '') + twitter_id: get_config('open_graph.twitter_id'), + twitter_site: get_config('open_graph.twitter_site'), + google_plus: get_config('open_graph.google_plus'), + fb_admins: get_config('open_graph.fb_admins'), + fb_app_id: get_config('open_graph.fb_app_id') }) %> <% } %> @@ -25,7 +25,7 @@ <%- css(iconcdn()) %> <%- css(iconcdn('material')) %> <%- css(fontcdn('Ubuntu:400,600|Source+Code+Pro')) %> -<%- css(cdn('highlight.js', '9.12.0', 'styles/' + get_config('article.highlight', 'atom-one-light') + '.min.css')) %> +<%- css(cdn('highlight.js', '9.12.0', 'styles/' + get_config('article.highlight') + '.min.css')) %> <%- css('css/style') %> <% if (has_config('plugins')) { %> diff --git a/layout/common/navbar.ejs b/layout/common/navbar.ejs index b0c3cef..935dc1a 100644 --- a/layout/common/navbar.ejs +++ b/layout/common/navbar.ejs @@ -5,7 +5,7 @@ <% if (has_config('logo.text') && get_config('logo.text')) { %> <%= get_config('logo.text') %> <% } else { %> - + <% } %> diff --git a/layout/widget/recent_posts.ejs b/layout/widget/recent_posts.ejs index 88a381c..56e7716 100644 --- a/layout/widget/recent_posts.ejs +++ b/layout/widget/recent_posts.ejs @@ -6,7 +6,7 @@ <% site.posts.sort('date', -1).limit(5).each(post => { %>

- <% if (!has_config('article.thumbnail') || get_config('article.thumbnail', true) !== false) { %> + <% if (!has_config('article.thumbnail') || get_config('article.thumbnail') !== false) { %>

<%= post.title %> diff --git a/package.json b/package.json index 9373e6a..183c998 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { "name": "hexo-theme-icarus", - "version": "0.2.1", + "version": "2.0.0", "private": true }