a new start

This commit is contained in:
2025-10-25 12:39:30 +02:00
commit c97cadef78
726 changed files with 454051 additions and 0 deletions

40
_js/src/clap-button.js Normal file
View File

@@ -0,0 +1,40 @@
// Copyright (c) 2020 Florian Klampfer <https://qwtel.com/>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import "broadcastchannel-polyfill";
import { webComponentsReady, stylesheetReady } from "./common";
(async () => {
await Promise.all([
...("customElements" in window
? []
: [
import(
/* webpackChunkName: "webcomponents" */ "./polyfills/webcomponents"
).then(
() =>
import(/* webpackChunkName: "shadydom" */ "./polyfills/shadydom"),
),
]),
]);
await webComponentsReady;
await stylesheetReady;
if (process.env.GET_CLAPS_API && !window.GET_CLAPS_API)
window.GET_CLAPS_API = process.env.GET_CLAPS_API;
import(/* webpackMode: "eager" */ "@getclaps/button");
})();

202
_js/src/common.js Normal file
View File

@@ -0,0 +1,202 @@
// Copyright (c) 2019 Florian Klampfer <https://qwtel.com/>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import { Observable, of } from "rxjs";
// HACK: Temporary MS Edge fix
// TODO: Move rx-element into separate file or module
export {
getScrollHeight,
getScrollLeft,
getScrollTop,
} from "@hydecorp/component/lib/util";
export { fromMediaQuery, fetchRx } from "@hydecorp/component/lib/creators";
export { subscribeWhen, filterWhen } from "@hydecorp/component/lib/operators";
export { createIntersectionObservable } from "@hydecorp/component/lib/observers";
const style = getComputedStyle(document.documentElement);
export const BREAK_POINT_3 = `(min-width: ${style.getPropertyValue("--break-point-3")})`;
export const BREAK_POINT_DYNAMIC = `(min-width: ${style.getPropertyValue("--break-point-dynamic")})`;
export const CONTENT_WIDTH_5 = parseFloat(
style.getPropertyValue("--content-width-5"),
);
export const CONTENT_MARGIN_5 = parseFloat(
style.getPropertyValue("--content-margin-5"),
);
export const DRAWER_WIDTH = parseFloat(
style.getPropertyValue("--sidebar-width"),
);
export const HALF_CONTENT = parseFloat(
style.getPropertyValue("--half-content"),
);
// Check the user agent for Safari and iOS Safari, to give them some special treatment...
const ua = navigator.userAgent.toLowerCase();
export const isSafari = ua.indexOf("safari") > 0 && ua.indexOf("chrome") < 0;
export const isMobile = ua.indexOf("mobile") > 0;
export const isMobileSafari = isSafari && isMobile;
export const isUCBrowser = ua.indexOf("ucbrowser") > 0;
export const isFirefox = ua.indexOf("firefox") > 0;
export const isFirefoxIOS = ua.indexOf("fxios") > 0 && ua.indexOf("safari") > 0;
export const hasCSSOM =
"attributeStyleMap" in Element.prototype && "CSS" in window && CSS.number;
export const webComponentsReady = new Promise((res) => {
if ("customElements" in window) res(true);
else document.addEventListener("WebComponentsReady", res);
});
// FIXME: Replace with something more robust!?
export const stylesheetReady = new Promise(function checkCSS(
res,
rej,
retries = 30,
) {
const drawerEl = document.querySelector("hy-drawer");
if (!drawerEl) res(true);
else if (getComputedStyle(drawerEl).getPropertyValue("--hy-drawer-width"))
res(true);
else if (retries <= 0) rej(Error("Stylesheet not loaded within 10 seconds"));
else setTimeout(() => checkCSS(res, rej, retries - 1), 1000 / 3);
});
export const once = (el, eventName) =>
new Promise((res) => el.addEventListener(eventName, res, { once: true }));
export const timeout = (t) => new Promise((res) => setTimeout(res, t));
// Takes an array of Modernizr feature tests and makes sure they all pass.
export function hasFeatures(features) {
if (!window.Modernizr) return true;
return [...features].every((feature) => {
const hasFeature = window.Modernizr[feature];
if (!hasFeature && process.env.DEBUG)
console.warn(`Feature '${feature}' missing!`);
return hasFeature;
});
}
// Some functions to hide and show content.
export function show() {
this.style.display = "block";
this.style.visibility = "visible";
}
export function hide() {
this.style.display = "none";
this.style.visibility = "hidden";
}
export function unshow() {
this.style.display = "";
this.style.visibility = "";
}
export const unhide = unshow;
// Same as `el.innerHTML = ''`, but not quite so hacky.
export function empty() {
while (this?.firstChild) this.removeChild(this.firstChild);
}
/**
* An observable wrapper for the WebAnimations API.
* Will return an observable that emits once when the animation finishes.
* @param {HTMLElement|null} el
* @param {AnimationKeyFrame | AnimationKeyFrame[] | null} effect
* @param {number|AnimationEffectTiming} timing
* @returns {Observable<Event>}
*/
export function animate(el, effect, timing) {
if (!el) return of(new CustomEvent("finish"));
return Observable.create((observer) => {
const anim = el.animate(effect, timing);
anim.addEventListener("finish", (e) => {
observer.next(e);
requestAnimationFrame(() => {
requestAnimationFrame(() => observer.complete());
});
});
return () => {
if (anim.playState !== "finished") anim.cancel();
};
});
}
/**
* @param {string} templateId
* @returns {HTMLElement|null}
*/
export function importTemplate(templateId) {
const template = document.getElementById(templateId);
return template && document.importNode(template.content, true);
}
export const body = document.body || document.documentElement;
export const rem = (units) =>
units * parseFloat(getComputedStyle(body).fontSize);
export const getViewWidth = () => window.innerWidth || body.clientWidth;
export const getViewHeight = () => window.innerHeight || body.clientHeight;
/**
* @template Q
* @template S
* @param {Worker} worker
* @param {Q} message
* @returns {Promise<S>}
*/
export function postMessage(worker, message) {
return new Promise((resolve, reject) => {
const messageChannel = new MessageChannel();
messageChannel.port1.onmessage = (event) => {
if (event.data.error) {
reject(event.data.error);
} else {
resolve(event.data);
}
};
worker.postMessage(message, [messageChannel.port2]);
});
}
const promisifyLoad = (loadFn) => (href) =>
new Promise((r) => loadFn(href).addEventListener("load", r));
/** @type {(href: string) => Promise<Event>} */
export const loadJS = promisifyLoad(window.loadJS);
/** @type {(href: string) => Promise<Event>} */
export const loadCSS = promisifyLoad(window.loadCSS);
/**
* @param {ArrayLike<Element>} els
* @param {IntersectionObserverInit} [options]
* @returns {Promise<IntersectionObserverEntry>}
*/
export function intersectOnce(els, options) {
return new Promise((res) => {
const io = new IntersectionObserver((entries) => {
if (entries.some((x) => x.isIntersecting)) {
els.forEach((el) => io.unobserve(el));
res(entries.find((x) => x.isIntersecting));
}
}, options);
els.forEach((el) => io.observe(el));
});
}

166
_js/src/cross-fader.js Normal file
View File

