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 @@ > 现代 · 强大 · 简洁

-Version +Version Author Hexo -node.js +node.js +GitHub repo size
Build Status Codacy Badge 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 @@ - 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) { %>