440 lines
12 KiB
HTML
440 lines
12 KiB
HTML
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<title>Tickle</title>
|
|
<meta charset="UTF-8" />
|
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
<link
|
|
href="https://fonts.googleapis.com/css2?family=Lobster&family=Nunito+Sans:ital,wght@0,400;0,700;1,400&display=swap"
|
|
rel="stylesheet"
|
|
/>
|
|
<style>
|
|
:root {
|
|
--accent: hotpink;
|
|
--background: #fdfdfd;
|
|
font-family: "Nunito Sans", sans-serif;
|
|
}
|
|
body,
|
|
html {
|
|
width: 100%;
|
|
height: 100%;
|
|
margin: 0;
|
|
padding: 0;
|
|
}
|
|
body {
|
|
padding: 3em;
|
|
max-width: 52em;
|
|
margin: 0 auto;
|
|
}
|
|
pre {
|
|
margin: 0;
|
|
padding: 3em;
|
|
max-width: inherit;
|
|
max-height: 100vh;
|
|
overflow: scroll;
|
|
background: rgb(192, 192, 192);
|
|
position: absolute;
|
|
top: 0;
|
|
left: 50%;
|
|
transition: all 1s cubic-bezier(0.68, -0.55, 0.265, 1.55);
|
|
transform: translate(-50%, -100%);
|
|
}
|
|
.is-source-enabled pre {
|
|
transform: translate(-50%, 0);
|
|
}
|
|
button {
|
|
display: inline-block;
|
|
border: none;
|
|
margin: 0;
|
|
text-decoration: none;
|
|
font-family: inherit;
|
|
font-size: 1rem;
|
|
display: flex;
|
|
justify-content: center;
|
|
align-content: center;
|
|
text-align: center;
|
|
vertical-align: middle;
|
|
background: var(--accent);
|
|
color: var(--background);
|
|
cursor: pointer;
|
|
padding: 0.5em 1em;
|
|
border-radius: 0 0 0.2em 0;
|
|
box-shadow: rgba(149, 157, 165, 0.2) 0px 8px 24px;
|
|
-webkit-appearance: none;
|
|
-moz-appearance: none;
|
|
}
|
|
h1,
|
|
h2,
|
|
h3,
|
|
h4 {
|
|
color: var(--accent);
|
|
font-family: "Lobster", serif;
|
|
}
|
|
a {
|
|
color: var(--accent);
|
|
position: relative;
|
|
text-decoration: none;
|
|
padding: 0.1em;
|
|
}
|
|
a::after {
|
|
content: "";
|
|
position: absolute;
|
|
background-color: var(--accent);
|
|
position: absolute;
|
|
left: 0;
|
|
bottom: 3px;
|
|
width: 100%;
|
|
height: 1px;
|
|
z-index: -1;
|
|
transition: all 0.1s ease-in;
|
|
}
|
|
a:hover {
|
|
color: var(--background);
|
|
transition: all 0.3s ease-in-out;
|
|
}
|
|
a:hover::after {
|
|
bottom: 0;
|
|
height: 100%;
|
|
transition: all 0.3s ease-in-out;
|
|
}
|
|
.menu,
|
|
.burger {
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
}
|
|
.menu {
|
|
padding: 1em;
|
|
transform: translateX(-100%);
|
|
display: flex;
|
|
flex-direction: column;
|
|
top: 0;
|
|
bottom: 0;
|
|
transition: all 0.3s ease-out;
|
|
gap: 1em;
|
|
}
|
|
.menu a {
|
|
text-decoration: none;
|
|
background: var(--background);
|
|
color: var(--accent);
|
|
box-shadow: rgba(100, 100, 111, 0.2) 0px 7px 29px 0px;
|
|
padding: 0.2em 0.4em;
|
|
}
|
|
.menu a:hover {
|
|
background: var(--accent);
|
|
color: var(--background);
|
|
}
|
|
.menu-is-open .menu {
|
|
transform: translateX(0);
|
|
transition-duration: 0.1s;
|
|
}
|
|
.menu-is-open .burger {
|
|
color: transparent;
|
|
}
|
|
|
|
.menu-is-open .burger,
|
|
#Loading {
|
|
top: 0;
|
|
right: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
border-radius: 0;
|
|
background: rgba(17, 17, 17, 0.2);
|
|
cursor: default;
|
|
}
|
|
#Loading {
|
|
position: fixed;
|
|
text-align: center;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 8rem;
|
|
color: var(--accent);
|
|
opacity: 0;
|
|
transition: opacity 0.3s ease-in, height 0s linear 0.3s;
|
|
height: 0;
|
|
display: flex;
|
|
overflow: hidden;
|
|
}
|
|
#Loading > * {
|
|
animation: load 1.2s infinite cubic-bezier(0.215, 0.61, 0.355, 1);
|
|
}
|
|
.is-loading #Loading {
|
|
opacity: 1;
|
|
height: 100%;
|
|
transition: opacity 1s ease-in, height 0 linear;
|
|
}
|
|
@keyframes load {
|
|
0% {
|
|
transform: scale(0.95);
|
|
}
|
|
5% {
|
|
transform: scale(1.1);
|
|
}
|
|
39% {
|
|
transform: scale(0.85);
|
|
}
|
|
45% {
|
|
transform: scale(1);
|
|
}
|
|
60% {
|
|
transform: scale(0.95);
|
|
}
|
|
100% {
|
|
transform: scale(0.9);
|
|
}
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<nav class="left-nav">
|
|
<button id="Burger" class="burger">≡</button>
|
|
<div id="Menu" class="menu"></div>
|
|
</nav>
|
|
<main id="Body"></main>
|
|
<pre id="Source"></pre>
|
|
<div id="Loading">
|
|
<p>❤</p>
|
|
</div>
|
|
<script type="module">
|
|
//@ts-check
|
|
|
|
/*****************************************************************
|
|
*
|
|
* "THE FRAMEWORK"
|
|
* a small set of utilities that help
|
|
*
|
|
****************************************************************/
|
|
/**
|
|
* markdown parser. Remove if you don't use markdown
|
|
*/
|
|
// @ts-ignore
|
|
import { micromark } from "https://esm.sh/micromark@3?bundle";
|
|
|
|
const is_debug_mode = /^localhost|127.0.0.1/.test(
|
|
window.location.hostname
|
|
);
|
|
|
|
/**
|
|
* A small utility to query elements and get back an array
|
|
*/
|
|
const $ = (parent, selector) => [
|
|
...(!selector
|
|
? document.querySelectorAll(parent)
|
|
: parent.querySelectorAll(selector)),
|
|
];
|
|
|
|
/**
|
|
* Assumes a provided url is external if it begins by a known protocol
|
|
* @param {string} url
|
|
*/
|
|
const isExternal = (url) =>
|
|
url && /^(https?|mailto|tel|ftp|ipfs|dat):/.test(url);
|
|
|
|
/**
|
|
* Makes sure urls to local pages get passed through the routing system
|
|
* @param {HTMLElement} container the element containing links to find
|
|
*/
|
|
const rewriteLocalUrls = (container) => {
|
|
$(container, "a").forEach((a) => {
|
|
const href = a.getAttribute("href");
|
|
if (href && !isExternal(href)) {
|
|
a.setAttribute("href", "#/" + href.replace(/^\.?\//, ""));
|
|
}
|
|
});
|
|
return container;
|
|
};
|
|
|
|
/**
|
|
* 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
|
|
*/
|
|
const getCurrentHashUrl = () => {
|
|
const [path, searchStr] = (
|
|
window.location.hash[1] === "/" ? window.location.hash.slice(2) : ""
|
|
).split("?");
|
|
const params = new URLSearchParams(searchStr);
|
|
return { path, params };
|
|
};
|
|
|
|
/**
|
|
* useful to check for transitions while developing styles, if the loading screen disappears too fast
|
|
* uses micromark. You can plug a different parser if you prefer
|
|
*/
|
|
const wait = is_debug_mode
|
|
? (val) => new Promise((ok) => setTimeout(ok, 1000, val))
|
|
: (val) => val;
|
|
|
|
/**
|
|
* @param {string} htmlString
|
|
*/
|
|
const generateDOM = (htmlString) =>
|
|
/** @type {HTMLElement} */ (
|
|
new DOMParser().parseFromString(
|
|
`<div>${htmlString}</div>`,
|
|
"text/html"
|
|
).firstChild
|
|
);
|
|
|
|
/**
|
|
* Loads and parses a markdown document. Makes use of micromark
|
|
* @param {string} path the path to load
|
|
*/
|
|
const loadMarkdown = (path) =>
|
|
fetch(is_debug_mode ? `./${path}?rand=${Math.random()}` : `./${path}`)
|
|
.then((response) => response.text())
|
|
.then(wait)
|
|
.then((raw) => {
|
|
const content = rewriteLocalUrls(generateDOM(micromark(raw)));
|
|
const firstTitleElement = content.querySelector("h1");
|
|
const title = firstTitleElement
|
|
? firstTitleElement.textContent
|
|
: path.replace(/\.\w{2, 4}$/, "");
|
|
return { title, raw, content };
|
|
});
|
|
|
|
/**
|
|
* parses a filelist string. That's a string that looks like
|
|
* ```
|
|
* name dd-mm-yyyy link name
|
|
* name dd-mm-yyyy
|
|
* name linkname
|
|
* name
|
|
* ```
|
|
* @param {string} lines
|
|
*/
|
|
const parseFileList = (lines) =>
|
|
lines
|
|
.trim()
|
|
.split(`\n`)
|
|
.map((line, index) => {
|
|
const today = new Date().toISOString().split("T")[0];
|
|
const result = line.match(
|
|
/(?<name>.+)\.(?<ext>\w{2,3})(?:\s+(?<date>[\d-]+)?(?<title>.+))?/
|
|
);
|
|
if (!result) {
|
|
console.error(`could not parse line ${index}: [${line}]`);
|
|
return null;
|
|
}
|
|
const {
|
|
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(Boolean)
|
|
.sort(({ date_unix: a }, { date_unix: b }) => a - b);
|
|
|
|
/*****************************************************************
|
|
*
|
|
* Creating references to the important stuff
|
|
*
|
|
****************************************************************/
|
|
|
|
/**
|
|
* The elements we will need
|
|
*/
|
|
const [Menu, Body, Loading, Source, Burger] = [
|
|
"Menu",
|
|
"Body",
|
|
"Loading",
|
|
"Source",
|
|
"Burger",
|
|
].map((id) => document.getElementById(id));
|
|
|
|
/**
|
|
* cache the original title to append it to the page title
|
|
*/
|
|
const mainTitle = document.title;
|
|
|
|
const showLoadingOverlay = () =>
|
|
document.body.classList.add("is-loading");
|
|
const hideLoadingOverlay = () =>
|
|
document.body.classList.remove("is-loading");
|
|
|
|
/*****************************************************************
|
|
*
|
|
* Router
|
|
*
|
|
* Things related to main navigation and to loading pages
|
|
*
|
|
****************************************************************/
|
|
|
|
/**
|
|
* Listens to hashchange event, and attempts to read the url.
|
|
* If the url is set,
|
|
*/
|
|
const onHashChange = async (evt) => {
|
|
const { path, params } = getCurrentHashUrl();
|
|
if (!path) {
|
|
return false;
|
|
}
|
|
showLoadingOverlay();
|
|
const { title, raw, content } = await loadMarkdown(path);
|
|
document.title = `${title} | ${mainTitle}`;
|
|
Body.innerHTML = "";
|
|
Source.innerHTML = raw;
|
|
Body.appendChild(content);
|
|
hideLoadingOverlay();
|
|
};
|
|
|
|
/**
|
|
* 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
|
|
*/
|
|
const fillMenuFromFileList = (lines) => {
|
|
const links = parseFileList(lines);
|
|
Menu.innerHTML = links
|
|
.map(({ href, title }) => `<a data-link href="#${href}">${title}</a>`)
|
|
.join(`\n`);
|
|
if (!getCurrentHashUrl().path) {
|
|
const href = links.find(({ index }) => index === 0).href;
|
|
window.location.hash = `#${href}`;
|
|
} else {
|
|
onHashChange();
|
|
}
|
|
};
|
|
|
|
/*****************************************************************
|
|
*
|
|
* Bootstrapping
|
|
*
|
|
* this is where things actually happen
|
|
*
|
|
****************************************************************/
|
|
|
|
const loadFileList = () => {
|
|
showLoadingOverlay();
|
|
|
|
fetch("./files.txt")
|
|
.then((response) => response.text())
|
|
.then(fillMenuFromFileList);
|
|
};
|
|
|
|
/**
|
|
* Loads the article list, parses it, creates the menu items
|
|
*/
|
|
(function bootstrap() {
|
|
Burger.addEventListener("click", () =>
|
|
document.body.classList.toggle("menu-is-open")
|
|
);
|
|
|
|
window.addEventListener("hashchange", onHashChange);
|
|
|
|
loadFileList();
|
|
|
|
document.addEventListener(
|
|
"keyup",
|
|
({ key }) =>
|
|
key === "?" && document.body.classList.toggle("is-source-enabled")
|
|
);
|
|
})();
|
|
</script>
|
|
</body>
|
|
</html>
|