forked from yagich/tickle-godot-frontend
		
	
		
			
				
	
	
		
			406 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			HTML
		
	
	
	
	
	
			
		
		
	
	
			406 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			HTML
		
	
	
	
	
	
<!DOCTYPE html>
 | 
						|
<html>
 | 
						|
  <head>
 | 
						|
    <title>Parcel Sandbox</title>
 | 
						|
    <meta charset="UTF-8" />
 | 
						|
    <style></style>
 | 
						|
  </head>
 | 
						|
  <body>
 | 
						|
    <template id="my-link">
 | 
						|
      <style>
 | 
						|
        :host {
 | 
						|
          --background-regular: hsla(196, 61%, 58%, 0.75);
 | 
						|
          --background-active: red;
 | 
						|
          text-decoration: none;
 | 
						|
          color: #18272f;
 | 
						|
          font-weight: 700;
 | 
						|
          cursor: pointer;
 | 
						|
          position: relative;
 | 
						|
          display: flex;
 | 
						|
        }
 | 
						|
 | 
						|
        :host span {
 | 
						|
          width: 100%;
 | 
						|
          height: 100%;
 | 
						|
        }
 | 
						|
 | 
						|
        :host::before {
 | 
						|
          content: "";
 | 
						|
          background-color: var(--background-regular);
 | 
						|
          position: absolute;
 | 
						|
          left: 0;
 | 
						|
          bottom: 3px;
 | 
						|
          width: 100%;
 | 
						|
          height: 8px;
 | 
						|
          z-index: -1;
 | 
						|
          transition: all 0.3s ease-in-out;
 | 
						|
        }
 | 
						|
 | 
						|
        :host(:hover)::before {
 | 
						|
          bottom: 0;
 | 
						|
          height: 100%;
 | 
						|
        }
 | 
						|
 | 
						|
        :host([active])::before {
 | 
						|
          background-color: var(--background-active);
 | 
						|
        }
 | 
						|
      </style>
 | 
						|
 | 
						|
      <span><slot /></span>
 | 
						|
    </template>
 | 
						|
    <template id="my-menu">
 | 
						|
      <style>
 | 
						|
        :host ul,
 | 
						|
        :host li {
 | 
						|
          list-style: none;
 | 
						|
          padding: 0;
 | 
						|
          margin: 0;
 | 
						|
        }
 | 
						|
        :host nav {
 | 
						|
          display: flex;
 | 
						|
          flex-direction: column;
 | 
						|
        }
 | 
						|
      </style>
 | 
						|
      <nav>
 | 
						|
        <slot />
 | 
						|
      </nav>
 | 
						|
    </template>
 | 
						|
    <my-menu id="menu">
 | 
						|
      <my-link main href="d">Home</my-link>
 | 
						|
      <h2>Articles</h2>
 | 
						|
    </my-menu>
 | 
						|
    <div id="App"></div>
 | 
						|
 | 
						|
    <script>
 | 
						|
      //@ts-check
 | 
						|
 | 
						|
      /**********************************************************************
 | 
						|
       *
 | 
						|
       * UTILITIES
 | 
						|
       *
 | 
						|
       * A few common methods to use in the project
 | 
						|
       *
 | 
						|
       *********************************************************************/
 | 
						|
 | 
						|
      const Signal = () => {
 | 
						|
        const listeners = new Set();
 | 
						|
 | 
						|
        return {
 | 
						|
          remove: listeners.delete.bind(listeners),
 | 
						|
          add(/** @type {(arg:any)=>void} */ listener) {
 | 
						|
            listeners.add(listener);
 | 
						|
            return listeners.delete.bind(listeners, listener);
 | 
						|
          },
 | 
						|
          emit(/** @type {any} */ data) {
 | 
						|
            listeners.forEach((l) => l(data));
 | 
						|
          },
 | 
						|
        };
 | 
						|
      };
 | 
						|
 | 
						|
      const getText = (/** @type {string} */ file) =>
 | 
						|
        fetch(`./${file}`)
 | 
						|
          .then((response) => response.text())
 | 
						|
          .catch((err) => {
 | 
						|
            console.error(`could not find file "${file}"`);
 | 
						|
            throw err;
 | 
						|
          });
 | 
						|
 | 
						|
      const parseMarkdown = (/** @type {string} */ text) =>
 | 
						|
        text
 | 
						|
          // lists
 | 
						|
          .replace(
 | 
						|
            /^\s*\n((?:\*\s.+\s*\n)+)([^\*])/gm,
 | 
						|
            (_, bullets, next) =>
 | 
						|
              `<ul>${bullets.replace(
 | 
						|
                /^\*\s(.+)/gm,
 | 
						|
                "<li>$1</li>"
 | 
						|
              )}\n</ul>\n\n${next}`
 | 
						|
          )
 | 
						|
          .replace(
 | 
						|
            /^\s*\n((?:\d\..+\s*\n)+)([^\*])/gm,
 | 
						|
            (_, bullets, next) =>
 | 
						|
              `<ol>${bullets.replace(
 | 
						|
                /^\d\.\s(.+)/gm,
 | 
						|
                "<li>$1</li>"
 | 
						|
              )}\n</ol>\n\n${next}`
 | 
						|
          )
 | 
						|
          // blockquotes
 | 
						|
          .replace(/^\>(.+)/gm, "<blockquote>$1</blockquote>")
 | 
						|
          // headers
 | 
						|
          .replace(/(#+)(.+)/g, (_, { length: l }, t) => `<h${l}>${t}</h${l}>`)
 | 
						|
          .replace(/^(.+)\n\=+/gm, "<h1>$1</h1>")
 | 
						|
          .replace(/^(.+)\n\-+/gm, "<h2>$1</h2>")
 | 
						|
          //images
 | 
						|
          .replace(/\!\[([^\]]+)\]\(([^\)]+)\)/g, '<img src="$2" alt="$1" />')
 | 
						|
          //links
 | 
						|
          .replace(
 | 
						|
            /[\[]{1}([^\]]+)[\]]{1}[\(]{1}([^\)\"]+)(\"(.+)\")?[\)]{1}/g,
 | 
						|
            '<a href="$2" title="$4">$1</a>'
 | 
						|
          )
 | 
						|
          //font styles
 | 
						|
          .replace(/[\*\_]{2}([^\*\_]+)[\*\_]{2}/g, "<strong>$1</strong>")
 | 
						|
          .replace(/[\*\_]{1}([^\*\_]+)[\*\_]{1}/g, "<em>$1</em>")
 | 
						|
          .replace(/[\~]{2}([^\~]+)[\~]{2}/g, "<del>$1</del>")
 | 
						|
          //pre
 | 
						|
          .replace(/^\s*\n\`\`\`(([^\s]+))?/gm, '<pre class="$2">')
 | 
						|
          .replace(/^\`\`\`\s*\n/gm, "</pre>\n\n")
 | 
						|
          //code
 | 
						|
          .replace(/[\`]{1}([^\`]+)[\`]{1}/g, "<code>$1</code>")
 | 
						|
          //p
 | 
						|
          .replace(/^\s*(\n)?(.+)/gm, (m) => {
 | 
						|
            return /\<(\/)?(h\d|ul|ol|li|blockquote|pre|img)/.test(m)
 | 
						|
              ? m
 | 
						|
              : "<p>" + m + "</p>";
 | 
						|
          })
 | 
						|
          //strip p from pre
 | 
						|
          .replace(/(\<pre.+\>)\s*\n\<p\>(.+)\<\/p\>/gm, "$1$2")
 | 
						|
          .trim();
 | 
						|
 | 
						|
      const getMarkdown = (/** @type {string} */ file) =>
 | 
						|
        getText(file).then(parseMarkdown);
 | 
						|
 | 
						|
      /**
 | 
						|
       *
 | 
						|
       * @param {string} tag
 | 
						|
       * @param {Record<string, any>} props
 | 
						|
       * @param {string|Node[]} children
 | 
						|
       * @returns
 | 
						|
       */
 | 
						|
      const el = (tag = "div", props = {}, children = []) => {
 | 
						|
        const node = document.createElement(tag);
 | 
						|
        Object.keys(props).forEach((key) => {
 | 
						|
          node.setAttribute(key, props[key]);
 | 
						|
        });
 | 
						|
        if (typeof children == "string") {
 | 
						|
          children = [document.createTextNode(children)];
 | 
						|
        }
 | 
						|
        children.forEach((child) => node.appendChild(child));
 | 
						|
        return node;
 | 
						|
      };
 | 
						|
 | 
						|
      const makeTitelize = (alwaysLowCaps = [], alwaysUpperCaps = []) => {
 | 
						|
        const specials = [...alwaysLowCaps, ...alwaysUpperCaps].reduce(
 | 
						|
          (result, word) =>
 | 
						|
            result.set(new RegExp("\\b" + word + "\\b", "gi"), word),
 | 
						|
          /** @type {Map<RegExp, string>}*/ (new Map())
 | 
						|
        );
 | 
						|
        const titelize = (/** @type {string} */ text) => {
 | 
						|
          text = text
 | 
						|
            .replace(/_-\//g, " ")
 | 
						|
            .replace(/\.\w+$/, "")
 | 
						|
            .replace(/\s+/, " ")
 | 
						|
            .split(" ")
 | 
						|
            .map((word) =>
 | 
						|
              word.length > 1
 | 
						|
                ? word[0].toUpperCase() + word.slice(1).toLowerCase()
 | 
						|
                : word
 | 
						|
            )
 | 
						|
            .join(" ");
 | 
						|
          for (const [key, value] of specials) {
 | 
						|
            text = text.replace(key, value);
 | 
						|
          }
 | 
						|
          return text;
 | 
						|
        };
 | 
						|
        return titelize;
 | 
						|
      };
 | 
						|
 | 
						|
      const titelize = makeTitelize(["the", "a"], ["TV", "ID", "AI"]);
 | 
						|
 | 
						|
      const Router = (() => {
 | 
						|
        const onRouteChange = Signal();
 | 
						|
        let route = "";
 | 
						|
 | 
						|
        const set = (/** @type {string} */ newRoute) => {
 | 
						|
          if (newRoute === route) {
 | 
						|
            return false;
 | 
						|
          }
 | 
						|
          window.location.hash = newRoute;
 | 
						|
          route = newRoute;
 | 
						|
          onRouteChange.emit(route);
 | 
						|
          return true;
 | 
						|
        };
 | 
						|
 | 
						|
        const get = () => window.location.hash.slice(1).replace(/\//gi, "/");
 | 
						|
 | 
						|
        const is = (href) => href === get();
 | 
						|
 | 
						|
        window.addEventListener("popstate", () => set(get()));
 | 
						|
 | 
						|
        return { set, get, is, onRouteChange };
 | 
						|
      })();
 | 
						|
 | 
						|
      const getTemplateClone = (/** @type {string} */ id) => {
 | 
						|
        const templateModel = /** @type {HTMLTemplateElement} */ (
 | 
						|
          document.getElementById(id)
 | 
						|
        );
 | 
						|
        const template = /** @type {HTMLElement} */ (
 | 
						|
          templateModel.content.cloneNode(true)
 | 
						|
        );
 | 
						|
        return template;
 | 
						|
      };
 | 
						|
 | 
						|
      /**********************************************************************
 | 
						|
       * WEB COMPONENTS
 | 
						|
       *
 | 
						|
       * Sources:
 | 
						|
       * https://web.dev/custom-elements-best-practices/
 | 
						|
       * https://googlechromelabs.github.io/howto-components/
 | 
						|
       *
 | 
						|
       * A set of neat components to use in the page
 | 
						|
       *
 | 
						|
       *********************************************************************/
 | 
						|
 | 
						|
      class CustomElement extends HTMLElement {
 | 
						|
        /** @type {ShadowRoot} */
 | 
						|
        shadow = this.attachShadow({ mode: "closed" });
 | 
						|
 | 
						|
        /**
 | 
						|
         * A user may set a property on an instance of an element, before its prototype has been connected to this class.
 | 
						|
         * Will check for any instance properties and run them through the proper class setters.
 | 
						|
         * @param {string} prop
 | 
						|
         */
 | 
						|
        _syncProperty(prop) {
 | 
						|
          if (this.hasOwnProperty(prop)) {
 | 
						|
            let value = this[prop];
 | 
						|
            delete this[prop];
 | 
						|
            this[prop] = value;
 | 
						|
          }
 | 
						|
        }
 | 
						|
      }
 | 
						|
 | 
						|
      class MyLink extends CustomElement {
 | 
						|
        constructor() {
 | 
						|
          super();
 | 
						|
          this.shadow.append(getTemplateClone("my-link"));
 | 
						|
          this.shadow.addEventListener("click", this._onClick.bind(this));
 | 
						|
          Router.onRouteChange.add(this.updateActive.bind(this));
 | 
						|
        }
 | 
						|
 | 
						|
        static get observedAttributes() {
 | 
						|
          return ["href", "active", "main"];
 | 
						|
        }
 | 
						|
 | 
						|
        _onClick() {
 | 
						|
          if (this.href) {
 | 
						|
            Router.set(this.href);
 | 
						|
          }
 | 
						|
        }
 | 
						|
 | 
						|
        attributeChangedCallback(property, oldValue, newValue) {
 | 
						|
          if (oldValue === newValue) {
 | 
						|
            return;
 | 
						|
          }
 | 
						|
          this[property] = newValue;
 | 
						|
        }
 | 
						|
 | 
						|
        updateActive() {
 | 
						|
          if (Router.is(this.href)) {
 | 
						|
            this.setAttribute("active", "");
 | 
						|
          } else {
 | 
						|
            this.removeAttribute("active");
 | 
						|
          }
 | 
						|
        }
 | 
						|
 | 
						|
        set href(/** @type {string}*/ value) {
 | 
						|
          this.setAttribute("href", value);
 | 
						|
          this.updateActive();
 | 
						|
        }
 | 
						|
 | 
						|
        get href() {
 | 
						|
          return this.getAttribute("href");
 | 
						|
        }
 | 
						|
 | 
						|
        set main(/** @type {boolean}*/ value) {
 | 
						|
          if (value) {
 | 
						|
            this.setAttribute("main", "");
 | 
						|
          } else {
 | 
						|
            this.removeAttribute("main");
 | 
						|
          }
 | 
						|
        }
 | 
						|
 | 
						|
        get main() {
 | 
						|
          return this.hasAttribute("main");
 | 
						|
        }
 | 
						|
 | 
						|
        connectedCallback() {
 | 
						|
          ["active", "main"].forEach((prop) => this._syncProperty(prop));
 | 
						|
          this.updateActive();
 | 
						|
          if (this.getAttribute("main")) {
 | 
						|
            console.log("sdfsdff");
 | 
						|
          }
 | 
						|
        }
 | 
						|
      }
 | 
						|
 | 
						|
      customElements.define("my-link", MyLink);
 | 
						|
 | 
						|
      class MyMenu extends CustomElement {
 | 
						|
        _handled = new Set();
 | 
						|
        constructor() {
 | 
						|
          super();
 | 
						|
          this.shadow.append(getTemplateClone("my-menu"));
 | 
						|
          const slot = this.shadow.querySelector("slot");
 | 
						|
          slot.addEventListener("slotchange", (event) => {
 | 
						|
            for (const child of slot.assignedElements()) {
 | 
						|
              if (this._handled.has(child) || !(child instanceof MyLink)) {
 | 
						|
                continue;
 | 
						|
              }
 | 
						|
              this._handled.add(child);
 | 
						|
              // TODO: pre-fetch
 | 
						|
              //console.log("new child: ", child);
 | 
						|
            }
 | 
						|
          });
 | 
						|
        }
 | 
						|
      }
 | 
						|
      customElements.define("my-menu", MyMenu);
 | 
						|
 | 
						|
      /**********************************************************************
 | 
						|
       * MARKDOWN PARSING
 | 
						|
       *********************************************************************/
 | 
						|
 | 
						|
      const load = (/** @type {string} */ file) =>
 | 
						|
        getMarkdown(file).then((md) => {
 | 
						|
          app.innerHTML = md;
 | 
						|
        });
 | 
						|
 | 
						|
      /**********************************************************************
 | 
						|
       * BOOSTRAPPING
 | 
						|
       *********************************************************************/
 | 
						|
 | 
						|
      getText("files.txt").then((lines) => {
 | 
						|
        lines
 | 
						|
          .split(`\n`)
 | 
						|
          .map((line) => {
 | 
						|
            const [file, maybeDate, ...rest] = line.split(/\s/);
 | 
						|
            const href = file.trim();
 | 
						|
            let date = maybeDate ? new Date(maybeDate) : new Date();
 | 
						|
            if (isNaN(date.getTime())) {
 | 
						|
              date = new Date();
 | 
						|
              rest.unshift(maybeDate);
 | 
						|
            }
 | 
						|
            const textContent = rest.length
 | 
						|
              ? rest.join(" ").trim()
 | 
						|
              : titelize(file);
 | 
						|
            return { href, date, textContent };
 | 
						|
          })
 | 
						|
          .sort(({ date: a }, { date: b }) => a.getTime() - b.getTime())
 | 
						|
          .forEach(({ href, date, textContent }) => {
 | 
						|
            const link = /** @type {MyLink} */ el(
 | 
						|
              "my-link",
 | 
						|
              { href },
 | 
						|
              textContent
 | 
						|
            );
 | 
						|
            document.getElementById("menu").appendChild(link);
 | 
						|
          });
 | 
						|
      });
 | 
						|
 | 
						|
      const app = document.getElementById("App");
 | 
						|
 | 
						|
      const BLOCKQUOTE = Symbol("blockquote");
 | 
						|
      const PARAGRAPH = Symbol("paragraph");
 | 
						|
      const LIST = Symbol("list");
 | 
						|
 | 
						|
      Router.onRouteChange.add((route) => load(route));
 | 
						|
    </script>
 | 
						|
  </body>
 | 
						|
</html>
 |