@@ -0,0 +1,166 @@
// Copyright (c) 2019 Florian Klampfer <https://qwtel.com/>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import { EMPTY, of } from "rxjs";
import { catchError, finalize, map, switchMap } from "rxjs/operators";
import { animate, fetchRx } from "./common";
const RE_CSS_URL = /url\s*\(['"]?(([^'"\\]|\\.)*)['"]?\)/u;
/** @param {Document} doc */
const calcHash = (doc) => {
const sidebar = doc.getElementById("_sidebar");
const sidebarBg = sidebar?.querySelector(".sidebar-bg");
const pageStyle = doc.getElementById("_pageStyle");
const source = doc.getElementById("source");
// const rule = Array.from(pageStyle?.sheet?.rules ?? []).find(r => r.selectorText === 'html');
// const accentColor = rule?.style.getPropertyValue('--accent-color') ?? '';
// const themeColor = rule?.style.getPropertyValue('--theme-color') ?? '';
return [
pageStyle?.innerText?.trim(),
sidebar?.classList,
sidebarBg?.classList,
sidebarBg?.style.backgroundImage,
source?.src,
].join("\n");
};
/**
* Consider a URL external if either the protocol, hostname or port is different.
* @param {URL} param0
* @param {Location=} location
*/
function isExternal({ protocol, host }, location = window.location) {
return protocol !== location.protocol || host !== location.host;
}
const objectURLs = new WeakMap();
export class CrossFader {
/** @param {number} fadeDuration */
constructor(fadeDuration) {
this.sidebar = document.getElementById("_sidebar");
this.fadeDuration = fadeDuration;
this.prevHash = calcHash(document);
this.themeColorEl = document.querySelector('meta[name="theme-color"]');
}
/** @param {Document} newDocument */
fetchImage2(newDocument) {
const sidebarBg = newDocument.querySelector(".sidebar-bg");
const video = sidebarBg?.querySelector("source");
const { backgroundImage = "" } = sidebarBg?.style ?? {};
const result = RE_CSS_URL.exec(backgroundImage);
const videoUrl = video?.src;
if (!result) {
return of("");
}
const url = new URL(result[1], window.location.origin);
return fetchRx(url.href, {
method: "GET",
headers: { Accept: "image/*" },
...(isExternal(url) ? { mode: "cors" } : {}),
}).pipe(
switchMap((r) => r.blob()),
map((blob) => [URL.createObjectURL(blob), videoUrl]),
catchError(() => of(url.href)),
);
}
/** @param {Document} newDocument */
fetchImage(newDocument) {
const hash = calcHash(newDocument);
if (hash === this.prevHash) return EMPTY;
return this.fetchImage2(newDocument).pipe(
map(([objectUrl, videoUrl]) => {
/** @type {HTMLDivElement} */
const div =
newDocument.querySelector(".sidebar-bg") ??
document.createElement("div");
if (objectUrl) {
div.style.backgroundImage = `url(${objectUrl})`;
objectURLs.set(div, objectUrl);
}
const video = div.querySelector("video");
if (video && videoUrl) {
video.querySelector("source").src = videoUrl;
}
return [div, hash, newDocument];
}),
);
}
/** @param {Document} newDocument */
updateStyle(newDocument) {
const classList = newDocument.getElementById("_sidebar")?.classList;
if (classList) this.sidebar.setAttribute("class", classList);
if (this.themeColorEl) {
const themeColor = newDocument.head.querySelector(
'meta[name="theme-color"]',
)?.content;
if (themeColor) {
window.setTimeout(() => {
if (this.themeColorEl) {
this.themeColorEl.content = themeColor;
}
}, 250);
}
}
try {
const pageStyle = document.getElementById("_pageStyle");
const newPageStyle = newDocument.getElementById("_pageStyle");
if (!newPageStyle) return;
pageStyle?.parentNode?.replaceChild(newPageStyle, pageStyle);
} catch (e) {
if (process.env.DEBUG) console.error(e);
}
}
/**
* @param {[HTMLDivElement]} param0
* @param {[HTMLDListElement, string, Document]} param1
*/
fade([prevDiv], [div, hash, newDocument]) {
prevDiv?.parentNode?.insertBefore(div, prevDiv.nextElementSibling);
this.updateStyle(newDocument);
// Only update the prev hash after we're actually in the fade stage
this.prevHash = hash;
return animate(div, [{ opacity: 0 }, { opacity: 1 }], {
duration: this.fadeDuration,
easing: "ease",
}).pipe(
finalize(() => {
if (objectURLs.has(prevDiv))
URL.revokeObjectURL(objectURLs.get(prevDiv));
prevDiv?.parentNode?.removeChild(prevDiv);
div.querySelector("video").play();
}),
);
}
}

81
_js/src/dark-mode.js Normal file
View File

@@ -0,0 +1,81 @@
// Copyright (c) 2019 Florian Klampfer <https://qwtel.com/>
import { importTemplate, stylesheetReady, once } from "./common";
const SEL_NAVBAR_BTN_BAR = "#_navbar > .content > .nav-btn-bar";
(async () => {
await stylesheetReady;
const darkMode = importTemplate("_dark-mode-template");
if (darkMode) {
const navbarEl = document.querySelector(SEL_NAVBAR_BTN_BAR);
navbarEl?.insertBefore(
darkMode,
navbarEl.querySelector(".nav-insert-marker"),
);
const metaEl = document.querySelector('meta[name="color-scheme"]');
let tId;
const navbarBtn = document.getElementById("_dark-mode");
navbarBtn?.addEventListener("click", (e) => {
e.preventDefault();
clearTimeout(tId);
const list = document.body.classList;
if (
list.contains("dark-mode") ||
("_sunset" in window &&
!list.contains("light-mode") &&
matchMedia("(prefers-color-scheme: dark)").matches)
) {
list.remove("dark-mode");
list.add("light-mode");
tId = setTimeout(() => {
if (metaEl) metaEl.content = "light";
document.documentElement.style.colorScheme = "light";
}, 250);
navbarBtn.dispatchEvent(
new CustomEvent("pivoine-dark-mode-toggle", {
detail: false,
bubbles: true,
}),
);
} else {
list.remove("light-mode");
list.add("dark-mode");
tId = setTimeout(() => {
if (metaEl) metaEl.content = "dark";
document.documentElement.style.colorScheme = "dark";
}, 250);
navbarBtn.dispatchEvent(
new CustomEvent("pivoine-dark-mode-toggle", {
detail: true,
bubbles: true,
}),
);
}
});
await once(document, "click");
const styleSheets = Array.from(document.styleSheets);
const inlineSheet = styleSheets.find(
(s) => s.ownerNode?.id === "_styleInline",
);
const linkSheet = styleSheets.find(
(s) => s.ownerNode?.id === "_stylePreload",
);
const setRule = (sheet) => {
if (!sheet) return;
const rule = Array.from(sheet.rules).find((rule) =>
rule.selectorText.startsWith(".color-transition"),
);
if (rule)
rule.style.transition =
"background-color 1s ease, border-color 1s ease";
};
setRule(inlineSheet);
setRule(linkSheet);
}
})();

275
_js/src/drawer.js Normal file
View File

