├── .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 | # `![cat](images/cat.gif)` will convert to 249 | # cat 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}>`); 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(//g, '') 26 | .replace(//g, '') 27 | ) 28 | 29 | return ret 30 | }) 31 | }) : [], 32 | ].flat(Infinity); 33 | }; 34 | -------------------------------------------------------------------------------- /lib/generator/entries/tags.js: -------------------------------------------------------------------------------- 1 | const { pick } = require('../../utils'); 2 | const { tagPosts: tagPostsProps } = require('./properties'); 3 | 4 | module.exports = function ({ theme, locals: { tags }, helpers }) { 5 | const config = theme.tag; 6 | 7 | if (!tags.length) return []; 8 | 9 | return [ 10 | helpers.generateJson({ 11 | path: 'tags', 12 | data: tags.map(tag => ({ name: tag.name, count: tag.posts.length })) 13 | }), 14 | helpers.generateHtml({ 15 | path: 'tags', 16 | data: { type: 'tags' } 17 | }), 18 | 19 | tags.map(tag => [ 20 | helpers.pagination.apply( 21 | tag.posts.map(pick(tagPostsProps)), 22 | { perPage: config.per_page, id: `tags/${tag.name}`, extend: { name: tag.name } }, 23 | [ 24 | { type: 'json' }, 25 | { type: 'html', extend: { type: 'tags' } }, 26 | ] 27 | ) 28 | ]) 29 | ].flat(Infinity); 30 | }; 31 | -------------------------------------------------------------------------------- /lib/generator/index.js: -------------------------------------------------------------------------------- 1 | const { published, visible } = require('../utils'); 2 | const generators = [ 3 | require('./config'), 4 | require('./entries'), 5 | require('./sitemap'), 6 | require('./manifest'), 7 | require('./sw') 8 | ]; 9 | const builtInRoutes = ['page', 'categories', 'tags', 'archives', 'search', '404']; 10 | 11 | module.exports = function (hexo) { 12 | // Remove hexo default generators 13 | ['index', 'post', 'page', 'archive', 'category', 'tag'] 14 | .forEach(name => delete hexo.extend.generator.store[name]); 15 | 16 | hexo.extend.generator.register('inside', function (locals) { 17 | const sLocals = { 18 | tags: getCollection(locals.tags), 19 | categories: getCollection(locals.categories), 20 | pages: locals.pages.filter(filterBuiltInRoutes).toArray(), 21 | posts: locals.posts.filter(published).filter(filterBuiltInRoutes).sort('-date').toArray(), 22 | indexRouteDetected: filterBuiltInRoutes.indexRouteDetected, 23 | }; 24 | 25 | return generators.map(fn => fn.call(this, sLocals)).flat(); 26 | }); 27 | 28 | /** 29 | * Filter built-in routes to improve compatibility 30 | * 31 | * @param {*} post 32 | * @returns {boolean} 33 | */ 34 | function filterBuiltInRoutes(post) { 35 | if (builtInRoutes.includes(post.path.split('/')[0])) { 36 | hexo.log.warn(post.source + ' won\'t be rendered.'); 37 | return false; 38 | } 39 | if (post.link === '') { 40 | if (filterBuiltInRoutes.indexRouteDetected) { 41 | hexo.log.warn('index route already set,', post.source + ' won\'t be rendered.'); 42 | return false; 43 | } 44 | filterBuiltInRoutes.indexRouteDetected = true; 45 | } 46 | 47 | return true; 48 | } 49 | 50 | /** 51 | * Sort posts of tag and category 52 | * 53 | * @param {any[]} collection 54 | * @returns {any[]} 55 | */ 56 | function getCollection(collection) { 57 | return collection.sort('name').map(data => ({ 58 | posts: data.posts.filter(i => published(i) && visible(i)).sort('-date'), 59 | name: data.name 60 | })).filter(data => data.posts.length) 61 | } 62 | }; 63 | -------------------------------------------------------------------------------- /lib/generator/manifest.js: -------------------------------------------------------------------------------- 1 | module.exports = function (locals) { 2 | const manifest = this.theme.config.pwa.manifest; 3 | 4 | if (!manifest) return; 5 | 6 | return [{ 7 | path: 'manifest.json', 8 | data: JSON.stringify(manifest) 9 | }]; 10 | }; 11 | -------------------------------------------------------------------------------- /lib/generator/sitemap.js: -------------------------------------------------------------------------------- 1 | const { visible } = require('../utils'); 2 | 3 | module.exports = function (locals) { 4 | const urlFn = ({ plink, updated }) => `${plink}${updated.toJSON()}`; 5 | const urlset = [...locals.posts, ...locals.pages].filter(visible).map(urlFn).join(''); 6 | 7 | return [{ 8 | path: 'sitemap.xml', 9 | data: `${urlset}` 10 | }]; 11 | }; 12 | -------------------------------------------------------------------------------- /lib/generator/sw.js: -------------------------------------------------------------------------------- 1 | const trim_slash_regex = /(^\/*|\/*$)/g; 2 | 3 | module.exports = function () { 4 | const workbox = this.theme.config.pwa.workbox; 5 | 6 | if (!workbox) return; 7 | 8 | const version = this.theme.config.runtime.hash; 9 | const root = ('/' + this.config.root.replace(trim_slash_regex, '')).replace(/^\/$/, ''); 10 | const globalExpire = workbox.expire * 60 * 60; 11 | let baseRules = [ 12 | { name: 'sw', strategy: 'networkOnly', regex: genRegex(workbox.name) }, 13 | { name: 'html', regex: `${root}/.*(:?/[^\\\\.]*/?)$`, expire: 0 }, 14 | ], rules = {}; 15 | 16 | baseRules.concat(workbox.rules || []).forEach(({ name, strategy, regex, expire }) => { 17 | rules[name] = { name: getCacheName(name), strategy, regex, expire: typeof expire === 'number' ? expire * 60 * 60 : globalExpire }; 18 | }); 19 | 20 | let script = [`importScripts('${workbox.cdn}');`]; 21 | 22 | if (workbox.module_path_prefix) 23 | script.push(`workbox.setConfig({ modulePathPrefix: '${workbox.module_path_prefix}' });`, ''); 24 | 25 | // clean up old caches 26 | script.push( 27 | `self.addEventListener('install', function (event) {`, 28 | ` event.waitUntil(`, 29 | ` caches.keys().then(function (names) {`, 30 | ` var validSets = ${JSON.stringify(Object.values(rules).map(i => i.name))};`, 31 | ` return Promise.all(`, 32 | ` names`, 33 | ` .filter(function (name) { return !~validSets.indexOf(name); })`, 34 | ` .map(function (name) {`, 35 | ` indexedDB && indexedDB.deleteDatabase(name);`, 36 | ` return caches.delete(name);`, 37 | ` })`, 38 | ` ).then(function() { self.skipWaiting() });`, 39 | ` })`, 40 | ` );`, 41 | `});`, 42 | '' 43 | ); 44 | 45 | // Routing 46 | for (const name in rules) { 47 | if (name === 'html') continue; 48 | 49 | const rule = rules[name]; 50 | const routes = [ 51 | `workbox.routing.registerRoute(new RegExp('${rule.regex}'), workbox.strategies.${rule.strategy}({`, 52 | ` cacheName: '${rule.name}',`, 53 | '}));' 54 | ]; 55 | 56 | if (rule.expire && rule.strategy !== 'networkOnly') 57 | routes.splice(2, 0, ` plugins: [ new workbox.expiration.Plugin({ maxAgeSeconds: ${rule.expire} }) ],`); 58 | 59 | script = script.concat(routes); 60 | }; 61 | 62 | script.push(''); 63 | 64 | // Special handling for html to avoid multiple redirects 65 | if (rules.html.expire) { 66 | script.push(`var htmlManager = new workbox.expiration.CacheExpiration('${rules.html.name}', { maxAgeSeconds: ${rules.html.expire} });`); 67 | script.push( 68 | `workbox.routing.registerRoute(new RegExp('${rules.html.regex}'), function(context) {`, 69 | ` var url = context.url.pathname;`, 70 | ` if (!url.endsWith('/')) url += '/';`, 71 | ` return caches.match(url)`, 72 | ` .then(res => {`, 73 | ` if (res) htmlManager.updateTimestamp(url);`, 74 | ` return res || fetch(url);`, 75 | ` })`, 76 | ` .then(res => {`, 77 | ` caches.open('${rules.html.name}').then(cache => cache.put(url, res));`, 78 | ` return res.clone();`, 79 | ` })`, 80 | ` .catch(() => fetch(url));`, 81 | `});` 82 | ); 83 | } else { 84 | script.push( 85 | `workbox.routing.registerRoute(new RegExp('${rules.html.regex}'), function(context) {`, 86 | ` var url = context.url.pathname;`, 87 | ` if (!url.endsWith('/')) url += '/';`, 88 | ` return fetch(url);`, 89 | `});` 90 | ); 91 | } 92 | 93 | return [{ 94 | path: workbox.name, 95 | data: script.join('\n') 96 | }]; 97 | 98 | function getCacheName(name) { 99 | return `is-${name}-${version}`; 100 | } 101 | 102 | }; 103 | 104 | function genRegex(str) { 105 | return str ? str.replace(trim_slash_regex, '').replace(/(\.|\?|\:)/g, '\\\\$1') : ''; 106 | } 107 | -------------------------------------------------------------------------------- /lib/helper/ga.js: -------------------------------------------------------------------------------- 1 | let cache; 2 | 3 | module.exports = function (id) { 4 | if (typeof cache === 'undefined') { 5 | cache = id ? [ 6 | ``, 7 | `` 8 | ].join('\n') : ''; 9 | } 10 | 11 | return cache; 12 | } 13 | -------------------------------------------------------------------------------- /lib/helper/index.js: -------------------------------------------------------------------------------- 1 | module.exports = function (hexo) { 2 | hexo.extend.helper.register('url_trim', require('./url_trim')); 3 | hexo.extend.helper.register('structured_data', require('./structured_data')); 4 | hexo.extend.helper.register('ga', require('./ga')); 5 | }; 6 | -------------------------------------------------------------------------------- /lib/helper/structured_data.js: -------------------------------------------------------------------------------- 1 | const url = require('url'); 2 | const supportedSocials = ['Facebook', 'Twitter', 'Instagram', 'YouTube', 'LinkedIn', 'Myspace', 'Pinterest', 'SoundCloud', 'Tumblr']; 3 | let avatar = ''; 4 | let website = null; 5 | let person = null; 6 | let org = null; 7 | 8 | module.exports = function (page) { 9 | const config = this.config; 10 | const theme = this.theme; 11 | const lang = config.language; 12 | const datum = []; 13 | 14 | if (!avatar) avatar = url.resolve(config.url, this.url_for(theme.profile.avatar)); 15 | 16 | if (!person) { 17 | person = { 18 | "@type": "Person", 19 | "name": config.author, 20 | "description": theme.profile.bio || config.description, 21 | "image": avatar 22 | }; 23 | 24 | // https://developers.google.com/search/docs/data-types/social-profile 25 | const socials = supportedSocials.map(social => theme.sns[social.toLowerCase()]).filter(i => i); 26 | if (socials.length) person.sameAs = supportedSocials.map(social => theme.sns[social.toLowerCase()]).filter(i => i); 27 | } 28 | 29 | if (!org) { 30 | org = { 31 | "@type": "Organization", 32 | "name": config.title, 33 | "logo": { 34 | "@type": "ImageObject", 35 | "url": avatar 36 | } 37 | }; 38 | } 39 | 40 | if (!website) { 41 | website = { 42 | "@context": "http://schema.org", 43 | "@type": "WebSite", 44 | "publisher": person, 45 | "url": config.url, 46 | "image": avatar, 47 | "description": config.description, 48 | "author": person, 49 | "inLanguage": { 50 | "@type": "Language", 51 | "alternateName": lang 52 | } 53 | }; 54 | } 55 | datum.push(website); 56 | 57 | // https://developers.google.com/search/docs/data-types/article 58 | if (page.type === 'post') { 59 | const category = page.categories.toArray()[0]; 60 | const article = { 61 | "@context": "http://schema.org", 62 | "@type": "Article", 63 | "articleSection": category ? category.name : '', 64 | "url": page.permalink, 65 | "headline": page.title, 66 | "image": page.thumbnail || avatar, 67 | "datePublished": page.date, 68 | "dateModified": page.updated, 69 | "keywords": page.tags ? page.tags.map(t => t.name).join(',') : '', 70 | "description": page.excerpt ? this.strip_html(page.excerpt) : config.description, 71 | "publisher": org, 72 | "author": person, 73 | "inLanguage": { 74 | "@type": "Language", 75 | "alternateName": lang 76 | }, 77 | "mainEntityOfPage": { 78 | "@type": "WebPage", 79 | "@id": page.permalink 80 | } 81 | }; 82 | if (page.thumbnail) article.thumbnailUrl = page.thumbnail; 83 | datum.push(article); 84 | }; 85 | 86 | return ''; 87 | } 88 | -------------------------------------------------------------------------------- /lib/helper/url_trim.js: -------------------------------------------------------------------------------- 1 | const { trimHtml } = require('../utils') 2 | 3 | module.exports = function (url) { 4 | return url ? trimHtml(url) + '/' : ''; 5 | } 6 | -------------------------------------------------------------------------------- /lib/plugins/disqus.js: -------------------------------------------------------------------------------- 1 | "use strict";function e(e){const t=Object.keys(e).reduce((t,r)=>e[r]||0===e[r]?t.concat(`${r.replace(/_/g,"-")}="${e[r]}"`):t,[]).join(" ");return t?" "+t:""}Object.defineProperty(exports,"__esModule",{value:!0});exports.exec=function(t,r={},s){const o={...r};return o.script=o.script||`//${o.shortname}.disqus.com/embed.js`,delete o.shortname,t.extend.generator.register("disqus.b4418a45.js",()=>({path:"disqus.b4418a45.js",data:s.i18n("!function(){\"use strict\";function t(t,e={},o){const n=document.createElement(t);return e.class&&n.classList.add(e.class),e.props&&Object.keys(e.props).forEach(t=>n[t]=e.props[t]),e.innerHTML&&(n.innerHTML=e.innerHTML),o&&o.forEach(t=>{n.appendChild(t)}),n}const[e,o]=JSON.parse('[\":host{position:relative;display:block;border-color:var(--inside-border-color)}.a{display:block;cursor:pointer;padding:1rem;border-radius:3px;background-color:var(--inside-accent-color-005);line-height:1.6;transition:background-color .15s;user-select:none}.a:active,.a:hover{color:#fff;background-color:var(--inside-accent-color);background-image:none}.a,.b{text-align:center}\",{\"button\":\"a\",\"loading\":\"b\"}]');!function(t,e){const o={root:null};e.shared&&e.shared(o),customElements.define(\"is-\"+t,class extends HTMLElement{static get observedAttributes(){return e.attrs}constructor(){super();const t=this.attachShadow({mode:\"open\"}),n=document.createElement(\"style\");n.textContent=e.style,e.created&&Object.assign(o,e.created(t)),o.root=t,e.style&&t.appendChild(n)}connectedCallback(){e.connected&&e.connected(o)}attributeChangedCallback(t,n,r){e.changed&&e.changed(o,[t,n,r])}})}(\"disqus\",{style:e,attrs:[\"autoload\",\"script\"],shared(t){document.addEventListener(\"inside\",({detail:e})=>{\"route\"===e.type&&(t.current={title:e.data.title,url:e.data.url,identifier:e.data.id})})},connected(e){const n={autoload:e.root.host.hasAttribute(\"autoload\"),script:e.root.host.getAttribute(\"script\")},r='
',i=t(\"div\",{class:o.loading,props:{innerHTML:\"Loading...\"}}),s=t(\"a\",{class:o.button,props:{textContent:\"{{ comments.load | Disqus }}\"}});function a(){if(window.DISQUS)return t();function t(){e.root.host.outerHTML=r,requestAnimationFrame(()=>{window.DISQUS.reset({reload:!0,config(){Object.assign(this.page,e.current)}})})}window.disqus_config=function(){Object.assign(this.page,e.current)},e.root.host.innerHTML=r,s.remove(),e.root.appendChild(i),function(t){const e=document.createElement(\"script\");return e.src=\"https:\"+t,new Promise((t,o)=>{e.onload=t,e.onerror=t=>{e.remove(),o(t)},document.body.appendChild(e)})}(n.script).then(t).catch(()=>{s.textContent=\"{{ comments.load_faild | Disqus }}\",e.root.appendChild(s)}).finally(()=>{i.remove()})}window.DISQUS||n.autoload?requestAnimationFrame(a):(s.addEventListener(\"click\",a),e.root.appendChild(s))}})}();\n")})),t.extend.injector.register("head_end",s.js("disqus.b4418a45.js")),[{position:"comments",template:``}]},exports.filename="disqus.b4418a45.js",exports.schema={type:"object",properties:{shortname:{type:"string"},script:{type:"string",format:"uri"},autoload:{type:"boolean",default:!0}},required:["shortname","autoload"],additionalProperties:!1}; 2 | -------------------------------------------------------------------------------- /lib/plugins/manifest.json: -------------------------------------------------------------------------------- 1 | {"cipher":"cipher.js","disqus":"disqus.js","palette":"palette.js"} 2 | -------------------------------------------------------------------------------- /lib/plugins/palette.js: -------------------------------------------------------------------------------- 1 | "use strict";function e(e){const t=Object.keys(e).reduce((t,r)=>e[r]||0===e[r]?t.concat(`${r.replace(/_/g,"-")}="${e[r]}"`):t,[]).join(" ");return t?" "+t:""}Object.defineProperty(exports,"__esModule",{value:!0});exports.exec=function(t,r={},i){return t.extend.generator.register("palette.5a4d0a6b.js",()=>({path:"palette.5a4d0a6b.js",data:i.i18n("!function(){\"use strict\";var e,_,t,n,o,r={},l=[],i=/acit|ex(?:s|g|n|p|$)|rph|grid|ows|mnc|ntw|ine[ch]|zoo|^ord|itera/i;function u(e,_){for(var t in _)e[t]=_[t];return e}function c(e){var _=e.parentNode;_&&_.removeChild(e)}function s(_,t,n){var o,r,l,i={};for(l in t)\"key\"==l?o=t[l]:\"ref\"==l?r=t[l]:i[l]=t[l];if(arguments.length>2&&(i.children=arguments.length>3?e.call(arguments,2):n),\"function\"==typeof _&&null!=_.defaultProps)for(l in _.defaultProps)void 0===i[l]&&(i[l]=_.defaultProps[l]);return a(_,i,o,r,null)}function a(e,n,o,r,l){var i={type:e,props:n,key:o,ref:r,__k:null,__:null,__b:0,__e:null,__d:void 0,__c:null,__h:null,constructor:void 0,__v:null==l?++t:l};return null==l&&null!=_.vnode&&_.vnode(i),i}function f(e){return e.children}function p(e,_){this.props=e,this.context=_}function h(e,_){if(null==_)return e.__?h(e.__,e.__.__k.indexOf(e)+1):null;for(var t;_0?a(y.type,y.props,y.key,y.ref?y.ref:null,y.__v):y)){if(y.__=t,y.__b=t.__b+1,null===(m=E[d])||m&&y.key==m.key&&y.type===m.type)E[d]=void 0;else for(v=0;v(_&&e.push(_),e),[]).join(\" \")}e=l.slice,_={__e:function(e,_,t,n){for(var o,r,l;_=_.__;)if((o=_.__c)&&!o.__)try{if((r=o.constructor)&&null!=r.getDerivedStateFromError&&(o.setState(r.getDerivedStateFromError(e)),l=o.__d),null!=o.componentDidCatch&&(o.componentDidCatch(e,n||{}),l=o.__d),l)return o.__E=o}catch(_){e=_}throw e}},t=0,p.prototype.setState=function(e,_){var t;t=null!=this.__s&&this.__s!==this.state?this.__s:this.__s=u({},this.state),\"function\"==typeof e&&(e=e(u({},t),this.props)),e&&u(t,e),null!=e&&this.__v&&(_&&this._sb.push(_),v(this))},p.prototype.forceUpdate=function(e){this.__v&&(this.__e=!0,e&&this.__h.push(e),v(this))},p.prototype.render=f,n=[],m.__r=0;var U,D,F,M,L=0,O=[],W=[],V=_.__b,j=_.__r,q=_.diffed,z=_.__c,I=_.unmount;function R(e){return L=1,function(e,t,n){var o=function(e,t){_.__h&&_.__h(D,e,L||t),L=0;var n=D.__H||(D.__H={__:[],__h:[]});return e>=n.__.length&&n.__.push({__V:W}),n.__[e]}(U++,2);if(o.t=e,!o.__c&&(o.__=[n?n(t):Q(void 0,t),function(e){var _=o.__N?o.__N[0]:o.__[0],t=o.t(_,e);_!==t&&(o.__N=[t,o.__[1]],o.__c.setState({}))}],o.__c=D,!D.u)){D.u=!0;var r=D.shouldComponentUpdate;D.shouldComponentUpdate=function(e,_,t){if(!o.__c.__H)return!0;var n=o.__c.__H.__.filter((function(e){return e.__c}));if(n.every((function(e){return!e.__N})))return!r||r.call(this,e,_,t);var l=!1;return n.forEach((function(e){if(e.__N){var _=e.__[0];e.__=e.__N,e.__N=void 0,_!==e.__[0]&&(l=!0)}})),!(!l&&o.__c.props===e)&&(!r||r.call(this,e,_,t))}}return o.__N||o.__}(Q,e)}function B(){for(var e;e=O.shift();)if(e.__P&&e.__H)try{e.__H.__h.forEach(J),e.__H.__h.forEach(K),e.__H.__h=[]}catch(t){e.__H.__h=[],_.__e(t,e.__v)}}_.__b=function(e){D=null,V&&V(e)},_.__r=function(e){j&&j(e),U=0;var _=(D=e.__c).__H;_&&(F===D?(_.__h=[],D.__h=[],_.__.forEach((function(e){e.__N&&(e.__=e.__N),e.__V=W,e.__N=e.i=void 0}))):(_.__h.forEach(J),_.__h.forEach(K),_.__h=[])),F=D},_.diffed=function(e){q&&q(e);var t=e.__c;t&&t.__H&&(t.__H.__h.length&&(1!==O.push(t)&&M===_.requestAnimationFrame||((M=_.requestAnimationFrame)||G)(B)),t.__H.__.forEach((function(e){e.i&&(e.__H=e.i),e.__V!==W&&(e.__=e.__V),e.i=void 0,e.__V=W}))),F=D=null},_.__c=function(e,t){t.some((function(e){try{e.__h.forEach(J),e.__h=e.__h.filter((function(e){return!e.__||K(e)}))}catch(n){t.some((function(e){e.__h&&(e.__h=[])})),t=[],_.__e(n,e.__v)}})),z&&z(e,t)},_.unmount=function(e){I&&I(e);var t,n=e.__c;n&&n.__H&&(n.__H.__.forEach((function(e){try{J(e)}catch(e){t=e}})),n.__H=void 0,t&&_.__e(t,n.__v))};var $=\"function\"==typeof requestAnimationFrame;function G(e){var _,t=function(){clearTimeout(n),$&&cancelAnimationFrame(_),setTimeout(e)},n=setTimeout(t,100);$&&(_=requestAnimationFrame(t))}function J(e){var _=D,t=e.__c;\"function\"==typeof t&&(e.__c=void 0,t()),D=_}function K(e){var _=D;e.__c=e.__(),D=_}function Q(e,_){return\"function\"==typeof _?_(e):_}const[X,Y]=JSON.parse('[\":host{display:block;margin:1em auto}section{display:flex;flex-wrap:wrap;justify-content:center;line-height:1;margin:auto}a{margin:1px;width:24px;height:24px;border-radius:24px;background-size:60px!important;font-size:24px;box-sizing:border-box;cursor:pointer;border:1px solid transparent;transition:transform .15s}a:hover{transform:scale(1.1);border-color:inherit}a.a{width:calc(3rem + 2px)}\",{\"is-lg\":\"a\"}]');function Z(e){const{color:_}=e,t=Math.min(_.length,e.col>0?e.col:5)||1,[n,o]=R(-1);return s(\"section\",{className:Y.root,style:{width:24*t+2*t+\"px\"}},_.map((e,_)=>s(\"a\",{style:{background:e,margin:\"1px\"},className:P(n===_&&Y[\"is-active\"]),title:e,onClick:()=>{var t;o(_),t={accent_color:e},document.dispatchEvent(new CustomEvent(\"inside\",{detail:{type:\"theme\",data:t}}))}})))}!function(e,_){const t={root:null};_.shared&&_.shared(t),customElements.define(\"is-\"+e,class extends HTMLElement{static get observedAttributes(){return _.attrs}constructor(){super();const e=this.attachShadow({mode:\"open\"}),n=document.createElement(\"style\");n.textContent=_.style,_.created&&Object.assign(t,_.created(e)),t.root=e,_.style&&e.appendChild(n)}connectedCallback(){_.connected&&_.connected(t)}attributeChangedCallback(e,n,o){_.changed&&_.changed(t,[e,n,o])}})}(\"palette\",{style:X,attrs:[\"theme\",\"col\"],created:t=>({render(n){!function(t,n,o){var l,i,u;_.__&&_.__(t,n),i=(l=\"function\"==typeof o)?null:o&&o.__k||n.__k,u=[],C(n,t=(!l&&o||n).__k=s(f,null,[t]),i||r,r,void 0!==n.ownerSVGElement,!l&&o?[o]:i?null:n.firstChild?e.call(n.childNodes):null,u,!l&&o?o:i?i.__e:n.firstChild,l),H(u,t)}(s(Z,n),t)},props:{}}),changed({props:e,render:_},[t,,n]){\"theme\"===t?_(Object.assign(e,{color:n.split(\",\")})):\"col\"!==t||Number.isNaN(+n)||_(Object.assign(e,{col:+n}))}})}();\n")})),t.extend.injector.register("head_end",i.js("palette.5a4d0a6b.js")),[{position:"sidebar",template:``}]},exports.filename="palette.5a4d0a6b.js",exports.schema={type:"object",properties:{theme:{type:"array",items:{type:"string",pattern:"^#(?:[0-9a-fA-F]{3}){1,2}$"}},col:{type:"number",minimum:1}},required:["theme"],additionalProperties:!1}; 2 | -------------------------------------------------------------------------------- /lib/renderer/index.js: -------------------------------------------------------------------------------- 1 | const renderer = require('./markdown'); 2 | 3 | module.exports = function (hexo) { 4 | hexo.config.markdown = Object.assign({ 5 | html: true, 6 | xhtmlOut: false, 7 | breaks: true, 8 | linkify: true, 9 | typographer: true, 10 | quotes: '“”‘’', 11 | }, hexo.config.markdown); 12 | 13 | hexo.extend.renderer.register('md', 'html', renderer, true); 14 | hexo.extend.renderer.register('markdown', 'html', renderer, true); 15 | hexo.extend.renderer.register('mkd', 'html', renderer, true); 16 | hexo.extend.renderer.register('mkdn', 'html', renderer, true); 17 | hexo.extend.renderer.register('mdwn', 'html', renderer, true); 18 | hexo.extend.renderer.register('mdtxt', 'html', renderer, true); 19 | hexo.extend.renderer.register('mdtext', 'html', renderer, true); 20 | }; 21 | -------------------------------------------------------------------------------- /lib/renderer/markdown/index.js: -------------------------------------------------------------------------------- 1 | const markdownIt = require('markdown-it'); 2 | const mixins = require('./mixins'); 3 | const plugins = require('./plugins'); 4 | 5 | module.exports = function (data) { 6 | const hexo = this; 7 | const config = hexo.config.markdown; 8 | const renderer = markdownIt(config); 9 | 10 | mixins.apply(hexo, [renderer, config]); 11 | plugins.apply(hexo, [renderer, config]); 12 | 13 | return renderer.render(data.text, { 14 | theme: hexo.theme.config, 15 | styles: hexo.theme.config.runtime.styles, 16 | uriReplacer: hexo.theme.config.runtime.uriReplacer, 17 | getHeadingId: new function () { 18 | const map = {}; 19 | return (title) => { 20 | if (map[title] === undefined) { 21 | map[title] = 0 22 | return title; 23 | } else { 24 | map[title] = map[title] + 1; 25 | return title + '-' + map[title]; 26 | } 27 | } 28 | } 29 | }); 30 | }; 31 | -------------------------------------------------------------------------------- /lib/renderer/markdown/mixins.js: -------------------------------------------------------------------------------- 1 | const { parseJs, isExternal, parsePipe } = require('../../utils'); 2 | const script_regex = /]*)>([\s\S]*)<\/script>/; 3 | 4 | module.exports = function mixins(md, config) { 5 | md.renderer.rules.table_open = function (tokens, idx, options, env, slf) { 6 | const token = tokens[idx]; 7 | const { styles } = env; 8 | 9 | return `
`; 10 | }; 11 | md.renderer.rules.table_close = function () { 12 | return '
'; 13 | }; 14 | md.renderer.rules.html_block = function (tokens, idx, options, env, slf) { 15 | const token = tokens[idx]; 16 | 17 | // transform script 18 | const html = token.content; 19 | const group = html.match(script_regex); 20 | if (group) { 21 | const minified = parseJs(group[2]); 22 | if (!minified) return html; 23 | group[1] && (group[1] = ' ' + group[1]); 24 | return `${minified}` 25 | } 26 | return html; 27 | }; 28 | 29 | md.core.ruler.after('inline', 'inside', function (state) { 30 | const { tokens, env } = state; 31 | const { theme, styles, getHeadingId, uriReplacer } = env; 32 | 33 | for (let i = 0; i < tokens.length; i++) { 34 | const token = tokens[i]; 35 | const inlineTokens = token.children || []; 36 | 37 | for (const inlineToken of inlineTokens) { 38 | // make block-level images zoomable 39 | if (inlineToken.type === 'image') { 40 | const title = inlineToken.attrGet('title'); 41 | const data = parsePipe(title); 42 | 43 | inlineToken.attrSet('loading', 'lazy'); 44 | inlineToken.attrSet('src', uriReplacer(inlineToken.attrGet('src'))); 45 | 46 | /** 47 | * hint for: 48 | * ```md 49 | * 50 | * ![alt](url) 51 | * 52 | * ``` 53 | */ 54 | if ( 55 | token.type === 'inline' && 56 | token.children.length === 1 && 57 | tokens[i - 1].type === 'paragraph_open' && 58 | tokens[i + 1].type === 'paragraph_close' 59 | ) data.block = true 60 | 61 | if (data.value) { 62 | inlineToken.attrSet('title', data.value); 63 | } 64 | // case: ![alt](url "|block") 65 | else if (title) { 66 | inlineToken.attrs.splice(inlineToken.attrIndex('title'), 1); 67 | } 68 | if (data.block) inlineToken.attrPush(['class', styles.blockimg]); 69 | } 70 | 71 | if (inlineToken.type === 'link_open') { 72 | const href = inlineToken.attrGet('href'); 73 | if (isExternal(href)) inlineToken.attrPush(['target', '_blank']); 74 | } 75 | } 76 | 77 | // todolist 78 | if ( 79 | i > 1 && 80 | token.type === 'inline' && 81 | tokens[i - 1].type === 'paragraph_open' && 82 | tokens[i - 2].type === 'list_item_open' 83 | ) { 84 | if (token.content[0] === '[' && token.content[2] === ']') { 85 | const checkbox = [ 86 | new state.Token('input', 'input', 0), 87 | new state.Token('tag_open', 'i', 1), 88 | new state.Token('tag_close', 'i', -1), 89 | ]; 90 | 91 | checkbox[0].attrPush(['type', 'checkbox']); 92 | checkbox[0].attrPush(['disabled', '']); 93 | if (token.content[1] !== ' ') checkbox[0].attrPush(['checked', '']); 94 | 95 | // remove [x], [ ] 96 | token.content = token.content.slice(4); 97 | token.children[0].content = token.children[0].content.slice(4); 98 | 99 | token.children.unshift(...checkbox); 100 | 101 | // add class name to ul/ol 102 | findClosest(tokens, i - 2, ['bullet_list_open', 'ordered_list_open']) 103 | .attrPush(['class', styles.checklist]); 104 | } 105 | } 106 | 107 | // heading anchor 108 | if (token.type === 'heading_open') { 109 | const link = [new state.Token('tag_open', 'a', 1), new state.Token('tag_close', 'a', -1)]; 110 | const headingInline = tokens[i + 1]; 111 | const id = getHeadingId( 112 | headingInline.children.map(t => t.content).join('').split(' ').join('-').toLowerCase() 113 | ); 114 | const href = '#' + id; 115 | 116 | link[0].attrPush(['title', href]); 117 | link[0].attrPush(['href', href]); 118 | token.attrPush(['id', id]); 119 | 120 | headingInline.children.push(...link); 121 | 122 | // skip next token since it is already been processed 123 | i++; 124 | } 125 | 126 | // empty th 127 | if (token.type === 'th_open') { 128 | const thInline = tokens[i + 1]; 129 | 130 | if (thInline.content === '' || thInline.content === ' ') { 131 | thInline.content = ''; 132 | thInline.children.length = 0; 133 | token.attrSet('style', 'padding:0'); 134 | // skip next token since it is already been processed 135 | i++; 136 | } 137 | } 138 | } 139 | }); 140 | } 141 | 142 | /** 143 | * @param {Array<{ type: string }>} tokens 144 | * @param {{ type: string }} fromTag 145 | * @param {number} fromTagIdx 146 | * @param {string[]} targetTypes 147 | * @returns {{ type: string }} 148 | */ 149 | function findClosest(tokens, fromTagIdx, targetTypes) { 150 | if (fromTagIdx < 0) return null; 151 | 152 | const current = tokens[fromTagIdx - 1]; 153 | if (targetTypes.includes(current.type)) return current; 154 | 155 | return findClosest(tokens, fromTagIdx - 1, targetTypes); 156 | } 157 | -------------------------------------------------------------------------------- /lib/renderer/markdown/plugins/collapse.js: -------------------------------------------------------------------------------- 1 | const container = require('markdown-it-container'); 2 | const { parsePipe } = require('../../../utils'); 3 | 4 | module.exports = [ 5 | container, 6 | 'collapse', 7 | { 8 | render(tokens, idx, options, env, slf) { 9 | const meta = parsePipe(tokens[idx].info.trim().slice(9)); 10 | 11 | return tokens[idx].nesting === 1 ? `
${meta.value}` : '
'; 12 | }, 13 | }, 14 | ]; 15 | -------------------------------------------------------------------------------- /lib/renderer/markdown/plugins/index.js: -------------------------------------------------------------------------------- 1 | const collapse = require('./collapse'); 2 | const tree = require('./tree'); 3 | const timeline = require('./timeline'); 4 | 5 | module.exports = function plugins(md, config) { 6 | const hexo = this; 7 | 8 | md.use(...collapse); 9 | md.use(...tree); 10 | md.use(...timeline); 11 | 12 | (config.plugins || []).forEach((plugin) => { 13 | if (typeof plugin === 'string') { 14 | return loadPlugin(plugin); 15 | } else { 16 | const id = Object.keys(plugin)[0]; 17 | loadPlugin(id, plugin[id]); 18 | } 19 | }); 20 | 21 | /** 22 | * be able to extend renderer, mainly load plugins. 23 | * @example 24 | * ```js 25 | * hexo.extend.filter.register('inside:renderer', function(renderer) { 26 | * renderer.use(container, 'collapse', [render(tokens, idx, options, env, slf) {}]); 27 | * }); 28 | * ``` 29 | */ 30 | hexo.execFilterSync('inside:renderer', md, { context: hexo }); 31 | 32 | function loadPlugin(id, config) { 33 | try { 34 | md.use(require(id), config); 35 | } catch (e) { 36 | hexo.log.error(e.message); 37 | } 38 | } 39 | }; 40 | -------------------------------------------------------------------------------- /lib/renderer/markdown/plugins/timeline.js: -------------------------------------------------------------------------------- 1 | const container = require('markdown-it-container'); 2 | 3 | module.exports = [ 4 | container, 5 | 'timeline', 6 | { 7 | render(tokens, idx, options, env, slf) { 8 | const { styles } = env; 9 | 10 | return tokens[idx].nesting === 1 ? `
` : '
'; 11 | }, 12 | }, 13 | ]; 14 | -------------------------------------------------------------------------------- /lib/renderer/markdown/plugins/tree.js: -------------------------------------------------------------------------------- 1 | const container = require('markdown-it-container'); 2 | const { parsePipe } = require('../../../utils'); 3 | 4 | module.exports = [ 5 | container, 6 | 'tree', 7 | { 8 | render(tokens, idx, options, env, slf) { 9 | const meta = parsePipe('|' + tokens[idx].info.trim().slice(5)); 10 | const { styles } = env; 11 | 12 | const treeIcon = styles['tree--' + meta.options.icon] || styles['tree--square']; 13 | 14 | if (tokens[idx].nesting === 1) { 15 | for (let i = idx; ; i++) { 16 | if (tokens[i].type === 'container_tree_close') { 17 | break; 18 | } 19 | if (tokens[i].type === 'inline' && tokens[i + 2].type === 'bullet_list_open') { 20 | tokens[i + 1].hidden = tokens[i - 1].hidden = false; 21 | tokens[i - 1].tag = tokens[i + 1].tag = 'summary'; 22 | tokens[i - 2].tag = 'details'; 23 | // find bullet_list_close to close the details 24 | let j = i + 2; 25 | while(true) { 26 | if (tokens[j].type === 'bullet_list_close') { 27 | tokens[j].tag = 'details'; 28 | break; 29 | } 30 | j += 1; 31 | } 32 | } 33 | } 34 | // hide ul open 35 | tokens[idx + 1].hidden = true; 36 | return `
`; 37 | } else { 38 | // hide ul close 39 | tokens[idx - 1].hidden = true; 40 | return '
'; 41 | } 42 | }, 43 | }, 44 | ]; 45 | -------------------------------------------------------------------------------- /lib/tag/canvas.js: -------------------------------------------------------------------------------- 1 | const { snippet } = require('../utils'); 2 | let currentTitle = ''; id = 0; 3 | 4 | /** 5 | * Canvas snippet 6 | * 7 | * Syntax: 8 | * {% canvas [width] [height] %} 9 | * ctx.fillStyle = 'red'; 10 | * ctx.fillRect(0, 0, w, h); 11 | * {% endcanvas %} 12 | */ 13 | module.exports = function (args, content) { 14 | if (this.title !== currentTitle) { 15 | id = 0; 16 | currentTitle = this.title; 17 | } 18 | 19 | let [width, height] = args; 20 | const cid = `canvas-${id}`; 21 | 22 | 23 | if (!(+width > 0)) width = 300; 24 | if (!(+height > 0)) height = 150; 25 | 26 | id++; 27 | 28 | return snippet( 29 | `var canvas = document.getElementById('${cid}'), ctx = canvas.getContext('2d'), w = ${width}, h = ${height}; ${content}`, 30 | code => `

` 31 | ) 32 | } 33 | -------------------------------------------------------------------------------- /lib/tag/gist.js: -------------------------------------------------------------------------------- 1 | const { htmlTag } = require('../utils'); 2 | 3 | module.exports = function (...args) { 4 | const id = args.shift(); 5 | const file = args.length ? `?file=${args[0]}` : ''; 6 | 7 | return htmlTag('script', { src: `//gist.github.com/${id}.js${file}` }); 8 | }; 9 | -------------------------------------------------------------------------------- /lib/tag/iframe.js: -------------------------------------------------------------------------------- 1 | const { htmlTag } = require('../utils'); 2 | 3 | module.exports = function ([src, width = '100%', height = 300]) { 4 | return htmlTag('iframe', { 5 | width, 6 | height, 7 | src, 8 | frameborder: '0', 9 | }); 10 | }; 11 | -------------------------------------------------------------------------------- /lib/tag/index.js: -------------------------------------------------------------------------------- 1 | module.exports = function (hexo) { 2 | hexo.extend.tag.register('gist', require('./gist')); 3 | hexo.extend.tag.register('iframe', require('./iframe')); 4 | hexo.extend.tag.register('canvas', require('./canvas'), true); 5 | }; 6 | -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | const crypto = require('crypto'); 2 | const util = require('hexo-util'); 3 | 4 | exports.type = function (value) { 5 | return Object.prototype.toString.call(value).slice(8, -1).toLowerCase() 6 | } 7 | 8 | exports.isObject = function (value) { 9 | return exports.type(value) === 'object'; 10 | } 11 | 12 | exports.isEmptyObject = function (value) { 13 | if (!exports.isObject(value)) return false; 14 | 15 | for (const key in value) return false; 16 | return true; 17 | } 18 | 19 | exports.pick = function (obj, keys) { 20 | if (keys === undefined) { 21 | keys = obj; 22 | return process; 23 | } 24 | 25 | return process(obj); 26 | 27 | function process(o) { 28 | o = Object.assign({}, o); 29 | let ret = {}; 30 | 31 | keys.forEach(key => { 32 | o.hasOwnProperty(key) && 33 | // delete property with value `false` to save some bytes 34 | (o[key] !== false && o[key] !== undefined && o[key] !== null) && 35 | (ret[key] = o[key]); 36 | 37 | }); 38 | 39 | if (ret.tags) { 40 | if (ret.tags.length) ret.tags = ret.tags.sort('name').map(tag => tag.name); 41 | else delete ret.tags; 42 | } 43 | if (ret.categories) { 44 | if (ret.categories.length) ret.categories = ret.categories.sort('name').map(cat => cat.name); 45 | else delete ret.categories; 46 | } 47 | if (ret.prev) ret.prev = { title: ret.prev.title, link: ret.prev.link }; 48 | if (ret.next) ret.next = { title: ret.next.title, link: ret.next.link }; 49 | 50 | return ret; 51 | } 52 | } 53 | 54 | exports.md5 = function (str, len = 20) { 55 | return crypto.createHash('md5').update(str).digest('hex').substring(0, len); 56 | } 57 | 58 | exports.base64 = function (str) { 59 | return Buffer.from(str).toString('base64').replace(/=/g, ''); 60 | } 61 | 62 | /** 63 | * Remove `/*.html` 64 | * 65 | * @param {string} url 66 | * @param {boolean} keepIndex keep `index` for `index.html`, used for post_asset_folder 67 | * @returns {string} 68 | */ 69 | exports.trimHtml = function (url, keepIndex) { 70 | if (!url) return ''; 71 | url = url.split('/') 72 | 73 | const last = url.pop() 74 | if (last) { 75 | if (last === 'index.html') { 76 | if (keepIndex) url.push('index') 77 | } else { 78 | url.push(last.split('.')[0]) 79 | } 80 | } 81 | 82 | return url.join('/'); 83 | } 84 | 85 | exports.Pagination = class { 86 | /** 87 | * @param {any} config 88 | */ 89 | constructor(config) { 90 | this.config = Object.assign(config); 91 | } 92 | 93 | /** 94 | * @param {any[]} posts 95 | * @param {{ perPage: number; id?: string | Function; extend?: object; dataFn?: Function; }} options 96 | * @param {(Array<{ type: 'html' | 'json'; dataFn?: Function; id: string | Function; extend?: object }>)} generationMeta 97 | * @returns {Array<{ path: string; layout?: string; data: any }>} 98 | */ 99 | apply(posts, options, generationMeta) { 100 | const len = posts.length; 101 | const perPage = options.perPage == undefined ? 10 : options.perPage == 0 ? len : +options.perPage; 102 | const total = Math.ceil(len / perPage); 103 | const ret = []; 104 | 105 | // Merge options into meta 106 | const commonProps = ['id', 'dataFn']; 107 | generationMeta = generationMeta.map(meta => { 108 | commonProps.forEach(prop => { 109 | if (meta[prop] === undefined && options[prop] !== undefined) meta[prop] = options[prop]; 110 | }); 111 | meta.extend = Object.assign({}, options.extend || {}, meta.extend || {}); 112 | 113 | return meta; 114 | }) 115 | 116 | for (let i = 1; i <= total; i++) { 117 | const data = { 118 | per_page: perPage, 119 | total, 120 | current: i, 121 | data: posts.slice((i - 1) * perPage, i * perPage) 122 | }; 123 | 124 | ret.push(generationMeta.map(meta => this._merge(i, data, meta))) 125 | } 126 | 127 | return ret; 128 | } 129 | 130 | /** 131 | * @param {number} index 132 | * @param {object} data 133 | * @param {({ type: 'html' | 'json'; layout?: string; dataFn?: Function; id: string | Function; extend?: object })} meta 134 | * @returns {{ path: string; layout?: string; data: any }} 135 | */ 136 | _merge(index, data, meta) { 137 | const type = meta.type; 138 | const base = this.config[type]; 139 | 140 | if (!base) return; 141 | 142 | const dataFn = meta.dataFn || (o => o); 143 | const pathFn = meta.pathFn || (o => o); 144 | const layout = meta.layout || base.layout; 145 | const extend = meta.extend || {}; 146 | const id = typeof meta.id === 'function' ? 147 | meta.id : 148 | (index => (meta.id || '') + (index === 1 ? '' : `/${index}`)); 149 | 150 | return base.generateFn({ 151 | path: pathFn(id(index)), 152 | layout, 153 | data: dataFn(Object.assign(data, extend)) 154 | }) 155 | } 156 | } 157 | 158 | /** 159 | * Parses toc of post 160 | * 161 | * @param {string} html 162 | * @param {number} depth 163 | * @returns {Array<{title: string; id: string; index: string; children?: any[]}>} 164 | */ 165 | exports.parseToc = function (html, depth) { 166 | if (!html || !depth) return []; 167 | if (depth > 4) depth = 4; 168 | if (depth < 1) depth = 3; 169 | const pointer = new function () { 170 | const data = {} 171 | const parents = [] 172 | let current = null 173 | let level = -1 174 | 175 | return { 176 | get data() { 177 | return data.children 178 | }, 179 | get level() { 180 | return level 181 | }, 182 | add(item) { 183 | level = item.level 184 | current.push({ 185 | id: item.id, 186 | title: item.text, 187 | index: [ 188 | parents.length ? parents[parents.length - 1].index : '', 189 | current.length + 1 190 | ].filter(i => i).join('.') 191 | }) 192 | }, 193 | open() { 194 | const parent = current ? current[current.length - 1] : data 195 | parents.push(parent) 196 | current = parent.children = [] 197 | }, 198 | close() { 199 | parents.pop() 200 | current = parents.length 201 | ? parents[parents.length - 1].children 202 | : parents; 203 | } 204 | } 205 | } 206 | const tocObj = util.tocObj(html, { max_depth: depth }); 207 | 208 | tocObj.forEach(i => { 209 | if (!pointer.level || i.level > pointer.level) 210 | pointer.open() 211 | else if (i.level < pointer.level) { 212 | let n = pointer.level - i.level; 213 | while (n--) pointer.close() 214 | } 215 | pointer.add(i) 216 | }) 217 | 218 | return pointer.data || [] 219 | } 220 | 221 | 222 | /** 223 | * This is a non-standard and partial implementation of json schema draft-07, 224 | * therefore can not be used else where 225 | * 226 | * @param {*} schema 227 | * @param {*} data 228 | * @param {*} payload 229 | */ 230 | exports.parseConfig = function (schema, data, payload = {}) { 231 | // https://github.com/epoberezkin/ajv/blob/master/lib/compile/formats.js 232 | const regexs = { 233 | date: /^\d\d\d\d-[0-1]\d-[0-3]\d$/, 234 | // uri: /^(?:[a-z][a-z0-9+-.]*:)(?:\/?\/)?[^\s]*$/i, 235 | // 'uri-reference': /^(?:(?:[a-z][a-z0-9+-.]*:)?\/?\/)?(?:[^\\\s#][^\s#]*)?(?:#[^\\\s]*)?$/i, 236 | email: /^[a-z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)*$/i, 237 | regex: (str) => { 238 | if (/[^\\]\\Z/.test(str)) return false; 239 | try { 240 | new RegExp(str); 241 | return true; 242 | } catch (e) { 243 | return false; 244 | } 245 | } 246 | }; 247 | const { type, isEmptyObject } = exports; 248 | const definitions = {} 249 | const parsed = parse(schema, data, '#'); 250 | return parsed.value !== undefined ? parsed.value : parsed.hint; 251 | 252 | /** 253 | * 254 | * @param {*} sub schema 255 | * @param {*} subv value for validation 256 | * @param {*} paths used for definitions 257 | */ 258 | function parse(sub, subv, paths) { 259 | // merge definitions 260 | if (sub.definitions) { 261 | for (const defKey in sub.definitions) { 262 | definitions[sub.definitions[defKey].$id || paths + '/definitions/' + defKey] = sub.definitions[defKey]; 263 | } 264 | } 265 | 266 | const ret = { 267 | hint: payload[sub.default] !== undefined ? payload[sub.default] : sub.default 268 | } 269 | 270 | if (sub.$ref) { 271 | if (!definitions[sub.$ref]) return ret; 272 | // same floor 273 | return parse(definitions[sub.$ref], subv, paths); 274 | } 275 | 276 | const sType = sub.type; 277 | let vType = type(subv); 278 | 279 | // Will be assignd to a new value if subv is not a primitive value 280 | let safeSubv = subv; 281 | 282 | // object can hold a `required` key, to ensure the correct hint can be dug, 283 | // `subv` must be an object 284 | if (sType === 'object' && vType !== 'object') { 285 | if (sub.required) { 286 | subv = {} 287 | vType = sType 288 | } else return ret 289 | } 290 | 291 | // enum may not hold a type property 292 | if (sub.enum) { 293 | // enum only accepts primitive values 294 | // simply uses `Array.indexOf()` 295 | if (!~sub.enum.indexOf(subv)) return ret 296 | } 297 | else if (sType && sType !== vType) return ret 298 | else if (sType === 'string') { 299 | // minLength 300 | if (sub.minLength !== undefined && subv.length < sub.minLength) return ret 301 | // maxLength 302 | if (sub.maxLength !== undefined && subv.length > sub.maxLength) return ret 303 | // pattern 304 | if (sub.pattern && !new RegExp(sub.pattern).test(subv)) return ret 305 | // format 306 | if (sub.format) { 307 | const regex = regexs[sub.format]; 308 | const passed = regex ? typeof regex === 'function' ? regex(subv) : new RegExp(regex).test(subv) : true; 309 | if (!passed) return ret 310 | } 311 | } 312 | else if (sType === 'number') { 313 | // minimum 314 | if (sub.minimum !== undefined && subv < sub.minimum) return ret 315 | // maximum 316 | if (sub.maximum !== undefined && subv > sub.maximum) return ret 317 | } 318 | else if (sType === 'boolean') { 319 | // nothing to do 320 | } 321 | else if (sType === 'object' || 322 | sub.properties !== undefined || // properties implicit object type 323 | sub.additionalProperties !== undefined // additionalProperties implicit object type 324 | ) { 325 | // if (ret.hint === undefined) ret.hint = {} 326 | 327 | const trunk = {} 328 | const required = sub.required || [] 329 | const properties = sub.properties || {} 330 | const allKeys = Array.from(new Set(Object.keys(subv || {}).concat(Object.keys(properties)))); 331 | for (const key of allKeys) { 332 | // A specific schema for [key] 333 | if (properties[key] !== undefined) { 334 | trunk[key] = parse(properties[key], subv[key], paths + '/properties/' + key) 335 | } 336 | else { 337 | if (sub.patternProperties) { 338 | for (const pkey in sub.patternProperties) { 339 | if (new RegExp(pkey).test(key)) { 340 | // Do not put definitions inside a patternProperties 341 | trunk[key] = parse(sub.patternProperties[pkey], subv[key], paths + '/properties/' + key) 342 | continue; 343 | } 344 | } 345 | } 346 | 347 | if (sub.additionalProperties !== undefined) { 348 | // fail 349 | if (sub.additionalProperties === false) { 350 | // if (strict) return ret 351 | } else { 352 | trunk[key] = parse(sub.additionalProperties, subv[key], paths + '/additionalProperties/' + key) 353 | } 354 | } 355 | else { 356 | trunk[key] = { 357 | value: subv[key], 358 | pass: true 359 | } 360 | } 361 | } 362 | 363 | // propertyNames is missing 364 | } 365 | 366 | // properties with valid name can goes here 367 | // Note a standard implementation may failed aleady 368 | 369 | // required 370 | for (const rkey of required) { 371 | if (!trunk[rkey].pass && trunk[rkey].hint === undefined) return ret 372 | } 373 | 374 | // combine values 375 | safeSubv = {} 376 | for (const key in trunk) { 377 | if (trunk[key].pass) safeSubv[key] = trunk[key].value 378 | // attempt to use default value only if the property is required 379 | else if (~required.indexOf(key)) { 380 | if (trunk[key].hint !== undefined) 381 | safeSubv[key] = trunk[key].hint 382 | else return ret; 383 | } 384 | } 385 | 386 | // Must after the combination 387 | const propLen = Object.keys(safeSubv).length 388 | if (sub.minProperties !== undefined && propLen < sub.minProperties) return ret 389 | if (sub.maxProperties !== undefined && propLen > sub.maxProperties) { 390 | // It's hard to make a hint since object is not a indexed collection 391 | return ret 392 | } 393 | 394 | // Dependencies 395 | if (sub.dependencies) { 396 | for (const depKey in sub.dependencies) { 397 | const dep = sub.dependencies[depKey] 398 | // Schema dependencies 399 | if (type(dep) === 'object') { 400 | const result = parse(dep, safeSubv, paths + '/dependencies/' + depKey + '/') 401 | if (!result.pass) return ret 402 | } 403 | // Property dependencies 404 | else if (safeSubv[depKey] !== undefined) { 405 | for (const depTargetKey of dep) { 406 | if (safeSubv[depTargetKey] === undefined) return ret 407 | } 408 | } 409 | } 410 | } 411 | 412 | // respect empty object 413 | if (isEmptyObject(safeSubv) && !isEmptyObject(subv)) safeSubv = undefined 414 | } 415 | else if (sType === 'array') { 416 | // if (ret.hint === undefined) ret.hint = [] 417 | 418 | // List validation 419 | if (sub.contains) { 420 | if (!subv.find(v => parse(sub.contains, v, paths + '/contains').pass)) return ret 421 | } 422 | else if (sub.items) { 423 | let results = []; 424 | if (type(sub.items) === 'object') { 425 | for (const subvi of subv) { 426 | // simply pass items 427 | const result = parse(sub.items, subvi, paths + '/items') 428 | // Only concat the value 429 | if (result.pass) results.push(result.value) 430 | // In strict mode, as long as one does not pass, it will fail. 431 | // else if (strict) return ret 432 | } 433 | 434 | if (!results.length) return ret 435 | } 436 | 437 | // Tuple validation 438 | else if (type(sub.items) === 'array') { 439 | for (let i = 0; i < sub.items.length; i++) { 440 | const result = parse(sub.items[i], subv[i], paths + '/items[' + i + ']') 441 | if (!result.pass) return ret 442 | results.push(result.value) 443 | } 444 | 445 | // Note results does not contain additional items, if any 446 | 447 | if (sub.additionalItems !== false) { 448 | const additionalV = subv.slice(sub.items.length) 449 | // by default, it’s okay to add additional items to end 450 | if (sub.additionalItems === undefined) { 451 | // concat additional value to results 452 | results = results.concat(additionalV) 453 | } 454 | // additionalItems is a schema 455 | else { 456 | const result = parse({ type: 'array', items: sub.additionalItems }, additionalV, paths + '/additionalItems') 457 | if (!result.pass) return ret 458 | else { 459 | // concat additional value to results 460 | results = results.concat(result.value) 461 | } 462 | } 463 | } 464 | // `additionalItems: false` has the effect of disallowing extra items in the array. 465 | else { 466 | if (sub.items.length !== subv.length) return ret 467 | } 468 | } 469 | 470 | safeSubv = results 471 | } 472 | 473 | // below uses `safeSubv` instead of subv 474 | 475 | if (sub.minItems !== undefined && safeSubv.length < sub.minItems) return ret 476 | if (sub.maxItems !== undefined && safeSubv.length > sub.maxItems) return ret 477 | if (sub.uniqueItems !== undefined && safeSubv.length !== new Set(safeSubv.map(JSON.stringify)).size) return ret 478 | } 479 | else if (sub.oneOf || sub.anyOf || sub.allOf) { 480 | const word = sub.oneOf ? 'oneOf' : sub.anyOf ? 'anyOf' : 'allOf'; 481 | let n = 0; 482 | for (let i = 0; i < sub[word].length; i++) { 483 | const result = parse(sub[word][i], subv, paths + '/' + word + '[' + i + ']') 484 | if (result.pass) { 485 | n++; 486 | 487 | // treat oneOf as anyOf, just return the first valid value 488 | if (word === 'oneOf' || word === 'anyOf') { 489 | safeSubv = result.value; 490 | break; 491 | } 492 | } 493 | } 494 | 495 | if ( 496 | !n || 497 | (sub.oneOf && n > 1) || 498 | (sub.allOf && n !== sub.allOf.length) 499 | ) return ret 500 | } 501 | 502 | ret.value = safeSubv; 503 | if (safeSubv !== undefined) ret.pass = true; 504 | return ret 505 | } 506 | } 507 | 508 | const identifierMap = { 509 | '+': 'plus', 510 | '&': 'and' 511 | }; 512 | exports.escapeIdentifier = function (str) { 513 | if (!str) return ''; 514 | return Object.keys(identifierMap).reduce((sum, i) => sum.replace(new RegExp('\\' + i, 'g'), identifierMap[i]), str); 515 | } 516 | 517 | const localeMap = { 518 | 'zh-cn': 'zh-Hans', 519 | 'zh-hk': 'zh-Hant', 520 | 'zh-tw': 'zh-Hant', 521 | 'en': 'en', 522 | 'ja': 'ja', 523 | }; 524 | const oldLocaleMap = { 525 | 'zh-Hans': 'zh-cn', 526 | 'zh-Hant': 'zh-hk', 527 | 'en': 'en', 528 | 'ja': 'ja', 529 | }; 530 | 531 | /** 532 | * Convert language code to ISO 639-1 533 | * 534 | * @param {string | string[]} ids 535 | * @param {boolean} toOld reverse 536 | * @returns {string} 537 | */ 538 | exports.localeId = function (ids, toOld) { 539 | const id = ids ? typeof ids === 'string' ? ids : ids[0] : ''; 540 | const lowerCasedId = id ? id.toLowerCase() : ''; 541 | 542 | if (toOld) 543 | return localeMap[lowerCasedId] !== undefined ? id : oldLocaleMap[id] || oldLocaleMap.en; 544 | 545 | return oldLocaleMap[id] !== undefined ? id : localeMap[lowerCasedId] || localeMap.en; 546 | } 547 | 548 | /** 549 | * Transform with babel, and minify with terser 550 | * 551 | * @param {string} code 552 | * @returns {string} 553 | */ 554 | exports.parseJs = jsParser(); 555 | 556 | /** 557 | * Minify HTML and CSS with html-minifier 558 | * 559 | * @param {string} code 560 | * @returns {string} 561 | */ 562 | exports.minifyHtml = htmlMinifier(); 563 | 564 | function jsParser() { 565 | let terser; 566 | try { 567 | terser = require('terser'); 568 | } catch (e) { return i => i || '' } 569 | 570 | const minify = terser.minify; 571 | 572 | return function (code) { 573 | if (!code || typeof code !== 'string') return ''; 574 | 575 | code = minify(code); 576 | if (code) code = code.code; 577 | else return ''; 578 | 579 | return code; 580 | } 581 | } 582 | 583 | function htmlMinifier() { 584 | let htmlMinifier; 585 | try { 586 | htmlMinifier = require('html-minifier').minify; 587 | } catch (e) { return i => i || '' } 588 | 589 | const options = { 590 | minifyCSS: true, 591 | collapseWhitespace: true, 592 | removeEmptyAttributes: true, 593 | removeComments: true 594 | }; 595 | 596 | return function (code) { 597 | if (!code || typeof code !== 'string') return ''; 598 | 599 | return htmlMinifier(code, options); 600 | } 601 | } 602 | 603 | /** 604 | * Wrap minified code with template 605 | * 606 | * Example: 607 | * 608 | * snippet('') => '' 609 | * 610 | * snippet('code') 611 | * => `` 612 | * 613 | * snippet('', '') 614 | * => `` 615 | * 616 | * snippet('code', code => ``) 617 | * => `` 618 | * 619 | * @param {string} code 620 | * @param {(string | ((code: string) => string))} template 621 | * @returns {string} 622 | */ 623 | exports.snippet = function (code, template = code => ``) { 624 | let content = ''; 625 | 626 | // ignore code if template is string 627 | if (typeof template === 'string') { 628 | content = template; 629 | } 630 | 631 | // template is function which relay on code 632 | else if (code) { 633 | content = template(exports.parseJs(`(function(){${code}})();`)); 634 | } 635 | 636 | return content || ''; 637 | } 638 | 639 | /** 640 | * Hide posts/pages from being indexed when visible=false 641 | * 642 | * @param {Post | Page} p 643 | * @returns {boolean} 644 | */ 645 | exports.visible = function (p) { 646 | return p.visible !== false; 647 | } 648 | 649 | /** 650 | * Make sure not to process unpublished articles 651 | * 652 | * @param {Post | Page} p 653 | * @returns {boolean} 654 | */ 655 | exports.published = function (p) { 656 | return Boolean(~['post', 'page'].indexOf(p.layout)); 657 | } 658 | 659 | /** 660 | * Parse css background, only support hex color 661 | * #fff url => { color: '#fff', image: url } 662 | * 663 | * @param {string} value 664 | * @return {{color?: string; image?: string}} 665 | */ 666 | exports.parseBackground = function (value) { 667 | if (!value) return {} 668 | const color_hex_regex = /^#(?:[0-9a-fA-F]{3}){1,2}$/ 669 | const part = value.split(/\s+/) 670 | const ret = {} 671 | 672 | // color at start 673 | if (color_hex_regex.test(part[0])) 674 | return { 675 | color: part[0], 676 | image: part.slice(1).join(' ') 677 | } 678 | 679 | // color at end 680 | const lastIndex = part.length - 1 681 | if (part[lastIndex] && color_hex_regex.test(part[lastIndex])) 682 | return { 683 | color: part.pop(), 684 | image: part.join(' ') 685 | } 686 | 687 | return { 688 | image: value 689 | } 690 | } 691 | 692 | /** 693 | * @param {string} link 694 | * @returns {boolean} 695 | */ 696 | exports.isExternal = function (link) { 697 | return /^(\w+:)?\/\//.test(link); 698 | } 699 | 700 | 701 | const selfClosingTags = ['img', 'input', 'link']; 702 | /** 703 | * @param {keyof HTMLElementTagNameMap} tag 704 | * @param {any} attrs 705 | * @param {string} text 706 | * @returns {string} 707 | */ 708 | exports.htmlTag = function (tag, attrs = {}, text) { 709 | if (tag === 'link') { 710 | if (!attrs.href) return ''; 711 | if (!attrs.rel) attrs.rel = 'stylesheet'; 712 | } 713 | else if (tag === 'style') { 714 | if (!text) return ''; 715 | } 716 | else if (tag === 'script') { 717 | if (attrs.src) text = ''; 718 | else if (text) { 719 | if (attrs.type === undefined) { 720 | text = exports.parseJs(`(function(){${text}})();`); 721 | if (!text) return ''; 722 | } 723 | } 724 | else return ''; 725 | } 726 | 727 | let temp = []; 728 | for (const k in attrs) { 729 | const v = attrs[k] 730 | if (v || v === 0) { 731 | temp.push(v !== true ? `${k}="${v}"` : k) 732 | } 733 | } 734 | 735 | temp = temp.join(' '); 736 | if (temp) temp = ' ' + temp; 737 | 738 | return exports.minifyHtml(selfClosingTags.includes(tag) 739 | ? `<${tag + temp}>` 740 | : `<${tag + temp}>${text || ''}`); 741 | } 742 | 743 | /** 744 | * "A picture | block | key: value" 745 | * => { value: "A picture", options: { block: true, key: value } } 746 | * "| block" 747 | * => { options: { block: true } } 748 | * 749 | * @param {string} value 750 | * @returns {{ value: string, options: any }} 751 | */ 752 | exports.parsePipe = function (value) { 753 | const ret = { options: {} }; 754 | 755 | if (!value) return ret; 756 | 757 | const partial = value.split('|').map((i) => i.trim()); 758 | 759 | if (partial[0]) ret.value = partial[0]; 760 | 761 | partial.slice(1).forEach((p) => { 762 | const [k, v] = p.split(':').map((i) => i.trim()); 763 | if (k) { 764 | ret.options[k] = v || true; 765 | } 766 | }); 767 | 768 | return ret; 769 | } 770 | 771 | /** 772 | * @param {ArrayLike} list 773 | * @param {(arg: any) => Promise} fn 774 | * @returns {Promise[]} 775 | */ 776 | exports.asyncMap = function(list, fn) { 777 | if (!list.length) return Promise.resolve([]); 778 | 779 | const ret = []; 780 | 781 | return run(); 782 | 783 | function run() { 784 | return fn(list.shift()).then(result => { 785 | ret.push(result); 786 | if (list.length) return run(); 787 | return ret; 788 | }) 789 | } 790 | } 791 | 792 | const cjk_regex = /[a-zA-Z0-9_\u0392-\u03c9\u00c0-\u00ff\u0600-\u06ff\u0400-\u04ff]+|[\u4e00-\u9fff\u3400-\u4dbf\uf900-\ufaff\u3040-\u309f\uac00-\ud7af]+/g; 793 | /** 794 | * https://github.com/yuehu/word-count/blob/master/index.js 795 | * 796 | * @param {string} text 797 | * @returns {number} 798 | */ 799 | exports.countWord = function(text) { 800 | const m = text.match(cjk_regex); 801 | let count = 0; 802 | if (!m) return 0; 803 | for (let i = 0, mLen = m.length; i < mLen; i++) { 804 | count += m[i].charCodeAt(0) >= 0x4e00 ? m[i].length : 1; 805 | } 806 | return count; 807 | } 808 | 809 | /** 810 | * @param {string} template 811 | * @param {(string | number)[] | { [key: string]: string | number } | number | string} payload 812 | * @returns {string} 813 | */ 814 | exports.sprintf = function(template, payload) { 815 | if (!payload) return template; 816 | else if (Array.isArray(payload)) { 817 | let i = 0; 818 | return template.replace(/%(s|d)/g, () => payload[i++]); 819 | } else if (typeof payload === 'string' || typeof payload === 'number') { 820 | return sprintf(template, [payload]); 821 | } else { 822 | for (const key in payload) { 823 | template = template.replace(new RegExp(':' + key, 'g'), payload[key]); 824 | } 825 | return template; 826 | } 827 | } 828 | 829 | // https://github.com/hexojs/hexo-i18n 830 | exports.flattenObject = function(data, obj = {}, parent = '') { 831 | if (!data) return; 832 | const keys = Object.keys(data); 833 | 834 | for (let i = 0, len = keys.length; i < len; i++) { 835 | const key = keys[i]; 836 | const item = data[key]; 837 | 838 | if (exports.type(item) === 'object') { 839 | exports.flattenObject(item, obj, parent + key + '.'); 840 | } else { 841 | obj[parent + key] = item; 842 | } 843 | } 844 | 845 | return obj; 846 | } 847 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hexo-theme-inside", 3 | "version": "2.7.1", 4 | "description": "SPA, flat and clean theme for Hexo.", 5 | "scripts": { 6 | "test": "jasmine --config=test/jasmine.json" 7 | }, 8 | "repository": { 9 | "type": "git", 10 | "url": "git+https://github.com/ikeq/hexo-theme-inside.git" 11 | }, 12 | "keywords": [ 13 | "hexo", 14 | "hexo theme", 15 | "spa" 16 | ], 17 | "author": "ikeq ", 18 | "maintainers": [ 19 | "ikeq " 20 | ], 21 | "license": "MIT", 22 | "bugs": { 23 | "url": "https://github.com/ikeq/hexo-theme-inside/issues" 24 | }, 25 | "homepage": "https://github.com/ikeq/hexo-theme-inside#readme", 26 | "dependencies": { 27 | "html-to-text": "^9.0.5", 28 | "markdown-it": "^14.1.0", 29 | "markdown-it-container": "^4.0.0" 30 | }, 31 | "devDependencies": { 32 | "cheerio": "^1.0.0", 33 | "hexo": "^7.3.0", 34 | "jasmine": "^5.3.0", 35 | "terser": "^5.32.0" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /scripts/index.js: -------------------------------------------------------------------------------- 1 | require('../lib/config')(hexo); 2 | require('../lib/renderer')(hexo); 3 | require('../lib/helper')(hexo); 4 | require('../lib/generator')(hexo); 5 | require('../lib/filter')(hexo); 6 | require('../lib/tag')(hexo); 7 | -------------------------------------------------------------------------------- /source/3rdpartylicenses.txt: -------------------------------------------------------------------------------- 1 | @angular/common 2 | MIT 3 | 4 | @angular/core 5 | MIT 6 | 7 | @angular/platform-browser 8 | MIT 9 | 10 | @angular/router 11 | MIT 12 | 13 | hexo-theme-inside-ng 14 | MIT 15 | 16 | rxjs 17 | Apache-2.0 18 | Apache License 19 | Version 2.0, January 2004 20 | http://www.apache.org/licenses/ 21 | 22 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 23 | 24 | 1. Definitions. 25 | 26 | "License" shall mean the terms and conditions for use, reproduction, 27 | and distribution as defined by Sections 1 through 9 of this document. 28 | 29 | "Licensor" shall mean the copyright owner or entity authorized by 30 | the copyright owner that is granting the License. 31 | 32 | "Legal Entity" shall mean the union of the acting entity and all 33 | other entities that control, are controlled by, or are under common 34 | control with that entity. For the purposes of this definition, 35 | "control" means (i) the power, direct or indirect, to cause the 36 | direction or management of such entity, whether by contract or 37 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 38 | outstanding shares, or (iii) beneficial ownership of such entity. 39 | 40 | "You" (or "Your") shall mean an individual or Legal Entity 41 | exercising permissions granted by this License. 42 | 43 | "Source" form shall mean the preferred form for making modifications, 44 | including but not limited to software source code, documentation 45 | source, and configuration files. 46 | 47 | "Object" form shall mean any form resulting from mechanical 48 | transformation or translation of a Source form, including but 49 | not limited to compiled object code, generated documentation, 50 | and conversions to other media types. 51 | 52 | "Work" shall mean the work of authorship, whether in Source or 53 | Object form, made available under the License, as indicated by a 54 | copyright notice that is included in or attached to the work 55 | (an example is provided in the Appendix below). 56 | 57 | "Derivative Works" shall mean any work, whether in Source or Object 58 | form, that is based on (or derived from) the Work and for which the 59 | editorial revisions, annotations, elaborations, or other modifications 60 | represent, as a whole, an original work of authorship. For the purposes 61 | of this License, Derivative Works shall not include works that remain 62 | separable from, or merely link (or bind by name) to the interfaces of, 63 | the Work and Derivative Works thereof. 64 | 65 | "Contribution" shall mean any work of authorship, including 66 | the original version of the Work and any modifications or additions 67 | to that Work or Derivative Works thereof, that is intentionally 68 | submitted to Licensor for inclusion in the Work by the copyright owner 69 | or by an individual or Legal Entity authorized to submit on behalf of 70 | the copyright owner. For the purposes of this definition, "submitted" 71 | means any form of electronic, verbal, or written communication sent 72 | to the Licensor or its representatives, including but not limited to 73 | communication on electronic mailing lists, source code control systems, 74 | and issue tracking systems that are managed by, or on behalf of, the 75 | Licensor for the purpose of discussing and improving the Work, but 76 | excluding communication that is conspicuously marked or otherwise 77 | designated in writing by the copyright owner as "Not a Contribution." 78 | 79 | "Contributor" shall mean Licensor and any individual or Legal Entity 80 | on behalf of whom a Contribution has been received by Licensor and 81 | subsequently incorporated within the Work. 82 | 83 | 2. Grant of Copyright License. Subject to the terms and conditions of 84 | this License, each Contributor hereby grants to You a perpetual, 85 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 86 | copyright license to reproduce, prepare Derivative Works of, 87 | publicly display, publicly perform, sublicense, and distribute the 88 | Work and such Derivative Works in Source or Object form. 89 | 90 | 3. Grant of Patent License. Subject to the terms and conditions of 91 | this License, each Contributor hereby grants to You a perpetual, 92 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 93 | (except as stated in this section) patent license to make, have made, 94 | use, offer to sell, sell, import, and otherwise transfer the Work, 95 | where such license applies only to those patent claims licensable 96 | by such Contributor that are necessarily infringed by their 97 | Contribution(s) alone or by combination of their Contribution(s) 98 | with the Work to which such Contribution(s) was submitted. If You 99 | institute patent litigation against any entity (including a 100 | cross-claim or counterclaim in a lawsuit) alleging that the Work 101 | or a Contribution incorporated within the Work constitutes direct 102 | or contributory patent infringement, then any patent licenses 103 | granted to You under this License for that Work shall terminate 104 | as of the date such litigation is filed. 105 | 106 | 4. Redistribution. You may reproduce and distribute copies of the 107 | Work or Derivative Works thereof in any medium, with or without 108 | modifications, and in Source or Object form, provided that You 109 | meet the following conditions: 110 | 111 | (a) You must give any other recipients of the Work or 112 | Derivative Works a copy of this License; and 113 | 114 | (b) You must cause any modified files to carry prominent notices 115 | stating that You changed the files; and 116 | 117 | (c) You must retain, in the Source form of any Derivative Works 118 | that You distribute, all copyright, patent, trademark, and 119 | attribution notices from the Source form of the Work, 120 | excluding those notices that do not pertain to any part of 121 | the Derivative Works; and 122 | 123 | (d) If the Work includes a "NOTICE" text file as part of its 124 | distribution, then any Derivative Works that You distribute must 125 | include a readable copy of the attribution notices contained 126 | within such NOTICE file, excluding those notices that do not 127 | pertain to any part of the Derivative Works, in at least one 128 | of the following places: within a NOTICE text file distributed 129 | as part of the Derivative Works; within the Source form or 130 | documentation, if provided along with the Derivative Works; or, 131 | within a display generated by the Derivative Works, if and 132 | wherever such third-party notices normally appear. The contents 133 | of the NOTICE file are for informational purposes only and 134 | do not modify the License. You may add Your own attribution 135 | notices within Derivative Works that You distribute, alongside 136 | or as an addendum to the NOTICE text from the Work, provided 137 | that such additional attribution notices cannot be construed 138 | as modifying the License. 139 | 140 | You may add Your own copyright statement to Your modifications and 141 | may provide additional or different license terms and conditions 142 | for use, reproduction, or distribution of Your modifications, or 143 | for any such Derivative Works as a whole, provided Your use, 144 | reproduction, and distribution of the Work otherwise complies with 145 | the conditions stated in this License. 146 | 147 | 5. Submission of Contributions. Unless You explicitly state otherwise, 148 | any Contribution intentionally submitted for inclusion in the Work 149 | by You to the Licensor shall be under the terms and conditions of 150 | this License, without any additional terms or conditions. 151 | Notwithstanding the above, nothing herein shall supersede or modify 152 | the terms of any separate license agreement you may have executed 153 | with Licensor regarding such Contributions. 154 | 155 | 6. Trademarks. This License does not grant permission to use the trade 156 | names, trademarks, service marks, or product names of the Licensor, 157 | except as required for reasonable and customary use in describing the 158 | origin of the Work and reproducing the content of the NOTICE file. 159 | 160 | 7. Disclaimer of Warranty. Unless required by applicable law or 161 | agreed to in writing, Licensor provides the Work (and each 162 | Contributor provides its Contributions) on an "AS IS" BASIS, 163 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 164 | implied, including, without limitation, any warranties or conditions 165 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 166 | PARTICULAR PURPOSE. You are solely responsible for determining the 167 | appropriateness of using or redistributing the Work and assume any 168 | risks associated with Your exercise of permissions under this License. 169 | 170 | 8. Limitation of Liability. In no event and under no legal theory, 171 | whether in tort (including negligence), contract, or otherwise, 172 | unless required by applicable law (such as deliberate and grossly 173 | negligent acts) or agreed to in writing, shall any Contributor be 174 | liable to You for damages, including any direct, indirect, special, 175 | incidental, or consequential damages of any character arising as a 176 | result of this License or out of the use or inability to use the 177 | Work (including but not limited to damages for loss of goodwill, 178 | work stoppage, computer failure or malfunction, or any and all 179 | other commercial damages or losses), even if such Contributor 180 | has been advised of the possibility of such damages. 181 | 182 | 9. Accepting Warranty or Additional Liability. While redistributing 183 | the Work or Derivative Works thereof, You may choose to offer, 184 | and charge a fee for, acceptance of support, warranty, indemnity, 185 | or other liability obligations and/or rights consistent with this 186 | License. However, in accepting such obligations, You may act only 187 | on Your own behalf and on Your sole responsibility, not on behalf 188 | of any other Contributor, and only if You agree to indemnify, 189 | defend, and hold each Contributor harmless for any liability 190 | incurred by, or claims asserted against, such Contributor by reason 191 | of your accepting any such warranty or additional liability. 192 | 193 | END OF TERMS AND CONDITIONS 194 | 195 | APPENDIX: How to apply the Apache License to your work. 196 | 197 | To apply the Apache License to your work, attach the following 198 | boilerplate notice, with the fields enclosed by brackets "[]" 199 | replaced with your own identifying information. (Don't include 200 | the brackets!) The text should be enclosed in the appropriate 201 | comment syntax for the file format. We also recommend that a 202 | file or class name and description of purpose be included on the 203 | same "printed page" as the copyright notice for easier 204 | identification within third-party archives. 205 | 206 | Copyright (c) 2015-2018 Google, Inc., Netflix, Inc., Microsoft Corp. and contributors 207 | 208 | Licensed under the Apache License, Version 2.0 (the "License"); 209 | you may not use this file except in compliance with the License. 210 | You may obtain a copy of the License at 211 | 212 | http://www.apache.org/licenses/LICENSE-2.0 213 | 214 | Unless required by applicable law or agreed to in writing, software 215 | distributed under the License is distributed on an "AS IS" BASIS, 216 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 217 | See the License for the specific language governing permissions and 218 | limitations under the License. 219 | 220 | 221 | 222 | tslib 223 | 0BSD 224 | Copyright (c) Microsoft Corporation. 225 | 226 | Permission to use, copy, modify, and/or distribute this software for any 227 | purpose with or without fee is hereby granted. 228 | 229 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 230 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY 231 | AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 232 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 233 | LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR 234 | OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 235 | PERFORMANCE OF THIS SOFTWARE. 236 | 237 | zone.js 238 | MIT 239 | The MIT License 240 | 241 | Copyright (c) 2010-2022 Google LLC. https://angular.io/license 242 | 243 | Permission is hereby granted, free of charge, to any person obtaining a copy 244 | of this software and associated documentation files (the "Software"), to deal 245 | in the Software without restriction, including without limitation the rights 246 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 247 | copies of the Software, and to permit persons to whom the Software is 248 | furnished to do so, subject to the following conditions: 249 | 250 | The above copyright notice and this permission notice shall be included in 251 | all copies or substantial portions of the Software. 252 | 253 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 254 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 255 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 256 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 257 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 258 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 259 | THE SOFTWARE. 260 | -------------------------------------------------------------------------------- /source/_manifest.json: -------------------------------------------------------------------------------- 1 | {"root":"is-a","styles":["styles.79f9d555e464cb1b669d.css"],"scripts":["runtime.a95bc83b747d4636.js","polyfills.13e521fb4f0cbc90.js","main.f5cfbba069c3444b.js"],"class":{"blockimg":"φbp","bounded":"φbq","checklist":"φbr","table":"φbs","timeline":"φbt","tree":"φbu","tree--arrow":"φbv","tree--circle":"φbw","tree--square":"φbx"},"theme":"theme.97aefb00.js"} 2 | -------------------------------------------------------------------------------- /source/runtime.a95bc83b747d4636.js: -------------------------------------------------------------------------------- 1 | (()=>{"use strict";var e,i={},_={};function n(e){var a=_[e];if(void 0!==a)return a.exports;var r=_[e]={exports:{}};return i[e](r,r.exports,n),r.exports}n.m=i,e=[],n.O=(a,r,u,l)=>{if(!r){var c=1/0;for(f=0;f=l)&&Object.keys(n.O).every(h=>n.O[h](r[t]))?r.splice(t--,1):(o=!1,l0&&e[f-1][2]>l;f--)e[f]=e[f-1];e[f]=[r,u,l]},n.n=e=>{var a=e&&e.__esModule?()=>e.default:()=>e;return n.d(a,{a}),a},n.d=(e,a)=>{for(var r in a)n.o(a,r)&&!n.o(e,r)&&Object.defineProperty(e,r,{enumerable:!0,get:a[r]})},n.o=(e,a)=>Object.prototype.hasOwnProperty.call(e,a),(()=>{var e={666:0};n.O.j=u=>0===e[u];var a=(u,l)=>{var t,s,[f,c,o]=l,v=0;if(f.some(b=>0!==e[b])){for(t in c)n.o(c,t)&&(n.m[t]=c[t]);if(o)var d=o(n)}for(u&&u(l);v{localStorage.removeItem(o),l(Object.assign(Object.assign({},window[o].theme.default),{name:"default"}),!0)};e&&e.hash===(window[o]||{}).hash?l(e.theme):t()}function a(){const e=localStorage.getItem(o);if(e)try{return JSON.parse(e)}catch(e){}}function l(e,t){var n,r;let c=document.querySelector('style[is="theme"]');const i=document.querySelector('meta[name="theme-color"]'),l=window[o]||{};c||(c=document.createElement("style"),c.setAttribute("is","theme"),document.body.appendChild(c));const d="dark"===(e.name||(null===(r=null===(n=a())||void 0===n?void 0:n.theme)||void 0===r?void 0:r.name))?"dark":"default";l.theme[d]=Object.assign({},l.theme.default,l.theme[d],e),l.color=[b(l.theme[d].sidebar_background).color||l.theme[d].accent_color].concat(b(l.theme[d].background).color||[]),t&&c.innerHTML||(c.innerHTML=f(l.theme[d]),i&&(i.content=l.color[l.color.length-1])),localStorage.setItem(o,JSON.stringify({theme:l.theme[d],hash:l.hash}))}function f(e){if(!e)return"";const o=function(e,t){const n={},{accent_color:o,foreground_color:r,border_color:c,background:i,sidebar_background:a,card_background:l,content_width:f,highlight:u}=e,g=function(e,t){var n={};for(var o in e)Object.prototype.hasOwnProperty.call(e,o)&&t.indexOf(o)<0&&(n[o]=e[o]);if(null!=e&&"function"==typeof Object.getOwnPropertySymbols){var r=0;for(o=Object.getOwnPropertySymbols(e);rs(e).hex)).filter((e=>e)),n.highlight.length<16&&(n.highlight=t.highlight)):n.highlight=t.highlight;return n}(e,c),r=(o.card_background.match(t)||o.card_background.match(n)||[])[0]||o.foreground_color,i=s(o.accent_color);return`html{${function(e={},t="var"){return n(e);function n(e,o=[],r){for(const c in e){const i=e[c];if(Array.isArray(i))i.forEach(((e,t)=>{let r=t.toString(16);return r.length<2&&(r=`0${r}`),n({[r]:e},o,c)}));else if("object"==typeof i)n(i,o,c);else{const e=(r?r+"_":"")+c;o.push([`--${t}-${e.replace(/[\._]/g,"-")}`,i,e])}}return o}}(Object.assign(Object.assign({},o),{card_color:r,accent_color_005:`rgba(${i.r},${i.g},${i.b},.05)`,accent_color_01:`rgba(${i.r},${i.g},${i.b},.1)`,accent_color_02:`rgba(${i.r},${i.g},${i.b},.2)`,accent_color_04:`rgba(${i.r},${i.g},${i.b},.4)`,accent_color_08:`rgba(${i.r},${i.g},${i.b},.8)`,accent_color:i.hex}),"inside").map((e=>e.slice(0,2).join(":"))).join(";")}}`}function d(e){const t=/(^data:image)|(^[^\(^'^"]*\.(jpg|png|gif|svg))/;return e.split(/\s+/).map((e=>e.match(t)?`url(${e})`:e)).join(" ")}function s(e,o){if(e=(e||"").trim(),t.test(e))return Object.assign({hex:e},g(e));if(n.test(e)){const t=e.match(n).slice(1,4).map((e=>+e)).filter((e=>e<256));if(3===t.length)return{hex:u.apply(null,t),r:t[0],g:t[1],b:t[2]}}return o?Object.assign({hex:o},g(o)):{}}function u(e,t,n){return"#"+((1<<24)+(e<<16)+(t<<8)+n).toString(16).slice(1)}function g(e){e=e.replace(/^#?([a-f\d])([a-f\d])([a-f\d])$/i,(function(e,t,n,o){return t+t+n+n+o+o}));const t=/^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(e);return t?{r:parseInt(t[1],16),g:parseInt(t[2],16),b:parseInt(t[3],16)}:null}function h(e,t=[]){const n=e?e.split(",").map((e=>e.trim())).filter((e=>e)):[];return Array.from(new Set(n.concat(t)))}function b(e){if(!e)return{};const n=e.split(/\s+/);if(t.test(n[0]))return{color:n[0],image:n.slice(1).join(" ")};const o=n.length-1;return n[o]&&t.test(n[o])?{color:n.pop(),image:n.join(" ")}:{image:e}}e.css=f,Object.defineProperty(e,"__esModule",{value:!0})})); 2 | -------------------------------------------------------------------------------- /test/README.md: -------------------------------------------------------------------------------- 1 | # hexo-theme-inside test case 2 | 3 | ## Start 4 | 5 | Inside the theme folder and run: 6 | 7 | ```bash 8 | npm install 9 | ``` 10 | 11 | Then run the test: 12 | 13 | ```bash 14 | npm test 15 | ``` 16 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | describe('hexo-theme-inside', function () { 2 | require('./scripts/utils'); 3 | require('./scripts/filters'); 4 | require('./scripts/tags'); 5 | require('./scripts/helpers'); 6 | require('./scripts/renderers'); 7 | }); 8 | -------------------------------------------------------------------------------- /test/jasmine.json: -------------------------------------------------------------------------------- 1 | { 2 | "spec_dir": "test", 3 | "spec_files": [ 4 | "index.js" 5 | ], 6 | "helpers": [ 7 | "helpers/**/*.js" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /test/scripts/filters/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Filters', function () { 4 | require('./template'); 5 | require('./post'); 6 | }); 7 | -------------------------------------------------------------------------------- /test/scripts/filters/post.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('post', function () { 4 | const filterPath = require.resolve('../../../lib/filter/post'); 5 | const post = { 6 | call(ctx, arg) { 7 | delete require.cache[filterPath] 8 | return require(filterPath).call(ctx, arg) 9 | } 10 | } 11 | 12 | beforeEach(function () { 13 | this.ctx = { 14 | theme: { 15 | config: { 16 | assets: { prefix: 'https://sample.com', suffix: '?q=80' }, 17 | comments: {}, 18 | reward: {}, 19 | toc: {}, 20 | copyright: { 21 | license: 'a' 22 | }, 23 | post: { reward: true, toc: true, copyright: true }, 24 | page: { reward: true, toc: true, copyright: true }, 25 | runtime: { 26 | dateHelper: o => o, 27 | uriReplacer: o => o, 28 | hasReward: true, 29 | hasComments: true, 30 | hasToc: true, 31 | copyright: { 32 | license: 'a' 33 | }, 34 | } 35 | } 36 | }, 37 | config: { 38 | url: '//example.com' 39 | } 40 | }; 41 | }); 42 | 43 | it('specify type', function () { 44 | const data = { 45 | layout: 'post', 46 | excerpt: '', 47 | content: '' 48 | } 49 | post.call(this.ctx, data); 50 | expect(data.type).toBe('post'); 51 | }) 52 | 53 | it('link', function () { 54 | const data = { 55 | layout: 'post', 56 | excerpt: '', 57 | content: '', 58 | slug: 'test', 59 | path: 'post/test' 60 | } 61 | post.call(this.ctx, data); 62 | expect(data.link).toBe('post/test'); 63 | expect(data.plink).toBe('//example.com/post/test/'); 64 | 65 | data.layout = 'page'; 66 | data.path = 'test' 67 | data.source = 'test/index.md'; 68 | post.call(this.ctx, data); 69 | expect(data.link).toBe('test'); 70 | expect(data.plink).toBe('//example.com/test/'); 71 | }) 72 | 73 | it('comments', function () { 74 | const data = { 75 | layout: 'post', 76 | excerpt: '', 77 | content: '', 78 | comments: true 79 | }; 80 | 81 | post.call(this.ctx, data); 82 | expect(data.comments).toBe(true); 83 | 84 | // local comments disabled 85 | data.comments = false; 86 | post.call(this.ctx, data); 87 | expect(data.comments).toBe(false); 88 | }) 89 | 90 | it('reward', function () { 91 | const data = { 92 | layout: 'post', 93 | excerpt: '', 94 | content: '' 95 | }; 96 | 97 | post.call(this.ctx, data); 98 | expect(data.reward).toBe(true); 99 | 100 | // local reward disabled 101 | data.reward = false; 102 | post.call(this.ctx, data); 103 | expect(data.reward).toBe(false); 104 | }); 105 | 106 | it('copyright', function () { 107 | const data = { 108 | layout: 'post', 109 | excerpt: '', 110 | content: '' 111 | }; 112 | 113 | post.call(this.ctx, data); 114 | expect(data.copyright).toEqual({ license: 'a' }); 115 | 116 | data.copyright = { license: 'b' }; 117 | post.call(this.ctx, data); 118 | expect(data.copyright).toEqual({ license: 'b' }); 119 | }); 120 | 121 | it('escape with data:image', function () { 122 | const data = { 123 | layout: 'post', 124 | thumbnail: 'data:image', 125 | excerpt: '', 126 | content: '

inline

', 127 | }; 128 | 129 | post.call(this.ctx, data); 130 | 131 | expect(data.thumbnail).toBe('data:image') 132 | expect(data.content).toBe('

inline

') 133 | }); 134 | 135 | it('escape with absolute path', function () { 136 | const data = { 137 | layout: 'post', 138 | thumbnail: 'https://abc.com', 139 | excerpt: '', 140 | content: '

inline

', 141 | }; 142 | 143 | post.call(this.ctx, data); 144 | 145 | expect(data.thumbnail).toBe('https://abc.com') 146 | expect(data.content).toBe('

inline

') 147 | }); 148 | 149 | it('parses color', function () { 150 | const data = { 151 | layout: 'post', 152 | thumbnail: 'img/sample.jpg #000', 153 | excerpt: '', 154 | content: '', 155 | }; 156 | 157 | post.call(this.ctx, data); 158 | 159 | expect(data.thumbnail).toBe('img/sample.jpg') 160 | expect(data.color).toBe('#000') 161 | }); 162 | 163 | it('don\'t parses color when `color` is specified', function () { 164 | const data = { 165 | layout: 'post', 166 | thumbnail: 'img/sample.jpg #000', 167 | color: '#fff', 168 | excerpt: '', 169 | content: '', 170 | }; 171 | 172 | post.call(this.ctx, data); 173 | 174 | expect(data.thumbnail).toBe('img/sample.jpg') 175 | expect(data.color).toBe('#fff') 176 | }); 177 | }); 178 | -------------------------------------------------------------------------------- /test/scripts/filters/template.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('template', function () { 4 | const Hexo = require('hexo'); 5 | const hexo = new Hexo(); 6 | const templates = require('../../../lib/filter/templates').bind(hexo); 7 | 8 | Object.assign(hexo.config, { 9 | title: 'Blog', 10 | language: 'en' 11 | }) 12 | hexo.theme.i18n.set('en', { 13 | 'title.archives': 'Archives', 14 | 'title.categories': 'Categories', 15 | 'title.tags': 'Tags', 16 | }); 17 | 18 | it('change title', function () { 19 | const config = hexo.config 20 | const data = { 21 | post: { page: { type: 'post', title: 'Hello word' }, config }, 22 | page: { page: { type: 'page', title: 'About' }, config }, 23 | posts: { page: { type: 'posts' }, config }, 24 | archives: { page: { type: 'archives' }, config }, 25 | categories: { page: { type: 'categories' }, config }, 26 | tags: { page: { type: 'tags' }, config }, 27 | }; 28 | 29 | Object.keys(data).forEach(key => templates(data[key])); 30 | 31 | expect(data.post.title).toBe('Hello word - Blog'); 32 | expect(data.posts.title).toBe('Blog'); 33 | expect(data.page.title).toBe('About - Blog'); 34 | expect(data.archives.title).toBe('Archives - Blog'); 35 | expect(data.categories.title).toBe('Categories - Blog'); 36 | expect(data.tags.title).toBe('Tags - Blog'); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /test/scripts/helpers/ga.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const cheerio = require('cheerio'); 4 | 5 | describe('ga', function () { 6 | const ga = require('../../../lib/helper/ga'); 7 | 8 | it('create ga script tag', function () { 9 | const $ = cheerio.load(ga('foo')); 10 | 11 | expect($('script').eq(0).attr('src')).toBe('//www.googletagmanager.com/gtag/js?id=foo'); 12 | expect($('script').eq(1)).not.toBeNull(); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /test/scripts/helpers/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Helpers', function () { 4 | require('./ga'); 5 | require('./url_trim'); 6 | require('./structured_data'); 7 | }); 8 | -------------------------------------------------------------------------------- /test/scripts/helpers/structured_data.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const cheerio = require('cheerio'); 4 | const urlFor = require('hexo/lib/plugins/helper/url_for'); 5 | 6 | describe('structured_data', function () { 7 | const Hexo = require('hexo'); 8 | const hexo = new Hexo(); 9 | const ctx = { 10 | url_for: urlFor.bind(hexo), 11 | config: hexo.config, 12 | theme: { profile: {}, sns: {} } 13 | }; 14 | const structuredData = require('../../../lib/helper/structured_data').bind(ctx); 15 | 16 | it('generate WebSite entry', function () { 17 | const $ = cheerio.load(structuredData({})); 18 | const json = JSON.parse($('script').html()); 19 | 20 | expect(json.length).toBe(1); 21 | expect(json[0]['@type']).toBe('WebSite'); 22 | }); 23 | 24 | it('generate additional Article entry for post page', function () { 25 | const $ = cheerio.load(structuredData({ type: 'post', categories: { toArray() { return [] } } })); 26 | const json = JSON.parse($('script').html()); 27 | 28 | expect(json.length).toBe(2); 29 | expect(json[0]['@type']).toBe('WebSite'); 30 | expect(json[1]['@type']).toBe('Article'); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /test/scripts/helpers/url_trim.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('url_trim', function () { 4 | const urlTrim = require('../../../lib/helper/url_trim'); 5 | 6 | it('trim `index.html` in the end', function () { 7 | expect(urlTrim('//abc.com/index.html')).toBe('//abc.com/'); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /test/scripts/renderers/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Renderers', function () { 4 | require('./md'); 5 | }); 6 | -------------------------------------------------------------------------------- /test/scripts/renderers/md.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('md', function () { 4 | const md = require('../../../lib/renderer/markdown'); 5 | const render = function (data) { 6 | return md.call({ 7 | execFilterSync() {}, 8 | config: { 9 | markdown: {} 10 | }, 11 | theme: { 12 | config: { 13 | runtime: { 14 | styles: { 15 | blockimg: 'img', 16 | table: 'tb', 17 | bounded: 'bd', 18 | checklist: 'cl' 19 | }, 20 | uriReplacer: _ => _ + '?' 21 | } 22 | }, 23 | } 24 | }, data).trim().replace(/\n/g, ''); 25 | } 26 | 27 | it('render img', function () { 28 | expect(render({ text: 'a ![img](img.png)' })) 29 | .toBe('

a img

'); 30 | // block img 31 | expect(render({ text: '![img](img.png "|block")' })) 32 | .toBe('

img

'); 33 | expect(render({ text: '![img](img.png "|||")' })) 34 | .toBe('

img

'); 35 | }); 36 | 37 | it('render link', function () { 38 | expect(render({ text: '[a](#a)' })) 39 | .toBe('

a

'); 40 | // external link 41 | expect(render({ text: '[a](http://abc)' })) 42 | .toBe('

a

'); 43 | }); 44 | it('render heading', function () { 45 | expect(render({ text: '# h1' })) 46 | .toBe('

h1

'); 47 | }); 48 | it('render list', function () { 49 | expect(render({ text: '- 1' })) 50 | .toBe('
  • 1
'); 51 | expect(render({ text: '1. 1' })) 52 | .toBe('
  1. 1
'); 53 | expect(render({ text: '- [ ] 1' })) 54 | .toBe('
  • 1
'); 55 | expect(render({ text: '- [x] 1' })) 56 | .toBe('
  • 1
'); 57 | }); 58 | it('render table', function () { 59 | expect(render({ 60 | text: ` 61 | a | b | c 62 | -|-|- 63 | a|b|c` })) 64 | .toBe('
abc
abc
'); 65 | // headless 66 | expect(render({ 67 | text: ` 68 |  | |  69 | -|-|- 70 | a|b|c` })) 71 | .toBe('
abc
'); 72 | }); 73 | }); 74 | -------------------------------------------------------------------------------- /test/scripts/tags/canvas.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const cheerio = require('cheerio'); 4 | 5 | describe('canvas', function () { 6 | const canvas = require('../../../lib/tag/canvas'); 7 | 8 | it('increase the id and reset when title is changed', function () { 9 | const $1 = cheerio.load(canvas.call({ title: 'foo' }, [])); 10 | const $2 = cheerio.load(canvas.call({ title: 'foo' }, [])); 11 | const $3 = cheerio.load(canvas.call({ title: 'bar' }, [])); 12 | 13 | expect($1('canvas').attr('id')).toBe('canvas-0'); 14 | expect($2('canvas').attr('id')).toBe('canvas-1'); 15 | expect($3('canvas').attr('id')).toBe('canvas-0'); 16 | }); 17 | 18 | it('create canvas and script', function () { 19 | const $ = cheerio.load(canvas([], 'ctx')); 20 | 21 | expect($('canvas')).not.toBeNull(); 22 | expect($('script').html()).not.toBe(''); 23 | }); 24 | 25 | it('set width and height', function () { 26 | const $1 = cheerio.load(canvas([])); 27 | const $2 = cheerio.load(canvas([100, 50])); 28 | 29 | expect($1('canvas').attr('width')).toBe('300'); 30 | expect($1('canvas').attr('height')).toBe('150'); 31 | expect($2('canvas').attr('width')).toBe('100'); 32 | expect($2('canvas').attr('height')).toBe('50'); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /test/scripts/tags/gist.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const cheerio = require('cheerio'); 4 | 5 | describe('gist', function () { 6 | const gist = require('../../../lib/tag/gist'); 7 | 8 | it('create additional tag', function () { 9 | const $ = cheerio.load(gist(['foo'])); 10 | 11 | expect($('script').attr('src')).toBe('//gist.github.com/foo.js'); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /test/scripts/tags/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Tags', function () { 4 | require('./canvas'); 5 | require('./gist'); 6 | }); 7 | -------------------------------------------------------------------------------- /test/scripts/utils/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('utils', function () { 4 | require('./rest'); 5 | require('./parseConfig'); 6 | }); 7 | -------------------------------------------------------------------------------- /test/scripts/utils/parseConfig.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('utils/parseConfig()', function () { 4 | const { parseConfig } = require('../../../lib/utils'); 5 | 6 | it('string', function () { 7 | const schema = { type: 'string' } 8 | expect(parseConfig(schema, "This is a string")).toBe("This is a string") 9 | expect(parseConfig(schema, '')).toBe('') 10 | expect(parseConfig(schema, 1)).toBeUndefined() 11 | }); 12 | 13 | it('string/length', function () { 14 | const schema = { type: 'string', minLength: 1, maxLength: 3 } 15 | expect(parseConfig(schema, '')).toBeUndefined() 16 | expect(parseConfig(schema, '1')).toBe('1') 17 | expect(parseConfig(schema, '1234')).toBeUndefined() 18 | }) 19 | 20 | it('string/pattern', function () { 21 | const schema = { type: 'string', pattern: "^(\\([0-9]{3}\\))?[0-9]{3}-[0-9]{4}$" } 22 | expect(parseConfig(schema, '555-1212')).toBe('555-1212') 23 | expect(parseConfig(schema, '(888)555-1212')).toBe('(888)555-1212') 24 | expect(parseConfig(schema, '(800)FLOWERS')).toBeUndefined() 25 | }) 26 | 27 | it('string/format', function () { 28 | // support date, email, uri, uri-reference, regex 29 | const date = { type: 'string', format: 'date' } 30 | const email = { type: 'string', format: 'email' } 31 | 32 | expect(parseConfig(date, '2018-11-13')).toBe('2018-11-13') 33 | expect(parseConfig(email, 'a@a.a')).toBe('a@a.a') 34 | 35 | expect(parseConfig(date, '2018-11-13-')).toBeUndefined() 36 | expect(parseConfig(email, 'a@a.a.')).toBeUndefined() 37 | }) 38 | 39 | it('number', function () { 40 | const schema = { type: 'number' } 41 | expect(parseConfig(schema, 0)).toBe(0) 42 | expect(parseConfig(schema, '0')).toBeUndefined() 43 | }); 44 | 45 | it('number/range', function () { 46 | const schema = { type: 'number', minimum: 1, maximum: 4 } 47 | 48 | expect(parseConfig(schema, 0)).toBeUndefined() 49 | expect(parseConfig(schema, 1)).toBe(1) 50 | expect(parseConfig(schema, 5)).toBeUndefined() 51 | }); 52 | 53 | it('boolean', function () { 54 | const schema = { type: 'boolean' } 55 | expect(parseConfig(schema, 0)).toBeUndefined() 56 | expect(parseConfig(schema, false)).toBe(false) 57 | expect(parseConfig(schema, true)).toBe(true) 58 | }); 59 | 60 | it('enum', function () { 61 | expect(parseConfig({ enum: [1, 3, 5] }, 0)).toBeUndefined() 62 | expect(parseConfig({ enum: [1, 3, false] }, false)).toBe(false) 63 | expect(parseConfig({ enum: ['1', 3, '5'] }, 3)).toBe(3) 64 | expect(parseConfig({ enum: ['1', '3', '5'] }, '3')).toBe('3') 65 | }); 66 | 67 | it('array', function () { 68 | const schema = { type: 'array' } 69 | expect(parseConfig(schema, [1, 2, '3'])).toEqual([1, 2, '3']) 70 | expect(parseConfig(schema, { "Not": "an array" })).toBeUndefined() 71 | }); 72 | 73 | it('array/contains', function () { 74 | const schema = { type: 'array', contains: { type: 'string' } } 75 | expect(parseConfig(schema, [1, 2, 'contains a string'])).toEqual([1, 2, 'contains a string']) 76 | expect(parseConfig(schema, [1, 2, 3])).toBeUndefined() 77 | }); 78 | 79 | it('array/items', function () { 80 | const schema = { type: 'array', items: { type: 'string' } } 81 | expect(parseConfig(schema, [1, 2, 3])).toBeUndefined() 82 | expect(parseConfig(schema, ['1', '2', '3'])).toEqual(['1', '2', '3']) 83 | }); 84 | 85 | it('array/tuple', function () { 86 | const schema = { type: 'array', items: [{ type: 'number' }, { type: 'string' }], additionalItems: { type: 'boolean' } } 87 | expect(parseConfig(schema, [1, 2, 3])).toBeUndefined() 88 | expect(parseConfig(schema, [1, '2', '3'])).toBeUndefined() 89 | expect(parseConfig(schema, [1, '2', false])).toEqual([1, '2', false]) 90 | 91 | schema.additionalItems = false 92 | expect(parseConfig(schema, [1, '2', false])).toBeUndefined() 93 | 94 | delete schema.additionalItems 95 | expect(parseConfig(schema, [1, '2', false])).toEqual([1, '2', false]) 96 | }); 97 | 98 | it('array/length', function () { 99 | const schema = { type: 'array', minItems: 2, maxItems: 2 } 100 | expect(parseConfig(schema, [1, 2, 3])).toBeUndefined() 101 | expect(parseConfig(schema, [1, 2])).toEqual([1, 2]) 102 | }); 103 | 104 | it('array/uniqueItems', function () { 105 | const schema = { type: 'array', uniqueItems: true } 106 | expect(parseConfig(schema, [1, 2, 1])).toBeUndefined() 107 | expect(parseConfig(schema, [1, { two: 2 }, { two: 2 }])).toBeUndefined() 108 | expect(parseConfig(schema, [1, { two: 2 }, { three: 3 }])).toEqual([1, { two: 2 }, { three: 3 }]) 109 | }); 110 | 111 | it('array/objectItems', function () { 112 | const schema = { type: 'array', items: { type: 'object', properties: { name: 'string', age: 'number' }, required: ['name', 'age'] } } 113 | expect(parseConfig(schema, [1, 2, 1])).toBeUndefined() 114 | expect(parseConfig(schema, [{ name: '1' }, { age: 2 }])).toBeUndefined() 115 | expect(parseConfig(schema, [{ name: '1', age: 2 }, { name: '1', age: 2 }])).toEqual([{ name: '1', age: 2 }, { name: '1', age: 2 }]) 116 | }); 117 | 118 | it('object', function () { 119 | const schema = { type: 'object' } 120 | expect(parseConfig(schema, {})).toEqual({}) 121 | expect(parseConfig(schema, { a: 1 })).toEqual({ a: 1 }) 122 | expect(parseConfig(schema, ["An", "array", "not", "an", "object"])).toBe(undefined); 123 | }); 124 | 125 | it('object/properties', function () { 126 | const schema = { 127 | type: 'object', 128 | "properties": { 129 | "number": { "type": "number" }, 130 | "string": { "type": "string" }, 131 | "enum": { 132 | "type": "string", 133 | "enum": ["a", "b", "c"] 134 | } 135 | } 136 | } 137 | expect(parseConfig(schema, { "number": 1, "string": "string", "enum": "a" })).toEqual({ "number": 1, "string": "string", "enum": "a" }) 138 | // a standard implementation should fail 139 | expect(parseConfig(schema, { "number": "1", "string": "string", "enum": "d" })).toEqual({ "string": "string" }) 140 | expect(parseConfig(schema, {})).toEqual({}) 141 | }); 142 | 143 | 144 | it('object/additionalProperties', function () { 145 | const schema = { 146 | type: 'object', 147 | "properties": { 148 | "number": { "type": "number" } 149 | }, 150 | additionalProperties: false 151 | } 152 | expect(parseConfig(schema, { "number": '1', "string": "string" })).toBe(undefined); 153 | expect(parseConfig(schema, { "number": 1, "string": "string" })).toEqual({ "number": 1 }) 154 | 155 | schema.additionalProperties = { "type": "string" }; 156 | expect(parseConfig(schema, { "number": 1, "string": "string" })).toEqual({ "number": 1, "string": "string" }) 157 | expect(parseConfig(schema, { "number": 1, "string": false })).toEqual({ "number": 1 }) 158 | // a standard implementation should fail 159 | // expect(parseConfig(schema, { "number": "1", "string": "string", "enum": "d" })).toEqual({ "string": "string" }) 160 | // expect(parseConfig(schema, {})).toEqual({}) 161 | }); 162 | 163 | it('object/size', function () { 164 | const schema = { 165 | "type": "object", 166 | "minProperties": 2, 167 | "maxProperties": 3 168 | } 169 | expect(parseConfig(schema, { "a": 0 })).toBe(undefined); 170 | expect(parseConfig(schema, { "a": 0, "b": 1 })).toEqual({ "a": 0, "b": 1 }) 171 | expect(parseConfig(schema, { "a": 0, "b": 1, "c": 2, "d": 3 })).toBe(undefined); 172 | }); 173 | 174 | it('object/patternProperties', function () { 175 | let schema = { 176 | "type": "object", 177 | "patternProperties": { 178 | "^S_": { "type": "string" }, 179 | "^I_": { "type": "number" } 180 | }, 181 | "additionalProperties": false 182 | } 183 | expect(parseConfig(schema, { "S_25": "This is a string" })).toEqual({ "S_25": "This is a string" }) 184 | expect(parseConfig(schema, { "I_0": 42 })).toEqual({ "I_0": 42 }) 185 | expect(parseConfig(schema, { "S_0": 42 })).toBe(undefined); 186 | expect(parseConfig(schema, { "I_42": "This is a string" })).toBe(undefined); 187 | 188 | schema = Object.assign(schema, { 189 | properties: { 190 | builtin: { "type": "number" }, 191 | }, 192 | additionalProperties: { "type": "string" } 193 | }) 194 | expect(parseConfig(schema, { "builtin": 42 })).toEqual({ "builtin": 42 }) 195 | expect(parseConfig(schema, { "keyword": "value" })).toEqual({ "keyword": "value" }) 196 | expect(parseConfig(schema, { "keyword": 42 })).toBe(undefined); 197 | }); 198 | 199 | it('object/required', function () { 200 | const schema = { 201 | "type": "object", 202 | "properties": { 203 | "name": { "type": "string" }, 204 | "email": { "type": "string" }, 205 | "address": { "type": "string" }, 206 | "telephone": { "type": "string" } 207 | }, 208 | "required": ["name", "email"] 209 | }; 210 | 211 | expect(parseConfig(schema, { 212 | "name": "William Shakespeare", 213 | "email": "bill@stratford-upon-avon.co.uk" 214 | })) 215 | .toEqual({ 216 | "name": "William Shakespeare", 217 | "email": "bill@stratford-upon-avon.co.uk" 218 | }); 219 | expect(parseConfig(schema, { 220 | "name": "William Shakespeare", 221 | "email": "bill@stratford-upon-avon.co.uk", 222 | "address": "Henley Street, Stratford-upon-Avon, Warwickshire, England", 223 | "authorship": "in question" 224 | })) 225 | .toEqual({ 226 | "name": "William Shakespeare", 227 | "email": "bill@stratford-upon-avon.co.uk", 228 | "address": "Henley Street, Stratford-upon-Avon, Warwickshire, England", 229 | "authorship": "in question" 230 | }); 231 | expect(parseConfig(schema, { 232 | "name": "William Shakespeare", 233 | "address": "Henley Street, Stratford-upon-Avon, Warwickshire, England", 234 | })) 235 | .toBe(undefined); 236 | 237 | // fallback to default 238 | schema.properties.email.default = 'a@a.a'; 239 | expect(parseConfig(schema, { 240 | "name": "William Shakespeare", 241 | "address": "Henley Street, Stratford-upon-Avon, Warwickshire, England", 242 | })) 243 | .toEqual({ 244 | "name": "William Shakespeare", 245 | "email": "a@a.a", 246 | "address": "Henley Street, Stratford-upon-Avon, Warwickshire, England", 247 | }); 248 | }) 249 | 250 | it('object/dependencies', function () { 251 | const schema = { 252 | "type": "object", 253 | 254 | "properties": { 255 | "name": { "type": "string" }, 256 | "credit_card": { "type": "number" }, 257 | "billing_address": { "type": "string" } 258 | }, 259 | 260 | "required": ["name"], 261 | 262 | "dependencies": { 263 | "credit_card": ["billing_address"] 264 | } 265 | }; 266 | 267 | // Property dependencies 268 | 269 | expect(parseConfig(schema, { 270 | "name": "John Doe", 271 | "credit_card": 5555555555555555, 272 | "billing_address": "555 Debtor's Lane" 273 | })) 274 | .toEqual({ 275 | "name": "John Doe", 276 | "credit_card": 5555555555555555, 277 | "billing_address": "555 Debtor's Lane" 278 | }); 279 | 280 | expect(parseConfig(schema, { 281 | "name": "John Doe", 282 | "credit_card": 5555555555555555 283 | })) 284 | .toBe(undefined); 285 | 286 | expect(parseConfig(schema, { 287 | "name": "John Doe" 288 | })) 289 | .toEqual({ 290 | "name": "John Doe" 291 | }); 292 | 293 | expect(parseConfig(schema, { 294 | "name": "John Doe", 295 | "billing_address": "555 Debtor's Lane" 296 | })) 297 | .toEqual({ 298 | "name": "John Doe", 299 | "billing_address": "555 Debtor's Lane" 300 | }); 301 | 302 | // bidirectional 303 | schema.dependencies = { 304 | "credit_card": ["billing_address"], 305 | "billing_address": ["credit_card"] 306 | }; 307 | expect(parseConfig(schema, { 308 | "name": "John Doe", 309 | "credit_card": 5555555555555555 310 | })) 311 | .toBe(undefined); 312 | expect(parseConfig(schema, { 313 | "name": "John Doe", 314 | "billing_address": "555 Debtor's Lane" 315 | })) 316 | .toBe(undefined); 317 | 318 | // Schema dependencies 319 | schema.dependencies = { 320 | "credit_card": { 321 | "properties": { 322 | "billing_address": { "type": "string" } 323 | }, 324 | "required": ["billing_address"] 325 | } 326 | }; 327 | expect(parseConfig(schema, { 328 | "name": "John Doe", 329 | "credit_card": 5555555555555555, 330 | "billing_address": "555 Debtor's Lane" 331 | })) 332 | .toEqual({ 333 | "name": "John Doe", 334 | "credit_card": 5555555555555555, 335 | "billing_address": "555 Debtor's Lane" 336 | }); 337 | expect(parseConfig(schema, { 338 | "name": "John Doe", 339 | "credit_card": 5555555555555555 340 | })) 341 | .toBe(undefined); 342 | expect(parseConfig(schema, { 343 | "name": "John Doe", 344 | "billing_address": "555 Debtor's Lane" 345 | })) 346 | .toEqual({ 347 | "name": "John Doe", 348 | "billing_address": "555 Debtor's Lane" 349 | }); 350 | 351 | }); 352 | 353 | it('definitions', function () { 354 | const schema = { 355 | definitions: { 356 | age: { type: 'number' }, 357 | gender: { $id: '#gender', enum: ['male', 'female', 'other'] } 358 | }, 359 | type: 'object', 360 | properties: { 361 | age: { $ref: '#/definitions/age' }, 362 | gender: { $ref: '#gender' }, 363 | class: { 364 | definitions: { 365 | color: { type: 'string' } 366 | }, 367 | type: 'object', 368 | properties: { 369 | color: { $ref: '#/properties/class/definitions/color' } 370 | } 371 | } 372 | } 373 | }; 374 | 375 | expect(parseConfig(schema, { 376 | age: 1, 377 | gender: 'other', 378 | class: { color: 0 } 379 | })) 380 | .toEqual({ 381 | age: 1, 382 | gender: 'other' 383 | }); 384 | 385 | expect(parseConfig(schema, { 386 | age: 1, 387 | gender: 'other', 388 | class: { color: 'this is a string' } 389 | })) 390 | .toEqual({ 391 | age: 1, 392 | gender: 'other', 393 | class: { color: 'this is a string' } 394 | }); 395 | }); 396 | 397 | it('anyOf', function () { 398 | const schema = { 399 | "anyOf": [ 400 | { "type": "string", "maxLength": 5 }, 401 | { "type": "number", "minimum": 0 } 402 | ] 403 | }; 404 | 405 | expect(parseConfig(schema, "short")).toBe("short") 406 | expect(parseConfig(schema, "too long")).toBeUndefined() 407 | expect(parseConfig(schema, 12)).toBe(12) 408 | expect(parseConfig(schema, -5)).toBeUndefined() 409 | }); 410 | 411 | it('allOf', function () { 412 | let schema = { 413 | "allOf": [ 414 | { "type": "string" }, 415 | { "type": "string", "maxLength": 5 } 416 | ] 417 | }; 418 | 419 | expect(parseConfig(schema, "short")).toBe("short") 420 | expect(parseConfig(schema, "too long")).toBeUndefined() 421 | 422 | schema = { 423 | "definitions": { 424 | "address": { 425 | "type": "object", 426 | "properties": { 427 | "street_address": { "type": "string" }, 428 | "city": { "type": "string" }, 429 | "state": { "type": "string" } 430 | }, 431 | "required": ["street_address", "city", "state"] 432 | } 433 | }, 434 | "allOf": [ 435 | { "$ref": "#/definitions/address" }, 436 | { 437 | "properties": { 438 | "type": { "enum": ["residential", "business"] } 439 | } 440 | } 441 | ] 442 | }; 443 | 444 | expect(parseConfig(schema, { 445 | "street_address": "1600 Pennsylvania Avenue NW", 446 | "city": "Washington", 447 | "state": "DC", 448 | "type": "business" 449 | })) 450 | .toEqual({ 451 | "street_address": "1600 Pennsylvania Avenue NW", 452 | "city": "Washington", 453 | "state": "DC", 454 | "type": "business" 455 | }); 456 | 457 | schema.additionalProperties = false 458 | expect(parseConfig(schema, { 459 | "street_address": "1600 Pennsylvania Avenue NW", 460 | "city": "Washington", 461 | "state": "DC", 462 | "type": "business" 463 | })).toBe(undefined) 464 | }); 465 | }); 466 | -------------------------------------------------------------------------------- /test/scripts/utils/rest.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const cheerio = require('cheerio'); 4 | 5 | describe('utils/rest', function () { 6 | const { type, isEmptyObject, pick, md5, base64, Pagination, parseToc, escapeIdentifier, localeId, parseJs, snippet, htmlTag, parseBackground, trimHtml, parsePipe } = require('../../../lib/utils'); 7 | 8 | it('type()', function () { 9 | expect(type({})).toBe('object'); 10 | expect(type([])).toBe('array'); 11 | expect(type()).toBe('undefined'); 12 | }); 13 | 14 | it('isEmptyObject()', function () { 15 | expect(isEmptyObject({})).toBe(true); 16 | expect(isEmptyObject({ a: 1 })).toBe(false); 17 | expect(isEmptyObject([])).toBe(false); 18 | }); 19 | 20 | it('pick()', function () { 21 | const o = { a: 1, b: 2, c: 3, d: false }; 22 | const keys = ['a', 'b', 'd']; 23 | const result = { a: 1, b: 2 }; 24 | const curying = pick(keys); 25 | 26 | expect(pick(o, keys)).toEqual(result); 27 | expect(curying(o)).toEqual(result); 28 | }); 29 | 30 | it('md5()', function () { 31 | expect(md5('foo')).toBe('acbd18db4cc2f85cedef'); 32 | expect(md5('foo', 6)).toBe('acbd18'); 33 | }) 34 | 35 | it('base64()', function () { 36 | expect(base64('foo')).toBe('Zm9v'); 37 | expect(base64('foo/')).toBe('Zm9vLw'); 38 | expect(base64('foo/a')).toBe('Zm9vL2E'); 39 | }) 40 | 41 | it('Pagination', function () { 42 | const pagination = new Pagination({ 43 | html: { generateFn: ({ path, data }) => ({ path: path + '/index.html', data, layout: 'index' }) }, 44 | json: { generateFn: ({ path, data }) => ({ path: 'api/' + path + '.json', data: JSON.stringify(data) }) }, 45 | }); 46 | const posts = [1, 2, 3, 4, 5, 6]; 47 | const datum = pagination.apply(posts, { perPage: 5, id: 'test' }, [{ type: 'html' }, { type: 'json' }]); 48 | 49 | expect(datum[0]).toEqual([ 50 | { path: 'test/index.html', data: { per_page: 5, total: 2, current: 1, data: [1, 2, 3, 4, 5] }, layout: 'index' }, 51 | { path: 'api/test.json', data: '{"per_page":5,"total":2,"current":1,"data":[1,2,3,4,5]}' }, 52 | ]); 53 | expect(datum[1]).toEqual([ 54 | { path: 'test/2/index.html', data: { per_page: 5, total: 2, current: 2, data: [6] }, layout: 'index' }, 55 | { path: 'api/test/2.json', data: '{"per_page":5,"total":2,"current":2,"data":[6]}' }, 56 | ]); 57 | }); 58 | 59 | it('parseToc()', function () { 60 | const cnt1 = ` 61 |

title 1

62 | content 63 |

title 1.1

64 | content 65 |

title 1.1.1

66 | content 67 |

title 1.1.1.1

68 | content 69 |
title 1.1.1.1.1
70 | content 71 |

title 2

72 | content 73 | `; 74 | const cnt2 = ` 75 |

title 1

76 | content 77 |

title 1.1.1

78 | content 79 | `; 80 | 81 | // depth is max to 4 82 | expect(parseToc(cnt1, 5)).toEqual([ 83 | { 84 | title: 'title 1', id: 'title-1', index: '1', children: [ 85 | { 86 | title: 'title 1.1', id: 'title-1-1', index: '1.1', children: [ 87 | { 88 | title: 'title 1.1.1', id: 'title-1-1-1', index: '1.1.1', children: [ 89 | { 90 | title: 'title 1.1.1.1', id: 'title-1-1-1-1', index: '1.1.1.1' 91 | } 92 | ] 93 | } 94 | ] 95 | } 96 | ] 97 | }, 98 | { title: 'title 2', id: 'title-2', index: '2' }, 99 | ]); 100 | expect(parseToc(cnt1, 1)).toEqual([ 101 | { title: 'title 1', id: 'title-1', index: '1' }, 102 | { title: 'title 2', id: 'title-2', index: '2' }, 103 | ]); 104 | 105 | expect(parseToc(cnt2)).toEqual([]); 106 | }); 107 | 108 | it('escapeIdentifier()', function () { 109 | expect(escapeIdentifier('A & B & C')).toBe('A and B and C'); 110 | expect(escapeIdentifier('A + B + C')).toBe('A plus B plus C'); 111 | }); 112 | 113 | it('localeId()', function () { 114 | expect(localeId('zh-cn')).toBe('zh-Hans'); 115 | expect(localeId('zh-hk')).toBe('zh-Hant'); 116 | expect(localeId('zh-tw')).toBe('zh-Hant'); 117 | expect(localeId('zh-CN')).toBe('zh-Hans'); 118 | expect(localeId('zh-HK')).toBe('zh-Hant'); 119 | expect(localeId('zh-TW')).toBe('zh-Hant'); 120 | expect(localeId('zh-Hans')).toBe('zh-Hans'); 121 | expect(localeId('zh-Hant')).toBe('zh-Hant'); 122 | expect(localeId('en')).toBe('en'); 123 | expect(localeId('wrong')).toBe('en'); 124 | 125 | expect(localeId('zh-Hans', true)).toBe('zh-cn'); 126 | expect(localeId('zh-Hant', true)).toBe('zh-hk'); 127 | expect(localeId('zh-Hant', true)).toBe('zh-hk'); 128 | expect(localeId('en')).toBe('en'); 129 | expect(localeId('wrong')).toBe('en'); 130 | 131 | 132 | expect(localeId(['zh-cn'])).toBe('zh-Hans'); 133 | expect(localeId(['zh-Hans'], true)).toBe('zh-cn'); 134 | }); 135 | 136 | it('jsParser()', function () { 137 | expect(parseJs()).toBe(''); 138 | expect(parseJs({})).toBe(''); 139 | expect(parseJs(`const foo = 1; // foo`)).toBe('var foo=1;'); 140 | expect(parseJs(`(function() { 141 | const foo = 1; // foo 142 | })();`)).toBe(''); 143 | }); 144 | 145 | it('snippet()', function () { 146 | expect(snippet('')).toBe(''); 147 | 148 | expect(snippet('alert(1)')) 149 | .toBe(''); 150 | 151 | expect(snippet('', 'whatever here')).toBe('whatever here'); 152 | 153 | expect(snippet('alert(1)', code => `${code}`)) 154 | .toBe('alert(1);'); 155 | }); 156 | 157 | it('htmlTag()', function () { 158 | expect(htmlTag('script')).toBe(''); 159 | expect(htmlTag('link')).toBe(''); 160 | expect(htmlTag('style')).toBe(''); 161 | 162 | expect(htmlTag('script', { src: 'xxx.js' })).toBe(''); 163 | expect(htmlTag('script', {}, 'var a=1;alert(1)')).toBe(''); 164 | // escape es code transformation when `type` is specified 165 | expect(htmlTag('script', { type: 'xxx' }, 'alert(1)')).toBe(''); 166 | 167 | expect(htmlTag('style', {}, 'body{}')).toBe(''); 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 | --------------------------------------------------------------------------------