diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml
index 4f2da0e3683..c665f755e11 100644
--- a/.github/workflows/cd.yml
+++ b/.github/workflows/cd.yml
@@ -2,13 +2,12 @@ name: CD
on:
push:
- branches:
- - production
- tags-ignore:
- - "**"
+ branches: [production]
+ tags-ignore: ["**"]
jobs:
release:
+ if: ${{ ! startsWith(github.event.head_commit.message, 'chore(release)') }}
permissions:
contents: write
issues: write
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 50a158b10a9..31b878632b1 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -1,17 +1,25 @@
-name: "CI"
+name: CI
+
on:
push:
branches:
- - "master"
- - "hotfix/**"
+ - master
+ - "hotfix/*"
paths-ignore:
- ".github/**"
- "!.github/workflows/ci.yml"
- - ".gitignore"
+ - .gitignore
- "docs/**"
- - "README.md"
- - "LICENSE"
+ - README.md
+ - LICENSE
pull_request:
+ paths-ignore:
+ - ".github/**"
+ - "!.github/workflows/ci.yml"
+ - .gitignore
+ - "docs/**"
+ - README.md
+ - LICENSE
jobs:
build:
diff --git a/.github/workflows/pr-filter.yml b/.github/workflows/pr-filter.yml
new file mode 100644
index 00000000000..8e9a18b736f
--- /dev/null
+++ b/.github/workflows/pr-filter.yml
@@ -0,0 +1,25 @@
+name: PR Filter
+
+on:
+ pull_request_target:
+ types: [opened, reopened]
+
+jobs:
+ check-template:
+ if: github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name
+ runs-on: ubuntu-latest
+ permissions:
+ pull-requests: write
+
+ steps:
+ - name: Checkout Code
+ uses: actions/checkout@v4
+
+ - name: Check PR Content
+ id: intercept
+ uses: actions/github-script@v7
+ with:
+ github-token: ${{ secrets.GITHUB_TOKEN }}
+ script: |
+ const script = require('.github/workflows/scripts/pr-filter.js');
+ await script({ github, context, core });
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
index 99114ea3cb6..b0f9713f2ef 100644
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -10,6 +10,7 @@ on:
required: true
BUILDER:
required: true
+ workflow_dispatch:
jobs:
launch:
diff --git a/.github/workflows/scripts/pr-filter.js b/.github/workflows/scripts/pr-filter.js
new file mode 100644
index 00000000000..03f50dc5ca2
--- /dev/null
+++ b/.github/workflows/scripts/pr-filter.js
@@ -0,0 +1,36 @@
+function hasTypes(markdown) {
+ return /## Type of change/.test(markdown) && /-\s\[x\]/i.test(markdown);
+}
+
+function hasDescription(markdown) {
+ return (
+ /## Description/.test(markdown) &&
+ !/## Description\s*\n\s*(##|\s*$)/.test(markdown)
+ );
+}
+
+module.exports = async ({ github, context, core }) => {
+ const pr = context.payload.pull_request;
+ const body = pr.body === null ? '' : pr.body;
+ const markdown = body.replace(//g, '');
+ const action = context.payload.action;
+
+ const isValid =
+ markdown !== '' && hasTypes(markdown) && hasDescription(markdown);
+
+ if (!isValid) {
+ await github.rest.pulls.update({
+ ...context.repo,
+ pull_number: pr.number,
+ state: 'closed'
+ });
+
+ await github.rest.issues.createComment({
+ ...context.repo,
+ issue_number: pr.number,
+ body: `Oops, it seems you've ${action} an invalid pull request. No worries, we'll close it for you.`
+ });
+
+ core.setFailed('PR content does not meet template requirements.');
+ }
+};
diff --git a/.github/workflows/style-lint.yml b/.github/workflows/style-lint.yml
index f84f3bcc432..5cb38a7a11a 100644
--- a/.github/workflows/style-lint.yml
+++ b/.github/workflows/style-lint.yml
@@ -1,8 +1,10 @@
-name: "Style Lint"
+name: Style Lint
on:
push:
- branches: ["master", "hotfix/**"]
+ branches:
+ - master
+ - "hotfix/*"
paths: ["_sass/**/*.scss"]
pull_request:
paths: ["_sass/**/*.scss"]
diff --git a/Gemfile b/Gemfile
index 1e30511c4dc..28500c6a3df 100644
--- a/Gemfile
+++ b/Gemfile
@@ -12,8 +12,10 @@ platforms :mingw, :x64_mingw, :mswin, :jruby do
gem "tzinfo-data"
end
-gem "wdm", "~> 0.1.1", :platforms => [:mingw, :x64_mingw, :mswin]
+gem "wdm", "~> 0.2.0", :platforms => [:mingw, :x64_mingw, :mswin]
# don't change it!
gem "jekyll-archives", path: "assets/jekyll-archives"
-gem 'jekyll-compose', group: [:jekyll_plugins]
\ No newline at end of file
+gem 'jekyll-compose', group: [:jekyll_plugins]
+gem "logger"
+
diff --git a/_includes/analytics/cloudflare.html b/_includes/analytics/cloudflare.html
index 1eeb1a924ff..9faa11ee3b4 100644
--- a/_includes/analytics/cloudflare.html
+++ b/_includes/analytics/cloudflare.html
@@ -4,4 +4,3 @@
src="https://static.cloudflareinsights.com/beacon.min.js"
data-cf-beacon='{"token": "{{ site.analytics.cloudflare.id }}"}'
>
-
diff --git a/_includes/analytics/fathom.html b/_includes/analytics/fathom.html
index 4b603d3aed5..216bb140c00 100644
--- a/_includes/analytics/fathom.html
+++ b/_includes/analytics/fathom.html
@@ -2,6 +2,5 @@
-
+ defer
+>
diff --git a/_includes/analytics/google.html b/_includes/analytics/google.html
index d0aac651c22..dfe4828c5f0 100644
--- a/_includes/analytics/google.html
+++ b/_includes/analytics/google.html
@@ -1,7 +1,7 @@
-
diff --git a/_includes/comments.html b/_includes/comment.html
similarity index 100%
rename from _includes/comments.html
rename to _includes/comment.html
diff --git a/_includes/comments/disqus.html b/_includes/comments/disqus.html
index 2b889a4e051..fd12a3c724e 100644
--- a/_includes/comments/disqus.html
+++ b/_includes/comments/disqus.html
@@ -1,38 +1,25 @@
-
-
-
-
-
diff --git a/_includes/comments/giscus.html b/_includes/comments/giscus.html
index f9becfe9631..8058472028f 100644
--- a/_includes/comments/giscus.html
+++ b/_includes/comments/giscus.html
@@ -1,21 +1,8 @@
-
-
-
diff --git a/_includes/head.html b/_includes/head.html
index af3acdb5301..310f52eb995 100644
--- a/_includes/head.html
+++ b/_includes/head.html
@@ -97,11 +97,32 @@
{% endif %}
-
+
{% unless site.theme_mode %}
- {% include mode-toggle.html %}
+
{% endunless %}
+ {% include js-selector.html lang=lang %}
+
+ {% if jekyll.environment == 'production' %}
+
+ {% if site.pwa.enabled %}
+
+ {% endif %}
+
+
+ {% for analytics in site.analytics %}
+ {% capture str %}{{ analytics }}{% endcapture %}
+ {% assign platform = str | split: '{' | first %}
+ {% if site.analytics[platform].id and site.analytics[platform].id != empty %}
+ {% include analytics/{{ platform }}.html %}
+ {% endif %}
+ {% endfor %}
+ {% endif %}
+
{% include metadata-hook.html %}
diff --git a/_includes/js-selector.html b/_includes/js-selector.html
index 4d77d06b590..fd4acca8afb 100644
--- a/_includes/js-selector.html
+++ b/_includes/js-selector.html
@@ -62,12 +62,12 @@
{% capture script %}/assets/js/dist/{{ js }}.min.js{% endcapture %}
-
+
{% if page.math %}
-
-
+
+
{% endif %}
@@ -84,26 +84,3 @@
{% endcase %}
{% endif %}
{% endif %}
-
-{% if page.mermaid %}
- {% include mermaid.html %}
-{% endif %}
-
-{% if jekyll.environment == 'production' %}
-
- {% if site.pwa.enabled %}
-
- {% endif %}
-
-
- {% for analytics in site.analytics %}
- {% capture str %}{{ analytics }}{% endcapture %}
- {% assign type = str | split: '{' | first %}
- {% if site.analytics[type].id and site.analytics[type].id != empty %}
- {% include analytics/{{ type }}.html %}
- {% endif %}
- {% endfor %}
-{% endif %}
diff --git a/_includes/jsdelivr-combine.html b/_includes/jsdelivr-combine.html
index cffa6995bf9..0611213b8f7 100644
--- a/_includes/jsdelivr-combine.html
+++ b/_includes/jsdelivr-combine.html
@@ -1,6 +1,6 @@
{% assign urls = include.urls | split: ',' %}
-{% assign combined_urls = nil %}
+{% assign combined_urls = null %}
{% assign domain = 'https://cdn.jsdelivr.net/' %}
@@ -15,12 +15,12 @@
{% endif %}
{% elsif url contains '//' %}
-
+
{% else %}
-
+
{% endif %}
{% endfor %}
{% if combined_urls %}
-
+
{% endif %}
diff --git a/_includes/mermaid.html b/_includes/mermaid.html
deleted file mode 100644
index a3a83edbdc4..00000000000
--- a/_includes/mermaid.html
+++ /dev/null
@@ -1,62 +0,0 @@
-
-
diff --git a/_includes/mode-toggle.html b/_includes/mode-toggle.html
deleted file mode 100644
index 113ec375467..00000000000
--- a/_includes/mode-toggle.html
+++ /dev/null
@@ -1,116 +0,0 @@
-
-
-
diff --git a/_includes/search-loader.html b/_includes/search-loader.html
index 2582580a91b..7fd065d8f59 100644
--- a/_includes/search-loader.html
+++ b/_includes/search-loader.html
@@ -19,29 +19,31 @@
{% capture not_found %}{{ site.data.locales[include.lang].search.no_results }}
{% endcapture %}
diff --git a/_includes/toc-status.html b/_includes/toc-status.html
new file mode 100644
index 00000000000..4b71caeefcc
--- /dev/null
+++ b/_includes/toc-status.html
@@ -0,0 +1,10 @@
+{% comment %}
+ Determine TOC state and return it through variable "enable_toc"
+{% endcomment %}
+
+{% assign enable_toc = false %}
+{% if site.toc and page.toc %}
+ {% if page.content contains '
+
{{- site.data.locales[include.lang].panel.toc -}}
diff --git a/_javascript/categories.js b/_javascript/categories.js
index 15d82513178..ce87d671ba2 100644
--- a/_javascript/categories.js
+++ b/_javascript/categories.js
@@ -1,5 +1,5 @@
import { basic, initSidebar, initTopbar } from './modules/layouts';
-import { categoryCollapse } from './modules/plugins';
+import { categoryCollapse } from './modules/components';
basic();
initSidebar();
diff --git a/_javascript/home.js b/_javascript/home.js
index ef22cb97c1f..7f628a17ad5 100644
--- a/_javascript/home.js
+++ b/_javascript/home.js
@@ -1,5 +1,5 @@
import { basic, initSidebar, initTopbar } from './modules/layouts';
-import { initLocaleDatetime, loadImg } from './modules/plugins';
+import { initLocaleDatetime, loadImg } from './modules/components';
loadImg();
initLocaleDatetime();
diff --git a/_javascript/misc.js b/_javascript/misc.js
index 52b40438c89..37130da14b8 100644
--- a/_javascript/misc.js
+++ b/_javascript/misc.js
@@ -1,5 +1,5 @@
import { basic, initSidebar, initTopbar } from './modules/layouts';
-import { initLocaleDatetime } from './modules/plugins';
+import { initLocaleDatetime } from './modules/components';
initSidebar();
initTopbar();
diff --git a/_javascript/modules/plugins.js b/_javascript/modules/components.js
similarity index 52%
rename from _javascript/modules/plugins.js
rename to _javascript/modules/components.js
index fb892e25bf5..95791a69fcf 100644
--- a/_javascript/modules/plugins.js
+++ b/_javascript/modules/components.js
@@ -3,4 +3,8 @@ export { initClipboard } from './components/clipboard';
export { loadImg } from './components/img-loading';
export { imgPopup } from './components/img-popup';
export { initLocaleDatetime } from './components/locale-datetime';
-export { toc } from './components/toc';
+export { initToc } from './components/toc';
+export { loadMermaid } from './components/mermaid';
+export { modeWatcher } from './components/mode-toggle';
+export { back2top } from './components/back-to-top';
+export { loadTooptip } from './components/tooltip-loader';
diff --git a/_javascript/modules/components/img-popup.js b/_javascript/modules/components/img-popup.js
index ac120435505..420a2265fa5 100644
--- a/_javascript/modules/components/img-popup.js
+++ b/_javascript/modules/components/img-popup.js
@@ -4,7 +4,6 @@
* Dependencies: https://github.com/biati-digital/glightbox
*/
-const html = document.documentElement;
const lightImages = '.popup:not(.dark)';
const darkImages = '.popup:not(.light)';
let selector = lightImages;
@@ -33,26 +32,17 @@ export function imgPopup() {
document.querySelector('.popup.dark') === null
);
- if (
- (html.hasAttribute('data-mode') &&
- html.getAttribute('data-mode') === 'dark') ||
- (!html.hasAttribute('data-mode') &&
- window.matchMedia('(prefers-color-scheme: dark)').matches)
- ) {
+ if (Theme.visualState === Theme.DARK) {
selector = darkImages;
}
let current = GLightbox({ selector: `${selector}` });
- if (hasDualImages && document.getElementById('mode-toggle')) {
+ if (hasDualImages && Theme.switchable) {
let reverse = null;
window.addEventListener('message', (event) => {
- if (
- event.source === window &&
- event.data &&
- event.data.direction === ModeToggle.ID
- ) {
+ if (event.source === window && event.data && event.data.id === Theme.ID) {
updateImages(current, reverse);
}
});
diff --git a/_javascript/modules/components/mermaid.js b/_javascript/modules/components/mermaid.js
new file mode 100644
index 00000000000..2b4759f4726
--- /dev/null
+++ b/_javascript/modules/components/mermaid.js
@@ -0,0 +1,60 @@
+/**
+ * Mermaid-js loader
+ */
+
+const MERMAID = 'mermaid';
+const themeMapper = Theme.getThemeMapper('default', 'dark');
+
+function refreshTheme(event) {
+ if (event.source === window && event.data && event.data.id === Theme.ID) {
+ // Re-render the SVG ›
+ const mermaidList = document.getElementsByClassName(MERMAID);
+
+ [...mermaidList].forEach((elem) => {
+ const svgCode = elem.previousSibling.children.item(0).innerHTML;
+ elem.textContent = svgCode;
+ elem.removeAttribute('data-processed');
+ });
+
+ const newTheme = themeMapper[Theme.visualState];
+
+ mermaid.initialize({ theme: newTheme });
+ mermaid.init(null, `.${MERMAID}`);
+ }
+}
+
+function setNode(elem) {
+ const svgCode = elem.textContent;
+ const backup = elem.parentElement;
+ backup.classList.add('d-none');
+ // Create mermaid node
+ const mermaid = document.createElement('pre');
+ mermaid.classList.add(MERMAID);
+ const text = document.createTextNode(svgCode);
+ mermaid.appendChild(text);
+ backup.after(mermaid);
+}
+
+export function loadMermaid() {
+ if (
+ typeof mermaid === 'undefined' ||
+ typeof mermaid.initialize !== 'function'
+ ) {
+ return;
+ }
+
+ const initTheme = themeMapper[Theme.visualState];
+
+ let mermaidConf = {
+ theme: initTheme
+ };
+
+ const basicList = document.getElementsByClassName('language-mermaid');
+ [...basicList].forEach(setNode);
+
+ mermaid.initialize(mermaidConf);
+
+ if (Theme.switchable) {
+ window.addEventListener('message', refreshTheme);
+ }
+}
diff --git a/_javascript/modules/components/mode-toggle.js b/_javascript/modules/components/mode-toggle.js
new file mode 100644
index 00000000000..455ff0a02dc
--- /dev/null
+++ b/_javascript/modules/components/mode-toggle.js
@@ -0,0 +1,15 @@
+/**
+ * Add listener for theme mode toggle
+ */
+
+const $toggle = document.getElementById('mode-toggle');
+
+export function modeWatcher() {
+ if (!$toggle) {
+ return;
+ }
+
+ $toggle.addEventListener('click', () => {
+ Theme.flip();
+ });
+}
diff --git a/_javascript/modules/components/mode-watcher.js b/_javascript/modules/components/mode-watcher.js
deleted file mode 100644
index 9eecd0961ef..00000000000
--- a/_javascript/modules/components/mode-watcher.js
+++ /dev/null
@@ -1,14 +0,0 @@
-/**
- * Add listener for theme mode toggle
- */
-const toggle = document.getElementById('mode-toggle');
-
-export function modeWatcher() {
- if (!toggle) {
- return;
- }
-
- toggle.addEventListener('click', () => {
- modeToggle.flipMode();
- });
-}
diff --git a/_javascript/modules/components/sidebar.js b/_javascript/modules/components/sidebar.js
deleted file mode 100644
index 6b562d84147..00000000000
--- a/_javascript/modules/components/sidebar.js
+++ /dev/null
@@ -1,27 +0,0 @@
-/**
- * Expand or close the sidebar in mobile screens.
- */
-
-const ATTR_DISPLAY = 'sidebar-display';
-
-class SidebarUtil {
- static isExpanded = false;
-
- static toggle() {
- if (SidebarUtil.isExpanded === false) {
- document.body.setAttribute(ATTR_DISPLAY, '');
- } else {
- document.body.removeAttribute(ATTR_DISPLAY);
- }
-
- SidebarUtil.isExpanded = !SidebarUtil.isExpanded;
- }
-}
-
-export function sidebarExpand() {
- document
- .getElementById('sidebar-trigger')
- .addEventListener('click', SidebarUtil.toggle);
-
- document.getElementById('mask').addEventListener('click', SidebarUtil.toggle);
-}
diff --git a/_javascript/modules/components/toc.js b/_javascript/modules/components/toc.js
index 56ce26fac50..e9086eedf9a 100644
--- a/_javascript/modules/components/toc.js
+++ b/_javascript/modules/components/toc.js
@@ -1,15 +1,33 @@
-export function toc() {
- if (document.querySelector('main h2, main h3')) {
- // see: https://github.com/tscanlin/tocbot#usage
- tocbot.init({
- tocSelector: '#toc',
- contentSelector: '.content',
- ignoreSelector: '[data-toc-skip]',
- headingSelector: 'h2, h3, h4',
- orderedList: false,
- scrollSmooth: false
- });
-
- document.getElementById('toc-wrapper').classList.remove('d-none');
+import { TocMobile as mobile } from './toc/toc-mobile';
+import { TocDesktop as desktop } from './toc/toc-desktop';
+
+const desktopMode = matchMedia('(min-width: 1200px)');
+
+function refresh(e) {
+ if (e.matches) {
+ if (mobile.popupOpened) {
+ mobile.hidePopup();
+ }
+
+ desktop.refresh();
+ } else {
+ mobile.refresh();
+ }
+}
+
+function init() {
+ if (document.querySelector('main>article[data-toc="true"]') === null) {
+ return;
+ }
+
+ // Avoid create multiple instances of Tocbot. Ref:
+ if (desktopMode.matches) {
+ desktop.init();
+ } else {
+ mobile.init();
}
+
+ desktopMode.onchange = refresh;
}
+
+export { init as initToc };
diff --git a/_javascript/modules/components/toc/toc-desktop.js b/_javascript/modules/components/toc/toc-desktop.js
new file mode 100644
index 00000000000..5021a72a082
--- /dev/null
+++ b/_javascript/modules/components/toc/toc-desktop.js
@@ -0,0 +1,22 @@
+export class TocDesktop {
+ /* Tocbot options Ref: https://github.com/tscanlin/tocbot#usage */
+ static options = {
+ tocSelector: '#toc',
+ contentSelector: '.content',
+ ignoreSelector: '[data-toc-skip]',
+ headingSelector: 'h2, h3, h4',
+ orderedList: false,
+ scrollSmooth: false,
+ headingsOffset: 16 * 2 // 2rem
+ };
+
+ static refresh() {
+ tocbot.refresh(this.options);
+ }
+
+ static init() {
+ if (document.getElementById('toc-wrapper')) {
+ tocbot.init(this.options);
+ }
+ }
+}
diff --git a/_javascript/modules/components/toc/toc-mobile.js b/_javascript/modules/components/toc/toc-mobile.js
new file mode 100644
index 00000000000..20e24a73c9f
--- /dev/null
+++ b/_javascript/modules/components/toc/toc-mobile.js
@@ -0,0 +1,125 @@
+/**
+ * TOC button, topbar and popup for mobile devices
+ */
+
+const $tocBar = document.getElementById('toc-bar');
+const $soloTrigger = document.getElementById('toc-solo-trigger');
+const $triggers = document.getElementsByClassName('toc-trigger');
+const $popup = document.getElementById('toc-popup');
+const $btnClose = document.getElementById('toc-popup-close');
+
+const SCROLL_LOCK = 'overflow-hidden';
+const CLOSING = 'closing';
+
+export class TocMobile {
+ static #invisible = true;
+ static #barHeight = 16 * 3; // 3rem
+
+ static options = {
+ tocSelector: '#toc-popup-content',
+ contentSelector: '.content',
+ ignoreSelector: '[data-toc-skip]',
+ headingSelector: 'h2, h3, h4',
+ orderedList: false,
+ scrollSmooth: false,
+ collapseDepth: 4,
+ headingsOffset: this.#barHeight
+ };
+
+ static initBar() {
+ const observer = new IntersectionObserver(
+ (entries) => {
+ entries.forEach((entry) => {
+ $tocBar.classList.toggle('invisible', entry.isIntersecting);
+ });
+ },
+ { rootMargin: `-${this.#barHeight}px 0px 0px 0px` }
+ );
+
+ observer.observe($soloTrigger);
+ this.#invisible = false;
+ }
+
+ static listenAnchors() {
+ const $anchors = document.getElementsByClassName('toc-link');
+ [...$anchors].forEach((anchor) => {
+ anchor.onclick = () => this.hidePopup();
+ });
+ }
+
+ static refresh() {
+ if (this.#invisible) {
+ this.initComponents();
+ }
+ tocbot.refresh(this.options);
+ this.listenAnchors();
+ }
+
+ static get popupOpened() {
+ return $popup.open;
+ }
+
+ static showPopup() {
+ this.lockScroll(true);
+ $popup.showModal();
+ const activeItem = $popup.querySelector('li.is-active-li');
+ activeItem.scrollIntoView({ block: 'center' });
+ }
+
+ static hidePopup() {
+ $popup.toggleAttribute(CLOSING);
+
+ $popup.addEventListener(
+ 'animationend',
+ () => {
+ $popup.toggleAttribute(CLOSING);
+ $popup.close();
+ },
+ { once: true }
+ );
+
+ this.lockScroll(false);
+ }
+
+ static lockScroll(enable) {
+ document.documentElement.classList.toggle(SCROLL_LOCK, enable);
+ document.body.classList.toggle(SCROLL_LOCK, enable);
+ }
+
+ static clickBackdrop(event) {
+ if ($popup.hasAttribute(CLOSING)) {
+ return;
+ }
+
+ const rect = event.target.getBoundingClientRect();
+ if (
+ event.clientX < rect.left ||
+ event.clientX > rect.right ||
+ event.clientY < rect.top ||
+ event.clientY > rect.bottom
+ ) {
+ this.hidePopup();
+ }
+ }
+
+ static initComponents() {
+ this.initBar();
+
+ [...$triggers].forEach((trigger) => {
+ trigger.onclick = () => this.showPopup();
+ });
+
+ $popup.onclick = (e) => this.clickBackdrop(e);
+ $btnClose.onclick = () => this.hidePopup();
+ $popup.oncancel = (e) => {
+ e.preventDefault();
+ this.hidePopup();
+ };
+ }
+
+ static init() {
+ tocbot.init(this.options);
+ this.listenAnchors();
+ this.initComponents();
+ }
+}
diff --git a/_javascript/modules/layouts/basic.js b/_javascript/modules/layouts/basic.js
index fb36a8b8a0c..b8eddf61588 100644
--- a/_javascript/modules/layouts/basic.js
+++ b/_javascript/modules/layouts/basic.js
@@ -1,7 +1,7 @@
-import { back2top } from '../components/back-to-top';
-import { loadTooptip } from '../components/tooltip-loader';
+import { back2top, loadTooptip, modeWatcher } from '../components';
export function basic() {
+ modeWatcher();
back2top();
loadTooptip();
}
diff --git a/_javascript/modules/layouts/sidebar.js b/_javascript/modules/layouts/sidebar.js
index 8795693c105..bbf5e7dab60 100644
--- a/_javascript/modules/layouts/sidebar.js
+++ b/_javascript/modules/layouts/sidebar.js
@@ -1,7 +1,19 @@
-import { modeWatcher } from '../components/mode-watcher';
-import { sidebarExpand } from '../components/sidebar';
+const ATTR_DISPLAY = 'sidebar-display';
+const $sidebar = document.getElementById('sidebar');
+const $trigger = document.getElementById('sidebar-trigger');
+const $mask = document.getElementById('mask');
+
+class SidebarUtil {
+ static #isExpanded = false;
+
+ static toggle() {
+ this.#isExpanded = !this.#isExpanded;
+ document.body.toggleAttribute(ATTR_DISPLAY, this.#isExpanded);
+ $sidebar.classList.toggle('z-2', this.#isExpanded);
+ $mask.classList.toggle('d-none', !this.#isExpanded);
+ }
+}
export function initSidebar() {
- modeWatcher();
- sidebarExpand();
+ $trigger.onclick = $mask.onclick = () => SidebarUtil.toggle();
}
diff --git a/_javascript/modules/theme.js b/_javascript/modules/theme.js
new file mode 100644
index 00000000000..f9ebf20246d
--- /dev/null
+++ b/_javascript/modules/theme.js
@@ -0,0 +1,135 @@
+/**
+ * Theme management class
+ *
+ * To reduce flickering during page load, this script should be loaded synchronously.
+ */
+class Theme {
+ static #modeKey = 'mode';
+ static #modeAttr = 'data-mode';
+ static #darkMedia = window.matchMedia('(prefers-color-scheme: dark)');
+ static switchable = !document.documentElement.hasAttribute(this.#modeAttr);
+
+ static get DARK() {
+ return 'dark';
+ }
+
+ static get LIGHT() {
+ return 'light';
+ }
+
+ /**
+ * @returns {string} Theme mode identifier
+ */
+ static get ID() {
+ return 'theme-mode';
+ }
+
+ /**
+ * Gets the current visual state of the theme.
+ *
+ * @returns {string} The current visual state, either the mode if it exists,
+ * or the system dark mode state ('dark' or 'light').
+ */
+ static get visualState() {
+ if (this.#hasMode) {
+ return this.#mode;
+ } else {
+ return this.#sysDark ? this.DARK : this.LIGHT;
+ }
+ }
+
+ static get #mode() {
+ return sessionStorage.getItem(this.#modeKey);
+ }
+
+ static get #isDarkMode() {
+ return this.#mode === this.DARK;
+ }
+
+ static get #hasMode() {
+ return this.#mode !== null;
+ }
+
+ static get #sysDark() {
+ return this.#darkMedia.matches;
+ }
+
+ /**
+ * Maps theme modes to provided values
+ * @param {string} light Value for light mode
+ * @param {string} dark Value for dark mode
+ * @returns {Object} Mapped values
+ */
+ static getThemeMapper(light, dark) {
+ return {
+ [this.LIGHT]: light,
+ [this.DARK]: dark
+ };
+ }
+
+ /**
+ * Initializes the theme based on system preferences or stored mode
+ */
+ static init() {
+ if (!this.switchable) {
+ return;
+ }
+
+ this.#darkMedia.addEventListener('change', () => {
+ const lastMode = this.#mode;
+ this.#clearMode();
+
+ if (lastMode !== this.visualState) {
+ this.#notify();
+ }
+ });
+
+ if (!this.#hasMode) {
+ return;
+ }
+
+ if (this.#isDarkMode) {
+ this.#setDark();
+ } else {
+ this.#setLight();
+ }
+ }
+
+ /**
+ * Flips the current theme mode
+ */
+ static flip() {
+ if (this.#hasMode) {
+ this.#clearMode();
+ } else {
+ this.#sysDark ? this.#setLight() : this.#setDark();
+ }
+ this.#notify();
+ }
+
+ static #setDark() {
+ document.documentElement.setAttribute(this.#modeAttr, this.DARK);
+ sessionStorage.setItem(this.#modeKey, this.DARK);
+ }
+
+ static #setLight() {
+ document.documentElement.setAttribute(this.#modeAttr, this.LIGHT);
+ sessionStorage.setItem(this.#modeKey, this.LIGHT);
+ }
+
+ static #clearMode() {
+ document.documentElement.removeAttribute(this.#modeAttr);
+ sessionStorage.removeItem(this.#modeKey);
+ }
+
+ /**
+ * Notifies other plugins that the theme mode has changed
+ */
+ static #notify() {
+ window.postMessage({ id: this.ID }, '*');
+ }
+}
+
+Theme.init();
+
+export default Theme;
diff --git a/_javascript/page.js b/_javascript/page.js
index 76e8ce97c10..4b03b790d14 100644
--- a/_javascript/page.js
+++ b/_javascript/page.js
@@ -1,9 +1,15 @@
import { basic, initSidebar, initTopbar } from './modules/layouts';
-import { loadImg, imgPopup, initClipboard } from './modules/plugins';
+import {
+ loadImg,
+ imgPopup,
+ initClipboard,
+ loadMermaid
+} from './modules/components';
loadImg();
imgPopup();
initSidebar();
initTopbar();
initClipboard();
+loadMermaid();
basic();
diff --git a/_javascript/post.js b/_javascript/post.js
index 9340f05e70d..dc472b42978 100644
--- a/_javascript/post.js
+++ b/_javascript/post.js
@@ -1,17 +1,20 @@
-import { basic, initSidebar, initTopbar } from './modules/layouts';
+import { basic, initTopbar, initSidebar } from './modules/layouts';
+
import {
loadImg,
imgPopup,
initLocaleDatetime,
initClipboard,
- toc
-} from './modules/plugins';
+ initToc,
+ loadMermaid
+} from './modules/components';
loadImg();
-toc();
+initToc();
imgPopup();
initSidebar();
initLocaleDatetime();
initClipboard();
initTopbar();
+loadMermaid();
basic();
diff --git a/_layouts/default.html b/_layouts/default.html
index 61cb8fc7899..9f9963905e6 100644
--- a/_layouts/default.html
+++ b/_layouts/default.html
@@ -69,14 +69,18 @@
-
+
{% if site.pwa.enabled %}
{% include_cached notification.html lang=lang %}
{% endif %}
-
- {% include js-selector.html lang=lang %}
+
+
+ {% for _include in layout.script_includes %}
+ {% assign _include_path = _include | append: '.html' %}
+ {% include {{ _include_path }} %}
+ {% endfor %}
{% include_cached search-loader.html lang=lang %}
Comments powered by Disqus.
-