const swap_event = new Event("swap"); let caches = {}; async function cached_fetch(url) { let cached = caches[url]; console.log("cache: " + cached); const res = cached === undefined ? await fetch(url) : await fetch(url, { headers: { "If-Modified-Since": cached.last_modified } }); if (res.status === 304 && cached !== undefined) { return cached.content; } if (res.status !== 200) return; if (!res.headers.get("content-type").startsWith("text/html")) return; const text = await res.text(); if (res.headers.get("last-modified")) { caches[url] = { content: text, last_modified: res.headers.get("last-modified") } } return text; } async function swap(url, stateful) { if (typeof url !== 'string') return; const segments = window.location.href.split("/"); if (url.startsWith(window.location.origin) && segments[segments.length - 1].includes("#")) { window.location.href = url; return; } if (stateful && window.location.href.endsWith(url)) return; const text = await cached_fetch(url); const content = new DOMParser().parseFromString(text, "text/html"); const stylesheets = [...content.querySelectorAll("link[rel='stylesheet']")]; // swap title document.title = content.title; // swap body html document.body.innerHTML = content.body.innerHTML; // swap stylesheets const old_sheets = document.head.querySelectorAll("link[rel='stylesheet']"); stylesheets.forEach(stylesheet => { let exists = false; old_sheets.forEach(old_sheet => { if (old_sheet.href === stylesheet.href) exists = true; }); if (!exists) document.head.appendChild(stylesheet); }); old_sheets.forEach(old_sheet => { let exists = false; stylesheets.forEach(stylesheet => { if (stylesheet.href === old_sheet.href) exists = true; }); if (!exists) old_sheet.remove(); }); // push history if (stateful) history.pushState(url, "", url); bind(document.body); } function bind(content) { if (typeof content !== 'object' || content.nodeType !== Node.ELEMENT_NODE) return; content.querySelectorAll("[swap-url]").forEach(element => { const href = element.attributes.getNamedItem('swap-url').value; element.addEventListener("click", event => { event.preventDefault(); swap(href, true); }); [...element.querySelectorAll("a[href], [swap-url]")].forEach(element => { if (element.href) { if (!element.href.endsWith(href)) return; element.attributes.removeNamedItem("href"); } const swap_url = element.attributes.getNamedItem("swap-url"); if (swap_url) { if (!swap_url.endsWith(href)) return; element.attributes.removeNamedItem("swap-url"); } }); }); content.querySelectorAll("a[href]:not([swap-url])").forEach(element => { if (element.href.includes("#")) return; if (!element.href.startsWith(window.location.origin)) return; const href = element.href.substring(window.location.origin.length); if (href.includes(".")) return; element.addEventListener("click", event => { event.preventDefault(); swap(element.href, true); }); }); document.dispatchEvent(swap_event); } window.addEventListener("popstate", event => { swap(event.state, false); }); bind(document.body);