@@ -0,0 +1,275 @@
// Copyright (c) 2019 Florian Klampfer <https://qwtel.com/>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import { fromEvent, merge, NEVER, combineLatest } from "rxjs";
import {
distinctUntilChanged,
map,
filter,
startWith,
switchMap,
tap,
throttleTime,
withLatestFrom,
} from "rxjs/operators";
import {
BREAK_POINT_3,
BREAK_POINT_DYNAMIC,
isSafari,
isMobile,
isMobileSafari,
hasCSSOM,
webComponentsReady,
stylesheetReady,
getScrollTop,
getViewWidth,
fromMediaQuery,
} from "./common";
(async () => {
await Promise.all([
...("customElements" in window
? []
: [
import(
/* webpackChunkName: "webcomponents" */ "./polyfills/webcomponents"
).then(
() =>
import(/* webpackChunkName: "shadydom" */ "./polyfills/shadydom"),
),
]),
...("ResizeObserver" in window
? []
: [
import(
/* webpackChunkName: "resize-observer" */ "./polyfills/resize-observer"
),
]),
]);
await webComponentsReady;
await stylesheetReady;
const MOBILE = 1;
const DESKTOP = 2;
const LARGE_DESKTOP = 3;
const subscribeWhen = (p$) => (source) => {
if (process.env.DEBUG && !p$) throw Error();
return p$.pipe(switchMap((p) => (p ? source : NEVER)));
};
// Determines the range from which to draw the drawer in pixels, counted from the left edge.
// It depends on the browser, e.g. Safari has a native gesture when sliding form the side,
// so we ignore the first 35 pixels (roughly the range for the native gesture),
// to avoid triggering both gestures.
function getRange(drawerWidth, size) {
if (size >= DESKTOP) return [0, drawerWidth];
if (isMobileSafari) return [35, 150];
return [0, 150];
}
// The functions below add an svg graphic to the sidebar
// that indicate that the sidebar can be drawn using touch gestures.
function setupIcon(drawerEl) {
const img = document.getElementById("_hrefSwipeSVG");
if (img) {
const svg = document.createElement("img");
svg.id = "_swipe";
svg.src = img.href;
svg.alt = "Swipe image";
svg.addEventListener("click", () => drawerEl.close());
document.getElementById("_sidebar")?.appendChild(svg);
}
}
function removeIcon() {
const svg = document.getElementById("_swipe");
svg?.parentNode?.removeChild(svg);
}
const detectSize = () =>
window.matchMedia(BREAK_POINT_DYNAMIC).matches
? LARGE_DESKTOP
: window.matchMedia(BREAK_POINT_3).matches
? DESKTOP
: MOBILE;
// First we get hold of some DOM elements.
const drawerEl = document.getElementById("_drawer");
const sidebarEl = document.getElementById("_sidebar");
const contentEl = sidebarEl?.querySelector(".sidebar-sticky");
if (!drawerEl || !sidebarEl || !contentEl) return;
document.getElementById("_menu")?.addEventListener("click", (e) => {
e.preventDefault();
e.stopPropagation();
drawerEl.toggle();
});
sidebarEl
.querySelectorAll('a[href^="/"]:not(.external)')
.forEach((el) => el.addEventListener("click", () => drawerEl.close()));
if (isSafari) drawerEl.setAttribute("threshold", "0");
if (!isMobile) drawerEl.setAttribute("mouseevents", "");
const [tValue, oValue] = hasCSSOM
? [
new CSSTransformValue([new CSSTranslate(CSS.px(0), CSS.px(0))]),
CSS.number(1),
]
: [null, null];
const updateSidebar = (t, size, distance) => {
const value = distance * t;
const opacity = size >= DESKTOP ? 1 : 1 - t;
if (hasCSSOM) {
tValue[0].x.value = value;
oValue.value = opacity;
sidebarEl.attributeStyleMap.set("transform", tValue);
contentEl.attributeStyleMap.set("opacity", oValue);
} else {
sidebarEl.style.transform = `translateX(${value}px)`;
contentEl.style.opacity = opacity;
}
};
// A flag for the 3 major viewport sizes we support
const size$ = merge(
fromMediaQuery(window.matchMedia(BREAK_POINT_3)),
fromMediaQuery(window.matchMedia(BREAK_POINT_DYNAMIC)),
).pipe(startWith({}), map(detectSize));
// An observable keeping track of the drawer (peek) width.
const peekWidth$ = fromEvent(drawerEl, "peek-width-change").pipe(
map((e) => e.detail),
);
// An observable keeping track the viewport width
const viewWidth$ = fromEvent(window, "resize", { passive: true }).pipe(
startWith({}),
map(getViewWidth),
);
// An observable keeping track of the distance between
// the middle point of the screen and the middle point of the drawer.
const distance$ = combineLatest(peekWidth$, viewWidth$).pipe(
map(([drawerWidth, viewWidth]) => viewWidth / 2 - drawerWidth / 2),
);
const t$ = merge(
distance$.pipe(
map(() =>
drawerEl.opacity !== undefined ? 1 - drawerEl.opacity : opened ? 0 : 1,
),
),
fromEvent(drawerEl, "hy-drawer-move").pipe(
map(({ detail: { opacity } }) => {
return 1 - opacity;
}),
),
);
drawerEl.addEventListener("hy-drawer-prepare", () => {
sidebarEl.style.willChange = "transform";
contentEl.style.willChange = "opacity";
});
drawerEl.addEventListener("hy-drawer-transitioned", () => {
sidebarEl.style.willChange = "";
contentEl.style.willChange = "";
});
// Save scroll position before the drawer gets initialized.
const scrollTop = getScrollTop();
// Start the drawer in `opened` state when the cover class is present,
// and the user hasn't started scrolling already.
const opened =
drawerEl.classList.contains("cover") &&
scrollTop <= 0 &&
!(history.state && history.state.closedOnce);
if (!opened) {
if (!history.state) history.replaceState({}, document.title);
history.state.closedOnce = true;
drawerEl.removeAttribute("opened");
}
const opened$ = fromEvent(drawerEl, "hy-drawer-transitioned").pipe(
map((e) => e.detail),
distinctUntilChanged(),
tap((opened) => {
if (!opened) {
removeIcon();
if (!history.state) history.replaceState({}, document.title);
history.state.closedOnce = true;
}
}),
startWith(opened),
);
// We need the height of the drawer in case we need to reset the scroll position
const drawerHeight = opened ? null : drawerEl.getBoundingClientRect().height;
drawerEl.addEventListener(
"hy-drawer-init",
() => {
drawerEl.classList.add("loaded");
setupIcon(drawerEl);
if (drawerHeight && scrollTop >= drawerHeight) {
window.scrollTo(0, scrollTop - drawerHeight);
}
},
{ once: true },
);
await import(/* webpackMode: "eager" */ "@hydecorp/drawer");
window._drawer = drawerEl;
t$.pipe(
withLatestFrom(size$, distance$),
tap((args) => updateSidebar(...args)),
).subscribe();
// Keeping the drawer updated.
peekWidth$
.pipe(
withLatestFrom(size$),
map((args) => getRange(...args)),
tap((range) => {
drawerEl.range = range;
}),
)
.subscribe();
// Hacky way of letting the cover page close when scrolling
fromEvent(document, "wheel", { passive: false })
.pipe(
subscribeWhen(opened$),
filter((e) => e.deltaY > 0),
tap((e) => {
if (drawerEl.translateX > 0) e.preventDefault();
}),
throttleTime(500),
tap(() => drawerEl.close()),
)
.subscribe();
})();

78
_js/src/entry.js Normal file
View File

@@ -0,0 +1,78 @@
// Copyright (c) 2019 Florian Klampfer <https://qwtel.com/>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import "@babel/polyfill";
import "../lib/version";
import "../lib/modernizr-custom";
import { hasFeatures } from "./common";
__webpack_public_path__ = window._publicPath;
const BASELINE = ["classlist", "eventlistener", "queryselector", "template"];
const DARK_MODE_FEATURES = ["customproperties"];
const DRAWER_FEATURES = [
"customproperties",
"history",
"matchmedia",
"opacity",
];
const PUSH_STATE_FEATURES = [
"history",
"matchmedia",
"opacity",
"cssanimations",
"cssremunit",
"documentfragment",
];
const CLAP_BUTTON_FEATURES = [
"customproperties",
"cssanimations",
"cssremunit",
];
const TOC_FEATURES = ["matchmedia", "cssremunit"];
if (hasFeatures(BASELINE)) {
import(/* webpackMode: "eager" */ "./upgrades");
if (!window._noNavbar) import(/* webpackChunkName: "navbar" */ "./navbar");
if (!window._noLightbox)
import(/* webpackChunkName: "lightbox" */ "./lightbox");
if (!window._noSound) import(/* webpackChunkName: "sound" */ "./sound");
if (hasFeatures(DARK_MODE_FEATURES)) {
// import(/* webpackMode: "eager" */ './pro/cookies-banner');
import(/* webpackMode: "eager" */ "./dark-mode");
}
if (!window._noSearch) import(/* webpackChunkName: "search" */ "./search");
if (window._clapButton && hasFeatures(CLAP_BUTTON_FEATURES)) {
import(/* webpackChunkName: "clap-button" */ "./clap-button");
}
// A list of Modernizr tests that are required for the drawer to work.
if (!window._noDrawer && hasFeatures(DRAWER_FEATURES)) {
import(/* webpackChunkName: "drawer" */ "./drawer");
}
if (!window._noPushState && hasFeatures(PUSH_STATE_FEATURES)) {
import(/* webpackChunkName: "push-state" */ "./push-state");
}
// if (!window._noToc && hasFeatures(TOC_FEATURES)) {
// import(/* webpackChunkName: "toc" */ './pro/toc');
// }
}

