diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 3a975ba..de79054 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -1,11 +1,4 @@ -Please make sure these boxes are checked before submitting your issue. Thank you! +Please make sure you took care of the following things before you submit this issue. Thank you! -- [ ] I have setuped and configurated the blog according to [Hexo official documentation](https://hexo.io/); -- [ ] I have read the [Theme Wiki](https://github.com/ppoffice/hexo-theme-icarus/wiki) carefully and created my own configuration file(_config.yml); -- [ ] I have looked up the [Issues](https://github.com/ppoffice/hexo-theme-icarus/issues) and found no duplicate issues. - -请在发布新Issue之前确保你已经进行了如下工作,谢谢! - -- [ ] 我已经按照[Hexo官方文档](https://hexo.io/)中的步骤安装与配置了Hexo; -- [ ] 我已经仔细地阅读了[主题Wiki](https://github.com/ppoffice/hexo-theme-icarus/wiki)并创建了自己的配置文件(_config.yml); -- [ ] 我已经搜索了[Issues](https://github.com/ppoffice/hexo-theme-icarus/issues),并且没有找到类似的问题。 +- [ ] I have set up and configured my blog according to [Hexo documentation](https://hexo.io/) and [Icarus Documentation](https://github.com/ppoffice/hexo-theme-icarus/wiki); +- [ ] I have looked up the [Github Issues](https://github.com/ppoffice/hexo-theme-icarus/issues) and found no solutions to my problems. diff --git a/LICENSE b/LICENSE index 0ad8407..98d68da 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2015 ZHANG Ruipeng +Copyright (c) 2015 PPOffice Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index e733776..604de4c 100644 --- a/README.md +++ b/README.md @@ -1,62 +1,134 @@ -# Icarus +

+ +
A simple, delicate, and modern theme for the static site generator Hexo. +
+Preview | +Documentation | +Download +
-### The blog theme you may fall in love with, coming to Hexo. [Preview](http://ppoffice.github.io/hexo-theme-icarus/) -![](http://ppoffice.github.io/hexo-theme-icarus/gallery/preview.png "") +[![GitHub release](https://img.shields.io/github/release/qubyte/rubidium.svg)](https://github.com/ppoffice/hexo-theme-icarus) +

-#### [View Documentation](https://github.com/ppoffice/hexo-theme-icarus/wiki) -:star: It is strongly recommended that you read the docs before using Icarus. +![Icarus](http://ppoffice.github.io/hexo-theme-icarus/gallery/preview.png "Icarus Preview") -## Features +### :cd: Installation -### Profile Sidebar +Download & extract or `git clone` Icarus from GitHub to your blog's theme folder, and that's it! -A nice place to show yourself. You can add your own information in your site's `_config.yml` +```shell +git clone https://github.com/ppoffice/hexo-theme-icarus.git themes/icarus +``` -![](http://ppoffice.github.io/hexo-theme-icarus/gallery/profile.png "") +Once started, Icarus will remind you of any missing dependencies and configuration files. -### Self-hosted Insite Search Engine -With the help of [Insight Search](https://github.com/ppoffice/hexo-theme-icarus/wiki/Search#insight-search), you can search anything inside your site without any third-party plugin. +### :gift: Features -![](http://ppoffice.github.io/hexo-theme-icarus/gallery/insight-search.png "") +**Extensive Plugin Support** -### Custom Comment Services -Icarus supports several comment services, give you better choices to communicate with your readers. +Icarus includes plentiful search, comment, sharing and other plugins out of the box. You can choose any of them to enrich your +blog experience, or build your own plugin easily referring to the existing Icarus plugins. -![](http://ppoffice.github.io/hexo-theme-icarus/gallery/custom-comments.png "") +Comment plugins -### Post Banner & Thumbnail +- [Changyan](http://ppoffice.github.io/hexo-theme-icarus/Plugins/Comment/changyan-comment-plugin/) +- [Disqus](http://ppoffice.github.io/hexo-theme-icarus/Plugins/Comment/disqus-comment-plugin/) +- [Facebook](http://ppoffice.github.io/hexo-theme-icarus/Plugins/Comment/facebook-comment-plugin/) +- [Gitment](http://ppoffice.github.io/hexo-theme-icarus/Plugins/Comment/gitment-comment-plugin/) +- [Isso](http://ppoffice.github.io/hexo-theme-icarus/Plugins/Comment/isso-comment-plugin/) +- [LiveRe](http://ppoffice.github.io/hexo-theme-icarus/Plugins/Comment/livere-comment-plugin/) +- [Valine](http://ppoffice.github.io/hexo-theme-icarus/Plugins/Comment/valine-comment-plugin/) -Thanks to [atika](https://github.com/atika), you can now [add thumbnails or banners](https://github.com/ppoffice/hexo-theme-icarus/wiki/Theme#thumbnail) to every post to create better reading experience. +Search plugins -### Responsive Layout +- [Insight Search](http://ppoffice.github.io/hexo-theme-icarus/Plugins/Search/insight-search-plugin/) +- [Google Custom Search Engine](http://ppoffice.github.io/hexo-theme-icarus/Plugins/Search/google-cse-plugin/) +- [Baidu Site Search](http://ppoffice.github.io/hexo-theme-icarus/Plugins/Search/baidu-search-plugin/) -Icarus knows on what screen size you are browsering the website, and reorganize the layout to fit your device. +Share plugins -![](http://ppoffice.github.io/hexo-theme-icarus/gallery/responsive.jpg "") +- [AddThis](http://ppoffice.github.io/hexo-theme-icarus/Plugins/Share/addthis-share-plugin/) +- [AddToAny](http://ppoffice.github.io/hexo-theme-icarus/Plugins/Share/addtoany-share-plugin/) +- [Baidu Share](http://ppoffice.github.io/hexo-theme-icarus/Plugins/Share/baidu-share-plugin/) +- [Share.js](http://ppoffice.github.io/hexo-theme-icarus/Plugins/Share/sharejs-share-plugin/) +- [ShareThis](http://ppoffice.github.io/hexo-theme-icarus/Plugins/Share/sharethis-share-plugin/) -### Custom Categories & Tags Pages +Other plugins -Get your categories and tags listed in single pages to make your blog more methodic. +- [Hexo Tag Plugin](http://ppoffice.github.io/hexo-theme-icarus/Configuration/Posts/hexo-built-in-tag-helpers/) +- [lightGallery & Justified Gallery](http://ppoffice.github.io/hexo-theme-icarus/Plugins/General/gallery-plugin/) +- [MathJax](http://ppoffice.github.io/hexo-theme-icarus/Plugins/General/mathjax-plugin/) +- [Site Analytics](http://ppoffice.github.io/hexo-theme-icarus/Plugins/General/site-analytics-plugin/) -### lightgallery +**Rich Code Highlight Theme Choices** -Icarus uses [lightgallery.js](https://sachinchoolur.github.io/lightgallery.js/) to showcase your photos. Just enable it in your configuration, and that's all! +Icarus directly import code highlight themes from the [highlight.js](https://highlightjs.org/) package, and makes more than +70 highlight themes available to you. -![](http://ppoffice.github.io/hexo-theme-icarus/gallery/lightgallery.jpg "") +
+
+
+
+
-### Justified Gallery +**Elastic Theme Configuration** -You can also use [justifiedgallery.js](http://miromannino.github.io/Justified-Gallery/) to display a photo grid within your posts. Just enable it in your configuration, and place your photos in a div with the class .justified-gallery +In addition to the minimalistic and easy-to-understand configuration design, Icarus allows you to set configurations on a +per-page basis with the ability to merge and override partial configurations. -### Sidebar +
+ + + + + + + + + + + + + +
_config.ymlpost.md
+
menu:
+    Archives: /archives
+    Categories: /categories
+    Tags: /tags
+    About: /about
+
+
title: A Simple Post
+menu:
+    Go Home: /index.html
+---
+# Here is some simple markdown.
+
+
-Icarus provides 6 built-in widgets: +**Responsive Layout** -- recent_posts -- category -- archives -- tag -- tagcloud -- links +No matter what modern browsering device your audiences are using, they can always get the best experience because Icarus's responsive +layout across multiple viewpoints. -All of them are enabled by default. You can edit them in `widget` setting. +![Responsive Layout](http://ppoffice.github.io/hexo-theme-icarus/gallery/responsive.png) + +### :hammer: Development + +This project is built with + +- Hexo 3.7.1 +- Ejs +- Stylus +- Bulma 0.7.2 + +Please refer to the documentation for Icarus implementation details. + +### :tada: Contribute + +:electric_plug: Write a plugin | +:triangular_flag_on_post: Report a bug | +:earth_asia: Add a translation + +### :memo: License + +This project is licensed under the MIT License - see the [LICENSE](https://github.com/ppoffice/hexo-theme-icarus/blob/master/LICENSE) file for details. diff --git a/_config.yml.example b/_config.yml.example deleted file mode 100644 index cd1eb00..0000000 --- a/_config.yml.example +++ /dev/null @@ -1,98 +0,0 @@ -# Menus -menu: - Home: . - Archives: archives - Categories: categories - Tags: tags - About: about - -# Customize -customize: - logo: - enabled: true - width: 40 - height: 40 - url: images/logo.png - profile: - enabled: true # Whether to show profile bar - fixed: true - avatar: css/images/avatar.png - gravatar: # Gravatar email address, if you enable Gravatar, your avatar config will be overriden - author: PPOffice - author_title: Web Developer & Designer - location: Harbin, China - follow: https://github.com/ppoffice/ - highlight: monokai - sidebar: right # sidebar position, options: left, right or leave it empty - thumbnail: true # enable posts thumbnail, options: true, false - favicon: # path to favicon - social_links: - github: http://github.com/ppoffice/hexo-theme-icarus - twitter: / - facebook: / - dribbble: / - rss: / - social_link_tooltip: true # enable the social link tooltip, options: true, false - -# Widgets -widgets: - - recent_posts - - category - - archive - - tag - - tagcloud - - links - -# Search -search: - insight: true # you need to install `hexo-generator-json-content` before using Insight Search - swiftype: # enter swiftype install key here - baidu: false # you need to disable other search engines to use Baidu search, options: true, false - -# Comment -comment: - disqus: # enter disqus shortname here - duoshuo: # enter duoshuo shortname here - youyan: # enter youyan uid here - facebook: # enter true to enable - isso: # enter the domain name of your own comment isso server eg. comments.example.com - changyan: # please fill in `appid` and `conf` to enable - appid: - conf: - gitment: - owner: #your github ID - repo: #the repo to store comments - #Register an OAuth application, and you will get a client ID and a client secret. - client_id: #your client ID - client_secret: #your client secret - livere: # enter livere uid here - valine: # Valine Comment System https://github.com/xCss/Valine - on: # enter true to enable - appId: # enter the leancloud application appId here - appKey: # enter the leancloud application appKey here - notify: # enter true to enable https://github.com/xCss/Valine/wiki/Valine-%E8%AF%84%E8%AE%BA%E7%B3%BB%E7%BB%9F%E4%B8%AD%E7%9A%84%E9%82%AE%E4%BB%B6%E6%8F%90%E9%86%92%E8%AE%BE%E7%BD%AE - verify: # enter true to enable - placeholder: Just Do It # enter the comment box placeholder - - -# Share -share: default # options: jiathis, bdshare, addtoany, default - -# Plugins -plugins: - lightgallery: true # options: true, false - justifiedgallery: true # options: true, false - google_analytics: # enter the tracking ID for your Google Analytics - google_site_verification: # enter Google site verification code - baidu_analytics: # enter Baidu Analytics hash key - mathjax: false # options: true, false - -# Miscellaneous -miscellaneous: - open_graph: # see http://ogp.me - fb_app_id: - fb_admins: - twitter_id: - google_plus: - links: - Hexo: http://hexo.io diff --git a/_source/about/index.md b/_source/about/index.md deleted file mode 100644 index fbeeeda..0000000 --- a/_source/about/index.md +++ /dev/null @@ -1,4 +0,0 @@ -title: "About" -layout: "page" ---- - diff --git a/_source/categories/index.md b/_source/categories/index.md deleted file mode 100644 index 969529b..0000000 --- a/_source/categories/index.md +++ /dev/null @@ -1,3 +0,0 @@ -title: "Categories" -layout: "categories" ---- diff --git a/_source/tags/index.md b/_source/tags/index.md deleted file mode 100644 index f91f0a6..0000000 --- a/_source/tags/index.md +++ /dev/null @@ -1,3 +0,0 @@ -title: "Tags" -layout: "tags" ---- diff --git a/includes/common/ConfigGenerator.js b/includes/common/ConfigGenerator.js new file mode 100644 index 0000000..189d695 --- /dev/null +++ b/includes/common/ConfigGenerator.js @@ -0,0 +1,84 @@ +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 generate(spec, parentConfig = null) { + if (!is.spec(spec)) { + return UNDEFINED; + } + if (spec.hasOwnProperty(requires) && !spec[requires](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 (key === '*') { + continue; + } + const value = generate(spec[key], defaults); + if (value !== UNDEFINED) { + if (defaults === UNDEFINED) { + defaults = {}; + } + if (spec[key].hasOwnProperty(doc)) { + defaults['#' + key] = spec[key][doc]; + } + defaults[key] = value; + } + } + return defaults; + } else if (types.includes('array') && spec.hasOwnProperty('*')) { + return [generate(spec['*'], {})]; + } + return UNDEFINED; +} + +class ConfigGenerator { + constructor(spec) { + this.spec = spec; + } + + generate() { + return yaml.safeDump(generate(this.spec), { + indent: 4, + lineWidth: 1024, + schema: YAML_SCHEMA + }).replace(/^(\s*)\'#.*?\':\s*\'*(.*?)\'*$/mg, '$1# $2'); // make comment lines + } +} + +module.exports = ConfigGenerator; diff --git a/includes/common/ConfigValidator.js b/includes/common/ConfigValidator.js new file mode 100644 index 0000000..f59440f --- /dev/null +++ b/includes/common/ConfigValidator.js @@ -0,0 +1,132 @@ +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; +} + + +function checkVersion(spec, config) { + if (!config.hasOwnProperty('version')) { + throw new VersionNotFoundError(); + } + const configVersion = formatVersion(config.version); + const specVersion = formatVersion(spec.version[defaultValue]); + if (isBreakingChange(specVersion, configVersion)) { + throw new VersionMismatchError(spec.version[defaultValue], config.version, compareVersion(specVersion, configVersion) > 0); + } +} + +class ConfigValidator { + constructor(spec) { + this.spec = spec; + } + + validate(config) { + checkVersion(this.spec, config); + validate(this.spec, config, null, []); + } +} + +module.exports = ConfigValidator; \ No newline at end of file diff --git a/includes/common/utils.js b/includes/common/utils.js new file mode 100644 index 0000000..882fccc --- /dev/null +++ b/includes/common/utils.js @@ -0,0 +1,148 @@ +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 = (() => ({ + string(value) { + return typeof (value) === 'string'; + }, + array(value) { + return Array.isArray(value); + }, + boolean(value) { + return typeof (value) === 'boolean'; + }, + object(value) { + return typeof (value) === 'object' && value.constructor == Object; + }, + function(value) { + return typeof (value) === 'function'; + }, + regexp(value) { + return value instanceof RegExp; + }, + undefined(value) { + return typeof (value) === 'undefined'; + }, + null(value) { + return value === null; + }, + spec(value) { + if (!value.hasOwnProperty(type)) { + return false; + } + if (!is.string(value[type]) && !is.array(value[type])) { + return false; + } + if (value.hasOwnProperty(doc) && !is.string(value[doc])) { + return false; + } + if (value.hasOwnProperty(required) && !is.boolean(value[required])) { + return false; + } + if (value.hasOwnProperty(requires) && !is.function(value[requires])) { + return false; + } + if (value.hasOwnProperty(format) && !is.regexp(value[format])) { + return false; + } + return true; + } +}))(); + +class ConfigError extends Error { + constructor(spec, path) { + super(); + this.spec = spec; + this.path = path; + } +} + +class InvalidSpecError extends ConfigError { + constructor(spec, path) { + super(spec, path); + this.message = `The specification '${path.join('.')}' is invalid.`; + } +} + +class MissingRequiredError extends ConfigError { + constructor(spec, path) { + super(spec, path); + this.message = `Configuration file do not have the required '${path.join('.')}' field.`; + } +} + +class TypeMismatchError extends ConfigError { + constructor(spec, path, config) { + super(spec, path); + this.config = config; + this.message = `Configuration '${path.join('.')}' is not one of the '${spec[type]}' type.`; + } +} + +class FormatMismatchError extends ConfigError { + constructor(spec, path, config) { + super(spec, path); + this.config = config; + this.message = `Configuration '${path.join('.')}' do not match the format '${spec[format]}'.`; + } +} + +class VersionError extends Error { +} + +class VersionNotFoundError extends VersionError { + constructor() { + super(`Version number is not found in the configuration file.`); + } +} + +class VersionMalformedError extends VersionError { + constructor(version) { + super(`Version number ${version} is malformed.`); + this.version = version; + } +} + +class VersionMismatchError extends VersionError { + constructor(specVersion, configVersion, isConfigVersionSmaller) { + super(); + this.specVersion = specVersion; + this.configVersion = configVersion; + if (isConfigVersionSmaller) { + this.message = `The configuration version ${configVersion} is far behind the specification version ${specVersion}.`; + } else { + this.message = `The configuration version ${configVersion} is way ahead of the specification version ${specVersion}.`; + } + } +} + +const errors = { + ConfigError, + InvalidSpecError, + MissingRequiredError, + TypeMismatchError, + FormatMismatchError, + VersionError, + VersionMalformedError, + VersionNotFoundError, + VersionMismatchError +} + +module.exports = { + is, + descriptors, + errors +}; \ No newline at end of file diff --git a/includes/filters/highlight.js b/includes/filters/highlight.js new file mode 100644 index 0000000..31ff0c5 --- /dev/null +++ b/includes/filters/highlight.js @@ -0,0 +1,25 @@ +const cheerio = require('cheerio'); + +module.exports = function (hexo) { + function patchCodeHighlight(content) { + const $ = cheerio.load(content, { decodeEntities: false }); + $('figure.highlight').addClass('hljs'); + $('figure.highlight .code .line span').each(function () { + const classes = $(this).attr('class').split(' '); + if (classes.length === 1) { + $(this).addClass('hljs-' + classes[0]); + $(this).removeClass(classes[0]); + } + }); + return $.html(); + } + + /** + * Add .hljs class name to the code blocks and code elements + */ + hexo.extend.filter.register('after_post_render', function (data) { + data.content = data.content ? patchCodeHighlight(data.content) : data.content; + data.excerpt = data.excerpt ? patchCodeHighlight(data.excerpt) : data.excerpt; + return data; + }); +} \ No newline at end of file diff --git a/includes/generators/categories.js b/includes/generators/categories.js new file mode 100644 index 0000000..c8eb846 --- /dev/null +++ b/includes/generators/categories.js @@ -0,0 +1,14 @@ +/** + * Category list page generator + */ +module.exports = function (hexo) { + hexo.extend.generator.register('categories', function (locals) { + return { + path: 'categories/', + layout: ['categories'], + data: Object.assign({}, locals, { + __categories: true + }) + }; + }); +} \ No newline at end of file diff --git a/includes/generators/category.js b/includes/generators/category.js new file mode 100644 index 0000000..0f019e3 --- /dev/null +++ b/includes/generators/category.js @@ -0,0 +1,34 @@ +const pagination = require('hexo-pagination'); + +module.exports = function (hexo) { + // ATTENTION: This will override the default category generator! + hexo.extend.generator.register('category', function(locals) { + const config = this.config; + const perPage = config.category_generator.per_page; + const paginationDir = config.pagination_dir || 'page'; + + function findParent(category) { + let parents = []; + if (category && category.hasOwnProperty('parent')) { + const parent = locals.categories.filter(cat => cat._id === category.parent).first(); + parents = [parent].concat(findParent(parent)); + } + return parents; + } + + return locals.categories.reduce(function(result, category){ + const posts = category.posts.sort('-date'); + const data = pagination(category.path, posts, { + perPage: perPage, + layout: ['category', 'archive', 'index'], + format: paginationDir + '/%d/', + data: { + category: category.name, + parents: findParent(category) + } + }); + + return result.concat(data); + }, []); + }); +} \ No newline at end of file diff --git a/includes/generators/insight.js b/includes/generators/insight.js new file mode 100644 index 0000000..88f7671 --- /dev/null +++ b/includes/generators/insight.js @@ -0,0 +1,43 @@ +const util = require('hexo-util'); + +/** + * Insight search content.json generator. + */ +module.exports = function (hexo) { + hexo.extend.generator.register('insight', function (locals) { + const url_for = hexo.extend.helper.get('url_for').bind(this); + function minify(str) { + return util.stripHTML(str).trim().replace(/\n/g, ' ').replace(/\s+/g, ' ') + .replace(/&#x([\da-fA-F]+);/g, function (match, hex) { + return String.fromCharCode(parseInt(hex, 16)); + }) + .replace(/&#([\d]+);/g, function (match, dec) { + return String.fromCharCode(dec); + }); + } + function postMapper(post) { + return { + title: post.title, + text: minify(post.content), + link: url_for(post.path) + } + } + function tagMapper(tag) { + return { + name: tag.name, + slug: tag.slug, + link: url_for(tag.path) + } + } + const site = { + pages: locals.pages.map(postMapper), + posts: locals.posts.map(postMapper), + tags: locals.tags.map(tagMapper), + categories: locals.categories.map(tagMapper) + }; + return { + path: '/content.json', + data: JSON.stringify(site) + }; + }); +} \ No newline at end of file diff --git a/includes/generators/tags.js b/includes/generators/tags.js new file mode 100644 index 0000000..e6a8a92 --- /dev/null +++ b/includes/generators/tags.js @@ -0,0 +1,14 @@ +/** + * Tag list page generator + */ +module.exports = function (hexo) { + hexo.extend.generator.register('tags', function (locals) { + return { + path: 'tags/', + layout: ['tags'], + data: Object.assign({}, locals, { + __tags: true + }) + }; + }); +} \ No newline at end of file diff --git a/includes/helpers/cdn.js b/includes/helpers/cdn.js new file mode 100644 index 0000000..14f1b61 --- /dev/null +++ b/includes/helpers/cdn.js @@ -0,0 +1,55 @@ +/** + * CDN static file resolvers. + * + * @example + * <%- cdn(package, version, filename) %> + * <%- fontcdn(fontName) %> + * <%- iconcdn() %> + */ +const cdn_providers = { + cdnjs: 'https://cdnjs.cloudflare.com/ajax/libs/${ package }/${ version }/${ filename }', + jsdelivr: 'https://cdn.jsdelivr.net/npm/${ package }@${ version }/${ filename }', + jsdelivr_github: 'https://cdn.jsdelivr.net/gh/user/${ package }@${ version }/${ filename }', + unpkg: 'https://unpkg.com/${ package }@${ version }/${ filename }' +}; + +const font_providers = { + google: 'https://fonts.googleapis.com/css?family=${ fontname }' +}; + +const icon_providers = { + fontawesome: 'https://use.fontawesome.com/releases/v5.4.1/css/all.css', + material: 'https://fonts.googleapis.com/icon?family=Material+Icons' +}; + +module.exports = function (hexo) { + 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) + .replace(/\${\s*version\s*}/gi, version) + .replace(/\${\s*filename\s*}/gi, filename); + }); + + 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 = 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; + }); +} \ No newline at end of file diff --git a/includes/helpers/config.js b/includes/helpers/config.js new file mode 100644 index 0000000..d29bcd7 --- /dev/null +++ b/includes/helpers/config.js @@ -0,0 +1,48 @@ +/** + * 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.spec'); +const descriptors = require('../common/utils').descriptors; + +module.exports = function (hexo) { + function readProperty(object, path) { + const paths = path.split('.'); + for (let path of paths) { + if (typeof (object) === 'undefined' || object === null || !object.hasOwnProperty(path)) { + return null; + } + object = object[path]; + } + return object; + } + + 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); + 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) { + const readProperty = hexo.extend.helper.get('get_config').bind(this); + return readProperty(configName, null, excludePage) != null; + }); + + hexo.extend.helper.register('get_config_from_obj', function (object, configName, defaultValue = null) { + const value = readProperty(object, configName); + return value === null ? defaultValue : value; + }); +} \ No newline at end of file diff --git a/includes/helpers/layout.js b/includes/helpers/layout.js new file mode 100644 index 0000000..226cc01 --- /dev/null +++ b/includes/helpers/layout.js @@ -0,0 +1,31 @@ +/** + * Helper functions for controlling layout. + * +* @example +* <%- get_widgets(position) %> +* <%- has_column() %> +* <%- column_count() %> + */ +module.exports = function (hexo) { + hexo.extend.helper.register('get_widgets', function (position) { + 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); + }); + + hexo.extend.helper.register('has_column', function (position) { + const getWidgets = hexo.extend.helper.get('get_widgets').bind(this); + return getWidgets(position).length > 0; + }); + + hexo.extend.helper.register('column_count', function () { + let columns = 1; + const hasColumn = hexo.extend.helper.get('has_column').bind(this); + columns += hasColumn('left') ? 1 : 0; + columns += hasColumn('right') ? 1 : 0; + return columns; + }); +} \ No newline at end of file diff --git a/includes/helpers/override.js b/includes/helpers/override.js new file mode 100644 index 0000000..84fbcbc --- /dev/null +++ b/includes/helpers/override.js @@ -0,0 +1,124 @@ +/** + * Helper functions that override Hexo built-in helpers. + * + * @example +* <%- _list_archives() %> +* <%- _list_categories() %> +* <%- _list_tags() %> +* <%- _toc() %> + */ +const cheerio = require('cheerio'); + +module.exports = function (hexo) { + hexo.extend.helper.register('_list_archives', function () { + const $ = cheerio.load(this.list_archives(), { decodeEntities: false }); + const archives = []; + $('.archive-list-item').each(function () { + archives.push({ + url: $(this).find('.archive-list-link').attr('href'), + name: $(this).find('.archive-list-link').text(), + count: $(this).find('.archive-list-count').text() + }); + }); + return archives; + }); + + hexo.extend.helper.register('_list_categories', function () { + const $ = cheerio.load(this.list_categories({ depth: 2 }), { decodeEntities: false }); + function traverse(root) { + const categories = []; + root.find('> .category-list-item').each(function () { + const category = { + url: $(this).find('> .category-list-link').attr('href'), + name: $(this).find('> .category-list-link').text(), + count: $(this).find('> .category-list-count').text() + }; + if ($(this).find('> .category-list-child').length) { + category['children'] = traverse($(this).find('> .category-list-child')); + } + categories.push(category); + }); + return categories; + } + return traverse($('.category-list')); + }); + + hexo.extend.helper.register('_list_tags', function () { + const $ = cheerio.load(this.list_tags(), { decodeEntities: false }); + const tags = []; + $('.tag-list-item').each(function () { + tags.push({ + url: $(this).find('.tag-list-link').attr('href'), + name: $(this).find('.tag-list-link').text(), + count: $(this).find('.tag-list-count').text() + }); + }); + return tags; + }); + + /** + * Export a tree of headings of an article + * { + * "1": { + * "id": "How-to-enable-table-of-content-for-a-post", + * "index": "1" + * }, + * "2": { + * "1": { + * "1": { + * "id": "Third-level-title", + * "index": "2.1.1" + * }, + * "id": "Second-level-title", + * "index": "2.1" + * }, + * "2": { + * "id": "Another-second-level-title", + * "index": "2.2" + * }, + * "id": "First-level-title", + * "index": "2" + * } + * } + */ + hexo.extend.helper.register('_toc', (content) => { + const $ = cheerio.load(content, { decodeEntities: false }); + const toc = {}; + const levels = [0, 0, 0]; + // Get top 3 headings that are present in the content + const tags = [1, 2, 3, 4, 5, 6].map(i => 'h' + i).filter(h => $(h).length > 0).slice(0, 3); + if (tags.length === 0) { + return toc; + } + $(tags.join(',')).each(function () { + const level = tags.indexOf(this.name); + const id = $(this).attr('id'); + const text = $(this).text(); + + for (let i = 0; i < levels.length; i++) { + if (i > level) { + levels[i] = 0; + } else if (i < level) { + if (levels[i] === 0) { + // if headings start with a lower level heading, set the former heading index to 1 + // e.g. h3, h2, h1, h2, h3 => 1.1.1, 1.2, 2, 2.1, 2.1.1 + levels[i] = 1; + } + } else { + levels[i] += 1; + } + } + let node = toc; + for (let i of levels.slice(0, level + 1)) { + if (!node.hasOwnProperty(i)) { + node[i] = {}; + } + node = node[i]; + } + node.id = id; + node.text = text; + node.index = levels.slice(0, level + 1).join('.'); + }); + return toc; + }); +} \ No newline at end of file diff --git a/includes/helpers/page.js b/includes/helpers/page.js new file mode 100644 index 0000000..4da91a0 --- /dev/null +++ b/includes/helpers/page.js @@ -0,0 +1,96 @@ +/** +* Helper functions for page/post. +* +* @example +* <%- is_categories(page) %> +* <%- is_tags(page) %> +* <%- page_title(page) %> +* <%- meta(post) %> +* <%- has_thumbnail(post) %> +* <%- get_thumbnail(post) %> +*/ +module.exports = function (hexo) { + function trim(str) { + return str.trim().replace(/^"(.*)"$/, '$1').replace(/^'(.*)'$/, '$1'); + } + + function split(str, sep) { + var result = []; + var matched = null; + while (matched = sep.exec(str)) { + result.push(matched[0]); + } + return result; + } + + hexo.extend.helper.register('is_categories', function (page = null) { + return (page === null ? this.page : page).__categories; + }); + + hexo.extend.helper.register('is_tags', function (page = null) { + return (page === null ? this.page : page).__tags; + }); + + /** + * Generate html head title based on page type + */ + hexo.extend.helper.register('page_title', function (page = null) { + page = page === null ? this.page : page; + let title = page.title; + + if (this.is_archive()) { + title = this._p('common.archive', Infinity); + if (this.is_month()) { + title += ': ' + page.year + '/' + page.month; + } else if (this.is_year()) { + title += ': ' + page.year; + } + } else if (this.is_category()) { + title = this._p('common.category', 1) + ': ' + page.category; + } else if (this.is_tag()) { + title = this._p('common.tag', 1) + ': ' + page.tag; + } else if (this.is_categories()) { + title = this._p('common.category', Infinity); + } else if (this.is_tags()) { + title = this._p('common.tag', Infinity); + } + + const siteTitle = hexo.extend.helper.get('get_config').bind(this)('title', '', true); + return [title, siteTitle].filter(str => typeof (str) !== 'undefined' && str.trim() !== '').join(' - '); + }); + + hexo.extend.helper.register('meta', function (post) { + var metas = post.meta || []; + var output = ''; + var metaDOMArray = metas.map(function (meta) { + var entities = split(meta, /(?:[^\\;]+|\\.)+/g); + var entityArray = entities.map(function (entity) { + var keyValue = split(entity, /(?:[^\\=]+|\\.)+/g); + if (keyValue.length < 2) { + return null; + } + var key = trim(keyValue[0]); + var value = trim(keyValue[1]); + return key + '="' + value + '"'; + }).filter(function (entity) { + return entity; + }); + return ''; + }); + return metaDOMArray.join('\n'); + }); + + hexo.extend.helper.register('has_thumbnail', function (post) { + const getConfig = hexo.extend.helper.get('get_config').bind(this); + const allowThumbnail = getConfig('article.thumbnail', true); + if (!allowThumbnail) { + return false; + } + return post.hasOwnProperty('thumbnail') && post.thumbnail; + }); + + hexo.extend.helper.register('get_thumbnail', function (post) { + const hasThumbnail = hexo.extend.helper.get('has_thumbnail').bind(this)(post); + return this.url_for(hasThumbnail ? post.thumbnail : 'images/thumbnail.svg'); + }); +} \ No newline at end of file diff --git a/includes/helpers/site.js b/includes/helpers/site.js new file mode 100644 index 0000000..ef50a0d --- /dev/null +++ b/includes/helpers/site.js @@ -0,0 +1,53 @@ +/** + * Helper functions related the site properties. + * + * @example +* <%- is_same_link(url_a, url_b) %> +* <%- post_count() %> +* <%- category_count() %> +* <%- tag_count() %> +* <%- duration() %> +* <%- word_count(content) %> + */ +const moment = require('moment'); + +module.exports = function (hexo) { + hexo.extend.helper.register('is_same_link', function (a, b) { + function santize(url) { + let paths = url.replace(/(^\w+:|^)\/\//, '').split('#')[0].split('/').filter(p => p.trim() !== ''); + if (paths.length > 0 && paths[paths.length - 1].trim() === 'index.html') { + paths = paths.slice(0, paths.length - 1) + } + return paths.join('/'); + } + return santize(this.url_for(a)) == santize(this.url_for(b)); + }); + + hexo.extend.helper.register('post_count', function () { + return this.site.posts.length; + }); + + hexo.extend.helper.register('category_count', function () { + return this.site.categories.filter(category => category.length).length; + }); + + hexo.extend.helper.register('tag_count', function () { + return this.site.tags.filter(tag => tag.length).length; + }); + + /** + * Export moment.duration + */ + hexo.extend.helper.register('duration', function () { + return moment.duration.apply(moment, arguments); + }); + + /** + * Get the word count of a paragraph. + */ + hexo.extend.helper.register('word_count', function (content) { + content = content.replace(/<\/?[a-z][^>]*>/gi, ''); + content = content.trim(); + return content ? (content.match(/[\u00ff-\uffff]|[a-zA-Z]+/g) || []).length : 0; + }); +} \ No newline at end of file diff --git a/includes/specs/article.spec.js b/includes/specs/article.spec.js new file mode 100644 index 0000000..d6793e1 --- /dev/null +++ b/includes/specs/article.spec.js @@ -0,0 +1,21 @@ +const { doc, type, defaultValue } = require('../common/utils').descriptors; + +module.exports = { + [type]: 'object', + [doc]: 'Article display settings', + highlight: { + [type]: 'string', + [doc]: 'Code highlight theme (https://github.com/highlightjs/highlight.js/tree/master/src/styles)', + [defaultValue]: 'atom-one-light' + }, + thumbnail: { + [type]: 'boolean', + [doc]: 'Whether to show article thumbnail images', + [defaultValue]: true + }, + readtime: { + [type]: 'boolean', + [doc]: 'Whether to show estimate article reading time', + [defaultValue]: true + } +}; \ No newline at end of file diff --git a/includes/specs/comment.spec.js b/includes/specs/comment.spec.js new file mode 100644 index 0000000..76b732d --- /dev/null +++ b/includes/specs/comment.spec.js @@ -0,0 +1,119 @@ +const { doc, type, defaultValue, required, requires } = require('../common/utils').descriptors; + +const ChangYanSpec = { + appid: { + [type]: 'string', + [doc]: 'Changyan comment app ID', + [required]: true, + [requires]: comment => comment.type === 'changyan' + }, + conf: { + [type]: 'string', + [doc]: 'Changyan comment configuration ID', + [required]: true, + [requires]: comment => comment.type === 'changyan' + } +}; + +const DisqusSpec = { + shortname: { + [type]: 'string', + [doc]: 'Disqus shortname', + [required]: true, + [requires]: comment => comment.type === 'disqus' + } +}; + +const GitmentSpec = { + owner: { + [type]: 'string', + [doc]: 'Your GitHub ID', + [required]: true, + [requires]: comment => comment.type === 'gitment' + }, + repo: { + [type]: 'string', + [doc]: 'The repo to store comments', + [required]: true, + [requires]: comment => comment.type === 'gitment' + }, + client_id: { + [type]: 'string', + [doc]: 'Your client ID', + [required]: true, + [requires]: comment => comment.type === 'gitment' + }, + client_secret: { + [type]: 'string', + [doc]: 'Your client secret', + [required]: true, + [requires]: comment => comment.type === 'gitment' + } +}; + +const IssoSpec = { + url: { + [type]: 'string', + [doc]: 'URL to your Isso comment service', + [required]: true, + [requires]: comment => comment.type === 'isso' + } +}; + +const LiveReSpec = { + uid: { + [type]: 'string', + [doc]: 'LiveRe comment service UID', + [required]: true, + [requires]: comment => comment.type === 'livere' + } +}; + +const ValineSpec = { + app_id: { + [type]: 'boolean', + [doc]: 'LeanCloud APP ID', + [required]: true, + [requires]: comment => comment.type === 'valine' + }, + app_key: { + [type]: 'boolean', + [doc]: 'LeanCloud APP key', + [required]: true, + [requires]: comment => comment.type === 'valine' + }, + notify: { + [type]: 'boolean', + [doc]: 'Enable email notification when someone comments', + [defaultValue]: false, + [requires]: comment => comment.type === 'valine' + }, + verify: { + [type]: 'boolean', + [doc]: 'Enable verification code service', + [defaultValue]: false, + [requires]: comment => comment.type === 'valine' + }, + placeholder: { + [type]: 'boolean', + [doc]: 'Placeholder text in the comment box', + [defaultValue]: 'Say something...', + [requires]: comment => comment.type === 'valine' + } +}; + +module.exports = { + [type]: 'object', + [doc]: 'Comment plugin settings (http://ppoffice.github.io/hexo-theme-icarus/categories/Configuration/Comment-Plugins)', + type: { + [type]: 'string', + [doc]: 'Name of the comment plugin', + [defaultValue]: null + }, + ...ChangYanSpec, + ...DisqusSpec, + ...GitmentSpec, + ...IssoSpec, + ...LiveReSpec, + ...ValineSpec +} \ No newline at end of file diff --git a/includes/specs/config.spec.js b/includes/specs/config.spec.js new file mode 100644 index 0000000..22c55cd --- /dev/null +++ b/includes/specs/config.spec.js @@ -0,0 +1,24 @@ +const { version } = require('../../package.json'); +const { type, required, defaultValue, doc } = require('../common/utils').descriptors; + +module.exports = { + [type]: 'object', + [doc]: 'Root of the configuration file', + [required]: true, + version: { + [type]: 'string', + [doc]: 'Version of the Icarus theme that is currently used', + [required]: true, + [defaultValue]: version + }, + ...require('./meta.spec'), + navbar: require('./navbar.spec'), + footer: require('./footer.spec'), + article: require('./article.spec'), + search: require('./search.spec'), + comment: require('./comment.spec'), + share: require('./share.spec'), + widgets: require('./widgets.spec'), + plugins: require('./plugins.spec'), + providers: require('./providers.spec') +}; \ No newline at end of file diff --git a/includes/specs/footer.spec.js b/includes/specs/footer.spec.js new file mode 100644 index 0000000..948a47a --- /dev/null +++ b/includes/specs/footer.spec.js @@ -0,0 +1,24 @@ +const { doc, type, defaultValue } = require('../common/utils').descriptors; + +module.exports = { + [type]: 'object', + [doc]: 'Footer section link settings', + links: { + ...require('./icon_link.spec'), + [doc]: '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' + } + } + } +}; \ No newline at end of file diff --git a/includes/specs/icon_link.spec.js b/includes/specs/icon_link.spec.js new file mode 100644 index 0000000..b3cb39a --- /dev/null +++ b/includes/specs/icon_link.spec.js @@ -0,0 +1,20 @@ +const { doc, type, required } = require('../common/utils').descriptors; + +module.exports = { + [type]: 'object', + [doc]: 'Link icon settings', + '*': { + [type]: ['string', 'object'], + [doc]: 'Path or URL to the menu item, and/or link icon class names', + icon: { + [required]: true, + [type]: 'string', + [doc]: 'Link icon class names' + }, + url: { + [required]: true, + [type]: 'string', + [doc]: 'Path or URL to the menu item' + } + } +}; \ No newline at end of file diff --git a/includes/specs/meta.spec.js b/includes/specs/meta.spec.js new file mode 100644 index 0000000..f5f2de3 --- /dev/null +++ b/includes/specs/meta.spec.js @@ -0,0 +1,52 @@ +const { doc, type, defaultValue } = require('../common/utils').descriptors; + +module.exports = { + favicon: { + [type]: 'string', + [doc]: 'Path or URL to the website\'s icon', + [defaultValue]: '/images/favicon.svg', + }, + rss: { + [type]: 'string', + [doc]: 'Path or URL to RSS atom.xml', + [defaultValue]: null + }, + logo: { + [type]: ['string', 'object'], + [defaultValue]: '/images/logo.svg', + [doc]: 'Path or URL to the website\'s logo to be shown on the left of the navigation bar or footer', + text: { + [type]: 'string', + [doc]: 'Text to be shown in place of the logo image' + } + }, + open_graph: { + [type]: 'object', + [doc]: 'Open Graph metadata (https://hexo.io/docs/helpers.html#open-graph)', + fb_app_id: { + [type]: 'string', + [doc]: 'Facebook App ID', + [defaultValue]: null + }, + fb_admins: { + [type]: 'string', + [doc]: 'Facebook Admin ID', + [defaultValue]: null + }, + twitter_id: { + [type]: 'string', + [doc]: 'Twitter ID', + [defaultValue]: null + }, + twitter_site: { + [type]: 'string', + [doc]: 'Twitter site', + [defaultValue]: null + }, + google_plus: { + [type]: 'string', + [doc]: 'Google+ profile link', + [defaultValue]: null + } + } +}; \ No newline at end of file diff --git a/includes/specs/navbar.spec.js b/includes/specs/navbar.spec.js new file mode 100644 index 0000000..1f55457 --- /dev/null +++ b/includes/specs/navbar.spec.js @@ -0,0 +1,31 @@ +const { doc, type, defaultValue } = require('../common/utils').descriptors; + +module.exports = { + [type]: 'object', + [doc]: 'Navigation bar link settings', + menu: { + [type]: 'object', + [doc]: 'Navigation bar menu links', + [defaultValue]: { + Home: '/', + Archives: '/archives', + Categories: '/categories', + Tags: '/tags', + About: '/about' + }, + '*': { + [type]: 'string', + [doc]: 'Path or URL to the menu item' + } + }, + links: { + ...require('./icon_link.spec'), + [doc]: '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' + } + } + } +}; \ No newline at end of file diff --git a/includes/specs/plugins.spec.js b/includes/specs/plugins.spec.js new file mode 100644 index 0000000..1196770 --- /dev/null +++ b/includes/specs/plugins.spec.js @@ -0,0 +1,49 @@ +const { doc, type, defaultValue, required, requires } = require('../common/utils').descriptors; + +module.exports = { + [type]: 'object', + [doc]: 'Other plugin settings', + gallery: { + [type]: 'boolean', + [doc]: 'Enable the lightGallery and Justified Gallery plugins (http://ppoffice.github.io/hexo-theme-icarus/2016/07/08/plugin/Gallery/)', + [defaultValue]: true + }, + 'outdated-browser': { + [type]: 'boolean', + [doc]: 'Enable the Outdated Browser plugin (http://outdatedbrowser.com/)', + [defaultValue]: true + }, + animejs: { + [type]: 'boolean', + [doc]: 'Enable page animations (http://animejs.com/)', + [defaultValue]: true + }, + mathjax: { + [type]: 'boolean', + [doc]: 'Enable the MathJax plugin (http://ppoffice.github.io/hexo-theme-icarus/2018/01/01/plugin/MathJax/)', + [defaultValue]: true + }, + 'back-to-top': { + [type]: 'boolean', + [doc]: 'Show the back to top button on mobile devices', + [defaultValue]: true + }, + 'google-analytics': { + [type]: ['boolean', 'object'], + [doc]: 'Google Analytics plugin settings (http://ppoffice.github.io/hexo-theme-icarus/2018/01/01/plugin/Analytics/#Google-Analytics)', + tracking_id: { + [type]: 'string', + [doc]: 'Google Analytics tracking id', + [defaultValue]: null + } + }, + 'baidu-analytics': { + [type]: ['boolean', 'object'], + [doc]: 'Baidu Analytics plugin settings (http://ppoffice.github.io/hexo-theme-icarus/2018/01/01/plugin/Analytics/#Baidu-Analytics)', + tracking_id: { + [type]: 'string', + [doc]: 'Baidu Analytics tracking id', + [defaultValue]: null + } + } +}; \ No newline at end of file diff --git a/includes/specs/providers.spec.js b/includes/specs/providers.spec.js new file mode 100644 index 0000000..2b7af1a --- /dev/null +++ b/includes/specs/providers.spec.js @@ -0,0 +1,21 @@ +const { doc, type, defaultValue } = require('../common/utils').descriptors; + +module.exports = { + [type]: 'object', + [doc]: 'CDN provider settings', + cdn: { + [type]: 'string', + [doc]: 'Name or URL of the JavaScript and/or stylesheet CDN provider', + [defaultValue]: 'cdnjs' + }, + fontcdn: { + [type]: 'string', + [doc]: 'Name or URL of the webfont CDN provider', + [defaultValue]: 'google' + }, + iconcdn: { + [type]: 'string', + [doc]: 'Name or URL of the webfont Icon CDN provider', + [defaultValue]: 'fontawesome' + } +}; \ No newline at end of file diff --git a/includes/specs/search.spec.js b/includes/specs/search.spec.js new file mode 100644 index 0000000..7c66071 --- /dev/null +++ b/includes/specs/search.spec.js @@ -0,0 +1,17 @@ +const { doc, type, defaultValue, required, requires } = require('../common/utils').descriptors; + +module.exports = { + [type]: 'object', + [doc]: 'Search plugin settings (http://ppoffice.github.io/hexo-theme-icarus/categories/Configuration/Search-Plugins)', + type: { + [type]: 'string', + [doc]: 'Name of the search plugin', + [defaultValue]: 'insight' + }, + cx: { + [type]: 'string', + [doc]: 'Google CSE cx value', + [required]: true, + [requires]: search => search.type === 'google-cse' + } +}; \ No newline at end of file diff --git a/includes/specs/share.spec.js b/includes/specs/share.spec.js new file mode 100644 index 0000000..ecc1cac --- /dev/null +++ b/includes/specs/share.spec.js @@ -0,0 +1,17 @@ +const { doc, type, defaultValue, required, requires } = require('../common/utils').descriptors; + +module.exports = { + [type]: 'object', + [doc]: 'Share plugin settings (http://ppoffice.github.io/hexo-theme-icarus/categories/Configuration/Share-Plugins)', + type: { + [type]: 'string', + [doc]: 'Share plugin name', + [defaultValue]: null + }, + install_url: { + [type]: 'string', + [doc]: 'URL to the share plugin script provided by share plugin service provider', + [required]: true, + [requires]: share => share.type === 'sharethis' || share.type === 'addthis' + } +} \ No newline at end of file diff --git a/includes/specs/widgets.spec.js b/includes/specs/widgets.spec.js new file mode 100644 index 0000000..8cda95d --- /dev/null +++ b/includes/specs/widgets.spec.js @@ -0,0 +1,145 @@ +const { doc, type, defaultValue, required, requires, format } = require('../common/utils').descriptors; + +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', + PPOffice: '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' + } +]; + +const ProfileSpec = { + author: { + [type]: 'string', + [doc]: 'Author name to be shown in the profile widget', + [defaultValue]: 'Your name' + }, + author_title: { + [type]: 'string', + [doc]: 'Title of the author to be shown in the profile widget', + [defaultValue]: 'Your title' + }, + location: { + [type]: 'string', + [doc]: 'Author\'s current location to be shown in the profile widget', + [defaultValue]: 'Your location' + }, + avatar: { + [type]: 'string', + [doc]: 'Path or URL to the avatar to be shown in the profile widget', + [defaultValue]: '/images/avatar.png' + }, + gravatar: { + [type]: 'string', + [doc]: 'Email address for the Gravatar to be shown in the profile widget', + }, + follow_link: { + [type]: 'string', + [doc]: 'Path or URL for the follow button', + }, + social_links: { + ...require('./icon_link.spec'), + [doc]: 'Links to be shown on the bottom of the profile widget', + } +}; + +for (let key in ProfileSpec) { + ProfileSpec[key][requires] = widget => widget.type === 'profile'; +} + +const LinksSpec = { + links: { + [type]: 'object', + [doc]: 'Links to be shown in the links widget', + [requires]: parent => parent.type === 'links', + '*': { + [type]: 'string', + [doc]: 'Path or URL to the link', + [required]: true + } + } +}; + +module.exports = { + [type]: 'array', + [doc]: 'Sidebar widget settings (http://ppoffice.github.io/hexo-theme-icarus/categories/Widgets/)', + [defaultValue]: DEFAULT_WIDGETS, + '*': { + [type]: 'object', + [doc]: 'Single widget settings', + type: { + [type]: 'string', + [doc]: 'Widget name', + [required]: true, + [defaultValue]: 'profile' + }, + position: { + [type]: 'string', + [doc]: 'Where should the widget be placed, left or right', + [format]: /^(left|right)$/, + [required]: true, + [defaultValue]: 'left' + }, + ...ProfileSpec, + ...LinksSpec + } +} \ No newline at end of file diff --git a/includes/tasks/check_config.js b/includes/tasks/check_config.js new file mode 100644 index 0000000..1632ac1 --- /dev/null +++ b/includes/tasks/check_config.js @@ -0,0 +1,46 @@ +const fs = require('fs'); +const util = require('util'); +const path = require('path'); +const logger = require('hexo-log')(); +const yaml = require('js-yaml'); + +const { errors } = require('../common/utils'); +const rootSpec = require('../specs/config.spec'); +const ConfigValidator = require('../common/ConfigValidator'); +const ConfigGenerator = require('../common/ConfigGenerator'); + +const CONFIG_PATH = path.join(__dirname, '../..', '_config.yml'); + +logger.info('Validating the configuration file'); + +if (!fs.existsSync(CONFIG_PATH)) { + const relativePath = path.relative(process.cwd(), CONFIG_PATH); + logger.warn(`${relativePath} is not found. We are creating one for you...`); + fs.writeFileSync(CONFIG_PATH, new ConfigGenerator(rootSpec).generate()); + logger.info(`${relativePath} is created. Please restart Hexo to apply changes.`); + process.exit(0); +} + +const validator = new ConfigValidator(rootSpec); +const config = yaml.safeLoad(fs.readFileSync(CONFIG_PATH)); +try { + validator.validate(config); +} catch (e) { + if (e instanceof errors.ConfigError) { + logger.error(e.message); + if (e.hasOwnProperty('spec')) { + logger.error('The specification of this configuration is:'); + logger.error(util.inspect(e.spec)); + } + if (e.hasOwnProperty('config')) { + logger.error('Configuration value is:'); + logger.error(util.inspect(e.config)); + } + } else if (e instanceof errors.VersionError) { + logger.error(e.message); + logger.warn(`To let us create a fresh configuration file for you, please rename or delete the following file:`); + logger.warn(CONFIG_PATH); + } else { + throw e; + } +} diff --git a/includes/tasks/check_deps.js b/includes/tasks/check_deps.js new file mode 100644 index 0000000..663e0d0 --- /dev/null +++ b/includes/tasks/check_deps.js @@ -0,0 +1,32 @@ +const logger = require('hexo-log')(); + +function checkDependency(name) { + try { + require.resolve(name); + return true; + } catch(e) { + logger.error(`Package ${name} is not installed.`) + } + return false; +} + +logger.info('Checking dependencies'); +const missingDeps = [ + 'js-yaml', + 'moment', + 'cheerio', + 'hexo-util', + 'hexo-log', + 'hexo-pagination', + 'hexo-generator-archive', + 'hexo-generator-category', + 'hexo-generator-index', + 'hexo-generator-tag', + 'hexo-renderer-ejs', + 'hexo-renderer-marked', + 'hexo-renderer-stylus', +].map(checkDependency).some(installed => !installed); +if (missingDeps) { + logger.error('Please install the missing dependencies from the root directory of your Hexo site.'); + process.exit(-1); +} diff --git a/includes/tasks/welcome.js b/includes/tasks/welcome.js new file mode 100644 index 0000000..264f15e --- /dev/null +++ b/includes/tasks/welcome.js @@ -0,0 +1,10 @@ +const logger = require('hexo-log')(); + +logger.info(`======================================= + ██╗ ██████╗ █████╗ ██████╗ ██╗ ██╗███████╗ + ██║██╔════╝██╔══██╗██╔══██╗██║ ██║██╔════╝ + ██║██║ ███████║██████╔╝██║ ██║███████╗ + ██║██║ ██╔══██║██╔══██╗██║ ██║╚════██║ + ██║╚██████╗██║ ██║██║ ██║╚██████╔╝███████║ + ╚═╝ ╚═════╝╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝ +=============================================`); \ No newline at end of file diff --git a/languages/en.yml b/languages/en.yml index 1084d52..401bc45 100644 --- a/languages/en.yml +++ b/languages/en.yml @@ -1,33 +1,35 @@ -index: - home: 'home' - search: 'Search' - archive: 'archive' - category: 'category' - uncategorized: 'uncategorized' - tag: 'tag' -nav: +common: + archive: + one: 'Archive' + other: 'Archives' + category: + one: 'Category' + other: 'Categories' + tag: + one: 'Tag' + other: 'Tags' + post: + one: 'Post' + other: 'Posts' + prev: 'Previous' next: 'Next' - prev: 'Prev' - older: 'Older' - newer: 'Newer' widget: - recents: 'recent' - archives: 'archives' - categories: 'categories' - links: 'links' - tags: 'tags' - tag_cloud: 'tag cloud' + follow: 'Follow' + recents: 'Recent' + links: 'Links' + tag_cloud: 'Tag Cloud' + catalogue: 'Catalogue' article: more: 'Read More' comments: 'Comments' - share: 'Share' - catalogue: 'Catalogue' -profile: - follow: 'FOLLOW' - post: 'post' - tag: 'tag' - posts: 'posts' - tags: 'tags' + read: 'read' + about: 'About' + words: 'words' +plugin: + backtotop: 'Back to Top' +search: + search: 'Search' + hint: 'Type something...' insight: hint: 'Type something...' posts: 'Posts' @@ -35,4 +37,3 @@ insight: categories: 'Categories' tags: 'Tags' untitled: '(Untitled)' - diff --git a/languages/es.yml b/languages/es.yml index 4e49c7e..264c1c6 100644 --- a/languages/es.yml +++ b/languages/es.yml @@ -1,33 +1,34 @@ #By SrWoOoW -index: - home: 'Inicio' - search: 'Buscar' - archive: 'Archivo' - category: 'Categoria' - uncategorized: 'Sin categoría' - tag: 'Etiqueta' -nav: - next: 'Siguiente ' +common: + archive: + one: 'Archivo' + other: 'Archivos' + category: + one: 'Categoria' + other: 'Categorias' + tag: + one: 'Etiqueta' + other: 'Etiquetas' + post: + one: 'Entrada' + other: 'Entradas' prev: 'Anterior' - older: 'Más viejo' - newer: 'Más nuevo' + next: 'Siguiente' widget: - recents: 'Recientes' - archives: 'Archivos' - categories: 'Categorias' - links: 'Links' - tags: 'Etiquetas' - tag_cloud: 'Nube de etiquetas' -article: - comments: 'Comentarios' - share: 'Compartir' - catalogue: 'Catálogo' -profile: follow: 'SEGUIR' - post: 'Entrada' - tag: 'Etiqueta' - posts: 'Entradas' - tags: 'Etiquetas' + recents: 'Recientes' + links: 'Links' + tag_cloud: 'Nube de etiquetas' + catalogue: 'Catálogo' +article: + more: 'Read More' + comments: 'Comentarios' + read: 'read' + about: 'About' + words: 'words' +search: + search: 'Search' + hint: 'Type something...' insight: hint: 'Type something...' posts: 'Entradas' diff --git a/languages/fr.yml b/languages/fr.yml index 28018e9..9bfdbeb 100644 --- a/languages/fr.yml +++ b/languages/fr.yml @@ -1,32 +1,33 @@ -index: - home: 'Racine' - search: 'Rechercher' - archive: 'Archive' - category: 'Catégorie' - uncategorized: 'Sans catégorie' - tag: 'Tag' -nav: - next: 'Suiv' +common: + archive: + one: 'Archive' + other: 'Archives' + category: + one: 'Catégorie' + other: 'Catégories' + tag: + one: 'Tag' + other: 'Tags' + post: + one: 'Article' + other: 'Articles' prev: 'Préc' - older: 'Plus ancien' - newer: 'Plus récent' + next: 'Suiv' widget: - recents: 'Récents' - archives: 'Archives' - categories: 'Catégories' - links: 'Liens' - tags: 'Tags' - tag_cloud: 'Nuage de tags' -article: - comments: 'Commentaires' - share: 'Partager' - catalogue: 'Catalogue' -profile: follow: 'SUIVRE' - post: 'Article' - tag: 'Tag' - posts: 'Articles' - tags: 'Tags' + recents: 'Récents' + links: 'Liens' + tag_cloud: 'Nuage de tags' + catalogue: 'Catalogue' +article: + more: 'Read More' + comments: 'Commentaires' + read: 'read' + about: 'About' + words: 'words' +search: + search: 'Search' + hint: 'Type something...' insight: hint: 'Type something...' posts: 'Articles' diff --git a/languages/id.yml b/languages/id.yml index 3d5e3ff..52c8c54 100644 --- a/languages/id.yml +++ b/languages/id.yml @@ -1,31 +1,33 @@ -index: - home: 'Beranda' - search: 'Cari' - archive: 'Arsip' - category: 'Kategori' - uncategorized: 'Tanpa Kategori' - tag: 'tag' -nav: - next: 'Berikutnya' +common: + archive: + one: 'Arsip' + other: 'Arsip' + category: + one: 'Kategori' + other: 'Kategori' + tag: + one: 'tag' + other: 'tag' + post: + one: 'pos' + other: 'pos' prev: 'Sebelumnya' - older: 'Lebih lama' - newer: 'Lebih baru' + next: 'Berikutnya' widget: - recents: 'Terbaru' - archives: 'Arsip' - categories: 'Kategori' - links: 'tautan' - tags: 'tag' - tag_cloud: 'awan tag' -article: - comments: 'Komentar' - share: 'Bagikan' -profile: follow: 'IKUTI' - post: 'pos' - tag: 'tag' - posts: 'pos' - tags: 'tag' + recents: 'Terbaru' + links: 'tautan' + tag_cloud: 'awan tag' + catalogue: 'Catalogue' +article: + more: 'Read More' + comments: 'Komentar' + read: 'read' + about: 'About' + words: 'words' +search: + search: 'Search' + hint: 'Tulis Sesuatu..' insight: hint: 'Tulis Sesuatu..' posts: 'Pos' diff --git a/languages/ja.yml b/languages/ja.yml index faf9ded..00928c7 100644 --- a/languages/ja.yml +++ b/languages/ja.yml @@ -1,32 +1,33 @@ -index: - home: 'ホーム' - search: '検索' - archive: 'アーカイブ' - category: 'カテゴリ' - uncategorized: '未分類' - tag: 'タグ' -nav: - next: '次' +common: + archive: + one: 'アーカイブ' + other: 'アーカイブ' + category: + one: 'カテゴリ' + other: 'カテゴリ' + tag: + one: 'タグ' + other: 'タグ' + post: + one: '投稿' + other: '投稿' prev: '前' - older: '古い記事' - newer: '新しい記事' + next: '次' widget: - recents: '最近の記事' - archives: 'アーカイブ' - categories: 'カテゴリ' - links: 'リンク' - tags: 'タグ' - tag_cloud: 'タグクラウド' -article: - comments: 'コメント' - share: '共有' - catalogue: 'カタログ' -profile: follow: 'フォローする' - post: '投稿' - tag: 'タグ' - posts: '投稿' - tags: 'タグ' + recents: '最近の記事' + links: 'リンク' + tag_cloud: 'タグクラウド' + catalogue: 'カタログ' +article: + more: 'Read More' + comments: 'コメント' + read: 'read' + about: 'About' + words: 'words' +search: + search: 'Search' + hint: 'Type something...' insight: hint: 'Type something...' posts: '投稿' diff --git a/languages/ko.yml b/languages/ko.yml index 88a5b05..4bf28e1 100644 --- a/languages/ko.yml +++ b/languages/ko.yml @@ -1,33 +1,33 @@ -index: - home: '홈' - search: '검색' - archive: '아카이브' - category: '카테고리' - uncategorized: '미지정' - tag: '태그' -nav: - next: '다음' +common: + archive: + one: '아카이브' + other: '아카이브' + category: + one: '카테고리' + other: '카테고리' + tag: + one: '태그' + other: '태그' + post: + one: '포스트' + other: '포스트' prev: '이전' - older: '이전 글' - newer: '다음 글' + next: '다음' widget: + follow: '팔로우' recents: '최근 글' - archives: '아카이브' - categories: '카테고리' links: '링크' - tags: '태그' tag_cloud: '태그 클라우드' + catalogue: '카탈로그' article: more: '자세히 보기' comments: '댓글' - share: '공유하기' - catalogue: '카탈로그' -profile: - follow: '팔로우' - post: '포스트' - tag: '태그' - posts: '포스트' - tags: '태그' + read: 'read' + about: 'About' + words: 'words' +search: + search: 'Search' + hint: 'Type something...' insight: hint: 'Type something...' posts: '포스트' diff --git a/languages/pt-BR.yml b/languages/pt-BR.yml index 6115cd0..5c1a164 100644 --- a/languages/pt-BR.yml +++ b/languages/pt-BR.yml @@ -1,33 +1,33 @@ -index: - home: 'home' - search: 'Pesquisar' - archive: 'arquivo' - category: 'categoria' - uncategorized: 'Sem categoria' - tag: 'tag' -nav: - next: 'Próximo' +common: + archive: + one: 'Arquivo' + other: 'Arquivos' + category: + one: 'Categoria' + other: 'Categorias' + tag: + one: 'Tag' + other: 'Tags' + post: + one: 'Artigo' + other: 'Artigos' prev: 'Anterior' - older: 'Antigos' - newer: 'Novos' + next: 'Próximo' widget: + follow: 'SEGUIR' recents: 'Recentes' - archives: 'Arquivos' - categories: 'Categorias' links: 'Links' - tags: 'Tags' tag_cloud: 'Nuvem de tags' + catalogue: 'Catálogo' article: more: 'Ler Mais' comments: 'Comentarios' - share: 'Compartilhar' - catalogue: 'Catálogo' -profile: - follow: 'SEGUIR' - post: 'Artigo' - tag: 'Tag' - posts: 'Artigos' - tags: 'Tags' + read: 'read' + about: 'About' + words: 'words' +search: + search: 'Search' + hint: 'Digite alguma coisa...' insight: hint: 'Digite alguma coisa...' posts: 'Artigos' @@ -35,4 +35,3 @@ insight: categories: 'Categorias' tags: 'Tags' untitled: '(Untitled)' - diff --git a/languages/ru.yml b/languages/ru.yml index 5e0e1bc..475f5e5 100644 --- a/languages/ru.yml +++ b/languages/ru.yml @@ -1,33 +1,33 @@ -index: - home: 'Главная' - search: 'Поиск' - archive: 'архив' - category: 'категории' - uncategorized: 'без категории' - tag: 'тег' -nav: - next: 'Далее' +common: + archive: + one: 'архив' + other: 'архивы' + category: + one: 'категории' + other: 'категории' + tag: + one: 'тег' + other: 'теги' + post: + one: 'пост' + other: 'посты' prev: 'Назад' - older: 'Старые' - newer: 'Новые' + next: 'Далее' widget: + follow: 'Подписаться' recents: 'недавние' - archives: 'архивы' - categories: 'категории' links: 'ссылки' - tags: 'теги' tag_cloud: 'облако тегов' + catalogue: 'Каталог' article: more: 'Читать дальше' comments: 'Комментарии' - share: 'Поделиться' - catalogue: 'Каталог' -profile: - follow: 'Подписаться' - post: 'пост' - tag: 'тег' - posts: 'посты' - tags: 'теги' + read: 'read' + about: 'About' + words: 'words' +search: + search: 'Search' + hint: 'Введите что-нибудь...' insight: hint: 'Введите что-нибудь...' posts: 'посты' diff --git a/languages/tr.yml b/languages/tr.yml index 2189334..8a7ba87 100644 --- a/languages/tr.yml +++ b/languages/tr.yml @@ -1,33 +1,33 @@ -index: - home: 'Anasayfa' - search: 'Ara' - archive: 'Arşiv' - category: 'Kategori' - uncategorized: 'Kategorisiz' - tag: 'Etiket' -nav: - next: 'Sonraki' +common: + archive: + one: 'Arşiv' + other: 'Arşivler' + category: + one: 'Kategori' + other: 'Kategoriler' + tag: + one: 'Etiket' + other: 'Etiketler' + post: + one: 'Gönderi' + other: 'Gönderiler' prev: 'Önceki' - older: 'Eski' - newer: 'Yeni' + next: 'Sonraki' widget: + follow: 'TAKİP ET' recents: 'Son' - archives: 'Arşivler' - categories: 'Kategoriler' links: 'Linkler' - tags: 'Etiketler' tag_cloud: 'Etiket bulutu' + catalogue: 'Katalog' article: more: 'Daha fazla oku' comments: 'Yorumlar' - share: 'Paylaş' - catalogue: 'Katalog' -profile: - follow: 'TAKİP ET' - post: 'Gönderi' - tag: 'Etiket' - posts: 'Gönderi' - tags: 'Etiket' + read: 'read' + about: 'About' + words: 'words' +search: + search: 'Search' + hint: 'Bir şeyler yaz...' insight: hint: 'Bir şeyler yaz...' posts: 'Gönderiler' diff --git a/languages/zh-CN.yml b/languages/zh-CN.yml index 6bdc98e..b3fadda 100644 --- a/languages/zh-CN.yml +++ b/languages/zh-CN.yml @@ -1,37 +1,39 @@ -index: - home: '主页' - search: '搜索' - archive: '归档' - category: '分类' - uncategorized: '未分类' - tag: '标签' -nav: - next: '下一页' +common: + archive: + one: '文档' + other: '文档' + category: + one: '分类' + other: '分类' + tag: + one: '标签' + other: '标签' + post: + one: '文章' + other: '文章' prev: '上一页' - older: '下一篇' - newer: '上一篇' + next: '下一页' widget: - recents: '最新文章' - archives: '归档' - categories: '分类' - links: '链接' - tags: '标签' - tag_cloud: '标签云' -article: - more: '查看更多' - comments: '评论' - share: '分享到' - catalogue: '文章目录' -profile: follow: '关注我' - post: '文章' - tag: '标签' - posts: '文章' - tags: '标签' + recents: '最新文章' + links: '链接' + tag_cloud: '标签云' + catalogue: '目录' +article: + more: '阅读更多' + comments: '评论' + read: '读完' + about: '大约' + words: '个字' +plugin: + backtotop: '回到顶端' +search: + search: '搜索' + hint: '想要查找什么...' insight: hint: '想要查找什么...' posts: '文章' pages: '页面' categories: '分类' tags: '标签' - untitled: '(未命名)' + untitled: '(无标题)' diff --git a/languages/zh-TW.yml b/languages/zh-TW.yml index 106d16d..6791f2a 100644 --- a/languages/zh-TW.yml +++ b/languages/zh-TW.yml @@ -1,33 +1,33 @@ -index: - home: '主頁' - search: '搜尋' - archive: '歸檔' - category: '分類' - uncategorized: '未分類' - tag: '標籤' -nav: - next: '下一頁' +common: + archive: + one: '歸檔' + other: '歸檔' + category: + one: '分類' + other: '分類' + tag: + one: '標籤' + other: '標籤' + post: + one: '文章' + other: '文章' prev: '上一頁' - older: '下一篇' - newer: '上一篇' + next: '下一頁' widget: + follow: '關注我' recents: '最新文章' - archives: '歸檔' - categories: '分類' links: '連結' - tags: '標籤' tag_cloud: '標籤雲' + catalogue: '文章目錄' article: more: '繼續閱讀' comments: '評論' - share: '分享到' - catalogue: '文章目錄' -profile: - follow: '關注我' - post: '文章' - tag: '標籤' - posts: '文章' - tags: '標籤' + read: 'read' + about: 'About' + words: 'words' +search: + search: '搜索' + hint: 'Type something...' insight: hint: 'Type something...' posts: '文章' diff --git a/layout/archive.ejs b/layout/archive.ejs index 5cedcb3..b6f054a 100644 --- a/layout/archive.ejs +++ b/layout/archive.ejs @@ -1,32 +1,56 @@ -
- <% var last; - page.posts.each(function(post, i) { - var year = post.date.year(); - if (last != year) { - last = year; %> -
- -

<%= year %>

-
- <% } %> -
- -
- <%- partial('common/post/title', { post: post, index: true, class_name: 'timeline-article-title' }) %> - +<% } +if (!page.year) { + let years = {}; + page.posts.each(p => years[p.date.year()] = null); + for (let year of Object.keys(years).sort((a, b) => b - a)) { + let posts = page.posts.filter(p => p.date.year() == year); %> + <%- buildArchive(posts, year, null) %> + <% } +} else { %> +<%- buildArchive(page.posts, page.year, page.month) %> +<% } %> <% if (page.total > 1) { %> - + <%- partial('common/paginator') %> <% } %> \ No newline at end of file diff --git a/layout/categories.ejs b/layout/categories.ejs index bcca277..09655f8 100644 --- a/layout/categories.ejs +++ b/layout/categories.ejs @@ -1,10 +1,30 @@ -
-
- <%= page.title %> +<% function build_list(categories) { + return categories.map(category => { + let result = `
  • + + + ${category.name} + + + ${category.count} + + `; + if (category.hasOwnProperty('children')) { + result += '
      ' + build_list(category.children) + '
    '; + } + return result + '
  • '; + }).join(''); +} +%> +
    +
    +
    -
    - <% if(site.categories.length) { %> - <%- list_categories(site.categories) %> - <% } %> -
    -
    \ No newline at end of file +
    \ No newline at end of file diff --git a/layout/category.ejs b/layout/category.ejs index a57e99c..9114435 100644 --- a/layout/category.ejs +++ b/layout/category.ejs @@ -1 +1,14 @@ -<%- partial('common/timeline', { type: 'category' }) %> \ No newline at end of file +
    +
    + +
    +
    +<%- partial('index', { page }) %> \ No newline at end of file diff --git a/layout/comment/changyan.ejs b/layout/comment/changyan.ejs index dc63230..eaddb6b 100644 --- a/layout/comment/changyan.ejs +++ b/layout/comment/changyan.ejs @@ -1,12 +1,14 @@ -<% if (typeof(script) !== 'undefined' && script) { %> - - - +<% if (!has_config('comment.appid') || !has_config('comment.conf')) { %> +
    + You forgot to set the appid or conf for Changyan. Please set it in _config.yml. +
    <% } else { %> -
    +
    + + <% } %> \ No newline at end of file diff --git a/layout/comment/counter.ejs b/layout/comment/counter.ejs deleted file mode 100644 index 849facc..0000000 --- a/layout/comment/counter.ejs +++ /dev/null @@ -1,17 +0,0 @@ -<% if (post.comments) { %> - <% if (theme.comment.disqus) { %> - <%= __('article.comments') %> - <% } else if (theme.comment.duoshuo) { %> - <%= __('article.comments') %> - <% } else if (theme.comment.youyan) { %> - <%= __('article.comments') %> - <% } else if (theme.comment.isso) { %> - <%= __('article.comments') %> - <% } else if (theme.comment.facebook) { %> - 0 <%= __('article.comments') %> - <% } else if (theme.comment.changyan && theme.comment.changyan.appid && theme.comment.changyan.conf) { %> - <%= __('article.comments') %> - <% } else if (theme.comment.livere) { %> - <%= __('article.comments') %> - <% } %> -<% } %> \ No newline at end of file diff --git a/layout/comment/disqus.ejs b/layout/comment/disqus.ejs index ba4a566..f36876d 100644 --- a/layout/comment/disqus.ejs +++ b/layout/comment/disqus.ejs @@ -1,20 +1,22 @@ -<% if (typeof(script) !== 'undefined' && script) { %> - -<% } else { %> -
    - +<% } %> +
    + <% if (!has_config('comment.shortname')) { %> +
    + You forgot to set the shortname for Disqus. Please set it in _config.yml.
    -<% } %> \ No newline at end of file + <% } %> + +
    \ No newline at end of file diff --git a/layout/comment/duoshuo.ejs b/layout/comment/duoshuo.ejs deleted file mode 100644 index 63f4c61..0000000 --- a/layout/comment/duoshuo.ejs +++ /dev/null @@ -1,52 +0,0 @@ -<% if (typeof(script) !== 'undefined' && script) { %> - -<% } else { %> -
    - -<% } %> \ No newline at end of file diff --git a/layout/comment/facebook.ejs b/layout/comment/facebook.ejs index 84dae84..f3c7c69 100644 --- a/layout/comment/facebook.ejs +++ b/layout/comment/facebook.ejs @@ -1,11 +1,8 @@ -<% if (typeof(script) !== 'undefined' && script) { %> -<% } else { %> -
    -<% } %> +
    diff --git a/layout/comment/gitment.ejs b/layout/comment/gitment.ejs index 682f28d..e5e3047 100644 --- a/layout/comment/gitment.ejs +++ b/layout/comment/gitment.ejs @@ -1,17 +1,23 @@ -<% if (typeof(script) !== 'undefined' && script) { %> - - - +<% if (!has_config('comment.owner') || !has_config('comment.repo') || !has_config('comment.client_id') || + !has_config('comment.client_secret')) { %> +
    + You forgot to set the owner, repo, client_id, or client_secret for Gitment. + Please set it in _config.yml. +
    +<% } else { %> +
    + + + <% } %> \ No newline at end of file diff --git a/layout/comment/index.ejs b/layout/comment/index.ejs deleted file mode 100644 index 3f6abc6..0000000 --- a/layout/comment/index.ejs +++ /dev/null @@ -1,28 +0,0 @@ -<% if (post.comments) { %> - <% if (theme.comment.disqus) { %> -
    <%- partial('comment/disqus') %>
    - <% } else if (theme.comment.duoshuo) { %> -
    <%- partial('comment/duoshuo') %>
    - <% } else if (theme.comment.youyan) { %> -
    <%- partial('comment/youyan') %>
    - <% } else if (theme.comment.facebook) { %> -
    <%- partial('comment/facebook') %>
    - <% } else if (theme.comment.isso) { %> -
    <%- partial('comment/isso') %>
    - <% } else if (theme.comment.changyan && theme.comment.changyan.appid && theme.comment.changyan.conf) { %> -
    <%- partial('comment/changyan') %>
    - <% } else if (theme.comment.livere) { %> -
    <%- partial('comment/livere') %>
    - <% } else if (theme.comment.valine && - theme.comment.valine.on && - theme.comment.valine.appId && - theme.comment.valine.appKey) { %> -
    <%- partial('comment/valine') %>
    - <% } else if (theme.comment.gitment && - theme.comment.gitment.owner && - theme.comment.gitment.repo && - theme.comment.gitment.client_id && - theme.comment.gitment.client_secret) { %> -
    <%- partial('comment/gitment') %>
    - <% } %> -<% } %> \ No newline at end of file diff --git a/layout/comment/isso.ejs b/layout/comment/isso.ejs index a10e53e..f0942e3 100644 --- a/layout/comment/isso.ejs +++ b/layout/comment/isso.ejs @@ -1,8 +1,10 @@ -<% if (typeof(script) !== 'undefined' && script) { %> - +<% if (!has_config('comment.url')) { %> +
    + You forgot to set the url for Isso. Please set it in _config.yml. +
    <% } else { %> -
    +
    + <% } %> diff --git a/layout/comment/livere.ejs b/layout/comment/livere.ejs index 562d0a1..000ec66 100644 --- a/layout/comment/livere.ejs +++ b/layout/comment/livere.ejs @@ -1,22 +1,22 @@ -<% if (typeof(script) !== 'undefined' && script) { %> - - - - - - +<% if (!has_config('comment.uid')) { %> +
    + You forgot to set the uid for LiveRe. Please set it in _config.yml. +
    <% } else { %> -
    >
    +
    + + +
    <% } %> \ No newline at end of file diff --git a/layout/comment/scripts.ejs b/layout/comment/scripts.ejs deleted file mode 100644 index 6367516..0000000 --- a/layout/comment/scripts.ejs +++ /dev/null @@ -1,26 +0,0 @@ -<% if (theme.comment.disqus) { %> - <%- partial('comment/disqus', { script: true }) %> -<% } else if (theme.comment.duoshuo) { %> - <%- partial('comment/duoshuo', { script: true }) %> -<% } else if (theme.comment.youyan) { %> - <%- partial('comment/youyan', { script: true }) %> -<% } else if (theme.comment.facebook) { %> - <%- partial('comment/facebook', { script: true }) %> -<% } else if (theme.comment.isso) { %> - <%- partial('comment/isso', { script: true }) %> -<% } else if (theme.comment.changyan && theme.comment.changyan.appid && theme.comment.changyan.conf) { %> - <%- partial('comment/changyan', { script: true }) %> -<% } else if (theme.comment.gitment && - theme.comment.gitment.owner && - theme.comment.gitment.repo && - theme.comment.gitment.client_id && - theme.comment.gitment.client_secret) { %> - <%- partial('comment/gitment', { script: true }) %> -<% } else if (theme.comment.livere) { %> - <%- partial('comment/livere', { script: true }) %> -<% } else if (theme.comment.valine && - theme.comment.valine.on && - theme.comment.valine.appId && - theme.comment.valine.appKey) { %> - <%- partial('comment/valine', { script: true }) %> -<% } %> diff --git a/layout/comment/valine.ejs b/layout/comment/valine.ejs index 13f3691..657b335 100644 --- a/layout/comment/valine.ejs +++ b/layout/comment/valine.ejs @@ -1,16 +1,19 @@ -<% if (typeof(script) !== 'undefined' && script) { %> - - - +<% if (!has_config('comment.app_id') || !has_config('comment.app_key')) { %> +
    + You forgot to set the app_id or app_key for Valine. Please set it in _config.yml. +
    <% } else { %> -
    +
    + + + <% } %> \ No newline at end of file diff --git a/layout/comment/youyan.ejs b/layout/comment/youyan.ejs deleted file mode 100644 index d45ed2f..0000000 --- a/layout/comment/youyan.ejs +++ /dev/null @@ -1,5 +0,0 @@ -<% if (typeof(script) !== 'undefined' && script) { %> - -<% } else { %> -
    -<% } %> \ No newline at end of file diff --git a/layout/common/article.ejs b/layout/common/article.ejs index 5464837..0dacfae 100644 --- a/layout/common/article.ejs +++ b/layout/common/article.ejs @@ -1,47 +1,103 @@ -
    -
    - <% if (post.banner) { %> - <%- partial('post/banner') %> - <% } %> - <% if (post.link || post.title) { %> -
    - <%- partial('post/title', { class_name: 'article-title' }) %> - <% if (post.layout != 'page') { %> - - <% } %> -
    - <% } %> - <%- partial('post/gallery') %> -
    - <% if (index && post.excerpt) { %> -

    <%- post.excerpt %>

    -

    - <%= __('article.more') %> -

    - <% } else { %> - <% if (!index && post.toc) { %> -
    - <%= __('article.catalogue') %> - <%- toc(post.content) %> -
    - <% } %> - <%- post.content %> - <% } %> -
    -
    - <%- partial('share/index', { post: post }) %> - <%- partial('comment/counter', { post: post }) %> -
    +
    + <% if (has_thumbnail(post)) { %> + - <% if (!index) { %> - <%- partial('post/nav') %> <% } %> -
    +
    + <% if (post.layout != 'page') { %> + + <% } %> +

    + <% if (index) { %> + <%= post.title %> + <% } else { %> + <%= post.title %> + <% } %> +

    +
    + <%- index && post.excerpt ? post.excerpt : post.content %> +
    + <% if (!index && post.tags && post.tags.length) { %> +
    +
    +
    + # + <%- list_tags(post.tags, { + class: 'has-link-grey ', + show_count: false, + style: 'link' + }) %> +
    +
    +
    + <% } %> + <% if (index && post.excerpt) { %> + + <% } %> + <% if (!index && has_config('share.type')) { %> + <%- partial('share/' + get_config('share.type')) %> + <% } %> +
    +
    -<% if (!index) { %> - <%- partial('comment/index') %> +<% if (!index && (post.prev || post.next)) { %> +
    +
    + <% if (post.prev){ %> + + <% } %> + <% if (post.next){ %> + + <% } %> +
    +
    +<% } %> + +<% if (!index && has_config('comment.type')) { %> +
    +
    +

    <%= __('article.comments') %>

    + <%- partial('comment/' + get_config('comment.type')) %> +
    +
    <% } %> \ No newline at end of file diff --git a/layout/common/footer.ejs b/layout/common/footer.ejs index e9c9067..05926f7 100644 --- a/layout/common/footer.ejs +++ b/layout/common/footer.ejs @@ -1,8 +1,39 @@ -