Skip to content
quarry Artifact [429aa3cae3]
Login

Artifact [429aa3cae3]

Artifact 429aa3cae3db247f3ffbdc673313da3e24f0d121adf0bd5a4cb6302e61321b61:


// composer.js — slash commands for quarry's markdown composers.
// First-party, dependency-free. A textarea opts in with data-slash.
// Typing "/" at the start of a line opens a snippet menu; Escape or
// clicking away closes it; without this script the textarea is simply
// a textarea. Snippets insert plain markdown — the raw text stays the
// source of truth.
(function () {
  "use strict";
  var ITEMS = [
    { label: "Heading", hint: "## ", text: "## " },
    { label: "Bold", hint: "**b**", text: "**bold**" },
    { label: "Code block", hint: "```", text: "```\ncode\n```\n" },
    { label: "Quote", hint: "> ", text: "> " },
    { label: "Bullet list", hint: "- ", text: "- " },
    { label: "Task list", hint: "- [ ]", text: "- [ ] " },
    { label: "Details", hint: "fold", text: "<details><summary>summary</summary>\n\nbody\n\n</details>\n" },
    { label: "Table", hint: "| |", text: "| a | b |\n| --- | --- |\n| 1 | 2 |\n" },
    { label: "Poll", hint: "ballot", text: "```poll\nquestion: Your question?\n- option one\n- option two\ncloses: 2026-12-31\n```\n" }
  ];
  var menu = null, target = null, slashAt = -1, sel = 0, items = ITEMS;

  // A textarea may add its own entries (data-slash-extra, a JSON list of
  // {label, hint, text}): the filing form's field commands.
  function itemsFor(ta) {
    try {
      var extra = JSON.parse(ta.getAttribute("data-slash-extra") || "[]");
      return Array.isArray(extra) ? extra.concat(ITEMS) : ITEMS;
    } catch (e) {
      return ITEMS;
    }
  }

  function close() {
    if (menu) menu.remove();
    menu = null; target = null; slashAt = -1; sel = 0;
  }
  function insert(item) {
    var ta = target, at = slashAt;
    close();
    ta.setRangeText(item.text, at, ta.selectionStart, "end");
    ta.focus();
    ta.dispatchEvent(new Event("input", { bubbles: true }));
  }
  function render() {
    var btns = menu.querySelectorAll("button");
    for (var i = 0; i < btns.length; i++) btns[i].classList.toggle("sel", i === sel);
  }
  function open(ta) {
    close();
    target = ta; slashAt = ta.selectionStart - 1;
    items = itemsFor(ta);
    menu = document.createElement("div");
    menu.className = "slash-menu";
    items.forEach(function (item, i) {
      var b = document.createElement("button");
      b.type = "button";
      b.textContent = item.label;
      var s = document.createElement("small");
      s.textContent = item.hint;
      b.appendChild(s);
      b.addEventListener("mousedown", function (ev) { ev.preventDefault(); insert(item); });
      b.addEventListener("mouseenter", function () { sel = i; render(); });
      menu.appendChild(b);
    });
    var r = ta.getBoundingClientRect();
    menu.style.left = (r.left + window.scrollX + 12) + "px";
    menu.style.top = (r.top + window.scrollY + 28) + "px";
    document.body.appendChild(menu);
    render();
  }
  document.addEventListener("input", function (ev) {
    var ta = ev.target;
    if (!ta.matches || !ta.matches("textarea[data-slash]")) return;
    var p = ta.selectionStart, v = ta.value;
    if (p > 0 && v[p - 1] === "/" && (p === 1 || v[p - 2] === "\n")) { open(ta); return; }
    if (menu) close();
  });
  document.addEventListener("keydown", function (ev) {
    if (!menu) return;
    if (ev.key === "Escape") { close(); return; }
    if (ev.key === "ArrowDown") { ev.preventDefault(); sel = (sel + 1) % items.length; render(); return; }
    if (ev.key === "ArrowUp") { ev.preventDefault(); sel = (sel + items.length - 1) % items.length; render(); return; }
    if (ev.key === "Enter" || ev.key === "Tab") { ev.preventDefault(); insert(items[sel]); }
  });
  document.addEventListener("click", function (ev) {
    if (menu && !menu.contains(ev.target)) close();
    // The mention completion list: the server renders the candidates,
    // this inserts the chosen one. Replace the trailing @word the
    // server matched, keep the caret after it, and tell the binding.
    var pick = ev.target.closest && ev.target.closest("#mentions li[data-handle]");
    if (pick) {
      var ul = pick.closest("#mentions");
      var form = ul.closest("form");
      var ta = form && form.querySelector("textarea");
      if (ta) {
        var m = /@[A-Za-z0-9_.-]*$/.exec(ta.value.slice(0, ta.selectionStart));
        var at = m ? ta.selectionStart - m[0].length : ta.selectionStart;
        ta.setRangeText("@" + pick.dataset.handle + " ", at, ta.selectionStart, "end");
        ta.focus();
        ta.dispatchEvent(new Event("input", { bubbles: true }));
      }
      ul.hidden = true;
    }
  });
})();