29
_js/src/flip/index.js Normal file
View File

@@ -0,0 +1,29 @@
// Copyright (c) 2019 Florian Klampfer <https://qwtel.com/>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import { merge } from "rxjs";
import { filter } from "rxjs/operators";
import { setupFLIPTitle } from "./title";
const FLIP_TYPES = ["title"];
export function setupFLIP(start$, ready$, fadeIn$, options) {
const other$ = start$.pipe(
filter(({ flipType }) => !FLIP_TYPES.includes(flipType)),
);
return merge(setupFLIPTitle(start$, ready$, fadeIn$, options), other$);
}

116
_js/src/flip/title.js Normal file
View File

@@ -0,0 +1,116 @@
// Copyright (c) 2019 Florian Klampfer <https://qwtel.com/>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import { Observable, of, zip } from "rxjs";
import { tap, finalize, filter, map, switchMap } from "rxjs/operators";
import { animate, empty } from "../common";
const TITLE_SELECTOR = ".page-title, .post-title";
/**
* @param {Observable<any>} start$
* @param {Observable<any>} ready$
* @param {Observable<any>} fadeIn$
* @param {any} opts
*/
export function setupFLIPTitle(
start$,
ready$,
fadeIn$,
{ animationMain, settings },
) {
if (!animationMain) return start$;
const flip$ = start$.pipe(
filter(({ flipType }) => flipType === "title"),
switchMap(({ anchor }) => {
if (!anchor) return of({});
const title = document.createElement("h1");
title.classList.add("page-title");
title.textContent = anchor.textContent;
title.style.transformOrigin = "left top";
const page = animationMain.querySelector(".page");
if (!page) return of({});
empty.call(page);
page.appendChild(title);
animationMain.style.position = "fixed";
animationMain.style.opacity = 1;
const first = anchor.getBoundingClientRect();
const last = title.getBoundingClientRect();
const firstFontSize = parseInt(getComputedStyle(anchor).fontSize, 10);
const lastFontSize = parseInt(getComputedStyle(title).fontSize, 10);
const invertX = first.left - last.left;
const invertY = first.top - last.top;
const invertScale = firstFontSize / lastFontSize;
anchor.style.opacity = 0;
const transform = [
{
transform: `translate3d(${invertX}px, ${invertY}px, 0) scale(${invertScale})`,
},
{ transform: "translate3d(0, 0, 0) scale(1)" },
];
return animate(title, transform, settings).pipe(
tap({
complete() {
animationMain.style.position = "absolute";
},
}),
);
}),
);
start$
.pipe(
switchMap(({ flipType }) =>
zip(
ready$.pipe(
filter(() => flipType === "title"),
map(({ replaceEls: [main] }) => {
const title = main.querySelector(TITLE_SELECTOR);
if (title) title.style.opacity = 0;
return title;
}),
),
fadeIn$,
).pipe(
map(([x]) => x),
tap((title) => {
if (title) title.style.opacity = 1;
animationMain.style.opacity = 0;
}),
finalize(() => {
animationMain.style.opacity = 0;
const page = animationMain.querySelector(".page");
empty.call(page);
}),
),
),
)
.subscribe();
return flip$;
}

337
_js/src/languages.json Normal file
View File

