aboutsummaryrefslogtreecommitdiff
path: root/themes/hugo-theme-stack/assets/ts
diff options
context:
space:
mode:
authorAlexander Neonxp Kiryukhin <i@neonxp.ru>2024-11-16 19:32:18 +0300
committerAlexander Neonxp Kiryukhin <i@neonxp.ru>2024-11-17 01:28:57 +0300
commit239d68f94c6250276850fbe95eaa6cdd5c38fb26 (patch)
treec576da169afc442f51eae6213ad0ff749ed86589 /themes/hugo-theme-stack/assets/ts
parent8e79098193fd0a8b65305dd8054cf7c424c60bc5 (diff)
Своя тема, полностью всё переделал
Diffstat (limited to 'themes/hugo-theme-stack/assets/ts')
-rw-r--r--themes/hugo-theme-stack/assets/ts/color.ts63
-rw-r--r--themes/hugo-theme-stack/assets/ts/colorScheme.ts92
-rw-r--r--themes/hugo-theme-stack/assets/ts/createElement.ts34
-rw-r--r--themes/hugo-theme-stack/assets/ts/gallery.ts186
-rw-r--r--themes/hugo-theme-stack/assets/ts/main.ts112
-rw-r--r--themes/hugo-theme-stack/assets/ts/menu.ts83
-rw-r--r--themes/hugo-theme-stack/assets/ts/scrollspy.ts131
-rw-r--r--themes/hugo-theme-stack/assets/ts/search.tsx333
-rw-r--r--themes/hugo-theme-stack/assets/ts/smoothAnchors.ts37
9 files changed, 0 insertions, 1071 deletions
diff --git a/themes/hugo-theme-stack/assets/ts/color.ts b/themes/hugo-theme-stack/assets/ts/color.ts
deleted file mode 100644
index 50581d1..0000000
--- a/themes/hugo-theme-stack/assets/ts/color.ts
+++ /dev/null
@@ -1,63 +0,0 @@
-interface colorScheme {
- hash: string, /// Regenerate color scheme when the image hash is changed
- DarkMuted: {
- hex: string,
- rgb: Number[],
- bodyTextColor: string
- },
- Vibrant: {
- hex: string,
- rgb: Number[],
- bodyTextColor: string
- }
-}
-
-let colorsCache: { [key: string]: colorScheme } = {};
-
-if (localStorage.hasOwnProperty('StackColorsCache')) {
- try {
- colorsCache = JSON.parse(localStorage.getItem('StackColorsCache'));
- }
- catch (e) {
- colorsCache = {};
- }
-}
-
-async function getColor(key: string, hash: string, imageURL: string) {
- if (!key) {
- /**
- * If no key is provided, do not cache the result
- */
- return await Vibrant.from(imageURL).getPalette();
- }
-
- if (!colorsCache.hasOwnProperty(key) || colorsCache[key].hash !== hash) {
- /**
- * If key is provided, but not found in cache, or the hash mismatches => Regenerate color scheme
- */
- const palette = await Vibrant.from(imageURL).getPalette();
-
- colorsCache[key] = {
- hash: hash,
- Vibrant: {
- hex: palette.Vibrant.hex,
- rgb: palette.Vibrant.rgb,
- bodyTextColor: palette.Vibrant.bodyTextColor
- },
- DarkMuted: {
- hex: palette.DarkMuted.hex,
- rgb: palette.DarkMuted.rgb,
- bodyTextColor: palette.DarkMuted.bodyTextColor
- }
- }
-
- /* Save the result in localStorage */
- localStorage.setItem('StackColorsCache', JSON.stringify(colorsCache));
- }
-
- return colorsCache[key];
-}
-
-export {
- getColor
-} \ No newline at end of file
diff --git a/themes/hugo-theme-stack/assets/ts/colorScheme.ts b/themes/hugo-theme-stack/assets/ts/colorScheme.ts
deleted file mode 100644
index 978e98e..0000000
--- a/themes/hugo-theme-stack/assets/ts/colorScheme.ts
+++ /dev/null
@@ -1,92 +0,0 @@
-type colorScheme = 'light' | 'dark' | 'auto';
-
-class StackColorScheme {
- private localStorageKey = 'StackColorScheme';
- private currentScheme: colorScheme;
- private systemPreferScheme: colorScheme;
-
- constructor(toggleEl: HTMLElement) {
- this.bindMatchMedia();
- this.currentScheme = this.getSavedScheme();
- if (window.matchMedia('(prefers-color-scheme: dark)').matches === true)
- this.systemPreferScheme = 'dark'
- else
- this.systemPreferScheme = 'light';
-
- this.dispatchEvent(document.documentElement.dataset.scheme as colorScheme);
-
- if (toggleEl)
- this.bindClick(toggleEl);
-
- if (document.body.style.transition == '')
- document.body.style.setProperty('transition', 'background-color .3s ease');
- }
-
- private saveScheme() {
- localStorage.setItem(this.localStorageKey, this.currentScheme);
- }
-
- private bindClick(toggleEl: HTMLElement) {
- toggleEl.addEventListener('click', (e) => {
- if (this.isDark()) {
- /// Disable dark mode
- this.currentScheme = 'light';
- }
- else {
- this.currentScheme = 'dark';
- }
-
- this.setBodyClass();
-
- if (this.currentScheme == this.systemPreferScheme) {
- /// Set to auto
- this.currentScheme = 'auto';
- }
-
- this.saveScheme();
- })
- }
-
- private isDark() {
- return (this.currentScheme == 'dark' || this.currentScheme == 'auto' && this.systemPreferScheme == 'dark');
- }
-
- private dispatchEvent(colorScheme: colorScheme) {
- const event = new CustomEvent('onColorSchemeChange', {
- detail: colorScheme
- });
- window.dispatchEvent(event);
- }
-
- private setBodyClass() {
- if (this.isDark()) {
- document.documentElement.dataset.scheme = 'dark';
- }
- else {
- document.documentElement.dataset.scheme = 'light';
- }
-
- this.dispatchEvent(document.documentElement.dataset.scheme as colorScheme);
- }
-
- private getSavedScheme(): colorScheme {
- const savedScheme = localStorage.getItem(this.localStorageKey);
-
- if (savedScheme == 'light' || savedScheme == 'dark' || savedScheme == 'auto') return savedScheme;
- else return 'auto';
- }
-
- private bindMatchMedia() {
- window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {
- if (e.matches) {
- this.systemPreferScheme = 'dark';
- }
- else {
- this.systemPreferScheme = 'light';
- }
- this.setBodyClass();
- });
- }
-}
-
-export default StackColorScheme;
diff --git a/themes/hugo-theme-stack/assets/ts/createElement.ts b/themes/hugo-theme-stack/assets/ts/createElement.ts
deleted file mode 100644
index 3a1e85e..0000000
--- a/themes/hugo-theme-stack/assets/ts/createElement.ts
+++ /dev/null
@@ -1,34 +0,0 @@
-/**
- * createElement
- * Edited from:
- * @link https://stackoverflow.com/a/42405694
- */
-function createElement(tag, attrs, children) {
- var element = document.createElement(tag);
-
- for (let name in attrs) {
- if (name && attrs.hasOwnProperty(name)) {
- let value = attrs[name];
-
- if (name == "dangerouslySetInnerHTML") {
- element.innerHTML = value.__html;
- }
- else if (value === true) {
- element.setAttribute(name, name);
- } else if (value !== false && value != null) {
- element.setAttribute(name, value.toString());
- }
- }
- }
- for (let i = 2; i < arguments.length; i++) {
- let child = arguments[i];
- if (child) {
- element.appendChild(
- child.nodeType == null ?
- document.createTextNode(child.toString()) : child);
- }
- }
- return element;
-}
-
-export default createElement; \ No newline at end of file
diff --git a/themes/hugo-theme-stack/assets/ts/gallery.ts b/themes/hugo-theme-stack/assets/ts/gallery.ts
deleted file mode 100644
index 9840f1e..0000000
--- a/themes/hugo-theme-stack/assets/ts/gallery.ts
+++ /dev/null
@@ -1,186 +0,0 @@
-declare global {
- interface Window {
- PhotoSwipe: any;
- PhotoSwipeUI_Default: any
- }
-}
-
-interface PhotoSwipeItem {
- w: number;
- h: number;
- src: string;
- msrc: string;
- title?: string;
- el: HTMLElement;
-}
-
-class StackGallery {
- private galleryUID: number;
- private items: PhotoSwipeItem[] = [];
-
- constructor(container: HTMLElement, galleryUID = 1) {
- if (window.PhotoSwipe == undefined || window.PhotoSwipeUI_Default == undefined) {
- console.error("PhotoSwipe lib not loaded.");
- return;
- }
-
- this.galleryUID = galleryUID;
-
- StackGallery.createGallery(container);
- this.loadItems(container);
- this.bindClick();
- }
-
- private loadItems(container: HTMLElement) {
- this.items = [];
-
- const figures = container.querySelectorAll('figure.gallery-image');
-
- for (const el of figures) {
- const figcaption = el.querySelector('figcaption'),
- img = el.querySelector('img');
-
- let aux: PhotoSwipeItem = {
- w: parseInt(img.getAttribute('width')),
- h: parseInt(img.getAttribute('height')),
- src: img.src,
- msrc: img.getAttribute('data-thumb') || img.src,
- el: el
- }
-
- if (figcaption) {
- aux.title = figcaption.innerHTML;
- }
-
- this.items.push(aux);
- }
- }
-
- public static createGallery(container: HTMLElement) {
- /// The process of wrapping image with figure tag is done using JavaScript instead of only Hugo markdown render hook
- /// because it can not detect whether image is being wrapped by a link or not
- /// and it lead to a invalid HTML construction (<a><figure><img></figure></a>)
-
- const images = container.querySelectorAll('img.gallery-image');
- for (const img of Array.from(images)) {
- /// Images are wrapped with figure tag if the paragraph has only images without texts
- /// This is done to allow inline images within paragraphs
- const paragraph = img.closest('p');
-
- if (!paragraph || !container.contains(paragraph)) continue;
-
- if (paragraph.textContent.trim() == '') {
- /// Once we insert figcaption, this check no longer works
- /// So we add a class to paragraph to mark it
- paragraph.classList.add('no-text');
- }
-
- let isNewLineImage = paragraph.classList.contains('no-text');
- if (!isNewLineImage) continue;
-
- const hasLink = img.parentElement.tagName == 'A';
-
- let el: HTMLElement = img;
- /// Wrap image with figure tag, with flex-grow and flex-basis values extracted from img's data attributes
- const figure = document.createElement('figure');
- figure.style.setProperty('flex-grow', img.getAttribute('data-flex-grow') || '1');
- figure.style.setProperty('flex-basis', img.getAttribute('data-flex-basis') || '0');
- if (hasLink) {
- /// Wrap <a> if it exists
- el = img.parentElement;
- }
- el.parentElement.insertBefore(figure, el);
- figure.appendChild(el);
-
- /// Add figcaption if it exists
- if (img.hasAttribute('alt')) {
- const figcaption = document.createElement('figcaption');
- figcaption.innerText = img.getAttribute('alt');
- figure.appendChild(figcaption);
- }
-
- /// Wrap img tag with <a> tag if image was not wrapped by <a> tag
- if (!hasLink) {
- figure.className = 'gallery-image';
-
- const a = document.createElement('a');
- a.href = img.src;
- a.setAttribute('target', '_blank');
- img.parentNode.insertBefore(a, img);
- a.appendChild(img);
- }
- }
-
- const figuresEl = container.querySelectorAll('figure.gallery-image');
-
- let currentGallery = [];
-
- for (const figure of figuresEl) {
- if (!currentGallery.length) {
- /// First iteration
- currentGallery = [figure];
- }
- else if (figure.previousElementSibling === currentGallery[currentGallery.length - 1]) {
- /// Adjacent figures
- currentGallery.push(figure);
- }
- else if (currentGallery.length) {
- /// End gallery
- StackGallery.wrap(currentGallery);
- currentGallery = [figure];
- }
- }
-
- if (currentGallery.length > 0) {
- StackGallery.wrap(currentGallery);
- }
- }
-
- /**
- * Wrap adjacent figure tags with div.gallery
- * @param figures
- */
- public static wrap(figures: HTMLElement[]) {
- const galleryContainer = document.createElement('div');
- galleryContainer.className = 'gallery';
-
- const parentNode = figures[0].parentNode,
- first = figures[0];
-
- parentNode.insertBefore(galleryContainer, first)
-
- for (const figure of figures) {
- galleryContainer.appendChild(figure);
- }
- }
-
- public open(index: number) {
- const pswp = document.querySelector('.pswp') as HTMLDivElement;
- const ps = new window.PhotoSwipe(pswp, window.PhotoSwipeUI_Default, this.items, {
- index: index,
- galleryUID: this.galleryUID,
- getThumbBoundsFn: (index) => {
- const thumbnail = this.items[index].el.getElementsByTagName('img')[0],
- pageYScroll = window.pageYOffset || document.documentElement.scrollTop,
- rect = thumbnail.getBoundingClientRect();
-
- return { x: rect.left, y: rect.top + pageYScroll, w: rect.width };
- }
- });
-
- ps.init();
- }
-
- private bindClick() {
- for (const [index, item] of this.items.entries()) {
- const a = item.el.querySelector('a');
-
- a.addEventListener('click', (e) => {
- e.preventDefault();
- this.open(index);
- })
- }
- }
-}
-
-export default StackGallery; \ No newline at end of file
diff --git a/themes/hugo-theme-stack/assets/ts/main.ts b/themes/hugo-theme-stack/assets/ts/main.ts
deleted file mode 100644
index f3160ae..0000000
--- a/themes/hugo-theme-stack/assets/ts/main.ts
+++ /dev/null
@@ -1,112 +0,0 @@
-/*!
-* Hugo Theme Stack
-*
-* @author: Jimmy Cai
-* @website: https://jimmycai.com
-* @link: https://github.com/CaiJimmy/hugo-theme-stack
-*/
-import StackGallery from "ts/gallery";
-import { getColor } from 'ts/color';
-import menu from 'ts/menu';
-import createElement from 'ts/createElement';
-import StackColorScheme from 'ts/colorScheme';
-import { setupScrollspy } from 'ts/scrollspy';
-import { setupSmoothAnchors } from "ts/smoothAnchors";
-
-let Stack = {
- init: () => {
- /**
- * Bind menu event
- */
- menu();
-
- const articleContent = document.querySelector('.article-content') as HTMLElement;
- if (articleContent) {
- new StackGallery(articleContent);
- setupSmoothAnchors();
- setupScrollspy();
- }
-
- /**
- * Add linear gradient background to tile style article
- */
- const articleTile = document.querySelector('.article-list--tile');
- if (articleTile) {
- let observer = new IntersectionObserver(async (entries, observer) => {
- entries.forEach(entry => {
- if (!entry.isIntersecting) return;
- observer.unobserve(entry.target);
-
- const articles = entry.target.querySelectorAll('article.has-image');
- articles.forEach(async articles => {
- const image = articles.querySelector('img'),
- imageURL = image.src,
- key = image.getAttribute('data-key'),
- hash = image.getAttribute('data-hash'),
- articleDetails: HTMLDivElement = articles.querySelector('.article-details');
-
- const colors = await getColor(key, hash, imageURL);
-
- articleDetails.style.background = `
- linear-gradient(0deg,
- rgba(${colors.DarkMuted.rgb[0]}, ${colors.DarkMuted.rgb[1]}, ${colors.DarkMuted.rgb[2]}, 0.5) 0%,
- rgba(${colors.Vibrant.rgb[0]}, ${colors.Vibrant.rgb[1]}, ${colors.Vibrant.rgb[2]}, 0.75) 100%)`;
- })
- })
- });
-
- observer.observe(articleTile)
- }
-
-
- /**
- * Add copy button to code block
- */
- const highlights = document.querySelectorAll('.article-content div.highlight');
- const copyText = `Copy`,
- copiedText = `Copied!`;
-
- highlights.forEach(highlight => {
- const copyButton = document.createElement('button');
- copyButton.innerHTML = copyText;
- copyButton.classList.add('copyCodeButton');
- highlight.appendChild(copyButton);
-
- const codeBlock = highlight.querySelector('code[data-lang]');
- if (!codeBlock) return;
-
- copyButton.addEventListener('click', () => {
- navigator.clipboard.writeText(codeBlock.textContent)
- .then(() => {
- copyButton.textContent = copiedText;
-
- setTimeout(() => {
- copyButton.textContent = copyText;
- }, 1000);
- })
- .catch(err => {
- alert(err)
- console.log('Something went wrong', err);
- });
- });
- });
-
- new StackColorScheme(document.getElementById('dark-mode-toggle'));
- }
-}
-
-window.addEventListener('load', () => {
- setTimeout(function () {
- Stack.init();
- }, 0);
-})
-
-declare global {
- interface Window {
- createElement: any;
- Stack: any
- }
-}
-
-window.Stack = Stack;
-window.createElement = createElement; \ No newline at end of file
diff --git a/themes/hugo-theme-stack/assets/ts/menu.ts b/themes/hugo-theme-stack/assets/ts/menu.ts
deleted file mode 100644
index 34615ba..0000000
--- a/themes/hugo-theme-stack/assets/ts/menu.ts
+++ /dev/null
@@ -1,83 +0,0 @@
-/**
- * Slide up/down
- * Code from https://dev.to/bmsvieira/vanilla-js-slidedown-up-4dkn
- * @param target
- * @param duration
- */
-let slideUp = (target: HTMLElement, duration = 500) => {
- target.classList.add('transiting');
- target.style.transitionProperty = 'height, margin, padding';
- target.style.transitionDuration = duration + 'ms';
- ///target.style.boxSizing = 'border-box';
- target.style.height = target.offsetHeight + 'px';
- target.offsetHeight;
- target.style.overflow = 'hidden';
- target.style.height = "0";
- target.style.paddingTop = "0";
- target.style.paddingBottom = "0";
- target.style.marginTop = "0";
- target.style.marginBottom = "0";
- window.setTimeout(() => {
- target.classList.remove('show')
- target.style.removeProperty('height');
- target.style.removeProperty('padding-top');
- target.style.removeProperty('padding-bottom');
- target.style.removeProperty('margin-top');
- target.style.removeProperty('margin-bottom');
- target.style.removeProperty('overflow');
- target.style.removeProperty('transition-duration');
- target.style.removeProperty('transition-property');
- target.classList.remove('transiting');
- }, duration);
-}
-
-let slideDown = (target: HTMLElement, duration = 500) => {
- target.classList.add('transiting');
- target.style.removeProperty('display');
-
- target.classList.add('show');
-
- let height = target.offsetHeight;
- target.style.overflow = 'hidden';
- target.style.height = "0";
- target.style.paddingTop = "0";
- target.style.paddingBottom = "0";
- target.style.marginTop = "0";
- target.style.marginBottom = "0";
- target.offsetHeight;
- ///target.style.boxSizing = 'border-box';
- target.style.transitionProperty = "height, margin, padding";
- target.style.transitionDuration = duration + 'ms';
- target.style.height = height + 'px';
- target.style.removeProperty('padding-top');
- target.style.removeProperty('padding-bottom');
- target.style.removeProperty('margin-top');
- target.style.removeProperty('margin-bottom');
- window.setTimeout(() => {
- target.style.removeProperty('height');
- target.style.removeProperty('overflow');
- target.style.removeProperty('transition-duration');
- target.style.removeProperty('transition-property');
- target.classList.remove('transiting');
- }, duration);
-}
-
-let slideToggle = (target, duration = 500) => {
- if (window.getComputedStyle(target).display === 'none') {
- return slideDown(target, duration);
- } else {
- return slideUp(target, duration);
- }
-}
-
-export default function () {
- const toggleMenu = document.getElementById('toggle-menu');
- if (toggleMenu) {
- toggleMenu.addEventListener('click', () => {
- if (document.getElementById('main-menu').classList.contains('transiting')) return;
- document.body.classList.toggle('show-menu');
- slideToggle(document.getElementById('main-menu'), 300);
- toggleMenu.classList.toggle('is-active');
- });
- }
-} \ No newline at end of file
diff --git a/themes/hugo-theme-stack/assets/ts/scrollspy.ts b/themes/hugo-theme-stack/assets/ts/scrollspy.ts
deleted file mode 100644
index 8a14085..0000000
--- a/themes/hugo-theme-stack/assets/ts/scrollspy.ts
+++ /dev/null
@@ -1,131 +0,0 @@
-// Implements a scroll spy system for the ToC, displaying the current section with an indicator and scrolling to it when needed.
-
-// Inspired from https://gomakethings.com/debouncing-your-javascript-events/
-function debounced(func: Function) {
- let timeout;
- return () => {
- if (timeout) {
- window.cancelAnimationFrame(timeout);
- }
-
- timeout = window.requestAnimationFrame(() => func());
- }
-}
-
-const headersQuery = ".article-content h1[id], .article-content h2[id], .article-content h3[id], .article-content h4[id], .article-content h5[id], .article-content h6[id]";
-const tocQuery = "#TableOfContents";
-const navigationQuery = "#TableOfContents li";
-const activeClass = "active-class";
-
-function scrollToTocElement(tocElement: HTMLElement, scrollableNavigation: HTMLElement) {
- let textHeight = tocElement.querySelector("a").offsetHeight;
- let scrollTop = tocElement.offsetTop - scrollableNavigation.offsetHeight / 2 + textHeight / 2 - scrollableNavigation.offsetTop;
- if (scrollTop < 0) {
- scrollTop = 0;
- }
- scrollableNavigation.scrollTo({ top: scrollTop, behavior: "smooth" });
-}
-
-type IdToElementMap = { [key: string]: HTMLElement };
-
-function buildIdToNavigationElementMap(navigation: NodeListOf<Element>): IdToElementMap {
- const sectionLinkRef: IdToElementMap = {};
- navigation.forEach((navigationElement: HTMLElement) => {
- const link = navigationElement.querySelector("a");
- const href = link.getAttribute("href");
- if (href.startsWith("#")) {
- sectionLinkRef[href.slice(1)] = navigationElement;
- }
- });
-
- return sectionLinkRef;
-}
-
-function computeOffsets(headers: NodeListOf<Element>) {
- let sectionsOffsets = [];
- headers.forEach((header: HTMLElement) => { sectionsOffsets.push({ id: header.id, offset: header.offsetTop }) });
- sectionsOffsets.sort((a, b) => a.offset - b.offset);
- return sectionsOffsets;
-}
-
-function setupScrollspy() {
- let headers = document.querySelectorAll(headersQuery);
- if (!headers) {
- console.warn("No header matched query", headers);
- return;
- }
-
- let scrollableNavigation = document.querySelector(tocQuery) as HTMLElement | undefined;
- if (!scrollableNavigation) {
- console.warn("No toc matched query", tocQuery);
- return;
- }
-
- let navigation = document.querySelectorAll(navigationQuery);
- if (!navigation) {
- console.warn("No navigation matched query", navigationQuery);
- return;
- }
-
- let sectionsOffsets = computeOffsets(headers);
-
- // We need to avoid scrolling when the user is actively interacting with the ToC. Otherwise, if the user clicks on a link in the ToC,
- // we would scroll their view, which is not optimal usability-wise.
- let tocHovered: boolean = false;
- scrollableNavigation.addEventListener("mouseenter", debounced(() => tocHovered = true));
- scrollableNavigation.addEventListener("mouseleave", debounced(() => tocHovered = false));
-
- let activeSectionLink: Element;
-
- let idToNavigationElement: IdToElementMap = buildIdToNavigationElementMap(navigation);
-
- function scrollHandler() {
- let scrollPosition = document.documentElement.scrollTop || document.body.scrollTop;
-
- let newActiveSection: HTMLElement | undefined;
-
- // Find the section that is currently active.
- // It is possible for no section to be active, so newActiveSection may be undefined.
- sectionsOffsets.forEach((section) => {
- if (scrollPosition >= section.offset - 20) {
- newActiveSection = document.getElementById(section.id);
- }
- });
-
- // Find the link for the active section. Once again, there are a few edge cases:
- // - No active section = no link => undefined
- // - No active section but the link does not exist in toc (e.g. because it is outside of the applicable ToC levels) => undefined
- let newActiveSectionLink: HTMLElement | undefined
- if (newActiveSection) {
- newActiveSectionLink = idToNavigationElement[newActiveSection.id];
- }
-
- if (newActiveSection && !newActiveSectionLink) {
- // The active section does not have a link in the ToC, so we can't scroll to it.
- console.debug("No link found for section", newActiveSection);
- } else if (newActiveSectionLink !== activeSectionLink) {
- if (activeSectionLink)
- activeSectionLink.classList.remove(activeClass);
- if (newActiveSectionLink) {
- newActiveSectionLink.classList.add(activeClass);
- if (!tocHovered) {
- // Scroll so that newActiveSectionLink is in the middle of scrollableNavigation, except when it's from a manual click (hence the tocHovered check)
- scrollToTocElement(newActiveSectionLink, scrollableNavigation);
- }
- }
- activeSectionLink = newActiveSectionLink;
- }
- }
-
- window.addEventListener("scroll", debounced(scrollHandler));
-
- // Resizing may cause the offset values to change: recompute them.
- function resizeHandler() {
- sectionsOffsets = computeOffsets(headers);
- scrollHandler();
- }
-
- window.addEventListener("resize", debounced(resizeHandler));
-}
-
-export { setupScrollspy }; \ No newline at end of file
diff --git a/themes/hugo-theme-stack/assets/ts/search.tsx b/themes/hugo-theme-stack/assets/ts/search.tsx
deleted file mode 100644
index 1c81dd1..0000000
--- a/themes/hugo-theme-stack/assets/ts/search.tsx
+++ /dev/null
@@ -1,333 +0,0 @@
-interface pageData {
- title: string,
- date: string,
- permalink: string,
- content: string,
- image?: string,
- preview: string,
- matchCount: number
-}
-
-interface match {
- start: number,
- end: number
-}
-
-/**
- * Escape HTML tags as HTML entities
- * Edited from:
- * @link https://stackoverflow.com/a/5499821
- */
-const tagsToReplace = {
- '&': '&amp;',
- '<': '&lt;',
- '>': '&gt;',
- '"': '&quot;',
- '…': '&hellip;'
-};
-
-function replaceTag(tag) {
- return tagsToReplace[tag] || tag;
-}
-
-function replaceHTMLEnt(str) {
- return str.replace(/[&<>"]/g, replaceTag);
-}
-
-function escapeRegExp(string) {
- return string.replace(/[.*+\-?^${}()|[\]\\]/g, '\\$&');
-}
-
-class Search {
- private data: pageData[];
- private form: HTMLFormElement;
- private input: HTMLInputElement;
- private list: HTMLDivElement;
- private resultTitle: HTMLHeadElement;
- private resultTitleTemplate: string;
-
- constructor({ form, input, list, resultTitle, resultTitleTemplate }) {
- this.form = form;
- this.input = input;
- this.list = list;
- this.resultTitle = resultTitle;
- this.resultTitleTemplate = resultTitleTemplate;
-
- /// Check if there's already value in the search input
- if (this.input.value.trim() !== '') {
- this.doSearch(this.input.value.split(' '));
- }
- else {
- this.handleQueryString();
- }
-
- this.bindQueryStringChange();
- this.bindSearchForm();
- }
-
- /**
- * Processes search matches
- * @param str original text
- * @param matches array of matches
- * @param ellipsis whether to add ellipsis to the end of each match
- * @param charLimit max length of preview string
- * @param offset how many characters before and after the match to include in preview
- * @returns preview string
- */
- private static processMatches(str: string, matches: match[], ellipsis: boolean = true, charLimit = 140, offset = 20): string {
- matches.sort((a, b) => {
- return a.start - b.start;
- });
-
- let i = 0,
- lastIndex = 0,
- charCount = 0;
-
- const resultArray: string[] = [];
-
- while (i < matches.length) {
- const item = matches[i];
-
- /// item.start >= lastIndex (equal only for the first iteration)
- /// because of the while loop that comes after, iterating over variable j
-
- if (ellipsis && item.start - offset > lastIndex) {
- resultArray.push(`${replaceHTMLEnt(str.substring(lastIndex, lastIndex + offset))} [...] `);
- resultArray.push(`${replaceHTMLEnt(str.substring(item.start - offset, item.start))}`);
- charCount += offset * 2;
- }
- else {
- /// If the match is too close to the end of last match, don't add ellipsis
- resultArray.push(replaceHTMLEnt(str.substring(lastIndex, item.start)));
- charCount += item.start - lastIndex;
- }
-
- let j = i + 1,
- end = item.end;
-
- /// Include as many matches as possible
- /// [item.start, end] is the range of the match
- while (j < matches.length && matches[j].start <= end) {
- end = Math.max(matches[j].end, end);
- ++j;
- }
-
- resultArray.push(`<mark>${replaceHTMLEnt(str.substring(item.start, end))}</mark>`);
- charCount += end - item.start;
-
- i = j;
- lastIndex = end;
-
- if (ellipsis && charCount > charLimit) break;
- }
-
- /// Add the rest of the string
- if (lastIndex < str.length) {
- let end = str.length;
- if (ellipsis) end = Math.min(end, lastIndex + offset);
-
- resultArray.push(`${replaceHTMLEnt(str.substring(lastIndex, end))}`);
-
- if (ellipsis && end != str.length) {
- resultArray.push(` [...]`);
- }
- }
-
- return resultArray.join('');
- }
-
- private async searchKeywords(keywords: string[]) {
- const rawData = await this.getData();
- const results: pageData[] = [];
-
- const regex = new RegExp(keywords.filter((v, index, arr) => {
- arr[index] = escapeRegExp(v);
- return v.trim() !== '';
- }).join('|'), 'gi');
-
- for (const item of rawData) {
- const titleMatches: match[] = [],
- contentMatches: match[] = [];
-
- let result = {
- ...item,
- preview: '',
- matchCount: 0
- }
-
- const contentMatchAll = item.content.matchAll(regex);
- for (const match of Array.from(contentMatchAll)) {
- contentMatches.push({
- start: match.index,
- end: match.index + match[0].length
- });
- }
-
- const titleMatchAll = item.title.matchAll(regex);
- for (const match of Array.from(titleMatchAll)) {
- titleMatches.push({
- start: match.index,
- end: match.index + match[0].length
- });
- }
-
- if (titleMatches.length > 0) result.title = Search.processMatches(result.title, titleMatches, false);
- if (contentMatches.length > 0) {
- result.preview = Search.processMatches(result.content, contentMatches);
- }
- else {
- /// If there are no matches in the content, use the first 140 characters as preview
- result.preview = replaceHTMLEnt(result.content.substring(0, 140));
- }
-
- result.matchCount = titleMatches.length + contentMatches.length;
- if (result.matchCount > 0) results.push(result);
- }
-
- /// Result with more matches appears first
- return results.sort((a, b) => {
- return b.matchCount - a.matchCount;
- });
- }
-
- private async doSearch(keywords: string[]) {
- const startTime = performance.now();
-
- const results = await this.searchKeywords(keywords);
- this.clear();
-
- for (const item of results) {
- this.list.append(Search.render(item));
- }
-
- const endTime = performance.now();
-
- this.resultTitle.innerText = this.generateResultTitle(results.length, ((endTime - startTime) / 1000).toPrecision(1));
- }
-
- private generateResultTitle(resultLen, time) {
- return this.resultTitleTemplate.replace("#PAGES_COUNT", resultLen).replace("#TIME_SECONDS", time);
- }
-
- public async getData() {
- if (!this.data) {
- /// Not fetched yet
- const jsonURL = this.form.dataset.json;
- this.data = await fetch(jsonURL).then(res => res.json());
- const parser = new DOMParser();
-
- for (const item of this.data) {
- item.content = parser.parseFromString(item.content, 'text/html').body.innerText;
- }
- }
-
- return this.data;
- }
-
- private bindSearchForm() {
- let lastSearch = '';
-
- const eventHandler = (e) => {
- e.preventDefault();
- const keywords = this.input.value.trim();
-
- Search.updateQueryString(keywords, true);
-
- if (keywords === '') {
- lastSearch = '';
- return this.clear();
- }
-
- if (lastSearch === keywords) return;
- lastSearch = keywords;
-
- this.doSearch(keywords.split(' '));
- }
-
- this.input.addEventListener('input', eventHandler);
- this.input.addEventListener('compositionend', eventHandler);
- }
-
- private clear() {
- this.list.innerHTML = '';
- this.resultTitle.innerText = '';
- }
-
- private bindQueryStringChange() {
- window.addEventListener('popstate', (e) => {
- this.handleQueryString()
- })
- }
-
- private handleQueryString() {
- const pageURL = new URL(window.location.toString());
- const keywords = pageURL.searchParams.get('keyword');
- this.input.value = keywords;
-
- if (keywords) {
- this.doSearch(keywords.split(' '));
- }
- else {
- this.clear()
- }
- }
-
- private static updateQueryString(keywords: string, replaceState = false) {
- const pageURL = new URL(window.location.toString());
-
- if (keywords === '') {
- pageURL.searchParams.delete('keyword')
- }
- else {
- pageURL.searchParams.set('keyword', keywords);
- }
-
- if (replaceState) {
- window.history.replaceState('', '', pageURL.toString());
- }
- else {
- window.history.pushState('', '', pageURL.toString());
- }
- }
-
- public static render(item: pageData) {
- return <article>
- <a href={item.permalink}>
- <div class="article-details">
- <h2 class="article-title" dangerouslySetInnerHTML={{ __html: item.title }}></h2>
- <section class="article-preview" dangerouslySetInnerHTML={{ __html: item.preview }}></section>
- </div>
- {item.image &&
- <div class="article-image">
- <img src={item.image} loading="lazy" />
- </div>
- }
- </a>
- </article>;
- }
-}
-
-declare global {
- interface Window {
- searchResultTitleTemplate: string;
- }
-}
-
-window.addEventListener('load', () => {
- setTimeout(function () {
- const searchForm = document.querySelector('.search-form') as HTMLFormElement,
- searchInput = searchForm.querySelector('input') as HTMLInputElement,
- searchResultList = document.querySelector('.search-result--list') as HTMLDivElement,
- searchResultTitle = document.querySelector('.search-result--title') as HTMLHeadingElement;
-
- new Search({
- form: searchForm,
- input: searchInput,
- list: searchResultList,
- resultTitle: searchResultTitle,
- resultTitleTemplate: window.searchResultTitleTemplate
- });
- }, 0);
-})
-
-export default Search; \ No newline at end of file
diff --git a/themes/hugo-theme-stack/assets/ts/smoothAnchors.ts b/themes/hugo-theme-stack/assets/ts/smoothAnchors.ts
deleted file mode 100644
index 16ab6a3..0000000
--- a/themes/hugo-theme-stack/assets/ts/smoothAnchors.ts
+++ /dev/null
@@ -1,37 +0,0 @@
-// Implements smooth scrolling when clicking on an anchor link.
-// This is required instead of using modern CSS because Chromium does not currently support scrolling
-// one element with scrollTo while another element is scrolled because of a click on a link. This would
-// thus not work with the ToC scrollspy and e.g. footnotes.
-
-// Here are additional links about this issue:
-// - https://stackoverflow.com/questions/49318497/google-chrome-simultaneously-smooth-scrollintoview-with-more-elements-doesn
-// - https://stackoverflow.com/questions/57214373/scrollintoview-using-smooth-function-on-multiple-elements-in-chrome
-// - https://bugs.chromium.org/p/chromium/issues/detail?id=833617
-// - https://bugs.chromium.org/p/chromium/issues/detail?id=1043933
-// - https://bugs.chromium.org/p/chromium/issues/detail?id=1121151
-
-const anchorLinksQuery = "a[href]";
-
-function setupSmoothAnchors() {
- document.querySelectorAll(anchorLinksQuery).forEach(aElement => {
- let href = aElement.getAttribute("href");
- if (!href.startsWith("#")) {
- return;
- }
- aElement.addEventListener("click", clickEvent => {
- clickEvent.preventDefault();
-
- const targetId = decodeURI(aElement.getAttribute("href").substring(1)),
- target = document.getElementById(targetId) as HTMLElement,
- offset = target.getBoundingClientRect().top - document.documentElement.getBoundingClientRect().top;
-
- window.history.pushState({}, "", aElement.getAttribute("href"));
- scrollTo({
- top: offset,
- behavior: "smooth"
- });
- });
- });
-}
-
-export { setupSmoothAnchors }; \ No newline at end of file