diff --git a/.travis.yml b/.travis.yml index 8c633ad..f5db5a4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,9 +1,8 @@ language: node_js node_js: - "8" - - "9" - "10" - - "11" + - "12" git: depth: 5 quiet: true diff --git a/README.md b/README.md index 06782d5..832b4d9 100644 --- a/README.md +++ b/README.md @@ -6,10 +6,11 @@ > 现代 · 强大 · 简洁
-
+
-
+
+
diff --git a/_config.example.yml b/_config.example.yml
index e808df8..360ccbb 100644
--- a/_config.example.yml
+++ b/_config.example.yml
@@ -372,13 +372,11 @@ vendors:
# You can check current the Suka Version Code at the end of theme config.
suka:
style_css:
- lazyload_img:
local_search_js:
hanabi_browser_js:
highlight_theme:
- gallery_css:
- gallery_js:
# You can fill in any suka built in highlight.js theme you want here, and it will override the prism theme config before.
+ lazyload_img:
# Spectre.css
# https://picturepan2.github.io/spectre/
# Version: 0.5.3
@@ -425,4 +423,4 @@ old_verison:
- 1.1.1
- 1.2.0
- 1.3.0
- - 1.3.2
\ No newline at end of file
+ - 1.3.2
diff --git a/includes/filter/prism.js b/includes/filter/prism.js
new file mode 100755
index 0000000..3e1ca2d
--- /dev/null
+++ b/includes/filter/prism.js
@@ -0,0 +1,85 @@
+/* hexo-prism-plugin
+ * author: ele828
+ * license: MIT
+ * patched by SukkaW for hexo-theme-suka
+ */
+
+'use strict';
+
+module.exports = function (hexo) {
+ // Plugin settings
+ const config = hexo.config.suka_theme.prism;
+
+ if (config.enable !== true) {
+ return;
+ }
+
+ const Prism = require('node-prismjs');
+
+ const map = {
+ ''': '\'',
+ '&': '&',
+ '>': '>',
+ '<': '<',
+ '"': '"'
+ };
+
+ const regex = /
([\s\S]*?)<\/code><\/pre>/igm;
+ const captionRegex = /(?![\s\S]*<\/p>/igm;
+
+ const line_number = config.line_number || true;
+
+ /**
+ * Unescape from Marked escape
+ * @param {String} str
+ * @return {String}
+ */
+ function unescape(str) {
+ if (!str || str === null) return '';
+ const re = new RegExp('(' + Object.keys(map).join('|') + ')', 'g');
+ return String(str).replace(re, (match) => map[match]);
+ }
+
+ /**
+ * Code transform for prism plugin.
+ * @param {Object} data
+ * @return {Object}
+ */
+ function PrismPlugin(data) {
+ // Patch for caption support
+ if (captionRegex.test(data.content)) {
+ // Attempt to parse the code
+ data.content = data.content.replace(captionRegex, (origin, lang, caption, code) => {
+ if (!lang || !caption || !code) return origin;
+ return `${caption} ${code}
`;
+ });
+ }
+
+ data.content = data.content.replace(regex, (origin, lang, code) => {
+ const lineNumbers = line_number ? 'line-numbers' : '';
+ const startTag = ``;
+ const endTag = `
`;
+ code = unescape(code);
+ let parsedCode = '';
+ if (Prism.languages[lang]) {
+ parsedCode = Prism.highlight(code, Prism.languages[lang]);
+ } else {
+ parsedCode = code;
+ }
+ if (line_number) {
+ const match = parsedCode.match(/\n(?!$)/g);
+ const linesNum = match ? match.length + 1 : 1;
+ let lines = new Array(linesNum + 1);
+ lines = lines.join('');
+ const startLine = ' ';
+ parsedCode += startLine + lines + endLine;
+ }
+ return startTag + parsedCode + endTag;
+ });
+
+ return data;
+ }
+
+ hexo.extend.filter.register('after_post_render', PrismPlugin);
+};
\ No newline at end of file
diff --git a/includes/generator/search.js b/includes/generator/search.js
new file mode 100644
index 0000000..def2683
--- /dev/null
+++ b/includes/generator/search.js
@@ -0,0 +1,82 @@
+module.exports = function (hexo) {
+ if (hexo.config.suka_theme.search.enable !== true) {
+ return;
+ }
+
+ const pathFn = require('path');
+ const { stripHTML } = require('hexo-util');
+
+ let config = hexo.config.suka_theme.search;
+
+ // Set default search path
+ if (!config.path) config.path = 'search.json';
+
+ function searchGenerator(locals = {}) {
+ const url_for = hexo.extend.helper.get('url_for').bind(this);
+
+ const parse = (item) => {
+ let _item = {};
+ if (item.title) _item.title = item.title;
+ if (item.date) _item.date = item.date;
+ if (item.path) _item.url = url_for(item.path);
+ if (item.tags && item.tags.length > 0) {
+ _item.tags = [];
+ item.tags.forEach((tag) => {
+ _item.tags.push(tag.name);
+ });
+ }
+ if (item.categories && item.categories.length > 0) {
+ _item.categories = [];
+ item.categories.forEach((cate) => {
+ _item.categories.push(cate.name);
+ });
+ }
+ if (item._content) {
+ _item.content = stripHTML(item.content.trim().replace(//gs, ''))
+ .replace(/\n/g, ' ').replace(/\s+/g, ' ')
+ .replace(new RegExp('(https?|ftp|file)://[-A-Za-z0-9+&@#/%?=~_|!:,.;]+[-A-Za-z0-9+&@#/%=~_|]', 'g'), '');
+ }
+ return _item;
+ };
+
+ const searchfield = config.field;
+
+ let posts,
+ pages;
+
+ if (searchfield) {
+ if (searchfield === 'post') {
+ posts = locals.posts.sort('-date');
+ } else if (searchfield === 'page') {
+ pages = locals.pages;
+ } else {
+ posts = locals.posts.sort('-date');
+ pages = locals.pages;
+ }
+ } else {
+ posts = locals.posts.sort('-date');
+ }
+
+ let res = [];
+
+ if (posts) {
+ posts.each((post) => {
+ res.push(parse(post));
+ });
+ }
+ if (pages) {
+ pages.each((page) => {
+ res.push(parse(page));
+ });
+ }
+
+ return {
+ path: config.path,
+ data: JSON.stringify(res)
+ };
+ }
+
+ if (pathFn.extname(config.path) === '.json') {
+ hexo.extend.generator.register('json', searchGenerator);
+ }
+};
\ No newline at end of file
diff --git a/includes/helpers/favicon.js b/includes/helpers/favicon.js
new file mode 100755
index 0000000..fa55348
--- /dev/null
+++ b/includes/helpers/favicon.js
@@ -0,0 +1,42 @@
+const { htmlTag } = require('hexo-util');
+
+module.exports = function (hexo) {
+ hexo.extend.helper.register('favicon', function () {
+ const url_for = hexo.extend.helper.get('url_for').bind(this);
+ const { favicon } = this.theme.head;
+
+ let html = '';
+
+ if (favicon.ico) {
+ html += htmlTag('link', { rel: 'icon', type: 'image/ico', href: url_for(favicon.ico) });
+ }
+ if (favicon.apple_touch_icon) {
+ html += htmlTag('link', { rel: 'apple-touch-icon', sizes: '180x180', href: url_for(favicon.apple_touch_icon) });
+ }
+ if (favicon.large) {
+ html += htmlTag('link', { rel: 'icon', typt: 'image/png', sizes: '192x192', href: url_for(favicon.large) });
+ }
+ if (favicon.medium) {
+ html += htmlTag('link', { rel: 'icon', typt: 'image/png', sizes: '32x32', href: url_for(favicon.medium) });
+ }
+ if (favicon.small) {
+ html += htmlTag('link', { rel: 'icon', typt: 'image/png', sizes: '16x16', href: url_for(favicon.small) });
+ }
+
+ return html;
+ });
+
+ hexo.extend.helper.register('site_logo', function () {
+ const full_url_for = hexo.extend.helper.get('full_url_for').bind(this);
+ const { favicon } = this.theme.head;
+ const getFavicon = (type) => full_url_for(favicon[type]);
+
+ if (favicon.large) return getFavicon('large');
+ if (favicon.apple_touch_icon) return getFavicon('apple_touch_icon');
+ if (favicon.medium) return getFavicon('medium');
+ if (favicon.small) return getFavicon('small');
+ if (favicon.ico) return getFavicon('ico');
+
+ return 'https://theme-suka.skk.moe/demo/img/suka-favicon.png';
+ });
+};
\ No newline at end of file
diff --git a/includes/helpers/page.js b/includes/helpers/page.js
new file mode 100644
index 0000000..aa5abde
--- /dev/null
+++ b/includes/helpers/page.js
@@ -0,0 +1,72 @@
+/**
+ * Generate title string based on page type
+ * @example
+ * <%- page_title(page) %>
+ * <%- page_descr(page) %>
+ * <%- page_tags(page) %>
+ */
+
+const { stripHTML } = require('hexo-util');
+
+module.exports = function (hexo) {
+ 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.__('archive');
+ if (this.is_month()) {
+ title += `: ${page.year}/${page.month}`;
+ } else if (this.is_year()) {
+ title += `: ${page.year}`;
+ }
+ } else if (this.is_category()) {
+ title = `${this.__('category')}: ${page.category}`;
+ } else if (this.is_tag()) {
+ title = `${this.__('tag')}: ${page.tag}`;
+ }
+
+ return [title, hexo.config.title].filter((str) => typeof (str) !== 'undefined' && str.trim() !== '').join(' | ');
+ });
+
+ hexo.extend.helper.register('page_descr', function (page = null) {
+ page = (page === null) ? this.page : page;
+
+ let description = page.description || page.excerpt || page.content || hexo.config.description ;
+
+ description = stripHTML(description).trim() // Remove prefixing/trailing spaces
+ .replace(/^s*/, '').replace(/s*$/, '')
+ .substring(0, 200)
+ .replace(//g, '>')
+ .replace(/&/g, '&')
+ .replace(/"/g, '"')
+ .replace(/'/g, ''')
+ .replace(/\n/g, ' '); // Replace new lines by spaces
+
+ return [description, hexo.config.author, hexo.config.title].filter((str) => typeof (str) !== 'undefined' && str.trim() !== '').join(' - ');
+ });
+
+ hexo.extend.helper.register('page_tags', function (page = null) {
+ page = (page === null) ? this.page : page;
+ const { config } = this;
+
+ let page_tags = page.keywords || page.tags,
+ site_tags = config.keywords || this.theme.head.keywords;
+
+ const parse = (tags) => {
+ let result = [];
+ if (tags) {
+ if (typeof tags === 'string') {
+ result.push(tags);
+ } else if (tags.length) {
+ result.push(tags.map(tag => tag.name ? tag.name : tag).filter(tags => !!tags).join(', '));
+ }
+ }
+ return result;
+ };
+
+ return [parse(page_tags), parse(site_tags)].filter(tags => tags.length && tags.length !== 0).join(', ');
+ });
+};
diff --git a/includes/helpers/qrcode.js b/includes/helpers/qrcode.js
new file mode 100755
index 0000000..f0e93ef
--- /dev/null
+++ b/includes/helpers/qrcode.js
@@ -0,0 +1,25 @@
+const qrImage = require('qr-image');
+
+module.exports = function (hexo) {
+ hexo.extend.helper.register('qrcode', (url, option) => {
+ const qrConfig = Object.assign(
+ {
+ size: 6,
+ margin: 0
+ },
+ option || {}
+ );
+
+ const qrUrl = url.replace('index.html', '');
+
+ const buffer = qrImage.imageSync(
+ qrUrl,
+ {
+ type: 'png',
+ size: qrConfig.size,
+ margin: qrConfig.margin
+ }
+ );
+ return `data:image/png;base64,${buffer.toString('base64')}`;
+ });
+};
\ No newline at end of file
diff --git a/includes/helpers/tags.js b/includes/helpers/tags.js
new file mode 100755
index 0000000..2c57ea7
--- /dev/null
+++ b/includes/helpers/tags.js
@@ -0,0 +1,135 @@
+'use strict';
+
+const urlFn = require('url');
+const moment = require('moment');
+const { escapeHTML, htmlTag, stripHTML } = require('hexo-util');
+
+module.exports = function (hexo) {
+ const { helper } = hexo.extend;
+
+ helper.register('_meta_generator', () => ``);
+
+ helper.register('css_async', (url) => ``);
+
+ helper.register('_open_graph', function (options = {}) {
+ function meta(name, content, escape) {
+ if (escape !== false && typeof content === 'string') {
+ content = escapeHTML(content);
+ }
+
+ return htmlTag('meta', { name, content });
+ }
+
+ function og(name, content, escape) {
+ if (escape !== false && typeof content === 'string') {
+ content = escapeHTML(content);
+ }
+
+ return htmlTag('meta', { property: name, content });
+ }
+
+ const { config, page } = this;
+ const { content } = page;
+
+ let images = options.image || options.images || page.photos || [];
+
+ const description = helper.get('page_descr').bind(this)();
+ const _tags = helper.get('page_tags').bind(this)();
+
+ let keywords;
+
+ const _title = helper.get('page_title').bind(this);
+ const title = (() => _title())();
+
+ const type = options.type || (this.is_post() ? 'article' : 'website');
+ const url = (options.url || this.url).replace('index.html', '');
+ const siteName = options.site_name || config.title;
+ const date = options.date !== false ? options.date || page.date : false;
+ const updated = options.updated !== false ? options.updated || page.updated : false;
+ const thumbnail = page.thumbnail;
+ const language = options.language || config.language;
+ const author = config.author;
+
+ // Images
+ if (!Array.isArray(images)) images = [images];
+
+ if (!images.length && content) {
+ images = images.slice();
+
+ if (content.includes(']*src=['"]([^'"]+)([^>]*>)/gi;
+ while (img = imgPattern.exec(content)) {
+ images.push(img[1]);
+ }
+ }
+ }
+ images = images.map(path => {
+ if (!urlFn.parse(path).host) {
+ // resolve `path`'s absolute path relative to current page's url
+ // `path` can be both absolute (starts with `/`) or relative.
+ return urlFn.resolve(url || config.url, path);
+ }
+
+ return path;
+ });
+
+ if (thumbnail) images.unshift(thumbnail);
+
+
+ let result = '';
+
+ result += og('og:title', title);
+ result += og('og:site_name', siteName);
+ result += og('og:type', type);
+ result += og('og:url', url, false);
+
+ if (language) result += og('og:locale', language, false);
+
+ if (description) result += meta('description', description, false);
+ if (_tags) {
+ if (typeof _tags === 'string') {
+ keywords = _tags
+ } else if (_tags.length) {
+ keywords = _tags.map(tag => tag.name ? tag.name : tag).filter(keyword => !!keyword).join()
+ }
+ result += meta('keywords', keywords);
+ }
+
+ images.forEach(path => {
+ result += og('og:image', path, false);
+ });
+
+ if (date) {
+ if ((moment.isMoment(date) || moment.isDate(date)) && !isNaN(date.valueOf())) {
+ result += og('article:published_time', date.toISOString());
+ }
+ }
+
+ if (updated) {
+ if ((moment.isMoment(updated) || moment.isDate(updated)) && !isNaN(updated.valueOf())) {
+ result += og('article:modified_time', updated.toISOString());
+ result += og('og:updated_time', updated.toISOString());
+ }
+ }
+
+ if (this.is_post()) {
+ if (author) {
+ result += og('article:author', author);
+ }
+
+ if (_tags) {
+ result += og('article:tag', keywords);
+ }
+
+ if (thumbnail) {
+ result += meta('twitter:card', 'summary_large_image');
+ result += meta('twitter:image', thumbnail, false);
+ } else {
+ result += meta('twitter:card', 'summary');
+ }
+ };
+
+ return result.trim();
+ });
+};
diff --git a/includes/tasks/check_deps.js b/includes/tasks/check_deps.js
new file mode 100644
index 0000000..c511819
--- /dev/null
+++ b/includes/tasks/check_deps.js
@@ -0,0 +1,26 @@
+const logger = require('hexo-log')();
+const pkg = require('../../package.json');
+
+const depsList = Object.keys(pkg.dependencies);
+
+function checkDep(name) {
+ try {
+ require.resolve(name);
+ return true;
+ } catch(e) {
+ logger.error(`Package ${name} is not installed.`);
+ }
+ return false;
+}
+
+logger.info('Checking dependencies');
+
+const missingDeps = depsList.map(checkDep).some(installed => !installed);
+
+if (missingDeps) {
+ logger.error('Please install the missing dependencies.');
+ logger.error('You can enter suka-theme directory and run following commands:');
+ logger.error('$ npm i --production');
+ logger.error('$ yarn --production # If you prefer yarn.');
+ process.exit(-1);
+}
\ No newline at end of file
diff --git a/includes/tasks/check_hexo.js b/includes/tasks/check_hexo.js
new file mode 100755
index 0000000..ec31f33
--- /dev/null
+++ b/includes/tasks/check_hexo.js
@@ -0,0 +1,11 @@
+const logger = require('hexo-log')();
+
+module.exports = function (hexo) {
+ if (!(/4.+?/).test(hexo.version)) {
+ logger.error('Please update Hexo to v4.0.0 or greater!');
+ logger.error('You can run following commands at your site directory:');
+ logger.error('$ npm i hexo@4');
+ logger.error('$ yarn add hexo@4 # If you prefer yarn.');
+ process.exit(-1);
+ }
+}
\ No newline at end of file
diff --git a/includes/tasks/welcome.js b/includes/tasks/welcome.js
new file mode 100644
index 0000000..b7f4f8d
--- /dev/null
+++ b/includes/tasks/welcome.js
@@ -0,0 +1,11 @@
+const logger = require('hexo-log')();
+
+logger.info(`--------------------------------------------------------
+ ____ _ _____ _
+/ ___| _ _| | ____ _ |_ _| |__ ___ _ __ ___ ___
+\\___ \\| | | | |/ / _\` | | | | '_ \\ / _ \\ '_ \` _ \\ / _ \\
+ ___) | |_| | < (_| | | | | | | | __/ | | | | | __/
+|____/ \\__,_|_|\\_\\__,_| |_| |_| |_|\\___|_| |_| |_|\\___|
+
+hexo-theme-suka ( https://theme-suka.skk.moe )
+--------------------------------------------------------------`);
diff --git a/layout/_pages/archive.ejs b/layout/_pages/archive.ejs
index 2e9cf0a..a21298c 100644
--- a/layout/_pages/archive.ejs
+++ b/layout/_pages/archive.ejs
@@ -19,15 +19,8 @@
- <% var last = 0, year, yearArr = []; %>
-
- <% page.posts.sort('date', -1).each(function(post) { %>
- <% year = post.date.year(); %>
- <% if (last != year){ %>
- <% if (yearArr.length != 0){ %>
- <% } %>
- <% yearArr.push(year); %>
- <% last = year; %>
+ <% function buildArchive(posts, year, month = null) {
+ const time = moment([page.year, (page.month) ? page.month - 1 : null].filter(i => i !== null)); %>
@@ -35,27 +28,12 @@
-
- <%= year %>
-
-
-
-
-
-
-
-
-
-
-
-
- <%= date(post.date, 'MM-DD') %>
- <%- post.title %>
+ <%= (month === null) ? year : (time.locale((config.language) ? config.language : 'en').format('MMMM YYYY')) %>
- <% } else { %>
+ <% posts.each(post => { %>
@@ -69,9 +47,18 @@
+ <% }) %>
+ <% } %>
+ <% if (!page.year) {
+ let years = {};
+ page.posts.each(post => years[post.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) %>
<% } %>
- <% }) %>
-
diff --git a/layout/_pages/gallery.ejs b/layout/_pages/gallery.ejs
deleted file mode 100644
index 3ff589f..0000000
--- a/layout/_pages/gallery.ejs
+++ /dev/null
@@ -1,12 +0,0 @@
-
- <% if (site.data.gallery) { %>
- <% for (var i in site.data.gallery) { %>
-
-
-
- <% } %>
- <% } %>
-
diff --git a/layout/_pages/links.ejs b/layout/_pages/links.ejs
index c505552..64ec7f5 100644
--- a/layout/_pages/links.ejs
+++ b/layout/_pages/links.ejs
@@ -3,7 +3,7 @@
<% if (site.data.links) { %>