@@ -0,0 +1,337 @@
{
"abap": "ABAP",
"actionscript": "ActionScript",
"as": "ActionScript",
"as3": "ActionScript",
"ada": "Ada",
"apache": "Apache",
"apex": "Apex",
"apiblueprint": "API Blueprint",
"apib": "API Blueprint",
"applescript": "AppleScript",
"armasm": "ArmAsm",
"augeas": "Augeas",
"aug": "Augeas",
"awk": "Awk",
"batchfile": "Batchfile",
"bat": "Batchfile",
"batch": "Batchfile",
"dosbatch": "Batchfile",
"winbatch": "Batchfile",
"bbcbasic": "BBCBASIC",
"bibtex": "BibTeX",
"bib": "BibTeX",
"biml": "BIML",
"bpf": "BPF",
"brainfuck": "Brainfuck",
"bsl": "1C (BSL)",
"c": "C",
"ceylon": "Ceylon",
"cfscript": "CFScript",
"cfc": "CFScript",
"clean": "Clean",
"clojure": "Clojure",
"clj": "Clojure",
"cljs": "Clojure",
"cmake": "CMake",
"cmhg": "CMHG",
"coffeescript": "CoffeeScript",
"coffee": "CoffeeScript",
"coffee-script": "CoffeeScript",
"common_lisp": "Common Lisp",
"cl": "Common Lisp",
"common-lisp": "Common Lisp",
"elisp": "Common Lisp",
"emacs-lisp": "Common Lisp",
"lisp": "Common Lisp",
"conf": "Config File",
"config": "Config File",
"configuration": "Config File",
"console": "Console",
"terminal": "Console",
"shell_session": "Console",
"shell-session": "Console",
"coq": "Coq",
"cpp": "C++",
"c++": "C++",
"crystal": "Crystal",
"cr": "Crystal",
"csharp": "C#",
"c#": "C#",
"cs": "C#",
"css": "CSS",
"csvs": "csvs",
"cuda": "CUDA",
"cypher": "Cypher",
"cython": "Cython",
"pyx": "Cython",
"pyrex": "Cython",
"d": "D",
"dlang": "D",
"dart": "Dart",
"datastudio": "Datastudio",
"diff": "diff",
"patch": "diff",
"udiff": "diff",
"digdag": "digdag",
"docker": "Docker",
"dockerfile": "Docker",
"dot": "DOT",
"ecl": "ECL",
"eex": "EEX",
"leex": "EEX",
"eiffel": "Eiffel",
"elixir": "Elixir",
"exs": "Elixir",
"elm": "Elm",
"epp": "EPP",
"erb": "ERB",
"eruby": "ERB",
"rhtml": "ERB",
"erlang": "Erlang",
"erl": "Erlang",
"escape": "Escape",
"esc": "Escape",
"factor": "Factor",
"fortran": "Fortran",
"freefem": "FreeFEM",
"ff": "FreeFEM",
"fsharp": "FSharp",
"gdscript": "GDScript",
"gd": "GDScript",
"ghc-cmm": "GHC Cmm (C--)",
"cmm": "GHC Cmm (C--)",
"ghc-core": "GHC Core",
"gherkin": "Gherkin",
"cucumber": "Gherkin",
"behat": "Gherkin",
"glsl": "GLSL",
"go": "Go",
"golang": "Go",
"gradle": "Gradle",
"graphql": "Graphql",
"groovy": "Groovy",
"hack": "Hack",
"hh": "Hack",
"haml": "Haml",
"HAML": "Haml",
"handlebars": "Handlebars",
"hbs": "Handlebars",
"mustache": "Handlebars",
"haskell": "Haskell",
"hs": "Haskell",
"haxe": "Haxe",
"hx": "Haxe",
"hcl": "Hashicorp Configuration Language",
"hlsl": "HLSL",
"hocon": "HOCON",
"hql": "HQL",
"html": "HTML",
"http": "HTTP",
"hylang": "HyLang",
"hy": "HyLang",
"idlang": "IDL",
"igorpro": "IgorPro",
"ini": "INI",
"io": "Io",
"irb": "Irb",
"pry": "Irb",
"irb_output": "Irb_output",
"isbl": "ISBL",
"java": "Java",
"javascript": "JavaScript",
"js": "JavaScript",
"jinja": "Jinja",
"django": "Jinja",
"jsl": "JSL",
"json": "JSON",
"json-doc": "Json-doc",
"jsonc": "Json-doc",
"jsonnet": "Jsonnet",
"jsp": "Jsp",
"jsx": "JSX",
"react": "JSX",
"julia": "Julia",
"jl": "Julia",
"kotlin": "Kotlin",
"lasso": "Lasso",
"lassoscript": "Lasso",
"liquid": "Liquid",
"literate_coffeescript": "Literate CoffeeScript",
"litcoffee": "Literate CoffeeScript",
"literate_haskell": "Literate Haskell",
"lithaskell": "Literate Haskell",
"lhaskell": "Literate Haskell",
"lhs": "Literate Haskell",
"livescript": "LiveScript",
"ls": "LiveScript",
"llvm": "LLVM",
"lua": "Lua",
"lustre": "Lustre",
"lutin": "Lutin",
"m68k": "M68k",
"magik": "Magik",
"make": "Make",
"makefile": "Make",
"mf": "Make",
"gnumake": "Make",
"bsdmake": "Make",
"markdown": "Markdown",
"md": "Markdown",
"mkd": "Markdown",
"mason": "Mason",
"mathematica": "Mathematica",
"wl": "Mathematica",
"matlab": "MATLAB",
"m": "MATLAB",
"minizinc": "MiniZinc",
"moonscript": "MoonScript",
"moon": "MoonScript",
"mosel": "Mosel",
"msgtrans": "MessageTrans",
"mxml": "MXML",
"nasm": "Nasm",
"nesasm": "NesAsm",
"nes": "NesAsm",
"nginx": "nginx",
"nim": "Nim",
"nimrod": "Nim",
"nix": "Nix",
"nixos": "Nix",
"objective_c": "Objective-C",
"objc": "Objective-C",
"obj-c": "Objective-C",
"obj_c": "Objective-C",
"objectivec": "Objective-C",
"objective_cpp": "Objective-C++",
"objcpp": "Objective-C++",
"obj-cpp": "Objective-C++",
"obj_cpp": "Objective-C++",
"objectivecpp": "Objective-C++",
"objc++": "Objective-C++",
"obj-c++": "Objective-C++",
"obj_c++": "Objective-C++",
"objectivec++": "Objective-C++",
"ocaml": "OCaml",
"openedge": "OpenEdge ABL",
"opentype_feature_file": "OpenType Feature File",
"fea": "OpenType Feature File",
"opentype": "OpenType Feature File",
"opentypefeature": "OpenType Feature File",
"pascal": "Pascal",
"perl": "Perl",
"pl": "Perl",
"php": "PHP",
"php3": "PHP",
"php4": "PHP",
"php5": "PHP",
"plaintext": "Plain Text",
"text": "Plain Text",
"plist": "Plist",
"pony": "Pony",
"powershell": "powershell",
"posh": "powershell",
"microsoftshell": "powershell",
"msshell": "powershell",
"praat": "Praat",
"prolog": "Prolog",
"prometheus": "Prometheus",
"properties": ".properties",
"protobuf": "Protobuf",
"proto": "Protobuf",
"puppet": "Puppet",
"pp": "Puppet",
"python": "Python",
"py": "Python",
"q": "Q",
"kdb+": "Q",
"qml": "QML",
"r": "R",
"R": "R",
"s": "R",
"S": "R",
"racket": "Racket",
"reasonml": "ReasonML",
"rego": "Rego",
"robot_framework": "Robot Framework",
"robot": "Robot Framework",
"robot-framework": "Robot Framework",
"ruby": "Ruby",
"rb": "Ruby",
"rust": "Rust",
"rs": "Rust",
"rust,no_run": "Rust",
"rs,no_run": "Rust",
"rust,ignore": "Rust",
"rs,ignore": "Rust",
"rust,should_panic": "Rust",
"rs,should_panic": "Rust",
"sas": "SAS",
"sass": "Sass",
"scala": "Scala",
"scheme": "Scheme",
"scss": "SCSS",
"sed": "sed",
"shell": "shell",
"bash": "shell",
"zsh": "shell",
"ksh": "shell",
"sh": "shell",
"sieve": "Sieve",
"slice": "Slice",
"slim": "Slim",
"smalltalk": "Smalltalk",
"st": "Smalltalk",
"squeak": "Smalltalk",
"smarty": "Smarty",
"sml": "SML",
"ml": "SML",
"solidity": "Solidity",
"sparql": "SPARQL",
"sqf": "SQF",
"sql": "SQL",
"supercollider": "SuperCollider",
"swift": "Swift",
"tap": "TAP",
"tcl": "Tcl",
"terraform": "Terraform",
"tf": "Terraform",
"tex": "TeX",
"TeX": "TeX",
"LaTeX": "TeX",
"latex": "TeX",
"toml": "TOML",
"tsx": "TSX",
"ttcn3": "TTCN3",
"tulip": "Tulip",
"turtle": "Turtle/TriG",
"twig": "Twig",
"typescript": "TypeScript",
"ts": "TypeScript",
"vala": "Vala",
"vb": "Visual Basic",
"visualbasic": "Visual Basic",
"vcl": "VCL: Varnish Configuration Language",
"varnishconf": "VCL: Varnish Configuration Language",
"varnish": "VCL: Varnish Configuration Language",
"velocity": "Velocity",
"verilog": "Verilog and System Verilog",
"vhdl": "VHDL 2008",
"viml": "VimL",
"vim": "VimL",
"vimscript": "VimL",
"ex": "VimL",
"vue": "Vue",
"vuejs": "Vue",
"wollok": "Wollok",
"xml": "XML",
"xojo": "Xojo",
"realbasic": "Xojo",
"xpath": "XPath",
"xquery": "XQuery",
"yaml": "YAML",
"yml": "YAML",
"yang": "YANG",
"zig": "Zig",
"zir": "Zig"
}

31
_js/src/lightbox.js Normal file
View File

@@ -0,0 +1,31 @@
import { fromEvent } from "rxjs";
import { webComponentsReady, stylesheetReady } from "./common.js";
(async () => {
await Promise.all([
...("customElements" in window
? []
: [
import(
/* webpackChunkName: "webcomponents" */ "./polyfills/webcomponents.js"
).then(
() =>
import(
/* webpackChunkName: "shadydom" */ "./polyfills/shadydom.js"
),
),
]),
]);
await webComponentsReady;
await stylesheetReady;
await import(/* webpackMode: "eager" */ "fslightbox");
const pushStateEl = document.querySelector("hy-push-state");
const after$ = fromEvent(pushStateEl, "hy-push-state-after");
after$.subscribe(() => {
refreshFsLightbox();
});
})();

98
_js/src/navbar.js Normal file
View File

@@ -0,0 +1,98 @@
// Copyright (c) 2018 Florian Klampfer <https://qwtel.com/>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import { fromEvent, timer, merge } from "rxjs";
import {
map,
filter,
pairwise,
mergeWith,
mapTo,
tap,
switchMap,
startWith,
share,
debounceTime,
} from "rxjs/operators";
import { hasCSSOM, getScrollTop, stylesheetReady, filterWhen } from "./common";
(async () => {
await stylesheetReady;
const navbarEl = document.getElementById("_navbar");
if (!navbarEl) return;
// FIXME: update when size changes
const height = navbarEl.clientHeight;
let offset = 0;
const tv = hasCSSOM
? new CSSTransformValue([new CSSTranslate(CSS.px(0), CSS.px(0))])
: null;
const checkNavbarInactive = () =>
!document.activeElement?.classList.contains("nav-btn");
const hashchange$ = fromEvent(window, "hashchange").pipe(
map((e) => new URL(e.newURL).hash),
filter((hash) => hash !== "" && hash !== "#_search-input"),
share(),
);
// To disable the navbar while the "scroll into view" animation is active.
// Wait for 50ms after scrolling has stopped before unlocking the navbar.
const notScrollIntoView$ = hashchange$.pipe(
switchMap(() =>
fromEvent(document, "scroll").pipe(
debounceTime(50),
mapTo(true),
startWith(false),
),
),
startWith(true),
);
// Certain events should make the navbar "jump" to a new position.
const jump$ = merge(
// Focusing any navbar element should show the navbar to enable keyboard-only interaction.
fromEvent(navbarEl, "focus", { capture: true }).pipe(mapTo(2 * height)),
// Scrolling to a certain headline should hide the navbar to prevent hiding it.
hashchange$.pipe(mapTo(-2 * height)),
);
fromEvent(document, "scroll", { passive: true })
.pipe(
filterWhen(notScrollIntoView$),
map(getScrollTop),
filter((x) => x >= 0),
pairwise(),
map(([prev, curr]) => prev - curr),
filter(checkNavbarInactive),
mergeWith(jump$),
tap((x) => {
offset += x;
offset = Math.max(-height, Math.min(0, offset));
if (hasCSSOM) {
tv[0].y.value = offset;
navbarEl.attributeStyleMap.set("transform", tv);
} else {
navbarEl.style.transform = `translateY(${offset}px)`;
}
}),
)
.subscribe();
})();

