From b2a4f8d4e0a6d15d94bffbd73035f6e4909b7a56 Mon Sep 17 00:00:00 2001 From: Xananax Date: Thu, 11 Aug 2022 14:38:08 +0200 Subject: [PATCH] testing moving the functionality into small modules --- index.html | 245 +----------------------------- modules/changeTitle.mjs | 12 ++ modules/documentMode.mjs | 13 ++ modules/fetchMarkdown.mjs | 24 +++ modules/fetchText.mjs | 16 ++ modules/generateDomFromString.mjs | 13 ++ modules/getCurrentHashUrl.mjs | 16 ++ modules/getElementById.mjs | 19 +++ modules/index.mjs | 15 ++ modules/isExternalUrl.mjs | 10 ++ modules/isLocalHost.mjs | 8 + modules/isNotNull.mjs | 10 ++ modules/onDocumentKey.mjs | 11 ++ modules/parseFileList.mjs | 40 +++++ modules/querySelectorAll.mjs | 40 +++++ modules/rewriteLocalUrls.mjs | 20 +++ modules/templateBlog.mjs | 107 +++++++++++++ modules/wait.mjs | 18 +++ modules/waitIfLocalHost.mjs | 31 ++++ 19 files changed, 428 insertions(+), 240 deletions(-) create mode 100644 modules/changeTitle.mjs create mode 100644 modules/documentMode.mjs create mode 100644 modules/fetchMarkdown.mjs create mode 100644 modules/fetchText.mjs create mode 100644 modules/generateDomFromString.mjs create mode 100644 modules/getCurrentHashUrl.mjs create mode 100644 modules/getElementById.mjs create mode 100644 modules/index.mjs create mode 100644 modules/isExternalUrl.mjs create mode 100644 modules/isLocalHost.mjs create mode 100644 modules/isNotNull.mjs create mode 100644 modules/onDocumentKey.mjs create mode 100644 modules/parseFileList.mjs create mode 100644 modules/querySelectorAll.mjs create mode 100644 modules/rewriteLocalUrls.mjs create mode 100644 modules/templateBlog.mjs create mode 100644 modules/wait.mjs create mode 100644 modules/waitIfLocalHost.mjs diff --git a/index.html b/index.html index cd2d05d..063d3ba 100644 --- a/index.html +++ b/index.html @@ -125,15 +125,15 @@ background: var(--accent); color: var(--background); } - .menu-is-open .menu { + .is-menu-open .menu { transform: translateX(0); transition-duration: 0.1s; } - .menu-is-open .burger { + .is-menu-open .burger { color: transparent; } - .menu-is-open .burger, + .is-menu-open .burger, #Loading { top: 0; right: 0; @@ -197,243 +197,8 @@

diff --git a/modules/changeTitle.mjs b/modules/changeTitle.mjs new file mode 100644 index 0000000..6897652 --- /dev/null +++ b/modules/changeTitle.mjs @@ -0,0 +1,12 @@ +/** + * Changes the document's title + * @param {string} title + * @param {string} mainTitle + * @returns + */ +export const changeTitle = (title, mainTitle = changeTitle.title) => + document && (document.title = `${title} | ${mainTitle}`); + +changeTitle.title = (document && document.title) || ""; + +export default changeTitle; diff --git a/modules/documentMode.mjs b/modules/documentMode.mjs new file mode 100644 index 0000000..3039f46 --- /dev/null +++ b/modules/documentMode.mjs @@ -0,0 +1,13 @@ +//@ts-check + +/** + * Creates a helper to add or remove global classes that begin with `is-` + * @param {string} name + */ +export const documentMode = (name) => ({ + on: () => document.body.classList.add(`is-${name}`), + off: () => document.body.classList.remove(`is-${name}`), + toggle: () => document.body.classList.toggle(`is-${name}`), +}); + +export default documentMode; diff --git a/modules/fetchMarkdown.mjs b/modules/fetchMarkdown.mjs new file mode 100644 index 0000000..131bebf --- /dev/null +++ b/modules/fetchMarkdown.mjs @@ -0,0 +1,24 @@ +//@ts-check + +import { fetchText } from "./fetchText.mjs"; +import { waitIfLocalHost } from "./waitIfLocalHost.mjs"; +import { rewriteLocalUrls } from "./rewriteLocalUrls.mjs"; +import { generateDomFromString } from "./generateDomFromString.mjs"; +// @ts-ignore +import { micromark } from "https://esm.sh/micromark@3?bundle"; + +/** + * Loads and parses a markdown document. Makes use of micromark + * @param {string} path the path to load + */ +export const fetchMarkdown = (path) => + fetchText(path) + .then(waitIfLocalHost()) + .then((raw) => { + const content = rewriteLocalUrls(generateDomFromString(micromark(raw))); + const firstTitleElement = content.querySelector("h1"); + const title = firstTitleElement + ? firstTitleElement.textContent + : path.replace(/\.\w{2, 4}$/, ""); + return { title, raw, content }; + }); diff --git a/modules/fetchText.mjs b/modules/fetchText.mjs new file mode 100644 index 0000000..684b578 --- /dev/null +++ b/modules/fetchText.mjs @@ -0,0 +1,16 @@ +//@ts-check + +import isLocalHost from "./isLocalHost.mjs"; + +/** + * Loads a text file. The path provided will be appened with a random number + * when running on localhost to avoid caching problems + * @param {string} path + * @returns {Promise} the loaded file + */ +export const fetchText = (path) => + fetch(isLocalHost ? `./${path}?rand=${Math.random()}` : `./${path}`).then( + (response) => response.text() + ); + +export default fetchText; diff --git a/modules/generateDomFromString.mjs b/modules/generateDomFromString.mjs new file mode 100644 index 0000000..1a4800b --- /dev/null +++ b/modules/generateDomFromString.mjs @@ -0,0 +1,13 @@ +//@ts-check + +/** + * Generates valid dom elements from a string + * @param {string} htmlString + */ +export const generateDomFromString = (htmlString) => + /** @type {HTMLElement} */ ( + new DOMParser().parseFromString(`
${htmlString}
`, "text/html") + .firstChild + ); + +export default generateDomFromString; diff --git a/modules/getCurrentHashUrl.mjs b/modules/getCurrentHashUrl.mjs new file mode 100644 index 0000000..6551fa9 --- /dev/null +++ b/modules/getCurrentHashUrl.mjs @@ -0,0 +1,16 @@ +//@ts-check + +/** + * Returns the hash part of the url, but only if it starts with a `/` + * This allows regular hashes to continue to work + * It reads also query parameters + */ +export const getCurrentHashUrl = () => { + const [path, searchStr] = ( + window.location.hash[1] === "/" ? window.location.hash.slice(2) : "" + ).split("?"); + const params = new URLSearchParams(searchStr); + return { path, params }; +}; + +export default getCurrentHashUrl; diff --git a/modules/getElementById.mjs b/modules/getElementById.mjs new file mode 100644 index 0000000..d32489d --- /dev/null +++ b/modules/getElementById.mjs @@ -0,0 +1,19 @@ +//@ts-check +import { isLocalHost } from "./isLocalHost.mjs"; + +/** + * Gets an element by id if the element exists, otherwise throws, but only if running in localhost environments. + * Use this in the initial setup to verify all elements exist + * @param {string} id + * @return {HTMLElement} + */ +export const getElementById = (id) => { + const element = document && document.getElementById(id); + if (isLocalHost && !element) { + throw new Error(`Element "#${id}" was not found`); + } + // @ts-ignore + return element; +}; + +export default getElementById; diff --git a/modules/index.mjs b/modules/index.mjs new file mode 100644 index 0000000..6dd8f3e --- /dev/null +++ b/modules/index.mjs @@ -0,0 +1,15 @@ +import { fetchText } from "./fetchText.mjs"; +import { generateDomFromString } from "./generateDomFromString.mjs"; +import { isExternalUrl } from "./isExternalUrl.mjs"; +import { isLocalHost } from "./isLocalHost.mjs"; +import { parseFileList } from "./parseFileList.mjs"; +import { + querySelectorDoc, + querySelectorParent, + querySelectorAll, +} from "./querySelectorAll.mjs"; +import { rewriteLocalUrls } from "./rewriteLocalUrls.mjs"; +import { wait } from "./wait.mjs"; +import { waitIfLocalHost } from "./waitIfLocalHost.mjs"; + +export { isExternalUrl, isLocalHost, rewriteLocalUrls, wait, waitIfLocalHost }; diff --git a/modules/isExternalUrl.mjs b/modules/isExternalUrl.mjs new file mode 100644 index 0000000..45c6b0e --- /dev/null +++ b/modules/isExternalUrl.mjs @@ -0,0 +1,10 @@ +//@ts-check + +/** + * Assumes a provided url is external if it begins by a known protocol + * @param {string} url + */ +export const isExternalUrl = (url) => + url && /^(https?|mailto|tel|ftp|ipfs|dat):/.test(url); + +export default isExternalUrl; diff --git a/modules/isLocalHost.mjs b/modules/isLocalHost.mjs new file mode 100644 index 0000000..2425133 --- /dev/null +++ b/modules/isLocalHost.mjs @@ -0,0 +1,8 @@ +//@ts-check + +/** @type {boolean} returns true if the window global object exists and the domain is localhost of 127.0.0.1 */ +export const isLocalHost = + typeof window !== "undefined" && + /^localhost|127.0.0.1/.test(window.location.hostname); + +export default isLocalHost; diff --git a/modules/isNotNull.mjs b/modules/isNotNull.mjs new file mode 100644 index 0000000..fed06fc --- /dev/null +++ b/modules/isNotNull.mjs @@ -0,0 +1,10 @@ +//@ts-check + +/** + * @template T + * @param {T} value + * @returns {value is NonNullable} + */ +export const isNotNull = (value) => value !== null; + +export default isNotNull; diff --git a/modules/onDocumentKey.mjs b/modules/onDocumentKey.mjs new file mode 100644 index 0000000..951dd56 --- /dev/null +++ b/modules/onDocumentKey.mjs @@ -0,0 +1,11 @@ +/** + * + * @param {string} keyToListen + * @param {()=>void} callback + */ +export const onDocumentKeyUp = (keyToListen, callback) => { + document.addEventListener( + "keyup", + ({ key }) => key === keyToListen && callback() + ); +}; diff --git a/modules/parseFileList.mjs b/modules/parseFileList.mjs new file mode 100644 index 0000000..24ab139 --- /dev/null +++ b/modules/parseFileList.mjs @@ -0,0 +1,40 @@ +//@ts-check + +import { isNotNull } from "./isNotNull.mjs"; + +/** + * parses a filelist string. That's a string that looks like + * ```md + * name dd-mm-yyyy link name + * name dd-mm-yyyy + * name linkname + * name + * ``` + * + * @param {string} lines + */ +export const parseFileList = (lines) => + // @ts-ignore + lines + .trim() + .split(`\n`) + .map((line, index) => { + const today = new Date().toISOString().split("T")[0]; + const result = line.match( + /(?.+)\.(?\w{2,3})(?:\s+(?[\d-]+)?(?.+))?/ + ); + if (!result) { + console.error(`could not parse line number ${index}`); + return null; + } + const { + // @ts-ignore + groups: { name, ext, date = today, title = name }, + } = result; + const href = `/${name}.${ext}`; + const date_unix = new Date(date).getTime(); + return { name, href, date, title, date_unix, index }; + }) + .filter(isNotNull); + +export default parseFileList; diff --git a/modules/querySelectorAll.mjs b/modules/querySelectorAll.mjs new file mode 100644 index 0000000..d4e2b20 --- /dev/null +++ b/modules/querySelectorAll.mjs @@ -0,0 +1,40 @@ +//@ts-check + +/** + * A small utility to query elements and get back an array + * @template {keyof HTMLElementTagNameMap} K + * @type {{ + * (selector: K): HTMLElementTagNameMap[K][] + * (parent: ParentNode, selector: K): HTMLElementTagNameMap[K][] + * }} + */ +// @ts-ignore +export const querySelectorAll = (parent, selector) => + // @ts-ignore + typeof selector === "undefined" + ? // @ts-ignore + querySelectorDoc(/** @type {keyof HTMLElementTagNameMap} */ (parent)) + : querySelectorParent(parent, selector); + +/** + * A small utility to query elements in the document and get back an array + * @template {keyof HTMLElementTagNameMap} K + * @param {K} selector + * @returns {HTMLElementTagNameMap[K][]} + */ +export const querySelectorDoc = (selector) => [ + ...document.querySelectorAll(selector), +]; + +/** + * A small utility to query elements in a parent and get back an array + * @template {keyof HTMLElementTagNameMap} K + * @param {ParentNode} parent + * @param {K} selector + * @returns {HTMLElementTagNameMap[K][]} + */ +export const querySelectorParent = (parent, selector) => [ + ...parent.querySelectorAll(selector), +]; + +export default querySelectorAll; diff --git a/modules/rewriteLocalUrls.mjs b/modules/rewriteLocalUrls.mjs new file mode 100644 index 0000000..acf5bbd --- /dev/null +++ b/modules/rewriteLocalUrls.mjs @@ -0,0 +1,20 @@ +//@ts-check + +import isExternalUrl from "./isExternalUrl.mjs"; +import { querySelectorParent } from "./querySelectorAll.mjs"; + +/** + * Makes sure urls to local pages get passed through the routing system + * @param {HTMLElement} container the element containing links to find + */ +export const rewriteLocalUrls = (container) => { + querySelectorParent(container, "a").forEach((a) => { + const href = a.getAttribute("href"); + if (href && !isExternalUrl(href)) { + a.setAttribute("href", "#/" + href.replace(/^\.?\//, "")); + } + }); + return container; +}; + +export default rewriteLocalUrls; diff --git a/modules/templateBlog.mjs b/modules/templateBlog.mjs new file mode 100644 index 0000000..2dbef40 --- /dev/null +++ b/modules/templateBlog.mjs @@ -0,0 +1,107 @@ +//@ts-check +import { changeTitle } from "./changeTitle.mjs"; +import { getCurrentHashUrl } from "./getCurrentHashUrl.mjs"; +import { fetchMarkdown } from "./fetchMarkdown.mjs"; +import { fetchText } from "./fetchText.mjs"; +import { parseFileList } from "./parseFileList.mjs"; +import { documentMode } from "./documentMode.mjs"; +import { getElementById } from "./getElementById.mjs"; +import { onDocumentKeyUp } from "./onDocumentKey.mjs"; + +/***************************************************************** + * + * Creating references to the important stuff + * + ****************************************************************/ + +/** + * The elements we will need + */ +const [Menu, Body, Source, Burger] = ["Menu", "Body", "Source", "Burger"].map( + getElementById +); + +const loadingMode = documentMode("loading"); +const sourceEnableMode = documentMode("source-enabled"); +const menuOpenMode = documentMode("menu-open"); + +/***************************************************************** + * + * Router + * + * Things related to main navigation and to loading pages + * + ****************************************************************/ + +/** + * Listens to hashchange event, and attempts to read the url. + * @param {HashChangeEvent} [_evt] + */ +export const onHashChange = async (_evt) => { + const { path, params } = getCurrentHashUrl(); + if (!path) { + return false; + } + loadingMode.on(); + const { title, raw, content } = await fetchMarkdown(path); + changeTitle(title); + Body.innerHTML = ""; + Source.innerHTML = raw; + Body.appendChild(content); + loadingMode.off(); +}; + +const loadFileList = () => { + loadingMode.on(); + + fetchText("./files.txt").then(fillMenuFromFileList); +}; + +/** + * Called when the file list is obtained (presumably through loading) + * parses the file list, then fills the side navigation + * If there's no page loaded, it also loads the first page in the list + * (the list gets sorted by date, but the first line is the one that gets used) + * @param {string} lines + */ +export const fillMenuFromFileList = (lines) => { + const links = parseFileList(lines).sort( + ({ date_unix: a }, { date_unix: b }) => a - b + ); + if (links.length < 1) { + return; + } + Menu.innerHTML = links + .map(({ href, title }) => `<a data-link href="#${href}">${title}</a>`) + .join(`\n`); + if (!getCurrentHashUrl().path) { + // @ts-ignore + const href = links.find(({ index }) => index === 0).href; + window.location.hash = `#${href}`; + } else { + onHashChange(); + } +}; + +/***************************************************************** + * + * Bootstrapping + * + * this is where things actually happen + * + ****************************************************************/ + +/** + * Loads the article list, parses it, creates the menu items + */ +export const bootstrap = () => { + Burger.addEventListener("click", menuOpenMode.toggle); + + window.addEventListener("hashchange", onHashChange); + + loadFileList(); + + onDocumentKeyUp("?", sourceEnableMode.toggle); +}; + +export default bootstrap; diff --git a/modules/wait.mjs b/modules/wait.mjs new file mode 100644 index 0000000..c43602b --- /dev/null +++ b/modules/wait.mjs @@ -0,0 +1,18 @@ +//@ts-check + +/** + * Waits the specified amount of time before returning the value + * @param {number} durationMs Duration, in milliseconds. Defaults to 1 second + * @returns + */ +export const wait = + (durationMs = 1000) => + /** + * @template T + * @param {T} [value] + * @returns {Promise<T>} + */ + (value) => + new Promise((ok) => setTimeout(ok, durationMs, value)); + +export default wait; diff --git a/modules/waitIfLocalHost.mjs b/modules/waitIfLocalHost.mjs new file mode 100644 index 0000000..831af30 --- /dev/null +++ b/modules/waitIfLocalHost.mjs @@ -0,0 +1,31 @@ +//@ts-check + +import wait from "./wait.mjs"; + +/** + * @template T + * @param {T} [value] + * @returns {Promise<T>} + */ +// @ts-ignore +const identity = (value) => Promise.resolve(value); + +/** + * Waits the specified amount of time before returning the value + * @param {number} _durationMs Duration, in milliseconds. Defaults to 1 second + * @returns + */ +const fakeWait = (_durationMs = 1000) => identity; + +import isLocalHost from "./isLocalHost.mjs"; + +/** + * useful to check for transitions while developing styles, if the loading screen + * disappears too fast for example. + * @template T + * @param {number} durationMs Duration, in milliseconds. Defaults to 1 second + * @returns {(value?: T | undefined) => Promise<T>} + */ +export const waitIfLocalHost = isLocalHost ? wait : fakeWait; + +export default waitIfLocalHost;