├── .github
├── ISSUE_TEMPLATE
│ ├── bug-report-----.md
│ ├── bug_report.md
│ ├── feature_request.md
│ └── question.md
├── renovate.json
└── stale.yml
├── .gitignore
├── .npmignore
├── .travis.yml
├── LICENSE
├── README.md
├── README_zh-Hans.md
├── _config.yml
├── languages
├── en.yml
├── ja.yml
├── zh-Hans.yml
└── zh-Hant.yml
├── layout
└── index.njk
├── lib
├── config.js
├── configSchema.json
├── filter
│ ├── index.js
│ ├── post.js
│ ├── ssr.js
│ └── templates.js
├── generator
│ ├── config.js
│ ├── entries
│ │ ├── archives.js
│ │ ├── categories.js
│ │ ├── index.js
│ │ ├── pages.js
│ │ ├── posts.js
│ │ ├── properties.js
│ │ ├── search.js
│ │ └── tags.js
│ ├── index.js
│ ├── manifest.js
│ ├── sitemap.js
│ └── sw.js
├── helper
│ ├── ga.js
│ ├── index.js
│ ├── structured_data.js
│ └── url_trim.js
├── plugins
│ ├── cipher.js
│ ├── disqus.js
│ ├── manifest.json
│ └── palette.js
├── renderer
│ ├── index.js
│ └── markdown
│ │ ├── index.js
│ │ ├── mixins.js
│ │ └── plugins
│ │ ├── collapse.js
│ │ ├── index.js
│ │ ├── timeline.js
│ │ └── tree.js
├── tag
│ ├── canvas.js
│ ├── gist.js
│ ├── iframe.js
│ └── index.js
└── utils.js
├── package.json
├── scripts
└── index.js
├── source
├── 3rdpartylicenses.txt
├── _manifest.json
├── _ssr.js
├── main.f5cfbba069c3444b.js
├── polyfills.13e521fb4f0cbc90.js
├── runtime.a95bc83b747d4636.js
├── styles.79f9d555e464cb1b669d.css
└── theme.97aefb00.js
└── test
├── README.md
├── index.js
├── jasmine.json
└── scripts
├── filters
├── index.js
├── post.js
└── template.js
├── helpers
├── ga.js
├── index.js
├── structured_data.js
└── url_trim.js
├── renderers
├── index.js
└── md.js
├── tags
├── canvas.js
├── gist.js
└── index.js
└── utils
├── index.js
├── parseConfig.js
└── rest.js
/.github/ISSUE_TEMPLATE/bug-report-----.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report [中文]
3 | about: Create a report to help us improve
4 | title: ''
5 | labels: bug
6 | assignees: ''
7 |
8 | ---
9 |
10 |
44 |
45 | 请删除此行及以上所有内容
46 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: ''
5 | labels: bug
6 | assignees: ''
7 |
8 | ---
9 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: ''
5 | labels: enhancement
6 | assignees: ''
7 |
8 | ---
9 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/question.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Question
3 | about: Ask whatever you want
4 | title: ''
5 | labels: question
6 | assignees: ''
7 |
8 | ---
9 |
--------------------------------------------------------------------------------
/.github/renovate.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": [
3 | "config:base"
4 | ],
5 | "baseBranches": [
6 | "dev"
7 | ]
8 | }
9 |
--------------------------------------------------------------------------------
/.github/stale.yml:
--------------------------------------------------------------------------------
1 | # Number of days of inactivity before an issue becomes stale
2 | daysUntilStale: 10
3 | # Number of days of inactivity before a stale issue is closed
4 | daysUntilClose: 1
5 | # Issues with these labels will never be considered stale
6 | exemptLabels:
7 | - bug
8 | - duplicate
9 | - enhancement
10 | - good first issue
11 | - help wanted
12 | - invalid
13 | - question
14 | - wontfix
15 | # Label to use when marking an issue as stale
16 | staleLabel: stale
17 | # Comment to post when marking an issue as stale. Set to `false` to disable
18 | markComment: >
19 | This issue has been automatically marked as stale because it has not had
20 | recent activity. It will be closed if no further activity occurs. Thank you
21 | for your contributions.
22 | # Comment to post when removing the stale label. Set to `false` to disable
23 | unmarkComment: false
24 | # Comment to post when closing a stale issue. Set to `false` to disable
25 | closeComment: false
26 | # Limit to only `issues` or `pulls`
27 | only: issues
28 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | package-lock.json
3 | yarn.lock
4 | .DS_Store
5 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | test/
2 | .travis.yml
3 | .github/
4 | .DS_Store
5 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 |
3 | cache:
4 | npm: true
5 |
6 | node_js:
7 | - "18"
8 |
9 | before_script:
10 | - rm -rf node_modules
11 | - rm -rf .git
12 | - git clone https://github.com/hexojs/hexo-theme-unit-test temp
13 | - mkdir temp/themes
14 | - mkdir temp/themes/landscape
15 | - rsync -av ./ ./temp/themes/landscape --exclude ./temp/
16 | - cd temp
17 | - npm install
18 | - cd themes/landscape
19 | - npm install
20 |
21 | script:
22 | - npm test
23 | - cd ../..
24 | - hexo g && hexo g
25 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 Elmore Cheng
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | Language:
3 | EN
4 |
中文
5 |
6 |
7 | INSIDE
8 |
9 | 🌈 SPA, Flat and clean theme for Hexo, built with Angular.
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | ## Preview
22 |
23 | - https://blog.oniuo.com
24 | - https://blog.oniuo.com/post/inside-theme-showcase
25 |
26 | ## Features
27 |
28 | - Custom theming
29 | - Built-in Search
30 | - Built-in [Disqus](https://disqus.com)
31 | - Flexible plugin mechanism
32 | - Enhanced content display
33 | - Reward, Copyright notice, Picture zooming
34 | - Table (headless table, long table)
35 | - Content addons, out of the box (Collapse, Timeline, Content Crypto)
36 | - Misc
37 | - [PWA](https://developers.google.com/web/progressive-web-apps) (Immersive design, Offline support ([workbox](https://developers.google.com/web/tools/workbox/)))
38 | - SEO (SSR、sitemap)
39 | - Print friendly
40 |
41 | ## Quick start
42 |
43 | 1\. Locate to `project/` and run
44 |
45 | ```bash
46 | npm install hexo-theme-inside
47 | ```
48 |
49 | 2\. Config `project/_config.yml`
50 |
51 | ```yaml
52 | theme: inside
53 | ```
54 |
55 | 3\. Copy [_config.yml](https://github.com/ikeq/hexo-theme-inside/blob/master/_config.yml) to `project/_config.inside.yml`, see [here](https://blog.oniuo.com/theme-inside) for full documentation.
56 |
57 | ## Changelog
58 |
59 | [releases](https://github.com/ikeq/hexo-theme-inside/releases)
60 |
61 | ## FAQ
62 |
63 | - Where to find front-end source code?
64 |
65 | https://bitbucket.org/ikeq/hexo-theme-inside-ng
66 |
67 | ## License
68 |
69 | [MIT](LICENSE)
70 |
--------------------------------------------------------------------------------
/README_zh-Hans.md:
--------------------------------------------------------------------------------
1 |
2 | Language:
3 |
EN
4 | 中文
5 |
6 |
7 | INSIDE
8 |
9 | 🌈 简约、现代的 SPA 主题, built with Angular.
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | ## 预览
22 |
23 | - https://blog.oniuo.com
24 | - https://blog.oniuo.com/post/inside-theme-showcase
25 |
26 | ## 特性
27 |
28 | - 自定义换肤
29 | - 内置搜索
30 | - 内置 [Disqus](https://disqus.com)
31 | - 灵活的插件机制
32 | - 增强的内容展示
33 | - 打赏、版权声明、图片缩放
34 | - 表格 (headless table, long table)
35 | - 内容组件,开箱即用 (Collapse、时间线、文字加密)
36 | - 其他
37 | - [PWA](https://developers.google.com/web/progressive-web-apps) (沉浸式设计、离线支持 ([workbox](https://developers.google.com/web/tools/workbox/)))
38 | - SEO (SSR、sitemap)
39 | - 打印机友好
40 |
41 | ## Quick start
42 |
43 | 1\. Locate to `project/` and run
44 |
45 | ```bash
46 | npm install hexo-theme-inside
47 | ```
48 |
49 | 2\. Config `project/_config.yml`
50 |
51 | ```yaml
52 | theme: inside
53 | ```
54 |
55 | 3\. Copy [_config.yml](https://github.com/ikeq/hexo-theme-inside/blob/master/_config.yml) to `project/_config.inside.yml`, see [here](https://blog.oniuo.com/theme-inside) for full documentation.
56 |
57 | ## Changelog
58 |
59 | [releases](https://github.com/ikeq/hexo-theme-inside/releases)
60 |
61 | ## FAQ
62 |
63 | - Where to find front-end source code?
64 |
65 | https://bitbucket.org/ikeq/hexo-theme-inside-ng
66 |
67 | ## License
68 |
69 | [MIT](LICENSE)
70 |
--------------------------------------------------------------------------------
/_config.yml:
--------------------------------------------------------------------------------
1 | # ---------------------------------------------------------------
2 | # Basic
3 | # ---------------------------------------------------------------
4 |
5 | # Appearance
6 | appearance:
7 | # Accent color, default '#2a2b33'
8 | accent_color:
9 | # Font color, default '#363636'
10 | foreground_color:
11 | # Border color, default '#e0e0e0'
12 | border_color:
13 | # body background, default '#f5f7fa'
14 | background:
15 | # Sidebar background (when opened), default to accent_color
16 | sidebar_background:
17 | # Card background, default '#fff'
18 | card_background:
19 |
20 | # All background settings above support image, e.g.,
21 | # '//www.toptal.com/designers/subtlepatterns/patterns/confectionary.png #f5f7fa' or
22 | # 'url(//www.toptal.com/designers/subtlepatterns/patterns/confectionary.png) #f5f7fa'
23 |
24 | # Maximum width of content
25 | # content_width: 640
26 |
27 | # Fonts
28 | font:
29 | # Url of font css file
30 | # Below is a font set containing Baloo Bhaijaan, Josefin Sans, Montserrat and Inconsolata from Google Fonts.
31 | url: //fonts.googleapis.com/css?family=Baloo+Bhaijaan|Inconsolata|Josefin+Sans|Montserrat
32 |
33 | # Base font which applied to body
34 | base: "'Josefin Sans', 'PingFang SC'"
35 | # Sidebar author font, fallback to `base`
36 | logo:
37 | # Sidebar menu font, fallback to `base`
38 | menu: 'Baloo Bhaijaan'
39 | # Font for:
40 | # - percentage on the fab button
41 | # - month text on archives page
42 | # - count
43 | # - pager
44 | # - toc index
45 | # - category pill
46 | # fallback to `base`
47 | label: Montserrat
48 | # Headline(h1, h2, h3, h4, h5, h6) font, fallback to `base`
49 | heading:
50 | # Code and code block
51 | code: Inconsolata, monospace
52 | # Base font for printing which applied to body
53 | print:
54 |
55 | # Code Syntax Highlighting
56 | # Uses an architecture called "base16" (https://github.com/chriskempson/base16),
57 | # the default theme is a customized Atelier Dune Light theme,
58 | # please feel free to explore more.
59 | highlight:
60 |
61 | # Default Light by Chris Kempson (http://chriskempson.com)
62 | # highlight: [
63 | # '#f8f8f8', '#e8e8e8', '#d8d8d8', '#b8b8b8',
64 | # '#585858', '#383838', '#282828', '#181818',
65 | # '#ab4642', '#dc9656', '#f7ca88', '#a1b56c',
66 | # '#86c1b9', '#7cafc2', '#ba8baf', '#a16946'
67 | # ]
68 |
69 | # Harmonic16 Light by Jannik Siebert (https://github.com/janniks)
70 | # highlight: [
71 | # '#f7f9fb', '#e5ebf1', '#cbd6e2', '#aabcce',
72 | # '#627e99', '#405c79', '#223b54', '#0b1c2c',
73 | # '#bf8b56', '#bfbf56', '#8bbf56', '#56bf8b',
74 | # '#568bbf', '#8b56bf', '#bf568b', '#bf5656'
75 | # ]
76 |
77 | # Tomorrow Night by Chris Kempson (http://chriskempson.com)
78 | # highlight: [
79 | # '#1d1f21', '#282a2e', '#373b41', '#969896',
80 | # '#b4b7b4', '#c5c8c6', '#e0e0e0', '#ffffff',
81 | # '#cc6666', '#de935f', '#f0c674', '#b5bd68',
82 | # '#8abeb7', '#81a2be', '#b294bb', '#a3685a'
83 | # ]
84 |
85 | darkmode:
86 | accent_color: '#539bf5'
87 | foreground_color: '#adbac7'
88 | border_color: '#373e47'
89 | background: '#22272e'
90 | sidebar_background: '#22272e'
91 | card_background: '#2d333b'
92 | highlight: [
93 | '#2d333b', '#444c56', '#3e4451', '#545862',
94 | '#565c64', '#abb2bf', '#b6bdca', '#c8ccd4',
95 | '#e06c75', '#d19a66', '#e5c07b', '#98c379',
96 | '#56b6c2', '#61afef', '#c678dd', '#be5046'
97 | ]
98 |
99 | # Sidebar profile
100 | profile:
101 | # `email` is used for gravatar(https://en.gravatar.com),
102 | # fallback to `hexo.config.email` if not specified
103 | email:
104 |
105 | # You can set Avatar URL directly
106 | # avatar:
107 |
108 | bio: Awesome guy.
109 |
110 | # Sidebar navigation links
111 | menu:
112 | Home: /
113 | # About: /about
114 | # Links: /links
115 | # Github: https://github.com/username
116 |
117 | # Social media URL
118 | sns:
119 | # Built-in icons
120 | # `email`, `feed`, `github`, `twitter`, `facebook`, `instagram`, `tumblr`, `dribbble`, `telegram`
121 | # `youtube`, `hangouts`, `linkedin`, `pinterest`, `soundcloud`, `myspace`, `weibo`, `qq
122 | - icon: email
123 | title: Email
124 | url: # default to `profile.email`
125 | - icon: feed
126 | title: Feed
127 | url: # default to `hexo.config.feed.path`
128 |
129 | # custom icon
130 | # - title: Email me
131 | # url: # default to `profile.email`
132 | # template:
133 | # - title: Love you
134 | # url:
135 | # template: |
136 | #
137 |
138 | # Sidebar footer copyright info
139 | footer:
140 | # Set to false to hide.
141 | # copyright: '© 2019 • John Doe'
142 |
143 | # Set to false to hide Hexo link.
144 | # powered: false
145 |
146 | # Set to false to hide theme link.
147 | # theme: false
148 |
149 | # Custom text.
150 | # custom: Hosted by Github Pages
152 |
153 |
154 | # ---------------------------------------------------------------
155 | # Content
156 | # ---------------------------------------------------------------
157 |
158 | # Page
159 | page:
160 | # Display Table of content, default to `true`
161 | # toc: false
162 |
163 | # Display reward, default to `false`
164 | # reward: true
165 |
166 | # Display copyright notice, default to `false`
167 | # copyright: true
168 |
169 |
170 | # Post
171 | post:
172 | # per_page: 10
173 | # reading_time: false
174 | # reading_time:
175 | # wpm: 150
176 | # text: ":words words in :minutes min"
177 |
178 | # The following settings are the same as the page
179 | # toc: false
180 | # reward: true
181 | # copyright: true
182 |
183 | # Archive
184 | archive:
185 | # per_page: 10
186 |
187 | # Tag
188 | tag:
189 | # per_page: 10
190 |
191 | # Category
192 | category:
193 | # per_page: 10
194 |
195 | # Search
196 | search:
197 | # Display a quick search button in fab
198 | # fab: true
199 |
200 | # Render a standalone searh page which can be configured in sidebar menu such as `Search: /search`
201 | # page: true
202 |
203 | # Local search
204 | # adapter:
205 | # range:
206 | # - post
207 | # - page
208 | # per_page: 20
209 | # limit: 10000
210 |
211 | # Custom search
212 | # adapter:
213 | # # Used for pagination
214 | # per_page: 10
215 | # # Optional
216 | # logo: //cdn.worldvectorlogo.com/logos/algolia.svg
217 | # request:
218 | # url: https://{APPLICATION_ID}-dsn.algolia.net/1/indexes/{INDEX}/query
219 | # method: post
220 | # # Available variables: :query, :per_page, :current
221 | # body: '{"query":":query","hitsPerPage":":per_page","page":":current"}'
222 | # headers:
223 | # X-Algolia-API-Key: {API_KEY}
224 | # X-Algolia-Application-Id: {APPLICATION_ID}
225 | # Content-Type: application/json; charset=UTF-8
226 | # keys:
227 | # # Used to retrieve result list
228 | # data: hits
229 | # # Used to retrieve current page number
230 | # current: page
231 | # # Used to retrieve total page number
232 | # total: nbPage
233 | # # Used to retrieve total hits number
234 | # hits: nbHits
235 | # # Used to retrieve cost time
236 | # time: processingTimeMS
237 | # # Used to retrieve title from per hit
238 | # title: _snippetResult.title.value
239 | # # Used to retrieve content from per hit
240 | # content: _snippetResult.content.value
241 |
242 |
243 | # ---------------------------------------------------------------
244 | # Content addons
245 | # ---------------------------------------------------------------
246 |
247 | # Prefix/Suffix of image URL, useful for CDN, for example,
248 | # `` will convert to
249 | #
250 | assets:
251 | # prefix: 'https://cdn.example.com'
252 | # suffix: '?m=webp&q=80'
253 |
254 | # Table of content, enabled by default, set to `false` to disable.
255 | toc:
256 | # The depth of toc, default to `3`, maximum to `4`.
257 | # depth: 3
258 |
259 | # Showing index before title, eg. 1.1 title, default to `true`
260 | # index: true
261 |
262 | # Reward
263 | reward:
264 | # Text which shows at the top.
265 | # text: Buy me a cup of coffee ☕.
266 |
267 | # Payment Methods
268 | # qrcode, url and text must be set at least one
269 | # wechat, alipay, paypal and bitcoin has a built-in icon,
270 | # use `name` to apply
271 | # methods:
272 | # - name: paypal
273 | # qrcode: paypal.jpg
274 | # url: https://paypal.me/imelmore
275 | # text: This is a paypal payment link
276 | # color: '#09bb07'
277 |
278 | # Copyright notice
279 | # It is possible to use front matter `copyright` to override this setting.
280 | copyright:
281 | # Display author, default to `true`
282 | # author: true
283 |
284 | # Display link, default to `true`
285 | # link: false
286 |
287 | # Set to `false` to hide
288 | # license: Attribution-NonCommercial-NoDerivatives 4.0 International
289 | # (CC BY-NC-ND 4.0)
291 |
292 | # Display published date, default to `false`
293 | # published: false
294 |
295 | # Display updated date, default to `false`
296 | # updated: false
297 |
298 | # Custom text, override above settings
299 | # custom: my copyright
300 |
301 | # Comments
302 | # Built-in support Disqus (https://disqus.com) and LiveRe (https://livere.com),
303 | # for other comment system, see plugins
304 | comments:
305 | # disqus:
306 | # shortname: disqus_shortname
307 | # # Or you can set disqus script URL directly
308 | # script: //disqus_shortname.disqus.com/embed.js
309 | # # Autoload, default to `true`, set to `false` to display a button
310 | # autoload: false
311 |
312 |
313 | # ---------------------------------------------------------------
314 | # Misc
315 | # ---------------------------------------------------------------
316 |
317 | # URL prefix for theme statics
318 | static_prefix:
319 |
320 | # URL prefix for json data
321 | # If your json file is put in a cdn server,
322 | # set data_prefix as '//cdn.com/path/to/path/your_json_dir' and data_dir as 'your_json_dir'
323 | data_prefix:
324 |
325 | # Folder where json file will be generated to, default to 'api'
326 | data_dir:
327 |
328 | # Favicon, default to `favicon.ico`
329 | favicon:
330 |
331 | # Google analytics
332 | # ga: UA-00000000-0
333 |
334 | # SEO
335 | seo:
336 | # Render structured-data in , default to `false`
337 | # structured_data: true
338 | # Server-side rendering (SSR), default to `false`
339 | # ssr: true
340 |
341 | # PWA
342 | pwa:
343 | # Workbox (https://developers.google.com/web/tools/workbox/) is a JavaScript
344 | # libraries for adding offline support to web apps,
345 | # disabled by default, remove the hash to enable
346 | # workbox:
347 | # # Workbox cdn, uses google by default, below is an example of using alicdn
348 | # cdn: https://g.alicdn.com/kg/workbox/3.3.0/workbox-sw.js
349 | # module_path_prefix: https://g.alicdn.com/kg/workbox/3.3.0/
350 | # # Expire time in hour, 4 by default, set to 0 to be permanent,
351 | # # caches will be forcibly deleted the next time the site is updated.
352 | # expire: 4
353 | # # The worker script name, `sw.js` by default
354 | # name: sw.js
355 | # # Custom rules
356 | # rules:
357 | # - name: jsdelivr
358 | # strategy: staleWhileRevalidate
359 | # regex: https://cdn\\.jsdelivr\\.net
360 | # - name: gtm
361 | # strategy: staleWhileRevalidate
362 | # regex: https://www\\.googletagmanager\\.com\?id=.*
363 | # - name: gravatar
364 | # strategy: staleWhileRevalidate
365 | # regex: https://www\\.gravatar\\.com
366 | # - name: theme
367 | # strategy: staleWhileRevalidate
368 | # regex: /.*\\.(?:js|css|woff2|png|jpg|gif)$
369 | # - name: cdn
370 | # strategy: staleWhileRevalidate
371 | # regex: https://cdn\\.yourdomain\\.com
372 | # - name: json
373 | # strategy: cacheFirst
374 | # regex: your_data_prefix/.*\\.json
375 |
376 | # manifest.json
377 | # manifest:
378 | # short_name:
379 | # name:
380 | # start_url: /
381 | # theme_color: '#2a2b33'
382 | # background_color: '#2a2b33'
383 | # icons:
384 | # - src: icon-194x194.png
385 | # sizes: 194x194 512x512
386 | # type: image/png
387 | # purpose: any
388 | # - src: icon-144x144.png
389 | # sizes: 144x144
390 | # type: image/png
391 | # purpose: any
392 |
393 | # Plugins
394 | plugins:
395 | # Built-in plugins
396 | # - cipher:
397 | # placeholder: Passcode is required
398 | # excerpt: Content encrypted
399 | # - palette:
400 | # col: 5
401 | # theme: [
402 | # '#673ab7',
403 | # '#3f51b5',
404 | # '#2196f3',
405 | # '#009688',
406 | # '#4caf50',
407 | # '#ff9800',
408 | # '#ff5722',
409 | # '#795548',
410 | # '#607D8B',
411 | # '#2a2b33'
412 | # ]
413 |
414 | # Global Inject script/style
415 | # By default script will be injected into , and style will be injected into
416 | # In general, script or style depends on their ext name.
417 | # - xxx.css
418 | # - xxx.jss
419 |
420 | # Dynamic inject html snippet
421 | # - position: # sidebar | post | page | comments | avatar | head_begin | head_end | body_begin | body_end
422 | # HTML code
423 | # template:
424 | # Or a path relative to `hexo.base_dir`
425 | # template: snippets/snippet-1.html
426 |
--------------------------------------------------------------------------------
/languages/en.yml:
--------------------------------------------------------------------------------
1 | menu:
2 | archives: Archives
3 | categories: Categories
4 | tags: Tags
5 |
6 | title:
7 | archives: Archive
8 | categories: Category
9 | tags: Tag
10 | search: Search
11 |
12 | footer:
13 | powered: Powered by %s
14 | theme: Theme
15 |
16 | reward:
17 | paypal: Paypal
18 | wechat: WeChat
19 | alipay: 支
20 | bitcoin: Bitcoin
21 |
22 | comments:
23 | load: Load %s
24 | load_faild: Faild to load %s, click to retry
25 |
26 | post:
27 | copyright:
28 | author: Author
29 | link: Link
30 | license: Copyright
31 | published: Published
32 | updated: Updated
33 | reading_time: "%(words)i words in %(minutes)i min"
34 |
35 | page:
36 | tags:
37 | zero: No tags
38 | one: 1 tag in total
39 | other: "%d tags in total"
40 |
41 | notfound:
42 | direct_failed: Failed to direct to %s
43 | empty: Nothing here.
44 |
45 | search:
46 | placeholder: Search
47 | hits:
48 | zero: No result found for ":query"
49 | one: 1 result found in :time ms
50 | other: ":hits results found in :time ms"
51 |
--------------------------------------------------------------------------------
/languages/ja.yml:
--------------------------------------------------------------------------------
1 | menu:
2 | archives: アーカイブ
3 | categories: カテゴリ
4 | tags: タグ
5 |
6 | title:
7 | archives: アーカイブ
8 | categories: カテゴリ
9 | tags: タグ
10 | search: 検索
11 |
12 | footer:
13 | powered: Powered by %s
14 | theme: テーマ
15 |
16 | reward:
17 | paypal: Paypal
18 | wechat: WeChat
19 | alipay: Alipay
20 | bitcoin: Bitcoin
21 |
22 | comments:
23 | load: "%s をロードする"
24 | load_faild: "%s の読み込みに失敗し、再試行"
25 |
26 | post:
27 | copyright:
28 | author: 著者
29 | link: 記事へのリンク
30 | license: 著作権表示
31 | published: 出版日
32 | updated: 更新日
33 | reading_time: "%(words)i 分で %(minutes)i 語"
34 |
35 | page:
36 | tags:
37 | zero: タグなし
38 | one: 全 1 タグ
39 | other: 全 %d タグ
40 |
41 | notfound:
42 | direct_failed: "%s への誘導に失敗しました。"
43 | empty: ここには何もありません。
44 |
45 | search:
46 | placeholder: 検索
47 | hits:
48 | zero: 「:query」の検索結果はありません
49 | one: ":time ミリ秒で検索結果 1 件"
50 | other: ":time ミリ秒で検索結果 :hits 件"
51 |
--------------------------------------------------------------------------------
/languages/zh-Hans.yml:
--------------------------------------------------------------------------------
1 | menu:
2 | archives: 归档
3 | categories: 分类
4 | tags: 标签
5 |
6 | title:
7 | archives: 归档
8 | categories: 分类
9 | tags: 标签
10 | search: 搜索
11 |
12 | footer:
13 | powered: 由 %s 强力驱动
14 | theme: 主题
15 |
16 | reward:
17 | paypal: Paypal
18 | wechat: 微信
19 | alipay: 支
20 | bitcoin: Bitcoin
21 |
22 | comments:
23 | load: 加载 %s
24 | load_faild: 加载 %s 失败,点击尝试重新加载
25 |
26 | post:
27 | copyright:
28 | author: 本文作者
29 | link: 本文链接
30 | license: 版权声明
31 | published: 发表日期
32 | updated: 更新日期
33 | reading_time: "%(words)i 字约 %(minutes)i 分钟"
34 |
35 | page:
36 | tags:
37 | zero: 暂无标签
38 | one: 目前共计 1 个标签
39 | other: 目前共计 %d 个标签
40 |
41 | notfound:
42 | direct_failed: 无法跳转到 %s
43 | empty: 什么也没有。
44 |
45 | search:
46 | placeholder: 搜索
47 | hits:
48 | zero: 未发现与「:query」相关的内容
49 | one: 1 条相关条目,使用了 :time 毫秒
50 | other: ":hits 条相关条目,使用了 :time 毫秒"
51 |
--------------------------------------------------------------------------------
/languages/zh-Hant.yml:
--------------------------------------------------------------------------------
1 | menu:
2 | archives: 歸檔
3 | categories: 分類
4 | tags: 標簽
5 |
6 | title:
7 | archives: 歸檔
8 | categories: 分類
9 | tags: 標簽
10 | search: 檢索
11 |
12 | footer:
13 | powered: 由 %s 強力驅動
14 | theme: 主題
15 |
16 | reward:
17 | paypal: Paypal
18 | wechat: WeChat
19 | alipay: Alipay
20 | bitcoin: Bitcoin
21 |
22 | comments:
23 | load: 加載 %s
24 | load_faild: 加載 %s 失敗,點擊嘗試重新加載
25 |
26 | post:
27 | copyright:
28 | author: 文章作者
29 | link: 文章連結
30 | license: 版權聲明
31 | published: 發表日期
32 | updated: 更新日期
33 | reading_time: "%(words)i 字約 %(minutes)i 分鍾"
34 |
35 | page:
36 | tags:
37 | zero: 暫無標籤
38 | one: 目前共有 1 個標籤
39 | other: 目前共有 %d 個標籤
40 |
41 | notfound:
42 | direct_failed: 無法跳轉到 %s
43 | empty: 什麼也沒有。
44 |
45 | search:
46 | placeholder: 檢索
47 | hits:
48 | zero: 未發現與「:query」相關的内容
49 | one: 1 條相關條目,使用了 :time 毫秒
50 | other: ":hits 條相關條目,使用了 :time 毫秒"
51 |
--------------------------------------------------------------------------------
/layout/index.njk:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | {{ title }}
6 |
7 |
8 |
9 |
10 | {% if theme.pwa.manifest -%}
11 |
12 |
13 |
14 | {%- endif %}
15 | {{ open_graph({ image: page.thumbnail if page.thumbnail else theme.profile.avatar }) }}
16 | {{ structured_data(page) if theme.seo.structured_data -}}
17 | {% if config.feed and config.feed.path -%}
18 |
19 | {%- endif %}
20 | {{ ga(theme.ga) if theme.ga }}
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/lib/config.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs');
2 | const path = require('path');
3 | const utils = require('./utils');
4 | const pkg = require('../package.json');
5 | const configSchema = require('./configSchema.json');
6 | const manifest = require('../source/_manifest.json');
7 | const { css } = require(`../source/${manifest.theme}`);
8 |
9 | module.exports = function (hexo) {
10 | const gravatar = hexo.extend.helper.get('gravatar');
11 | const date = hexo.extend.helper.get('date');
12 | const urlFor = hexo.extend.helper.get('url_for');
13 | const js = hexo.extend.helper.get('js').bind(hexo);
14 | const pluginManifest = loadManifest([path.join(hexo.theme_dir, 'lib/plugins')]);
15 |
16 | hexo.on('generateBefore', function () {
17 | const site = hexo.config;
18 | const theme = Object.assign(hexo.theme.config || {}, site.theme_config);
19 | const email = (theme.profile && theme.profile.email) || site.email || '';
20 | const feed = site.feed ? urlFor.call(hexo, site.feed.path) : '';
21 | const result = utils.parseConfig(configSchema, theme, {
22 | $email: email,
23 | $feed: feed,
24 | $copyright: `© ${new Date().getFullYear()} • ${site.author}`,
25 | $gravatar: gravatar(email, 160),
26 | $title: site.title,
27 | $description: site.description,
28 | });
29 |
30 | // override default language
31 | site.language = utils.localeId(site.language);
32 |
33 | const __ = hexo.theme.i18n.__(site.language);
34 |
35 | if (!result.data_prefix) result.data_prefix = result.data_dir;
36 |
37 | // convert menu to array
38 | if (result.menu) {
39 | result.menu = Object.keys(result.menu).map((k) => {
40 | const item = [k, result.menu[k]];
41 | if (utils.isExternal(item[1])) item.push(1);
42 | return item;
43 | });
44 | }
45 |
46 | // sns
47 | if (result.sns) {
48 | const sns = Array.isArray(result.sns)
49 | ? result.sns
50 | : (() => {
51 | const ret = [];
52 | // keep key order
53 | for (let key in result.sns) {
54 | ret.push({
55 | icon: utils.escapeIdentifier(key),
56 | title: key,
57 | url: result.sns[key],
58 | });
59 | }
60 | return ret;
61 | })();
62 |
63 | result.sns = sns.reduce((ret, i) => {
64 | if (i.icon) {
65 | if (i.icon === 'email') {
66 | i.url = `mailto:${i.url || email}`;
67 | } else if (i.icon === 'feed') {
68 | i.url = i.url || feed;
69 | }
70 | if (i.url) {
71 | i.template = ``;
72 | }
73 | }
74 |
75 | if (i.template) {
76 | ret.push([i.title || '', i.url || '', i.template]);
77 | }
78 | return ret;
79 | }, []);
80 | }
81 |
82 | // theme vars
83 | hexo.extend.injector.register('head_end', ``);
84 | // theme.js
85 | hexo.extend.injector.register('head_end', ``);
86 |
87 | // disqus
88 | if (result.comments && result.comments.disqus) {
89 | result.plugins.push(...execPlugin('disqus', result.comments.disqus));
90 | }
91 |
92 | {
93 | const entries = [
94 | // plugins comes first to ensure that their libs is ready when executing dynamic code.
95 | ...result.plugins,
96 | ...manifest.styles,
97 | ...manifest.scripts,
98 | ];
99 | const injectorPoints = ['head_begin', 'head_end', 'body_begin', 'body_end'];
100 | const positionedPlugins = { $t: [] };
101 |
102 | if (result.appearance.font && result.appearance.font.url) {
103 | entries.unshift({
104 | position: 'head_end',
105 | template: ``,
106 | });
107 | }
108 |
109 | process();
110 |
111 | result.plugins = positionedPlugins;
112 |
113 | function process() {
114 | let item = entries.shift();
115 |
116 | if (!item) return;
117 |
118 | if (typeof item === 'string') {
119 | // Direct with url
120 | if (!pluginManifest[item]) {
121 | const ext = path.extname(item);
122 | const src = urlFor.call(hexo, item);
123 | if (ext === '.css') {
124 | item = ``;
125 | } else if (ext === '.js') {
126 | item = ``;
127 | } else return process();
128 |
129 | hexo.extend.injector.register(injectorPoints[ext === '.css' ? 1 : 3], item);
130 | return process();
131 | }
132 |
133 | // Built in plugins without options
134 | item = { [item]: {} };
135 | }
136 |
137 | if (item.position && injectorPoints.includes(item.position)) {
138 | hexo.extend.injector.register(item.position, item.template);
139 | return process();
140 | }
141 |
142 | // Built in plugins
143 | const executed = execPlugin(Object.keys(item)[0], Object.values(item)[0]);
144 | if (executed) {
145 | if (executed.length) entries.unshift(...executed);
146 | return process();
147 | }
148 |
149 | /**
150 | * Positioned plugins, go last
151 | * convert into the following format
152 | * {
153 | * $t: ['0', '1', '2', '3'],
154 | * sidebar: [indexes],
155 | * post: [indexes],
156 | * page: [indexes],
157 | * comments: [indexes]
158 | * }
159 | */
160 | const index = positionedPlugins.$t.length;
161 | positionedPlugins.$t.push(utils.minifyHtml(loadSnippet(item.template)));
162 |
163 | (Array.isArray(item.position) ? item.position : [item.position]).forEach((p) =>
164 | (positionedPlugins[p] || (positionedPlugins[p] = [])).push(index)
165 | );
166 |
167 | process();
168 | }
169 | }
170 |
171 | // override boolean value to html string
172 | if (result.footer.powered)
173 | result.footer.powered = __(
174 | 'footer.powered',
175 | 'Hexo'
176 | );
177 | if (result.footer.theme)
178 | result.footer.theme =
179 | __('footer.theme') +
180 | ' - Inside';
181 |
182 | // root selector
183 | hexo.extend.injector.register('body_begin', `<${manifest.root}>${manifest.root}>`);
184 |
185 | result.runtime = {
186 | styles: manifest.class,
187 | hash: utils.md5(
188 | [...hexo.locals.getters.pages().sort('-date').toArray(), ...hexo.locals.getters.posts().sort('-date').toArray()]
189 | .filter(utils.published)
190 | .map((i) => i.updated.toJSON())
191 | .join('') +
192 | JSON.stringify(result) +
193 | pkg.version,
194 | 6
195 | ),
196 |
197 | // runtime helpers
198 | hasComments: !!(result.comments || (result.plugins && result.plugins.comments)),
199 | hasReward: !!result.reward,
200 | hasToc: !!result.toc,
201 | renderReadingTime: (() => {
202 | const { reading_time } = result.post;
203 | if (!reading_time) return false;
204 |
205 | let htmlToText = null;
206 | try {
207 | htmlToText = require('html-to-text');
208 | } catch {
209 | return false;
210 | }
211 |
212 | const wpm = reading_time.wpm || 150;
213 | const compile = reading_time.text
214 | ? (o) => utils.sprintf(reading_time.text, o)
215 | : (o) => __('post.reading_time', o);
216 |
217 | return (content) => {
218 | const words = utils.countWord(
219 | htmlToText.convert(content, {
220 | ignoreImage: false,
221 | ignoreHref: true,
222 | wordwrap: false,
223 | })
224 | );
225 | return compile({ words, minutes: Math.round(words / wpm) || 1 });
226 | };
227 | })(),
228 | copyright: result.copyright,
229 | dateHelper: date.bind({
230 | page: { lang: utils.localeId(site.language, true) },
231 | config: site,
232 | }),
233 | uriReplacer: (() => {
234 | let assetsFn = (src) => src;
235 | if (result.assets) {
236 | const prefix = result.assets.prefix ? result.assets.prefix + '/' : '';
237 | const suffix = result.assets.suffix || '';
238 | assetsFn = (src) => prefix + `${src}${suffix}`.replace(/\/{2,}/g, '/');
239 | }
240 |
241 | return (src, assetPath) => {
242 | assetPath = assetPath ? assetPath + '/' : '';
243 |
244 | // skip both external and absolute path
245 | return /^(\/\/?|http|data\:image)/.test(src) ? src : assetsFn(`${assetPath}${src}`);
246 | };
247 | })(),
248 | };
249 |
250 | hexo.theme.config = result;
251 |
252 | /**
253 | * @param {string} name built-in plugin name
254 | * @param {*} options plugin options
255 | * @returns {any[]} return a plugin list
256 | */
257 | function execPlugin(name, options) {
258 | if (!pluginManifest[name]) return;
259 |
260 | const plugin = require(pluginManifest[name]);
261 | const res = plugin.exec(hexo, utils.parseConfig(plugin.schema, options), {
262 | md5: utils.md5,
263 | i18n: (template) =>
264 | template.replace(/{{([\.a-zA-Z0-9_\| ]+)}}/g, (_, $1) => {
265 | const [key, t] = $1.split('|').map((i) => i.trim());
266 | const payload = t.split(',');
267 | return __(
268 | key,
269 | t.includes(':')
270 | ? payload.reduce((map, i) => {
271 | const [k, v] = i.split(':');
272 | return {
273 | ...map,
274 | [k.trim()]: v.trim() || true,
275 | };
276 | }, {})
277 | : payload
278 | );
279 | }),
280 | js,
281 | });
282 |
283 | return Array.isArray(res) ? res : [];
284 | }
285 | });
286 |
287 | /**
288 | * @param {string} pathOrCode plugin.template or plugin.code
289 | * @returns {string}
290 | */
291 | function loadSnippet(pathOrCode) {
292 | // simple but enough
293 | if (/[\n\:]/.test(pathOrCode)) return pathOrCode;
294 |
295 | const templateUrl = path.join(hexo.base_dir, pathOrCode);
296 | if (!fs.existsSync(templateUrl)) return pathOrCode;
297 |
298 | return fs.readFileSync(templateUrl, 'utf8');
299 | }
300 | };
301 |
302 | function loadManifest(includePaths = []) {
303 | return Object.assign({}, ...includePaths.map(load));
304 |
305 | function load(dir) {
306 | let map = {};
307 | try {
308 | const manifestPath = path.join(dir, 'manifest.json');
309 | if (fs.existsSync(manifestPath)) {
310 | map = require(manifestPath) || {};
311 | }
312 | } catch {
313 | return map;
314 | }
315 |
316 | return Object.keys(map).reduce((prefixMap, name) => {
317 | return {
318 | ...prefixMap,
319 | [name]: path.join(dir, map[name]),
320 | };
321 | }, {});
322 | }
323 | }
324 |
--------------------------------------------------------------------------------
/lib/configSchema.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "http://json-schema.org/draft-07/schema",
3 | "definitions": {
4 | "email": { "type": "string", "format": "email", "default": "$email" },
5 | "per_page": { "type": "number", "minimum": 0, "default": 10 },
6 | "color": { "type": "string", "pattern": "^#(?:[0-9a-fA-F]{3}){1,2}$", "default": "#2a2b33" },
7 | "request": {
8 | "type": "object",
9 | "properties": {
10 | "url": { "type": "string", "format": "uri", "minLength": 1 },
11 | "method": { "enum": ["post", "get", "POST", "GET"] },
12 | "body": { "type": "string" },
13 | "headers": {
14 | "type": "object",
15 | "additionalProperties": { "type": "string" }
16 | }
17 | },
18 | "additionalProperties": false,
19 | "required": ["url"]
20 | },
21 | "highlight": {
22 | "type": "array",
23 | "items": { "$ref": "#/definitions/color" },
24 | "minItems": 15
25 | }
26 | },
27 | "type": "object",
28 | "properties": {
29 | "appearance": {
30 | "type": "object",
31 | "properties": {
32 | "accent_color": { "$ref": "#/definitions/color" },
33 | "foreground_color": { "$ref": "#/definitions/color" },
34 | "border_color": { "$ref": "#/definitions/color" },
35 | "background": { "type": "string" },
36 | "sidebar_background": { "type": "string" },
37 | "card_background": { "type": "string" },
38 | "highlight": { "$ref": "#/definitions/highlight" },
39 | "content_width": {
40 | "oneOf": [
41 | { "type": "number" },
42 | { "type": "string" }
43 | ]
44 | },
45 | "font": {
46 | "type": "object",
47 | "properties": {
48 | "url": { "type": "string", "format": "uri" },
49 | "logo": { "type": "string" },
50 | "menu": { "type": "string" },
51 | "label": { "type": "string" },
52 | "heading": { "type": "string" },
53 | "code": { "type": "string" },
54 | "base": { "type": "string" },
55 | "print": { "type": "string" }
56 | },
57 | "additionalProperties": false
58 | },
59 | "darkmode": {
60 | "type": "object",
61 | "properties": {
62 | "accent_color": { "$ref": "#/definitions/color" },
63 | "foreground_color": { "$ref": "#/definitions/color" },
64 | "border_color": { "$ref": "#/definitions/color" },
65 | "background": { "type": "string" },
66 | "sidebar_background": { "type": "string" },
67 | "card_background": { "type": "string" },
68 | "highlight": { "$ref": "#/definitions/highlight" }
69 | },
70 | "additionalProperties": false
71 | }
72 | },
73 | "required": ["accent_color"],
74 | "additionalProperties": false
75 | },
76 | "profile": {
77 | "type": "object",
78 | "properties": {
79 | "email": { "$ref": "#/definitions/email" },
80 | "avatar": { "type": "string", "format": "uri", "default": "$gravatar" },
81 | "bio": { "type": "string" }
82 | },
83 | "additionalProperties": false,
84 | "required": ["email", "avatar"]
85 | },
86 | "menu": { "type": "object", "additionalProperties": { "type": "string" } },
87 | "sns": {
88 | "oneOf": [
89 | {
90 | "type": "object",
91 | "properties": {
92 | "email": {
93 | "oneOf": [
94 | { "$ref": "#/definitions/email" },
95 | { "type": "null" }
96 | ]
97 | },
98 | "feed": {
99 | "oneOf": [
100 | { "type": "string", "format": "uri", "default": "$feed" },
101 | { "type": "null" }
102 | ]
103 | }
104 | },
105 | "patternProperties": {
106 | "^(github|twitter|facebook|instagram|tumblr|dribbble|telegram|youtube|hangouts|linkedin|pinterest|soundcloud|myspace|weibo|qq)$": { "type": "string" }
107 | }
108 | },
109 | {
110 | "type": "array",
111 | "items": {
112 | "type": "object",
113 | "properties": {
114 | "icon": { "type": "string" },
115 | "title": { "type": "string" },
116 | "url": { "type": "string" },
117 | "template": { "type": "string" }
118 | },
119 | "additionalProperties": false
120 | }
121 | }
122 | ]
123 | },
124 | "footer": {
125 | "type": "object",
126 | "properties": {
127 | "copyright": {
128 | "oneOf":[
129 | { "type": "string", "contentMediaType": "text/html" },
130 | { "enum": [false] }
131 | ],
132 | "default": "$copyright"
133 | },
134 | "powered": { "type": "boolean", "default": true },
135 | "theme": { "type": "boolean", "default": true },
136 | "custom": { "type": "string" }
137 | },
138 | "required": ["copyright", "powered", "theme"],
139 | "additionalProperties": false
140 | },
141 | "assets": {
142 | "type": "object",
143 | "properties": {
144 | "prefix": { "type": "string" },
145 | "suffix": { "type": "string" }
146 | },
147 | "additionalProperties": false
148 | },
149 | "toc": {
150 | "type": "object",
151 | "properties": {
152 | "depth": { "type": "number", "minimum": 1, "maximum": 4, "default": 3 },
153 | "index": { "type": "boolean", "default": true }
154 | },
155 | "required": ["depth", "index"],
156 | "additionalProperties": false
157 | },
158 | "reward": {
159 | "type": "object",
160 | "properties": {
161 | "text": { "type": "string", "default": "Buy me a cup of coffee ☕." },
162 | "methods": {
163 | "type": "array",
164 | "items": {
165 | "type": "object",
166 | "properties": {
167 | "name": { "type": "string" },
168 | "text": { "type": "string" },
169 | "qrcode": { "type": "string" },
170 | "url": { "type": "string" },
171 | "color": { "$ref": "#/definitions/color" }
172 | },
173 | "required": ["name"],
174 | "minProperties": 2,
175 | "additionalProperties": false
176 | }
177 | }
178 | },
179 | "required": ["methods"],
180 | "additionalProperties": false
181 | },
182 | "copyright": {
183 | "type": "object",
184 | "properties": {
185 | "author": { "type": "boolean", "default": true },
186 | "link": { "type": "boolean", "default": true },
187 | "license": {
188 | "oneOf": [
189 | { "type": "string", "contentMediaType": "text/html" },
190 | { "enum": [false] }
191 | ],
192 | "default": "Attribution-NonCommercial-NoDerivatives 4.0 International (CC BY-NC-ND 4.0)"
193 | },
194 | "published": { "type": "boolean" },
195 | "updated": { "type": "boolean" },
196 | "custom": { "type": "string" }
197 | },
198 | "required": ["author", "link", "license"],
199 | "additionalProperties": false
200 | },
201 | "comments": {
202 | "oneOf":[
203 | {
204 | "type": "object",
205 | "properties": {
206 | "disqus": {
207 | "type": "object",
208 | "properties": {
209 | "shortname": { "type": "string" },
210 | "script": { "type": "string", "format": "uri" },
211 | "autoload": { "type": "boolean", "default": true }
212 | },
213 | "required": ["shortname", "autoload"],
214 | "additionalProperties": false
215 | }
216 | },
217 | "additionalProperties": false
218 | },
219 | {
220 | "type": "object",
221 | "properties": {
222 | "livere": {
223 | "type": "object",
224 | "properties": {
225 | "uid": { "type": "string" },
226 | "script": { "type": "string", "format": "uri", "default": "https://cdn-city.livere.com/js/embed.dist.js" },
227 | "autoload": { "type": "boolean", "default": true }
228 | },
229 | "required": ["uid", "script", "autoload"],
230 | "additionalProperties": false
231 | }
232 | },
233 | "additionalProperties": false
234 | }
235 | ]
236 | },
237 | "page": {
238 | "type": "object",
239 | "properties": {
240 | "toc": { "type": "boolean", "default": true },
241 | "reward": { "type": "boolean" },
242 | "copyright": { "type": "boolean" }
243 | },
244 | "required": ["toc"],
245 | "additionalProperties": false
246 | },
247 | "post": {
248 | "type": "object",
249 | "properties": {
250 | "per_page": { "$ref": "#/definitions/per_page" },
251 | "toc": { "type": "boolean", "default": true },
252 | "reward": { "type": "boolean" },
253 | "copyright": { "type": "boolean" },
254 | "reading_time": {
255 | "oneOf": [
256 | { "type": "boolean" },
257 | {
258 | "type": "object",
259 | "properties": {
260 | "wpm": { "type": "number", "default": 150 },
261 | "text": { "type": "string" }
262 | },
263 | "additionalProperties": false
264 | }
265 | ],
266 | "default": true
267 | }
268 | },
269 | "required": ["per_page", "toc", "reading_time"],
270 | "additionalProperties": false
271 | },
272 | "archive": {
273 | "type": "object",
274 | "properties": {
275 | "per_page": { "$ref": "#/definitions/per_page" }
276 | },
277 | "required": ["per_page"],
278 | "additionalProperties": false
279 | },
280 | "tag": {
281 | "type": "object",
282 | "properties": {
283 | "per_page": { "$ref": "#/definitions/per_page" }
284 | },
285 | "required": ["per_page"],
286 | "additionalProperties": false
287 | },
288 | "category": {
289 | "type": "object",
290 | "properties": {
291 | "per_page": { "$ref": "#/definitions/per_page" }
292 | },
293 | "required": ["per_page"],
294 | "additionalProperties": false
295 | },
296 | "search": {
297 | "type": "object",
298 | "properties": {
299 | "fab": { "enum": [true] },
300 | "page": { "enum": [true] },
301 | "adapter": {
302 | "oneOf": [
303 | {
304 | "type": "object",
305 | "properties": {
306 | "range": { "type": "array", "items": { "enum": ["post", "page"] } },
307 | "per_page": { "$ref": "#/definitions/per_page" },
308 | "limit": { "type": "number", "default": 10000 }
309 | },
310 | "required": ["range", "per_page", "limit"],
311 | "additionalProperties": false
312 | },
313 | {
314 | "type": "object",
315 | "properties": {
316 | "per_page": { "$ref": "#/definitions/per_page" },
317 | "logo": { "type": "string" },
318 | "request": { "$ref": "#/definitions/request" },
319 | "keys": {
320 | "type": "object",
321 | "properties": {
322 | "data": { "type": "string" },
323 | "current": { "type": "string" },
324 | "total": { "type": "string" },
325 | "hits": { "type": "string" },
326 | "time": { "type": "string" },
327 | "title": { "type": "string" },
328 | "content": { "type": "string" }
329 | },
330 | "required": ["data", "current"],
331 | "additionalProperties": false
332 | }
333 | },
334 | "required": ["per_page", "request", "keys"],
335 | "additionalProperties": false
336 | }
337 | ]
338 | }
339 | },
340 | "required": ["adapter"],
341 | "minProperties": 2,
342 | "additionalProperties": false
343 | },
344 | "static_prefix": { "type": "string" },
345 | "data_prefix": { "type": "string" },
346 | "data_dir": { "type": "string", "default": "api" },
347 | "favicon": { "type": "string", "default": "favicon.ico" },
348 | "ga": { "type": "string" },
349 | "seo": {
350 | "type": "object",
351 | "properties": {
352 | "structured_data": { "type": "boolean" },
353 | "ssr": { "type": "boolean" }
354 | },
355 | "additionalProperties": false,
356 | "default": {}
357 | },
358 | "plugins": {
359 | "definitions": {
360 | "position": {
361 | "enum": [
362 | "sidebar", "avatar", "post", "page", "comments", "head_begin", "head_end", "body_begin", "body_end"
363 | ]
364 | }
365 | },
366 | "type": "array",
367 | "items": {
368 | "oneOf": [
369 | { "type": "string" },
370 | {
371 | "type": "object",
372 | "properties": {
373 | "template": { "type": "string" },
374 | "position": {
375 | "oneOf": [
376 | { "$ref": "#/properties/plugins/definitions/position" },
377 | { "type": "array", "items": { "$ref": "#/properties/plugins/definitions/position" } }
378 | ]
379 | }
380 | },
381 | "required": ["template", "position"],
382 | "additionalProperties": false
383 | },
384 | {
385 | "type": "object",
386 | "additionalProperties": true
387 | }
388 | ]
389 | },
390 | "default": []
391 | },
392 | "pwa": {
393 | "type": "object",
394 | "properties": {
395 | "workbox": {
396 | "definitions": {
397 | "expire": { "type": "number", "minimum": 0, "default": 4 }
398 | },
399 | "type": "object",
400 | "properties": {
401 | "cdn": { "type": "string", "format": "uri", "default": "https://storage.googleapis.com/workbox-cdn/releases/3.6.3/workbox-sw.js" },
402 | "module_path_prefix": { "type": "string" },
403 | "expire": { "$ref": "#/properties/pwa/properties/workbox/definitions/expire" },
404 | "name": { "type": "string", "pattern": "^\\w*\\.js$", "default": "sw.js" },
405 | "rules": {
406 | "type": "array",
407 | "items": {
408 | "type": "object",
409 | "properties": {
410 | "name": { "type": "string" },
411 | "strategy": { "enum": ["networkOnly", "cacheFirst", "cacheOnly", "staleWhileRevalidate"] },
412 | "regex": { "type": "string", "format": "regex" },
413 | "expire": { "$ref": "#/properties/pwa/properties/workbox/definitions/expire" }
414 | },
415 | "required": ["name", "strategy", "regex"],
416 | "additionalProperties": false
417 | }
418 | }
419 | },
420 | "required": ["cdn", "expire", "name", "rules"],
421 | "additionalProperties": false
422 | },
423 | "manifest": {
424 | "type": "object",
425 | "properties": {
426 | "name": { "type": "string", "default": "$title" },
427 | "short_name": { "type": "string", "default": "$title" },
428 | "description": { "type": "string", "default": "$description" },
429 | "start_url": { "type": "string", "format": "uri", "default": "." },
430 | "theme_color": { "$ref": "#/definitions/color" },
431 | "background_color": { "$ref": "#/definitions/color" },
432 | "icons": {
433 | "type": "array",
434 | "items": {
435 | "type": "object",
436 | "properties": {
437 | "src": { "type": "string" },
438 | "sizes": { "type": "string" },
439 | "type": { "type": "string" },
440 | "purpose": { "type": "string" }
441 | },
442 | "required": ["src", "sizes", "type"],
443 | "additionalProperties": false
444 | }
445 | },
446 | "display": { "enum": ["minimal-ui", "fullscreen", "standalone", "browser"], "default": "minimal-ui" }
447 | },
448 | "required": ["name", "short_name", "description", "start_url", "theme_color", "background_color", "icons", "display"],
449 | "additionalProperties": false
450 | }
451 | },
452 | "additionalProperties": false,
453 | "default": {}
454 | },
455 | "markdown": {
456 | "type": "object",
457 | "properties": {
458 | "html": { "type": "boolean" },
459 | "xhtmlOut": { "type": "boolean" },
460 | "breaks": { "type": "boolean" },
461 | "linkify": { "type": "boolean" },
462 | "typographer": { "type": "boolean" },
463 | "quotes": { "type": "string" },
464 | "plugins": {
465 | "type": "array",
466 | "items": {
467 | "oneOf": [
468 | { "type": "string" },
469 | { "type": "object" }
470 | ]
471 | }
472 | }
473 | },
474 | "additionalProperties": false,
475 | "default": {}
476 | }
477 | },
478 | "required": ["appearance", "profile", "footer", "toc", "page", "post", "archive", "tag", "category", "data_dir", "favicon", "pwa", "seo", "markdown", "plugins"],
479 | "additionalProperties": false
480 | }
481 |
--------------------------------------------------------------------------------
/lib/filter/index.js:
--------------------------------------------------------------------------------
1 | module.exports = function (hexo) {
2 | hexo.extend.filter.register('after_post_render', require('./post'));
3 | hexo.extend.filter.register('template_locals', require('./templates'));
4 | hexo.extend.filter.register('before_exit', require('./ssr'));
5 | };
6 |
--------------------------------------------------------------------------------
/lib/filter/post.js:
--------------------------------------------------------------------------------
1 | const { parseToc, isObject, isEmptyObject, trimHtml } = require('../utils');
2 | const date_formats = [
3 | 'll', // Sep 4, 1986
4 | 'L', // 09/04/1986
5 | 'MM-DD' // 06-17
6 | ];
7 |
8 | module.exports = function (data) {
9 | if (data.layout !== 'page' && data.layout !== 'post') return;
10 |
11 | const { config, theme: { config: theme } } = this;
12 | const { hasComments, hasReward, hasToc, copyright, dateHelper, uriReplacer, renderReadingTime } = theme.runtime;
13 | const isPage = data.layout === 'page';
14 |
15 | // pre format date for i18n
16 | data.date_formatted = date_formats.reduce((ret, format) => {
17 | ret[format] = dateHelper(data.date, format)
18 | return ret
19 | }, {})
20 |
21 | // relative link
22 | data.link = trimHtml(data.path).replace(/^\//, '');
23 | // permalink link
24 | data.plink = `${config.url}/${data.link ? `${data.link}/` : ''}`;
25 | // type
26 | data.type = isPage ? 'page' : 'post';
27 |
28 | // comments
29 | data.comments = hasComments && data.comments !== false;
30 |
31 | // asset path (for post_asset_folder)
32 | const assetPath = config.post_asset_folder
33 | ? (isPage ? trimHtml(data.path, true) : data.link)
34 | : undefined;
35 |
36 | // Make sure articles without titles are also accessible
37 | if (!data.title) data.title = data.slug
38 |
39 | // post thumbnail
40 | if (!isPage && data.thumbnail) {
41 | const particals = data.thumbnail.split(' ');
42 | data.thumbnail = uriReplacer(particals[0], assetPath);
43 | if (particals[1] && !data.color)
44 | data.color = particals[1];
45 | }
46 |
47 | // reward
48 | if (hasReward && theme[data.type].reward && data.reward !== false) data.reward = true;
49 |
50 | // copyright
51 | let cr;
52 | if (
53 | (copyright && theme[data.type].copyright && data.copyright === undefined) ||
54 | (copyright && data.copyright === true)
55 | ) {
56 | cr = Object.assign({}, copyright);
57 | }
58 | // override page/post.copyright with front matter
59 | else if (isObject(data.copyright)) {
60 | cr = Object.assign({}, data.copyright);
61 | }
62 | if (cr) {
63 | if (cr.custom) cr = { custom: cr.custom };
64 | else {
65 | if (cr.author) cr.author = data.author || config.author;
66 | else delete cr.author;
67 | if (cr.link) cr.link = `${data.plink}`;
68 | else delete cr.link;
69 | if (cr.published) cr.published = dateHelper(data.date, 'LL');
70 | else delete cr.published;
71 | if (cr.updated) cr.updated = dateHelper(data.updated, 'LL');
72 | else delete cr.updated;
73 | if (!cr.license) delete cr.license;
74 | }
75 |
76 | if (!isEmptyObject(cr)) data.copyright = cr;
77 | else delete data.copyright;
78 | } else delete data.copyright;
79 |
80 | // toc
81 | if (hasToc && theme[data.type].toc && data.toc !== false) {
82 | const toc = parseToc(data.content, theme.toc.depth);
83 | if (toc.length) data.toc = toc;
84 | else delete data.toc;
85 | } else delete data.toc;
86 |
87 | // reading time
88 | if (renderReadingTime) {
89 | data.reading_time = renderReadingTime(data.content);
90 | }
91 | };
92 |
--------------------------------------------------------------------------------
/lib/filter/ssr.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs');
2 | const path = require('path');
3 | const { magenta } = require('chalk');
4 | const { trimHtml, asyncMap } = require('../utils');
5 |
6 | module.exports = function () {
7 | const hexo = this;
8 | const { theme: { config: theme } } = this;
9 |
10 | if (!theme.seo || !theme.seo.ssr || !~['g', 'generate'].indexOf(hexo.env.cmd)) return;
11 |
12 | const render = require(path.join(hexo.theme_dir, 'source/_ssr.js')).Renderer();
13 | const { generatedConfig, generatedRoutes } = hexo.theme.config.runtime;
14 | const root = hexo.config.root.replace(/^[\\|\/]+|[\\|\/]+$/g, '');
15 |
16 | asyncMap(generatedRoutes, (route) => {
17 | const documentUrl = path.join(hexo.public_dir, route);
18 |
19 | return render({
20 | url: '/' + trimHtml(route),
21 | document: fs.readFileSync(documentUrl, 'utf8'),
22 | resolve: {
23 | config: {
24 | ...generatedConfig,
25 | ssr: true
26 | },
27 | data(url) {
28 | url = path.join(hexo.public_dir, url.replace(new RegExp('^[\\|/]+' + root), ''));
29 |
30 | return require(url);
31 | },
32 | },
33 | }).then(html => {
34 | fs.writeFileSync(documentUrl, html);
35 | hexo.log.info('SSR: %s', magenta(route));
36 | });
37 | });
38 | };
39 |
--------------------------------------------------------------------------------
/lib/filter/templates.js:
--------------------------------------------------------------------------------
1 | let locale, configTitle, titleFn;
2 |
3 | module.exports = function (locals) {
4 | if (titleFn === undefined) {
5 | locale = this.theme.i18n.get(locals.config.language);
6 | configTitle = locals.config.title;
7 | titleFn = {
8 | archives: p => locale['title.archives'],
9 | categories: p => locale['title.categories'] + (p.name ? ` : ${p.name}` : ''),
10 | tags: p => locale['title.tags'] + (p.name ? ` : ${p.name}` : ''),
11 | page: p => p.title,
12 | posts: p => p.title,
13 | post: p => p.title,
14 | };
15 | }
16 |
17 | const { page } = locals;
18 | const title = titleFn[page.type] ? titleFn[page.type](page) : '';
19 |
20 | locals.title = title ? `${title} - ${configTitle}` : configTitle;
21 |
22 | return locals;
23 | }
24 |
--------------------------------------------------------------------------------
/lib/generator/config.js:
--------------------------------------------------------------------------------
1 | const { pick, md5, parseBackground, flattenObject } = require('../utils');
2 |
3 | module.exports = function (locals) {
4 | const js = this.extend.helper.get('js').bind(this);
5 | const site = this.config,
6 | theme = this.theme.config,
7 | config = Object.assign(
8 | pick(site, ['title', 'author']),
9 | pick(theme, ['profile', 'menu', 'sns', 'footer', 'toc', 'reward', 'plugins', 'data_prefix']),
10 | {
11 | count: {
12 | posts: countOverflow(locals.posts.length),
13 | categories: countOverflow(locals.categories.length),
14 | tags: countOverflow(locals.tags.length)
15 | },
16 | hash: theme.runtime.hash,
17 | locale: this.theme.i18n.get(site.language),
18 | theme: {
19 | default: flattenObject({ ...theme.appearance, darkmode: undefined }),
20 | dark: flattenObject(theme.appearance.darkmode)
21 | }
22 | },
23 | );
24 |
25 | // extra color from background setting
26 | // [sidebar bg, body bg] | [sidebar bg] | body bg
27 | const { accent_color, sidebar_background, background } = theme.appearance
28 | // [color with sidebar open, color with sidebar close]
29 | config.color = [
30 | parseBackground(sidebar_background).color || accent_color]
31 | .concat(parseBackground(background).color || (theme.pwa && theme.pwa.theme_color) || [])
32 |
33 | // post routes
34 | config.routes = {}
35 | config.routes.posts = [...locals.posts
36 | .reduce((set, post) => {
37 | // convert `/path/to/path/` to `:a/:b/:c`
38 | const link = post.link.split('/').filter(i => i)
39 | .map((_, i) => ':' + String.fromCharCode(97 + i))
40 | .join('/')
41 | set.add(link)
42 | return set
43 | }, new Set)].sort()
44 |
45 | // allow post/page can be set as index
46 | config.index = [...locals.posts, ...locals.pages].find(i => i.link === '') ? 'index' : 'page';
47 |
48 | // page routes
49 | if (locals.pages.length)
50 | config.routes.pages = [...locals.pages
51 | .reduce((set, post) => {
52 | if (post.link === undefined) return set;
53 | // convert `/path/to/path/` to `path/:a/:b`
54 | const link = post.link.split('/').filter(i => i)
55 | .map((partial, i) => i === 0 ? partial : ':' + String.fromCharCode(97 + i))
56 | .join('/')
57 | set.add(link)
58 | return set
59 | }, new Set)].sort()
60 |
61 | if (config.count.categories) config.category0 = locals.categories[0].name;
62 |
63 | // search
64 | if (theme.search) {
65 | const adapter = theme.search.adapter;
66 | if (adapter.range) {
67 | config.search = { local: true }
68 | if (adapter.per_page) config.search.per_page = adapter.per_page
69 | } else {
70 | config.search = adapter
71 | }
72 | // Merge search.fab, search.page into adapter
73 | if (theme.search.fab) config.search.fab = true;
74 | if (theme.search.page) config.search.page = true;
75 | }
76 |
77 | // Cache config for ssr
78 | if (theme.seo.ssr) theme.runtime.generatedConfig = config;
79 |
80 | let data = 'window.__inside__=' + JSON.stringify(config);
81 |
82 | if (theme.pwa.workbox)
83 | data += `\n;navigator.serviceWorker && location.protocol === 'https:' && window.addEventListener('load', function() { navigator.serviceWorker.register('${theme.pwa.workbox.name}') })`
84 |
85 | const path = `config.${md5(data)}.js`;
86 | this.extend.injector.register('head_begin', js(path));
87 |
88 | return [{
89 | path,
90 | data
91 | }];
92 | };
93 |
94 | function countOverflow(count) {
95 | return count > 999 ? '999+' : count;
96 | }
97 |
--------------------------------------------------------------------------------
/lib/generator/entries/archives.js:
--------------------------------------------------------------------------------
1 | const { pick, localeId, visible } = require('../../utils');
2 | const { archive: archiveProps } = require('./properties');
3 |
4 | module.exports = function ({ site, theme, locals, helpers }) {
5 | const posts = locals.posts.filter(visible).map(pick(archiveProps));
6 | const config = theme.archive;
7 | const dateHelper = helpers.date.bind({ page: { lang: localeId(site.language, true) }, config })
8 |
9 | if (!posts.length) return [];
10 |
11 | return helpers.pagination.apply(posts, { perPage: config.per_page, id: 'archives' }, [
12 | { type: 'json', dataFn: classify },
13 | { type: 'html', extend: { type: 'archives' } },
14 | ]).flat(Infinity);
15 |
16 | /**
17 | * Classify posts with `year` and `month`
18 | *
19 | * @param {object} data
20 | * @returns {data}
21 | */
22 | function classify(data) {
23 | const posts = data.data;
24 |
25 | if (!posts.length) return [];
26 |
27 | const desc = (a, b) => parseInt(a) < parseInt(b) ? 1 : -1;
28 | const cfyPosts = {};
29 |
30 | posts.forEach(post => {
31 | const date = post.date.clone(),
32 | year = date.year(),
33 | month = date.month() + 1;
34 |
35 | if (cfyPosts[year]) {
36 | if (cfyPosts[year][month]) cfyPosts[year][month].push(post)
37 | else cfyPosts[year][month] = [post]
38 | } else {
39 | cfyPosts[year] = {
40 | [month]: [post]
41 | }
42 | }
43 |
44 | });
45 |
46 | data.data = Object.keys(cfyPosts).sort(desc).map(year => {
47 | return {
48 | year: year,
49 | months: Object.keys(cfyPosts[year]).sort(desc).map(month => {
50 | return {
51 | month: dateHelper(month, 'MMM'),
52 | entries: cfyPosts[year][month]
53 | }
54 | })
55 | }
56 | })
57 |
58 | return data;
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/lib/generator/entries/categories.js:
--------------------------------------------------------------------------------
1 | const { pick } = require('../../utils');
2 | const { categoryPosts: categoryPostsProps } = require('./properties');
3 |
4 | module.exports = function ({ theme, locals: { categories }, helpers }) {
5 | const config = theme.category;
6 |
7 | if (!categories.length) return [];
8 |
9 | return [
10 | helpers.generateJson({
11 | path: 'categories',
12 | data: categories.map(i => ({ name: i.name, count: i.posts.length }))
13 | }),
14 | helpers.generateHtml({
15 | path: 'categories',
16 | data: { type: 'categories' }
17 | }),
18 | categories.map(category =>
19 | helpers.pagination.apply(
20 | category.posts.map(pick(categoryPostsProps)),
21 | { perPage: config.per_page, id: `categories/${category.name}`, extend: { name: category.name } },
22 | [
23 | { type: 'json' },
24 | { type: 'html', extend: { type: 'categories' } },
25 | ]
26 | )
27 | )
28 | ].flat(Infinity);
29 | };
30 |
--------------------------------------------------------------------------------
/lib/generator/entries/index.js:
--------------------------------------------------------------------------------
1 | const { base64, Pagination } = require('../../utils');
2 |
3 | module.exports = function (locals) {
4 | const theme = this.theme.config;
5 | const generationMeta = {
6 | html: { generateFn: ({ path, data }) => ({ path: path + '/index.html', data, layout: 'index' }) },
7 | json: { generateFn: ({ path, data }) => ({ path: theme.data_dir + '/' + base64(path) + '.json', data: JSON.stringify(data) }) },
8 | };
9 | const pagination = new Pagination(generationMeta);
10 | const ret = [].concat.apply([], ['pages', 'posts', 'tags', 'categories', 'archives', 'search']
11 | .map(item => require('./' + item)({
12 | site: this.config, theme, locals,
13 | helpers: {
14 | generateJson: (...args) => args.map(generationMeta.json.generateFn),
15 | generateHtml: (...args) => args.map(generationMeta.html.generateFn),
16 | pagination,
17 | date: this.extend.helper.get('date'),
18 | },
19 | })));
20 |
21 | // Cache page routes for ssr
22 | if (theme.seo.ssr)
23 | theme.runtime.generatedRoutes = ret.filter(i => i?.layout === 'index').map(i => i.path);
24 |
25 | return ret;
26 | };
27 |
--------------------------------------------------------------------------------
/lib/generator/entries/pages.js:
--------------------------------------------------------------------------------
1 | const { pick } = require('../../utils');
2 | const { page: pageProps } = require('./properties');
3 |
4 | module.exports = function ({ locals: { pages }, helpers }) {
5 | return [
6 | pages.map(page => [
7 | helpers.generateJson({
8 | path: page.link || 'index',
9 | data: pick(page, pageProps)
10 | }),
11 | helpers.generateHtml({
12 | path: page.link,
13 | data: page
14 | })
15 | ]),
16 |
17 | helpers.generateHtml({
18 | path: '404',
19 | data: { type: 'pages', title: '404' }
20 | })
21 | ].flat(Infinity);
22 | }
23 |
--------------------------------------------------------------------------------
/lib/generator/entries/posts.js:
--------------------------------------------------------------------------------
1 | const { pick, visible } = require('../../utils');
2 | const { post: postProps, postList: postListProps } = require('./properties');
3 |
4 | module.exports = function ({ theme, locals: { posts, indexRouteDetected }, helpers }) {
5 | const len = posts.length;
6 | const config = theme.post;
7 | const getPageId = indexRouteDetected
8 | ? index => `page/${index}`
9 | : index => index === 1 ? '' : `page/${index}`;
10 |
11 | return [
12 | posts.map((post, i) => {
13 | if (i) post.prev = posts[i - 1];
14 | if (i < len - 1) post.next = posts[i + 1];
15 |
16 | return [
17 | helpers.generateJson({
18 | path: post.link || 'index',
19 | data: pick(post, postProps)
20 | }),
21 | helpers.generateHtml({
22 | path: `/${post.link}`,
23 | data: post
24 | })
25 | ];
26 | }),
27 |
28 | helpers.pagination.apply(posts.filter(visible).sort((a, b) => {
29 | const ai = typeof a.sticky === 'number' ? a.sticky : 0;
30 | const bi = typeof b.sticky === 'number' ? b.sticky : 0;
31 |
32 | if (ai !== bi) return bi - ai;
33 | if (a.date === b.date) return 0;
34 | return a.date > b.date ? -1 : 1;
35 | }).map(pick(postListProps)), { perPage: config.per_page }, [
36 | { type: 'json', id: 'page' },
37 | { type: 'html', id: getPageId, extend: { type: 'posts' } },
38 | ])
39 | ].flat(Infinity);
40 | }
41 |
--------------------------------------------------------------------------------
/lib/generator/entries/properties.js:
--------------------------------------------------------------------------------
1 | const listProps = ['title', 'date', 'date_formatted', 'link'];
2 |
3 | module.exports = {
4 | archive: listProps,
5 | categoryPosts: listProps,
6 | tagPosts: listProps,
7 | page: ['title', 'date', 'date_formatted', 'updated', 'content', 'link', 'comments', 'dropcap', 'plink', 'toc', 'reward', 'copyright', 'meta'],
8 | post: ['title', 'date', 'date_formatted', 'author', 'thumbnail', 'color', 'link', 'comments', 'dropcap', 'tags', 'categories', 'updated', 'content', 'prev', 'next', 'plink', 'toc', 'reward', 'copyright', 'reading_time'],
9 | postList: ['title', 'date', 'date_formatted', 'author', 'thumbnail', 'color', 'excerpt', 'link', 'tags', 'categories'],
10 | search: ['title', 'date', 'date_formatted', 'author', 'updated', 'content', 'thumbnail', 'color', 'plink']
11 | }
12 |
--------------------------------------------------------------------------------
/lib/generator/entries/search.js:
--------------------------------------------------------------------------------
1 | const { pick } = require('../../utils');
2 | const { search: searchProps } = require('./properties');
3 | const { stripHTML } = require('hexo-util');
4 |
5 | module.exports = function ({ theme, locals, helpers }) {
6 | const config = theme.search;
7 |
8 | if (!config) return
9 |
10 | return [
11 | config.page ? helpers.generateHtml({
12 | path: 'search',
13 | data: { type: 'search' }
14 | }) : [],
15 |
16 | config.adapter.range ? helpers.generateJson({
17 | path: 'search',
18 | data: config.adapter.range.map(key => locals[key + 's'])
19 | .flat()
20 | .slice(0, config.limit)
21 | .map(post => {
22 | const ret = pick(post, searchProps)
23 | ret.content = stripHTML(
24 | ret.content
25 | .replace(/');
168 | expect(htmlTag('link', { href: 'xxx.css' })).toBe('');
169 | });
170 |
171 | it('parseBackground()', function () {
172 | expect(parseBackground('')).toEqual({})
173 | expect(parseBackground('#fff xxx.jpg xx')).toEqual({ color: '#fff', image: 'xxx.jpg xx' })
174 | expect(parseBackground('xx xxx.jpg #fff')).toEqual({ color: '#fff', image: 'xx xxx.jpg' })
175 | expect(parseBackground('#fff')).toEqual({ color: '#fff', image: '' })
176 | expect(parseBackground('xxx.jpg')).toEqual({ image: 'xxx.jpg' })
177 | })
178 |
179 | it('trimHtml()', function () {
180 | expect(trimHtml('post/a/b/')).toBe('post/a/b')
181 | expect(trimHtml('post/a/b.html')).toBe('post/a/b')
182 | expect(trimHtml('post/a/index.html')).toBe('post/a')
183 | expect(trimHtml('post/a/index.html', true)).toBe('post/a/index')
184 | })
185 |
186 | it('parsePipe()', function () {
187 | expect(parsePipe('a|b|c:1|d:2')).toEqual({ value: 'a', options: { b: true, c: '1', d: '2' } });
188 | expect(parsePipe('|b|c:1|d:2')).toEqual({ options: { b: true, c: '1', d: '2' } });
189 | expect(parsePipe(' a | b | c : 1 | d : 2 '))
190 | .toEqual({ value: 'a', options: { b: true, c: '1', d: '2' } });
191 | })
192 | });
193 |
--------------------------------------------------------------------------------