View File

@@ -0,0 +1,2 @@
import "whatwg-fetch";
import "abortcontroller-polyfill/dist/polyfill-patch-fetch";

View File

@@ -0,0 +1,2 @@
import { ResizeObserver } from "@juggle/resize-observer";
window.ResizeObserver = window.ResizeObserver || ResizeObserver;

View File

@@ -0,0 +1,2 @@
import "@webcomponents/shadydom";
import "@webcomponents/shadycss/entrypoints/scoping-shim";

View File

@@ -0,0 +1,41 @@
import "@webcomponents/webcomponents-platform";
import "@webcomponents/url";
import "@webcomponents/template";
import "@webcomponents/custom-elements";
const { customElements } = window;
let shouldFlush = false;
/** @type {?function()} */
let flusher = null;
if (customElements["polyfillWrapFlushCallback"]) {
customElements["polyfillWrapFlushCallback"]((flush) => {
flusher = flush;
if (shouldFlush) {
flush();
}
});
}
function flushAndFire() {
if (window.HTMLTemplateElement.bootstrap) {
window.HTMLTemplateElement.bootstrap(window.document);
}
flusher && flusher();
shouldFlush = true;
document.dispatchEvent(
new CustomEvent("WebComponentsReady", { bubbles: true }),
);
}
if (document.readyState !== "complete") {
// this script may come between DCL and load, so listen for both, and cancel load listener if DCL fires
window.addEventListener("load", flushAndFire);
window.addEventListener("DOMContentLoaded", () => {
window.removeEventListener("load", flushAndFire);
flushAndFire();
});
} else {
flushAndFire();
}

232
_js/src/push-state.js Normal file
View File

@@ -0,0 +1,232 @@
// Copyright (c) 2019 Florian Klampfer <https://qwtel.com/>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import { fromEvent, merge, timer, zip } from "rxjs";
import {
tap,
exhaustMap,
map,
mapTo,
mergeMap,
pairwise,
share,
startWith,
switchMap,
takeUntil,
} from "rxjs/operators";
import { animate, empty, importTemplate, webComponentsReady } from "./common";
import { CrossFader } from "./cross-fader";
import { setupFLIP } from "./flip";
(async () => {
await Promise.all([
...("fetch" in window
? []
: [import(/* webpackChunkName: "fetch" */ "./polyfills/fetch")]),
...("customElements" in window
? []
: [
import(
/* webpackChunkName: "webcomponents" */ "./polyfills/webcomponents"
),
]),
...("animate" in Element.prototype
? []
: [import(/* webpackChunkName: "webanimations" */ "web-animations-js")]),
...("IntersectionObserver" in window
? []
: [
import(
/* webpackChunkName: "intersection-observer" */ "intersection-observer"
),
]),
]);
await webComponentsReady;
const NAVBAR_SEL = "#_navbar > .content > .nav-btn-bar";
const CROSS_FADE_DURATION = 2000;
const FADE_OUT = [{ opacity: 1 }, { opacity: 0 }];
const FADE_IN = [
{ opacity: 0, transform: "translateY(-3rem)" },
{ opacity: 1, transform: "translateY(0)" },
];
function setupAnimationMain(pushStateEl) {
pushStateEl?.parentNode?.insertBefore(
importTemplate("_animation-template"),
pushStateEl,
);
return pushStateEl?.previousElementSibling;
}
function setupLoading(navbarEl) {
navbarEl?.appendChild(importTemplate("_loading-template"));
return navbarEl?.lastElementChild;
}
function setupErrorPage(main, url) {
const { pathname } = url;
const error = importTemplate("_error-template");
const anchor = error?.querySelector(".this-link");
if (anchor) {
anchor.href = pathname;
anchor.textContent = pathname;
}
main?.appendChild(error);
return main?.lastElementChild;
}
function getFlipType(el) {
if (el?.classList.contains("flip-title")) return "title";
if (el?.classList.contains("flip-project")) return "project";
return el?.getAttribute?.("data-flip");
}
const pushStateEl = document.querySelector("hy-push-state");
if (!pushStateEl) return;
const duration = Number(pushStateEl.getAttribute("duration")) || 350;
const settings = {
duration: duration,
easing: "ease",
};
const animateFadeOut = ({ main }) =>
animate(main, FADE_OUT, { ...settings, fill: "forwards" }).pipe(
mapTo({ main }),
);
const animateFadeIn = ({ replaceEls: [main], flipType }) =>
animate(main, FADE_IN, settings).pipe(mapTo({ main, flipType }));
const drawerEl = document.querySelector("hy-drawer");
const navbarEl = document.querySelector(NAVBAR_SEL);
const animationMain = setupAnimationMain(pushStateEl);
const loadingEl = setupLoading(navbarEl);
const fromEventX = (eventName, mapFn) =>
fromEvent(pushStateEl, eventName).pipe(
map(({ detail }) => detail),
mapFn ? map(mapFn) : (_) => _,
share(),
);
const start$ = fromEventX("hy-push-state-start", (detail) =>
Object.assign(detail, { flipType: getFlipType(detail.anchor) }),
);
const ready$ = fromEventX("hy-push-state-ready");
const after$ = fromEventX("hy-push-state-after");
const progress$ = fromEventX("hy-push-state-progress");
const error$ = fromEventX("hy-push-state-networkerror");
const fadeOut$ = start$.pipe(
map((context) =>
Object.assign(context, { main: document.getElementById("_main") }),
),
tap(({ main }) => {
main.style.pointerEvents = "none";
}),
window._noDrawer && drawerEl?.classList.contains("cover")
? tap(() => {
drawerEl.classList.remove("cover");
drawerEl.parentNode?.appendChild(drawerEl);
})
: (_) => _,
exhaustMap(animateFadeOut),
tap(({ main }) => empty.call(main)),
share(),
);
if (loadingEl) {
progress$.subscribe(() => {
loadingEl.style.display = "flex";
});
ready$.subscribe(() => {
loadingEl.style.display = "none";
});
}
const fadeIn$ = after$.pipe(
switchMap((context) => {
const p = animateFadeIn(context).toPromise();
context.transitionUntil(p);
return p;
}),
share(),
);
const flip$ = setupFLIP(start$, ready$, merge(fadeIn$, error$), {
animationMain,
settings: settings,
}).pipe(share());
start$
.pipe(
switchMap((context) => {
const promise = zip(timer(duration), fadeOut$, flip$).toPromise();
context.transitionUntil(promise);
return promise;
}),
)
.subscribe();
// FIXME: Keeping permanent subscription? turn into hot observable?
fadeOut$.subscribe();
flip$.subscribe();
const sidebarBg = document.querySelector(".sidebar-bg");
if (sidebarBg) {
const crossFader = new CrossFader(CROSS_FADE_DURATION);
after$
.pipe(
switchMap(({ document }) =>
zip(crossFader.fetchImage(document), fadeIn$).pipe(
map(([x]) => x),
takeUntil(start$),
),
),
startWith([sidebarBg]),
pairwise(),
mergeMap(([prev, curr]) => crossFader.fade(prev, curr)),
)
.subscribe();
}
error$
.pipe(
switchMap(({ url }) => {
if (loadingEl) loadingEl.style.display = "none";
const main = document.getElementById("_main");
if (main) main.style.pointerEvents = "";
empty.call(animationMain?.querySelector(".page"));
empty.call(main);
setupErrorPage(main, url);
return animate(main, FADE_IN, { ...settings, fill: "forwards" });
}),
)
.subscribe();
import(/* webpackMode: "eager" */ "@hydecorp/push-state");
window._pushState = pushStateEl;
})();

