Compare commits

..

No commits in common. "master" and "v8.28.0" have entirely different histories.

40 changed files with 306 additions and 470 deletions

View File

@ -7,7 +7,7 @@ jobs:
triage:
runs-on: ubuntu-latest
steps:
- uses: actions/labeler@v7
- uses: actions/labeler@v6
# 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@v7
uses: actions/setup-node@v6
- 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@v7
uses: actions/setup-python@v6
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@v7
uses: actions/setup-node@v6
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@v7
uses: actions/setup-node@v6
- 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://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">
<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">
</picture>
</a>

View File

@ -88,6 +88,7 @@ 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>
@ -104,7 +105,7 @@ menu:
#home: / || fa fa-home
#about: /about/ || fa fa-user
#tags: /tags/ || fa fa-tags
#categories: /categories/ || fa fa-layer-group
#categories: /categories/ || fa fa-th
#archives: /archives/ || fa fa-archive
#schedule: /schedule/ || fa fa-calendar
#sitemap: /sitemap.xml || fa fa-sitemap
@ -364,6 +365,8 @@ 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
@ -572,8 +575,6 @@ math:
enable: false
# Available values: none | ams | all
tags: none
# Available values: overflow | scroll | scale | truncate | linebreak
display_overflow: scroll
katex:
enable: false
@ -599,6 +600,10 @@ 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
@ -809,13 +814,13 @@ plausible:
script_url: # https://plausible.io/js/script.js
site_domain: # www.example.com
# 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.
# 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.
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,12 +1,17 @@
# 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.3.1
version: 7.0.0
file: css/all.min.css
alias: font-awesome
integrity: sha256-4Lad8m4ZWW1Lgb9+sMVLYEfnIh7BjV1NQMEe79Pviks=
integrity: sha256-VHqXKFhhMxcpubYf9xiWdCiojEbY9NexQ4jh8AxbvcM=
prism:
name: prismjs
version: 1.30.0
@ -27,21 +32,21 @@ prism_line_numbers:
integrity: sha256-9cmf7tcLdXpKsPi/2AWE93PbZpTp4M4tqzFk+lWomjU=
mathjax:
name: mathjax
version: 4.1.3
file: tex-mml-chtml.js
integrity: sha256-ihPtDDm7pTmSF9ViEsItlvsrBTsNkz0MOXMchzOwCQw=
version: 3.2.2
file: es5/tex-mml-chtml.js
integrity: sha256-MASABpB4tYktI2Oitl4t+78w/lyA+D7b/s9GEP0JOGI=
katex:
name: katex
version: 0.18.4
version: 0.16.9
file: dist/katex.min.css
alias: KaTeX
integrity: sha256-GAwtd9Q019pR1mJcUKlk1P1v29ubyHlqCgFsMMSZMfs=
integrity: sha256-UF1fgpAiu3tPJN/uCqEUHNe7pnr+QR0SQDNfgglgtcM=
copy_tex_js:
name: katex
version: 0.18.4
version: 0.16.9
file: dist/contrib/copy-tex.min.js
alias: KaTeX
integrity: sha256-xXHEm1IMuOkEAoczgoayF4Pjm0dmYLi1bZ4cA37q6v8=
integrity: sha256-Us54+rSGDSTvIhKKUs4kygE2ipA0RXpWWh0/zLqw3bs=
pjax:
name: '@next-theme/pjax'
version: 0.6.0
@ -50,16 +55,16 @@ pjax:
integrity: sha256-vxLn1tSKWD4dqbMRyv940UYw4sXgMtYcK6reefzZrao=
fancybox_js:
name: '@fancyapps/ui'
version: 6.1.14
version: 5.0.31
file: dist/fancybox/fancybox.umd.js
alias: fancyapps-ui
integrity: sha256-QnJV4HT5r8i89ovZGRD86iynZqHerEbT2xt69vDM36I=
integrity: sha256-a+H7FYzJv6oU2hfsfDGM2Ohw/cR9v+hPfxHCLdmCrE8=
fancybox_css:
name: '@fancyapps/ui'
version: 6.1.14
version: 5.0.31
file: dist/fancybox/fancybox.css
alias: fancyapps-ui
integrity: sha256-mdaMxrItEW6hMIVJJNPKuX0B7KCSJQVEv3mVY4N2we4=
integrity: sha256-gkQVf8UKZgQ0HyuxL/VnacadJ+D2Kox2TCEBuNQg5+w=
mediumzoom:
name: medium-zoom
version: 1.1.0
@ -73,24 +78,24 @@ lazyload:
integrity: sha256-mOFREFhqmHeQbXpK2lp4nA3qooVgACfh88fpJftLBbc=
pangu:
name: pangu
version: 7.2.1
version: 7.2.0
file: dist/browser/pangu.umd.js
integrity: sha256-kRsxecBnwG6mULFPe2E6UKnNm8XfDOuDwCOprjLZuX8=
integrity: sha256-JnmRRnJK7DC6RQJbAJb6AXOM9OmWzS6z8eYultk/48Y=
quicklink:
name: quicklink
version: 3.0.2
version: 3.0.1
file: dist/quicklink.umd.js
integrity: sha256-Qo2BTvlsBVXIr5fEHp8xlBpUmkbDINr5cQZeSfMTES8=
integrity: sha256-44BednzIpUeQJcY8qtLyarFu0UCCTbgmWOvaoehiFQQ=
disqusjs_js:
name: disqusjs
version: 3.2.1
version: 3.1.0
file: dist/browser/disqusjs.es2015.umd.min.js
integrity: sha256-6a5jzAWTjd18Nq6rZZn1x9hZLDZAilRDnd0arUFteBM=
integrity: sha256-gdbBT62HLzqWTnXQED3WQL/ItFCuWsA5gd6dM/wgdiE=
disqusjs_css:
name: disqusjs
version: 3.2.1
version: 3.1.0
file: dist/browser/styles/disqusjs.css
integrity: sha256-9SkCf6N7ls5uSFyENCeHM+m7Qv3lHH9Eg/CxdCajarI=
integrity: sha256-E4RzceSomICFY1PT/yDQrN8DsLSwdScDEiQAevHabp0=
gitalk_js:
name: gitalk
version: 1.8.0
@ -101,11 +106,21 @@ 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.57.0
version: 5.36.0
file: dist/lite/builds/browser.umd.js
integrity: sha256-NJrnXIMFVRGMM2IieD83YpTvsPPIFKKyj4K/MNyTRQU=
integrity: sha256-o49zoHLDPBm9WLg6AiliAVi3axCRTqe3ltTxLvhzAVg=
local_search:
name: hexo-generator-searchdb
version: 1.5.0
@ -149,11 +164,11 @@ creative_commons:
alias: creativecommons-vocabulary
wavedrom:
name: wavedrom
version: 3.6.2
version: 3.5.0
file: wavedrom.min.js
integrity: sha256-3hPWxTqi1K1sjmlESiOTH6lfGwYmz8/SFqoAcPxrjwg=
integrity: sha256-INLAoJc6quTNfiMWkGZniYO2cxE8mHpddnLow1m6RFs=
wavedrom_skin:
name: wavedrom
version: 3.6.2
version: 3.5.0
file: skins/default.js
integrity: sha256-bUKjUpel+qLgflZd3EfdbhIorwRYgSkaZKM2s/Bi0VI=
integrity: sha256-fduc/Zszk5ezWws2uInY/ALWVmIrmV6VTgXbsYSReFI=

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://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">
<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">
</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://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">
<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">
</picture>
</a>

