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
}
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 => { %>
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
}