"use strict";

/* global window, document, MutationObserver, Element, HTMLElement, HTMLTextAreaElement, InputEvent, Node */

(() => {
  const VERSION = 1;
  const MAXIMUM_UTF16_UNITS = 2_000_000;
  const MINIMUM_SPLIT_RATIO = 0.3;
  const MAXIMUM_SPLIT_RATIO = 0.7;
  const CONTENT_THEME_PATH = "vendor/vditor/dist/css/content-theme";
  const sessionID = window.__KITO_SESSION_ID__;
  const mount = document.querySelector("#vditor");
  const status = document.querySelector("#bridge-status");
  const bridge = window.webkit?.messageHandlers?.kitoBridge;
  const externalLinkBridge =
    window.webkit?.messageHandlers?.kitoExternalLink;
  const clipboardBridge =
    window.webkit?.messageHandlers?.kitoClipboard;
  const colorScheme = window.matchMedia?.("(prefers-color-scheme: dark)");
  const plainFallbackLanguages = new Set([
    "abc",
    "echarts",
    "flowchart",
    "graphviz",
    "markmap",
    "math",
    "mermaid",
    "mindmap",
    "plantuml",
    "smiles",
  ]);
  const formatControlSelectors = Object.freeze({
    bold: '.vditor-toolbar button[data-type="bold"]',
    italic: '.vditor-toolbar button[data-type="italic"]',
    strikethrough: '.vditor-toolbar button[data-type="strike"]',
    link: '.vditor-toolbar button[data-type="link"]',
    heading1: '.vditor-toolbar button[data-tag="h1"]',
    heading2: '.vditor-toolbar button[data-tag="h2"]',
    heading3: '.vditor-toolbar button[data-tag="h3"]',
    heading4: '.vditor-toolbar button[data-tag="h4"]',
    heading5: '.vditor-toolbar button[data-tag="h5"]',
    heading6: '.vditor-toolbar button[data-tag="h6"]',
    bulletList: '.vditor-toolbar button[data-type="list"]',
    numberedList: '.vditor-toolbar button[data-type="ordered-list"]',
    taskList: '.vditor-toolbar button[data-type="check"]',
    blockquote: '.vditor-toolbar button[data-type="quote"]',
    horizontalRule: '.vditor-toolbar button[data-type="line"]',
    inlineCode: '.vditor-toolbar button[data-type="inline-code"]',
    codeBlock: '.vditor-toolbar button[data-type="code"]',
    table: '.vditor-toolbar button[data-type="table"]',
  });
  const formatTemplateValues = Object.freeze({
    image: "![Image description](image-url)",
  });
  const selectedBlockCommands = new Set([
    "bulletList",
    "numberedList",
    "taskList",
    "blockquote",
    "heading1",
    "heading2",
    "heading3",
    "heading4",
    "heading5",
    "heading6",
  ]);
  const selectedMarkdownCommands = new Set([
    "bold",
    "italic",
    "strikethrough",
    "link",
    "inlineCode",
    "codeBlock",
    "table",
    "image",
  ]);

  let editorPort = null;
  let epoch = 0;
  let hydrated = false;
  let pendingHydrationRevision = null;
  let tailRevision = 0;
  let tailMarkdown = "";
  let acknowledgedRevision = 0;
  let acknowledgedMarkdown = "";
  const pendingChanges = new Map();
  let appearance = "system";
  let viewMode = "split";
  let splitRatio = 0.5;
  let splitResizer = null;
  let viewModeBeforePrint = null;
  let lastEditorSelection = null;
  let isComposing = false;
  let suppressEditorInput = false;
  let pendingEditorInput = null;

  function editorElement() {
    return mount?.querySelector(".vditor-sv") ?? null;
  }

  function editorContains(editor, node) {
    return editor === node || editor.contains(node);
  }

  function captureEditorSelection() {
    const editor = editorElement();
    const selection = window.getSelection();
    if (!editor || !selection || selection.rangeCount === 0) return;

    const range = selection.getRangeAt(0);
    if (
      editorContains(editor, range.startContainer) &&
      editorContains(editor, range.endContainer)
    ) {
      lastEditorSelection = range.cloneRange();
    }
  }

  function restoreEditorSelection() {
    const editor = editorElement();
    const selection = window.getSelection();
    if (
      !editor ||
      !selection ||
      !lastEditorSelection ||
      !lastEditorSelection.startContainer.isConnected ||
      !lastEditorSelection.endContainer.isConnected ||
      !editorContains(editor, lastEditorSelection.startContainer) ||
      !editorContains(editor, lastEditorSelection.endContainer)
    ) {
      lastEditorSelection = null;
      return false;
    }

    selection.removeAllRanges();
    selection.addRange(lastEditorSelection.cloneRange());
    return true;
  }

  document.addEventListener("selectionchange", captureEditorSelection);

  function setStatus(label, state) {
    if (!status) return;
    status.textContent = label;
    status.dataset.state = state;
  }

  function post(message) {
    if (!bridge) {
      setStatus("Bridge unavailable", "error");
      return Promise.reject(new Error("bridgeUnavailable"));
    }
    return bridge.postMessage(message);
  }

  function isValidSplitRatio(value) {
    return (
      Number.isFinite(value) &&
      value >= MINIMUM_SPLIT_RATIO &&
      value <= MAXIMUM_SPLIT_RATIO
    );
  }

  function clampedSplitRatio(value) {
    return Math.min(MAXIMUM_SPLIT_RATIO, Math.max(MINIMUM_SPLIT_RATIO, value));
  }

  function applyLayout(requestedViewMode, requestedSplitRatio) {
    if (
      !["editor", "split", "reader"].includes(requestedViewMode) ||
      !isValidSplitRatio(requestedSplitRatio)
    ) {
      throw new Error("invalidLayout");
    }

    viewMode = requestedViewMode;
    splitRatio = requestedSplitRatio;
    document.documentElement.dataset.viewMode = viewMode;
    document.documentElement.style.setProperty(
      "--editor-split",
      `${splitRatio * 100}%`,
    );
    if (splitResizer) {
      splitResizer.setAttribute(
        "aria-valuenow",
        String(Math.round(splitRatio * 100)),
      );
      splitResizer.tabIndex = viewMode === "split" ? 0 : -1;
      splitResizer.setAttribute("aria-hidden", String(viewMode !== "split"));
    }
  }

  function persistSplitRatio() {
    post({
      version: VERSION,
      kind: "splitRatioChanged",
      sessionID,
      splitRatio,
    })
      .then((reply) => {
        if (
          !reply ||
          Object.keys(reply).join(",") !== "accepted" ||
          reply.accepted !== true
        ) {
          throw new Error("splitRatioRejected");
        }
      })
      .catch(() => setStatus("Unable to save divider position", "error"));
  }

  function installSplitResizer() {
    const content = mount.querySelector(".vditor-content");
    const reader = mount.querySelector(".vditor-preview");
    if (!content || !reader || splitResizer) return;

    splitResizer = document.createElement("div");
    splitResizer.className = "kito-split-resizer";
    splitResizer.setAttribute("role", "separator");
    splitResizer.setAttribute("aria-label", "Resize Editor and Reader");
    splitResizer.setAttribute("aria-orientation", "vertical");
    splitResizer.setAttribute("aria-valuemin", "30");
    splitResizer.setAttribute("aria-valuemax", "70");
    content.insertBefore(splitResizer, reader);

    let dragging = false;
    let dragStartRatio = splitRatio;
    const updateFromPointer = (event) => {
      if (!dragging) return;
      const bounds = content.getBoundingClientRect();
      if (bounds.width <= 0) return;
      applyLayout(
        "split",
        clampedSplitRatio((event.clientX - bounds.left) / bounds.width),
      );
    };

    splitResizer.addEventListener("pointerdown", (event) => {
      if (event.button !== 0 || viewMode !== "split") return;
      dragging = true;
      dragStartRatio = splitRatio;
      splitResizer.dataset.dragging = "true";
      splitResizer.setPointerCapture?.(event.pointerId);
      updateFromPointer(event);
      event.preventDefault();
    });
    splitResizer.addEventListener("pointermove", updateFromPointer);
    splitResizer.addEventListener("pointerup", (event) => {
      if (!dragging) return;
      updateFromPointer(event);
      dragging = false;
      delete splitResizer.dataset.dragging;
      splitResizer.releasePointerCapture?.(event.pointerId);
      persistSplitRatio();
    });
    splitResizer.addEventListener("pointercancel", () => {
      if (dragging) applyLayout("split", dragStartRatio);
      dragging = false;
      delete splitResizer.dataset.dragging;
    });
    splitResizer.addEventListener("dblclick", () => {
      applyLayout("split", 0.5);
      persistSplitRatio();
    });
    splitResizer.addEventListener("keydown", (event) => {
      const step = event.shiftKey ? 0.05 : 0.01;
      let nextRatio = splitRatio;
      if (event.key === "ArrowLeft") nextRatio -= step;
      else if (event.key === "ArrowRight") nextRatio += step;
      else if (event.key === "Home") nextRatio = MINIMUM_SPLIT_RATIO;
      else if (event.key === "End") nextRatio = MAXIMUM_SPLIT_RATIO;
      else if (event.key === "Enter") nextRatio = 0.5;
      else return;

      event.preventDefault();
      applyLayout("split", clampedSplitRatio(nextRatio));
      persistSplitRatio();
    });
    applyLayout(viewMode, splitRatio);
  }

  function resolvedTheme(requestedAppearance) {
    const useDark =
      requestedAppearance === "dark" ||
      (requestedAppearance === "system" && colorScheme?.matches === true);
    return {
      content: useDark ? "dark" : "light",
      editor: useDark ? "dark" : "classic",
    };
  }

  function createEditorPort(editor) {
    return Object.freeze({
      getMarkdown() {
        return editor.getValue();
      },

      replaceMarkdown(markdown) {
        editor.setValue(markdown, true);
      },

      replaceDocumentMarkdown(markdown) {
        editor.setValue(markdown, true);
        reportContentChange(markdown);
      },

      setEnabled(enabled) {
        if (enabled) {
          editor.enable();
        } else {
          editor.disabled();
        }
        mount.dataset.bridgeEnabled = String(enabled);
        mount.setAttribute("aria-busy", String(!enabled));
      },

      focus() {
        editor.focus();
      },

      resetHistory() {
        editor.clearStack();
      },

      renderReader() {
        editor.renderPreview();
      },

      setTheme(requestedAppearance) {
        const theme = resolvedTheme(requestedAppearance);
        document.documentElement.dataset.appearance = requestedAppearance;
        editor.setTheme(
          theme.editor,
          theme.content,
          undefined,
          CONTENT_THEME_PATH,
        );
      },

      insertMarkdown(markdown) {
        editor.insertValue(markdown);
      },

      replaceSelectedMarkdown(markdown) {
        editor.updateValue(
          markdown
            .replaceAll("&", "&amp;")
            .replaceAll("<", "&lt;")
            .replaceAll(">", "&gt;"),
        );
      },

      replaceSelectedMarkdownExactly(markdown, exactDocumentMarkdown) {
        const editable = editorElement();
        const selection = window.getSelection();
        if (!editable || !selection || selection.rangeCount === 0) {
          throw new Error("selectionUnavailable");
        }
        const range = selection.getRangeAt(0);
        if (
          !editorContains(editable, range.startContainer) ||
          !editorContains(editable, range.endContainer)
        ) {
          throw new Error("selectionUnavailable");
        }

        range.deleteContents();
        const replacement = document.createTextNode(markdown);
        range.insertNode(replacement);
        range.setStartAfter(replacement);
        range.collapse(true);
        selection.removeAllRanges();
        selection.addRange(range);
        lastEditorSelection = range.cloneRange();

        reportContentChange(exactDocumentMarkdown);
        suppressEditorInput = true;
        try {
          editable.dispatchEvent(
            new InputEvent("input", {
              bubbles: true,
              data: markdown,
              inputType: "insertText",
            }),
          );
        } finally {
          suppressEditorInput = false;
        }
        editor.setValue(exactDocumentMarkdown, true);
      },
    });
  }

  function matchesBlockMarker(line, command) {
    const content = line.trimStart();
    if (command.startsWith("heading")) {
      const level = command.slice("heading".length);
      return new RegExp(`^#{${level}}\\s+`).test(content);
    }
    if (command === "numberedList") return /^\d+\.\s+/.test(content);
    if (command === "taskList") return /^[-+*]\s+\[[ xX]\]\s+/.test(content);
    if (command === "bulletList") {
      return /^[-+*]\s+(?!\[[ xX]\]\s+)/.test(content);
    }
    return /^>\s+/.test(content);
  }

  function activeBlockMarkerPattern(command) {
    if (command.startsWith("heading")) {
      return new RegExp(`^#{${command.slice("heading".length)}}\\s+`);
    }
    if (command === "numberedList") return /^\d+\.\s+/;
    if (command === "taskList") return /^[-+*]\s+\[[ xX]\]\s+/;
    if (command === "bulletList") return /^[-+*]\s+/;
    return /^>\s+/;
  }

  function removeBlockMarker(line) {
    const indentation = line.match(/^\s*/)?.[0] ?? "";
    const content = line.slice(indentation.length).replace(
      /^(?:#{1,6}\s+|[-+*]\s+(?:\[[ xX]\]\s+)?|\d+\.\s+|>\s+)/,
      "",
    );
    return { content, indentation };
  }

  function transformSelectedBlocks(markdown, command) {
    const lines = markdown.split("\n");
    const contentLines = lines.filter((line) => line.trim() !== "");
    if (
      command === "taskList" &&
      contentLines.length > 0 &&
      contentLines.every((line) => matchesBlockMarker(line, command))
    ) {
      const shouldComplete = !contentLines.every((line) =>
        /^\s*[-+*]\s+\[[xX]\]\s+/.test(line),
      );
      return lines
        .map((line) =>
          line.replace(
            /^(\s*[-+*]\s+\[)[ xX](\]\s+)/,
            `$1${shouldComplete ? "x" : " "}$2`,
          ),
        )
        .join("\n");
    }
    const shouldRemove =
      contentLines.length > 0 &&
      contentLines.every((line) => matchesBlockMarker(line, command));
    let listNumber = 0;

    return lines
      .map((line) => {
        if (line.trim() === "") return line;
        if (shouldRemove) {
          const indentation = line.match(/^\s*/)?.[0] ?? "";
          const content = line
            .slice(indentation.length)
            .replace(activeBlockMarkerPattern(command), "");
          return indentation + content;
        }

        const { content, indentation } = removeBlockMarker(line);
        let marker = "* ";
        if (command.startsWith("heading")) {
          marker = `${"#".repeat(Number(command.slice("heading".length)))} `;
        } else if (command === "numberedList") marker = `${++listNumber}. `;
        else if (command === "taskList") marker = "* [ ] ";
        else if (command === "blockquote") marker = "> ";
        return indentation + marker + content;
      })
      .join("\n");
  }

  function toggleTaskAtOffset(markdown, offset) {
    const lineStart = markdown.lastIndexOf("\n", Math.max(0, offset - 1)) + 1;
    const nextLineBreak = markdown.indexOf("\n", offset);
    const lineEnd = nextLineBreak === -1 ? markdown.length : nextLineBreak;
    const line = markdown.slice(lineStart, lineEnd);
    const toggled = line.replace(
      /^(\s*[-+*]\s+\[)([ xX])(\]\s+)/,
      (_, prefix, state, suffix) =>
        `${prefix}${state.toLowerCase() === "x" ? " " : "x"}${suffix}`,
    );
    if (toggled === line) return null;
    return markdown.slice(0, lineStart) + toggled + markdown.slice(lineEnd);
  }

  function splitTrailingNewlines(markdown) {
    const match = markdown.match(/(?:\r?\n)+$/);
    const trailingNewlines = match?.[0] ?? "";
    return {
      content: trailingNewlines
        ? markdown.slice(0, -trailingNewlines.length)
        : markdown,
      lineBreak: markdown.includes("\r\n") ? "\r\n" : "\n",
      trailingNewlines,
    };
  }

  function sourceMarkdownForSelection(selectedMarkdown) {
    if (
      /(?:\r?\n)$/.test(selectedMarkdown) &&
      selectedMarkdown.replace(/(?:\r?\n)+$/, "") ===
        tailMarkdown.replace(/(?:\r?\n)+$/, "")
    ) {
      return tailMarkdown;
    }
    return selectedMarkdown;
  }

  function normalizedLineEndings(markdown) {
    return markdown.replaceAll("\r\n", "\n");
  }

  function sourceOffsetForNormalizedOffset(markdown, normalizedOffset) {
    let sourceOffset = 0;
    let currentNormalizedOffset = 0;
    while (
      sourceOffset < markdown.length &&
      currentNormalizedOffset < normalizedOffset
    ) {
      sourceOffset += markdown.startsWith("\r\n", sourceOffset) ? 2 : 1;
      currentNormalizedOffset += 1;
    }
    return sourceOffset;
  }

  function sourceSelectionSnapshot() {
    const editor = editorElement();
    const selection = window.getSelection();
    if (!editor || !selection || selection.rangeCount === 0) return null;

    const range = selection.getRangeAt(0);
    if (
      !editorContains(editor, range.startContainer) ||
      !editorContains(editor, range.endContainer)
    ) {
      return null;
    }

    const fullRange = document.createRange();
    fullRange.selectNodeContents(editor);
    const fullText = normalizedLineEndings(fullRange.toString());
    const normalizedTailMarkdown = normalizedLineEndings(tailMarkdown);
    if (
      fullText.replace(/(?:\r?\n)+$/, "") !==
      normalizedTailMarkdown.replace(/(?:\r?\n)+$/, "")
    ) {
      return null;
    }

    const prefixRange = document.createRange();
    prefixRange.selectNodeContents(editor);
    prefixRange.setEnd(range.startContainer, range.startOffset);
    const rawPrefixLength = normalizedLineEndings(prefixRange.toString()).length;
    const normalizedSelectionLength = normalizedLineEndings(
      range.toString(),
    ).length;
    const normalizedStart = Math.min(
      rawPrefixLength,
      normalizedTailMarkdown.length,
    );
    const reachesDisplayEnd =
      rawPrefixLength + normalizedSelectionLength >= fullText.length;
    const normalizedEnd = reachesDisplayEnd
      ? normalizedTailMarkdown.length
      : Math.min(
          normalizedStart + normalizedSelectionLength,
          normalizedTailMarkdown.length,
        );
    const start = sourceOffsetForNormalizedOffset(
      tailMarkdown,
      normalizedStart,
    );
    const end = sourceOffsetForNormalizedOffset(tailMarkdown, normalizedEnd);

    return {
      start,
      end,
      markdown: tailMarkdown.slice(start, end),
    };
  }

  function toggleEachSelectedLine(
    content,
    lineBreak,
    isFormatted,
    unwrap,
    wrap,
  ) {
    const lines = content.split(/\r?\n/);
    const nonEmptyLines = lines.filter((line) => line !== "");
    const shouldRemove =
      nonEmptyLines.length > 0 && nonEmptyLines.every(isFormatted);
    return lines
      .map((line) => {
        if (line === "") return line;
        if (shouldRemove) return unwrap(line);
        return isFormatted(line) ? line : wrap(line);
      })
      .join(lineBreak);
  }

  function hasMarker(line, marker) {
    return (
      line.length >= marker.length * 2 &&
      line.startsWith(marker) &&
      line.endsWith(marker)
    );
  }

  function isItalic(line) {
    return hasMarker(line, "*") && !hasMarker(line, "**");
  }

  function linkMatch(line) {
    return line.match(/^\[((?:\\.|[^\]])*)\]\([^)]*\)$/);
  }

  function unescapedLabel(value) {
    return value.replace(/\\(\\|\[|\])/g, "$1");
  }

  function inlineCodeMatch(line) {
    return line.match(/^(`+)([\s\S]*)\1$/);
  }

  function escapedLabel(value) {
    return value
      .replaceAll("\\", "\\\\")
      .replaceAll("[", "\\[")
      .replaceAll("]", "\\]");
  }

  function codeFenceFor(content, minimumLength) {
    const matches = content.match(/`+/g) ?? [];
    const longestRun = matches.reduce(
      (longest, value) => Math.max(longest, value.length),
      0,
    );
    return "`".repeat(Math.max(minimumLength, longestRun + 1));
  }

  function compactSelectedLabel(content) {
    return content.replace(/\s+/g, " ").trim();
  }

  function transformSelectedMarkdown(markdown, command) {
    const { content, lineBreak, trailingNewlines } =
      splitTrailingNewlines(markdown);
    if (content === "") return markdown;

    let transformed;
    switch (command) {
      case "bold":
        transformed = toggleEachSelectedLine(
          content,
          lineBreak,
          (line) => hasMarker(line, "**"),
          (line) => line.slice(2, -2),
          (line) => `**${line}**`,
        );
        break;
      case "italic":
        transformed = toggleEachSelectedLine(
          content,
          lineBreak,
          isItalic,
          (line) => line.slice(1, -1),
          (line) => `*${line}*`,
        );
        break;
      case "strikethrough":
        transformed = toggleEachSelectedLine(
          content,
          lineBreak,
          (line) => hasMarker(line, "~~"),
          (line) => line.slice(2, -2),
          (line) => `~~${line}~~`,
        );
        break;
      case "link":
        transformed = toggleEachSelectedLine(
          content,
          lineBreak,
          (line) => linkMatch(line) !== null,
          (line) => unescapedLabel(linkMatch(line)?.[1] ?? line),
          (line) => `[${escapedLabel(line)}](https://)`,
        );
        break;
      case "inlineCode":
        transformed = toggleEachSelectedLine(
          content,
          lineBreak,
          (line) => inlineCodeMatch(line) !== null,
          (line) => inlineCodeMatch(line)?.[2] ?? line,
          (line) => {
            const fence = codeFenceFor(line, 1);
            return `${fence}${line}${fence}`;
          },
        );
        break;
      case "codeBlock": {
        const lines = content.split(/\r?\n/);
        const openingFence = lines[0]?.match(/^`{3,}$/)?.[0];
        if (
          openingFence &&
          lines.length >= 2 &&
          lines.at(-1) === openingFence
        ) {
          transformed = lines.slice(1, -1).join(lineBreak);
          break;
        }
        const fence = codeFenceFor(content, 3);
        transformed = `${fence}${lineBreak}${content}${lineBreak}${fence}`;
        break;
      }
      case "table": {
        const cell = compactSelectedLabel(content).replaceAll("|", "\\|");
        transformed =
          `| Column 1 | Column 2 | Column 3 |${lineBreak}` +
          `| --- | --- | --- |${lineBreak}` +
          `| ${cell} |  |  |`;
        break;
      }
      case "image":
        transformed = `![${escapedLabel(compactSelectedLabel(content))}](image-url)`;
        break;
      default:
        throw new Error("invalidSelectedFormat");
    }
    return transformed + trailingNewlines;
  }

  function enhanceAccessibility() {
    mount.setAttribute("aria-label", "Markdown editor and reader");

    const editor = mount.querySelector(".vditor-sv");
    if (editor) {
      editor.setAttribute("role", "textbox");
      editor.setAttribute("aria-label", "Markdown editor");
      editor.setAttribute("aria-multiline", "true");
    }

    const reader = mount.querySelector(".vditor-preview");
    if (reader) {
      reader.setAttribute("role", "region");
      reader.setAttribute("aria-label", "Markdown reader");
    }
  }

  function renderPlainFallbacks(element) {
    element.querySelectorAll('[class*="language-"]').forEach((candidate) => {
      const languageClass = [...candidate.classList].find((className) =>
        className.startsWith("language-"),
      );
      const language = languageClass?.slice("language-".length);
      if (!language || !plainFallbackLanguages.has(language)) return;

      candidate.classList.remove(languageClass);
      candidate.classList.add("language-plaintext", "kito-plain-fallback");
      candidate.dataset.kitoLanguage = language;
    });
  }

  function scopedLocalImageURL(source) {
    if (!source || source.startsWith("/") || /^[a-z][a-z0-9+.-]*:/i.test(source)) {
      return null;
    }
    const path = source.startsWith("./") ? source.slice(2) : source;
    const components = path.split("/");
    if (
      components.length < 2 ||
      !components[0].endsWith(".assets") ||
      components.some((component) => !component || component === "." || component === "..")
    ) {
      return null;
    }
    try {
      const encodedPath = components
        .map((component) => encodeURIComponent(decodeURIComponent(component)))
        .join("/");
      return `kito-image:///${encodedPath}`;
    } catch {
      return null;
    }
  }

  function trimBareLink(candidate) {
    let link = candidate;
    let suffix = "";
    while (/[.,!?;:]$/.test(link)) {
      suffix = link.slice(-1) + suffix;
      link = link.slice(0, -1);
    }
    for (const [opening, closing] of [
      ["(", ")"],
      ["[", "]"],
      ["{", "}"],
    ]) {
      const openingCount = link.split(opening).length - 1;
      let closingCount = link.split(closing).length - 1;
      while (link.endsWith(closing) && closingCount > openingCount) {
        suffix = closing + suffix;
        link = link.slice(0, -1);
        closingCount -= 1;
      }
    }
    return { link, suffix };
  }

  function enhanceBareLinks(element) {
    const walker = document.createTreeWalker(
      element,
      window.NodeFilter.SHOW_TEXT,
    );
    const textNodes = [];
    while (walker.nextNode()) textNodes.push(walker.currentNode);

    for (const textNode of textNodes) {
      if (
        textNode.parentElement?.closest(
          "a, code, pre, kbd, samp, script, style, textarea",
        )
      ) {
        continue;
      }
      const text = textNode.nodeValue ?? "";
      const matches = [
        ...text.matchAll(/\b(?:https?:\/\/|www\.)[^\s<]+/giu),
      ];
      if (matches.length === 0) continue;

      const fragment = document.createDocumentFragment();
      let cursor = 0;
      for (const match of matches) {
        const matchIndex = match.index ?? 0;
        const { link, suffix } = trimBareLink(match[0]);
        if (link.length === 0) continue;
        fragment.append(text.slice(cursor, matchIndex));
        const anchor = document.createElement("a");
        anchor.href = link.startsWith("www.") ? `http://${link}` : link;
        anchor.textContent = link;
        anchor.dataset.kitoAutolink = "true";
        fragment.append(anchor, suffix);
        cursor = matchIndex + match[0].length;
      }
      fragment.append(text.slice(cursor));
      textNode.replaceWith(fragment);
    }
  }

  function enhanceAdjacentTextEmphasis(element) {
    const walker = document.createTreeWalker(
      element,
      window.NodeFilter.SHOW_TEXT,
    );
    const textNodes = [];
    while (walker.nextNode()) textNodes.push(walker.currentNode);

    for (const textNode of textNodes) {
      if (
        textNode.parentElement?.closest(
          "a, code, pre, kbd, samp, script, style, textarea, strong, em, del",
        )
      ) {
        continue;
      }
      const text = textNode.nodeValue ?? "";
      const matches = [
        ...text.matchAll(
          /(?<![*_~])(\*\*\*|___|\*\*|__|~~|\*|_)(?![*_~])(?=\S)([^\r\n]*?\p{P})\1(?=[\p{L}\p{M}\p{N}])/gu,
        ),
      ];
      if (matches.length === 0) continue;

      const fragment = document.createDocumentFragment();
      let cursor = 0;
      for (const match of matches) {
        const matchIndex = match.index ?? 0;
        const marker = match[1];
        const content = match[2] ?? "";
        fragment.append(text.slice(cursor, matchIndex));

        let formatted;
        if (marker === "***" || marker === "___") {
          formatted = document.createElement("strong");
          const emphasis = document.createElement("em");
          emphasis.textContent = content;
          formatted.append(emphasis);
        } else if (marker === "**" || marker === "__") {
          formatted = document.createElement("strong");
          formatted.textContent = content;
        } else if (marker === "~~") {
          formatted = document.createElement("del");
          formatted.textContent = content;
        } else {
          formatted = document.createElement("em");
          formatted.textContent = content;
        }
        fragment.append(formatted);
        cursor = matchIndex + match[0].length;
      }
      fragment.append(text.slice(cursor));
      textNode.replaceWith(fragment);
    }
  }

  function renderPreviewContent(element) {
    renderPlainFallbacks(element);
    enhanceAdjacentTextEmphasis(element);
    enhanceBareLinks(element);
    element.querySelectorAll("img[src]").forEach((image) => {
      const localURL = scopedLocalImageURL(image.getAttribute("src"));
      if (localURL) image.setAttribute("src", localURL);
    });
  }

  function removeInlineHandlers(element) {
    const candidates = [element, ...element.querySelectorAll("*")];
    for (const candidate of candidates) {
      for (const attribute of [...candidate.attributes]) {
        if (attribute.name.toLowerCase().startsWith("on")) {
          candidate.removeAttribute(attribute.name);
        }
      }
    }
  }

  function selectedReaderText() {
    const reader = mount.querySelector(".vditor-preview");
    const selection = window.getSelection();
    if (
      !reader ||
      !selection ||
      selection.isCollapsed ||
      selection.rangeCount === 0
    ) {
      return null;
    }

    const range = selection.getRangeAt(0);
    if (
      !reader.contains(range.startContainer) ||
      !reader.contains(range.endContainer)
    ) {
      return null;
    }
    const text = selection.toString();
    return text.length > 0 ? text : null;
  }

  function normalizedHeadingFragment(value) {
    return value
      .normalize("NFC")
      .trim()
      .toLocaleLowerCase("en-US")
      .replace(/[^\p{Letter}\p{Mark}\p{Number}\s_-]/gu, "")
      .replace(/[\s-]+/g, "-")
      .replace(/^-+|-+$/g, "");
  }

  function readerFragmentTarget(reader, encodedTargetID) {
    let targetID;
    try {
      targetID = decodeURIComponent(encodedTargetID);
    } catch {
      targetID = encodedTargetID;
    }

    const exactTarget = document.getElementById(targetID);
    if (exactTarget && reader.contains(exactTarget)) return exactTarget;

    const requestedSlug = normalizedHeadingFragment(targetID);
    if (!requestedSlug) return null;
    const slugCounts = new Map();
    const headings = reader.querySelectorAll("h1, h2, h3, h4, h5, h6");
    for (const heading of headings) {
      const baseSlug = normalizedHeadingFragment(heading.textContent ?? "");
      if (!baseSlug) continue;
      const duplicateIndex = slugCounts.get(baseSlug) ?? 0;
      slugCounts.set(baseSlug, duplicateIndex + 1);
      const headingSlug =
        duplicateIndex === 0 ? baseSlug : `${baseSlug}-${duplicateIndex}`;
      if (headingSlug === requestedSlug) return heading;
    }
    return null;
  }

  function installVendorGuards() {
    const observer = new MutationObserver((records) => {
      for (const record of records) {
        for (const node of record.addedNodes) {
          if (node instanceof Element) removeInlineHandlers(node);
        }
      }
    });
    observer.observe(mount, { childList: true, subtree: true });

    document.addEventListener(
      "copy",
      (event) => {
        const text = selectedReaderText();
        if (text === null) return;

        event.preventDefault();
        event.stopImmediatePropagation();
        if (!clipboardBridge) {
          setStatus("Unable to copy", "error");
          return;
        }
        clipboardBridge
          .postMessage({ text })
          .then((result) => {
            if (result?.copied !== true) {
              throw new Error("copyFailed");
            }
          })
          .catch(() => setStatus("Unable to copy", "error"));
      },
      true,
    );

    mount.addEventListener(
      "click",
      (event) => {
        const link =
          event.target instanceof Element
            ? event.target.closest(".vditor-preview a[href]")
            : null;
        if (!(link instanceof window.HTMLAnchorElement)) return;
        const rawHref = link.getAttribute("href")?.trim();
        if (rawHref?.startsWith("#")) {
          const reader = link.closest(".vditor-preview");
          event.preventDefault();
          event.stopImmediatePropagation();
          if (!(reader instanceof HTMLElement) || rawHref.length === 1) return;
          readerFragmentTarget(reader, rawHref.slice(1))?.scrollIntoView({
            behavior: "auto",
            block: "start",
          });
          return;
        }
        let destination;
        try {
          destination = new window.URL(link.href);
        } catch {
          return;
        }
        if (!["http:", "https:"].includes(destination.protocol)) return;

        event.preventDefault();
        event.stopImmediatePropagation();
        if (!externalLinkBridge) {
          setStatus("Unable to open link", "error");
          return;
        }
        externalLinkBridge
          .postMessage({ url: destination.href })
          .catch(() => setStatus("Unable to open link", "error"));
      },
      true,
    );

    mount.addEventListener(
      "click",
      (event) => {
        const trigger =
          event.target instanceof Element
            ? event.target.closest(".vditor-copy > span")
            : null;
        const source = trigger?.previousElementSibling;
        if (!(source instanceof HTMLTextAreaElement)) return;

        event.preventDefault();
        event.stopImmediatePropagation();
        source.select();
        if (!clipboardBridge) {
          source.blur();
          setStatus("Unable to copy", "error");
          return;
        }
        clipboardBridge
          .postMessage({ text: source.value })
          .then((result) => {
            if (result?.copied !== true) {
              throw new Error("copyFailed");
            }
            trigger.setAttribute(
              "aria-label",
              window.VditorI18n?.copied ?? "Copied",
            );
          })
          .catch(() => setStatus("Unable to copy", "error"));
        source.blur();
      },
      true,
    );
  }

  function isHighSurrogate(codeUnit) {
    return codeUnit >= 0xd800 && codeUnit <= 0xdbff;
  }

  function isLowSurrogate(codeUnit) {
    return codeUnit >= 0xdc00 && codeUnit <= 0xdfff;
  }

  function isWellFormedUTF16(value) {
    if (typeof String.prototype.isWellFormed === "function") {
      return String.prototype.isWellFormed.call(value);
    }
    for (let index = 0; index < value.length; index += 1) {
      const codeUnit = value.charCodeAt(index);
      if (isHighSurrogate(codeUnit)) {
        if (
          index + 1 >= value.length ||
          !isLowSurrogate(value.charCodeAt(index + 1))
        ) {
          return false;
        }
        index += 1;
      } else if (isLowSurrogate(codeUnit)) {
        return false;
      }
    }
    return true;
  }

  function splitsSurrogatePair(value, index) {
    return (
      index > 0 &&
      index < value.length &&
      isHighSurrogate(value.charCodeAt(index - 1)) &&
      isLowSurrogate(value.charCodeAt(index))
    );
  }

  function makeTextChange(previous, next) {
    let startUTF16 = 0;
    const prefixLimit = Math.min(previous.length, next.length);
    while (
      startUTF16 < prefixLimit &&
      previous.charCodeAt(startUTF16) === next.charCodeAt(startUTF16)
    ) {
      startUTF16 += 1;
    }
    if (
      splitsSurrogatePair(previous, startUTF16) ||
      splitsSurrogatePair(next, startUTF16)
    ) {
      startUTF16 -= 1;
    }

    let previousEnd = previous.length;
    let nextEnd = next.length;
    const suffixChunkSize = 8_192;
    while (
      previousEnd - startUTF16 >= suffixChunkSize &&
      nextEnd - startUTF16 >= suffixChunkSize
    ) {
      const previousChunkStart = previousEnd - suffixChunkSize;
      const nextChunkStart = nextEnd - suffixChunkSize;
      if (
        previous.slice(previousChunkStart, previousEnd) !==
        next.slice(nextChunkStart, nextEnd)
      ) {
        break;
      }
      previousEnd = previousChunkStart;
      nextEnd = nextChunkStart;
    }
    while (
      previousEnd > startUTF16 &&
      nextEnd > startUTF16 &&
      previous.charCodeAt(previousEnd - 1) === next.charCodeAt(nextEnd - 1)
    ) {
      previousEnd -= 1;
      nextEnd -= 1;
    }
    if (
      splitsSurrogatePair(previous, previousEnd) ||
      splitsSurrogatePair(next, nextEnd)
    ) {
      previousEnd += 1;
      nextEnd += 1;
    }

    return Object.freeze({
      deleteCountUTF16: previousEnd - startUTF16,
      insertedText: next.slice(startUTF16, nextEnd),
      resultUTF16Length: next.length,
      startUTF16,
    });
  }

  function applyTextChange(markdown, change) {
    const deletionEnd = change.startUTF16 + change.deleteCountUTF16;
    if (
      deletionEnd > markdown.length ||
      splitsSurrogatePair(markdown, change.startUTF16) ||
      splitsSurrogatePair(markdown, deletionEnd)
    ) {
      throw new Error("invalidPendingChange");
    }
    const result =
      markdown.slice(0, change.startUTF16) +
      change.insertedText +
      markdown.slice(deletionEnd);
    if (result.length !== change.resultUTF16Length) {
      throw new Error("invalidPendingResult");
    }
    return result;
  }

  function isAcceptedTextChangeReply(reply, expectedRevision) {
    if (!reply || typeof reply !== "object") return false;
    const keys = Object.keys(reply).sort();
    return (
      keys.join(",") === "acceptedRevision,kind,sessionID,version" &&
      reply.version === VERSION &&
      reply.kind === "textChangeAccepted" &&
      reply.sessionID === sessionID &&
      reply.acceptedRevision === expectedRevision
    );
  }

  function drainAcceptedChanges() {
    while (true) {
      const nextRevision = acknowledgedRevision + 1;
      const entry = pendingChanges.get(nextRevision);
      if (!entry?.accepted) break;

      acknowledgedMarkdown = applyTextChange(
        acknowledgedMarkdown,
        entry.change,
      );
      acknowledgedRevision = nextRevision;
      pendingChanges.delete(nextRevision);
    }

    if (acknowledgedRevision === tailRevision && pendingChanges.size === 0) {
      editorPort.resetHistory();
      setStatus("Synced to Swift", "ready");
    }
  }

  function rejectTextChange(outgoingEpoch) {
    if (outgoingEpoch !== epoch || !editorPort) return;
    epoch += 1;
    pendingChanges.clear();
    tailRevision = acknowledgedRevision;
    tailMarkdown = acknowledgedMarkdown;
    editorPort.replaceMarkdown(acknowledgedMarkdown);
    hydrated = false;
    editorPort.setEnabled(false);
    setStatus("Bridge rejected edit", "error");
  }

  function canonicalMarkdownForPendingInput(markdown) {
    if (pendingEditorInput?.allowsTrailingLineBreakIncrease) return markdown;

    const current = splitTrailingNewlines(tailMarkdown);
    const next = splitTrailingNewlines(markdown);
    const currentTrailingCount = normalizedLineEndings(
      current.trailingNewlines,
    ).length;
    const nextTrailingCount = normalizedLineEndings(next.trailingNewlines).length;
    if (nextTrailingCount <= currentTrailingCount) return markdown;
    return next.content + current.trailingNewlines;
  }

  function reportContentChange(markdown) {
    if (!hydrated || !editorPort) return;
    if (isComposing || suppressEditorInput) return;
    markdown = canonicalMarkdownForPendingInput(markdown);
    if (markdown.length > MAXIMUM_UTF16_UNITS || !isWellFormedUTF16(markdown)) {
      editorPort.replaceMarkdown(tailMarkdown);
      setStatus("Edit is too large", "error");
      return;
    }
    if (markdown === tailMarkdown) return;

    const change = makeTextChange(tailMarkdown, markdown);
    const baseRevision = tailRevision;
    const outgoingRevision = baseRevision + 1;
    if (!Number.isSafeInteger(outgoingRevision)) {
      rejectTextChange(epoch);
      return;
    }
    const outgoingEpoch = epoch;
    tailRevision = outgoingRevision;
    tailMarkdown = markdown;
    pendingChanges.set(outgoingRevision, {
      accepted: false,
      change,
      epoch: outgoingEpoch,
    });
    setStatus("Syncing…", "pending");
    post({
      version: VERSION,
      kind: "textChange",
      sessionID,
      baseRevision,
      revision: outgoingRevision,
      ...change,
    })
      .then((reply) => {
        if (outgoingEpoch !== epoch) return;
        if (!isAcceptedTextChangeReply(reply, outgoingRevision)) {
          throw new Error("unexpectedAcknowledgement");
        }
        const entry = pendingChanges.get(outgoingRevision);
        if (!entry || entry.epoch !== outgoingEpoch) return;
        entry.accepted = true;
        drainAcceptedChanges();
      })
      .catch(() => rejectTextChange(outgoingEpoch));
  }

  function forwardCommand(command) {
    if (!hydrated || !editorPort) return;
    pendingEditorInput = null;
    const hadPendingChanges = pendingChanges.size > 0;
    const commandEpoch = epoch + 1;
    epoch = commandEpoch;
    pendingChanges.clear();
    hydrated = false;
    editorPort.setEnabled(false);
    setStatus(command === "undo" ? "Undoing…" : "Redoing…", "pending");
    post({
      version: VERSION,
      kind: "command",
      sessionID,
      command,
    })
      .then((reply) => {
        if (commandEpoch !== epoch) return;
        if (reply?.accepted === false) {
          if (hadPendingChanges) throw new Error("pendingChangeWasNotUndoable");
          hydrated = true;
          editorPort.setEnabled(true);
          setStatus(
            command === "undo" ? "Nothing to undo" : "Nothing to redo",
            "ready",
          );
          editorPort.focus();
          return;
        }
        if (reply?.accepted !== true) {
          throw new Error("commandRejected");
        }
      })
      .catch(() => {
        if (commandEpoch !== epoch) return;
        hydrated = false;
        editorPort.setEnabled(false);
        setStatus("Native undo unavailable", "error");
      });
  }

  function isEditorEvent(event) {
    return Boolean(
      mount && event.target instanceof Node && mount.contains(event.target),
    );
  }

  document.addEventListener(
    "keydown",
    (event) => {
      if (!isEditorEvent(event) || !event.metaKey) return;
      const key = event.key.toLowerCase();
      const digit = event.code.match(/^Digit([1-8])$/)?.[1] ?? key;
      let formatCommand = null;
      if (event.altKey && /^[1-6]$/.test(digit)) {
        formatCommand = `heading${digit}`;
      } else if (event.shiftKey && digit === "7") {
        formatCommand = "numberedList";
      } else if (event.shiftKey && digit === "8") {
        formatCommand = "bulletList";
      }
      if (formatCommand) {
        event.preventDefault();
        event.stopImmediatePropagation();
        window.kitoNative.performCommand(formatCommand);
        return;
      }

      const command = key === "z" ? (event.shiftKey ? "redo" : "undo") : null;
      const normalizedCommand = key === "y" ? "redo" : command;
      if (!normalizedCommand) return;

      event.preventDefault();
      event.stopImmediatePropagation();
      forwardCommand(normalizedCommand);
    },
    true,
  );

  document.addEventListener(
    "compositionstart",
    (event) => {
      if (!isEditorEvent(event)) return;
      isComposing = true;
    },
    true,
  );

  document.addEventListener(
    "compositionend",
    (event) => {
      if (!isEditorEvent(event) || !isComposing) return;
      isComposing = false;
      Promise.resolve().then(() => {
        const markdown = editorPort?.getMarkdown();
        if (typeof markdown === "string") reportContentChange(markdown);
      });
    },
    true,
  );

  document.addEventListener(
    "beforeinput",
    (event) => {
      if (!isEditorEvent(event)) return;
      const data = typeof event.data === "string" ? event.data : "";
      const mayChangeLineStructure =
        ["insertParagraph", "insertLineBreak", "insertFromPaste", "insertFromDrop"].includes(
          event.inputType,
        ) ||
        event.inputType.startsWith("delete") ||
        data.includes("\n");
      pendingEditorInput = {
        allowsTrailingLineBreakIncrease: mayChangeLineStructure,
      };
      if (event.inputType !== "historyUndo" && event.inputType !== "historyRedo") {
        return;
      }

      event.preventDefault();
      event.stopImmediatePropagation();
      forwardCommand(event.inputType === "historyUndo" ? "undo" : "redo");
    },
    true,
  );

  function normalizedOffsetForSourceOffset(markdown, sourceOffset) {
    let normalizedOffset = 0;
    let sourceIndex = 0;
    while (sourceIndex < sourceOffset) {
      if (
        markdown.charCodeAt(sourceIndex) === 0x000d &&
        markdown.charCodeAt(sourceIndex + 1) === 0x000a
      ) {
        sourceIndex += 2;
      } else {
        sourceIndex += 1;
      }
      normalizedOffset += 1;
    }
    return normalizedOffset;
  }

  function domPositionAtUTF16Offset(root, requestedOffset) {
    const walker = document.createTreeWalker(
      root,
      window.NodeFilter.SHOW_TEXT,
    );
    let remaining = requestedOffset;
    let lastTextNode = null;
    while (walker.nextNode()) {
      const textNode = walker.currentNode;
      const length = textNode.nodeValue?.length ?? 0;
      if (remaining <= length) {
        return { node: textNode, offset: remaining };
      }
      remaining -= length;
      lastTextNode = textNode;
    }
    if (remaining === 0 && lastTextNode) {
      return {
        node: lastTextNode,
        offset: lastTextNode.nodeValue?.length ?? 0,
      };
    }
    return requestedOffset === 0 ? { node: root, offset: 0 } : null;
  }

  function sourceLinePosition(markdown, sourceOffset) {
    const normalizedPrefix = markdown
      .slice(0, sourceOffset)
      .replace(/\r\n?/g, "\n");
    const lines = normalizedPrefix.split("\n");
    return {
      column: lines.at(-1)?.length ?? 0,
      lineIndex: lines.length - 1,
    };
  }

  function directEditorLines(editor) {
    return Array.from(editor.children).filter((child) => {
      const display = window.getComputedStyle?.(child).display;
      return display !== "inline" && display !== "inline-block";
    });
  }

  function editorPositionAtHeading(editor, sourceOffset, ordinal) {
    const headingMarker = editor
      .querySelectorAll('[data-type="heading-marker"]')
      .item(ordinal);
    if (headingMarker instanceof HTMLElement) {
      const markerPosition = domPositionAtUTF16Offset(headingMarker, 0);
      if (markerPosition) {
        return {
          ...markerPosition,
          scrollTarget:
            headingMarker.closest("[data-block]") ?? headingMarker,
        };
      }
    }

    const sourcePosition = sourceLinePosition(tailMarkdown, sourceOffset);
    const editorLines = directEditorLines(editor);
    const targetLine = editorLines[sourcePosition.lineIndex];
    if (targetLine) {
      const linePosition = domPositionAtUTF16Offset(
        targetLine,
        sourcePosition.column,
      );
      if (linePosition) {
        return { ...linePosition, scrollTarget: targetLine };
      }
    }

    const normalizedOffset = normalizedOffsetForSourceOffset(
      tailMarkdown,
      sourceOffset,
    );
    const fallbackPosition = domPositionAtUTF16Offset(
      editor,
      normalizedOffset,
    );
    return fallbackPosition
      ? { ...fallbackPosition, scrollTarget: fallbackPosition.node.parentElement }
      : null;
  }

  function navigateEditorToHeading(sourceUTF16Offset, ordinal) {
    const editor = editorElement();
    const selection = window.getSelection();
    if (!editor || !selection) return false;

    const position = editorPositionAtHeading(
      editor,
      sourceUTF16Offset,
      ordinal,
    );
    if (!position) return false;

    const range = document.createRange();
    range.setStart(position.node, position.offset);
    range.collapse(true);
    selection.removeAllRanges();
    selection.addRange(range);
    lastEditorSelection = range.cloneRange();
    editorPort.focus();

    if (typeof editor.scrollTo === "function") {
      const caretBounds = range.getBoundingClientRect?.();
      const targetBounds = position.scrollTarget?.getBoundingClientRect?.();
      const editorBounds = editor.getBoundingClientRect?.();
      const targetTop =
        caretBounds?.top ??
        targetBounds?.top ??
        position.scrollTarget?.offsetTop ??
        editor.scrollTop;
      const editorTop = editorBounds?.top ?? 0;
      editor.scrollTo({
        behavior: "auto",
        top:
          editor.scrollTop +
          targetTop -
          editorTop -
          editor.clientHeight / 2,
      });
    }
    return true;
  }

  function navigateReaderToHeading(ordinal) {
    const headings = mount.querySelectorAll(
      ".vditor-preview h1, .vditor-preview h2, .vditor-preview h3, " +
        ".vditor-preview h4, .vditor-preview h5, .vditor-preview h6",
    );
    const heading = headings.item(ordinal);
    if (!(heading instanceof HTMLElement)) return false;
    heading.scrollIntoView({ behavior: "auto", block: "center" });
    return true;
  }

  window.kitoNative = Object.freeze({
    loadDocument(payload) {
      if (
        !editorPort ||
        !payload ||
        payload.sessionID !== sessionID ||
        !Number.isSafeInteger(payload.revision) ||
        payload.revision < 0 ||
        typeof payload.markdown !== "string" ||
        payload.markdown.length > MAXIMUM_UTF16_UNITS ||
        !isWellFormedUTF16(payload.markdown) ||
        !["light", "dark", "system"].includes(payload.appearance) ||
        !["editor", "split", "reader"].includes(payload.viewMode) ||
        !isValidSplitRatio(payload.splitRatio)
      ) {
        throw new Error("invalidHydration");
      }

      epoch += 1;
      pendingChanges.clear();
      pendingHydrationRevision = payload.revision;
      tailRevision = payload.revision;
      tailMarkdown = payload.markdown;
      acknowledgedRevision = payload.revision;
      acknowledgedMarkdown = payload.markdown;
      appearance = payload.appearance;
      viewMode = payload.viewMode;
      splitRatio = payload.splitRatio;
      lastEditorSelection = null;
      isComposing = false;
      hydrated = false;
      editorPort.setEnabled(false);
      editorPort.setTheme(appearance);
      applyLayout(viewMode, splitRatio);
      editorPort.replaceMarkdown(payload.markdown);
      setStatus("Restoring…", "pending");

      return { loadedRevision: payload.revision };
    },

    commitHydration(committedRevision) {
      if (
        !editorPort ||
        !Number.isSafeInteger(committedRevision) ||
        committedRevision < 0 ||
        committedRevision !== pendingHydrationRevision
      ) {
        throw new Error("invalidHydrationCommit");
      }

      pendingHydrationRevision = null;
      hydrated = true;
      editorPort.setEnabled(true);
      setStatus("Synced to Swift", "ready");
      if (viewMode !== "reader") editorPort.focus();

      return { committedRevision };
    },

    failHydration() {
      epoch += 1;
      pendingChanges.clear();
      pendingHydrationRevision = null;
      hydrated = false;
      editorPort?.setEnabled(false);
      setStatus("Unable to restore editor", "error");
    },

    setLayout(requestedViewMode, requestedSplitRatio) {
      if (!editorPort) throw new Error("editorUnavailable");
      applyLayout(requestedViewMode, requestedSplitRatio);
      return { splitRatio, viewMode };
    },

    setAppearance(requestedAppearance) {
      if (
        !editorPort ||
        !["light", "dark", "system"].includes(requestedAppearance)
      ) {
        throw new Error("invalidAppearance");
      }
      appearance = requestedAppearance;
      editorPort.setTheme(appearance);
      return { appearance };
    },

    navigateToHeading(
      acceptedRevision,
      sourceUTF16Offset,
      ordinal,
    ) {
      if (
        !hydrated ||
        !editorPort ||
        !Number.isSafeInteger(acceptedRevision) ||
        acceptedRevision < 0 ||
        !Number.isSafeInteger(sourceUTF16Offset) ||
        sourceUTF16Offset < 0 ||
        sourceUTF16Offset > tailMarkdown.length ||
        splitsSurrogatePair(tailMarkdown, sourceUTF16Offset) ||
        (sourceUTF16Offset > 0 &&
          tailMarkdown.charCodeAt(sourceUTF16Offset - 1) === 0x000d &&
          tailMarkdown.charCodeAt(sourceUTF16Offset) === 0x000a) ||
        !Number.isSafeInteger(ordinal) ||
        ordinal < 0
      ) {
        throw new Error("invalidOutlineNavigation");
      }
      if (acceptedRevision !== tailRevision) {
        throw new Error("staleOutlineNavigation");
      }

      let navigated = false;
      if (viewMode !== "reader") {
        navigated =
          navigateEditorToHeading(sourceUTF16Offset, ordinal) || navigated;
      }
      if (viewMode !== "editor") {
        navigated = navigateReaderToHeading(ordinal) || navigated;
      }
      return { navigated, revision: acceptedRevision };
    },

    async prepareForPrint() {
      if (!hydrated || !editorPort) throw new Error("printUnavailable");
      if (viewModeBeforePrint === null) viewModeBeforePrint = viewMode;
      applyLayout("reader", splitRatio);
      editorPort.renderReader();
      await new Promise((resolve) => window.setTimeout(resolve, 100));
      const reader = mount.querySelector(".vditor-preview");
      if (!(reader instanceof HTMLElement)) {
        throw new Error("printUnavailable");
      }
      renderPreviewContent(reader);
      return { ready: true };
    },

    async readerPlainText() {
      if (!hydrated || !editorPort) throw new Error("exportUnavailable");
      editorPort.renderReader();
      await new Promise((resolve) => window.setTimeout(resolve, 100));
      const reader = mount.querySelector(".vditor-preview");
      if (!(reader instanceof HTMLElement)) {
        throw new Error("exportUnavailable");
      }
      renderPreviewContent(reader);
      const text = reader.innerText
        .replaceAll("\u00a0", " ")
        .replaceAll("\r\n", "\n")
        .replaceAll("\r", "\n")
        .replace(/[ \t]+\n/gu, "\n")
        .trim();
      return { text: text.length > 0 ? `${text}\n` : "" };
    },

    finishPrinting() {
      if (viewModeBeforePrint === null) return { restored: false };
      const restoredViewMode = viewModeBeforePrint;
      viewModeBeforePrint = null;
      applyLayout(restoredViewMode, splitRatio);
      return { restored: true };
    },

    insertMarkdown(markdown) {
      if (
        typeof markdown !== "string" ||
        markdown.length === 0 ||
        markdown.length > MAXIMUM_UTF16_UNITS
      ) {
        throw new Error("invalidInsertion");
      }
      if (!hydrated || !editorPort || viewMode === "reader") {
        throw new Error("insertionUnavailable");
      }

      const restoredSelection = restoreEditorSelection();
      if (!restoredSelection) editorPort.focus();
      editorPort.insertMarkdown(markdown);
      editorPort.focus();
      return { inserted: true };
    },

    performCommand(command) {
      const selector = formatControlSelectors[command];
      const template = formatTemplateValues[command];
      if (!selector && !template) throw new Error("invalidFormat");
      if (!hydrated || !editorPort || viewMode === "reader") {
        throw new Error("formatUnavailable");
      }

      const restoredSelection = restoreEditorSelection();
      if (!restoredSelection) editorPort.focus();
      const selectionSnapshot = sourceSelectionSnapshot();
      const selectedMarkdown =
        selectionSnapshot?.markdown ??
        sourceMarkdownForSelection(window.getSelection()?.toString() ?? "");
      const caretTaskToggle =
        command === "taskList" &&
        selectionSnapshot?.markdown === "" &&
        toggleTaskAtOffset(tailMarkdown, selectionSnapshot.start);
      if (caretTaskToggle) {
        editorPort.replaceDocumentMarkdown(caretTaskToggle);
      } else if (
        selectedMarkdown !== "" &&
        selectedBlockCommands.has(command)
      ) {
        editorPort.replaceSelectedMarkdown(
          transformSelectedBlocks(selectedMarkdown, command),
        );
      } else if (
        selectedMarkdown !== "" &&
        selectedMarkdownCommands.has(command)
      ) {
        if (!selectionSnapshot) throw new Error("selectionMappingUnavailable");
        const replacement = transformSelectedMarkdown(
          selectedMarkdown,
          command,
        );
        editorPort.replaceSelectedMarkdownExactly(
          replacement,
          tailMarkdown.slice(0, selectionSnapshot.start) +
            replacement +
            tailMarkdown.slice(selectionSnapshot.end),
        );
      } else if (template) {
        editorPort.insertMarkdown(template);
      } else {
        const control = mount.querySelector(selector);
        if (!(control instanceof HTMLElement)) {
          throw new Error("formatControlUnavailable");
        }
        control.click();
      }
      editorPort.focus();
      return { command };
    },
  });

  function editorDidBecomeReady() {
    if (!editorPort) {
      setStatus("Editor failed to initialize", "error");
      return;
    }

    editorPort.setEnabled(false);
    editorPort.setTheme(appearance);
    enhanceAccessibility();
    installSplitResizer();
    setStatus("Connecting…", "pending");
    post({
      version: VERSION,
      kind: "ready",
      sessionID,
      capabilities: [
        "immediateContentChanges",
        "incrementalUTF16Changes",
        "nativeUndoForwarding",
      ],
    }).catch(() => setStatus("Handshake failed", "error"));
  }

  function initializeEditor() {
    if (!mount || !status) return;
    if (typeof window.Vditor !== "function") {
      setStatus("Editor resources unavailable", "error");
      return;
    }

    try {
      const initialTheme = resolvedTheme("system");
      installVendorGuards();
      const editor = new window.Vditor(mount, {
        value: "",
        width: "100%",
        height: "100%",
        minHeight: 0,
        placeholder: "Start with a thought…",
        lang: "en_US",
        mode: "sv",
        theme: initialTheme.editor,
        icon: "ant",
        cdn: "vendor/vditor",
        cache: { enable: false },
        counter: { enable: false },
        outline: { enable: false, position: "left" },
        resize: { enable: false },
        toolbarConfig: { hide: true, pin: false },
        toolbar: [
          "bold",
          "italic",
          "strike",
          "link",
          "headings",
          "list",
          "ordered-list",
          "check",
          "quote",
          "line",
          "inline-code",
          "code",
          "table",
          "undo",
          "redo",
        ],
        hint: { emoji: {}, emojiPath: "", extend: [], parse: false },
        preview: {
          delay: 80,
          maxWidth: 780,
          actions: [],
          hljs: { enable: false, lineNumber: false, style: "github" },
          math: { engine: "KaTeX", inlineDigit: false },
          markdown: {
            sanitize: true,
            gfmAutoLink: true,
            footnotes: true,
            codeBlockPreview: false,
            mathBlockPreview: false,
          },
          theme: { current: initialTheme.content, path: CONTENT_THEME_PATH },
          render: { media: { enable: false } },
          parse: renderPreviewContent,
        },
        link: { isOpen: false },
        image: { isPreview: false },
        input: reportContentChange,
        after: editorDidBecomeReady,
      });
      editorPort = createEditorPort(editor);
    } catch {
      setStatus("Editor failed to initialize", "error");
    }
  }

  colorScheme?.addEventListener?.("change", () => {
    if (appearance === "system") editorPort?.setTheme("system");
  });

  initializeEditor();
})();