View File

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

View File

@ -1,8 +1,12 @@
<div class="site-brand-container">
<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 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>
<div class="site-meta">
@ -23,9 +27,11 @@
{%- 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 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>
</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-lines"></i>
<i class="far fa-file-word"></i>
</span>
<span class="post-meta-item-text">{{ __('symbols_count_time.count') + __('symbol.colon') }}</span>
<span>{{ symbolsCount(post) }}</span>

View File

@ -1,7 +1,5 @@
{%- if theme.algolia_search.enable or theme.local_search.enable %}
{# 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="search-pop-overlay">
<div class="popup search-popup">
<div class="search-header">
<span class="search-icon">
@ -22,5 +20,5 @@
</div>
</div>
</div>
</div></template>
</div>
{%- endif %}

View File

@ -6,6 +6,7 @@
<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,5 +1,4 @@
{{ next_data('mathjax', theme.math.mathjax, {
js: theme.vendors.mathjax,
font_path: theme.vendors.mathjax_font_path
js: theme.vendors.mathjax
}) }}
{{ next_js('third-party/math/mathjax.js') }}

View File

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

View File

@ -1,6 +1,6 @@
{
"name": "hexo-theme-next",
"version": "8.29.0",
"version": "8.28.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": "12.0.0",
"c8": "11.0.0",
"chai": "6.2.2",
"eslint": "10.8.0",
"eslint": "10.6.0",
"hexo": "8.1.2",
"hexo-renderer-marked": "7.0.1",
"js-yaml": "4.3.0",
"mocha": "11.8.0",
"js-yaml": "4.2.0",
"mocha": "11.7.6",
"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 { body_scrollbar, cache, changyan, growingio_analytics, language_switcher, lazyload, leancloud_visitors } = hexo.theme.config;
const { cache, changyan, growingio_analytics, language_switcher, 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,14 +29,6 @@ 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,10 +57,5 @@ 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,9 +19,7 @@ hexo.extend.helper.register('next_inject', function(point) {
hexo.extend.helper.register('next_js', function(file, {
pjax = false,
module = false,
async = false,
defer = !async
module = false
} = {}) {
const { next_version } = this;
const { internal, custom_cdn_url } = this.theme.vendors;
@ -34,21 +32,18 @@ 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 ? ' defer' : ''}${async ? ' async' : ''}></script>`;
return `<script ${pjax ? 'data-pjax ' : ''}${module ? 'type="module" ' : ''}src="${src}" defer></script>`;
});
hexo.extend.helper.register('next_vendors', function(name, {
async = false,
defer = !async
} = {}) {
hexo.extend.helper.register('next_vendors', function(name) {
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 ? ' defer' : ''}${async ? ' async' : ''}></script>`;
return `<script src="${url}"${defer ? ' defer' : ''}${async ? ' async' : ''}></script>`;
if (integrity) return `<script src="${url}" integrity="${integrity}" crossorigin="anonymous" defer></script>`;
return `<script src="${url}" defer></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 = [];
const vendors = ['anime'];
if (theme.prism.enable && !config.prismjs.preprocess) {
vendors.push('prism', 'prism_autoloader');
if (config.prismjs.line_number) {

View File

@ -28,21 +28,11 @@
}
if (hexo-config('reward_settings.animation')) {
span {
transform-origin: center bottom;
}
&:hover span {
animation: next-bounce 1.4s infinite ease-in-out;
animation: next-roll .1s infinite linear;
// The animation may affect :hover of img in dark mode
pointer-events: none;
}
@media (prefers-reduced-motion: reduce) {
&:hover span {
animation: none;
}
}
}
}
@ -54,28 +44,12 @@
}
}
@keyframes next-bounce {
0%, 100% {
transform: translateY(0) scale(1);
@keyframes next-roll {
from {
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);
to {
transform: rotateZ(-30deg);
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -14,38 +14,16 @@ 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 => 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);
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);
});
});
}
};
@ -57,17 +35,18 @@ NexT.motion.middleWares = {
function getMistLineSettings(targets) {
sequence.push({
targets,
styles : { transform: 'scaleX(1)' },
scaleX : [0, 1],
duration: 500,
overlap : 200
deltaT : '-=200'
});
}
function pushToSequence(targets, sequenceQueue = false) {
sequence.push({
targets,
styles : { opacity: 1, top: '0px' },
overlap: sequenceQueue ? 200 : 0
opacity: 1,
top : 0,
deltaT : sequenceQueue ? '-=200' : '-=0'
});
}
@ -85,7 +64,7 @@ NexT.motion.middleWares = {
sequence.push({
targets,
complete: () => targets.classList.add('animated', menuItemTransition),
overlap : 200
deltaT : '-=200'
});
});
}
@ -113,7 +92,7 @@ NexT.motion.middleWares = {
sequence.push({
targets,
complete: () => targets.classList.add('animated', animation),
overlap : 100
deltaT : '-=100'
});
});
}
@ -122,7 +101,7 @@ NexT.motion.middleWares = {
sequence.push({
targets,
complete: () => targets.classList.add('animated', post_block),
overlap : 100
deltaT : '-=100'
});
animate(coll_header, targets.querySelectorAll('.collection-header'));
animate(post_header, targets.querySelectorAll('.post-header'));
@ -144,7 +123,7 @@ NexT.motion.middleWares = {
sequence.push({
targets,
complete: () => targets.classList.add('animated', sidebarTransition),
overlap : 100
deltaT : '-=100'
});
});
}
@ -154,7 +133,7 @@ NexT.motion.middleWares = {
footer() {
return [{
targets: document.querySelector('.footer'),
styles : { opacity: 1 }
opacity: 1
}];
}
};

View File

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

View File

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

View File

@ -3,17 +3,13 @@
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));
sidebarDimmer.addEventListener('click', this.clickHandler.bind(this));
document.querySelector('.sidebar-dimmer').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,8 +31,5 @@ document.addEventListener('page:loaded', () => {
image.wrap(imageWrapLink);
});
// Disable hash handling to avoid conflicts with PJAX navigation.
Fancybox.bind('[data-fancybox]', {
Hash: false
});
Fancybox.bind('[data-fancybox]');
});

View File

@ -4,12 +4,6 @@ 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: { '[+]': [['$', '$']] },
@ -18,7 +12,7 @@ document.addEventListener('page:loaded', () => {
options: {
renderActions: {
insertedScript: [200, () => {
document.querySelectorAll('mjx-container:not([display])').forEach(node => {
document.querySelectorAll('mjx-container').forEach(node => {
const target = node.parentNode;
if (target.nodeName.toLowerCase() === 'li') {
target.parentNode.classList.add('has-jax');
@ -26,8 +20,7 @@ document.addEventListener('page:loaded', () => {
});
}, '', false]
}
},
output
}
};
NexT.utils.getScript(CONFIG.mathjax.js, {
attributes: {

View File

@ -1,7 +1,6 @@
/* 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);
@ -20,92 +19,83 @@ document.addEventListener('DOMContentLoaded', () => {
return result;
};
let latestSearchId = 0;
let isSearching = false;
let pendingQuery = null;
const searchAlgolia = async (searchText, page, searchId) => {
if (searchId !== latestSearchId) return;
container.setAttribute('aria-busy', 'true');
const searchAlgolia = async (searchText, page = 0) => {
if (isSearching) {
pendingQuery = { searchText, page };
return;
}
isSearching = true;
const startTime = Date.now();
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;
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>`;
}
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>';
}
pagination += '</nav>';
}
container.innerHTML = `<div class="search-stats">
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();
if (input.value.trim() !== searchText) return;
const searchId = ++latestSearchId;
await searchAlgolia(searchText, Number(element.dataset.index), searchId);
});
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));
});
}
} 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');
});
}
isSearching = false;
if (pendingQuery !== null && (pendingQuery.searchText !== searchText || pendingQuery.page !== page)) {
const { searchText, page } = pendingQuery;
pendingQuery = null;
searchAlgolia(searchText, page);
}
};
const inputEventFunction = async searchId => {
if (searchId !== latestSearchId) return;
const inputEventFunction = async () => {
const searchText = input.value.trim();
if (searchText === '') return;
if (searchText === '') {
container.innerHTML = '<div class="search-result-icon"><i class="fab fa-algolia fa-5x"></i></div>';
return;
}
// Algolia client will automatically cache the data for same queries
await searchAlgolia(searchText, 0, searchId);
await searchAlgolia(searchText, 0);
};
const debouncedSearch = NexT.utils.debounce(inputEventFunction, 500);
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);
});
input.addEventListener('input', debouncedSearch);
// Handle and trigger popup window
document.querySelectorAll('.popup-trigger').forEach(element => {

View File

@ -6,7 +6,6 @@ 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,95 +1,57 @@
/* global CONFIG */
/* global CONFIG, firebase */
firebase.initializeApp({
apiKey : CONFIG.firestore.apiKey,
projectId: CONFIG.firestore.projectId
});
(function() {
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 documentName = title => `${documents}/${CONFIG.firestore.collection}/${title}`;
const getValue = value => Number(value?.integerValue ?? value?.doubleValue ?? 0);
const getCount = async (title, increaseCount) => {
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) {
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'
}
}]
}
}]
})
// Increase count
count++;
doc.set({
count
});
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);
return 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);
};
const db = firebase.firestore();
const articles = db.collection(CONFIG.firestore.collection);
document.addEventListener('page:loaded', async () => {
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;
});
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);
}
} catch (error) {
console.warn('Failed to load Firestore visitor count:', error);
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;
});
}
});
})();

View File

@ -19,17 +19,6 @@ 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');
@ -204,7 +193,12 @@ NexT.utils = {
}, { passive: true });
backToTop?.addEventListener('click', () => {
NexT.utils.scrollTo(window, 0);
window.anime({
targets : document.scrollingElement,
duration : 500,
easing : 'linear',
scrollTop: 0
});
});
},
@ -264,7 +258,12 @@ NexT.utils = {
}, 1000);
if (!CONFIG.stickytabs) return;
const offset = nav.parentNode.getBoundingClientRect().top + window.scrollY + 10;
NexT.utils.scrollTo(window, offset);
window.anime({
targets : document.scrollingElement,
duration : 500,
easing : 'linear',
scrollTop: offset
});
});
});
@ -312,8 +311,15 @@ NexT.utils = {
element.addEventListener('click', event => {
event.preventDefault();
const offset = target.getBoundingClientRect().top + window.scrollY;
NexT.utils.scrollTo(window, offset);
history.pushState(null, document.title, element.href);
window.anime({
targets : document.scrollingElement,
duration : 500,
easing : 'linear',
scrollTop: offset,
complete : () => {
history.pushState(null, document.title, element.href);
}
});
});
return target;
});
@ -336,6 +342,8 @@ 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');
});
@ -348,13 +356,9 @@ NexT.utils = {
if (activateEle.classList.contains('nav-item')) {
activateEle.classList.add('active');
} else { // .nav-child or .nav
// 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);
// scrollHeight isn't reliable for transitioning child items.
// The last nav-item in a list has a margin-bottom of 5px.
navChildHeight += listItemHeight + 5;
navChildHeight += (singleHeight * activateEle.childElementCount) + 5;
activateEle.style.setProperty('--height', `${navChildHeight}px`);
}
activateEle = activateEle.parentElement;
@ -363,7 +367,12 @@ 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;
NexT.utils.scrollTo(tocElement, tocElement.scrollTop - (tocElement.offsetHeight / 2) + target.getBoundingClientRect().top - tocElement.getBoundingClientRect().top);
window.anime({
targets : tocElement,
duration : 200,
easing : 'linear',
scrollTop: tocElement.scrollTop - (tocElement.offsetHeight / 2) + target.getBoundingClientRect().top - tocElement.getBoundingClientRect().top
});
},
updateSidebarPosition() {

View File

@ -1,73 +0,0 @@
'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,7 +1,6 @@
'use strict';
describe('Helpers', () => {
require('./engine');
require('./font');
require('./next-url');
});