add tickle file
This commit is contained in:
		
							
								
								
									
										439
									
								
								server_files/index.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										439
									
								
								server_files/index.html
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,439 @@
 | 
			
		||||
<!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>
 | 
			
		||||
		Reference in New Issue
	
	Block a user