Compare commits

...

30 Commits

Author SHA1 Message Date
雨奇晴好
d25776ea38
Fix 1px scroll offset on page navigation when Pjax is enabled (#995) 2026-09-08 10:35:06 +08:00
Mimi
3e4d43597d Update _vendors.yml 2026-09-02 09:26:54 +08:00
Mimi
6037f68b7a New animation next-bounce 2026-08-25 01:36:07 +08:00
Mimi
6468f139cb Update default icon 2026-08-20 22:10:51 +08:00
Mimi
d62fdfa759 Simplify site-nav-toggle element 2026-08-20 14:10:46 +08:00
Mimi
96d34a0bb9 Update site-nav-toggle style 2026-08-20 11:12:58 +08:00
Mimi
6560fa6dd6 Fix sidebar-dimmer animation 2026-08-14 17:39:50 +08:00
Mimi
66e3095a31 Fix search-popup animation using template 2026-08-14 16:45:16 +08:00
Mimi
120d41d0fa Fix preload condition for theme preconnect option 2026-08-14 01:20:31 +08:00
Mimi
4b249983ed Release v8.29.0 2026-08-05 15:05:48 +08:00
where where
ba0ab0e8f7
Support optional async and defer attributes in script helpers (#952)
Co-authored-by: Mimi <1119186082@qq.com>
2026-08-05 15:03:54 +08:00
renovate[bot]
41c19bfc47
Update dependency mocha to v11.8.0 (#980)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-08-05 13:03:55 +08:00
renovate[bot]
88cdd52f52
Update dependency js-yaml to v4.3.0 [SECURITY] (#979)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-08-05 12:58:07 +08:00
renovate[bot]
b3ac16962d
Update actions/labeler action to v7 (#977)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-08-05 12:57:52 +08:00
renovate[bot]
5b71ed5eae
Update actions/setup-python action to v7 (#976)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-08-05 12:57:40 +08:00
renovate[bot]
5a476ca06f
Update dependency c8 to v12 (#975)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-08-05 12:57:29 +08:00
renovate[bot]
abf7ec2aeb
Update actions/setup-node action to v7 (#974)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-08-05 12:57:15 +08:00
renovate[bot]
f7f13675e5
Update dependency eslint to v10.8.0 (#972)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-08-05 12:56:54 +08:00
Mimi
db11583d55 Update _vendors.yml 2026-08-05 12:04:44 +08:00
Mimi
abd6a790ba
Configure MathJax overflow handling (#986) 2026-08-05 11:07:25 +08:00
Mimi
c0232764b3
Support MathJax 4 (#985) 2026-08-05 10:28:55 +08:00
Mimi
091af0fd4f
Fix TOC height calculation for wrapped headings (#984) 2026-08-05 01:15:32 +08:00
Mimi
854e2006b6
Disable Fancybox hash handling with PJAX (#983) 2026-08-04 17:26:12 +08:00
Mimi
5c7574fd3f Improve Algolia search concurrency 2026-08-04 14:55:10 +08:00
Mimi
4352696dba refactor: use Firestore REST API 2026-08-04 14:21:17 +08:00
Mimi
940b2263f6 Deprecate body_scrollbar.stable option 2026-08-04 12:32:47 +08:00
Mimi
8fa97a173a
Remove animejs (#982) 2026-08-03 22:51:17 +08:00
Mimi
1b306b9824
Replace Anime.js scrolling with native APIs (#981) 2026-08-03 18:30:05 +08:00
Mimi
d54ab89ea2 Deprecate lazyload option 2026-08-02 23:57:30 +08:00
Mimi
333b2c7908 Update Netlify logo URL 2026-07-01 21:54:50 +08:00
40 changed files with 475 additions and 311 deletions

View File

@ -7,7 +7,7 @@ jobs:
triage:
runs-on: ubuntu-latest
steps:
- uses: actions/labeler@v6
- uses: actions/labeler@v7
# https://github.com/peaceiris/actions-label-commenter#work-with-other-auto-label-actions
with:
repo-token: "${{ secrets.GH_PAT }}"

View File

@ -8,7 +8,7 @@ jobs:
steps:
- uses: actions/checkout@v7
- name: Use Node.js
uses: actions/setup-node@v6
uses: actions/setup-node@v7
- name: Install Dependencies
run: npm install
- run: npm run eslint
@ -21,7 +21,7 @@ jobs:
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Python
uses: actions/setup-python@v6
uses: actions/setup-python@v7
with:
python-version: '3.x'
- name: Install Dependencies

View File

@ -15,7 +15,7 @@ jobs:
steps:
- uses: actions/checkout@v7
- name: Use Node.js
uses: actions/setup-node@v6
uses: actions/setup-node@v7
with:
node-version: '24'
registry-url: 'https://registry.npmjs.org'

View File

@ -14,7 +14,7 @@ jobs:
with:
repository: hexojs/hexo-starter
- name: Use Node.js
uses: actions/setup-node@v6
uses: actions/setup-node@v7
- name: Install Dependencies
run: npm install
- name: Install hexo-tag-embed

View File

@ -148,9 +148,9 @@ NexT send special thanks to these great services that sponsor our core infrastru
<a href="https://www.netlify.com">
<picture>
<source media="(prefers-color-scheme: dark)" height="40" srcset="https://www.netlify.com/v3/img/components/full-logo-dark.svg">
<source media="(prefers-color-scheme: light)" height="40" srcset="https://www.netlify.com/v3/img/components/full-logo-light.svg">
<img alt="Netlify Logo" height="40" src="https://www.netlify.com/v3/img/components/full-logo-light.svg">
<source media="(prefers-color-scheme: dark)" height="40" srcset="https://raw.githubusercontent.com/netlify/netlify-example-styles/main/public/images/logo-netlify-small-fullcolor-darkmode.svg">
<source media="(prefers-color-scheme: light)" height="40" srcset="https://raw.githubusercontent.com/netlify/netlify-example-styles/main/public/images/logo-netlify-small-fullcolor-lightmode.svg">
<img alt="Netlify Logo" height="40" src="https://raw.githubusercontent.com/netlify/netlify-example-styles/main/public/images/logo-netlify-small-fullcolor-lightmode.svg">
</picture>
</a>

View File

@ -88,7 +88,6 @@ open_graph:
#twitter_id: <twitter:creator>
#twitter_site: <twitter:site>
#twitter_image: <twitter:image>
#google_plus: <g+:profile_link>
#fb_admins: <fb:admin_id>
#fb_app_id: <fb:app_id>
@ -105,7 +104,7 @@ menu:
#home: / || fa fa-home
#about: /about/ || fa fa-user
#tags: /tags/ || fa fa-tags
#categories: /categories/ || fa fa-th
#categories: /categories/ || fa fa-layer-group
#archives: /archives/ || fa fa-archive
#schedule: /schedule/ || fa fa-calendar
#sitemap: /sitemap.xml || fa fa-sitemap
@ -365,8 +364,6 @@ theme_color:
body_scrollbar:
# Place the scrollbar over the content.
overlay: false
# Reserving space for the scrollbar gutter even if the content is not overflowing.
stable: false
codeblock:
# Code Highlight theme
@ -575,6 +572,8 @@ math:
enable: false
# Available values: none | ams | all
tags: none
# Available values: overflow | scroll | scale | truncate | linebreak
display_overflow: scroll
katex:
enable: false
@ -600,10 +599,6 @@ fancybox: false
# For more information: https://medium-zoom.francoischalifour.com
mediumzoom: false
# Vanilla JavaScript plugin for lazyloading images.
# For more information: https://apoorv.pro/lozad.js/demo/
lazyload: false
# Automatically insert whitespace between CJK and half-width characters.
# For more information: https://github.com/vinta/pangu.js
# Server-side plugin: https://github.com/next-theme/hexo-pangu
@ -814,13 +809,13 @@ plausible:
script_url: # https://plausible.io/js/script.js
site_domain: # www.example.com
# Another tool to show number of visitors to each article.
# Visit https://console.firebase.google.com/u/0/ to get apiKey and projectId.
# Visit https://firebase.google.com/docs/firestore/ to get more information about firestore.
# Show the number of visitors to each article using the Firestore REST API.
# Visit https://console.firebase.google.com/ to get projectId.
# Visit https://firebase.google.com/docs/firestore/use-rest-api for more information.
# Anonymous requests are authorized by your Firestore Security Rules.
firestore:
enable: false
collection: articles # Required, a string collection name to access firestore database
apiKey: # Required
projectId: # Required
# Show Views / Visitors of the website / page with busuanzi.

View File

@ -1,17 +1,12 @@
# This file is automatically generated
# See https://github.com/next-theme/plugins
anime:
name: animejs
version: 3.2.1
file: lib/anime.min.js
integrity: sha256-XL2inqUJaslATFnHdJOi9GfQ60on8Wx1C2H8DYiN1xY=
fontawesome:
name: '@fortawesome/fontawesome-free'
version: 7.0.0
version: 7.3.1
file: css/all.min.css
alias: font-awesome
integrity: sha256-VHqXKFhhMxcpubYf9xiWdCiojEbY9NexQ4jh8AxbvcM=
integrity: sha256-4Lad8m4ZWW1Lgb9+sMVLYEfnIh7BjV1NQMEe79Pviks=
prism:
name: prismjs
version: 1.30.0
@ -32,21 +27,21 @@ prism_line_numbers:
integrity: sha256-9cmf7tcLdXpKsPi/2AWE93PbZpTp4M4tqzFk+lWomjU=
mathjax:
name: mathjax
version: 3.2.2
file: es5/tex-mml-chtml.js
integrity: sha256-MASABpB4tYktI2Oitl4t+78w/lyA+D7b/s9GEP0JOGI=
version: 4.1.3
file: tex-mml-chtml.js
integrity: sha256-ihPtDDm7pTmSF9ViEsItlvsrBTsNkz0MOXMchzOwCQw=
katex:
name: katex
version: 0.16.9
version: 0.18.4
file: dist/katex.min.css
alias: KaTeX
integrity: sha256-UF1fgpAiu3tPJN/uCqEUHNe7pnr+QR0SQDNfgglgtcM=
integrity: sha256-GAwtd9Q019pR1mJcUKlk1P1v29ubyHlqCgFsMMSZMfs=
copy_tex_js:
name: katex
version: 0.16.9
version: 0.18.4
file: dist/contrib/copy-tex.min.js
alias: KaTeX
integrity: sha256-Us54+rSGDSTvIhKKUs4kygE2ipA0RXpWWh0/zLqw3bs=
integrity: sha256-xXHEm1IMuOkEAoczgoayF4Pjm0dmYLi1bZ4cA37q6v8=
pjax:
name: '@next-theme/pjax'
version: 0.6.0
@ -55,16 +50,16 @@ pjax:
integrity: sha256-vxLn1tSKWD4dqbMRyv940UYw4sXgMtYcK6reefzZrao=
fancybox_js:
name: '@fancyapps/ui'
version: 5.0.31
version: 6.1.14
file: dist/fancybox/fancybox.umd.js
alias: fancyapps-ui
integrity: sha256-a+H7FYzJv6oU2hfsfDGM2Ohw/cR9v+hPfxHCLdmCrE8=
integrity: sha256-QnJV4HT5r8i89ovZGRD86iynZqHerEbT2xt69vDM36I=
fancybox_css:
name: '@fancyapps/ui'
version: 5.0.31
version: 6.1.14
file: dist/fancybox/fancybox.css
alias: fancyapps-ui
integrity: sha256-gkQVf8UKZgQ0HyuxL/VnacadJ+D2Kox2TCEBuNQg5+w=
integrity: sha256-mdaMxrItEW6hMIVJJNPKuX0B7KCSJQVEv3mVY4N2we4=
mediumzoom:
name: medium-zoom
version: 1.1.0
@ -78,24 +73,24 @@ lazyload:
integrity: sha256-mOFREFhqmHeQbXpK2lp4nA3qooVgACfh88fpJftLBbc=
pangu:
name: pangu
version: 7.2.0
version: 7.2.1
file: dist/browser/pangu.umd.js
integrity: sha256-JnmRRnJK7DC6RQJbAJb6AXOM9OmWzS6z8eYultk/48Y=
integrity: sha256-kRsxecBnwG6mULFPe2E6UKnNm8XfDOuDwCOprjLZuX8=
quicklink:
name: quicklink
version: 3.0.1
version: 3.0.2
file: dist/quicklink.umd.js
integrity: sha256-44BednzIpUeQJcY8qtLyarFu0UCCTbgmWOvaoehiFQQ=
integrity: sha256-Qo2BTvlsBVXIr5fEHp8xlBpUmkbDINr5cQZeSfMTES8=
disqusjs_js:
name: disqusjs
version: 3.1.0
version: 3.2.1
file: dist/browser/disqusjs.es2015.umd.min.js
integrity: sha256-gdbBT62HLzqWTnXQED3WQL/ItFCuWsA5gd6dM/wgdiE=
integrity: sha256-6a5jzAWTjd18Nq6rZZn1x9hZLDZAilRDnd0arUFteBM=
disqusjs_css:
name: disqusjs
version: 3.1.0
version: 3.2.1
file: dist/browser/styles/disqusjs.css
integrity: sha256-E4RzceSomICFY1PT/yDQrN8DsLSwdScDEiQAevHabp0=
integrity: sha256-9SkCf6N7ls5uSFyENCeHM+m7Qv3lHH9Eg/CxdCajarI=
gitalk_js:
name: gitalk
version: 1.8.0
@ -106,21 +101,11 @@ gitalk_css:
version: 1.8.0
file: dist/gitalk.css
integrity: sha256-AJnUHL7dBv6PGaeyPQJcgQPDjt/Hn/PvYZde1iqfp8U=
firebase_app:
name: firebase
version: 12.2.1
file: firebase-app-compat.js
integrity: sha256-d21UgtIcA6c6qwr8nWk6lxs/KrpbWhknQN7qBjWA2Kk=
firebase_firestore:
name: firebase
version: 12.2.1
file: firebase-firestore-compat.js
integrity: sha256-leHatffkFuVGIg7ABaA5BDsqsEUtktL4tftb3OdQfg0=
algolia_search:
name: algoliasearch
version: 5.36.0
version: 5.57.0
file: dist/lite/builds/browser.umd.js
integrity: sha256-o49zoHLDPBm9WLg6AiliAVi3axCRTqe3ltTxLvhzAVg=
integrity: sha256-NJrnXIMFVRGMM2IieD83YpTvsPPIFKKyj4K/MNyTRQU=
local_search:
name: hexo-generator-searchdb
version: 1.5.0
@ -164,11 +149,11 @@ creative_commons:
alias: creativecommons-vocabulary
wavedrom:
name: wavedrom
version: 3.5.0
version: 3.6.2
file: wavedrom.min.js
integrity: sha256-INLAoJc6quTNfiMWkGZniYO2cxE8mHpddnLow1m6RFs=
integrity: sha256-3hPWxTqi1K1sjmlESiOTH6lfGwYmz8/SFqoAcPxrjwg=
wavedrom_skin:
name: wavedrom
version: 3.5.0
version: 3.6.2
file: skins/default.js
integrity: sha256-fduc/Zszk5ezWws2uInY/ALWVmIrmV6VTgXbsYSReFI=
integrity: sha256-bUKjUpel+qLgflZd3EfdbhIorwRYgSkaZKM2s/Bi0VI=

View File

@ -147,9 +147,9 @@ Also, we welcome Issue or PR to our [official-plugins][official-plugins-url].
<a href="https://www.netlify.com">
<picture>
<source media="(prefers-color-scheme: dark)" height="40" srcset="https://www.netlify.com/v3/img/components/full-logo-dark.svg">
<source media="(prefers-color-scheme: light)" height="40" srcset="https://www.netlify.com/v3/img/components/full-logo-light.svg">
<img alt="Netlify Logo" height="40" src="https://www.netlify.com/v3/img/components/full-logo-light.svg">
<source media="(prefers-color-scheme: dark)" height="40" srcset="https://raw.githubusercontent.com/netlify/netlify-example-styles/main/public/images/logo-netlify-small-fullcolor-darkmode.svg">
<source media="(prefers-color-scheme: light)" height="40" srcset="https://raw.githubusercontent.com/netlify/netlify-example-styles/main/public/images/logo-netlify-small-fullcolor-lightmode.svg">
<img alt="Netlify Logo" height="40" src="https://raw.githubusercontent.com/netlify/netlify-example-styles/main/public/images/logo-netlify-small-fullcolor-lightmode.svg">
</picture>
</a>

View File

@ -148,9 +148,9 @@ NexT 特别感谢这些支持我们核心基础设施的优质服务:
<a href="https://www.netlify.com">
<picture>
<source media="(prefers-color-scheme: dark)" height="40" srcset="https://www.netlify.com/v3/img/components/full-logo-dark.svg">
<source media="(prefers-color-scheme: light)" height="40" srcset="https://www.netlify.com/v3/img/components/full-logo-light.svg">
<img alt="Netlify Logo" height="40" src="https://www.netlify.com/v3/img/components/full-logo-light.svg">
<source media="(prefers-color-scheme: dark)" height="40" srcset="https://raw.githubusercontent.com/netlify/netlify-example-styles/main/public/images/logo-netlify-small-fullcolor-darkmode.svg">
<source media="(prefers-color-scheme: light)" height="40" srcset="https://raw.githubusercontent.com/netlify/netlify-example-styles/main/public/images/logo-netlify-small-fullcolor-lightmode.svg">
<img alt="Netlify Logo" height="40" src="https://raw.githubusercontent.com/netlify/netlify-example-styles/main/public/images/logo-netlify-small-fullcolor-lightmode.svg">
</picture>
</a>

View File

@ -10,7 +10,7 @@
{{- meta_generator() }}
{%- endif %}
{%- if config.preconnect %}
{%- if theme.preconnect %}
<link rel="preload" href="{{ url_for(theme.css) }}/main.css" as="style">
{%- endif %}
{{ next_pre() }}

View File

@ -1,12 +1,8 @@
<div class="site-brand-container">
<div class="site-nav-toggle">
<div class="toggle" aria-label="{{ __('accessibility.nav_toggle') }}" role="button">
{%- if theme.menu %}
<span class="toggle-line"></span>
<span class="toggle-line"></span>
<span class="toggle-line"></span>
{%- endif %}
</div>
<div class="site-nav-search popup-trigger" aria-label="{{ __('menu.search') }}" role="button">
{%- if theme.algolia_search.enable or theme.local_search.enable %}
<i class="fa fa-search fa-fw fa-lg"></i>
{%- endif %}
</div>
<div class="site-meta">
@ -27,11 +23,9 @@
{%- endif %}
</div>
<div class="site-nav-right">
<div class="toggle popup-trigger" aria-label="{{ __('menu.search') }}" role="button">
{%- if theme.algolia_search.enable or theme.local_search.enable %}
<i class="fa fa-search fa-fw fa-lg"></i>
{%- endif %}
</div>
<div class="site-nav-toggle" aria-label="{{ __('accessibility.nav_toggle') }}" role="button">
{%- if theme.menu %}
<i class="fa fa-ellipsis fa-fw fa-lg"></i>
{%- endif %}
</div>
</div>

View File

@ -88,7 +88,7 @@
{%- if config.symbols_count_time.symbols %}
<span class="post-meta-item" title="{{ __('symbols_count_time.count') }}">
<span class="post-meta-item-icon">
<i class="far fa-file-word"></i>
<i class="far fa-file-lines"></i>
</span>
<span class="post-meta-item-text">{{ __('symbols_count_time.count') + __('symbol.colon') }}</span>
<span>{{ symbolsCount(post) }}</span>

View File

@ -1,5 +1,7 @@
{%- if theme.algolia_search.enable or theme.local_search.enable %}
<div class="search-pop-overlay">
{# Keep the popup out of the render tree to prevent Safari from animating it before main.css is applied.
When main.css loads slowly, the unstyled search popup briefly appears and shrinks away. #}
<template id="search-popup-template"><div class="search-pop-overlay">
<div class="popup search-popup">
<div class="search-header">
<span class="search-icon">
@ -20,5 +22,5 @@
</div>
</div>
</div>
</div>
</div></template>
{%- endif %}

View File

@ -6,7 +6,6 @@
<span class="toggle-line"></span>
<span class="toggle-line"></span>
</div>
<div class="sidebar-dimmer"></div>
{%- endif %}
{%- if theme.back2top.enable and not theme.back2top.sidebar %}

View File

@ -1,4 +1,5 @@
{{ next_data('mathjax', theme.math.mathjax, {
js: theme.vendors.mathjax
js: theme.vendors.mathjax,
font_path: theme.vendors.mathjax_font_path
}) }}
{{ next_js('third-party/math/mathjax.js') }}

View File

@ -1,6 +1,7 @@
{%- if theme.firestore.enable %}
{{ next_vendors('firebase_app') }}
{{ next_vendors('firebase_firestore') }}
{{ next_data('firestore', theme.firestore) }}
{{ next_data('firestore', {
collection: theme.firestore.collection,
projectId: theme.firestore.projectId
}) }}
{{ next_js('third-party/statistics/firestore.js') }}
{%- endif %}

View File

@ -1,6 +1,6 @@
{
"name": "hexo-theme-next",
"version": "8.28.0",
"version": "8.29.0",
"description": "Elegant and powerful theme for Hexo.",
"main": "package.json",
"files": [
@ -36,13 +36,13 @@
"homepage": "https://theme-next.js.org",
"devDependencies": {
"@next-theme/eslint-config": "0.0.5",
"c8": "11.0.0",
"c8": "12.0.0",
"chai": "6.2.2",
"eslint": "10.6.0",
"eslint": "10.8.0",
"hexo": "8.1.2",
"hexo-renderer-marked": "7.0.1",
"js-yaml": "4.2.0",
"mocha": "11.7.6",
"js-yaml": "4.3.0",
"mocha": "11.8.0",
"stylelint": "16.26.1",
"stylelint-stylus": "1.0.0"
}

View File

@ -10,7 +10,7 @@ module.exports = hexo => {
hexo.log.warn('Documentation: https://theme-next.js.org/docs/getting-started/configuration.html');
}
const { cache, changyan, growingio_analytics, language_switcher, leancloud_visitors } = hexo.theme.config;
const { body_scrollbar, cache, changyan, growingio_analytics, language_switcher, lazyload, leancloud_visitors } = hexo.theme.config;
const warning = function(...args) {
hexo.log.warn(`Since ${args[0]} is turned on, the ${args[1]} is disabled to avoid potential hazards.`);
};
@ -29,6 +29,14 @@ module.exports = hexo => {
if (growingio_analytics) {
hexo.log.warn('growingio_analytics is deprecated. Please migrate to another analytics provider.');
}
if (body_scrollbar?.stable) {
hexo.log.warn('The `body_scrollbar.stable` theme option is deprecated and will be removed in the next version.');
}
if (lazyload) {
hexo.log.warn('The `lazyload` theme option is deprecated and will be removed in the next version.');
hexo.log.warn('Use `marked.lazyload: true` in Hexo `_config.yml` instead. hexo-renderer-marked will add `loading="lazy"` to images.');
hexo.log.warn('Documentation: https://github.com/hexojs/hexo-renderer-marked#options');
}
if (leancloud_visitors?.enable) {
hexo.log.warn('LeanCloud will stop public services on 2027-01-12. Please migrate your data as soon as possible.');
hexo.log.warn('Announcement: https://docs.leancloud.app/sdk/announcements/sunset-announcement/');

View File

@ -57,5 +57,10 @@ module.exports = hexo => {
// Omit integrity for local assets.
integrity: plugins === 'local' ? undefined : value.integrity
};
if (key === 'mathjax') {
vendors.mathjax_font_path = plugins === 'local'
? url_for.call(hexo, 'lib/@mathjax/mathjax-newcm-font')
: undefined;
}
}
};

View File

@ -19,7 +19,9 @@ hexo.extend.helper.register('next_inject', function(point) {
hexo.extend.helper.register('next_js', function(file, {
pjax = false,
module = false
module = false,
async = false,
defer = !async
} = {}) {
const { next_version } = this;
const { internal, custom_cdn_url } = this.theme.vendors;
@ -32,18 +34,21 @@ hexo.extend.helper.register('next_js', function(file, {
custom : custom_cdn_url
});
const src = links[internal] || links.local;
return `<script ${pjax ? 'data-pjax ' : ''}${module ? 'type="module" ' : ''}src="${src}" defer></script>`;
return `<script ${pjax ? 'data-pjax ' : ''}${module ? 'type="module" ' : ''}src="${src}"${defer ? ' defer' : ''}${async ? ' async' : ''}></script>`;
});
hexo.extend.helper.register('next_vendors', function(name) {
hexo.extend.helper.register('next_vendors', function(name, {
async = false,
defer = !async
} = {}) {
const { url, integrity } = this.theme.vendors[name];
const type = url.endsWith('css') ? 'css' : 'js';
if (type === 'css') {
if (integrity) return `<link rel="stylesheet" href="${url}" integrity="${integrity}" crossorigin="anonymous">`;
return `<link rel="stylesheet" href="${url}">`;
}
if (integrity) return `<script src="${url}" integrity="${integrity}" crossorigin="anonymous" defer></script>`;
return `<script src="${url}" defer></script>`;
if (integrity) return `<script src="${url}" integrity="${integrity}" crossorigin="anonymous"${defer ? ' defer' : ''}${async ? ' async' : ''}></script>`;
return `<script src="${url}"${defer ? ' defer' : ''}${async ? ' async' : ''}></script>`;
});
hexo.extend.helper.register('next_data', function(name, ...data) {

View File

@ -4,7 +4,7 @@
hexo.extend.helper.register('js_vendors', function() {
const { config, theme } = this;
const vendors = ['anime'];
const vendors = [];
if (theme.prism.enable && !config.prismjs.preprocess) {
vendors.push('prism', 'prism_autoloader');
if (config.prismjs.line_number) {

View File

@ -28,11 +28,21 @@
}
if (hexo-config('reward_settings.animation')) {
span {
transform-origin: center bottom;
}
&:hover span {
animation: next-roll .1s infinite linear;
animation: next-bounce 1.4s infinite ease-in-out;
// The animation may affect :hover of img in dark mode
pointer-events: none;
}
@media (prefers-reduced-motion: reduce) {
&:hover span {
animation: none;
}
}
}
}
@ -44,12 +54,28 @@
}
}
@keyframes next-roll {
from {
transform: rotateZ(30deg);
@keyframes next-bounce {
0%, 100% {
transform: translateY(0) scale(1);
}
to {
transform: rotateZ(-30deg);
12% {
transform: translateY(0) scale(1.04, .96);
}
36% {
transform: translateY(-7px) scale(.98, 1.02);
}
58% {
transform: translateY(0) scale(1.03, .97);
}
72% {
transform: translateY(-2px) scale(1);
}
84% {
transform: translateY(0) scale(1);
}
}

View File

@ -1,5 +1,5 @@
if (hexo-config('math.mathjax.enable')) {
mjx-container[jax='CHTML'][display='true'], .has-jax {
.has-jax {
overflow: auto hidden;
}

View File

@ -12,11 +12,10 @@
.site-brand-container {
display: flex;
flex-shrink: 0;
padding: 0 10px;
}
.use-motion {
.column, .site-brand-container .toggle {
.column {
opacity: 0;
}
}

View File

@ -1,20 +1,19 @@
.site-nav-toggle, .site-nav-right {
.site-nav-toggle, .site-nav-search {
color: var(--text-color);
display: none;
padding: 0 10px;
.fa {
transition: transform .5s;
}
+mobile() {
flex-column();
}
}
.toggle {
color: var(--text-color);
padding: 10px;
width: 22px;
.toggle-line {
background: var(--text-color);
border-radius: 1px;
}
}
.site-nav-on .site-nav-toggle .fa {
transform: rotate(-90deg);
}
.site-nav {

View File

@ -20,16 +20,10 @@
padding: 20px 0;
}
.site-nav-toggle, .site-nav-right {
.site-nav-toggle, .site-nav-search {
color: white;
+tablet() {
flex-column();
}
.toggle {
color: white;
.toggle-line {
background: white;
}
}
}

View File

@ -15,7 +15,6 @@ body { margin-top: 2rem; }
}
.use-motion .column,
.use-motion .site-brand-container .toggle,
.use-motion .footer { opacity: initial; }
.use-motion .site-title,

View File

@ -1,4 +1,4 @@
/* global CONFIG */
/* global NexT, CONFIG */
document.addEventListener('DOMContentLoaded', () => {
'use strict';
@ -13,12 +13,7 @@ document.addEventListener('DOMContentLoaded', () => {
// If the page opens with a specific hash, just jump out
if (!isNaN(top) && location.hash === '') {
// Auto scroll to the position
window.anime({
targets : document.scrollingElement,
duration : 200,
easing : 'linear',
scrollTop: top
});
NexT.utils.scrollTo(window, top);
}
};
// Register everything
@ -36,17 +31,25 @@ document.addEventListener('DOMContentLoaded', () => {
// Save the position by clicking the icon
link.addEventListener('click', () => {
doSaveScroll();
window.anime({
targets : link,
if (typeof link.animate !== 'function') {
link.style.top = '-30px';
setTimeout(() => {
link.style.top = '';
}, 600);
return;
}
const animation = link.animate([{}, { top: '-30px' }], {
duration: 200,
easing : 'linear',
top : -30,
complete: () => {
setTimeout(() => {
link.style.top = '';
}, 400);
}
fill : 'forwards'
});
animation.finished.then(() => {
link.style.top = '-30px';
animation.cancel();
setTimeout(() => {
link.style.top = '';
}, 400);
}).catch(() => {});
});
scrollToMark();
document.addEventListener('pjax:success', scrollToMark);

View File

@ -14,16 +14,38 @@ NexT.motion.integrator = {
return this;
},
bootstrap() {
if (typeof Element.prototype.animate !== 'function') {
document.body.classList.remove('use-motion');
CONFIG.motion.enable = false;
return;
}
if (!CONFIG.motion.async) this.queue = [this.queue.flat()];
this.queue.forEach(sequence => {
const timeline = window.anime.timeline({
duration: CONFIG.motion?.duration ?? 200,
easing : 'linear'
});
sequence.forEach(item => {
if (item.deltaT) timeline.add(item, item.deltaT);
else timeline.add(item);
});
this.queue.forEach(sequence => this.schedule(sequence));
},
schedule(sequence) {
let cursor = 0;
sequence.forEach(item => {
const duration = item.duration ?? CONFIG.motion?.duration ?? 200;
const start = Math.max(0, cursor - (item.overlap ?? 0));
const end = start + duration;
cursor = Math.max(cursor, end);
if (item.styles) {
const targets = typeof item.targets === 'string' ? document.querySelectorAll(item.targets) : [item.targets].filter(Boolean);
targets.forEach(target => {
const animation = target.animate([{}, item.styles], {
delay : start,
duration,
easing: 'linear',
fill : 'forwards'
});
animation.finished.then(() => {
Object.assign(target.style, item.styles);
animation.cancel();
}).catch(() => {});
});
}
if (item.complete) setTimeout(item.complete, end);
});
}
};
@ -35,18 +57,17 @@ NexT.motion.middleWares = {
function getMistLineSettings(targets) {
sequence.push({
targets,
scaleX : [0, 1],
styles : { transform: 'scaleX(1)' },
duration: 500,
deltaT : '-=200'
overlap : 200
});
}
function pushToSequence(targets, sequenceQueue = false) {
sequence.push({
targets,
opacity: 1,
top : 0,
deltaT : sequenceQueue ? '-=200' : '-=0'
styles : { opacity: 1, top: '0px' },
overlap: sequenceQueue ? 200 : 0
});
}
@ -64,7 +85,7 @@ NexT.motion.middleWares = {
sequence.push({
targets,
complete: () => targets.classList.add('animated', menuItemTransition),
deltaT : '-=200'
overlap : 200
});
});
}
@ -92,7 +113,7 @@ NexT.motion.middleWares = {
sequence.push({
targets,
complete: () => targets.classList.add('animated', animation),
deltaT : '-=100'
overlap : 100
});
});
}
@ -101,7 +122,7 @@ NexT.motion.middleWares = {
sequence.push({
targets,
complete: () => targets.classList.add('animated', post_block),
deltaT : '-=100'
overlap : 100
});
animate(coll_header, targets.querySelectorAll('.collection-header'));
animate(post_header, targets.querySelectorAll('.post-header'));
@ -123,7 +144,7 @@ NexT.motion.middleWares = {
sequence.push({
targets,
complete: () => targets.classList.add('animated', sidebarTransition),
deltaT : '-=100'
overlap : 100
});
});
}
@ -133,7 +154,7 @@ NexT.motion.middleWares = {
footer() {
return [{
targets: document.querySelector('.footer'),
opacity: 1
styles : { opacity: 1 }
}];
}
};

View File

@ -9,8 +9,7 @@ NexT.boot.registerEvents = function() {
NexT.utils.updateFooterPosition();
// Mobile top menu bar.
document.querySelector('.site-nav-toggle .toggle').addEventListener('click', event => {
event.currentTarget.classList.toggle('toggle-close');
document.querySelector('.site-nav-toggle').addEventListener('click', event => {
const siteNav = document.querySelector('.site-nav');
if (!siteNav) return;
siteNav.style.setProperty('--scroll-height', siteNav.scrollHeight + 'px');

View File

@ -26,7 +26,8 @@ const pjax = new Pjax({
},
analytics: false,
cacheBust: false,
scrollTo : !CONFIG.bookmark.enable
// scrollTo accepts a number (vertical offset), a number array ([x, y]), or false (do not scroll).
scrollTo : CONFIG.bookmark.enable ? false : 0
});
document.addEventListener('pjax:success', () => {

View File

@ -3,13 +3,17 @@
document.addEventListener('DOMContentLoaded', () => {
const isRight = CONFIG.sidebar.position === 'right';
// Create the dimmer dynamically to prevent an unwanted animation before main.css is applied.
const sidebarDimmer = document.createElement('div');
sidebarDimmer.className = 'sidebar-dimmer';
document.body.appendChild(sidebarDimmer);
const sidebarToggleMotion = {
mouse: {},
init() {
window.addEventListener('mousedown', this.mousedownHandler.bind(this));
window.addEventListener('mouseup', this.mouseupHandler.bind(this));
document.querySelector('.sidebar-dimmer').addEventListener('click', this.clickHandler.bind(this));
sidebarDimmer.addEventListener('click', this.clickHandler.bind(this));
document.querySelector('.sidebar-toggle').addEventListener('click', this.clickHandler.bind(this));
window.addEventListener('sidebar:show', this.showSidebar);
window.addEventListener('sidebar:hide', this.hideSidebar);

View File

@ -31,5 +31,8 @@ document.addEventListener('page:loaded', () => {
image.wrap(imageWrapLink);
});
Fancybox.bind('[data-fancybox]');
// Disable hash handling to avoid conflicts with PJAX navigation.
Fancybox.bind('[data-fancybox]', {
Hash: false
});
});

View File

@ -4,6 +4,12 @@ document.addEventListener('page:loaded', () => {
if (!CONFIG.enableMath) return;
if (typeof MathJax === 'undefined') {
const output = {
displayOverflow: CONFIG.mathjax.display_overflow
};
if (CONFIG.mathjax.font_path) {
output.fontPath = CONFIG.mathjax.font_path;
}
window.MathJax = {
tex: {
inlineMath: { '[+]': [['$', '$']] },
@ -12,7 +18,7 @@ document.addEventListener('page:loaded', () => {
options: {
renderActions: {
insertedScript: [200, () => {
document.querySelectorAll('mjx-container').forEach(node => {
document.querySelectorAll('mjx-container:not([display])').forEach(node => {
const target = node.parentNode;
if (target.nodeName.toLowerCase() === 'li') {
target.parentNode.classList.add('has-jax');
@ -20,7 +26,8 @@ document.addEventListener('page:loaded', () => {
});
}, '', false]
}
}
},
output
};
NexT.utils.getScript(CONFIG.mathjax.js, {
attributes: {

View File

@ -1,6 +1,7 @@
/* global CONFIG, NexT, pjax */
document.addEventListener('DOMContentLoaded', () => {
document.body.appendChild(document.querySelector('#search-popup-template').content.cloneNode(true));
const { indexName, appID, apiKey, hits } = CONFIG.algolia;
const client = window['algoliasearch/lite'].liteClient(appID, apiKey);
@ -19,83 +20,92 @@ document.addEventListener('DOMContentLoaded', () => {
return result;
};
let isSearching = false;
let pendingQuery = null;
let latestSearchId = 0;
const searchAlgolia = async (searchText, page = 0) => {
if (isSearching) {
pendingQuery = { searchText, page };
return;
}
isSearching = true;
const searchAlgolia = async (searchText, page, searchId) => {
if (searchId !== latestSearchId) return;
container.setAttribute('aria-busy', 'true');
const startTime = Date.now();
const result = await client.search({
requests: [{
indexName,
page,
query : searchText,
hitsPerPage : hits.per_page || 10,
attributesToRetrieve : ['permalink'],
attributesToHighlight: ['title', 'excerpt', 'excerptStrip', 'contentStripTruncate'],
highlightPreTag : '<mark class="search-keyword">',
highlightPostTag : '</mark>'
}]
});
const data = result.results[0];
if (data.nbHits === 0) {
container.innerHTML = '<div class="search-result-icon"><i class="far fa-frown fa-5x"></i></div>';
} else {
const stats = CONFIG.i18n.hits_time
.replace('${hits}', data.nbHits)
.replace('${time}', Date.now() - startTime);
let pagination = '';
if (data.nbPages > 1) {
pagination += '<nav class="pagination algolia-pagination">';
for (let i = 0; i < data.nbPages; i++) {
if (i === page) {
pagination += `<span class="page-number current">${i + 1}</span>`;
} else {
pagination += `<a class="page-number" href="#" data-index=${i}>${i + 1}</a>`;
}
}
pagination += '</nav>';
}
try {
const result = await client.search({
requests: [{
indexName,
page,
query : searchText,
hitsPerPage : hits.per_page || 10,
attributesToRetrieve : ['permalink'],
attributesToHighlight: ['title', 'excerpt', 'excerptStrip', 'contentStripTruncate'],
highlightPreTag : '<mark class="search-keyword">',
highlightPostTag : '</mark>'
}]
});
if (searchId !== latestSearchId) return;
container.innerHTML = `<div class="search-stats">
const data = result.results[0];
if (data.nbHits === 0) {
container.innerHTML = '<div class="search-result-icon"><i class="far fa-frown fa-5x"></i></div>';
} else {
const stats = CONFIG.i18n.hits_time
.replace('${hits}', data.nbHits)
.replace('${time}', Date.now() - startTime);
let pagination = '';
if (data.nbPages > 1) {
pagination += '<nav class="pagination algolia-pagination">';
for (let i = 0; i < data.nbPages; i++) {
if (i === page) {
pagination += `<span class="page-number current">${i + 1}</span>`;
} else {
pagination += `<a class="page-number" href="#" data-index=${i}>${i + 1}</a>`;
}
}
pagination += '</nav>';
}
container.innerHTML = `<div class="search-stats">
<span>${stats}</span>
<img src="${CONFIG.images}/logo-algolia-nebula-blue-full.svg" alt="Algolia">
</div>
<hr>
<ul class="search-result-list">${data.hits.map(formatHits).join('')}</ul>
${pagination}`;
if (typeof pjax === 'object') pjax.refresh(container);
container.querySelectorAll('.page-number').forEach(element => {
element.addEventListener('click', async event => {
event.preventDefault();
await searchAlgolia(searchText, Number(element.dataset.index));
if (typeof pjax === 'object') pjax.refresh(container);
container.querySelectorAll('.page-number').forEach(element => {
element.addEventListener('click', async event => {
event.preventDefault();
if (input.value.trim() !== searchText) return;
const searchId = ++latestSearchId;
await searchAlgolia(searchText, Number(element.dataset.index), searchId);
});
});
});
}
isSearching = false;
if (pendingQuery !== null && (pendingQuery.searchText !== searchText || pendingQuery.page !== page)) {
const { searchText, page } = pendingQuery;
pendingQuery = null;
searchAlgolia(searchText, page);
}
} catch (error) {
if (searchId !== latestSearchId) return;
console.warn('Algolia search failed:', error);
container.innerHTML = '<div class="search-result-icon"><i class="far fa-frown fa-5x"></i></div>';
} finally {
if (searchId === latestSearchId) container.removeAttribute('aria-busy');
}
};
const inputEventFunction = async () => {
const inputEventFunction = async searchId => {
if (searchId !== latestSearchId) return;
const searchText = input.value.trim();
if (searchText === '') {
container.innerHTML = '<div class="search-result-icon"><i class="fab fa-algolia fa-5x"></i></div>';
return;
}
if (searchText === '') return;
// Algolia client will automatically cache the data for same queries
await searchAlgolia(searchText, 0);
await searchAlgolia(searchText, 0, searchId);
};
const debouncedSearch = NexT.utils.debounce(inputEventFunction, 500);
input.addEventListener('input', debouncedSearch);
input.addEventListener('input', () => {
const searchId = ++latestSearchId;
if (input.value.trim() === '') {
container.removeAttribute('aria-busy');
container.innerHTML = '<div class="search-result-icon"><i class="fab fa-algolia fa-5x"></i></div>';
} else {
container.setAttribute('aria-busy', 'true');
}
debouncedSearch(searchId);
});
// Handle and trigger popup window
document.querySelectorAll('.popup-trigger').forEach(element => {

View File

@ -6,6 +6,7 @@ document.addEventListener('DOMContentLoaded', () => {
console.warn('`hexo-generator-searchdb` plugin is not installed!');
return;
}
document.body.appendChild(document.querySelector('#search-popup-template').content.cloneNode(true));
const localSearch = new LocalSearch({
path : CONFIG.path,
top_n_per_article: CONFIG.localsearch.top_n_per_article,

View File

@ -1,57 +1,95 @@
/* global CONFIG, firebase */
firebase.initializeApp({
apiKey : CONFIG.firestore.apiKey,
projectId: CONFIG.firestore.projectId
});
/* global CONFIG */
(function() {
const getCount = async (doc, increaseCount) => {
// IncreaseCount will be false when not in article page
const d = await doc.get();
// Has no data, initialize count
let count = d.exists ? d.data().count : 0;
// If first view this article
if (increaseCount) {
// Increase count
count++;
doc.set({
count
});
}
return count;
const database = `projects/${CONFIG.firestore.projectId}/databases/(default)`;
const documents = `${database}/documents`;
const api = `https://firestore.googleapis.com/v1/${documents}`;
const fetchFirestore = async (path, options) => {
const response = await fetch(`${api}${path}`, options);
if (response.status === 404) return null;
if (!response.ok) throw new Error(`Firestore request failed with status ${response.status}`);
return response.json();
};
const db = firebase.firestore();
const articles = db.collection(CONFIG.firestore.collection);
const documentName = title => `${documents}/${CONFIG.firestore.collection}/${title}`;
const getValue = value => Number(value?.integerValue ?? value?.doubleValue ?? 0);
const getCount = async (title, increaseCount) => {
if (increaseCount) {
const data = await fetchFirestore(':commit', {
method : 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
writes: [{
transform: {
document : documentName(title),
fieldTransforms: [{
fieldPath: 'count',
increment: {
integerValue: '1'
}
}]
}
}]
})
});
return getValue(data.writeResults[0].transformResults[0]);
}
const collection = encodeURIComponent(CONFIG.firestore.collection);
const document = await fetchFirestore(`/${collection}/${encodeURIComponent(title)}`);
return getValue(document?.fields?.count);
};
const getCounts = async titles => {
if (titles.length === 0) return [];
const data = await fetchFirestore(':batchGet', {
method : 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
documents: titles.map(documentName),
mask : {
fieldPaths: ['count']
}
})
});
const counts = new Map(data.map(result => {
const document = result.found;
return [document?.name ?? result.missing, getValue(document?.fields?.count)];
}));
return titles.map(title => counts.get(documentName(title)) ?? 0);
};
document.addEventListener('page:loaded', async () => {
if (CONFIG.page.isPost) {
// Fix issue #118
// https://developer.mozilla.org/en-US/docs/Web/API/Node/textContent
const title = document.querySelector('.post-title').textContent.trim();
const doc = articles.doc(title);
let increaseCount = CONFIG.hostname === location.hostname;
if (localStorage.getItem(title)) {
increaseCount = false;
} else {
// Mark as visited
localStorage.setItem(title, true);
try {
if (CONFIG.page.isPost) {
// Fix issue #118
// https://developer.mozilla.org/en-US/docs/Web/API/Node/textContent
const title = document.querySelector('.post-title').textContent.trim();
let increaseCount = CONFIG.hostname === location.hostname;
if (localStorage.getItem(title)) {
increaseCount = false;
} else {
// Mark as visited
localStorage.setItem(title, true);
}
const count = await getCount(title, increaseCount);
document.querySelector('.firestore-visitors-count').innerText = count;
} else if (CONFIG.page.isHome) {
const titles = [...document.querySelectorAll('.post-title')].map(element => element.textContent.trim());
const counts = await getCounts(titles);
const metas = document.querySelectorAll('.firestore-visitors-count');
counts.forEach((val, idx) => {
metas[idx].innerText = val;
});
}
const count = await getCount(doc, increaseCount);
document.querySelector('.firestore-visitors-count').innerText = count;
} else if (CONFIG.page.isHome) {
const promises = [...document.querySelectorAll('.post-title')].map(element => {
const title = element.textContent.trim();
const doc = articles.doc(title);
return getCount(doc);
});
const counts = await Promise.all(promises);
const metas = document.querySelectorAll('.firestore-visitors-count');
counts.forEach((val, idx) => {
metas[idx].innerText = val;
});
} catch (error) {
console.warn('Failed to load Firestore visitor count:', error);
}
});
})();

View File

@ -19,6 +19,17 @@ HTMLElement.prototype.wrap = function(wrapper) {
NexT.utils = {
scrollTo(target, top) {
if (window.CSS?.supports('scroll-behavior', 'smooth')) {
target.scrollTo({
top,
behavior: 'smooth'
});
} else {
target.scrollTo(0, top);
}
},
registerExtURL() {
document.querySelectorAll('span.exturl').forEach(element => {
const link = document.createElement('a');
@ -193,12 +204,7 @@ NexT.utils = {
}, { passive: true });
backToTop?.addEventListener('click', () => {
window.anime({
targets : document.scrollingElement,
duration : 500,
easing : 'linear',
scrollTop: 0
});
NexT.utils.scrollTo(window, 0);
});
},
@ -258,12 +264,7 @@ NexT.utils = {
}, 1000);
if (!CONFIG.stickytabs) return;
const offset = nav.parentNode.getBoundingClientRect().top + window.scrollY + 10;
window.anime({
targets : document.scrollingElement,
duration : 500,
easing : 'linear',
scrollTop: offset
});
NexT.utils.scrollTo(window, offset);
});
});
@ -311,15 +312,8 @@ NexT.utils = {
element.addEventListener('click', event => {
event.preventDefault();
const offset = target.getBoundingClientRect().top + window.scrollY;
window.anime({
targets : document.scrollingElement,
duration : 500,
easing : 'linear',
scrollTop: offset,
complete : () => {
history.pushState(null, document.title, element.href);
}
});
NexT.utils.scrollTo(window, offset);
history.pushState(null, document.title, element.href);
});
return target;
});
@ -342,8 +336,6 @@ NexT.utils = {
const target = navItemList[index];
if (!target || target.classList.contains('active-current')) return;
const singleHeight = navItemList[navItemList.length - 1].offsetHeight;
nav.querySelectorAll('.active').forEach(navItem => {
navItem.classList.remove('active', 'active-current');
});
@ -356,9 +348,13 @@ NexT.utils = {
if (activateEle.classList.contains('nav-item')) {
activateEle.classList.add('active');
} else { // .nav-child or .nav
// scrollHeight isn't reliable for transitioning child items.
// Exclude nested lists so each item's own wrapped row is counted once.
const listItemHeight = [...activateEle.children].reduce((height, navItem) => {
const navChild = [...navItem.children].find(element => element.classList.contains('nav-child'));
return height + navItem.getBoundingClientRect().height - (navChild?.getBoundingClientRect().height || 0);
}, 0);
// The last nav-item in a list has a margin-bottom of 5px.
navChildHeight += (singleHeight * activateEle.childElementCount) + 5;
navChildHeight += listItemHeight + 5;
activateEle.style.setProperty('--height', `${navChildHeight}px`);
}
activateEle = activateEle.parentElement;
@ -367,12 +363,7 @@ NexT.utils = {
// Scrolling to center active TOC element if TOC content is taller then viewport.
const tocElement = document.querySelector(CONFIG.scheme === 'Pisces' || CONFIG.scheme === 'Gemini' ? '.sidebar-panel-container' : '.sidebar');
if (!document.querySelector('.sidebar-toc-active')) return;
window.anime({
targets : tocElement,
duration : 200,
easing : 'linear',
scrollTop: tocElement.scrollTop - (tocElement.offsetHeight / 2) + target.getBoundingClientRect().top - tocElement.getBoundingClientRect().top
});
NexT.utils.scrollTo(tocElement, tocElement.scrollTop - (tocElement.offsetHeight / 2) + target.getBoundingClientRect().top - tocElement.getBoundingClientRect().top);
},
updateSidebarPosition() {

73
test/helpers/engine.js Normal file
View File

@ -0,0 +1,73 @@
'use strict';
const helpers = {};
global.hexo = {
extend: {
helper: {
register(name, helper) {
helpers[name] = helper;
}
}
}
};
require('../../scripts/helpers/engine');
delete global.hexo;
describe('engine', () => {
const context = {
next_version: '8.28.0',
theme : {
js : 'js',
vendors: {
internal : 'local',
custom_cdn_url: '',
library : {
url: 'https://cdn.example.com/library.js'
},
secure_library: {
url : 'https://cdn.example.com/secure.js',
integrity: 'sha256-example'
},
stylesheet: {
url: 'https://cdn.example.com/library.css'
}
}
},
url_for(path) {
return '/' + path;
}
};
const nextJs = helpers.next_js.bind(context);
const nextVendors = helpers.next_vendors.bind(context);
it('keeps defer as the default for NexT scripts', () => {
nextJs('main.js').should.eql('<script src="/js/main.js" defer></script>');
});
it('supports custom loading options for NexT scripts', () => {
nextJs('main.js', { defer: false }).should.eql('<script src="/js/main.js"></script>');
nextJs('main.js', { async: true }).should.eql('<script src="/js/main.js" async></script>');
nextJs('main.js', { defer: true, async: true }).should.eql('<script src="/js/main.js" defer async></script>');
});
it('preserves existing NexT script options', () => {
nextJs('main.js', { pjax: true, module: true }).should.eql('<script data-pjax type="module" src="/js/main.js" defer></script>');
});
it('keeps defer as the default for vendor scripts', () => {
nextVendors('library').should.eql('<script src="https://cdn.example.com/library.js" defer></script>');
nextVendors('secure_library').should.eql('<script src="https://cdn.example.com/secure.js" integrity="sha256-example" crossorigin="anonymous" defer></script>');
});
it('supports custom loading options for vendor scripts', () => {
nextVendors('library', { defer: false }).should.eql('<script src="https://cdn.example.com/library.js"></script>');
nextVendors('library', { async: true }).should.eql('<script src="https://cdn.example.com/library.js" async></script>');
});
it('does not apply script loading options to stylesheets', () => {
nextVendors('stylesheet', { async: true }).should.eql('<link rel="stylesheet" href="https://cdn.example.com/library.css">');
});
});

View File

@ -1,6 +1,7 @@
'use strict';
describe('Helpers', () => {
require('./engine');
require('./font');
require('./next-url');
});