138
_js/src/search.js Normal file
View File

@@ -0,0 +1,138 @@
import { fromEvent, merge, timer, zip } from "rxjs";
import {
tap,
exhaustMap,
map,
mapTo,
mergeMap,
pairwise,
share,
startWith,
switchMap,
takeUntil,
filter,
} from "rxjs/operators";
import { webComponentsReady, importTemplate, stylesheetReady } from "./common";
(async () => {
await Promise.all([
...("customElements" in window
? []
: [
import(
/* webpackChunkName: "webcomponents" */ "./polyfills/webcomponents.js"
).then(
() =>
import(
/* webpackChunkName: "shadydom" */ "./polyfills/shadydom.js"
),
),
]),
]);
await webComponentsReady;
await stylesheetReady;
const SEL_NAVBAR_BTN_BAR = "#_navbar > .content > .nav-btn-bar";
const search = importTemplate("_search-template");
const pushStateEl = document.querySelector("hy-push-state");
if (!pushStateEl || !search) return;
await import(/* webpackMode: "eager" */ "@honeymachine/search");
const navbarEl = document.querySelector(SEL_NAVBAR_BTN_BAR);
navbarEl?.insertBefore(search, navbarEl.querySelector(".nav-insert-marker"));
const searchBtn = document.getElementById("_search");
const searchEl = document.querySelector("hm-search");
const documents = await fetch("/search.json").then((r) => r.json());
searchEl.setAttribute("documents", JSON.stringify(documents));
searchEl.setAttribute(
"fields",
JSON.stringify(["title", "description", "category", "tags"]),
);
const start$ = fromEvent(pushStateEl, "hy-push-state-start");
const ready$ = fromEvent(pushStateEl, "hy-push-state-after");
const search$ = fromEvent(searchEl, "search");
const click$ = fromEvent(searchBtn, "click");
let articles = (" " + document.getElementById("_main").innerHTML).slice(1);
const reset = () => {
const result = document.getElementById("_main");
result.classList.remove("search-results");
result.innerHTML = articles;
result.querySelectorAll("img, h1").forEach((article) => {
article.setAttribute("style", "opacity: 1;");
});
};
start$.subscribe(() => {
searchEl.clear();
reset();
});
ready$.subscribe(() => {
const result = document.getElementById("_main");
if (result.innerHTML) {
articles = (" " + result.innerHTML).slice(1);
}
});
search$.subscribe((e) => {
const result = document.getElementById("_main");
const hits = e.detail;
if (hits.length === 0) {
reset();
} else {
result.classList.add("search-results");
result.innerHTML = "";
hits.forEach((hit) => {
const item = documents.find((doc) => doc.id === hit.ref);
const articleEl = document.createElement("article");
articleEl.classList.add("search-result", "page", "post", "mb6");
articleEl.setAttribute("role", "article");
articleEl.setAttribute("id", "post-" + item.id);
articleEl.innerHTML = `
<header>
<h1 class="post-title flip-project-title">
<a href="${item.url}" class="flip-title">${item.title}</a>
</h1>
<a
href="${item.url}"
class="no-hover no-print-link flip-project"
tabindex="-1"
>
<div class="img-wrapper lead aspect-ratio sixteen-nine flip-project-img">
<img
src="${item.image}"
alt="${item.title}"
width="864"
height="486"
loading="lazy"
/>
</div>
</a>
<p class="note-sm">
${item.description}
</p>
</header>
`;
result.appendChild(articleEl);
});
}
});
click$.subscribe(() => {
searchEl.hidden = !searchEl.hidden;
!searchEl.active ? searchEl.focus() : searchEl.clear();
searchEl.setAttribute("aria-expanded", !searchEl.hidden);
});
})();

120
_js/src/sound.js Normal file
View File

@@ -0,0 +1,120 @@
import { fromEvent } from "rxjs";
import { webComponentsReady, stylesheetReady } from "./common.js";
import { Wave } from "@foobar404/wave";
(async () => {
await Promise.all([
...("customElements" in window
? []
: [
import(
/* webpackChunkName: "webcomponents" */ "./polyfills/webcomponents.js"
).then(
() =>
import(
/* webpackChunkName: "shadydom" */ "./polyfills/shadydom.js"
),
),
]),
]);
await webComponentsReady;
await stylesheetReady;
const sound = document.querySelector(".sound-player");
if (!sound) return;
const fallback = JSON.parse(sound.getAttribute("data-featured"));
let featured = fallback;
let currentIndex = undefined;
const randomTrackIndex = () =>
Math.floor(Math.random() * (featured.tracks.length - 1));
const updateTrack = (index, active) => {
if (index != null) {
const track = featured.tracks[index];
audioElement.setAttribute(
"src",
`/assets/sounds/${featured.slug}/${track}`,
);
currentIndex = index;
}
if (active) {
const track = featured.tracks[currentIndex];
buttonElement.textContent = `[ ${track.substring(0, track.lastIndexOf("."))} ]`;
audioElement.play();
canvasElement.classList.remove("hidden");
} else {
buttonElement.textContent = "[ SOUND ON ]";
audioElement.pause();
canvasElement.classList.add("hidden");
}
};
const updateSound = () => {
const sound = document.querySelector(".sound-wrapper");
if (!sound) return;
featured = JSON.parse(sound.getAttribute("data-featured"));
updateTrack(0, !audioElement.paused);
const buttons = document.querySelectorAll(".sound-wrapper .sound-item");
buttons.forEach((button, i) => {
fromEvent(button, "click").subscribe((event) => {
event.preventDefault();
updateTrack(i, true);
});
});
};
const audioElement = document.querySelector(".sound-player audio");
const buttonElement = document.querySelector(".sound-player a");
const canvasElement = document.querySelector(".sound-player canvas");
const pushStateEl = document.querySelector("hy-push-state");
const wave = new Wave(audioElement, canvasElement);
wave.addAnimation(
new wave.animations.Arcs({
lineColor: {
gradient: [
"#d16ba5",
"#c777b9",
"#ba83ca",
"#aa8fd8",
"#9a9ae1",
"#8aa7ec",
"#79b3f4",
"#69bff8",
"#52cffe",
"#41dfff",
"#46eefa",
"#5ffbf1",
],
rotate: 45,
},
lineWidth: 10,
diameter: 4,
count: 20,
frequencyBand: "lows",
}),
);
fromEvent(buttonElement, "click").subscribe(() => {
updateTrack(undefined, audioElement.paused);
});
fromEvent(audioElement, "ended").subscribe(() => {
updateTrack(randomTrackIndex(), true);
});
fromEvent(pushStateEl, "hy-push-state-after").subscribe(() => {
updateSound();
});
updateTrack(randomTrackIndex(), false);
updateSound();
})();

320
_js/src/upgrades.js Normal file
View File

