testing moving the functionality into small modules #5
							
								
								
									
										245
									
								
								index.html
									
									
									
									
									
								
							
							
						
						
									
										245
									
								
								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 @@
 | 
			
		||||
      <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")
 | 
			
		||||
        );
 | 
			
		||||
      })();
 | 
			
		||||
      import blog from "./modules/templateBlog.mjs";
 | 
			
		||||
      blog();
 | 
			
		||||
    </script>
 | 
			
		||||
  </body>
 | 
			
		||||
</html>
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										12
									
								
								modules/changeTitle.mjs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								modules/changeTitle.mjs
									
									
									
									
									
										Normal file
									
								
							@@ -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;
 | 
			
		||||
							
								
								
									
										13
									
								
								modules/documentMode.mjs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								modules/documentMode.mjs
									
									
									
									
									
										Normal file
									
								
							@@ -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;
 | 
			
		||||
							
								
								
									
										24
									
								
								modules/fetchMarkdown.mjs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								modules/fetchMarkdown.mjs
									
									
									
									
									
										Normal file
									
								
							@@ -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 };
 | 
			
		||||
    });
 | 
			
		||||
							
								
								
									
										16
									
								
								modules/fetchText.mjs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								modules/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;
 | 
			
		||||
							
								
								
									
										13
									
								
								modules/generateDomFromString.mjs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								modules/generateDomFromString.mjs
									
									
									
									
									
										Normal file
									
								
							@@ -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(`<div>${htmlString}</div>`, "text/html")
 | 
			
		||||
      .firstChild
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
export default generateDomFromString;
 | 
			
		||||
							
								
								
									
										16
									
								
								modules/getCurrentHashUrl.mjs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								modules/getCurrentHashUrl.mjs
									
									
									
									
									
										Normal file
									
								
							@@ -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;
 | 
			
		||||
							
								
								
									
										19
									
								
								modules/getElementById.mjs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								modules/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;
 | 
			
		||||
							
								
								
									
										15
									
								
								modules/index.mjs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								modules/index.mjs
									
									
									
									
									
										Normal file
									
								
							@@ -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 };
 | 
			
		||||
							
								
								
									
										10
									
								
								modules/isExternalUrl.mjs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								modules/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/isLocalHost.mjs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								modules/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/isNotNull.mjs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								modules/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;
 | 
			
		||||
							
								
								
									
										11
									
								
								modules/onDocumentKey.mjs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								modules/onDocumentKey.mjs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,11 @@
 | 
			
		||||
/**
 | 
			
		||||
 *
 | 
			
		||||
 * @param {string} keyToListen
 | 
			
		||||
 * @param {()=>void} callback
 | 
			
		||||
 */
 | 
			
		||||
export const onDocumentKeyUp = (keyToListen, callback) => {
 | 
			
		||||
  document.addEventListener(
 | 
			
		||||
    "keyup",
 | 
			
		||||
    ({ key }) => key === keyToListen && callback()
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										40
									
								
								modules/parseFileList.mjs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								modules/parseFileList.mjs
									
									
									
									
									
										Normal file
									
								
							@@ -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(
 | 
			
		||||
        /(?<name>.+)\.(?<ext>\w{2,3})(?:\s+(?<date>[\d-]+)?(?<title>.+))?/
 | 
			
		||||
      );
 | 
			
		||||
      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;
 | 
			
		||||
							
								
								
									
										40
									
								
								modules/querySelectorAll.mjs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								modules/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/rewriteLocalUrls.mjs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								modules/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 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;
 | 
			
		||||
							
								
								
									
										107
									
								
								modules/templateBlog.mjs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										107
									
								
								modules/templateBlog.mjs
									
									
									
									
									
										Normal file
									
								
							@@ -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;
 | 
			
		||||
							
								
								
									
										18
									
								
								modules/wait.mjs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								modules/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;
 | 
			
		||||
							
								
								
									
										31
									
								
								modules/waitIfLocalHost.mjs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								modules/waitIfLocalHost.mjs
									
									
									
									
									
										Normal file
									
								
							@@ -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;
 | 
			
		||||
		Reference in New Issue
	
	Block a user