/* gmi.js is licensed under CC0 */ class Gemini { static syntax = { P: "", A: "=>", UL: "*", BLOCKQUOTE: ">", PRE: "```", H1: "#", H2: "##", H3: "###", }; static line(line, type) { if (typeof line === "string") line = { content: line, type: type || "P" }; let dom = Gemini.render(line).dom; return { get dom() { return dom; }, get type() { return this.dom.nodeName; }, set type(type) { dom = Gemini.render({ dom: this.dom, type, content: Gemini.contentFrom(this.dom), }).dom; }, get content() { return Gemini.contentFrom(dom); }, set content(content) { Gemini.render({ dom, type: dom.nodeName, content }); }, get editable() { return this.dom.contentEditable === "true"; }, set editable(value) { Gemini.render({ dom: this.dom, type: this.type, content: this.content, editable: value, }); }, delete() { return this.dom.remove(); }, get gmi() { const syntax = Gemini.syntax[this.type]; const content = Gemini.contentFrom(this.dom).replace(/\n?$/, ""); switch (this.type.toUpperCase()) { case "PRE": return `${syntax}\n${content}\n${syntax}`; break; default: return content .split("\n") .map((line) => `${syntax !== "" ? syntax + " " : ""}${line}`) .join("\n"); } }, get before() { return Gemini.line(this.dom.previousElementSibling); }, set before(line) { this.before.dom.after(line.dom); }, get after() { return Gemini.line(this.dom.nextElementSibling); }, set after(line) { this.after.dom.before(line.dom); }, }; } static render(line) { if (line.dom && line.dom.nodeName !== line.type) { const replacement = document.createElement(line.type); line.dom.replaceWith(replacement); line.dom = replacement; } else if (line.nodeName) { line = { dom: line, type: line.nodeName, content: Gemini.contentFrom(line), editable: line.contentEditable, }; } else { line.dom = line.dom || document.createElement(line.type || "P"); } line.dom.contentEditable = line.editable || "inherit"; switch (line.type.toUpperCase()) { case "A": const { href, content } = Gemini.link(line.content); line.dom.innerHTML = line.editable && href !== content ? `${href} ${content}` : content; line.dom.href = href; break; case "UL": line.dom.innerHTML = line.content .split("\n") .map((content) => (content.length > 0 ? `
  • ${content}
  • ` : "")) .join("\n"); break; case "BLOCKQUOTE": line.dom.innerHTML = line.content .split("\n") .map((content) => `
    ${content}
    `) .join("\n"); break; case "PRE": line.dom.textContent = line.content; break; default: line.dom.innerHTML = line.content.replace(/\n+/g, "
    "); } return line; } static contentFrom(dom) { switch (dom.nodeName.toUpperCase()) { case "BLOCKQUOTE": return Array.from(dom.childNodes) .map((child) => child.textContent) .join("\n"); break; case "UL": return Array.from(dom.children) .map((child) => child.textContent) .join("\n"); break; case "A": const { href, content } = Gemini.link(dom.textContent); return `${href || dom.href} ${content}`; break; case "PRE": return dom.textContent; break; default: return dom.innerHTML.replace(/
    /g, "\n"); } } // TODO: rename/move/idk this is awk static link(content = "") { return /((?[^\s]+\/\/[^\s]+)\s)?(?.+)/.exec(content).groups; } constructor(root) { this.root = root; } get editable() { return this.root.contentEditable === "true"; } set editable(value) { this.root.contentEditable = value; this.lines.forEach((line) => (line.editable = value)); } get lines() { return Array.from(this.root.children) .filter((el) => ["P", "BLOCKQUOTE", "A", "PRE", "UL", "H1", "H2", "H3"].includes( el.nodeName ) ) .map(Gemini.line); } set lines(lines) { this.root.textContent = ""; this.root.append(...lines.map((line) => line.dom)); } get source() { return this.lines.map((line) => line.gmi).join("\n"); } // TODO: set source is a DOM thing, but parsing should be isomorphic // set source(gmi) {} download() { const el = document.createElement("a"); el.setAttribute( "href", "data:text/gemini;charset=utf-8," + encodeURIComponent(this.source) ); el.setAttribute( "download", `${this.lines[0].content.replace(/\s/g, "_")}.gmi` ); el.style.display = "none"; document.body.appendChild(el); el.click(); document.body.removeChild(el); } get foreground() { return getComputedStyle(this.root).getPropertyValue("--foreground"); } set foreground(value) { return this.root.style.setProperty("--foreground", value); } get background() { return getComputedStyle(this.root).getPropertyValue("--background"); } set background(value) { return this.root.style.setProperty("--background", value); } get size() { return getComputedStyle(this.root).getPropertyValue("--font-size"); } set size(value) { return this.root.style.setProperty("--font-size", value); } get lineHeight() { return getComputedStyle(this.root).getPropertyValue("--line-height"); } set lineHeight(value) { return this.root.style.setProperty("--line-height", value); } get serif() { return getComputedStyle(this.root).getPropertyValue("--serif"); } set serif(value) { return this.root.style.setProperty("--serif", value); } get sans() { return getComputedStyle(this.root).getPropertyValue("--sans"); } set sans(value) { return this.root.style.setProperty("--sans", value); } get mono() { return getComputedStyle(this.root).getPropertyValue("--mono"); } set mono(value) { return this.root.style.setProperty("--mono", value); } }