@@ -0,0 +1,320 @@
import {
importTemplate,
intersectOnce,
loadCSS,
stylesheetReady,
once,
} from "./common";
import { fromEvent } from "rxjs";
import { concatMap } from "rxjs/operators";
import { createElement } from "create-element-x/library";
import tippy from "tippy.js";
// import LANG from './languages.json';
const toggleClass = (element, ...cls) => {
element.classList.remove(...cls);
void element.offsetWidth;
element.classList.add(...cls);
};
(async () => {
await Promise.all([
...("animate" in Element.prototype
? []
: [import(/* webpackChunkName: "webanimations" */ "web-animations-js")]),
...("IntersectionObserver" in window
? []
: [
import(
/* webpackChunkName: "intersection-observer" */ "intersection-observer"
),
]),
]);
await stylesheetReady;
const FN_SEL = "li[id^='fn:']";
const FN_LINK_SEL = "a[href^='#fn:']";
const HORIZONTAL_SCROLL_SEL =
'pre, table:not(.highlight), .katex-display, .break-layout, mjx-container[jax="CHTML"][display="true"]';
const CODE_BLOCK_SEL = "pre.highlight > code";
const CODE_TITLE_RE = /(?:title|file):\s*['"`](([^'"`\\]|\\.)*)['"`]/iu;
const HEADING_SELECTOR = "h1[id], h2[id], h3[id], h4[id], h5[id], h6[id]";
const IMG_FADE_DURATION = 500;
const IMG_KEYFRAMES = [{ opacity: 0 }, { opacity: 1 }];
const IMG_SETTINGS = {
fill: "forwards",
duration: IMG_FADE_DURATION,
easing: "ease",
};
const pushStateEl = document.querySelector("hy-push-state");
const CODE_LINE_HEIGHT = 1.5;
/** @param {(param0: HTMLElement|null) => void} fn
* @param {any=} opts */
function ready(fn, opts) {
if (pushStateEl && !window._noPushState) {
pushStateEl.addEventListener(
"hy-push-state-ready",
({
detail: {
replaceEls: [main],
},
}) => fn(main),
opts,
);
}
fn(document.getElementById("_main"));
}
/** @param {(param0: HTMLElement|null) => void} fn
* @param {any=} opts */
function load(fn, opts) {
if (pushStateEl && !window._noPushState) {
pushStateEl.addEventListener(
"hy-push-state-load",
() => fn(document.getElementById("_main")),
opts,
);
}
fn(document.getElementById("_main"));
}
let init = true;
ready((main) => {
if (!main) return;
tippy(main.querySelectorAll(".post-date > .ellipsis"), {
trigger: "click",
touch: true,
interactive: true,
allowHTML: true,
maxWidth: "none",
placement: "bottom-start",
offset: 0,
content: (el) => el.innerHTML,
onTrigger(instance, event) {
if (event.target.tagName === "A") {
instance._hideOnce = true;
}
},
onShow(instance) {
if (instance._hideOnce) {
return (instance._hideOnce = false);
}
},
});
tippy(main.querySelectorAll("abbr[title]"), {
trigger: "click",
touch: true,
maxWidth: 500,
content: (el) => el.getAttribute("title"),
});
tippy(main.querySelectorAll(".sidebar-social [title]"), {
touch: true,
content: (el) => el.getAttribute("title"),
});
main.querySelectorAll(HEADING_SELECTOR).forEach((h) => {
const df = importTemplate("_permalink-template");
const a = df.querySelector(".permalink");
a.href = `#${h.id}`;
h.appendChild(df);
});
const toc = main.querySelector("#markdown-toc");
if (toc) toc.classList.add("toc-hide");
if ("clipboard" in navigator && "ClipboardItem" in window) {
Array.from(main.querySelectorAll(CODE_BLOCK_SEL)).forEach((el) => {
const container = el?.parentNode?.parentNode;
const writeText = async () => {
await navigator.clipboard.write([
new ClipboardItem({
"text/plain": new Blob([el.textContent], { type: "text/plain" }),
}),
]);
toggleClass(copyBtn, "copy-success");
};
const copyBtn = createElement(
"button",
{ onClick: writeText },
createElement("small", { class: "icon-copy", title: "Copy" }),
createElement("small", { class: "icon-checkmark", title: "Done" }),
);
container?.appendChild(copyBtn);
});
}
Array.from(main.querySelectorAll(CODE_BLOCK_SEL))
.map((code) => code.children[0])
.forEach((el) => {
const result = CODE_TITLE_RE.exec(el?.innerText);
if (!result) return;
const [, fileName] = result;
const code = el.parentNode;
// Remove the first line
const child0 = el.childNodes[0];
const nli = child0.wholeText.indexOf("\n");
if (nli > -1) {
const restNode = child0.splitText(nli);
code.insertBefore(restNode, code.firstChild);
}
// Remove element before making changes
code.removeChild(el);
// Remove newline
code.childNodes[0].splitText(1);
code.removeChild(code.childNodes[0]);
const container = code.parentNode.parentNode;
// Language
// const highlighter = container.parentNode;
// const [, lang] = highlighter.classList.value.match(/language-(\w*)/) ?? [];
// const language = LANG[lang];
const header = createElement(
"div",
{ class: "pre-header break-layout" },
createElement(
"span",
{},
createElement("small", { class: "icon-file-empty" }),
" ",
fileName,
),
// !language ? null : createElement('small', { class: 'fr lang' }, language),
);
container.insertBefore(header, container.firstChild);
});
if ("complete" in HTMLImageElement.prototype) {
main
.querySelectorAll("img[width][height][loading=lazy]")
.forEach((el) => {
if (init && el.complete) return;
el.style.opacity = "0";
// TODO: replace with loading spinner
el.addEventListener(
"load",
() => el.animate(IMG_KEYFRAMES, IMG_SETTINGS),
{ once: true },
);
});
init = false;
}
// main.querySelectorAll(pushStateEl.linkSelector).forEach(anchor => {
// caches.match(anchor.href).then(m => {
// if (m) requestAnimationFrame(() => anchor.classList.add("visited"));
// });
// });
});
/** @type {Promise<{}>|null} */
let katexPromise = null;
load(() => {
const main = document.getElementById("_main");
if (!main) return;
const toc = main.querySelector("#markdown-toc");
if (toc) {
toc.classList.remove("toc-hide");
toc.classList.add("toc-show");
}
main.querySelectorAll(FN_SEL).forEach((li) => (li.tabIndex = 0));
main
.querySelectorAll(FN_LINK_SEL)
.forEach((a) =>
a.addEventListener("click", (e) =>
document
.getElementById(e.currentTarget.getAttribute("href").substr(1))
?.focus(),
),
);
main
.querySelectorAll(HORIZONTAL_SCROLL_SEL)
.forEach((el) =>
el.addEventListener(
"touchstart",
(e) => el.scrollLeft > 0 && e.stopPropagation(),
{ passive: false },
),
);
// Array.from(main.querySelectorAll(CODE_BLOCK_SEL)).forEach((code) => {
// Array.from(code.querySelectorAll('span[class^="c"]'))
// .filter((c1) => c1.innerText.includes('!!'))
// .forEach((c1) => {
// const [, n] = c1.innerText.match(/!!\s*(\d+)/) || [, '1'];
// const hl = createElement('span', { class: '__hl', style: `height: ${Number(n) * CODE_LINE_HEIGHT}em` });
// c1.innerText = c1.innerText.replace(`!!${n}`, '!!');
// const hasContent = c1.innerText?.match(/[\p{L}|\d]/u);
// if (!hasContent) {
// c1.parentElement?.replaceChild(hl, c1);
// } else {
// c1.innerText = c1.innerText.replace('!!', '');
// c1.parentElement?.insertBefore(hl, c1);
// }
// });
// });
const katexHref = document.getElementById("_katexPreload")?.href;
if (!katexPromise && katexHref) {
intersectOnce(main.querySelectorAll(".katex"), {
rootMargin: "1440px",
}).then(() => {
katexPromise = loadCSS(katexHref);
});
}
});
const mathJaxEl = document.getElementById("_MathJax");
if (pushStateEl && mathJaxEl) {
const mathJax2To3 = ({
detail: {
replaceEls: [mainEl],
},
}) => {
mainEl
.querySelectorAll('script[type="math/tex; mode=display"]')
.forEach((el) => {
el.outerHTML = el.innerText
.replace("% <![CDATA[", "\\[")
.replace("%]]>", "\\]");
});
mainEl.querySelectorAll('script[type="math/tex"]').forEach((el) => {
el.outerHTML = `\\(${el.innerText}\\)`;
});
};
mathJax2To3({ detail: { replaceEls: [document] } });
if (!("MathJax" in window)) await once(mathJaxEl, "load");
await MathJax.typesetPromise();
if (!window._noPushState) {
pushStateEl.addEventListener("ready", mathJax2To3);
fromEvent(pushStateEl, "after")
.pipe(concatMap(() => MathJax.typesetPromise()))
.subscribe();
}
}
})();