refactor(schema): rewrite config generator & add migration
parent
46530f3c4e
commit
02d1996215
|
@ -106,5 +106,5 @@ dist
|
|||
# Stores VSCode versions used for testing VSCode extensions
|
||||
.vscode-test
|
||||
|
||||
_config.yml
|
||||
_config.yml*
|
||||
yarn.lock
|
||||
|
|
|
@ -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;
|
|
@ -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;
|
|
@ -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
|
||||
};
|
|
@ -0,0 +1 @@
|
|||
module.exports = require('./v2_v3');
|
|
@ -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;
|
||||
}
|
||||
};
|
|
@ -12,7 +12,7 @@
|
|||
"type": "string",
|
||||
"description": "Changyan app ID"
|
||||
},
|
||||
"shortname": {
|
||||
"conf": {
|
||||
"type": "string",
|
||||
"description": "Changyan configuration ID"
|
||||
}
|
||||
|
|
|
@ -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": [
|
||||
|
|
|
@ -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": [
|
||||
|
|
|
@ -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": [
|
||||
|
|
|
@ -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": [
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -21,7 +21,8 @@
|
|||
"Tags": "/tags",
|
||||
"About": "/about"
|
||||
}
|
||||
]
|
||||
],
|
||||
"nullable": true
|
||||
},
|
||||
"links": {
|
||||
"$ref": "/misc/poly_links.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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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": [
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"const": "patreon"
|
||||
"const": "buymeacoffee"
|
||||
},
|
||||
"url": {
|
||||
"type": "string",
|
||||
|
|
|
@ -23,6 +23,6 @@
|
|||
"required": [
|
||||
"type",
|
||||
"business",
|
||||
"currencyCode"
|
||||
"currency_code"
|
||||
]
|
||||
}
|
|
@ -6,5 +6,6 @@
|
|||
"items": {
|
||||
"type": "string",
|
||||
"description": "Meta tag specified in <attribute>=<value> style\nE.g., name=theme-color;content=#123456 => <meta name=\"theme-color\" content=\"#123456\">"
|
||||
}
|
||||
},
|
||||
"nullable": true
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -36,5 +36,6 @@
|
|||
"icon": "fab fa-github"
|
||||
}
|
||||
}
|
||||
]
|
||||
],
|
||||
"nullable": true
|
||||
}
|
|
@ -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": [
|
||||
|
|
|
@ -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": [
|
||||
|
|
|
@ -9,7 +9,11 @@
|
|||
"string",
|
||||
"number"
|
||||
],
|
||||
"description": "Hotjar site id"
|
||||
"description": "Hotjar site id",
|
||||
"nullable": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"site_id"
|
||||
]
|
||||
}
|
|
@ -22,7 +22,8 @@
|
|||
"Hexo": "https://hexo.io",
|
||||
"Bulma": "https://bulma.io"
|
||||
}
|
||||
]
|
||||
],
|
||||
"nullable": true
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -1,10 +0,0 @@
|
|||
const logger = require('hexo-log')();
|
||||
|
||||
logger.info(`=======================================
|
||||
██╗ ██████╗ █████╗ ██████╗ ██╗ ██╗███████╗
|
||||
██║██╔════╝██╔══██╗██╔══██╗██║ ██║██╔════╝
|
||||
██║██║ ███████║██████╔╝██║ ██║███████╗
|
||||
██║██║ ██╔══██║██╔══██╗██║ ██║╚════██║
|
||||
██║╚██████╗██║ ██║██║ ██║╚██████╔╝███████║
|
||||
╚═╝ ╚═════╝╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝
|
||||
=============================================`);
|
|
@ -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;
|
|
@ -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;
|
||||
module.exports = {
|
||||
Schema,
|
||||
SchemaLoader,
|
||||
DefaultValue
|
||||
};
|
||||
|
|
|
@ -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
|
||||
});
|
||||
}
|
||||
};
|
|
@ -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",
|
||||
|
|
127
scripts/index.js
127
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);
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue