| 1 | /* goframework.io: helpers the page's scripts share, as window.GF. */ |
| 2 | (function () { |
| 3 | "use strict"; |
| 4 | |
| 5 | // el builds an element: el("a", {href: "#x", class: "y"}, "text", child). |
| 6 | // Text is always text: nothing here parses markup. |
| 7 | function el(tag, attributes, ...children) { |
| 8 | const node = document.createElement(tag); |
| 9 | for (const [key, value] of Object.entries(attributes || {})) { |
| 10 | if (value === undefined || value === null || value === false) continue; |
| 11 | if (key === "class") node.className = value; |
| 12 | else if (key === "dataset") Object.assign(node.dataset, value); |
| 13 | else if (key.startsWith("on") && typeof value === "function") node.addEventListener(key.slice(2), value); |
| 14 | else node.setAttribute(key, value === true ? "" : String(value)); |
| 15 | } |
| 16 | append(node, children); |
| 17 | return node; |
| 18 | } |
| 19 | |
| 20 | function append(node, children) { |
| 21 | for (const child of children.flat(Infinity)) { |
| 22 | if (child === undefined || child === null || child === false) continue; |
| 23 | node.append(child instanceof Node ? child : document.createTextNode(String(child))); |
| 24 | } |
| 25 | return node; |
| 26 | } |
| 27 | |
| 28 | function svg(tag, attributes, ...children) { |
| 29 | const node = document.createElementNS("http://www.w3.org/2000/svg", tag); |
| 30 | for (const [key, value] of Object.entries(attributes || {})) { |
| 31 | if (value !== undefined && value !== null) node.setAttribute(key, String(value)); |
| 32 | } |
| 33 | append(node, children); |
| 34 | return node; |
| 35 | } |
| 36 | |
| 37 | // store reads and writes preferences; any failure (private windows, |
| 38 | // blocked storage) leaves the page working without them. |
| 39 | const store = { |
| 40 | get(key) { try { return window.localStorage.getItem(key); } catch (error) { return null; } }, |
| 41 | set(key, value) { try { if (value === null) window.localStorage.removeItem(key); else window.localStorage.setItem(key, value); } catch (error) { /* not kept */ } }, |
| 42 | }; |
| 43 | |
| 44 | // copy puts text on the clipboard; where that is refused it selects a |
| 45 | // field holding it, so the reader can copy by hand. |
| 46 | async function copy(text, button) { |
| 47 | let done = false; |
| 48 | try { |
| 49 | if (navigator.clipboard && window.isSecureContext) { |
| 50 | await navigator.clipboard.writeText(text); |
| 51 | done = true; |
| 52 | } |
| 53 | } catch (error) { /* fall back below */ } |
| 54 | if (!done) { |
| 55 | const field = el("textarea", { class: "visually-hidden", "aria-hidden": "true" }, text); |
| 56 | document.body.append(field); |
| 57 | field.select(); |
| 58 | try { done = document.execCommand("copy"); } catch (error) { done = false; } |
| 59 | field.remove(); |
| 60 | } |
| 61 | if (button) { |
| 62 | const before = button.textContent; |
| 63 | button.textContent = done ? "Copied" : "Press Ctrl+C"; |
| 64 | button.classList.toggle("done", done); |
| 65 | setTimeout(() => { button.textContent = before; button.classList.remove("done"); }, 1600); |
| 66 | } |
| 67 | return done; |
| 68 | } |
| 69 | |
| 70 | function copyButton(text, label) { |
| 71 | const button = el("button", { class: "copy", type: "button", "aria-label": "Copy " + (label || text) }, "Copy"); |
| 72 | button.addEventListener("click", (event) => { event.preventDefault(); event.stopPropagation(); copy(text, button); }); |
| 73 | return button; |
| 74 | } |
| 75 | |
| 76 | // highlight colours Go: keywords, strings, comments, numbers, calls. A |
| 77 | // small tokenizer, enough for the page's snippets. |
| 78 | const keywords = new Set(("break case chan const continue default defer else fallthrough for func go goto if import interface map package range " + |
| 79 | "return select struct switch type var nil true false iota any error string bool int int64 uint64 float64 byte rune").split(" ")); |
| 80 | function highlight(code) { |
| 81 | const out = document.createDocumentFragment(); |
| 82 | const pattern = /(\/\/[^\n]*|\/\*[\s\S]*?\*\/)|("(?:[^"\\\n]|\\.)*"|`[^`]*`|'(?:[^'\\\n]|\\.)*')|\b(\d+(?:\.\d+)?(?:e[+-]?\d+)?)\b|\b([A-Za-z_][A-Za-z0-9_]*)(?=\s*[(\[])|\b([A-Za-z_][A-Za-z0-9_]*)\b/g; |
| 83 | let at = 0; |
| 84 | for (const match of code.matchAll(pattern)) { |
| 85 | if (match.index > at) out.append(code.slice(at, match.index)); |
| 86 | const [text, comment, string, number, call, word] = match; |
| 87 | let kind = ""; |
| 88 | if (comment) kind = "tok-c"; |
| 89 | else if (string) kind = "tok-s"; |
| 90 | else if (number) kind = "tok-n"; |
| 91 | else if (call) kind = keywords.has(call) ? "tok-k" : "tok-f"; |
| 92 | else if (word && keywords.has(word)) kind = "tok-k"; |
| 93 | out.append(kind ? el("span", { class: kind }, text) : text); |
| 94 | at = match.index + text.length; |
| 95 | } |
| 96 | if (at < code.length) out.append(code.slice(at)); |
| 97 | return out; |
| 98 | } |
| 99 | |
| 100 | function codeBlock(code) { |
| 101 | return el("pre", {}, el("code", {}, highlight(code))); |
| 102 | } |
| 103 | |
| 104 | window.GF = { el, svg, append, store, copy, copyButton, highlight, codeBlock }; |
| 105 | })(); |