├── .gitignore ├── LICENSE ├── README.md ├── _config.yml.example ├── languages ├── de.yml ├── en.yml ├── es.yml ├── fr.yml ├── ko.yml ├── pl.yml ├── ru.yml ├── zh-cn.yml └── zh-tw.yml ├── layout ├── archive.ejs ├── categories.ejs ├── category.ejs ├── comment │ ├── changyan.ejs │ ├── disqus.ejs │ ├── facebook.ejs │ ├── gitment.ejs │ ├── isso.ejs │ ├── livere.ejs │ ├── valine.ejs │ └── youyan.ejs ├── common │ ├── article.ejs │ ├── footer.ejs │ ├── head.ejs │ ├── languages.ejs │ ├── navbar.ejs │ ├── paginator.ejs │ └── scripts.ejs ├── index.ejs ├── layout.ejs ├── plugins │ ├── clipboard.ejs │ ├── gallery.ejs │ ├── google-analytics.ejs │ ├── katex.ejs │ └── mathjax.ejs ├── post.ejs ├── search │ ├── google-cse.ejs │ └── insight.ejs ├── share │ ├── addthis.ejs │ └── sharethis.ejs ├── tag.ejs └── tags.ejs ├── lib ├── i18n.js └── rfc5646.js ├── package-lock.json ├── package.json ├── scripts ├── 01_check.js ├── 10_i18n.js ├── 99_config.js ├── 99_content.js └── 99_tags.js └── source ├── css ├── insight.scss └── style.scss ├── images ├── check.svg ├── exclamation.svg ├── info.svg ├── logo.png ├── question.svg └── quote-left.svg └── js ├── insight.js └── script.js /.gitignore: -------------------------------------------------------------------------------- 1 | _config.yml 2 | _config.*.yml 3 | node_modules -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018 PPOffice 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |
A simple and retro styled Hexo theme, concentrating more on your ideas. 4 |
5 | Preview | 6 | Download | 7 | Documentation 8 |