// --- the composer's two views ------------------------------------------
// Rich, the draft as one editable document, and Markdown, the textarea
// the form posts.
(function () {
  "use strict";

  // --- rich: one document over the production renderer ---------------
  // Rich is the draft as one editable document. Every block is first
  // drawn by the PRODUCTION renderer (markup-blocks), so Rich reads as
  // the post will. Stepping into a block turns it into its editing
  // form, plain elements this script can write back as markdown; on
  // leaving, a block that changed is rendered by the server again and
  // one that did not goes back to exactly what it was. A block nobody
  // edits keeps every byte of its source, so a saved post's next
  // version differs only where it was edited. Tables, polls, diagrams
  // and markdown this script cannot write back are cards, each with an
  // editor of its own. The textarea stays the source of truth: every
  // change writes it and fires input, and the Markdown tab is it.
  var ZW = "​";
  function esc(s) {
    return String(s).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
  }
  function el(tag, attrs, text) {
    var e = document.createElement(tag);
    Object.keys(attrs || {}).forEach(function (k) { e.setAttribute(k, attrs[k]); });
    if (text) e.textContent = text;
    return e;
  }
  function icon(id) { return '<svg class="ic" aria-hidden="true"><use href="#' + id + '"/></svg>'; }
  function fenceOf(l) { var m = /^ {0,3}(`{3,}|~{3,})/.exec(l); return m ? m[1] : ""; }
  function listItem(l) { return /^ {0,3}([-*+]|\d{1,9}[.)])[ \t]/.test(l); }
  function fenceBody(src) {
    var l = src.replace(/\n+$/, "").split("\n");
    return l.slice(1, l.length - (l.length > 1 && fenceOf(l[l.length - 1]) ? 1 : 0)).join("\n");
  }
  // split is markup.SplitBlocks, for a block's own text.
  function split(src) {
    var lines = src.split(/(?<=\n)/), out = [], cur = "", fence = "", after = false, content = false, list = false, html = 0;
    lines.forEach(function (ln) {
      var blank = ln.trim() === "", t = ln.trim().toLowerCase();
      if (fence) {
        var f = fenceOf(ln);
        if (f && f[0] === fence[0] && f.length >= fence.length && ln.trim().split(fence[0]).join("") === "") fence = "";
      } else if (!blank) {
        if (after && html === 0 && !/^[ \t]/.test(ln) && content && !(list && listItem(ln))) { out.push(cur); cur = ""; content = false; }
        if (!content) list = listItem(ln);
        content = true;
        var o = fenceOf(ln);
        if (o) fence = o;
        if (t[0] === "<") { html += (t.match(/<details/g) || []).length - (t.match(/<\/details/g) || []).length; if (html < 0) html = 0; }
      }
      after = blank && !fence;
      cur += ln;
    });
    if (cur) out.push(cur);
    return out;
  }

  // What a block is, from its source: prose is edited in place, the
  // rest are cards.
  function kindOf(src) {
    var t = src.replace(/^\n+|\n+$/g, ""), lines = t.split("\n"), f = fenceOf(lines[0]);
    if (f) {
      var lang = lines[0].trim().slice(f.length).trim();
      return lang === "pikchr" ? "diagram" : lang === "poll" ? "poll" : "prose";
    }
    if (/^\|.*\|\s*$/.test(lines[0]) && lines[1] && /^\|[\s:|-]+\|\s*$/.test(lines[1])) return "table";
    if (/^<details/i.test(lines[0])) return "prose";
    if (/^</.test(lines[0]) || /^\[\^[^\]]+\]:/.test(lines[0])) return "source";
    if (lines.every(function (l) { return /^ {0,3}\[[^\]]+\]:\s*\S/.test(l) || !l.trim(); })) return "source";
    return faithful(t) ? "prose" : "source";
  }
  // A block is edited in place only when its editing form writes back
  // the same words; anything this script would lose stays a card.
  function words(s) { return s.replace(/\\(.)/g, "$1").replace(/[^\p{L}\p{N}]+/gu, " ").trim(); }
  function faithful(md) {
    var box = document.createElement("div");
    box.innerHTML = renderAll(md + "\n");
    var back = Array.prototype.map.call(box.children, mdNode).join("\n\n");
    return words(back) === words(md);
  }

  // ---- markdown -> the editing form -----------------------------------
  var REF_IC = { ticket: "i-tkt-open", landing: "i-landing", person: "i-person", wiki: "i-book", "check-in": "i-commit" };
  function refHTML(tok) {
    var kind = tok[0] === "@" ? "person" : tok.slice(0, 2) === "[[" ? "wiki" : tok.indexOf("!") >= 0 ? "landing" : tok[0] === "[" ? "check-in" : "ticket";
    var label = kind === "wiki" ? tok.slice(2, -2) : tok;
    return '<a class="ref rt-ref" contenteditable="false" href="#" data-tok="' + esc(tok) + '" title="' + kind + '">' + icon(REF_IC[kind]) + esc(label) + "</a>";
  }
  // keepHTML is markdown this script shows but does not edit: an
  // attachment, a footnote mark, inline HTML. It is written back as is.
  function keepHTML(md, label, cls) {
    return '<span class="rt-keep' + (cls ? " " + cls : "") + '" contenteditable="false" data-md="' + esc(md) + '">' + esc(label) + "</span>";
  }
  var staged = {}; // attach:NAME -> a blob URL, for files staged in this page
  function inline(s) {
    var stash = [];
    function keep(h) { stash.push(h); return "\u0000" + (stash.length - 1) + "\u0000"; }
    s = s.replace(/`([^`]+)`/g, function (_, c) { return keep("<code>" + esc(c) + "</code>"); });
    s = s.replace(/!\[([^\]]*)\]\(([^)\s]+)\)/g, function (m, a, u) {
      if (staged[u]) return keep('<img alt="' + esc(a) + '" src="' + esc(staged[u]) + '" data-upload="' + esc(u) + '">');
      if (/^https:\/\//.test(u)) return keep('<img alt="' + esc(a) + '" src="' + esc(u) + '">');
      return keep(keepHTML(m, a || u.replace(/^attach:/, ""), "rt-file"));
    });
    s = s.replace(/\[([^\]]+)\]\(([^)\s]+)\)/g, function (_, t, u) { return keep('<a href="' + esc(u) + '">' + esc(t) + "</a>"); });
    s = s.replace(/\[\^([^\]]+)\]/g, function (m, n) { return keep(keepHTML(m, n, "rt-fn")); });
    s = s.replace(/\[\[[^\]\n]+\]\]/g, function (m) { return keep(refHTML(m)); });
    s = s.replace(/<\/?[a-zA-Z][^>]*>/g, function (m) { return keep(keepHTML(m, m)); });
    s = s.replace(/(^|[\s(])((?:[\w.-]+:)?[#!][1-9]\d{0,8}|@[A-Za-z0-9](?:[\w.-]{0,62}\w)?|\[[0-9a-f]{6,40}\])(?![\w])/g, function (_, pre, tok) { return pre + keep(refHTML(tok)); });
    s = s.replace(/(^|\s)(https?:\/\/[^\s<]+[^\s<.,;:!?)])/g, function (_, pre, u) { return pre + keep('<a href="' + esc(u) + '">' + esc(u) + "</a>"); });
    s = esc(s)
      .replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>")
      .replace(/(^|[^\w*])\*([^*\s][^*]*)\*(?!\*)/g, "$1<em>$2</em>")
      .replace(/(^|[^\w])_([^_\s][^_]*)_(?!\w)/g, "$1<em>$2</em>")
      .replace(/~~([^~]+)~~/g, "<s>$1</s>")
      .replace(/\\([*_`~\\#>\-!@\[\]|<])/g, "$1");
    return s.replace(/\u0000(\d+)\u0000/g, function (_, i) { return stash[+i]; });
  }
  function listHTML(lines) {
    // Items by indentation: deeper lines nest under the item above them.
    var root = { kids: [], depth: -1 }, stack = [root];
    lines.forEach(function (l) {
      var m = /^(\s*)([-*+]|\d{1,9}[.)])\s+(\[[ xX]\]\s+)?(.*)$/.exec(l);
      if (!m) { var top = stack[stack.length - 1]; if (top.text !== undefined && l.trim()) top.text += " " + l.trim(); return; }
      var d = m[1].replace(/\t/g, "  ").length;
      while (stack.length > 1 && stack[stack.length - 1].depth >= d) stack.pop();
      var it = { depth: d, ord: /\d/.test(m[2]), task: m[3] ? (/x/i.test(m[3]) ? "x" : " ") : null, text: m[4], kids: [] };
      stack[stack.length - 1].kids.push(it);
      stack.push(it);
    });
    function out(items) {
      if (!items.length) return "";
      var tag = items[0].ord ? "ol" : "ul";
      return "<" + tag + ">" + items.map(function (it) {
        var box = it.task !== null ? '<input type="checkbox" contenteditable="false"' + (it.task === "x" ? " checked" : "") + ">" : "";
        return "<li" + (it.task !== null ? ' class="task"' : "") + ">" + box + inline(it.text) + out(it.kids) + "</li>";
      }).join("") + "</" + tag + ">";
    }
    return out(root.kids);
  }
  var ALERTS = { note: "Note", tip: "Tip", important: "Important", warning: "Warning", caution: "Caution" };
  function renderBlock(b) {
    var t = b.replace(/^\n+|\n+$/g, ""), lines = t.split("\n"), m;
    if (!t.trim()) return "";
    var f = fenceOf(lines[0]);
    if (f) {
      var lang = lines[0].trim().slice(f.length).trim();
      return '<pre data-lang="' + esc(lang) + '"><code>' + esc(fenceBody(t)) + "</code></pre>";
    }
    if (/^<details/i.test(lines[0])) return foldHTML(t);
    // A heading is drawn a level down, as the post draws it (the page
    // owns the h1): ## is an h3.
    if ((m = /^(#{1,6}) (.*)$/.exec(t)) && lines.length === 1) {
      var h = Math.min(m[1].length + 1, 6);
      return "<h" + h + ">" + inline(m[2]) + "</h" + h + ">";
    }
    if (/^(---+|\*\*\*+|___+)$/.test(t)) return "<hr>";
    if (lines.every(function (l) { return /^>/.test(l); })) {
      var body = lines.map(function (l) { return l.replace(/^> ?/, ""); }), alert = /^\[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)\]\s*$/i.exec(body[0]);
      if (alert) body = body.slice(1);
      var paras = body.join("\n").split(/\n\s*\n/).map(function (p) { return "<p>" + inline(p).replace(/\n/g, "<br>") + "</p>"; }).join("");
      if (alert) {
        var k = alert[1].toLowerCase();
        return '<blockquote data-alert="' + k + '" data-label="' + ALERTS[k] + '">' + paras + "</blockquote>";
      }
      return "<blockquote>" + paras + "</blockquote>";
    }
    if (listItem(lines[0])) return listHTML(lines);
    return "<p>" + inline(lines.join("\n")).replace(/\n/g, "<br>") + "</p>";
  }
  // A fold is edited in place: its summary is a line of text and what it
  // hides is ordinary Rich content inside the open fold.
  function foldHTML(t) {
    var sum = /<summary>([\s\S]*?)<\/summary>/i.exec(t);
    var body = t.replace(/^[\s\S]*?<\/summary>/i, "").replace(/<\/details>\s*$/i, "").trim();
    return '<details open class="rt-fold"><summary>' + inline(sum ? sum[1].trim() : "Details") + "</summary>" +
      (body ? renderAll(body + "\n") : "<p><br></p>") + "</details>";
  }
  function renderAll(src) { return split(src).map(renderBlock).join(""); }

  // ---- the editing form -> markdown -------------------------------------
  function mdText(s) {
    s = s.replace(/​/g, "").replace(/ /g, " ").replace(/([*`\\]|~~|<(?=[a-zA-Z\/]))/g, "\\$1");
    // An underscore inside a word is a letter, not emphasis.
    return s.replace(/_/g, function (u, i) { return /\w/.test(s[i - 1] || "") && /\w/.test(s[i + 1] || "") ? u : "\\_"; });
  }
  function mdInline(node, skipLists) {
    var out = "";
    node.childNodes.forEach(function (n) {
      if (n.nodeType === 3) { out += mdText(n.nodeValue); return; }
      if (n.nodeType !== 1) return;
      var t = n.tagName;
      if (skipLists && (t === "UL" || t === "OL")) return;
      if (t === "INPUT") return;
      if (n.hasAttribute("data-md")) { out += n.getAttribute("data-md"); return; }
      if (t === "A" && n.hasAttribute("data-tok")) { out += n.getAttribute("data-tok"); return; }
      if (t === "IMG") { out += "![" + (n.getAttribute("alt") || "") + "](" + (n.getAttribute("data-upload") || n.getAttribute("src")) + ")"; return; }
      var inner = mdInline(n);
      if (!inner.trim() && t !== "BR") { out += inner; return; }
      if (t === "STRONG" || t === "B") out += "**" + inner + "**";
      else if (t === "EM" || t === "I") out += "_" + inner + "_";
      else if (t === "S" || t === "DEL" || t === "STRIKE") out += "~~" + inner + "~~";
      else if (t === "CODE") out += "`" + n.textContent.replace(/​/g, "") + "`";
      else if (t === "A") out += inner === n.getAttribute("href") ? inner : "[" + inner + "](" + n.getAttribute("href") + ")";
      else if (t === "BR") out += "\n";
      else out += inner;
    });
    return out;
  }
  // A line that would read as a heading, list, quote or rule is escaped.
  function guardLines(s) {
    return s.split("\n").map(function (l) { return /^(#{1,6}\s|[-*+]\s|\d{1,9}[.)]\s|>|---|\|)/.test(l) ? "\\" + l : l; }).join("\n");
  }
  function mdList(list, depth) {
    var i = 0, pad = "  ".repeat(depth);
    return Array.prototype.filter.call(list.children, function (c) { return c.tagName === "LI"; }).map(function (li) {
      i++;
      var box = li.querySelector(":scope > input[type=checkbox]");
      var head = pad + (list.tagName === "OL" ? i + ". " : "- ") + (box ? (box.checked ? "[x] " : "[ ] ") : "") + mdInline(li, true).trim();
      var subs = Array.prototype.filter.call(li.children, function (c) { return c.tagName === "UL" || c.tagName === "OL"; }).map(function (s) { return mdList(s, depth + 1); });
      return [head].concat(subs).join("\n");
    }).join("\n");
  }
  function mdNode(n) {
    var t = n.tagName;
    if (/^H[1-6]$/.test(t)) return "######".slice(0, Math.max(1, +t[1] - 1)) + " " + mdInline(n).trim();
    if (t === "UL" || t === "OL") return mdList(n, 0);
    if (t === "BLOCKQUOTE") {
      var ps = n.children.length ? Array.prototype.slice.call(n.children) : [n];
      var body = ps.map(function (c) { return mdInline(c).trim().split("\n").map(function (l) { return "> " + l; }).join("\n"); }).join("\n>\n");
      var a = n.getAttribute("data-alert");
      return a ? "> [!" + a.toUpperCase() + "]\n" + body : body;
    }
    if (t === "PRE") return "```" + (n.getAttribute("data-lang") || "") + "\n" + n.textContent.replace(/​/g, "").replace(/\n$/, "") + "\n```";
    if (t === "HR") return "---";
    if (t === "DETAILS") {
      var summ = n.querySelector(":scope > summary"), parts = [];
      Array.prototype.forEach.call(n.children, function (c) {
        if (c.tagName === "SUMMARY") return;
        var m = mdNode(c);
        if (m.replace(/​/g, "").trim()) parts.push(m);
      });
      return "<details><summary>" + (summ ? mdInline(summ).trim() : "Details") + "</summary>\n\n" + parts.join("\n\n") + "\n\n</details>";
    }
    return guardLines(mdInline(n).replace(/[ \t]+$/gm, "").replace(/\n+$/, ""));
  }

  // ---- table and poll sources --------------------------------------------
  function pollOf(src) {
    var body = fenceBody(src), q = /^question:\s*(.*)$/m.exec(body), c = /^closes:\s*(.*)$/m.exec(body);
    return { q: q ? q[1] : "", opts: body.split("\n").filter(function (l) { return /^- /.test(l); }).map(function (l) { return l.slice(2); }), closes: c ? c[1].trim() : "" };
  }
  function pollSrc(d) {
    return "```poll\nquestion: " + d.q + "\n" + d.opts.map(function (o) { return "- " + o; }).join("\n") + (d.closes ? "\ncloses: " + d.closes : "") + "\n```";
  }
  function tableOf(src) {
    var lines = src.replace(/\n+$/, "").split("\n").filter(function (l) { return l.trim(); });
    var cells = function (l) { return l.trim().replace(/^\||\|$/g, "").split(/(?<!\\)\|/).map(function (c) { return c.trim(); }); };
    return { rows: lines.filter(function (l, i) { return i !== 1; }).map(cells), align: lines[1] ? cells(lines[1]) : [] };
  }
  function tableSrc(t) {
    var n = Math.max.apply(null, t.rows.map(function (r) { return r.length; }));
    var row = function (r) { var c = r.slice(); while (c.length < n) c.push(""); return "| " + c.join(" | ") + " |"; };
    var al = [];
    for (var i = 0; i < n; i++) al.push(t.align[i] && /^:?-+:?$/.test(t.align[i]) ? t.align[i] : "---");
    return [row(t.rows[0])].concat(["| " + al.join(" | ") + " |"], t.rows.slice(1).map(row)).join("\n");
  }
  // pikchr-studio takes the source in the fragment, as the rendered
  // diagram's own link does (markup.StudioLink): base64url of raw
  // DEFLATE, so the source never reaches a server.
  var STUDIO = "https://pikchr-studio.daniel-mestas.workers.dev/";
  function studioLink(body, done) {
    if (!window.CompressionStream) { done(STUDIO); return; }
    var cs = new CompressionStream("deflate-raw"), w = cs.writable.getWriter();
    w.write(new TextEncoder().encode(body));
    w.close();
    new Response(cs.readable).arrayBuffer().then(function (buf) {
      var bin = "", a = new Uint8Array(buf);
      for (var i = 0; i < a.length; i++) bin += String.fromCharCode(a[i]);
      done(STUDIO + "#z=" + btoa(bin).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, ""));
    });
  }

  function richEditor(ta, host, root) {
    var blocksURL = host.getAttribute("data-blocks"), suggestURL = host.getAttribute("data-suggest");
    var hint = "Write, or type / to insert";
    var touch = matchMedia("(pointer: coarse)").matches || innerWidth < 640;
    var doc = el("div", { class: "rt-doc", contenteditable: "true", role: "textbox", "aria-multiline": "true",
      "aria-label": "the post, as it will read", spellcheck: "true", "data-hook": "rich-doc" });
    host.appendChild(doc);
    try { document.execCommand("defaultParagraphSeparator", false, "p"); } catch (e) {}

    // The formatting bar: a bubble over a selection, docked above the
    // keyboard on a phone. Its controls write the editing form; the
    // markdown follows from it.
    var fmt = el("div", { class: "rt-pop rt-fmt", role: "toolbar", "aria-label": "formatting", "data-hook": "rich-bar" });
    var styles = [["p", "Text"], ["h1", "Heading 1"], ["h2", "Heading 2"], ["h3", "Heading 3"], ["ul", "Bulleted list"], ["ol", "Numbered list"],
      ["task", "Task list"], ["blockquote", "Quote"], ["note", "Note"], ["tip", "Tip"], ["warning", "Warning"]];
    fmt.innerHTML = '<div class="rt-tools"><select class="rt-style" aria-label="paragraph style">' +
      styles.map(function (s) { return '<option value="' + s[0] + '">' + s[1] + "</option>"; }).join("") + "</select>" +
      '<span class="rt-sep"></span>' +
      '<button type="button" data-cmd="bold" aria-label="bold" title="Bold (⌘B)"><b>B</b></button>' +
      '<button type="button" data-cmd="italic" aria-label="italic" title="Italic (⌘I)"><i>I</i></button>' +
      '<button type="button" data-cmd="strikeThrough" aria-label="strikethrough" title="Strikethrough"><s>S</s></button>' +
      '<button type="button" data-cmd="code" aria-label="code" title="Code"><svg class="ic" viewBox="0 0 16 16" aria-hidden="true"><path d="M5.5 4 1.5 8l4 4M10.5 4l4 4-4 4" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/></svg></button>' +
      '<button type="button" data-cmd="link" aria-label="link" title="Link (⌘K)">' + icon("i-link") + "</button>" +
      '<button type="button" data-cmd="attach" aria-label="attach a file" title="Attach a file" hidden><svg class="ic" viewBox="0 0 16 16" aria-hidden="true"><path d="M13.5 7.5 8 13a3.5 3.5 0 0 1-5-5l5.8-5.8a2.3 2.3 0 0 1 3.3 3.3L6.3 11.3a1.2 1.2 0 0 1-1.7-1.7L9.8 4.4" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg></button></div>' +
      '<div class="rt-lang"><label>Language <input class="rt-lang-in" placeholder="go, sh, text" list="rt-langs"></label>' +
      '<datalist id="rt-langs"><option value="go"><option value="sh"><option value="js"><option value="sql"><option value="zig"><option value="diff"><option value="text"></datalist></div>' +
      '<div class="rt-linkin"><input class="rt-href" type="url" placeholder="https://" aria-label="link address"><button type="button" class="btn rt-link-ok">Link</button></div>';
    var lp = el("div", { class: "rt-pop rt-linkpop", role: "dialog", "aria-label": "link" });
    lp.innerHTML = '<input class="rt-lp-href" type="url" aria-label="link address"><button type="button" class="btn rt-lp-save">Save</button>' +
      '<a class="rt-lp-open" target="_blank" rel="noopener" aria-label="open the link" title="Open">↗</a>' +
      '<button type="button" class="rt-lp-remove" aria-label="remove the link" title="Remove the link">' + icon("i-x") + "</button>";
    var menu = el("div", { class: "rt-menu", role: "listbox", "aria-label": "suggestions", "data-hook": "rich-menu" });
    var tp = el("div", { class: "rt-pop rt-tpick", role: "dialog", "aria-label": "table size" });
    tp.innerHTML = '<div class="rt-tp-grid"></div><div class="rt-tp-l">Columns × rows</div>';
    [fmt, lp, menu, tp].forEach(function (p) { host.appendChild(p); });
    var styleSel = fmt.querySelector(".rt-style"), href = fmt.querySelector(".rt-href"), langIn = fmt.querySelector(".rt-lang-in");

    // The composer's attachment control, when the reader may upload:
    // its file input is the Attach action, and what it stages is put in
    // the text where the caret was (attach.js tells us).
    var ctl = root.querySelector("[data-hook=attach-composer]");
    var filePick = ctl && ctl.querySelector("[data-hook=attach-choose] input[type=file]");
    if (filePick) {
      fmt.querySelector('[data-cmd="attach"]').hidden = false;
      filePick.addEventListener("change", function () { filePick.removeAttribute("accept"); });
    }

    // ---- the document --------------------------------------------------
    var seq = 0, written = null, lastBlock = null;
    function view(kind, src, html) {
      var d = el("div", { class: kind === "prose" ? "rt-view" : "rt-card", contenteditable: "false" });
      d.setAttribute(kind === "prose" ? "data-md" : "data-src", src);
      if (kind !== "prose") {
        d.setAttribute("data-kind", kind);
        d.innerHTML = '<div class="rt-card-view"></div>';
        d.firstChild.innerHTML = html;
      } else d.innerHTML = html;
      dropNotes(d);
      orSource(d);
      return d;
    }
    // A block renders on its own, so footnotes would list under every
    // block that cites one; the definitions' own card lists them.
    // A card the renderer draws as nothing (an unreferenced footnote,
    // link definitions) shows its markdown, so it can still be found.
    function orSource(d) {
      var v = d.querySelector(".rt-card-view");
      if (v && !v.textContent.trim() && !v.querySelector("img, svg, table")) {
        v.innerHTML = '<pre class="rt-raw"></pre>';
        v.firstChild.textContent = d.getAttribute("data-src").replace(/\n+$/, "");
      }
    }
    function dropNotes(d) {
      if (d.getAttribute("data-kind") === "source" && /^\[\^/.test(d.getAttribute("data-src") || "")) return;
      Array.prototype.forEach.call(d.querySelectorAll(".footnotes"), function (f) { f.remove(); });
    }
    function render(src, done) {
      fetch(blocksURL, {
        method: "POST",
        headers: { "Content-Type": "application/x-www-form-urlencoded" },
        body: "body=" + encodeURIComponent(src)
      }).then(function (r) { return r.ok ? r.json() : Promise.reject(r.status); })
        .then(function (out) { done(out.blocks || []); })
        .catch(function () { done(null); });
    }
    function load(done) {
      var mine = ++seq, src = ta.value;
      // A text Rich did not write (the Markdown tab, a restored draft)
      // is a version of its own.
      if (hist[pos] !== src) { lastAt = 0; remember(src); }
      if (src.trim() === "") { doc.innerHTML = "<p><br></p>"; written = src; tidy(); if (done) done(); return; }
      host.setAttribute("aria-busy", "true");
      render(src, function (blocks) {
        if (mine !== seq) return;
        host.removeAttribute("aria-busy");
        if (!blocks) { doc.textContent = "The rich view could not load; the Markdown tab has your text."; doc.contentEditable = "false"; return; }
        doc.contentEditable = "true";
        doc.textContent = "";
        blocks.forEach(function (b) { doc.appendChild(view(kindOf(b.source), b.source, b.html)); });
        written = src;
        tidy();
        if (done) done();
      });
    }
    // Rendering one block again: its markdown with the draft's link
    // definitions after it, so a reference link still resolves.
    function defs() {
      return Array.prototype.filter.call(doc.querySelectorAll(":scope > .rt-card[data-kind=source]"), function (c) {
        return /^ {0,3}\[[^\]^][^\]]*\]:/.test(c.getAttribute("data-src"));
      }).map(function (c) { return c.getAttribute("data-src").trim(); }).join("\n");
    }
    function renderInto(target, src, inner) {
      var d = defs();
      render(src.replace(/\n+$/, "") + (d ? "\n\n" + d : "") + "\n", function (blocks) {
        if (!blocks || !target.isConnected) return;
        var html = blocks.map(function (b) { return b.html; }).join("");
        if (inner) inner.innerHTML = html; else target.innerHTML = html;
        dropNotes(target);
        orSource(target);
      });
    }

    // Serialising: a view or card is its source; a block in its editing
    // form that nobody changed is its source too; anything else is
    // written from the editing form.
    function intact(n) {
      var g = n._grp;
      if (!g || g.nodes[0] !== n) return false;
      for (var i = 0, x = n; i < g.nodes.length; i++, x = x.nextElementSibling) {
        if (x !== g.nodes[i] || x.outerHTML !== g.snaps[i]) return false;
      }
      return true;
    }
    function serialize() {
      var out = "", gen = false, kids = doc.children;
      for (var i = 0; i < kids.length; i++) {
        var n = kids[i], text;
        if (n.classList.contains("rt-view")) { text = n.getAttribute("data-md"); gen = false; }
        else if (n.classList.contains("rt-card")) { text = n.getAttribute("data-src"); gen = false; }
        else if (intact(n)) { text = n._grp.src; i += n._grp.nodes.length - 1; gen = false; }
        else {
          text = mdNode(n);
          if (text.replace(/​/g, "").trim() === "") continue;
          text += "\n\n";
          gen = true;
        }
        if (out && !/\n\n$/.test(out)) out = out.replace(/\n*$/, "\n\n");
        out += text;
      }
      return gen ? out.replace(/\n*$/, "\n") : out;
    }
    function write() {
      var md = serialize();
      if (md === ta.value) return;
      remember(md);
      written = md;
      ta.value = md;
      ta.dispatchEvent(new Event("input", { bubbles: true }));
    }

    // Undo and redo. The browser's own history cannot follow a document
    // whose blocks are swapped for their renderings, so Rich keeps its
    // own: the markdown after each pause in typing. Undo puts a version
    // back and the caret at the end of the first block it changed.
    var hist = [], pos = 0, lastAt = 0;
    function remember(md) {
      var now = Date.now();
      hist = hist.slice(0, pos + 1);
      if (now - lastAt < 1000 && pos > 0 && hist.length > 1) hist[pos] = md;
      else { hist.push(md); pos = hist.length - 1; }
      if (hist.length > 200) { hist.shift(); pos--; }
      lastAt = now;
    }
    function travel(step) {
      var to = pos + step;
      if (to < 0 || to >= hist.length) return;
      var from = split(serialize()), md = hist[to], dest = split(md), at = 0;
      while (at < from.length && at < dest.length && from[at] === dest[at]) at++;
      pos = to;
      lastAt = 0;
      ta.value = md;
      written = md;
      ta.dispatchEvent(new Event("input", { bubbles: true }));
      lastBlock = null;
      load(function () {
        var kids = doc.children, n = kids[Math.min(at, kids.length - 1)];
        if (n && n.classList.contains("rt-view")) open(n, undefined, undefined, true);
        else if (n && !n.classList.contains("rt-card")) placeIn(n);
        tidy();
      });
    }
    // The Markdown tab or a restored draft changed the text: Rich reads
    // it again, now if it is showing, else when it is next shown.
    ta.addEventListener("input", function () {
      if (ta.value === written) return;
      written = null;
      if (!host.hidden) load();
    });

    // Stepping into a view: it becomes its editing form, the caret where
    // the reader put it.
    function open(v, x, y, atEnd) {
      var src = v.getAttribute("data-md"), box = document.createElement("div");
      box.innerHTML = renderAll(src);
      var nodes = Array.prototype.slice.call(box.children);
      if (!nodes.length) return null;
      var g = { src: src, nodes: nodes, snaps: [], view: v };
      nodes.forEach(function (n) { n._grp = g; });
      v.replaceWith.apply(v, nodes);
      g.snaps = nodes.map(function (n) { return n.outerHTML; });
      var r = x !== undefined && document.caretRangeFromPoint ? document.caretRangeFromPoint(x, y) : null;
      if (r && nodes.some(function (n) { return n.contains(r.startContainer); })) {
        sel().removeAllRanges(); sel().addRange(r); doc.focus();
      } else placeIn(atEnd ? nodes[nodes.length - 1] : nodes[0], !atEnd);
      lastBlock = topOf(blockAt());
      return nodes;
    }
    // Leaving a block in its editing form: unchanged, it is the view it
    // was; changed, it is rendered again from what it now says.
    // force settles even with the caret still inside: a press on another
    // block, before the selection has moved.
    function settle(n, force) {
      n = n && topOf(n);
      if (!n || !n.isConnected || n.nodeType !== 1 || n.classList.contains("rt-view") || n.classList.contains("rt-card")) return;
      var g = n._grp, here = topOf(blockAt());
      if (!force && (here === n || (g && g.nodes.indexOf(here) >= 0))) return;
      if (g && g.nodes.every(function (x) { return x.isConnected; }) && intact(g.nodes[0])) {
        g.nodes[0].replaceWith(g.view);
        g.nodes.slice(1).forEach(function (x) { x.remove(); });
        return;
      }
      if (n.tagName === "P" && n.textContent.replace(/​/g, "") === "" && !n.querySelector("img, .rt-keep")) return;
      var md = mdNode(n);
      if (!md.replace(/​/g, "").trim()) return;
      var v = view("prose", md + "\n\n", "");
      v.innerHTML = n.innerHTML === "" ? "" : n.outerHTML;
      v.querySelectorAll("[contenteditable]").forEach(function (x) { x.removeAttribute("contenteditable"); });
      n.replaceWith(v);
      v.classList.add("rt-settled");
      setTimeout(function () { v.classList.remove("rt-settled"); }, 650);
      renderInto(v, md);
    }
    function watchLeave() {
      var b = topOf(blockAt());
      if (b === lastBlock) return;
      var prev = lastBlock;
      lastBlock = b;
      if (prev) { settle(prev); write(); }
    }
    function tidy() {
      var last = doc.lastElementChild;
      if (!last || last.classList.contains("rt-view") || last.classList.contains("rt-card") || !/^(P|H[1-6])$/.test(last.tagName)) doc.appendChild(el("p")).innerHTML = "<br>";
      var here = blockAt();
      Array.prototype.forEach.call(doc.children, function (n) {
        if (n.tagName === "DIV" && !n.classList.contains("rt-view") && !n.classList.contains("rt-card")) {
          var p = el("p");
          p.innerHTML = n.innerHTML;
          n.replaceWith(p);
          n = p;
        }
        if (n.tagName === "P") {
          var empty = n.textContent.replace(/​/g, "") === "" && !n.querySelector("img, .rt-keep");
          var on = empty && (doc.children.length === 1 || n === here);
          if (on) n.setAttribute("data-hint", hint); else n.removeAttribute("data-hint");
        }
      });
    }
    function after() { tidy(); write(); placeFmt(); }

    // ---- selection helpers ------------------------------------------------
    function sel() { return window.getSelection(); }
    function blockAt() {
      var s = sel();
      if (!s.rangeCount || !s.anchorNode || !doc.contains(s.anchorNode) || s.anchorNode === doc) return null;
      var n = s.anchorNode;
      while (n && n.parentNode !== doc && !(n.parentNode && n.parentNode.tagName === "DETAILS" && n.parentNode.parentNode === doc)) n = n.parentNode;
      return n && n.nodeType === 1 ? n : null;
    }
    function topOf(n) { while (n && n.parentNode && n.parentNode !== doc) n = n.parentNode; return n && n.parentNode === doc ? n : null; }
    function caretRect() {
      var s = sel();
      if (!s.rangeCount) return null;
      var rects = s.getRangeAt(0).getClientRects();
      if (rects.length) return rects[rects.length - 1];
      var b = blockAt();
      return b ? b.getBoundingClientRect() : null;
    }
    function placeIn(n, atStart) {
      var r = document.createRange();
      r.selectNodeContents(n);
      r.collapse(!!atStart);
      sel().removeAllRanges();
      sel().addRange(r);
      doc.focus();
    }
    function placeAfter(node) {
      var r = document.createRange();
      r.setStartAfter(node);
      r.collapse(true);
      sel().removeAllRanges();
      sel().addRange(r);
    }
    function exec(cmd, val) { document.execCommand(cmd, false, val || null); doc.focus(); }
    function insertNode(node) {
      var s = sel();
      if (s.rangeCount && doc.contains(s.anchorNode) && s.anchorNode !== doc) {
        var r = s.getRangeAt(0);
        r.deleteContents();
        r.insertNode(node);
        var gap = document.createTextNode(" ");
        node.after(gap);
        placeAfter(gap);
      } else {
        var p = el("p");
        p.appendChild(node);
        doc.insertBefore(p, doc.lastElementChild);
      }
    }

    // ---- paragraph styles (moved by hand; formatBlock refuses inside input)
    function styleOf(b) {
      if (!b) return "p";
      var t = b.tagName.toLowerCase();
      if (t === "blockquote") return b.getAttribute("data-alert") || "blockquote";
      if (t === "ul" && b.querySelector(":scope > li.task")) return "task";
      // Drawn a level down: an h2 is markdown's #.
      if (/^h[2-4]$/.test(t)) return "h" + (+t[1] - 1);
      return /^(ul|ol)$/.test(t) ? t : "p";
    }
    function setStyle(v) {
      var b = blockAt();
      if (!b || b.tagName === "PRE" || b.tagName === "HR" || b.tagName === "SUMMARY" || b.tagName === "DETAILS") return;
      var items = b.tagName === "UL" || b.tagName === "OL" || (b.tagName === "BLOCKQUOTE" && b.firstElementChild) ? Array.prototype.slice.call(b.children) : [b];
      function move(from, to) {
        Array.prototype.slice.call(from.childNodes).forEach(function (c) {
          if (!(c.nodeType === 1 && (c.tagName === "INPUT" || c.tagName === "UL" || c.tagName === "OL"))) to.appendChild(c);
        });
        if (!to.textContent && !to.querySelector("img, input")) to.innerHTML = "<br>";
        else if (!to.textContent && to.querySelector("input")) to.appendChild(document.createTextNode(ZW));
        return to;
      }
      var made = [], caret = null;
      if (v === "ul" || v === "ol" || v === "task") {
        var list = el(v === "ol" ? "ol" : "ul");
        items.forEach(function (it) {
          var li = el("li");
          if (v === "task") { li.className = "task"; li.appendChild(el("input", { type: "checkbox", contenteditable: "false" })); }
          var old = it.querySelector && it.querySelector(":scope > input[type=checkbox]");
          if (old && v === "task") li.firstChild.checked = old.checked;
          move(it, li);
          list.appendChild(li);
          caret = li;
        });
        made.push(list);
      } else if (v === "blockquote" || ALERTS[v]) {
        var q = el("blockquote");
        if (ALERTS[v]) { q.setAttribute("data-alert", v); q.setAttribute("data-label", ALERTS[v]); }
        items.forEach(function (it) { var p = move(it, el("p")); q.appendChild(p); caret = p; });
        made.push(q);
      } else {
        // Headings by their markdown level, drawn a level down.
        var tag = /^h[1-6]$/.test(v) ? "h" + Math.min(+v[1] + 1, 6) : v;
        items.forEach(function (it) { var n = move(it, el(tag)); made.push(n); caret = n; });
      }
      b.replaceWith.apply(b, made);
      lastBlock = made[made.length - 1];
      if (caret) placeIn(caret);
      after();
    }

    // ---- markdown as you type ------------------------------------------------
    var BLOCK_RULES = [[/^#\s$/, "h1"], [/^##\s$/, "h2"], [/^###\s$/, "h3"], [/^[-*]\s$/, "ul"], [/^1[.)]\s$/, "ol"], [/^\[\s?\]\s$/, "task"], [/^>\s$/, "blockquote"]];
    var INLINE_RULES = [
      [/!\[([^\]]*)\]\((https:\/\/[^)\s]+)\)$/, "img"], [/\[([^\]]+)\]\(([^)\s]+)\)$/, "a"], [/\*\*([^*\s][^*]*)\*\*$/, "strong"],
      [/__([^_\s][^_]*)__$/, "strong"], [/~~([^~]+)~~$/, "s"], [/`([^`]+)`$/, "code"],
      [/(?:^|[^\w*])\*([^*\s][^*]*)\*$/, "em"], [/(?:^|[^\w_])_([^_\s][^_]*)_$/, "em"]
    ];
    function beforeCaret() {
      var s = sel();
      if (!s.rangeCount || !s.isCollapsed) return null;
      var n = s.anchorNode;
      if (!n || n.nodeType !== 3) return null;
      return { node: n, off: s.anchorOffset, text: n.nodeValue.slice(0, s.anchorOffset) };
    }
    function onType(ev) {
      if (ev.inputType !== "insertText") return;
      var b = blockAt();
      if (ev.data === " " && b) {
        var an = sel().anchorNode, li = an && (an.nodeType === 3 ? an.parentNode : an).closest("li");
        if (li && /^\[\s?\]\s$/.test(li.textContent.replace(/ /g, " ")) && !li.classList.contains("task")) {
          li.textContent = "";
          li.classList.add("task");
          li.appendChild(el("input", { type: "checkbox", contenteditable: "false" }));
          li.appendChild(document.createTextNode(ZW));
          placeIn(li);
          return;
        }
        if (b.tagName === "P") {
          var head = b.textContent.replace(/ /g, " ");
          for (var i = 0; i < BLOCK_RULES.length; i++) {
            if (BLOCK_RULES[i][0].test(head)) { b.innerHTML = "<br>"; placeIn(b, true); setStyle(BLOCK_RULES[i][1]); return; }
          }
        }
      }
      var t = beforeCaret();
      if (!t) return;
      for (var k = 0; k < INLINE_RULES.length; k++) {
        var m = INLINE_RULES[k][0].exec(t.text);
        if (!m) continue;
        var kind = INLINE_RULES[k][1], whole = kind === "img" ? m[0] : m[0].replace(/^[^*_~`\[]/, ""), start = t.off - whole.length;
        var r = document.createRange();
        r.setStart(t.node, start);
        r.setEnd(t.node, t.off);
        r.deleteContents();
        var n = el(kind);
        if (kind === "img") { n.alt = m[1]; n.src = m[2]; } else n.textContent = m[1];
        if (kind === "a") n.setAttribute("href", m[2]);
        r.insertNode(n);
        var gap = document.createTextNode(ZW);
        n.after(gap);
        placeAfter(gap);
        return;
      }
    }
    function atEnd(n) {
      var s = sel(), r = document.createRange();
      r.selectNodeContents(n);
      r.setStart(s.anchorNode, s.anchorOffset);
      return r.toString().replace(/​/g, "") === "";
    }
    function atStart(n) {
      var s = sel(), r = document.createRange();
      if (!s.rangeCount || !s.isCollapsed) return false;
      r.selectNodeContents(n);
      r.setEnd(s.anchorNode, s.anchorOffset);
      return r.toString().replace(/​/g, "") === "";
    }
    function onEnter(ev) {
      var b = blockAt();
      if (!b) return;
      if (b.tagName === "SUMMARY") {
        ev.preventDefault();
        var first = b.nextElementSibling;
        if (!first) { first = el("p"); first.innerHTML = "<br>"; b.after(first); }
        placeIn(first, true);
        return;
      }
      var txt = b.textContent.replace(/​/g, "");
      if (b.tagName === "P" && /^```(\w*)$/.test(txt)) {
        ev.preventDefault();
        var pre = el("pre", { "data-lang": RegExp.$1 });
        pre.innerHTML = "<code>" + ZW + "</code>";
        b.replaceWith(pre);
        lastBlock = pre;
        placeIn(pre.firstChild);
        after();
        return;
      }
      if (b.tagName === "BLOCKQUOTE" || b.tagName === "UL" || b.tagName === "OL") {
        var an = sel().anchorNode, line = an && (an.nodeType === 3 ? an.parentNode : an).closest("li, blockquote > p");
        if (line && b.contains(line) && line.textContent.replace(/​/g, "").trim() === "" && !line.querySelector("ul,ol")) {
          var parentList = line.parentNode;
          if (line.tagName === "LI" && parentList !== b) { ev.preventDefault(); outdent(line); after(); return; }
          if (line === b.lastElementChild || line === parentList.lastElementChild) {
            ev.preventDefault();
            line.remove();
            var np = el("p");
            np.innerHTML = "<br>";
            if (!b.children.length) b.replaceWith(np); else b.after(np);
            placeIn(np);
            after();
            return;
          }
        }
        if (line && line.classList && line.classList.contains("task")) {
          // A new task line is a task too.
          ev.preventDefault();
          var li = el("li", { class: "task" });
          li.appendChild(el("input", { type: "checkbox", contenteditable: "false" }));
          li.appendChild(document.createTextNode(ZW));
          line.after(li);
          placeIn(li);
          after();
          return;
        }
      }
      if (b.tagName === "PRE") {
        ev.preventDefault();
        if (/\n​?$/.test(b.textContent) && atEnd(b)) {
          b.firstChild.textContent = b.textContent.replace(/\n​?$/, "");
          var p = el("p");
          p.innerHTML = "<br>";
          b.after(p);
          placeIn(p);
          after();
          return;
        }
        document.execCommand("insertText", false, "\n");
      }
    }
    function liAt() {
      var n = sel().anchorNode;
      return n && doc.contains(n) ? (n.nodeType === 3 ? n.parentNode : n).closest("li") : null;
    }
    function indent(li) {
      var prev = li.previousElementSibling;
      if (!prev) return;
      var tag = li.parentNode.tagName, sub = Array.prototype.find.call(prev.children, function (c) { return c.tagName === tag; });
      if (!sub) { sub = el(tag.toLowerCase()); prev.appendChild(sub); }
      var r = sel().getRangeAt(0).cloneRange();
      sub.appendChild(li);
      sel().removeAllRanges();
      sel().addRange(r);
    }
    function outdent(li) {
      var list = li.parentNode, holder = list.parentNode;
      if (holder.tagName !== "LI") return;
      var r = sel().rangeCount ? sel().getRangeAt(0).cloneRange() : null;
      holder.after(li);
      if (!list.children.length) list.remove();
      if (r) { sel().removeAllRanges(); sel().addRange(r); }
    }

    // ---- the formatting bar -------------------------------------------------
    var saved = null;
    function placeFmt() {
      var s = sel(), inDoc = s.rangeCount && doc.contains(s.anchorNode) && document.activeElement === doc && !host.hidden;
      if (fmt.classList.contains("linking") || fmt.contains(document.activeElement)) return;
      var b = blockAt(), inCode = !!(b && b.tagName === "PRE");
      fmt.classList.toggle("coding", inCode);
      if (inCode) langIn.value = b.getAttribute("data-lang") || "";
      if (touch) {
        fmt.classList.toggle("show", !!inDoc);
        fmt.classList.add("dock");
        var vv = window.visualViewport;
        fmt.style.bottom = (vv ? Math.max(8, innerHeight - vv.height - vv.offsetTop + 8) : 8) + "px";
      } else {
        if (!inDoc || (s.isCollapsed && !inCode)) { fmt.classList.remove("show"); return; }
        var r = inCode ? b.getBoundingClientRect() : s.getRangeAt(0).getBoundingClientRect();
        fmt.classList.add("show");
        var w = fmt.offsetWidth, x = inCode ? r.right - w : Math.min(Math.max(8, r.left + r.width / 2 - w / 2), innerWidth - w - 8), y = r.top - fmt.offsetHeight - 8;
        if (y < 8) y = r.bottom + 8;
        fmt.style.left = Math.max(8, x) + "px";
        fmt.style.top = y + "px";
      }
      styleSel.value = styleOf(b);
      ["bold", "italic", "strikeThrough"].forEach(function (c) {
        var on = false;
        try { on = document.queryCommandState(c); } catch (e) {}
        fmt.querySelector('[data-cmd="' + c + '"]').setAttribute("aria-pressed", on ? "true" : "false");
      });
    }
    function toggleCode() {
      var s = sel();
      if (!s.rangeCount) return;
      var r = s.getRangeAt(0), c = r.commonAncestorContainer, inCode = (c.nodeType === 3 ? c.parentNode : c).closest("code");
      if (inCode && doc.contains(inCode)) { inCode.replaceWith(document.createTextNode(inCode.textContent)); after(); return; }
      if (r.collapsed) return;
      var code = el("code");
      code.textContent = r.toString();
      r.deleteContents();
      r.insertNode(code);
      after();
    }
    function startLink() {
      var s = sel();
      if (!s.rangeCount || s.isCollapsed) return;
      saved = s.getRangeAt(0).cloneRange();
      fmt.classList.add("linking", "show");
      href.value = "";
      href.focus();
    }
    function finishLink(ok) {
      fmt.classList.remove("linking");
      if (saved) { sel().removeAllRanges(); sel().addRange(saved); }
      if (ok && href.value.trim()) exec("createLink", href.value.trim());
      saved = null;
      doc.focus();
      after();
    }
    function pickFile(accept) {
      var s = sel();
      saved = s.rangeCount && doc.contains(s.anchorNode) ? s.getRangeAt(0).cloneRange() : null;
      if (!filePick) return;
      // The control takes any file; Image asks the chooser for pictures.
      if (accept) filePick.setAttribute("accept", accept); else filePick.removeAttribute("accept");
      filePick.click();
    }
    fmt.addEventListener("mousedown", function (ev) { if (ev.target.closest("button") && !ev.target.closest(".rt-linkin")) ev.preventDefault(); });
    fmt.addEventListener("click", function (ev) {
      var b = ev.target.closest("button[data-cmd]");
      if (!b) return;
      var c = b.getAttribute("data-cmd");
      if (c === "code") toggleCode();
      else if (c === "link") startLink();
      else if (c === "attach") pickFile();
      else { exec(c); after(); }
    });
    styleSel.addEventListener("mousedown", function () { var s = sel(); saved = s.rangeCount ? s.getRangeAt(0).cloneRange() : null; });
    styleSel.addEventListener("change", function () {
      if (saved) { sel().removeAllRanges(); sel().addRange(saved); saved = null; }
      setStyle(styleSel.value);
    });
    fmt.querySelector(".rt-link-ok").addEventListener("click", function () { finishLink(true); });
    href.addEventListener("keydown", function (ev) {
      if (ev.key === "Enter") { ev.preventDefault(); finishLink(true); }
      if (ev.key === "Escape") finishLink(false);
    });
    var codeBlock = null;
    langIn.addEventListener("focus", function () { var b = blockAt(); codeBlock = b && b.tagName === "PRE" ? b : codeBlock; });
    langIn.addEventListener("input", function () { if (codeBlock) { codeBlock.setAttribute("data-lang", langIn.value.trim()); write(); } });
    langIn.addEventListener("keydown", function (ev) {
      if (ev.key === "Enter" || ev.key === "Escape") { ev.preventDefault(); if (codeBlock) placeIn(codeBlock.firstChild || codeBlock); }
    });

    // ---- tapping a link: change or remove it ----------------------------------
    var lpHref = lp.querySelector(".rt-lp-href"), lpOpen = lp.querySelector(".rt-lp-open"), lpLink = null;
    function openLinkPop(a) {
      lpLink = a;
      lpHref.value = a.getAttribute("href");
      // Open follows only a web or same-site address: a draft may be
      // somebody else's text, and a javascript: link is not a link.
      var safe = /^(https?:|\/(?!\/))/i.test(a.getAttribute("href") || "");
      lpOpen.hidden = !safe;
      if (safe) lpOpen.href = a.href; else lpOpen.removeAttribute("href");
      lp.classList.add("show");
      var r = a.getBoundingClientRect(), w = lp.offsetWidth;
      lp.style.left = Math.max(8, Math.min(r.left, innerWidth - w - 8)) + "px";
      lp.style.top = (r.bottom + 8 + lp.offsetHeight > innerHeight ? r.top - lp.offsetHeight - 8 : r.bottom + 8) + "px";
    }
    function closeLinkPop() { lp.classList.remove("show"); lpLink = null; }
    lp.querySelector(".rt-lp-save").addEventListener("click", function () {
      if (lpLink && lpHref.value.trim()) lpLink.setAttribute("href", lpHref.value.trim());
      closeLinkPop();
      after();
    });
    lpHref.addEventListener("keydown", function (ev) {
      if (ev.key === "Enter") { ev.preventDefault(); lp.querySelector(".rt-lp-save").click(); }
      if (ev.key === "Escape") closeLinkPop();
    });
    lp.querySelector(".rt-lp-remove").addEventListener("click", function () {
      if (lpLink) lpLink.replaceWith.apply(lpLink, Array.prototype.slice.call(lpLink.childNodes));
      closeLinkPop();
      after();
    });

    // ---- one menu, two uses: "/" inserts, # ! @ [[ link quarry things ----------
    var mItems = [], mIdx = 0, mState = null;
    function emptyHere(b) { if (b && b.tagName === "P") { b.innerHTML = "<br>"; placeIn(b); } }
    var INSERT = [
      ["Heading 1", "#", function () { setStyle("h1"); }], ["Heading 2", "##", function () { setStyle("h2"); }],
      ["Heading 3", "###", function () { setStyle("h3"); }],
      ["Bulleted list", "-", function () { setStyle("ul"); }], ["Numbered list", "1.", function () { setStyle("ol"); }],
      ["Task list", "[ ]", function () { setStyle("task"); }], ["Quote", ">", function () { setStyle("blockquote"); }],
      ["Note", "[!NOTE]", function () { setStyle("note"); }], ["Warning", "[!WARNING]", function () { setStyle("warning"); }],
      ["Code block", "```", function () {
        var b = blockAt(), pre = el("pre", { "data-lang": "" });
        pre.innerHTML = "<code>" + ZW + "</code>";
        b.replaceWith(pre);
        lastBlock = pre;
        placeIn(pre.firstChild);
      }],
      ["Table", "| |", function () { openTablePick(); }],
      ["Diagram", "pikchr", function () { insertCard("diagram", '```pikchr\nbox "one" fit; arrow; box "two" fit\n```'); }],
      ["Poll", "ballot", function () { insertCard("poll", "```poll\nquestion: Your question?\n- option one\n- option two\n```"); }],
      ["Fold", "details", function () { insertFold(); }],
      ["Divider", "---", function () {
        var b = blockAt(), hr = el("hr"), p = el("p");
        p.innerHTML = "<br>";
        b.replaceWith(hr);
        hr.after(p);
        placeIn(p);
      }]
    ];
    if (filePick) {
      INSERT.push(["Image", "picture", function () { pickFile("image/*"); }]);
      INSERT.push(["Attach a file", "any file", function () { pickFile(); }]);
    }
    function insertCard(kind, src) {
      var b = blockAt(), c = view(kind, src + "\n\n", ""), p = el("p");
      p.innerHTML = "<br>";
      if (b) b.replaceWith(c); else doc.insertBefore(c, doc.lastElementChild);
      c.after(p);
      placeIn(p);
      lastBlock = p;
      renderInto(c, src, c.querySelector(".rt-card-view"));
      openCard(c);
    }
    function insertFold() {
      var b = blockAt(), box = el("div");
      box.innerHTML = foldHTML("<details><summary>Summary</summary>\n\nWhat it hides.\n\n</details>");
      var f = box.firstChild;
      if (b) b.replaceWith(f); else doc.insertBefore(f, doc.lastElementChild);
      lastBlock = f;
      var sm = f.querySelector("summary"), r = document.createRange();
      r.selectNodeContents(sm);
      sel().removeAllRanges();
      sel().addRange(r);
      doc.focus();
    }
    // "/ Table": tap out the size, and an empty table of that size opens.
    var tpGrid = tp.querySelector(".rt-tp-grid"), tpL = tp.querySelector(".rt-tp-l"), tpAt = null;
    function markTP(c, r) {
      Array.prototype.forEach.call(tpGrid.children, function (b, i) { b.classList.toggle("on", i % 6 < c && Math.floor(i / 6) < r); });
      tpL.textContent = c + " × " + r;
    }
    for (var ti = 0; ti < 36; ti++) (function (ti) {
      var c = ti % 6 + 1, r = Math.floor(ti / 6) + 1;
      var b = el("button", { type: "button", "aria-label": c + " columns, " + r + " rows" });
      b.addEventListener("pointerenter", function () { markTP(c, r); });
      b.addEventListener("focus", function () { markTP(c, r); });
      b.addEventListener("mousedown", function (ev) { ev.preventDefault(); });
      b.addEventListener("click", function () { makeTable(c, r); });
      tpGrid.appendChild(b);
    })(ti);
    function openTablePick() {
      tpAt = blockAt();
      var r = caretRect();
      markTP(0, 0);
      tpL.textContent = "Columns × rows";
      tp.classList.add("show");
      var x = r ? r.left : 16, y = r ? r.bottom + 6 : 80;
      if (y + tp.offsetHeight > innerHeight - 8 && r) y = Math.max(8, r.top - tp.offsetHeight - 6);
      tp.style.left = Math.max(8, Math.min(x, innerWidth - tp.offsetWidth - 8)) + "px";
      tp.style.top = y + "px";
    }
    function closeTablePick() { tp.classList.remove("show"); }
    function makeTable(c, r) {
      closeTablePick();
      if (tpAt && doc.contains(tpAt)) placeIn(tpAt);
      var hd = [], al = [], empty = [], body = [], i;
      for (i = 0; i < c; i++) { hd.push("Column " + (i + 1)); al.push("---"); empty.push(" "); }
      for (i = 0; i < r; i++) body.push("| " + empty.join(" | ") + " |");
      insertCard("table", ["| " + hd.join(" | ") + " |", "| " + al.join(" | ") + " |"].concat(body).join("\n"));
      after();
    }
    function showMenu(rows, head) {
      menu.innerHTML = head ? '<div class="rt-menu-h">' + esc(head) + "</div>" : "";
      mItems = rows;
      mIdx = 0;
      if (!rows.length) menu.insertAdjacentHTML("beforeend", '<div class="rt-none">' + (mState && mState.busy ? "Searching…" : "No match") + "</div>");
      rows.forEach(function (row, i) {
        var bt = el("button", { type: "button", role: "option" });
        bt.innerHTML = row.html;
        bt.addEventListener("mousedown", function (ev) { ev.preventDefault(); choose(i); });
        menu.appendChild(bt);
      });
      var r = caretRect();
      if (!r) return;
      menu.classList.add("show");
      var x = Math.min(r.left, innerWidth - menu.offsetWidth - 8), y = r.bottom + 6;
      if (y + menu.offsetHeight > (window.visualViewport ? visualViewport.height : innerHeight) - (touch ? 60 : 8)) y = Math.max(8, r.top - menu.offsetHeight - 6);
      menu.style.left = Math.max(8, x) + "px";
      menu.style.top = y + "px";
      markMenu();
    }
    function markMenu() { Array.prototype.forEach.call(menu.querySelectorAll("button"), function (b, i) { b.classList.toggle("sel", i === mIdx); }); }
    function closeMenu() { menu.classList.remove("show"); mState = null; }
    function choose(i) {
      var row = mItems[i];
      if (!row) return;
      var st = mState;
      closeMenu();
      row.go(st);
      after();
    }
    function openSlash() {
      mState = { kind: "/" };
      showMenu(INSERT.map(function (it) {
        return { html: esc(it[0]) + "<small>" + esc(it[1]) + "</small>", go: function () { emptyHere(blockAt()); it[2](); } };
      }), "Insert");
    }
    // The pickers: after #, !, @ or [[, quarry is asked what that could
    // name here, and the choice goes in as the reference, drawn as it
    // will read.
    var KINDS = { "#": ["ticket", "Tickets"], "!": ["landing", "Landings"], "@": ["person", "People"], "[[": ["wiki", "Wiki pages"] };
    var askTimer = null, askSeq = 0;
    function refMenu() {
      var t = beforeCaret();
      if (!t) { closeMenu(); return; }
      var st = mState, q = t.text.slice(st.start + st.trig.length);
      if (st.node !== t.node || t.off < st.start + st.trig.length || /[\s\]]/.test(q) || q.length > 40) { closeMenu(); return; }
      st.q = q;
      clearTimeout(askTimer);
      askTimer = setTimeout(function () {
        var mine = ++askSeq;
        fetch(suggestURL + "?k=" + KINDS[st.trig][0] + "&q=" + encodeURIComponent(q), { headers: { Accept: "application/json" } })
          .then(function (r) { return r.ok ? r.json() : { picks: [] }; })
          .catch(function () { return { picks: [] }; })
          .then(function (out) {
            if (mine !== askSeq || mState !== st) return;
            st.busy = false;
            showMenu((out.picks || []).map(function (x) {
              return { html: '<span class="k">' + esc(x.tok.replace(/^\[\[|\]\]$/g, "")) + "</span>" + esc(x.text || ""), go: function (s) {
                var r = document.createRange();
                r.setStart(s.node, s.start);
                r.setEnd(s.node, Math.min(s.node.nodeValue.length, s.start + s.trig.length + s.q.length));
                r.deleteContents();
                var box = el("span");
                box.innerHTML = refHTML(x.tok);
                var chip = box.firstChild;
                r.insertNode(chip);
                var sp = document.createTextNode(" ");
                chip.after(sp);
                placeAfter(sp);
              } };
            }), KINDS[st.trig][1]);
          });
      }, 120);
    }
    function maybeOpenRef(ev) {
      var t = beforeCaret();
      if (!t || ev.inputType !== "insertText" || !suggestURL) return false;
      var trig = /\[\[$/.test(t.text) ? "[[" : /(^|[\s(])[#!@]$/.test(t.text) ? t.text.slice(-1) : null;
      if (!trig) return false;
      mState = { kind: "ref", trig: trig, node: t.node, start: t.off - trig.length, busy: true };
      showMenu([], KINDS[trig][1]);
      refMenu();
      return true;
    }

    // ---- cards ----------------------------------------------------------------
    function openCard(c) {
      if (c.classList.contains("open")) return;
      c.classList.add("open");
      var kind = c.getAttribute("data-kind"), box = el("div", { class: "rt-card-src" }), foot = el("div", { class: "rt-card-foot" });
      var viewEl = c.querySelector(".rt-card-view"), timer = null;
      function update(src) {
        c.setAttribute("data-src", src + "\n\n");
        write();
        clearTimeout(timer);
        timer = setTimeout(function () { renderInto(c, src, viewEl); }, 250);
      }
      var first = null, cur = c.getAttribute("data-src");
      if (kind === "poll") {
        var d = pollOf(cur);
        var q = el("input", { class: "rt-in", "aria-label": "question", placeholder: "Question" });
        q.value = d.q;
        var list = el("div", { class: "rt-opts" }), add = el("button", { type: "button", class: "btn" }, "+ Option");
        var closes = el("input", { type: "date", class: "rt-in", "aria-label": "closes" });
        closes.value = /^\d{4}-\d\d-\d\d$/.test(d.closes) ? d.closes : "";
        var drawOpts = function () {
          list.textContent = "";
          d.opts.forEach(function (o, i) {
            var row = el("div", { class: "rt-row" }), inp = el("input", { class: "rt-in", "aria-label": "option " + (i + 1) });
            var x = el("button", { type: "button", class: "rt-x", "aria-label": "remove option " + (i + 1) }, "×");
            inp.value = o;
            inp.addEventListener("input", function () { d.opts[i] = inp.value; update(pollSrc(d)); });
            inp.addEventListener("keydown", function (ev) {
              if (ev.key === "Enter") { ev.preventDefault(); d.opts.splice(i + 1, 0, ""); drawOpts(); update(pollSrc(d)); list.querySelectorAll("input")[i + 1].focus(); }
            });
            x.addEventListener("click", function () { if (d.opts.length > 1) { d.opts.splice(i, 1); drawOpts(); update(pollSrc(d)); } });
            row.appendChild(inp);
            row.appendChild(x);
            list.appendChild(row);
          });
        };
        drawOpts();
        q.addEventListener("input", function () { d.q = q.value; update(pollSrc(d)); });
        closes.addEventListener("input", function () { d.closes = closes.value; update(pollSrc(d)); });
        add.addEventListener("click", function () { d.opts.push(""); drawOpts(); update(pollSrc(d)); list.lastElementChild.querySelector("input").focus(); });
        var cl = el("label", { class: "rt-l" }, "Closes ");
        cl.appendChild(closes);
        var bar = el("div", { class: "rt-bar" });
        bar.appendChild(add);
        bar.appendChild(cl);
        box.appendChild(q);
        box.appendChild(list);
        box.appendChild(bar);
        foot.appendChild(el("span", {}, "Readers vote once it is posted"));
        first = q;
      } else if (kind === "table") {
        var t = tableOf(cur), grid = el("div", { class: "rt-grid" });
        var drawGrid = function () {
          grid.textContent = "";
          var n = Math.max.apply(null, t.rows.map(function (r) { return r.length; }));
          t.rows.forEach(function (r) { while (r.length < n) r.push(""); });
          var tbl = el("table", { class: "rt-table" }), hx = el("tr", { class: "rt-colx" });
          for (var j = 0; j < n; j++) (function (j) {
            var td = el("td"), x = el("button", { type: "button", class: "rt-x", "aria-label": "remove column " + (j + 1) }, "×");
            x.addEventListener("click", function () {
              if (n > 1) { t.rows.forEach(function (r) { r.splice(j, 1); }); t.align.splice(j, 1); drawGrid(); update(tableSrc(t)); }
            });
            td.appendChild(x);
            hx.appendChild(td);
          })(j);
          hx.appendChild(el("td"));
          tbl.appendChild(hx);
          t.rows.forEach(function (r, i) {
            var tr = el("tr");
            r.forEach(function (cell, j) {
              var td = el(i ? "td" : "th"), inp = el("input", { class: "rt-cell", "aria-label": (i ? "row " + i : "header") + ", column " + (j + 1) });
              inp.value = cell.replace(/\\\|/g, "|");
              inp.addEventListener("input", function () { t.rows[i][j] = inp.value.replace(/\|/g, "\\|"); update(tableSrc(t)); });
              if (i === t.rows.length - 1 && j === r.length - 1) {
                inp.addEventListener("keydown", function (ev) {
                  if (ev.key === "Tab" && !ev.shiftKey) {
                    ev.preventDefault();
                    t.rows.push(t.rows[0].map(function () { return ""; }));
                    drawGrid();
                    update(tableSrc(t));
                    var ins = grid.querySelectorAll("input");
                    ins[ins.length - r.length].focus();
                  }
                });
              }
              td.appendChild(inp);
              tr.appendChild(td);
            });
            var tx = el("td");
            if (i) {
              var x = el("button", { type: "button", class: "rt-x", "aria-label": "remove row " + i }, "×");
              x.addEventListener("click", function () { t.rows.splice(i, 1); drawGrid(); update(tableSrc(t)); });
              tx.appendChild(x);
            }
            tr.appendChild(tx);
            tbl.appendChild(tr);
          });
          grid.appendChild(tbl);
        };
        drawGrid();
        var ar = el("button", { type: "button", class: "btn" }, "+ Row"), ac = el("button", { type: "button", class: "btn" }, "+ Column");
        ar.addEventListener("click", function () { t.rows.push(t.rows[0].map(function () { return ""; })); drawGrid(); update(tableSrc(t)); });
        ac.addEventListener("click", function () { t.rows.forEach(function (r, i) { r.push(i ? "" : "Column"); }); drawGrid(); update(tableSrc(t)); });
        var tb = el("div", { class: "rt-bar" });
        tb.appendChild(ar);
        tb.appendChild(ac);
        box.appendChild(grid);
        box.appendChild(tb);
        foot.appendChild(el("span", {}, "Tab moves between cells; Tab in the last cell adds a row"));
        first = grid.querySelector("input");
      } else {
        var src = el("textarea", { class: "rt-srcbox", "aria-label": "this block's markdown", spellcheck: "false" });
        src.value = cur.replace(/\n+$/, "");
        box.appendChild(src);
        var size = function () { src.style.height = "auto"; src.style.height = src.scrollHeight + 2 + "px"; };
        foot.appendChild(el("span", {}, kind === "diagram" ? "pikchr, drawn as you type" : "Markdown, drawn as you type"));
        if (kind === "diagram") {
          var studio = el("a", { class: "btn", target: "_blank", rel: "noopener", href: STUDIO, "data-hook": "rich-studio" }, "Edit in pikchr-studio ↗");
          var relink = function () { studioLink(fenceBody(src.value), function (u) { studio.href = u; }); };
          relink();
          foot.appendChild(studio);
          src.addEventListener("input", relink);
        }
        src.addEventListener("input", function () { size(); update(src.value); });
        setTimeout(size, 0);
        first = src;
      }
      var done = el("button", { type: "button", class: "btn-primary rt-done", "data-hook": "rich-done" }, "Done");
      done.addEventListener("click", function () { closeCard(c); });
      foot.appendChild(done);
      box.appendChild(foot);
      c.insertBefore(box, viewEl);
      box.addEventListener("keydown", function (ev) { if (ev.key === "Escape") { ev.preventDefault(); closeCard(c); } });
      setTimeout(function () { if (first) first.focus(); }, 0);
    }
    function closeCard(c) {
      c.classList.remove("open");
      var s = c.querySelector(".rt-card-src");
      if (s) s.remove();
      // An emptied card is no block at all.
      if (!c.getAttribute("data-src").trim()) c.remove();
      write();
    }

    // ---- paste and drop ---------------------------------------------------------
    var KEEP = { P: 1, H2: 1, H3: 1, H4: 1, UL: 1, OL: 1, LI: 1, BLOCKQUOTE: 1, PRE: 1, CODE: 1, STRONG: 1, B: 1, EM: 1, I: 1, S: 1, DEL: 1, A: 1, BR: 1 };
    function clean(node) {
      var out = document.createDocumentFragment();
      node.childNodes.forEach(function (n) {
        if (n.nodeType === 3) { out.appendChild(document.createTextNode(n.nodeValue)); return; }
        if (n.nodeType !== 1 || /^(SCRIPT|STYLE|META|LINK|TITLE|IMG)$/.test(n.tagName)) return;
        var t = /^(DIV|SECTION|ARTICLE)$/.test(n.tagName) ? "P" : /^H[1-6]$/.test(n.tagName) ? "H" + Math.min(+n.tagName[1] + 1, 4) : n.tagName;
        if (!KEEP[t]) { out.appendChild(clean(n)); return; }
        var e = el(t);
        if (t === "A" && /^https?:/.test(n.getAttribute("href") || "")) e.setAttribute("href", n.getAttribute("href"));
        e.appendChild(clean(n));
        out.appendChild(e);
      });
      return out;
    }
    doc.addEventListener("paste", function (ev) {
      var cd = ev.clipboardData;
      if (!cd) return;
      // Files are attach.js's: it stages them and tells us their names.
      if (cd.files && cd.files.length) { if (!ctl) ev.preventDefault(); return; }
      var text = cd.getData("text/plain"), html = cd.getData("text/html"), s = sel();
      ev.preventDefault();
      if (/^https?:\/\/\S+$/.test(text.trim()) && s.rangeCount && !s.isCollapsed) { exec("createLink", text.trim()); after(); return; }
      if (html) {
        var box = el("div"), tmp = el("div");
        box.innerHTML = html;
        tmp.appendChild(clean(box));
        document.execCommand("insertHTML", false, tmp.innerHTML);
      } else document.execCommand("insertText", false, text);
      after();
    });
    doc.addEventListener("drop", function (ev) {
      if (!ev.dataTransfer || !ev.dataTransfer.files || !ev.dataTransfer.files.length) return;
      if (document.caretRangeFromPoint) {
        var r = document.caretRangeFromPoint(ev.clientX, ev.clientY);
        if (r && doc.contains(r.startContainer)) { sel().removeAllRanges(); sel().addRange(r); }
      }
    });
    // A file staged by the attachment control goes in where the caret
    // was: an image as itself, anything else as a link to it.
    root.addEventListener("attach-staged", function (ev) {
      if (host.hidden) return;
      var d = ev.detail || {}, ref = "attach:" + encodeURI(d.name).replace(/[()]/g, function (c) { return "%" + c.charCodeAt(0).toString(16).toUpperCase(); });
      if (saved && doc.contains(saved.startContainer)) { sel().removeAllRanges(); sel().addRange(saved); saved = null; }
      var n;
      if (d.image && d.url) {
        staged[ref] = d.url;
        n = el("img", { alt: d.name.replace(/\.[^.]+$/, ""), src: d.url, "data-upload": ref });
      } else {
        n = el("a", { href: ref });
        n.textContent = d.name;
      }
      insertNode(n);
      after();
    });

    // ---- wiring -------------------------------------------------------------------
    doc.addEventListener("mousedown", function (ev) {
      var v = ev.target.closest(".rt-view");
      if (v && v.parentNode === doc) {
        ev.preventDefault();
        var x = ev.clientX, y = ev.clientY;
        settle(lastBlock, true);
        if (v.isConnected) open(v, x, y);
        after();
        return;
      }
      var c = ev.target.closest(".rt-card");
      if (c && !ev.target.closest(".rt-card-src")) { ev.preventDefault(); openCard(c); }
    });
    doc.addEventListener("click", function (ev) {
      if (ev.target.closest(".rt-card, .rt-view")) { if (!ev.target.closest(".rt-card-src")) ev.preventDefault(); return; }
      if (ev.target.closest("summary") && doc.contains(ev.target)) { ev.preventDefault(); return; }
      var box = ev.target.closest("input[type=checkbox]");
      if (box && doc.contains(box)) { setTimeout(function () { box.toggleAttribute("checked", box.checked); after(); }, 0); return; }
      var a = ev.target.closest("a");
      if (a && doc.contains(a)) { ev.preventDefault(); if (!a.hasAttribute("data-tok")) openLinkPop(a); }
    });
    // Undo from the Edit menu or a gesture arrives as beforeinput.
    doc.addEventListener("beforeinput", function (ev) {
      if (ev.inputType === "historyUndo" || ev.inputType === "historyRedo") {
        ev.preventDefault();
        write();
        travel(ev.inputType === "historyUndo" ? -1 : 1);
      }
    });
    doc.addEventListener("input", function (ev) {
      onType(ev);
      var b = blockAt();
      if (ev.inputType === "insertText" && ev.data === "/" && b && b.tagName === "P" && b.textContent.replace(/​/g, "") === "/") openSlash();
      else if (mState && mState.kind === "ref") refMenu();
      else if (!maybeOpenRef(ev) && mState) closeMenu();
      after();
    });
    // The caret beside a view or a card: Backspace and Delete open the
    // view instead of deleting it whole, and step over a card.
    function neighbour(back) {
      var b = blockAt();
      if (!b || b.parentNode !== doc) return null;
      if (back ? !atStart(b) : !atEnd(b)) return null;
      var n = back ? b.previousElementSibling : b.nextElementSibling;
      return n && (n.classList.contains("rt-view") || n.classList.contains("rt-card")) ? n : null;
    }
    doc.addEventListener("keydown", function (ev) {
      var mod = ev.metaKey || ev.ctrlKey;
      if (menu.classList.contains("show")) {
        if (ev.key === "ArrowDown") { ev.preventDefault(); mIdx = (mIdx + 1) % Math.max(1, mItems.length); markMenu(); return; }
        if (ev.key === "ArrowUp") { ev.preventDefault(); mIdx = (mIdx + mItems.length - 1) % Math.max(1, mItems.length); markMenu(); return; }
        if ((ev.key === "Enter" || ev.key === "Tab") && mItems.length) { ev.preventDefault(); choose(mIdx); return; }
        if (ev.key === "Escape") { closeMenu(); return; }
      }
      if (mod && ev.key.toLowerCase() === "b") { ev.preventDefault(); exec("bold"); after(); return; }
      if (mod && ev.key.toLowerCase() === "i") { ev.preventDefault(); exec("italic"); after(); return; }
      if (mod && ev.key.toLowerCase() === "k") { ev.preventDefault(); startLink(); return; }
      if (mod && ev.key.toLowerCase() === "z") { ev.preventDefault(); write(); travel(ev.shiftKey ? 1 : -1); return; }
      if (mod && ev.key.toLowerCase() === "y") { ev.preventDefault(); write(); travel(1); return; }
      if (ev.key === "Backspace" || ev.key === "Delete" || ev.key === "ArrowUp" || ev.key === "ArrowDown") {
        var back = ev.key === "Backspace" || ev.key === "ArrowUp", nb = neighbour(back);
        if (nb) {
          ev.preventDefault();
          // The block the caret leaves settles once the caret is out.
          if (nb.classList.contains("rt-view")) { var from = topOf(blockAt()); open(nb, undefined, undefined, back); lastBlock = from; watchLeave(); after(); }
          else if (ev.key[0] === "A") { var past = back ? nb.previousElementSibling : nb.nextElementSibling; if (past && !past.classList.contains("rt-view") && !past.classList.contains("rt-card")) placeIn(past, !back); }
          return;
        }
      }
      if (ev.key === "Tab") {
        var li = liAt();
        if (li) { ev.preventDefault(); if (ev.shiftKey) outdent(li); else indent(li); after(); }
        return;
      }
      if (ev.key === "Enter" && ev.shiftKey) { ev.preventDefault(); document.execCommand("insertLineBreak"); return; }
      if (ev.key === "Enter") onEnter(ev);
    });
    document.addEventListener("selectionchange", function () {
      if (host.hidden || fmt.contains(document.activeElement)) return;
      var s = sel();
      // The caret parked beside a view (arrowing past one) opens it.
      if (s.rangeCount && s.anchorNode === doc) {
        var v = doc.childNodes[s.anchorOffset] || doc.childNodes[s.anchorOffset - 1];
        if (v && v.classList && v.classList.contains("rt-view")) { open(v); after(); return; }
      }
      watchLeave();
      placeFmt();
    });
    doc.addEventListener("blur", function () {
      setTimeout(function () {
        if (fmt.contains(document.activeElement) || lp.contains(document.activeElement)) return;
        if (!fmt.classList.contains("linking")) fmt.classList.remove("show");
        if (lastBlock && document.activeElement !== doc) {
          var prev = lastBlock;
          lastBlock = null;
          settle(prev, true);
          write();
        }
      }, 150);
    });
    window.addEventListener("resize", placeFmt);
    if (window.visualViewport) { visualViewport.addEventListener("resize", placeFmt); visualViewport.addEventListener("scroll", placeFmt); }
    document.addEventListener("mousedown", function (ev) {
      if (!menu.contains(ev.target)) closeMenu();
      if (!lp.contains(ev.target) && !(ev.target.closest && ev.target.closest(".rt-doc a"))) closeLinkPop();
      if (tp.classList.contains("show") && !tp.contains(ev.target) && !menu.contains(ev.target)) closeTablePick();
    });
    document.addEventListener("keydown", function (ev) { if (ev.key === "Escape" && tp.classList.contains("show")) closeTablePick(); });
    function hide() { fmt.classList.remove("show"); closeMenu(); closeLinkPop(); closeTablePick(); }
    return {
      // Shown: read the textarea again unless it is what Rich wrote.
      show: function () { if (written === null || written !== ta.value) load(); },
      hide: hide
    };
  }

  document.querySelectorAll(".f-composer").forEach(function (root) {
    var ta = root.querySelector("textarea[data-slash]");
    var tabs = root.querySelector(".f-tabs");
    var split = root.querySelector(".f-split");
    var richPane = root.querySelector(".f-rich");
    if (!ta) return;
    // Rich is the default; a reader who picks Markdown keeps it on every
    // composer, remembered in this browser only. Without script neither
    // tab exists and the composer is the textarea.
    if (tabs && split && richPane) {
      var rich = richEditor(ta, richPane, root);
      var show = function (which, user) {
        tabs.querySelectorAll("[data-tab]").forEach(function (b) {
          b.classList.toggle("here", b.getAttribute("data-tab") === which);
        });
        split.hidden = which !== "write";
        richPane.hidden = which !== "rich";
        if (user) {
          try { localStorage.setItem("quarry.composer.tab", which); } catch (e) {}
        }
        if (which === "rich") rich.show();
        else {
          rich.hide();
          if (user) ta.focus();
        }
      };
      tabs.hidden = false;
      tabs.addEventListener("click", function (ev) {
        var btn = ev.target.closest("[data-tab]");
        if (btn) show(btn.getAttribute("data-tab"), true);
      });
      var first = "rich";
      try { if (localStorage.getItem("quarry.composer.tab") === "write") first = "write"; } catch (e) {}
      show(first, false);
    }
  });

  // One press, one post. A composer's submit button stayed live while
  // the request was in flight, so a slow page took a second tap as a
  // second comment — two identical posts, seconds apart, is what that
  // looks like afterwards. The button is disabled for the life of the
  // navigation and put back if the page is restored from the back /
  // forward cache, which is the one case where the form is still on
  // screen and still wants to work.
  document.addEventListener("submit", function (ev) {
    var form = ev.target;
    if (!form || form.method.toLowerCase() !== "post" || ev.defaultPrevented) return;
    var btns = form.querySelectorAll("button[type=submit], button:not([type]), input[type=submit]");
    // Disabled after the event has been dispatched, so the pressed
    // button's own name and value still reach the server.
    setTimeout(function () {
      Array.prototype.forEach.call(btns, function (b) { b.disabled = true; });
    }, 0);
  });
  window.addEventListener("pageshow", function (ev) {
    if (!ev.persisted) return;
    document.querySelectorAll("button[disabled], input[type=submit][disabled]").forEach(function (b) {
      b.disabled = false;
    });
  });
})();