refactor
This commit is contained in:
21
modules/utils/changeTitle.mjs
Normal file
21
modules/utils/changeTitle.mjs
Normal file
@ -0,0 +1,21 @@
|
||||
//@ts-check
|
||||
|
||||
/**
|
||||
* Changes the document's title. Instead of passing the main title, you can also
|
||||
* change the functions `title` member:
|
||||
* ```js
|
||||
* changeTitle.title = "My Site"
|
||||
* changeTitle("Home") // produces "Home | My Site"
|
||||
* ```
|
||||
* if not `title` is passed, the document title (as found when running the function
|
||||
* the first time) will be used.
|
||||
* @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;
|
14
modules/utils/documentMode.mjs
Normal file
14
modules/utils/documentMode.mjs
Normal file
@ -0,0 +1,14 @@
|
||||
//@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}`),
|
||||
has: () => document.body.classList.contains(`is-${name}`),
|
||||
});
|
||||
|
||||
export default documentMode;
|
35
modules/utils/documentState.mjs
Normal file
35
modules/utils/documentState.mjs
Normal file
@ -0,0 +1,35 @@
|
||||
//@ts-check
|
||||
|
||||
/**
|
||||
* Creates a document state object that can toggle between exclusive states.
|
||||
* All passed states' css classnames will be prepended with `mode-`.
|
||||
* @param {string[]} states
|
||||
*/
|
||||
export const documentState = (states) => {
|
||||
const all = states.map((state) => `mode-${state}`);
|
||||
|
||||
/**
|
||||
* @param {any} state
|
||||
* @returns {state is number}
|
||||
*/
|
||||
const isValidIndex = (state) =>
|
||||
typeof state === "number" && state >= 0 && state < all.length;
|
||||
|
||||
/**
|
||||
* @param {number} state
|
||||
*/
|
||||
const is = (state) =>
|
||||
isValidIndex(state) && document.body.classList.contains(all[state]);
|
||||
|
||||
/**
|
||||
* @param {number|undefined|null|false} state
|
||||
*/
|
||||
const set = (state = false) => {
|
||||
document.body.classList.remove(...all);
|
||||
isValidIndex(state) && document.body.classList.add(all[state]);
|
||||
};
|
||||
|
||||
return { set, is };
|
||||
};
|
||||
|
||||
export default documentState;
|
23
modules/utils/fetchMarkdown.mjs
Normal file
23
modules/utils/fetchMarkdown.mjs
Normal file
@ -0,0 +1,23 @@
|
||||
//@ts-check
|
||||
|
||||
import { fetchText } from "./fetchText.mjs";
|
||||
import { waitIfLocalHost } from "./waitIfLocalHost.mjs";
|
||||
import { generateDomFromString } from "./generateDomFromString.mjs";
|
||||
import {getFirstTitleContent} from "./getFirstTitleContent.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 = generateDomFromString(micromark(raw));
|
||||
const title = getFirstTitleContent(content) || path.replace(/\.\w{2, 4}$/, "");
|
||||
return { title, raw, content };
|
||||
});
|
||||
|
||||
export default fetchMarkdown;
|
16
modules/utils/fetchText.mjs
Normal file
16
modules/utils/fetchText.mjs
Normal file
@ -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<string>} the loaded file
|
||||
*/
|
||||
export const fetchText = (path) =>
|
||||
fetch(isLocalHost ? `./${path}?rand=${Math.random()}` : `./${path}`).then(
|
||||
(response) => response.text()
|
||||
);
|
||||
|
||||
export default fetchText;
|
15
modules/utils/generateDomFromString.mjs
Normal file
15
modules/utils/generateDomFromString.mjs
Normal file
@ -0,0 +1,15 @@
|
||||
//@ts-check
|
||||
|
||||
/**
|
||||
* Generates valid dom elements from a string
|
||||
* @param {string} htmlString
|
||||
*/
|
||||
export const generateDomFromString = (htmlString) =>{
|
||||
const children = new DOMParser().parseFromString(`<div>${htmlString}</div>`, "text/html")
|
||||
.children
|
||||
const fragment = document.createDocumentFragment()
|
||||
fragment.append(...children)
|
||||
return fragment
|
||||
;}
|
||||
|
||||
export default generateDomFromString;
|
23
modules/utils/getCurrentHashUrl.mjs
Normal file
23
modules/utils/getCurrentHashUrl.mjs
Normal file
@ -0,0 +1,23 @@
|
||||
//@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 const hasCurrentHashUrl = () => getCurrentHashUrl().path !== "";
|
||||
|
||||
export const hasNoHashUrl = () => getCurrentHashUrl().path === "";
|
||||
|
||||
getCurrentHashUrl.hasCurrentHashUrl = hasCurrentHashUrl;
|
||||
getCurrentHashUrl.hasNoHashUrl = hasNoHashUrl;
|
||||
|
||||
export default getCurrentHashUrl;
|
19
modules/utils/getElementById.mjs
Normal file
19
modules/utils/getElementById.mjs
Normal file
@ -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;
|
12
modules/utils/getFirstTitleContent.mjs
Normal file
12
modules/utils/getFirstTitleContent.mjs
Normal file
@ -0,0 +1,12 @@
|
||||
/**
|
||||
* Returns the first title content in the document, if there is one.
|
||||
* @param {Node} content
|
||||
* @returns
|
||||
*/
|
||||
export const getFirstTitleContent = (content = document) => {
|
||||
/** @type {HTMLHeadElement} */
|
||||
const firstTitleElement = content.querySelector("h1");
|
||||
return firstTitleElement ? firstTitleElement.textContent || "" : "";
|
||||
};
|
||||
|
||||
export default getFirstTitleContent;
|
17
modules/utils/html.mjs
Normal file
17
modules/utils/html.mjs
Normal file
@ -0,0 +1,17 @@
|
||||
/**
|
||||
* Does exactly the same as simply using ``, but allows to
|
||||
* neatly highlight the html string in editors that support it.
|
||||
* Packages the resulting string as a template
|
||||
* @param {TemplateStringsArray} strings
|
||||
* @param {...any} expressions
|
||||
*/
|
||||
export function html(strings, ...expressions){
|
||||
const parsed = strings.reduce((previous, current, i) => {
|
||||
return previous + current + (expressions[i] ? expressions[i] : '')
|
||||
}, '')
|
||||
const template = document.createElement("template")
|
||||
template.innerHTML = parsed
|
||||
return template
|
||||
}
|
||||
|
||||
export default html
|
17
modules/utils/identity.mjs
Normal file
17
modules/utils/identity.mjs
Normal file
@ -0,0 +1,17 @@
|
||||
/**
|
||||
* @template T
|
||||
* @param {T} [value]
|
||||
* @returns {Promise<T>}
|
||||
*/
|
||||
export const awaitedIdentity = (value) => Promise.resolve(value);
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @param {T} [value]
|
||||
* @returns {T}
|
||||
*/
|
||||
export const identity = (value) => value
|
||||
|
||||
identity.awaited = awaitedIdentity;
|
||||
|
||||
export default identity;
|
69
modules/utils/index.mjs
Normal file
69
modules/utils/index.mjs
Normal file
@ -0,0 +1,69 @@
|
||||
//@ts-check
|
||||
|
||||
import { changeTitle } from "./changeTitle.mjs";
|
||||
import { documentMode } from "./documentMode.mjs";
|
||||
import { documentState } from "./documentState.mjs";
|
||||
import { fetchMarkdown } from "./fetchMarkdown.mjs";
|
||||
import { fetchText } from "./fetchText.mjs";
|
||||
import { generateDomFromString } from "./generateDomFromString.mjs";
|
||||
import {
|
||||
getCurrentHashUrl,
|
||||
hasCurrentHashUrl,
|
||||
hasNoHashUrl,
|
||||
} from "./getCurrentHashUrl.mjs";
|
||||
import { getElementById } from "./getElementById.mjs";
|
||||
import { getFirstTitleContent } from "./getFirstTitleContent.mjs";
|
||||
import { identity, awaitedIdentity } from "./identity.mjs";
|
||||
import { html } from "./html.mjs";
|
||||
import { isExternalUrl } from "./isExternalUrl.mjs";
|
||||
import { isLocalHost } from "./isLocalHost.mjs";
|
||||
import { isNotNull } from "./isNotNull.mjs";
|
||||
import { not } from "./not.mjs";
|
||||
import {
|
||||
onDocumentKeyUp,
|
||||
onDocumentKeyDown,
|
||||
onDocumentKey,
|
||||
} from "./onDocumentKey.mjs";
|
||||
import { print, makeTemplate } from "./print.mjs";
|
||||
import {
|
||||
querySelectorDoc,
|
||||
querySelectorParent,
|
||||
querySelectorAll,
|
||||
} from "./querySelectorAll.mjs";
|
||||
import { rewriteLocalUrls } from "./rewriteLocalUrls.mjs";
|
||||
import { today } from "./today.mjs";
|
||||
import { wait } from "./wait.mjs";
|
||||
import { waitIfLocalHost } from "./waitIfLocalHost.mjs";
|
||||
|
||||
export {
|
||||
changeTitle,
|
||||
documentMode,
|
||||
documentState,
|
||||
fetchMarkdown,
|
||||
fetchText,
|
||||
generateDomFromString,
|
||||
getCurrentHashUrl,
|
||||
hasCurrentHashUrl,
|
||||
hasNoHashUrl,
|
||||
getElementById,
|
||||
getFirstTitleContent,
|
||||
html,
|
||||
identity,
|
||||
awaitedIdentity,
|
||||
isExternalUrl,
|
||||
isLocalHost,
|
||||
isNotNull,
|
||||
not,
|
||||
onDocumentKeyUp,
|
||||
onDocumentKeyDown,
|
||||
onDocumentKey,
|
||||
print,
|
||||
makeTemplate,
|
||||
querySelectorDoc,
|
||||
querySelectorParent,
|
||||
querySelectorAll,
|
||||
rewriteLocalUrls,
|
||||
today,
|
||||
wait,
|
||||
waitIfLocalHost,
|
||||
};
|
10
modules/utils/isExternalUrl.mjs
Normal file
10
modules/utils/isExternalUrl.mjs
Normal file
@ -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;
|
8
modules/utils/isLocalHost.mjs
Normal file
8
modules/utils/isLocalHost.mjs
Normal file
@ -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;
|
10
modules/utils/isNotNull.mjs
Normal file
10
modules/utils/isNotNull.mjs
Normal file
@ -0,0 +1,10 @@
|
||||
//@ts-check
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @param {T} value
|
||||
* @returns {value is NonNullable<T>}
|
||||
*/
|
||||
export const isNotNull = (value) => value !== null;
|
||||
|
||||
export default isNotNull;
|
9
modules/utils/not.mjs
Normal file
9
modules/utils/not.mjs
Normal file
@ -0,0 +1,9 @@
|
||||
/**
|
||||
* Inverter.
|
||||
* Easier to read than "!"
|
||||
* @param {any} a
|
||||
* @returns
|
||||
*/
|
||||
export const not = a => !a
|
||||
|
||||
export default not
|
43
modules/utils/onDocumentKey.mjs
Normal file
43
modules/utils/onDocumentKey.mjs
Normal file
@ -0,0 +1,43 @@
|
||||
//@ts-check
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} keyToListen
|
||||
* @param {()=>void} callback
|
||||
*/
|
||||
export const onDocumentKeyUp = (keyToListen, callback) => {
|
||||
document.addEventListener(
|
||||
"keyup",
|
||||
({ key }) => key === keyToListen && callback()
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} keyToListen
|
||||
* @param {()=>void} callback
|
||||
*/
|
||||
export const onDocumentKeyDown = (keyToListen, callback) => {
|
||||
document.addEventListener(
|
||||
"keydown",
|
||||
({ key }) => key === keyToListen && callback()
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} keyToListen
|
||||
* @param {(down:boolean)=>void} callback
|
||||
*/
|
||||
export const onDocumentKey = (keyToListen, callback) => {
|
||||
document.addEventListener(
|
||||
"keyup",
|
||||
({ key }) => key === keyToListen && callback(false)
|
||||
);
|
||||
document.addEventListener(
|
||||
"keydown",
|
||||
({ key }) => key === keyToListen && callback(true)
|
||||
);
|
||||
};
|
||||
|
||||
export default onDocumentKey;
|
19
modules/utils/print.mjs
Normal file
19
modules/utils/print.mjs
Normal file
@ -0,0 +1,19 @@
|
||||
/**
|
||||
* Mini-mustache templating system. Simply replaces all occurrences of {{key}} with the value of the key.
|
||||
* @param {string} str
|
||||
* @param {Record<string, any>} replacements
|
||||
*/
|
||||
export const print = (str, replacements) =>
|
||||
str.replace(/{{(.*?)}}/g, (_match, key) => replacements[key] || '')
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} str
|
||||
* @returns {(replacements: Record<string, any>) => string}
|
||||
*/
|
||||
export const makeTemplate = (str) => print.bind(null, str)
|
||||
|
||||
print.makeTemplate = makeTemplate
|
||||
|
||||
export default print
|
40
modules/utils/querySelectorAll.mjs
Normal file
40
modules/utils/querySelectorAll.mjs
Normal file
@ -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;
|
20
modules/utils/rewriteLocalUrls.mjs
Normal file
20
modules/utils/rewriteLocalUrls.mjs
Normal file
@ -0,0 +1,20 @@
|
||||
//@ts-check
|
||||
|
||||
import isExternalUrl from "./isExternalUrl.mjs";
|
||||
import { querySelectorParent } from "./querySelectorAll.mjs";
|
||||
|
||||
/**
|
||||
* Makes sure urls to local pages are prepended with #/
|
||||
* @param {ParentNode} 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) && !href.startsWith("#")) {
|
||||
a.setAttribute("href", "#/" + href.replace(/^\.?\//, ""));
|
||||
}
|
||||
});
|
||||
return container;
|
||||
};
|
||||
|
||||
export default rewriteLocalUrls;
|
3
modules/utils/today.mjs
Normal file
3
modules/utils/today.mjs
Normal file
@ -0,0 +1,3 @@
|
||||
export const today = new Date().toISOString().split("T")[0];
|
||||
|
||||
export default today
|
18
modules/utils/wait.mjs
Normal file
18
modules/utils/wait.mjs
Normal file
@ -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;
|
24
modules/utils/waitIfLocalHost.mjs
Normal file
24
modules/utils/waitIfLocalHost.mjs
Normal file
@ -0,0 +1,24 @@
|
||||
//@ts-check
|
||||
import wait from "./wait.mjs";
|
||||
import { awaitedIdentity } from "./identity.mjs";
|
||||
import isLocalHost from "./isLocalHost.mjs";
|
||||
|
||||
|
||||
/**
|
||||
* 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) => awaitedIdentity;
|
||||
|
||||
|
||||
/**
|
||||
* 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;
|
Reference in New Issue
Block a user