9 | 10 | ![Minos](http://ppoffice.github.io/hexo-theme-minos/gallery/preview.png "Minos Preview") 11 | 12 | ### :gift: Features 13 | 14 | **Extensive Plugin Support** 15 | 16 | Minos includes plentiful search, comment, sharing and other plugins out of the box. You can choose any of them to enrich your 17 | blog experience, or build your own plugin easily referring to the existing Minos plugins. 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 |
Comment plugins
ChangyanDisqusFacebook comments pluginGitmentIssoLiveReValineYouyan
34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 |
Search pluginsShare plugins
Insight SearchGoogle custom searchAddThisShareThis
46 | 47 | 48 | 49 | 50 | 51 | 52 | 54 | 55 | 56 |
Other plugins
Hexo Tag PluginlightGallery & 53 | Justified GalleryMathJax
57 | 58 | **Rich Code Highlight Theme Choices** 59 | 60 | Minos directly import code highlight themes from the [highlight.js](https://highlightjs.org/) package, and makes more than 61 | 70 highlight themes available to you. 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 |
71 | 72 | **Elastic Theme Configuration** 73 | 74 | In addition to the minimalistic and easy-to-understand configuration design, Minos allows you to set configurations on a 75 | per-page and per-language basis with the ability to merge and override partial configurations. 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 89 | 94 | 101 | 102 | 103 | 104 | 105 | 106 | 107 |
_config.yml_config.zh-cn.ymlpost.md
85 |
menu:
 86 |   Archives: /archives
 87 |   Lifestyle: /categories/LifeStyle
88 |
90 |
menu:
 91 |   归档: /archives
 92 |   生活: /categories/LifeStyle
93 |
95 |
title: A Simple Post
 96 | menu:
 97 |   Go Home: /index.html
 98 | ---
 99 | # Here is some simple markdown.
100 |
108 | 109 | **Multi-language Support** 110 | 111 | Minos supports the using of multiple translations on different pages in one site with its elastic configuration system. 112 | Simply decide your preferred language for each page (or use the [language folder](https://hexo.io/docs/internationalization.html#Path)) 113 | and create an additional `_config..yml`, then you are ready to go. 114 | 115 | **Responsive Layout** 116 | 117 | No matter what modern browsering device your audiences are using, they can always get the best experience because Minos's responsive 118 | layout across multiple viewpoints. 119 | 120 | ![Responsive Layout](https://ppoffice.github.io/hexo-theme-minos/gallery/responsive.png) 121 | 122 | ### :cd: Installation 123 | 124 | Download & extract or `git clone` Minos from GitHub to your blog's theme folder, and that's it! 125 | 126 | ```shell 127 | git clone https://github.com/ppoffice/hexo-theme-minos.git themes/minos 128 | ``` 129 | 130 | Once started, Minos will remind you of any missing dependencies and configuration files. 131 | 132 | ### :hammer: Development 133 | 134 | This project is built with 135 | 136 | - Hexo 3.6.0 137 | - Ejs 138 | - Sass 139 | 140 | Please refer to the documentation for Minos implementation details. 141 | 142 | ### :tada: Contribute 143 | 144 | :electric_plug: Write a plugin | 145 | :triangular_flag_on_post: Report a bug | 146 | :earth_asia: Add a translation 147 | 148 | ### :memo: License 149 | 150 | This project is licensed under the MIT License - see the [LICENSE](https://github.com/ppoffice/hexo-theme-minos/blob/master/LICENSE) file for details. -------------------------------------------------------------------------------- /_config.yml.example: -------------------------------------------------------------------------------- 1 | # Website's icon url. 2 | favicon: /favicon.png 3 | 4 | # Open Graph metadata (https://hexo.io/docs/helpers.html#open-graph) 5 | open_graph: 6 | twitter_id: 7 | twitter_site: 8 | google_plus: 9 | fb_admins: 10 | fb_app_id: 11 | 12 | # Website's logo shown on the left of the navigation bar. 13 | # It can either be an url to an image or a string if the following option is set. 14 | # logo: 15 | # text: 16 | logo: 17 | 18 | article: 19 | # Show word count and estimated reading time. 20 | readtime: true 21 | # Code highlight theme, please see https://highlightjs.org/static/demo/ 22 | highlight: atom-one-light 23 | # Date format: relative | full | relative_full | full_relative 24 | date_format: relative 25 | 26 | # Navigation bar menu links. 27 | menu: 28 | Archives: /archives 29 | Lifestyle: /categories/LifeStyle 30 | Music: /categories/Music 31 | Technology: /categories/Technology 32 | About: /about 33 | 34 | # Search plugin settings. 35 | search: 36 | type: insight 37 | 38 | # Navigation bar toc settings 39 | toc: false 40 | 41 | # Share plugin settings. 42 | share: 43 | type: sharethis 44 | install_url: 45 | 46 | # Comment plugin settings. 47 | comment: 48 | type: disqus 49 | shortname: 50 | 51 | # Other plugins and their settings. 52 | plugins: 53 | mathjax: true 54 | katex: false 55 | gallery: true 56 | clipboard: true 57 | google-analytics: 58 | tracking_id: 59 | 60 | # Additional navigation bar links on the right. 61 | # Links can either be text or icon. The following link is set to be an icon link whose icon 62 | # is set to the class name of a FontAwesome 5 icon. The `footer_links` below also gives an 63 | # example of setting plain text links. 64 | navbar_links: 65 | GitHub: 66 | icon: fab fa-github 67 | url: https://github.com/ppoffice/hexo-theme-minos 68 | 69 | # Links at the bottom of the page. 70 | footer_links: 71 | GitHub: https://github.com/ppoffice/hexo-theme-minos 72 | -------------------------------------------------------------------------------- /languages/de.yml: -------------------------------------------------------------------------------- 1 | name: 'German' 2 | common: 3 | archives: 'Archiv' 4 | category: 'Kategorie' 5 | tag: 'Tag' 6 | categories: 'Kategorien' 7 | tags: 'Tags' 8 | nav: 9 | next: 'Nächstes' 10 | prev: 'Vorheriges' 11 | search: 'Suche' 12 | toc: 'Inhaltsverzeichnis' 13 | article: 14 | read_more: 'mehr' 15 | read: 'lesen' 16 | about: 'Über' 17 | words: 'Wörter' 18 | comments: 'Kommentare' 19 | contents: 'Inhalte' 20 | search: 21 | hint: 'Suche etwas...' 22 | insight: 23 | hint: 'Suche etwas...' 24 | posts: 'Posts' 25 | pages: 'Seiten' 26 | categories: 'Kategorien' 27 | tags: 'Tags' 28 | untitled: '(Ohne Titel)' 29 | -------------------------------------------------------------------------------- /languages/en.yml: -------------------------------------------------------------------------------- 1 | name: 'English' 2 | common: 3 | archives: 'Archives' 4 | category: 'Category' 5 | tag: 'Tag' 6 | categories: 'Categories' 7 | tags: 'Tags' 8 | nav: 9 | next: 'Next' 10 | prev: 'Prev' 11 | search: 'Search' 12 | toc: 'Table of Contents' 13 | article: 14 | read_more: 'Read More' 15 | read: 'read' 16 | about: 'About' 17 | words: 'words' 18 | comments: 'Comments' 19 | contents: 'Contents' 20 | search: 21 | hint: 'Type something...' 22 | insight: 23 | hint: 'Type something...' 24 | posts: 'Posts' 25 | pages: 'Pages' 26 | categories: 'Categories' 27 | tags: 'Tags' 28 | untitled: '(Untitled)' 29 | powered_by: 'Powered by' -------------------------------------------------------------------------------- /languages/es.yml: -------------------------------------------------------------------------------- 1 | name: 'Spanish' 2 | common: 3 | archives: 'Archivo' 4 | category: 'Categoría' 5 | tag: 'Tag' 6 | categories: 'Categorías' 7 | tags: 'Tags' 8 | nav: 9 | next: 'Siguiente' 10 | prev: 'Anterior' 11 | search: 'Buscar' 12 | toc: 'Tabla de Contenidos' 13 | article: 14 | read_more: 'Leer más' 15 | read: 'de lectura' 16 | about: 'Alrededor de' 17 | words: 'palabras' 18 | comments: 'Comentarios' 19 | contents: 'Contents' 20 | search: 21 | hint: 'Escribe algo...' 22 | insight: 23 | hint: 'Escribe algo...' 24 | posts: 'Posts' 25 | pages: 'Páginas' 26 | categories: 'Categorías' 27 | tags: 'Tags' 28 | untitled: '(Sin título)' 29 | -------------------------------------------------------------------------------- /languages/fr.yml: -------------------------------------------------------------------------------- 1 | name: 'Français' 2 | common: 3 | archives: 'Archives' 4 | category: 'Catégorie' 5 | tag: 'Tag' 6 | categories: 'Catégories' 7 | tags: 'Tags' 8 | nav: 9 | next: 'Suivant' 10 | prev: 'Précédent' 11 | search: 'Chercher' 12 | toc: 'Table des Matières' 13 | article: 14 | read_more: 'Lire la Suite' 15 | read: 'lire' 16 | about: 'À propos' 17 | words: 'mots' 18 | comments: 'Commentaires' 19 | contents: 'Sommaire' 20 | search: 21 | hint: 'Que voulez-vous chercher ?' 22 | insight: 23 | hint: 'Que voulez-vous chercher ?' 24 | posts: 'Posts' 25 | pages: 'Pages' 26 | categories: 'Catégories' 27 | tags: 'Tags' 28 | untitled: '(Sans Titre)' -------------------------------------------------------------------------------- /languages/ko.yml: -------------------------------------------------------------------------------- 1 | name: '한국어' 2 | common: 3 | archives: '목록' 4 | category: '카테고리' 5 | tag: '태그' 6 | categories: '카테고리 목록' 7 | tags: '태그 목록' 8 | nav: 9 | next: '다음' 10 | prev: '이전' 11 | search: '검색' 12 | toc: '목차' 13 | article: 14 | read_more: '더 읽기' 15 | read: '소요' 16 | about: '약' 17 | words: '단어' 18 | comments: '댓글' 19 | contents: '본문' 20 | search: 21 | hint: '아무거나 입력하세요' 22 | insight: 23 | hint: '아무거나 입력하세요' 24 | posts: '글' 25 | pages: '페이지' 26 | categories: '카테고리 목록' 27 | tags: '태그 목록' 28 | untitled: '(제목없음)' 29 | -------------------------------------------------------------------------------- /languages/pl.yml: -------------------------------------------------------------------------------- 1 | name: 'Polish' 2 | common: 3 | archives: 'Archiwum' 4 | category: 'Kategoria' 5 | tag: 'Tag' 6 | categories: 'Kategorie' 7 | tags: 'Tagi' 8 | nav: 9 | next: 'Następna strona' 10 | prev: 'Poprzednia strona' 11 | search: 'Szukaj' 12 | toc: 'Spis treści' 13 | article: 14 | read_more: 'Czytaj dalej' 15 | read: 'czytania' 16 | about: 'Około' 17 | words: 'słów' 18 | comments: 'Komentarze' 19 | contents: 'Treść' 20 | search: 21 | hint: 'Zacznij pisać...' 22 | insight: 23 | hint: 'Zacznij pisać...' 24 | posts: 'Posty' 25 | pages: 'Strony' 26 | categories: 'Kategorie' 27 | tags: 'Tagi' 28 | untitled: '(Bez tytułu)' 29 | -------------------------------------------------------------------------------- /languages/ru.yml: -------------------------------------------------------------------------------- 1 | name: 'RU' 2 | common: 3 | archives: 'Архив' 4 | category: 'Категория' 5 | tag: 'Тег' 6 | categories: 'Категории' 7 | tags: 'Теги' 8 | nav: 9 | next: 'Далее' 10 | prev: 'Назад' 11 | search: 'Поиск' 12 | toc: 'Оглавление' 13 | article: 14 | read_more: 'Читать далее' 15 | read: 'читать' 16 | about: 'О' 17 | words: 'Слова' 18 | comments: 'Комментарии' 19 | contents: 'Содержание' 20 | search: 21 | hint: 'Введите строку...' 22 | insight: 23 | hint: 'Введите строку...' 24 | posts: 'Статьи' 25 | pages: 'Страницы' 26 | categories: 'Категории' 27 | tags: 'Теги' 28 | untitled: '(Без названия)' 29 | powered_by: 'Разработано на базе' 30 | -------------------------------------------------------------------------------- /languages/zh-cn.yml: -------------------------------------------------------------------------------- 1 | name: '简体中文' 2 | common: 3 | archives: '归档' 4 | category: '分类' 5 | tag: '标签' 6 | categories: '分类' 7 | tags: '标签' 8 | nav: 9 | next: '下一页' 10 | prev: '上一页' 11 | search: '搜索' 12 | toc: '目录' 13 | article: 14 | read_more: '阅读更多' 15 | read: '读完' 16 | about: '约' 17 | words: '字' 18 | comments: '评论' 19 | contents: '目录' 20 | search: 21 | hint: '站内搜索' 22 | insight: 23 | hint: '站内搜索' 24 | posts: '文章' 25 | pages: '页面' 26 | categories: '分类' 27 | tags: '标签' 28 | untitled: '(无标题)' -------------------------------------------------------------------------------- /languages/zh-tw.yml: -------------------------------------------------------------------------------- 1 | name: '繁體中文' 2 | common: 3 | archives: '封存' 4 | category: '分類' 5 | tag: '標籤' 6 | categories: '分類' 7 | tags: '標籤' 8 | nav: 9 | next: '下一頁' 10 | prev: '上一頁' 11 | search: '搜尋' 12 | toc: '目錄' 13 | article: 14 | read_more: '閱讀更多' 15 | read: '讀完' 16 | about: '約' 17 | words: '字' 18 | comments: '評論' 19 | contents: '目錄' 20 | search: 21 | hint: '站內搜尋' 22 | insight: 23 | hint: '站內搜尋' 24 | posts: '文章' 25 | pages: '頁面' 26 | categories: '分類' 27 | tags: '標籤' 28 | untitled: '(無標題)' 29 | -------------------------------------------------------------------------------- /layout/archive.ejs: -------------------------------------------------------------------------------- 1 |
2 |
3 | <% const groups = {}; 4 | const years = []; 5 | page.posts.each(post => { 6 | const year = post.date.year(); 7 | if (typeof(groups[year]) === 'undefined') { 8 | groups[year] = []; 9 | years.push(year); 10 | } 11 | groups[year].push(post); 12 | }); 13 | years.sort((a, b) => b - a); %> 14 | <% for (let year of years) { %> 15 |
16 |

<%= year %>

17 |
18 | <% for (let post of groups[year].sort((a, b) => b.date.diff(a.date))) { %> 19 |
20 | 22 |
<%= post.title %>
23 |
24 | <% } %> 25 |
26 |
27 | <% } %> 28 | <% if (page.total > 1) { %> 29 | <%- partial('common/paginator') %> 30 | <% } %> 31 |
32 |
-------------------------------------------------------------------------------- /layout/categories.ejs: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
<%= __('common.categories') %>
5 |
6 |
7 |
8 |
9 |
10 |
11 | <% (page._categories || page.categories).forEach(category => {%> 12 | 13 | 14 | 15 | <% }) %> 16 |
17 |
18 |
-------------------------------------------------------------------------------- /layout/category.ejs: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
<%= page.category %>
5 |
6 |
7 |
8 | <%- partial('index', { page }) %> -------------------------------------------------------------------------------- /layout/comment/changyan.ejs: -------------------------------------------------------------------------------- 1 | <% if (!has_config('comment.appid') || !has_config('comment.conf')) { %> 2 |
3 | You forgot to set the appid or conf for Changyan. Please set it in _config.yml. 4 |
5 | <% } else { %> 6 |
7 | 8 | 14 | <% } %> -------------------------------------------------------------------------------- /layout/comment/disqus.ejs: -------------------------------------------------------------------------------- 1 | <% if (has_config('comment.shortname')) { 2 | // Disqus supported languages. 3 | // Last update at 4/18/2018 4 | const SUPPORTED_LANGS = { 5 | 'sq': 'Albanian', 6 | 'ar': 'Arabic', 7 | 'az': 'Azerbaijani', 8 | 'eu': 'Basque', 9 | 'ca': 'Catalan', 10 | 'zh': 'Chinese', 11 | 'cs': 'Czech', 12 | 'da': 'Danish', 13 | 'nl': 'Dutch', 14 | 'en': 'English', 15 | 'fi': 'Finnish', 16 | 'fr': 'French', 17 | 'de': 'German', 18 | 'el': 'Greek', 19 | 'he': 'Hebrew', 20 | 'hu': 'Hungarian', 21 | 'id': 'Indonesian', 22 | 'it': 'Italian', 23 | 'ja': 'Japanese', 24 | 'ko': 'Korean', 25 | 'lv': 'Latvian', 26 | 'nb': 'Norwegian Bokmål', 27 | 'fa': 'Persian', 28 | 'pl': 'Polish', 29 | 'pt': 'Portuguese', 30 | 'pt_BR': 'Portuguese (Brazil)', 31 | 'ro': 'Romanian', 32 | 'ru': 'Russian', 33 | 'sr': 'Serbian', 34 | 'sr@latin': 'Serbian (Latin)', 35 | 'sk': 'Slovak', 36 | 'sl': 'Slovenian', 37 | 'es_AR': 'Spanish (Argentina)', 38 | 'es_MX': 'Spanish (Mexico)', 39 | 'es_ES': 'Spanish (Spain)', 40 | 'sv_SE': 'Swedish (Sweden)', 41 | 'tr': 'Turkish', 42 | 'uk': 'Ukrainian', 43 | 'vec': 'Venetian' 44 | }; 45 | 46 | function getDisqusLanguage(language) { 47 | const variant = language.split(/[-_]/).map((l, i) => i === 0 ? l.toLowerCase() : l.toUpperCase()).join('_'); 48 | if (SUPPORTED_LANGS.hasOwnProperty(variant)) { 49 | return variant; 50 | } 51 | if (SUPPORTED_LANGS.hasOwnProperty(variant.split('_')[0])) { 52 | return variant.split('_')[0]; 53 | } 54 | return 'en'; 55 | } 56 | %> 57 | 72 | <% } %> 73 |
74 | <% if (!has_config('comment.shortname')) { %> 75 |
76 | You forgot to set the shortname for Disqus. Please set it in _config.yml. 77 |
78 | <% } %> 79 | 80 |
-------------------------------------------------------------------------------- /layout/comment/facebook.ejs: -------------------------------------------------------------------------------- 1 | 8 |
9 | -------------------------------------------------------------------------------- /layout/comment/gitment.ejs: -------------------------------------------------------------------------------- 1 | <% if (!has_config('comment.owner') || !has_config('comment.repo') || !has_config('comment.client_id') || 2 | !has_config('comment.client_secret')) { %> 3 |
4 | You forgot to set the owner, repo, client_id, or client_secret for Gitment. 5 | Please set it in _config.yml. 6 |
7 | <% } else { %> 8 |
9 | 10 | 11 | 23 | <% } %> -------------------------------------------------------------------------------- /layout/comment/isso.ejs: -------------------------------------------------------------------------------- 1 | <% if (!has_config('comment.url')) { %> 2 |
3 | You forgot to set the url for Isso. Please set it in _config.yml. 4 |
5 | <% } else { %> 6 |
7 | 9 | <% } %> 10 | 11 | -------------------------------------------------------------------------------- /layout/comment/livere.ejs: -------------------------------------------------------------------------------- 1 | <% if (!has_config('comment.uid')) { %> 2 |
3 | You forgot to set the uid for LiveRe. Please set it in _config.yml. 4 |
5 | <% } else { %> 6 |
7 | 20 | 21 |
22 | <% } %> -------------------------------------------------------------------------------- /layout/comment/valine.ejs: -------------------------------------------------------------------------------- 1 | <% if (!has_config('comment.app_id') || !has_config('comment.app_key')) { %> 2 |
3 | You forgot to set the appId or appKey for Valine. Please set it in _config.yml. 4 |
5 | <% } else { %> 6 |
7 | 8 | 9 | 23 | <% } %> 24 | -------------------------------------------------------------------------------- /layout/comment/youyan.ejs: -------------------------------------------------------------------------------- 1 | <% if (!has_config('comment.uid')) { %> 2 |
3 | You forgot to set the uid for Youyan. Please set it in _config.yml. 4 |
5 | <% } else { %> 6 | 7 |
8 | <% } %> -------------------------------------------------------------------------------- /layout/common/article.ejs: -------------------------------------------------------------------------------- 1 |
2 |

3 | <% if (index) { %> 4 | 5 | <% } else { %> 6 | <%= post.title %> 7 | <% } %> 8 |

9 | 41 |
42 | <% if (post.excerpt && index){ %> 43 | <%- post.excerpt %> 44 |

45 | <%= __('article.read_more') %> 46 |

47 | <% } else { %> 48 | <%- post.content %> 49 | <% } %> 50 |
51 | <% if (!index && post.tags && post.tags.length){ %> 52 |
53 | <% (post._tags || post.tags).forEach(tag => { %> 54 | 55 | <% }) %> 56 |
57 | <% } %> 58 | <% if (!index && (post.prev || post.next)) { %> 59 |
60 | 61 | <% if (post.prev){ %> 62 | <%= post.prev.title %> 63 | <% } %> 64 | 65 | 66 | <% if (post.next){ %> 67 | <%= post.next.title %> 68 | <% } %> 69 | 70 |
71 | <% } %> 72 |
73 | 74 | <% if (!index && has_config('share.type')) { %> 75 |
76 | <%- partial('share/' + get_config('share.type')) %> 77 |
78 | <% } %> 79 | 80 | <% if (!index && get_config('comments') && has_config('comment.type')) { %> 81 |
82 |

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

83 | <%- partial('comment/' + get_config('comment.type')) %> 84 |
85 | <% } %> -------------------------------------------------------------------------------- /layout/common/footer.ejs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /layout/common/head.ejs: -------------------------------------------------------------------------------- 1 | 2 | <%= page_title() %> 3 | 4 | 5 | 6 | 7 | <% const languages = display_languages(); 8 | const pageLanguage = page_language(page); 9 | if (languages.length > 1) { 10 | languages.forEach((language) => { 11 | if (language !== 'default' && pageLanguage !== language) { %> 12 | 13 | <% } 14 | }); 15 | } %> 16 | 17 | <% if (has_config('description')) { %> 18 | 19 | <% } %> 20 | 21 | <% if (has_config('keywords')) { %> 22 | 23 | <% } %> 24 | 25 | <% if (has_config('open_graph')) { %> 26 | <%- open_graph({ 27 | twitter_id: get_config('open_graph.twitter_id', ''), 28 | twitter_site: get_config('open_graph.twitter_site', ''), 29 | google_plus: get_config('open_graph.google_plus', ''), 30 | fb_admins: get_config('open_graph.fb_admins', ''), 31 | fb_app_id: get_config('open_graph.fb_app_id', '') 32 | }) %> 33 | <% } %> 34 | 35 | <% if (has_config('rss')) { %> 36 | 37 | <% } %> 38 | 39 | <% if (has_config('favicon')) { %> 40 | 41 | <% } %> 42 | 43 | 44 | 45 | 46 | <% if (!(has_config('plugins.gallery') && get_config('plugins.gallery') === false)) { %> 47 | 48 | 49 | <% } %> 50 | 51 | 52 | 53 | <%- css('css/style') %> 54 | 55 | 56 | 57 | <% if (has_config('plugins')) { %> 58 | <% for (let plugin in get_config('plugins')) { %> 59 | <%- partial('plugins/' + plugin, {head: true}) %> 60 | <% } %> 61 | <% } %> 62 | -------------------------------------------------------------------------------- /layout/common/languages.ejs: -------------------------------------------------------------------------------- 1 | <% const languages = display_languages(); 2 | 3 | if (languages.length > 1) {%> 4 |
5 | 27 |
28 | <%} %> -------------------------------------------------------------------------------- /layout/common/navbar.ejs: -------------------------------------------------------------------------------- 1 | <% function is_same_link(a, b) { 2 | function santize(url) { 3 | let paths = url.replace(/(^\w+:|^)\/\//, '').split('#')[0].split('/'); 4 | if (paths.length > 0 && paths[paths.length - 1].trim() === 'index.html') { 5 | paths = paths.slice(0, paths.length - 1) 6 | } 7 | return paths.join('/'); 8 | } 9 | 10 | return santize(url_for(a)) == santize(url_for(b)); 11 | } %> 12 | 78 | -------------------------------------------------------------------------------- /layout/common/paginator.ejs: -------------------------------------------------------------------------------- 1 | <% function link_url(i) { 2 | return url_for(i === 1 ? page.base : page.base + get_config('pagination_dir') + '/' + i + '/'); 3 | } 4 | 5 | function pagination(c, m) { 6 | var current = c, 7 | last = m, 8 | delta = 1, 9 | left = current - delta, 10 | right = current + delta + 1, 11 | range = [], 12 | elements = [], 13 | l; 14 | 15 | for (let i = 1; i <= last; i++) { 16 | if (i == 1 || i == last || i >= left && i < right) { 17 | range.push(i); 18 | } 19 | } 20 | 21 | for (let i of range) { 22 | if (l) { 23 | if (i - l === 2) { 24 | elements.push(`
  • ${ l + 1 }
  • `); 25 | } else if (i - l !== 1) { 26 | elements.push(`
  • `); 27 | } 28 | } 29 | elements.push(`
  • ${ i }
  • `); 30 | l = i; 31 | } 32 | return elements; 33 | } %> 34 | -------------------------------------------------------------------------------- /layout/common/scripts.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |
    6 |
    Your browser is out-of-date!
    7 |

    Update your browser to view this website correctly. Update my browser now

    8 |

    ×

    9 |
    10 | 11 | 21 | 22 | 28 | 29 | <% if (has_config('plugins')) { %> 30 | <% for (let plugin in get_config('plugins')) { %> 31 | <%- partial('plugins/' + plugin, {head: false}) %> 32 | <% } %> 33 | <% } %> 34 | 35 | <%- js('js/script') %> -------------------------------------------------------------------------------- /layout/index.ejs: -------------------------------------------------------------------------------- 1 |
    2 |
    3 | <% page.posts.each(function(post){ %> 4 | <%- partial('common/article', { post, index: true }) %> 5 | <% }); %> 6 | <% if (page.total > 1) { %> 7 | <%- partial('common/paginator') %> 8 | <% } %> 9 |
    10 |
    -------------------------------------------------------------------------------- /layout/layout.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | <%- partial('common/head') %> 5 | 6 | 7 | <%- partial('common/navbar', { page }) %> 8 | <%- body %> 9 | <%- partial('common/footer') %> 10 | <%- partial('common/scripts') %> 11 | 12 | <% if (has_config('search.type')) { %> 13 | <%- partial('search/' + get_config('search.type')) %> 14 | <% } %> 15 | 16 | -------------------------------------------------------------------------------- /layout/plugins/clipboard.ejs: -------------------------------------------------------------------------------- 1 | <% if (!head && !(has_config('plugins.clipboard') && get_config('plugins.clipboard') === false)) { %> 2 | 3 | 35 | 64 | <% } %> -------------------------------------------------------------------------------- /layout/plugins/gallery.ejs: -------------------------------------------------------------------------------- 1 | <% if (!head && !(has_config('plugins.gallery') && get_config('plugins.gallery') === false)) { %> 2 | 3 | 4 | 17 | <% } %> -------------------------------------------------------------------------------- /layout/plugins/google-analytics.ejs: -------------------------------------------------------------------------------- 1 | <% if (head && has_config('plugins.google-analytics.tracking_id')) { %> 2 | 3 | 10 | <% } %> 11 | -------------------------------------------------------------------------------- /layout/plugins/katex.ejs: -------------------------------------------------------------------------------- 1 | <% if (!head && !(has_config('plugins.katex') && get_config('plugins.katex') === false)) { %> 2 | 3 | 4 | 6 | 7 | 14 | <% } %> -------------------------------------------------------------------------------- /layout/plugins/mathjax.ejs: -------------------------------------------------------------------------------- 1 | <% if (!head && !(has_config('plugins.mathjax') && get_config('plugins.mathjax') === false)) { %> 2 | 3 | 22 | <% } %> -------------------------------------------------------------------------------- /layout/post.ejs: -------------------------------------------------------------------------------- 1 |
    2 |
    3 | <%- partial('common/article', { post: page, index: false }) %> 4 |
    5 |
    -------------------------------------------------------------------------------- /layout/search/google-cse.ejs: -------------------------------------------------------------------------------- 1 | 32 | -------------------------------------------------------------------------------- /layout/search/insight.ejs: -------------------------------------------------------------------------------- 1 | 13 | 28 | <%- js('js/insight') %> -------------------------------------------------------------------------------- /layout/share/addthis.ejs: -------------------------------------------------------------------------------- 1 | <% if (!has_config('share.install_url')) { %> 2 |
    3 | You need to set install_url to use AddThis. Please set it in _config.yml. 4 |
    5 | <% } else { %> 6 |
    7 | 8 | <% } %> -------------------------------------------------------------------------------- /layout/share/sharethis.ejs: -------------------------------------------------------------------------------- 1 | <% if (!has_config('share.install_url')) { %> 2 |
    3 | You need to set install_url to use ShareThis. Please set it in _config.yml. 4 |
    5 | <% } else { %> 6 |
    7 | 8 | <% } %> -------------------------------------------------------------------------------- /layout/tag.ejs: -------------------------------------------------------------------------------- 1 |
    2 |
    3 |
    4 |
    #<%= page.tag %>
    5 |
    6 |
    7 |
    8 | <%- partial('index', { page }) %> -------------------------------------------------------------------------------- /layout/tags.ejs: -------------------------------------------------------------------------------- 1 |
    2 |
    3 |
    4 |
    #<%= __('common.tags') %>
    5 |
    6 |
    7 |
    8 |
    9 |
    10 |
    11 | <% (page._tags || page.tags).forEach(tag => {%> 12 | 13 | 14 | 15 | <% }) %> 16 |
    17 |
    18 |
    -------------------------------------------------------------------------------- /lib/i18n.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | const util = require('hexo-util'); 3 | const RFC5646_TAGS = require('./rfc5646'); 4 | 5 | const Pattern = util.Pattern; 6 | 7 | module.exports = function (hexo) { 8 | function pathJoin(...paths) { 9 | return paths.join('/'); 10 | } 11 | 12 | function formatRfc5646(language) { 13 | if (!language) { 14 | return ''; 15 | } 16 | return language.split(/[-_]/).map((l, i) => i === 0 ? l.toLowerCase() : l.toUpperCase()).join('-'); 17 | } 18 | 19 | function formatIso639(language) { 20 | if (!language) { 21 | return ''; 22 | } 23 | return language.split(/[-_]/)[0].toLowerCase(); 24 | } 25 | 26 | function getClosestRfc5646WithCountryCode(language) { 27 | if (!language) { 28 | return ''; 29 | } 30 | const iso639 = formatRfc5646(language).split('-')[0]; 31 | const result = Object.keys(RFC5646_TAGS).find(tag => tag.startsWith(iso639 + '-')); 32 | return result ? result : iso639; 33 | } 34 | 35 | function getUsedLanguages() { 36 | return hexo.theme.i18n.list(); 37 | } 38 | 39 | function getDisplayLanguages() { 40 | let languages = hexo.config.language; 41 | if (!languages) { 42 | return ['default']; 43 | } 44 | languages = [].concat(hexo.config.language); 45 | if (!Array.isArray(languages)) { 46 | languages = [languages]; 47 | } 48 | if (languages.indexOf('default') > -1) { 49 | languages.splice(languages.indexOf('default'), 1); 50 | } 51 | return languages; 52 | } 53 | 54 | function isLanguageValid(language) { 55 | const variants = [language, formatRfc5646(language)]; 56 | return variants.some(variant => RFC5646_TAGS.hasOwnProperty(variant)); 57 | } 58 | 59 | function injectLanguages(func) { 60 | return function(locals) { 61 | return func.call(this, getDisplayLanguages(), locals); 62 | } 63 | } 64 | 65 | function getPageLanguage(post) { 66 | const languages = getUsedLanguages(); 67 | let lang = post.lang || post.language; 68 | if (!lang && post.source) { 69 | const path = post.source.startsWith('_posts/') ? post.source.slice('_posts/'.length) : post.source; 70 | const pattern = new Pattern(`${hexo.config.i18n_dir}/*path`); 71 | const data = pattern.match(path); 72 | 73 | if (data && data.lang && ~languages.indexOf(data.lang)) { 74 | lang = data.lang; 75 | } 76 | } 77 | return lang; 78 | } 79 | 80 | function isDefaultLanguage(language) { 81 | return !language || getDisplayLanguages().indexOf(language) === 0; 82 | } 83 | 84 | function postFilter(language) { 85 | return function (post) { 86 | let lang = getPageLanguage(post); 87 | return (lang === language || (isDefaultLanguage(language) && !lang)) && (post.indexing !== false); 88 | } 89 | } 90 | 91 | function url_for(path) { 92 | return hexo.extend.helper.get('url_for').call(hexo, path); 93 | } 94 | 95 | return { 96 | pathJoin, 97 | isDefaultLanguage, 98 | url_for, 99 | postFilter, 100 | injectLanguages, 101 | getUsedLanguages, 102 | getDisplayLanguages, 103 | getPageLanguage, 104 | isLanguageValid, 105 | formatRfc5646, 106 | formatIso639, 107 | getClosestRfc5646WithCountryCode 108 | }; 109 | }; -------------------------------------------------------------------------------- /lib/rfc5646.js: -------------------------------------------------------------------------------- 1 | // https://gist.github.com/msikma/8912e62ed866778ff8cd 2 | 3 | // List of language tags according to RFC 5646. 4 | // See for info on how to parse 5 | // these language tags. Some duplicates have been removed. 6 | const RFC5646_LANGUAGE_TAGS = { 7 | 'af': 'Afrikaans', 8 | 'af-ZA': 'Afrikaans (South Africa)', 9 | 'ar': 'Arabic', 10 | 'ar-AE': 'Arabic (U.A.E.)', 11 | 'ar-BH': 'Arabic (Bahrain)', 12 | 'ar-DZ': 'Arabic (Algeria)', 13 | 'ar-EG': 'Arabic (Egypt)', 14 | 'ar-IQ': 'Arabic (Iraq)', 15 | 'ar-JO': 'Arabic (Jordan)', 16 | 'ar-KW': 'Arabic (Kuwait)', 17 | 'ar-LB': 'Arabic (Lebanon)', 18 | 'ar-LY': 'Arabic (Libya)', 19 | 'ar-MA': 'Arabic (Morocco)', 20 | 'ar-OM': 'Arabic (Oman)', 21 | 'ar-QA': 'Arabic (Qatar)', 22 | 'ar-SA': 'Arabic (Saudi Arabia)', 23 | 'ar-SY': 'Arabic (Syria)', 24 | 'ar-TN': 'Arabic (Tunisia)', 25 | 'ar-YE': 'Arabic (Yemen)', 26 | 'az': 'Azeri (Latin)', 27 | 'az-AZ': 'Azeri (Latin) (Azerbaijan)', 28 | 'az-Cyrl-AZ': 'Azeri (Cyrillic) (Azerbaijan)', 29 | 'be': 'Belarusian', 30 | 'be-BY': 'Belarusian (Belarus)', 31 | 'bg': 'Bulgarian', 32 | 'bg-BG': 'Bulgarian (Bulgaria)', 33 | 'bs-BA': 'Bosnian (Bosnia and Herzegovina)', 34 | 'ca': 'Catalan', 35 | 'ca-ES': 'Catalan (Spain)', 36 | 'cs': 'Czech', 37 | 'cs-CZ': 'Czech (Czech Republic)', 38 | 'cy': 'Welsh', 39 | 'cy-GB': 'Welsh (United Kingdom)', 40 | 'da': 'Danish', 41 | 'da-DK': 'Danish (Denmark)', 42 | 'de': 'German', 43 | 'de-AT': 'German (Austria)', 44 | 'de-CH': 'German (Switzerland)', 45 | 'de-DE': 'German (Germany)', 46 | 'de-LI': 'German (Liechtenstein)', 47 | 'de-LU': 'German (Luxembourg)', 48 | 'dv': 'Divehi', 49 | 'dv-MV': 'Divehi (Maldives)', 50 | 'el': 'Greek', 51 | 'el-GR': 'Greek (Greece)', 52 | 'en': 'English', 53 | 'en-AU': 'English (Australia)', 54 | 'en-BZ': 'English (Belize)', 55 | 'en-CA': 'English (Canada)', 56 | 'en-CB': 'English (Caribbean)', 57 | 'en-GB': 'English (United Kingdom)', 58 | 'en-IE': 'English (Ireland)', 59 | 'en-JM': 'English (Jamaica)', 60 | 'en-NZ': 'English (New Zealand)', 61 | 'en-PH': 'English (Republic of the Philippines)', 62 | 'en-TT': 'English (Trinidad and Tobago)', 63 | 'en-US': 'English (United States)', 64 | 'en-ZA': 'English (South Africa)', 65 | 'en-ZW': 'English (Zimbabwe)', 66 | 'eo': 'Esperanto', 67 | 'es': 'Spanish', 68 | 'es-AR': 'Spanish (Argentina)', 69 | 'es-BO': 'Spanish (Bolivia)', 70 | 'es-CL': 'Spanish (Chile)', 71 | 'es-CO': 'Spanish (Colombia)', 72 | 'es-CR': 'Spanish (Costa Rica)', 73 | 'es-DO': 'Spanish (Dominican Republic)', 74 | 'es-EC': 'Spanish (Ecuador)', 75 | 'es-ES': 'Spanish (Spain)', 76 | 'es-GT': 'Spanish (Guatemala)', 77 | 'es-HN': 'Spanish (Honduras)', 78 | 'es-MX': 'Spanish (Mexico)', 79 | 'es-NI': 'Spanish (Nicaragua)', 80 | 'es-PA': 'Spanish (Panama)', 81 | 'es-PE': 'Spanish (Peru)', 82 | 'es-PR': 'Spanish (Puerto Rico)', 83 | 'es-PY': 'Spanish (Paraguay)', 84 | 'es-SV': 'Spanish (El Salvador)', 85 | 'es-UY': 'Spanish (Uruguay)', 86 | 'es-VE': 'Spanish (Venezuela)', 87 | 'et': 'Estonian', 88 | 'et-EE': 'Estonian (Estonia)', 89 | 'eu': 'Basque', 90 | 'eu-ES': 'Basque (Spain)', 91 | 'fa': 'Farsi', 92 | 'fa-IR': 'Farsi (Iran)', 93 | 'fi': 'Finnish', 94 | 'fi-FI': 'Finnish (Finland)', 95 | 'fo': 'Faroese', 96 | 'fo-FO': 'Faroese (Faroe Islands)', 97 | 'fr': 'French', 98 | 'fr-BE': 'French (Belgium)', 99 | 'fr-CA': 'French (Canada)', 100 | 'fr-CH': 'French (Switzerland)', 101 | 'fr-FR': 'French (France)', 102 | 'fr-LU': 'French (Luxembourg)', 103 | 'fr-MC': 'French (Principality of Monaco)', 104 | 'gl': 'Galician', 105 | 'gl-ES': 'Galician (Spain)', 106 | 'gu': 'Gujarati', 107 | 'gu-IN': 'Gujarati (India)', 108 | 'he': 'Hebrew', 109 | 'he-IL': 'Hebrew (Israel)', 110 | 'hi': 'Hindi', 111 | 'hi-IN': 'Hindi (India)', 112 | 'hr': 'Croatian', 113 | 'hr-BA': 'Croatian (Bosnia and Herzegovina)', 114 | 'hr-HR': 'Croatian (Croatia)', 115 | 'hu': 'Hungarian', 116 | 'hu-HU': 'Hungarian (Hungary)', 117 | 'hy': 'Armenian', 118 | 'hy-AM': 'Armenian (Armenia)', 119 | 'id': 'Indonesian', 120 | 'id-ID': 'Indonesian (Indonesia)', 121 | 'is': 'Icelandic', 122 | 'is-IS': 'Icelandic (Iceland)', 123 | 'it': 'Italian', 124 | 'it-CH': 'Italian (Switzerland)', 125 | 'it-IT': 'Italian (Italy)', 126 | 'ja': 'Japanese', 127 | 'ja-JP': 'Japanese (Japan)', 128 | 'ka': 'Georgian', 129 | 'ka-GE': 'Georgian (Georgia)', 130 | 'kk': 'Kazakh', 131 | 'kk-KZ': 'Kazakh (Kazakhstan)', 132 | 'kn': 'Kannada', 133 | 'kn-IN': 'Kannada (India)', 134 | 'ko': 'Korean', 135 | 'ko-KR': 'Korean (Korea)', 136 | 'kok': 'Konkani', 137 | 'kok-IN': 'Konkani (India)', 138 | 'ky': 'Kyrgyz', 139 | 'ky-KG': 'Kyrgyz (Kyrgyzstan)', 140 | 'lt': 'Lithuanian', 141 | 'lt-LT': 'Lithuanian (Lithuania)', 142 | 'lv': 'Latvian', 143 | 'lv-LV': 'Latvian (Latvia)', 144 | 'mi': 'Maori', 145 | 'mi-NZ': 'Maori (New Zealand)', 146 | 'mk': 'FYRO Macedonian', 147 | 'mk-MK': 'FYRO Macedonian (Former Yugoslav Republic of Macedonia)', 148 | 'mn': 'Mongolian', 149 | 'mn-MN': 'Mongolian (Mongolia)', 150 | 'mr': 'Marathi', 151 | 'mr-IN': 'Marathi (India)', 152 | 'ms': 'Malay', 153 | 'ms-BN': 'Malay (Brunei Darussalam)', 154 | 'ms-MY': 'Malay (Malaysia)', 155 | 'mt': 'Maltese', 156 | 'mt-MT': 'Maltese (Malta)', 157 | 'nb': 'Norwegian (Bokm?l)', 158 | 'nb-NO': 'Norwegian (Bokm?l) (Norway)', 159 | 'nl': 'Dutch', 160 | 'nl-BE': 'Dutch (Belgium)', 161 | 'nl-NL': 'Dutch (Netherlands)', 162 | 'nn-NO': 'Norwegian (Nynorsk) (Norway)', 163 | 'ns': 'Northern Sotho', 164 | 'ns-ZA': 'Northern Sotho (South Africa)', 165 | 'pa': 'Punjabi', 166 | 'pa-IN': 'Punjabi (India)', 167 | 'pl': 'Polish', 168 | 'pl-PL': 'Polish (Poland)', 169 | 'ps': 'Pashto', 170 | 'ps-AR': 'Pashto (Afghanistan)', 171 | 'pt': 'Portuguese', 172 | 'pt-BR': 'Portuguese (Brazil)', 173 | 'pt-PT': 'Portuguese (Portugal)', 174 | 'qu': 'Quechua', 175 | 'qu-BO': 'Quechua (Bolivia)', 176 | 'qu-EC': 'Quechua (Ecuador)', 177 | 'qu-PE': 'Quechua (Peru)', 178 | 'ro': 'Romanian', 179 | 'ro-RO': 'Romanian (Romania)', 180 | 'ru': 'Russian', 181 | 'ru-RU': 'Russian (Russia)', 182 | 'sa': 'Sanskrit', 183 | 'sa-IN': 'Sanskrit (India)', 184 | 'se': 'Sami', 185 | 'se-FI': 'Sami (Finland)', 186 | 'se-NO': 'Sami (Norway)', 187 | 'se-SE': 'Sami (Sweden)', 188 | 'sk': 'Slovak', 189 | 'sk-SK': 'Slovak (Slovakia)', 190 | 'sl': 'Slovenian', 191 | 'sl-SI': 'Slovenian (Slovenia)', 192 | 'sq': 'Albanian', 193 | 'sq-AL': 'Albanian (Albania)', 194 | 'sr-BA': 'Serbian (Latin) (Bosnia and Herzegovina)', 195 | 'sr-Cyrl-BA': 'Serbian (Cyrillic) (Bosnia and Herzegovina)', 196 | 'sr-SP': 'Serbian (Latin) (Serbia and Montenegro)', 197 | 'sr-Cyrl-SP': 'Serbian (Cyrillic) (Serbia and Montenegro)', 198 | 'sv': 'Swedish', 199 | 'sv-FI': 'Swedish (Finland)', 200 | 'sv-SE': 'Swedish (Sweden)', 201 | 'sw': 'Swahili', 202 | 'sw-KE': 'Swahili (Kenya)', 203 | 'syr': 'Syriac', 204 | 'syr-SY': 'Syriac (Syria)', 205 | 'ta': 'Tamil', 206 | 'ta-IN': 'Tamil (India)', 207 | 'te': 'Telugu', 208 | 'te-IN': 'Telugu (India)', 209 | 'th': 'Thai', 210 | 'th-TH': 'Thai (Thailand)', 211 | 'tl': 'Tagalog', 212 | 'tl-PH': 'Tagalog (Philippines)', 213 | 'tn': 'Tswana', 214 | 'tn-ZA': 'Tswana (South Africa)', 215 | 'tr': 'Turkish', 216 | 'tr-TR': 'Turkish (Turkey)', 217 | 'tt': 'Tatar', 218 | 'tt-RU': 'Tatar (Russia)', 219 | 'ts': 'Tsonga', 220 | 'uk': 'Ukrainian', 221 | 'uk-UA': 'Ukrainian (Ukraine)', 222 | 'ur': 'Urdu', 223 | 'ur-PK': 'Urdu (Islamic Republic of Pakistan)', 224 | 'uz': 'Uzbek (Latin)', 225 | 'uz-UZ': 'Uzbek (Latin) (Uzbekistan)', 226 | 'uz-Cyrl-UZ': 'Uzbek (Cyrillic) (Uzbekistan)', 227 | 'vi': 'Vietnamese', 228 | 'vi-VN': 'Vietnamese (Viet Nam)', 229 | 'xh': 'Xhosa', 230 | 'xh-ZA': 'Xhosa (South Africa)', 231 | 'zh': 'Chinese', 232 | 'zh-CN': 'Chinese (S)', 233 | 'zh-HK': 'Chinese (Hong Kong)', 234 | 'zh-MO': 'Chinese (Macau)', 235 | 'zh-SG': 'Chinese (Singapore)', 236 | 'zh-TW': 'Chinese (T)', 237 | 'zu': 'Zulu', 238 | 'zu-ZA': 'Zulu (South Africa)' 239 | }; 240 | 241 | module.exports = RFC5646_LANGUAGE_TAGS; 242 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hexo-theme-minos", 3 | "version": "2.0.0", 4 | "lockfileVersion": 1 5 | } 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hexo-theme-minos", 3 | "version": "2.0.0", 4 | "private": true, 5 | "devDependencies": {}, 6 | "dependencies": {} 7 | } 8 | -------------------------------------------------------------------------------- /scripts/01_check.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const logger = require('hexo-log')(); 4 | 5 | logger.info(`======================================= 6 | ███╗ ███╗ ██╗ ███╗ ██╗ ██████╗ ███████╗ 7 | ████╗ ████║ ██║ ████╗ ██║ ██╔═══██╗ ██╔════╝ 8 | ██╔████╔██║ ██║ ██╔██╗ ██║ ██║ ██║ ███████╗ 9 | ██║╚██╔╝██║ ██║ ██║╚██╗██║ ██║ ██║ ╚════██║ 10 | ██║ ╚═╝ ██║ ██║ ██║ ╚████║ ╚██████╔╝ ███████║ 11 | ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═══╝ ╚═════╝ ╚══════╝ 12 | =============================================`); 13 | 14 | function checkDependency(name) { 15 | try { 16 | require.resolve(name); 17 | return true; 18 | } catch(e) { 19 | logger.error(`Package ${name} is not installed.`) 20 | } 21 | return false; 22 | } 23 | 24 | logger.info('Checking dependencies'); 25 | const missingDeps = [ 26 | 'moment', 27 | 'lodash', 28 | 'cheerio', 29 | 'js-yaml', 30 | 'highlight.js', 31 | 'hexo-util', 32 | 'hexo-generator-archive', 33 | 'hexo-generator-category', 34 | 'hexo-generator-index', 35 | 'hexo-generator-tag', 36 | 'hexo-renderer-ejs', 37 | 'hexo-renderer-marked', 38 | 'hexo-renderer-sass', 39 | ].map(checkDependency).some(installed => !installed); 40 | if (missingDeps) { 41 | logger.error('Please install the missing dependencies in the root directory of your Hexo site.'); 42 | process.exit(-1); 43 | } 44 | 45 | const themeRoot = path.join(__dirname, '..'); 46 | const mainConfigPath = path.join(themeRoot, '_config.yml'); 47 | 48 | logger.info('Checking if the configuration file exists'); 49 | if (!fs.existsSync(mainConfigPath)) { 50 | logger.warn(`${mainConfigPath} is not found. Please create one from the template _config.yml.example.`) 51 | } 52 | 53 | const { getUsedLanguages, getDisplayLanguages, isLanguageValid } = require('../lib/i18n')(hexo); 54 | 55 | logger.info('Checking language names against RFC5646 specs'); 56 | const invalidLanguages = getUsedLanguages().filter(language => !isLanguageValid(language)); 57 | if (invalidLanguages.length > 0) { 58 | logger.warn(`Language ${invalidLanguages} indicated by some posts is not a valid RFC5646 language.`) 59 | } 60 | const invalidDisplayLanguages = getDisplayLanguages().filter(language => !isLanguageValid(language)); 61 | if (invalidDisplayLanguages.length > 0) { 62 | logger.warn(`Language ${invalidDisplayLanguages} set in the configuration file is not a valid RFC5646 language.`) 63 | } 64 | -------------------------------------------------------------------------------- /scripts/10_i18n.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | const util = require('hexo-util'); 3 | const postGenerator = require('hexo/lib/plugins/generator/post'); 4 | const indexGenerator = require('hexo-generator-index/lib/generator'); 5 | const archiveGenerator = require('hexo-generator-archive/lib/generator'); 6 | const categoryGenerator = require('hexo-generator-category/lib/generator'); 7 | const tagGenerator = require('hexo-generator-tag/lib/generator'); 8 | const { 9 | pathJoin, 10 | isDefaultLanguage, 11 | url_for, 12 | postFilter, 13 | injectLanguages, 14 | getUsedLanguages, 15 | getDisplayLanguages, 16 | getPageLanguage, 17 | isLanguageValid, 18 | formatRfc5646, 19 | formatIso639, 20 | getClosestRfc5646WithCountryCode 21 | } = require('../lib/i18n')(hexo); 22 | 23 | /** 24 | * Modify previous and next post link 25 | */ 26 | hexo.extend.generator.register('post', function(locals) { 27 | return postGenerator(locals).map(route => { 28 | let post = route.data; 29 | if (post.next) { 30 | let next = post.next; 31 | while (next && post.lang !== next.lang) { 32 | next = next.next; 33 | } 34 | post.next = next; 35 | if (next) { 36 | next.prev = post; 37 | } 38 | } 39 | if (post.prev) { 40 | let prev = post.prev; 41 | while (prev && post.lang !== prev.lang) { 42 | prev = prev.prev; 43 | } 44 | post.prev = prev; 45 | if (prev) { 46 | prev.next = post; 47 | } 48 | } 49 | return route; 50 | }); 51 | }); 52 | 53 | /** 54 | * Multi-language index generator. 55 | * 56 | * ATTENTION: This will override the default index generator! 57 | */ 58 | hexo.extend.generator.register('index', injectLanguages(function(languages, locals) { 59 | return _.flatten(languages.map((language) => { 60 | // Filter posts by language considering. Posts without a language is considered of the default language. 61 | const posts = locals.posts.filter(postFilter(language)); 62 | if (posts.length === 0) { 63 | return null; 64 | } 65 | const routes = indexGenerator.call(this, Object.assign({}, locals, { 66 | posts: posts 67 | })); 68 | if (isDefaultLanguage(language)) { 69 | return routes; 70 | } 71 | return routes.map(route => { 72 | const data = Object.assign({}, route.data, { 73 | base: pathJoin(language, route.data.base), 74 | current_url: pathJoin(language, route.data.current_url) 75 | }); 76 | return Object.assign({}, route, { 77 | path: pathJoin(language, route.path), 78 | data: data 79 | }); 80 | }); 81 | }).filter(post => post !== null)); 82 | })); 83 | 84 | /** 85 | * Multi-language archive generator. 86 | * 87 | * ATTENTION: This will override the default archive generator! 88 | */ 89 | hexo.extend.generator.register('archive', injectLanguages(function(languages, locals) { 90 | return _.flatten(languages.map((language) => { 91 | // Filter posts by language considering. Posts without a language is considered of the default language. 92 | const posts = locals.posts.filter(postFilter(language)); 93 | if (posts.length === 0) { 94 | return null; 95 | } 96 | const routes = archiveGenerator.call(this, Object.assign({}, locals, { 97 | posts: posts 98 | })); 99 | if (isDefaultLanguage(language)) { 100 | return routes; 101 | } 102 | return routes.map(route => { 103 | const data = Object.assign({}, route.data, { 104 | base: pathJoin(language, route.data.base), 105 | current_url: pathJoin(language, route.data.current_url) 106 | }); 107 | return Object.assign({}, route, { 108 | path: pathJoin(language, route.path), 109 | data: data 110 | }); 111 | }); 112 | }).filter(post => post !== null)); 113 | })); 114 | 115 | /** 116 | * Multi-language category generator. 117 | * 118 | * ATTENTION: This will override the default category generator! 119 | */ 120 | hexo.extend.generator.register('category', injectLanguages(function(languages, locals) { 121 | return _.flatten(languages.map((language) => { 122 | const categories = locals.categories.map(category => { 123 | // Filter posts by language considering. Posts without a language is considered of the default language. 124 | const posts = category.posts.filter(postFilter(language)); 125 | if (posts.length === 0) { 126 | return null; 127 | } 128 | return Object.assign({}, category, { 129 | posts: posts 130 | }); 131 | }).filter(category => category !== null); 132 | if (categories.length === 0) { 133 | return null; 134 | } 135 | 136 | const routes = categoryGenerator.call(this, Object.assign({}, locals, { 137 | categories: categories 138 | })); 139 | if (isDefaultLanguage(language)) { 140 | return routes; 141 | } 142 | return routes.map(route => { 143 | const data = Object.assign({}, route.data, { 144 | base: pathJoin(language, route.data.base), 145 | current_url: pathJoin(language, route.data.current_url) 146 | }); 147 | return Object.assign({}, route, { 148 | path: pathJoin(language, route.path), 149 | data: data 150 | }); 151 | }); 152 | }).filter(post => post !== null)); 153 | })); 154 | 155 | /** 156 | * Multi-language tag generator. 157 | * 158 | * ATTENTION: This will override the default tag generator! 159 | */ 160 | hexo.extend.generator.register('tag', injectLanguages(function(languages, locals) { 161 | return _.flatten(languages.map((language) => { 162 | const tags = locals.tags.map(tag => { 163 | // Filter posts by language considering. Posts without a language is considered of the default language. 164 | const posts = tag.posts.filter(postFilter(language)); 165 | if (posts.length === 0) { 166 | return null; 167 | } 168 | return Object.assign({}, tag, { 169 | posts: posts 170 | }); 171 | }).filter(category => category !== null); 172 | if (tags.length === 0) { 173 | return null; 174 | } 175 | 176 | const routes = tagGenerator.call(this, Object.assign({}, locals, { 177 | tags: tags 178 | })); 179 | if (isDefaultLanguage(language)) { 180 | return routes; 181 | } 182 | 183 | return routes.map(route => { 184 | const data = Object.assign({}, route.data, { 185 | base: pathJoin(language, route.data.base), 186 | current_url: pathJoin(language, route.data.current_url) 187 | }); 188 | return Object.assign({}, route, { 189 | path: pathJoin(language, route.path), 190 | data: data 191 | }); 192 | }); 193 | }).filter(post => post !== null)); 194 | })); 195 | 196 | /** 197 | * Category list page generator 198 | */ 199 | hexo.extend.generator.register('categories', injectLanguages(function(languages, locals) { 200 | return languages.map((language) => { 201 | const categories = locals.categories.map(category => { 202 | // Filter posts by language considering. Posts without a language is considered of the default language. 203 | const posts = category.posts.filter(postFilter(language)); 204 | if (posts.length === 0) { 205 | return null; 206 | } 207 | return Object.assign({}, category, { 208 | posts: posts, 209 | path: isDefaultLanguage(language) ? category.path : pathJoin(language, category.path) 210 | }); 211 | }).filter(category => category !== null); 212 | return { 213 | path: isDefaultLanguage(language) ? 'categories/' : pathJoin(language, 'categories/'), 214 | layout: ['categories'], 215 | data: Object.assign({}, locals, { 216 | _categories: categories, 217 | __categories: true 218 | }) 219 | }; 220 | }) 221 | })); 222 | 223 | /** 224 | * Tag list page generator 225 | */ 226 | hexo.extend.generator.register('tags', injectLanguages(function(languages, locals) { 227 | return languages.map((language) => { 228 | const tags = locals.tags.map(tag => { 229 | // Filter posts by language considering. Posts without a language is considered of the default language. 230 | const posts = tag.posts.filter(postFilter(language)); 231 | if (posts.length === 0) { 232 | return null; 233 | } 234 | return Object.assign({}, tag, { 235 | posts: posts, 236 | path: isDefaultLanguage(language) ? tag.path : pathJoin(language, tag.path) 237 | }); 238 | }).filter(category => category !== null); 239 | return { 240 | path: isDefaultLanguage(language) ? 'tags/' : pathJoin(language, 'tags/'), 241 | layout: ['tags'], 242 | data: Object.assign({}, locals, { 243 | _tags: tags, 244 | __tags: true 245 | }) 246 | }; 247 | }) 248 | })); 249 | 250 | /** 251 | * Multi-language insight search content.json generator. 252 | * 253 | * ATTENTION: This will override the default insight search content.json generator! 254 | */ 255 | hexo.extend.generator.register('insight', injectLanguages(function(languages, locals) { 256 | function minify(str) { 257 | return util.stripHTML(str).trim().replace(/\n/g, ' ').replace(/\s+/g, ' ') 258 | .replace(/&#x([\da-fA-F]+);/g, (match, hex) => { 259 | return String.fromCharCode(parseInt(hex, 16)); 260 | }) 261 | .replace(/&#([\d]+);/g, (match, dec) => { 262 | return String.fromCharCode(dec); 263 | }); 264 | } 265 | function postMapper(post) { 266 | return { 267 | title: post.title, 268 | text: minify(post.content), 269 | link: url_for(post.path) 270 | } 271 | } 272 | function tagMapper(language) { 273 | return function (tag) { 274 | return { 275 | name: tag.name, 276 | slug: tag.slug, 277 | link: url_for(isDefaultLanguage(language) ? tag.path : pathJoin(language, tag.path)) 278 | } 279 | } 280 | } 281 | return languages.map((language) => { 282 | const site = { 283 | pages: locals.pages.filter(postFilter(language)).map(postMapper), 284 | posts: locals.posts.filter(postFilter(language)).map(postMapper), 285 | tags: locals.tags.filter(tag => tag.posts.some(postFilter(language))) 286 | .map(tagMapper(language)), 287 | categories: locals.categories.filter(category => category.posts.some(postFilter(language))) 288 | .map(tagMapper(language)), 289 | }; 290 | return { 291 | path: isDefaultLanguage(language) ? 'content.json' : 'content.' + language + '.json', 292 | data: JSON.stringify(site) 293 | }; 294 | }); 295 | })); 296 | 297 | /** 298 | * Append language directory to the post tags and categories 299 | */ 300 | hexo.extend.filter.register('before_post_render', function(data) { 301 | data.lang = getPageLanguage(data); 302 | data._categories = data.categories ? data.categories.map(category => { 303 | return { 304 | name: category.name, 305 | path: !isDefaultLanguage(data.lang) ? pathJoin(data.lang, category.path) : category.path 306 | }; 307 | }) : []; 308 | data._tags = data.tags ? data.tags.map(tag => { 309 | return { 310 | name: tag.name, 311 | path: !isDefaultLanguage(data.lang) ? pathJoin(data.lang, tag.path) : tag.path 312 | }; 313 | }) : []; 314 | return data; 315 | }); 316 | 317 | /** 318 | * Get all languages set in the site's _config.yml 319 | */ 320 | hexo.extend.helper.register('display_languages', function () { 321 | return getDisplayLanguages(); 322 | }); 323 | 324 | /** 325 | * Test if the given language is sites default language. 326 | */ 327 | hexo.extend.helper.register('is_default_language', function (language) { 328 | return isDefaultLanguage(language); 329 | }); 330 | 331 | /** 332 | * Get page language. Returns empty if language is not found or is default language. 333 | */ 334 | hexo.extend.helper.register('page_language', function () { 335 | return getPageLanguage(this.page); 336 | }); 337 | 338 | /** 339 | * Get page path given a certain language tag 340 | */ 341 | hexo.extend.helper.register('i18n_path', function (language) { 342 | const path = this.page.path; 343 | const lang = getPageLanguage(this.page); 344 | const base = path.startsWith(lang) ? path.slice(lang.length + 1) : path; 345 | return (language ? '/' + language : '') + '/' + base; 346 | }); 347 | 348 | /** 349 | * Format language to RFC5646 style 350 | */ 351 | hexo.extend.helper.register('rfc5646', function (language) { 352 | return formatRfc5646(language); 353 | }); 354 | 355 | /** 356 | * Return the ISO639 part of the language tag 357 | */ 358 | hexo.extend.helper.register('iso639', function (language) { 359 | return formatIso639(language); 360 | }); 361 | 362 | /** 363 | * Get the closest language tag to the provided language tag 364 | */ 365 | hexo.extend.helper.register('closest_rfc5646_with_country_code', function (language) { 366 | return getClosestRfc5646WithCountryCode(language); 367 | }); 368 | 369 | /** 370 | * Get the language name 371 | */ 372 | hexo.extend.helper.register('language_name', function (language) { 373 | const name = hexo.theme.i18n.__(language)('name'); 374 | return name === 'name' ? language : name; 375 | }); -------------------------------------------------------------------------------- /scripts/99_config.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const yaml = require('js-yaml'); 4 | 5 | const altConfigs = {}; 6 | const themeRoot = path.join(__dirname, '..'); 7 | 8 | /** 9 | * Theme configuration helper. 10 | */ 11 | function getConfig(config, path) { 12 | const paths = path.split('.'); 13 | for (let path of paths) { 14 | if (typeof(config) === 'undefined' || config === null || !config.hasOwnProperty(path)) { 15 | return null; 16 | } 17 | config = config[path]; 18 | } 19 | return config; 20 | } 21 | 22 | /** 23 | * Get alternative theme config file by page language 24 | * 25 | * @param lang page language 26 | * @returns Object merged theme config 27 | */ 28 | function getThemeConfig(lang = null) { 29 | if (lang) { 30 | if (!altConfigs.hasOwnProperty(lang)) { 31 | const configPath = path.join(themeRoot, '_config.' + lang + '.yml'); 32 | if (fs.existsSync(configPath)) { 33 | const config = yaml.load(fs.readFileSync(configPath)); 34 | if (config != null) { 35 | altConfigs[lang] = config; 36 | } 37 | } 38 | } 39 | if (altConfigs.hasOwnProperty(lang) && altConfigs[lang]) { 40 | return Object.assign({}, hexo.theme.config, altConfigs[lang]); 41 | } 42 | } 43 | return hexo.theme.config; 44 | } 45 | 46 | hexo.extend.helper.register('has_config', function (configName, excludePage = false) { 47 | return getConfig(Object.assign({}, 48 | this.config, 49 | getThemeConfig(this.page.lang), 50 | !excludePage ? this.page : {}), configName) !== null; 51 | }); 52 | 53 | hexo.extend.helper.register('get_config', function (configName, defaultValue = null, excludePage = false) { 54 | let config = getConfig(Object.assign({}, 55 | this.config, 56 | getThemeConfig(this.page.lang), 57 | !excludePage ? this.page : {}), configName); 58 | return config === null ? defaultValue : config; 59 | }); -------------------------------------------------------------------------------- /scripts/99_content.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | const moment = require('moment'); 3 | const cheerio = require('cheerio'); 4 | const { formatRfc5646, formatIso639, getClosestRfc5646WithCountryCode, getPageLanguage } = require('../lib/i18n')(hexo); 5 | 6 | const MOMENTJS_SUPPORTED_LANGUAGES = ['af', 'ar-dz', 'ar-kw', 'ar-ly', 'ar-ma', 'ar-sa', 7 | 'ar-tn', 'ar', 'az', 'be', 'bg', 'bm', 'bn', 'bo', 'br', 'bs', 'ca', 'cs', 'cv', 'cy', 8 | 'da', 'de-at', 'de-ch', 'de', 'dv', 'el', 'en-au', 'en-ca', 'en-gb', 'en-ie', 'en-il', 9 | 'en-nz', 'eo', 'es-do', 'es-us', 'es', 'et', 'eu', 'fa', 'fi', 'fo', 'fr-ca', 'fr-ch', 10 | 'fr', 'fy', 'gd', 'gl', 'gom-latn', 'gu', 'he', 'hi', 'hr', 'hu', 'hy-am', 'id', 'is', 11 | 'it', 'ja', 'jv', 'ka', 'kk', 'km', 'kn', 'ko', 'ky', 'lb', 'lo', 'lt', 'lv', 'me', 12 | 'mi', 'mk', 'ml', 'mn', 'mr', 'ms-my', 'ms', 'mt', 'my', 'nb', 'ne', 'nl-be', 'nl', 13 | 'nn', 'pa-in', 'pl', 'pt-br', 'pt', 'ro', 'ru', 'sd', 'se', 'si', 'sk', 'sl', 'sq', 14 | 'sr-cyrl', 'sr', 'ss', 'sv', 'sw', 'ta', 'te', 'tet', 'tg', 'th', 'tl-ph', 'tlh', 'tr', 15 | 'tzl', 'tzm-latn', 'tzm', 'ug-cn', 'uk', 'ur', 'uz-latn', 'uz', 'vi', 'x-pseudo', 'yo', 16 | 'zh-cn', 'zh-hk', 'zh-tw']; 17 | 18 | function getMomentLocale(language) { 19 | let locale = formatRfc5646(language); 20 | if (MOMENTJS_SUPPORTED_LANGUAGES.indexOf(locale) === -1) { 21 | if (MOMENTJS_SUPPORTED_LANGUAGES.indexOf(formatIso639(locale)) > -1) { 22 | locale = formatIso639(locale); 23 | } else if (MOMENTJS_SUPPORTED_LANGUAGES.indexOf(getClosestRfc5646WithCountryCode(locale).toLowerCase()) > -1) { 24 | locale = getClosestRfc5646WithCountryCode(locale); 25 | } 26 | } 27 | return locale; 28 | } 29 | 30 | function injectMomentLocale(func) { 31 | return function () { 32 | let language = getMomentLocale(getPageLanguage(this.page)); 33 | moment.locale(language); 34 | const args = Array.prototype.slice.call(arguments).map(arg => { 35 | if (arg instanceof moment) { 36 | return moment(arg).locale(language); 37 | } 38 | return arg; 39 | }); 40 | return func.apply(this, args); 41 | } 42 | } 43 | 44 | hexo.extend.helper.register('is_categories', function () { 45 | return this.page.__categories; 46 | }); 47 | 48 | hexo.extend.helper.register('is_tags', function () { 49 | return this.page.__tags; 50 | }); 51 | 52 | /** 53 | * Generate html head title based on page type 54 | */ 55 | hexo.extend.helper.register('page_title', function () { 56 | const page = this.page; 57 | let title = page.title; 58 | 59 | if (this.is_archive()) { 60 | title = this.__('common.archives'); 61 | if (this.is_month()) { 62 | title += ': ' + page.year + '/' + page.month; 63 | } else if (this.is_year()) { 64 | title += ': ' + page.year; 65 | } 66 | } else if (this.is_category()) { 67 | title = this.__('common.category') + ': ' + page.category; 68 | } else if (this.is_tag()) { 69 | title = this.__('common.tag') + ': ' + page.tag; 70 | } else if (this.is_categories()) { 71 | title = this.__('common.categories'); 72 | } else if (this.is_tags()) { 73 | title = this.__('common.tags'); 74 | } 75 | 76 | const getConfig = hexo.extend.helper.get('get_config').bind(this); 77 | 78 | return [title, getConfig('title', '', true)].filter(str => typeof (str) !== 'undefined' && str.trim() !== '').join(' - '); 79 | }); 80 | 81 | /** 82 | * Format date to string without year. 83 | */ 84 | hexo.extend.helper.register('format_date', injectMomentLocale(function (date) { 85 | return moment(date).format('MMM D'); 86 | })); 87 | 88 | /** 89 | * Format date to string with year. 90 | */ 91 | hexo.extend.helper.register('format_date_full', injectMomentLocale(function (date) { 92 | return moment(date).format('MMM D YYYY'); 93 | })); 94 | 95 | /** 96 | * Get moment.js supported page locale 97 | */ 98 | hexo.extend.helper.register('momentjs_locale', function () { 99 | return getMomentLocale(getPageLanguage(this.page)); 100 | }); 101 | 102 | /** 103 | * Export moment.duration 104 | */ 105 | hexo.extend.helper.register('duration', injectMomentLocale(function () { 106 | return moment.duration.apply(null, arguments); 107 | })); 108 | 109 | /** 110 | * Get the word count of a paragraph. 111 | */ 112 | hexo.extend.helper.register('word_count', (content) => { 113 | content = content.replace(/<\/?[a-z][^>]*>/gi, ''); 114 | content = content.trim(); 115 | return content ? (content.match(/[\u00ff-\uffff]|[a-zA-Z]+/g) || []).length : 0; 116 | }); 117 | 118 | /** 119 | * Export a list of headings of an article 120 | * [ 121 | * ['1', 'heading-anchor-1', 'Title of the heading 1', 1], 122 | * ['1.1', 'heading-anchor-1-1', 'Title of the heading 1.1', 2], 123 | * ] 124 | */ 125 | hexo.extend.helper.register('toc_list', (content) => { 126 | const $ = cheerio.load(content, { decodeEntities: false }); 127 | const levels = [0, 0, 0]; 128 | const levelTags = []; 129 | // Get top 3 headings 130 | for (let i = 1; i <= 6; i++) { 131 | if ($('h' + i).length > 0) { 132 | levelTags.push('h' + i); 133 | } 134 | if (levelTags.length === 3) { 135 | break; 136 | } 137 | } 138 | const tocList = []; 139 | if (levelTags.length === 0) { 140 | return tocList; 141 | } 142 | const headings = $(levelTags.join(',')); 143 | headings.each(function () { 144 | const level = levelTags.indexOf(this.name); 145 | const id = $(this).attr('id'); 146 | const text = _.escape($(this).text()); 147 | 148 | for (let i = 0; i < levels.length; i++) { 149 | if (i > level) { 150 | levels[i] = 0; 151 | } else if (i < level) { 152 | // if headings start with a lower level heading, set the former heading index to 1 153 | // e.g. h3, h2, h1, h2, h3 => 1.1.1, 1.2, 2, 2.1, 2.1.1 154 | if (levels[i] === 0) { 155 | levels[i] = 1; 156 | } 157 | } else { 158 | levels[i] += 1; 159 | } 160 | } 161 | tocList.push([levels.slice(0, level + 1).join('.'), id, text, level + 1]); 162 | }); 163 | return tocList; 164 | }); 165 | -------------------------------------------------------------------------------- /scripts/99_tags.js: -------------------------------------------------------------------------------- 1 | const cheerio = require('cheerio'); 2 | 3 | /** 4 | * Colored quote block 5 | */ 6 | hexo.extend.tag.register('colorquote', function (args, content) { 7 | var type = args[0]; 8 | return '
    ' + hexo.render.renderSync({ text: content, engine: 'markdown' }) + '
    '; 9 | }, { ends: true }); 10 | 11 | const rEscapeContent = /]*)>([\s\S]*?)<\/escape>/g; 12 | const rPlaceholder = /(?:<|<)epacse(?:[^>]*)(?:>|>)(\d+)(?:<|<)\/epacse(?:[^>]*)(?:>|>)/g; 13 | const cache = []; 14 | function escapeContent(str) { 15 | return ''; 16 | } 17 | 18 | hexo.extend.filter.register('before_post_render', function (data) { 19 | data.content = data.content.replace(rEscapeContent, function (match, content) { 20 | return escapeContent(content); 21 | }); 22 | return data; 23 | }); 24 | 25 | hexo.extend.filter.register('after_post_render', function (data) { 26 | data.content = data.content.replace(rPlaceholder, function () { 27 | return cache[arguments[1]]; 28 | }); 29 | return data; 30 | }); 31 | 32 | function patchCodeHighlight(content) { 33 | const $ = cheerio.load(content, { decodeEntities: true }); 34 | $('figure.highlight').addClass('hljs'); 35 | $('figure.highlight .code .line span').each(function () { 36 | const classes = $(this).attr('class').split(' '); 37 | if (classes.length === 1) { 38 | $(this).addClass('hljs-' + classes[0]); 39 | $(this).removeClass(classes[0]); 40 | } 41 | }); 42 | return $.html(); 43 | } 44 | 45 | /** 46 | * Add .hljs class name to the code blocks and code elements. 47 | * Note: must be put after the above escape patch (hexojs/hexo#2400) 48 | */ 49 | hexo.extend.filter.register('after_post_render', function (data) { 50 | data.content = data.content ? patchCodeHighlight(data.content) : data.content; 51 | data.excerpt = data.excerpt ? patchCodeHighlight(data.excerpt) : data.excerpt; 52 | return data; 53 | }); 54 | -------------------------------------------------------------------------------- /source/css/insight.scss: -------------------------------------------------------------------------------- 1 | .ins-section-container { 2 | position: relative; 3 | background: #f7f7f7; 4 | } 5 | 6 | .ins-section { 7 | font-size: 14px; 8 | line-height: 16px; 9 | } 10 | 11 | .ins-section .ins-section-header, 12 | .ins-section .ins-search-item { 13 | padding: 8px 15px; 14 | } 15 | 16 | .ins-section .ins-section-header { 17 | color: #9a9a9a; 18 | border-bottom: 1px solid #e2e2e2; 19 | } 20 | 21 | .ins-section .ins-slug { 22 | margin-left: 5px; 23 | color: #9a9a9a; 24 | } 25 | 26 | .ins-section .ins-slug:before { 27 | content: '('; 28 | } 29 | 30 | .ins-section .ins-slug:after { 31 | content: ')'; 32 | } 33 | 34 | .ins-section .ins-search-item header, 35 | .ins-section .ins-search-item .ins-search-preview { 36 | overflow: hidden; 37 | white-space: nowrap; 38 | text-overflow: ellipsis; 39 | } 40 | 41 | .ins-section .ins-search-item header .ins-title { 42 | margin-left: 8px; 43 | } 44 | 45 | .ins-section .ins-search-item .ins-search-preview { 46 | height: 15px; 47 | font-size: 12px; 48 | color: #9a9a9a; 49 | margin: 5px 0 0 20px; 50 | } 51 | 52 | .ins-section .ins-search-item:hover, 53 | .ins-section .ins-search-item.active { 54 | color: #fff; 55 | background: #3273dc; 56 | } 57 | 58 | .ins-section .ins-search-item:hover .ins-slug, 59 | .ins-section .ins-search-item.active .ins-slug, 60 | .ins-section .ins-search-item:hover .ins-search-preview, 61 | .ins-section .ins-search-item.active .ins-search-preview { 62 | color: #fff; 63 | } -------------------------------------------------------------------------------- /source/css/style.scss: -------------------------------------------------------------------------------- 1 | $family-serif: "Ovo", "Georgia", "STZhongsong", "Microsoft YaHei", serif; 2 | $family-mono: "Source Code Pro", monospace, "Microsoft YaHei"; 3 | $border-color: #dbdbdb; 4 | 5 | body, button, input, select, textarea { 6 | font-family: $family-serif; 7 | } 8 | 9 | 10 | a.tag.article-tag { 11 | font-size: 0.9rem; 12 | text-decoration: none; 13 | } 14 | 15 | @media screen and (min-width: 1024px) { 16 | .container { 17 | max-width: 800px; 18 | width: 800px; 19 | } 20 | 21 | .navbar.is-transparent .navbar-dropdown a.navbar-item:hover { 22 | color: #3273dc; 23 | } 24 | } 25 | 26 | .navbar-main { 27 | border-bottom: 1px solid $border-color; 28 | transition: 0.1s ease-in; 29 | 30 | .navbar-logo { 31 | font-size: 1.5rem; 32 | font-weight: 600; 33 | } 34 | 35 | .navbar-item { 36 | text-align: center; 37 | } 38 | 39 | a.navbar-item.is-active { 40 | color: #3273dc; 41 | } 42 | 43 | .toc .navbar-dropdown { 44 | max-height: 500px; 45 | overflow-y: auto; 46 | } 47 | } 48 | 49 | .article { 50 | .second-date-block { 51 | color: gray; 52 | font-size: 0.8rem; 53 | } 54 | 55 | & + .article { 56 | padding-top: 2rem; 57 | border-top: 1px solid $border-color; 58 | } 59 | 60 | .article-title a, 61 | .article-meta a { 62 | color: inherit; 63 | border-bottom: 1px dotted transparent; 64 | &:hover { 65 | border-bottom-color: black; 66 | } 67 | } 68 | 69 | .article-meta { 70 | font-size: .9rem; 71 | text-transform: uppercase; 72 | 73 | @media screen and (max-width: 768px) { 74 | margin-top: 0.75rem; 75 | 76 | .column { 77 | padding-top: 0; 78 | padding-bottom: 0; 79 | } 80 | } 81 | 82 | .article-category-link { 83 | margin: 0 0.3rem; 84 | } 85 | } 86 | 87 | .article-entry { 88 | margin-bottom: 3rem; 89 | h1, h2, h3, h4, h5, h6 { 90 | cursor: pointer; 91 | position: relative; 92 | 93 | &:hover:before { 94 | content: '#'; 95 | position: absolute; 96 | right: 100%; 97 | } 98 | } 99 | 100 | a { 101 | word-break: break-word; 102 | } 103 | } 104 | 105 | .article-more-link a { 106 | font-size: .9rem; 107 | color: darkgray; 108 | border-bottom: 1px dotted transparent; 109 | &:hover { 110 | border-bottom-color: darkgray; 111 | } 112 | } 113 | 114 | .article-nav { 115 | a { 116 | color: darkgray; 117 | position: relative; 118 | display: inline-block; 119 | } 120 | 121 | .article-nav-prev a { 122 | padding-left: 1rem; 123 | 124 | &:before { 125 | top: 0; 126 | left: 0; 127 | content: '<'; 128 | position: absolute; 129 | } 130 | } 131 | 132 | .article-nav-next { 133 | text-align: right; 134 | 135 | a { 136 | padding-right: 1rem; 137 | 138 | &:after { 139 | top: 0; 140 | right: 0; 141 | content: '>'; 142 | position: absolute; 143 | } 144 | } 145 | } 146 | 147 | 148 | @media screen and (max-width: 768px) { 149 | .article-nav-prev, 150 | .article-nav-next { 151 | text-align: center; 152 | } 153 | } 154 | } 155 | 156 | .caption { 157 | display: block; 158 | color: darkgray; 159 | font-size: .95rem; 160 | text-align: center; 161 | } 162 | 163 | a[target="_blank"] { 164 | &:after { 165 | display: none; 166 | content: "\f35d"; 167 | font-size: .8rem; 168 | font-family: "Font Awesome 5 Solid"; 169 | } 170 | 171 | .fa-external-link-alt { 172 | margin-left: .3rem; 173 | width: .8rem; 174 | } 175 | } 176 | 177 | blockquote { 178 | &.colorquote { 179 | position: relative; 180 | 181 | &:before { 182 | content: " "; 183 | position: absolute; 184 | 185 | top: 50%; 186 | left: -14.5px; 187 | margin-top: -12px; 188 | width: 24px; 189 | height: 24px; 190 | 191 | border-radius: 50%; 192 | text-align: center; 193 | 194 | color: white; 195 | background-size: 16px 16px; 196 | background-position: 4px 4px; 197 | background-repeat: no-repeat; 198 | } 199 | 200 | &.info { 201 | border-color: hsl(204, 86%, 53%); 202 | background-color: lighten(hsl(204, 86%, 53%), 40%); 203 | 204 | &:before { 205 | background-color: hsl(204, 86%, 53%); 206 | background-image: url("../images/info.svg"); 207 | } 208 | } 209 | 210 | &.success { 211 | border-color: hsl(141, 71%, 48%); 212 | background-color: lighten(hsl(141, 71%, 48%), 40%); 213 | 214 | &:before { 215 | background-color: hsl(141, 71%, 48%); 216 | background-image: url("../images/check.svg"); 217 | } 218 | } 219 | 220 | &.warning { 221 | border-color: hsl(48, 100%, 67%); 222 | background-color: lighten(hsl(48, 100%, 67%), 24%); 223 | 224 | &:before { 225 | background-color: hsl(48, 100%, 67%); 226 | background-image: url("../images/question.svg"); 227 | } 228 | } 229 | 230 | &.danger { 231 | border-color: hsl(348, 100%, 61%); 232 | background-color: lighten(hsl(348, 100%, 61%), 24%); 233 | 234 | &:before { 235 | background-color: hsl(348, 100%, 61%); 236 | background-image: url("../images/exclamation.svg"); 237 | } 238 | } 239 | } 240 | 241 | &.pullquote { 242 | float: right; 243 | max-width: 50%; 244 | font-size: 1.15rem; 245 | position: relative; 246 | 247 | @media screen and (max-width: 768px) { 248 | float: none; 249 | max-width: 100%; 250 | } 251 | 252 | &:before { 253 | content: " "; 254 | 255 | position: absolute; 256 | top: 5px; 257 | left: 5px; 258 | width: 3rem; 259 | height: 3rem; 260 | 261 | opacity: 0.1; 262 | background: url("../images/quote-left.svg") 0 0/3rem 3rem no-repeat; 263 | } 264 | } 265 | 266 | footer { 267 | cite { 268 | font-style: normal; 269 | 270 | &:before { 271 | content: "—"; 272 | padding: 0 .3em; 273 | } 274 | } 275 | } 276 | } 277 | 278 | .gist { 279 | table { 280 | tr:hover { 281 | background: transparent; 282 | } 283 | td { 284 | border: none; 285 | } 286 | } 287 | 288 | .file { 289 | all: initial; 290 | } 291 | } 292 | } 293 | 294 | .sharebox { 295 | margin-top: 3rem; 296 | } 297 | 298 | .comments { 299 | margin-top: 3rem; 300 | padding-top: 3rem; 301 | border-top: 1px solid $border-color; 302 | 303 | & > .title { 304 | font-weight: 300; 305 | } 306 | } 307 | 308 | .archive { 309 | &:not(:last-child) { 310 | margin-bottom: 3rem; 311 | } 312 | 313 | .articles { 314 | border-left: 1px solid $border-color; 315 | } 316 | 317 | .article { 318 | border-top: none; 319 | margin-left: -1px; 320 | padding: 1rem 1.5rem; 321 | border-left: 3px solid transparent; 322 | 323 | &:hover { 324 | border-left-color: black; 325 | } 326 | 327 | time { 328 | font-size: .9rem; 329 | color: darkgray; 330 | } 331 | 332 | h6 { 333 | margin: 0; 334 | a { 335 | color: inherit; 336 | border-bottom: 1px dotted transparent; 337 | 338 | &:hover { 339 | border-bottom-color: darkgray; 340 | } 341 | } 342 | } 343 | } 344 | } 345 | 346 | .footer { 347 | font-size: .9rem; 348 | background: transparent; 349 | 350 | .content { 351 | padding-top: 2rem; 352 | border-top: 1px solid $border-color; 353 | } 354 | } 355 | 356 | .searchbox { 357 | display: none; 358 | 359 | &.show { 360 | display: block; 361 | } 362 | 363 | .searchbox-mask, 364 | .searchbox-container { 365 | position: fixed; 366 | } 367 | 368 | .searchbox-mask { 369 | top: 0; 370 | left: 0; 371 | width: 100%; 372 | height: 100%; 373 | z-index: 100; 374 | background: rgba(0,0,0,0.5); 375 | } 376 | 377 | .searchbox-selectable { 378 | cursor: pointer; 379 | } 380 | 381 | .searchbox-input-wrapper { 382 | position: relative; 383 | 384 | .searchbox-input { 385 | width: 100%; 386 | border: none; 387 | outline: none; 388 | font-size: 16px; 389 | box-shadow: none; 390 | font-weight: 200; 391 | border-radius: 0; 392 | background: #fff; 393 | line-height: 20px; 394 | box-sizing: border-box; 395 | padding: 12px 28px 12px 20px; 396 | border-bottom: 1px solid #e2e2e2; 397 | } 398 | 399 | .searchbox-close { 400 | top: 50%; 401 | right: 6px; 402 | width: 20px; 403 | height: 20px; 404 | font-size: 16px; 405 | margin-top: -11px; 406 | position: absolute; 407 | text-align: center; 408 | display: inline-block; 409 | 410 | &:hover { 411 | color: #3273dc; 412 | } 413 | } 414 | } 415 | 416 | .searchbox-result-wrapper { 417 | left: 0; 418 | right: 0; 419 | top: 45px; 420 | bottom: 0; 421 | overflow-y: auto; 422 | position: absolute; 423 | } 424 | 425 | .searchbox-container { 426 | left: 50%; 427 | top: 100px; 428 | width: 540px; 429 | z-index: 101; 430 | bottom: 100px; 431 | margin-left: -270px; 432 | box-sizing: border-box; 433 | 434 | @media screen and (max-width: 559px), screen and (max-height: 479px) { 435 | top: 0; 436 | left: 0; 437 | margin: 0; 438 | width: 100%; 439 | height: 100%; 440 | background: #f7f7f7; 441 | } 442 | } 443 | } 444 | 445 | code, pre { 446 | font-size: 0.85rem; 447 | font-family: $family-mono; 448 | } 449 | 450 | figure.highlight { 451 | padding: 0; 452 | width: 100%; 453 | margin: 0 0 1.5rem; 454 | 455 | pre, 456 | table tr:hover { 457 | color: inherit; 458 | background: transparent; 459 | } 460 | 461 | table { 462 | width: auto; 463 | } 464 | 465 | pre { 466 | padding: 0; 467 | overflow: visible; 468 | 469 | .line { 470 | line-height: 1.5rem; 471 | } 472 | } 473 | 474 | figcaption, 475 | .gutter { 476 | background: rgba(200, 200, 200, 0.15); 477 | } 478 | 479 | figcaption { 480 | padding: .3em .75em; 481 | text-align: left; 482 | font-style: normal; 483 | font-size: .8rem; 484 | 485 | span { 486 | font-weight: 500; 487 | font-family: $family-mono; 488 | } 489 | 490 | a { 491 | float: right; 492 | color: #9a9a9a; 493 | } 494 | } 495 | 496 | .gutter { 497 | text-align: right; 498 | } 499 | 500 | .tag, 501 | .title, 502 | .number, 503 | .section { 504 | display: inherit; 505 | font: inherit; 506 | margin: inherit; 507 | padding: inherit; 508 | background: inherit; 509 | height: inherit; 510 | text-align: inherit; 511 | vertical-align: inherit; 512 | min-width: inherit; 513 | border-radius: inherit; 514 | } 515 | } 516 | 517 | .pagination { 518 | margin: 0 -1em; 519 | 520 | .pagination-previous, 521 | .pagination-next, 522 | .pagination-link { 523 | border: 1px solid transparent; 524 | 525 | &:active { 526 | box-shadow: none; 527 | } 528 | } 529 | 530 | 531 | .pagination-link { 532 | &:hover { 533 | border-color: black; 534 | } 535 | } 536 | 537 | .pagination-link.is-current { 538 | background-color: black; 539 | } 540 | 541 | .pagination-previous a, 542 | .pagination-next a { 543 | position: relative; 544 | color: inherit; 545 | } 546 | 547 | .pagination-previous a { 548 | padding-left: 1rem; 549 | 550 | &:before { 551 | top: 0; 552 | left: 0; 553 | content: '<'; 554 | position: absolute; 555 | } 556 | } 557 | 558 | .pagination-next a { 559 | padding-right: 1rem; 560 | 561 | &:after { 562 | top: 0; 563 | right: 0; 564 | content: '>'; 565 | position: absolute; 566 | } 567 | } 568 | } 569 | 570 | .section-heading { 571 | padding-bottom: 0; 572 | 573 | h5 { 574 | margin-bottom: 0; 575 | text-transform: uppercase; 576 | 577 | & > svg { 578 | margin-right: 0.5rem; 579 | } 580 | } 581 | 582 | & + .section { 583 | padding-top: 2rem; 584 | } 585 | } 586 | 587 | .gallery-item { 588 | position: relative; 589 | display: inline-block; 590 | overflow: hidden; 591 | margin: 0; 592 | padding: 0; 593 | line-height: 0; 594 | max-width: 100%; 595 | 596 | &:hover { 597 | .caption { 598 | display: initial; 599 | } 600 | } 601 | 602 | .caption { 603 | display: none; 604 | position: absolute; 605 | opacity: .7; 606 | bottom: 0; 607 | padding: 5px; 608 | background-color: #000; 609 | left: 0; 610 | right: 0; 611 | margin: 0; 612 | color: #fff; 613 | font-size: 12px; 614 | font-weight: 300; 615 | font-family: inherit !important; 616 | line-height: 1.5; 617 | } 618 | } 619 | 620 | .video-container { 621 | width: 100%; 622 | margin-bottom: 20px; 623 | padding-top: 56.25%; 624 | position: relative; 625 | iframe { 626 | top: 0px; 627 | left: 0px; 628 | width: 100%; 629 | height: 100%; 630 | position: absolute; 631 | } 632 | } 633 | 634 | @import "insight"; 635 | -------------------------------------------------------------------------------- /source/images/check.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /source/images/exclamation.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /source/images/info.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /source/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ppoffice/hexo-theme-minos/b614b06b18da49983d48fcccef46644a613fdd16/source/images/logo.png -------------------------------------------------------------------------------- /source/images/question.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /source/images/quote-left.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /source/js/insight.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Insight search plugin 3 | * @author PPOffice { @link https://github.com/ppoffice } 4 | */ 5 | (function ($, CONFIG) { 6 | var $main = $('.ins-search'); 7 | var $input = $main.find('.ins-search-input'); 8 | var $wrapper = $main.find('.ins-section-wrapper'); 9 | var $container = $main.find('.ins-section-container'); 10 | $main.parent().remove('.ins-search'); 11 | $('body').append($main); 12 | 13 | function section (title) { 14 | return $('
    ').addClass('ins-section') 15 | .append($('
    ').addClass('ins-section-header').text(title)); 16 | } 17 | 18 | function searchItem (icon, title, slug, preview, url) { 19 | return $('
    ').addClass('ins-selectable').addClass('ins-search-item') 20 | .append($('
    ').append($('').addClass('fa').addClass('fa-' + icon)) 21 | .append($('').addClass('ins-title').text(title != null && title !== '' ? title : CONFIG.TRANSLATION['UNTITLED'])) 22 | .append(slug ? $('').addClass('ins-slug').text(slug) : null)) 23 | .append(preview ? $('

    ').addClass('ins-search-preview').text(preview) : null) 24 | .attr('data-url', url); 25 | } 26 | 27 | function sectionFactory (type, array) { 28 | var sectionTitle; 29 | var $searchItems; 30 | if (array.length === 0) return null; 31 | sectionTitle = CONFIG.TRANSLATION[type]; 32 | switch (type) { 33 | case 'POSTS': 34 | case 'PAGES': 35 | $searchItems = array.map(function (item) { 36 | // Use config.root instead of permalink to fix url issue 37 | return searchItem('file', item.title, null, item.text.slice(0, 150), item.link); 38 | }); 39 | break; 40 | case 'CATEGORIES': 41 | case 'TAGS': 42 | $searchItems = array.map(function (item) { 43 | return searchItem(type === 'CATEGORIES' ? 'folder' : 'tag', item.name, item.slug, null, item.link); 44 | }); 45 | break; 46 | default: 47 | return null; 48 | } 49 | return section(sectionTitle).append($searchItems); 50 | } 51 | 52 | function parseKeywords (keywords) { 53 | return keywords.split(' ').filter(function (keyword) { 54 | return !!keyword; 55 | }).map(function (keyword) { 56 | return keyword.toUpperCase(); 57 | }); 58 | } 59 | 60 | /** 61 | * Judge if a given post/page/category/tag contains all of the keywords. 62 | * @param Object obj Object to be weighted 63 | * @param Array fields Object's fields to find matches 64 | */ 65 | function filter (keywords, obj, fields) { 66 | var keywordArray = parseKeywords(keywords); 67 | var containKeywords = keywordArray.filter(function (keyword) { 68 | var containFields = fields.filter(function (field) { 69 | if (!obj.hasOwnProperty(field)) 70 | return false; 71 | if (obj[field].toUpperCase().indexOf(keyword) > -1) 72 | return true; 73 | }); 74 | if (containFields.length > 0) 75 | return true; 76 | return false; 77 | }); 78 | return containKeywords.length === keywordArray.length; 79 | } 80 | 81 | function filterFactory (keywords) { 82 | return { 83 | POST: function (obj) { 84 | return filter(keywords, obj, ['title', 'text']); 85 | }, 86 | PAGE: function (obj) { 87 | return filter(keywords, obj, ['title', 'text']); 88 | }, 89 | CATEGORY: function (obj) { 90 | return filter(keywords, obj, ['name', 'slug']); 91 | }, 92 | TAG: function (obj) { 93 | return filter(keywords, obj, ['name', 'slug']); 94 | } 95 | }; 96 | } 97 | 98 | /** 99 | * Calculate the weight of a matched post/page/category/tag. 100 | * @param Object obj Object to be weighted 101 | * @param Array fields Object's fields to find matches 102 | * @param Array weights Weight of every field 103 | */ 104 | function weight (keywords, obj, fields, weights) { 105 | var value = 0; 106 | parseKeywords(keywords).forEach(function (keyword) { 107 | var pattern = new RegExp(keyword, 'img'); // Global, Multi-line, Case-insensitive 108 | fields.forEach(function (field, index) { 109 | if (obj.hasOwnProperty(field)) { 110 | var matches = obj[field].match(pattern); 111 | value += matches ? matches.length * weights[index] : 0; 112 | } 113 | }); 114 | }); 115 | return value; 116 | } 117 | 118 | function weightFactory (keywords) { 119 | return { 120 | POST: function (obj) { 121 | return weight(keywords, obj, ['title', 'text'], [3, 1]); 122 | }, 123 | PAGE: function (obj) { 124 | return weight(keywords, obj, ['title', 'text'], [3, 1]); 125 | }, 126 | CATEGORY: function (obj) { 127 | return weight(keywords, obj, ['name', 'slug'], [1, 1]); 128 | }, 129 | TAG: function (obj) { 130 | return weight(keywords, obj, ['name', 'slug'], [1, 1]); 131 | } 132 | }; 133 | } 134 | 135 | function search (json, keywords) { 136 | var WEIGHTS = weightFactory(keywords); 137 | var FILTERS = filterFactory(keywords); 138 | var posts = json.posts; 139 | var pages = json.pages; 140 | var tags = json.tags; 141 | var categories = json.categories; 142 | return { 143 | posts: posts.filter(FILTERS.POST).sort(function (a, b) { return WEIGHTS.POST(b) - WEIGHTS.POST(a); }).slice(0, 5), 144 | pages: pages.filter(FILTERS.PAGE).sort(function (a, b) { return WEIGHTS.PAGE(b) - WEIGHTS.PAGE(a); }).slice(0, 5), 145 | categories: categories.filter(FILTERS.CATEGORY).sort(function (a, b) { return WEIGHTS.CATEGORY(b) - WEIGHTS.CATEGORY(a); }).slice(0, 5), 146 | tags: tags.filter(FILTERS.TAG).sort(function (a, b) { return WEIGHTS.TAG(b) - WEIGHTS.TAG(a); }).slice(0, 5) 147 | }; 148 | } 149 | 150 | function searchResultToDOM (searchResult) { 151 | $container.empty(); 152 | for (var key in searchResult) { 153 | $container.append(sectionFactory(key.toUpperCase(), searchResult[key])); 154 | } 155 | } 156 | 157 | function scrollTo ($item) { 158 | if ($item.length === 0) return; 159 | var wrapperHeight = $wrapper[0].clientHeight; 160 | var itemTop = $item.position().top - $wrapper.scrollTop(); 161 | var itemBottom = $item[0].clientHeight + $item.position().top; 162 | if (itemBottom > wrapperHeight + $wrapper.scrollTop()) { 163 | $wrapper.scrollTop(itemBottom - $wrapper[0].clientHeight); 164 | } 165 | if (itemTop < 0) { 166 | $wrapper.scrollTop($item.position().top); 167 | } 168 | } 169 | 170 | function selectItemByDiff (value) { 171 | var $items = $.makeArray($container.find('.ins-selectable')); 172 | var prevPosition = -1; 173 | $items.forEach(function (item, index) { 174 | if ($(item).hasClass('active')) { 175 | prevPosition = index; 176 | return; 177 | } 178 | }); 179 | var nextPosition = ($items.length + prevPosition + value) % $items.length; 180 | $($items[prevPosition]).removeClass('active'); 181 | $($items[nextPosition]).addClass('active'); 182 | scrollTo($($items[nextPosition])); 183 | } 184 | 185 | function gotoLink ($item) { 186 | if ($item && $item.length) { 187 | location.href = $item.attr('data-url'); 188 | } 189 | } 190 | 191 | $.getJSON(CONFIG.CONTENT_URL, function (json) { 192 | if (location.hash.trim() === '#ins-search') { 193 | $main.addClass('show'); 194 | } 195 | $input.on('input', function () { 196 | var keywords = $(this).val(); 197 | searchResultToDOM(search(json, keywords)); 198 | }); 199 | $input.trigger('input'); 200 | }); 201 | 202 | var touch = false; 203 | $(document).on('click focus', '.navbar-main .search', function () { 204 | $main.addClass('show'); 205 | $main.find('.ins-search-input').focus(); 206 | }).on('click touchend', '.ins-search-item', function (e) { 207 | if (e.type !== 'click' && !touch) { 208 | return; 209 | } 210 | gotoLink($(this)); 211 | touch = false; 212 | }).on('click touchend', '.ins-close', function (e) { 213 | if (e.type !== 'click' && !touch) { 214 | return; 215 | } 216 | $main.removeClass('show'); 217 | touch = false; 218 | }).on('keydown', function (e) { 219 | if (!$main.hasClass('show')) return; 220 | switch (e.keyCode) { 221 | case 27: // ESC 222 | $main.removeClass('show'); break; 223 | case 38: // UP 224 | selectItemByDiff(-1); break; 225 | case 40: // DOWN 226 | selectItemByDiff(1); break; 227 | case 13: //ENTER 228 | gotoLink($container.find('.ins-selectable.active').eq(0)); break; 229 | } 230 | }).on('touchstart', function (e) { 231 | touch = true; 232 | }).on('touchmove', function (e) { 233 | touch = false; 234 | }); 235 | })(jQuery, window.INSIGHT_CONFIG); -------------------------------------------------------------------------------- /source/js/script.js: -------------------------------------------------------------------------------- 1 | (function ($) { 2 | $('.navbar-burger').click(function () { 3 | $(this).toggleClass('is-active'); 4 | $('.navbar-main .navbar-start').toggleClass('is-active'); 5 | $('.navbar-main .navbar-end').toggleClass('is-active'); 6 | }); 7 | 8 | // Hide Header on on scroll down 9 | var didScroll; 10 | var lastScrollTop = 0; 11 | var delta = 5; 12 | var navbarHeight = $('.navbar-main').outerHeight(); 13 | 14 | $(window).scroll(function(event){ 15 | didScroll = true; 16 | }); 17 | 18 | setInterval(function() { 19 | if (didScroll) { 20 | hasScrolled(); 21 | didScroll = false; 22 | } 23 | }, 250); 24 | 25 | function hasScrolled() { 26 | var st = $(this).scrollTop(); 27 | 28 | // Make sure they scroll more than delta 29 | if(Math.abs(lastScrollTop - st) <= delta) { 30 | return; 31 | } 32 | 33 | // If they scrolled down and are past the navbar, add class .navbar-down. 34 | // This is necessary so you never see what is "behind" the navbar. 35 | if (st > lastScrollTop && st > navbarHeight) { 36 | var posY = Math.min(st, navbarHeight); 37 | // Scroll Down 38 | $('.navbar-main').css({ 39 | '-webkit-transform' : 'translateY(-' + posY + 'px)', 40 | '-moz-transform' : 'translateY(-' + posY + 'px)', 41 | '-ms-transform' : 'translateY(-' + posY + 'px)', 42 | '-o-transform' : 'translateY(-' + posY + 'px)', 43 | 'transform' : 'translateY(-' + posY + 'px)' 44 | }); 45 | } else { 46 | // Scroll Up 47 | if(st + $(window).height() < $(document).height()) { 48 | $('.navbar-main').css({ 49 | '-webkit-transform' : 'translateY(0px)', 50 | '-moz-transform' : 'translateY(0px)', 51 | '-ms-transform' : 'translateY(0px)', 52 | '-o-transform' : 'translateY(0px)', 53 | 'transform' : 'translateY(0px)' 54 | }); 55 | } 56 | } 57 | 58 | lastScrollTop = st; 59 | } 60 | 61 | $('.article.gallery img:not(".not-gallery-item")').each(function () { 62 | // wrap images with link and add caption if possible 63 | if ($(this).parent('a').length === 0) { 64 | $(this).wrap(''); 65 | if (this.alt) { 66 | $(this).after('

    ' + this.alt + '
    '); 67 | } 68 | } 69 | }); 70 | 71 | $('.article-entry').find('h1, h2, h3, h4, h5, h6').on('click', function () { 72 | if ($(this).get(0).id) { 73 | window.location.hash = $(this).get(0).id; 74 | } 75 | }); 76 | 77 | if (typeof(moment) === 'function') { 78 | $('.article-meta time').each(function () { 79 | $(this).text(moment($(this).attr('datetime')).fromNow()); 80 | }); 81 | } 82 | })(jQuery); --------------------------------------------------------------------------------