(function () { var form = document.querySelector("[data-editor-form]"); var textarea = document.querySelector("[data-document-editor]"); var statusNodes = document.querySelectorAll("[data-sync-status], [data-sync-status-secondary]"); var hashEl = document.querySelector("[data-editor-hash]"); var documentShell = document.querySelector("[data-document-path][data-document-hash]"); var conflictNotice = document.querySelector("[data-conflict-notice]"); var conflictHash = document.querySelector("[data-conflict-hash]"); var conflictStatus = document.querySelector("[data-conflict-status]"); var conflictDiff = document.querySelector("[data-conflict-diff]"); var conflictApply = document.querySelector("[data-conflict-apply]"); var conflictDismiss = document.querySelector("[data-conflict-dismiss]"); var conflictResolution = document.querySelector("[data-conflict-resolution]"); var conflictResolutionMount = document.querySelector("[data-conflict-codemirror-mount]"); var conflictUseServer = document.querySelector("[data-conflict-use='server']"); var conflictUseLocal = document.querySelector("[data-conflict-use='local']"); var conflictUseBoth = document.querySelector("[data-conflict-use='both']"); var codeMirrorContainer = document.querySelector("[data-codemirror-mount]"); var codeMirrorReadyPromise = null; var codeMirrorModules = null; if (!form || !textarea || statusNodes.length === 0) { return; } var path = form.getAttribute("data-document-path"); var baseHash = documentShell ? documentShell.getAttribute("data-document-hash") : ""; var saveTimer = null; var lastSavedValue = textarea.value; var editor = null; var currentConflict = null; var currentConflictLocalContent = ""; var resolvedConflictContent = ""; var conflictDiffView = null; var resolutionEditor = null; var suppressResolutionEvents = false; var doneLink = document.querySelector("[data-done-link]"); var isSaving = false; var suppressBeforeUnload = false; function setStatus(nextStatus) { statusNodes.forEach(function (node) { node.textContent = nextStatus; node.dataset.state = nextStatus.toLowerCase(); }); } function getEditorValue() { return editor ? editor.getValue() : textarea.value; } function setEditorValue(value) { textarea.value = value; if (editor && editor.getValue() !== value) { editor.setValue(value); } } function getResolutionValue() { return resolutionEditor ? resolutionEditor.getValue() : resolvedConflictContent; } function setResolutionEditorValue(value) { resolvedConflictContent = value; if (conflictResolution) { conflictResolution.value = value; } if (resolutionEditor && resolutionEditor.getValue() !== value) { suppressResolutionEvents = true; resolutionEditor.setValue(value); suppressResolutionEvents = false; } } function ensureTrailingNewline(value) { return value.endsWith("\n") ? value : value + "\n"; } function buildCombinedResolution(serverContent, localContent) { return ensureTrailingNewline(serverContent) + "\n" + ensureTrailingNewline(localContent); } function setConflictMessage(message) { if (conflictStatus) { conflictStatus.textContent = message; } } function setResolutionContent(content, message) { setResolutionEditorValue(content); if (conflictApply) { conflictApply.disabled = false; } if (message) { setConflictMessage(message); } } async function loadDiffs() { var modules = await Promise.all([ import("https://cdn.jsdelivr.net/npm/@pierre/diffs@1.1.22/+esm"), import("https://cdn.jsdelivr.net/npm/@pierre/diffs@1.1.22/dist/style.js"), ]); return { renderer: modules[0], stylesheet: modules[1].default || "", }; } function styleDiffContainer(diffContainer) { diffContainer.style.setProperty("--diffs-light-bg", "#fcfaf5"); diffContainer.style.setProperty("--diffs-light", "#1c2430"); diffContainer.style.setProperty("--diffs-dark-bg", "#1e1e1e"); diffContainer.style.setProperty("--diffs-dark", "#e8ecf0"); diffContainer.style.setProperty("--diffs-addition-color", "#3f8f59"); diffContainer.style.setProperty("--diffs-deletion-color", "#c8553d"); diffContainer.style.setProperty("--diffs-modified-color", "#c67a2a"); diffContainer.style.setProperty("--diffs-font-family", "'Iosevka', 'JetBrains Mono', ui-monospace, SFMono-Regular, monospace"); diffContainer.style.setProperty("--diffs-header-font-family", "'Inter', system-ui, -apple-system, BlinkMacSystemFont, sans-serif"); } function injectDiffStyles(diffContainer, stylesheet) { var root = diffContainer.shadowRoot; if (!root || !stylesheet || root.querySelector("[data-cairnquire-diffs-style]")) { return; } var style = document.createElement("style"); style.setAttribute("data-cairnquire-diffs-style", ""); style.textContent = stylesheet; root.prepend(style); } function renderConflictResolver(conflict, localContent) { if (!conflictDiff) { return; } if (conflictDiffView) { conflictDiffView.cleanUp(); conflictDiffView = null; } conflictDiff.replaceChildren(); currentConflictLocalContent = localContent; setResolutionEditorValue(localContent); if (conflictApply) { conflictApply.disabled = false; } ensureResolutionEditor(); setConflictMessage("Loading conflict resolver..."); loadDiffs().then(function (diffsModule) { var diffs = diffsModule.renderer; var diffContainer = document.createElement("div"); styleDiffContainer(diffContainer); conflictDiff.appendChild(diffContainer); var serverFile = { name: path, contents: conflict.currentContent || "", lang: "markdown", cacheKey: "server:" + (conflict.currentHash || ""), }; var localFile = { name: path, contents: localContent, lang: "markdown", cacheKey: "local:" + (conflict.baseHash || "") + ":" + localContent.length, }; conflictDiffView = new diffs.FileDiff({ disableFileHeader: false, diffStyle: "unified", diffIndicators: "classic", lineDiffType: "word", overflow: "scroll", parseDiffOptions: { context: 4 }, }); conflictDiffView.render({ oldFile: serverFile, newFile: localFile, fileContainer: diffContainer, }); injectDiffStyles(diffContainer, diffsModule.stylesheet); setConflictMessage("Red deletions are from the server copy. Green additions are from your queued edit."); }).catch(function () { setConflictMessage("Could not load the diff renderer. Your edit is still queued locally."); }); } function showConflict(conflict, localContent) { currentConflict = conflict; setStatus("Conflict"); form.hidden = true; if (conflictHash && conflict && conflict.currentHash) { conflictHash.textContent = conflict.currentHash; } if (conflictNotice) { conflictNotice.hidden = false; } renderConflictResolver(conflict, localContent || getEditorValue()); } function hideConflict() { form.hidden = false; if (conflictNotice) { conflictNotice.hidden = true; } if (conflictDiff) { conflictDiff.replaceChildren(); } if (conflictDiffView) { conflictDiffView.cleanUp(); conflictDiffView = null; } if (conflictApply) { conflictApply.disabled = true; } setResolutionEditorValue(""); currentConflict = null; currentConflictLocalContent = ""; } async function saveNow() { var value = getEditorValue(); if (value === lastSavedValue) { setStatus("Saved"); return "unchanged"; } isSaving = true; hideConflict(); setStatus("Saving"); if (!window.MDHubSync) { isSaving = false; setStatus("Queued"); return "queued"; } try { var result = await window.MDHubSync.saveDocument(path, value, baseHash); if (result.status === "saved") { lastSavedValue = value; if (hashEl && result.result && result.result.hash) { hashEl.textContent = result.result.hash; baseHash = result.result.hash; if (documentShell) { documentShell.setAttribute("data-document-hash", result.result.hash); } } setStatus("Saved"); isSaving = false; return "saved"; } if (result.status === "conflict") { showConflict(result.conflict, value); isSaving = false; return "conflict"; } setStatus("Queued"); return "queued"; } catch (err) { setStatus("Queued"); throw err; } finally { isSaving = false; } } async function applyResolvedConflict() { if (!window.MDHubSync || !currentConflict) { return; } var resolutionContent = getResolutionValue(); resolvedConflictContent = resolutionContent; setConflictMessage("Saving resolved document..."); if (conflictApply) { conflictApply.disabled = true; } var result = await window.MDHubSync.saveDocument(path, resolutionContent, currentConflict.currentHash); if (result.status === "saved") { setEditorValue(resolutionContent); lastSavedValue = resolutionContent; if (hashEl && result.result && result.result.hash) { hashEl.textContent = result.result.hash; baseHash = result.result.hash; if (documentShell) { documentShell.setAttribute("data-document-hash", result.result.hash); } } hideConflict(); setStatus("Saved"); return; } if (result.status === "conflict") { showConflict(result.conflict, resolutionContent); return; } setStatus("Queued"); setConflictMessage("Resolved document is queued and will retry when the server is reachable."); } function restorePendingConflict() { if (!window.MDHubCache || !window.MDHubCache.getPendingEdit) { return; } var normalizedPath = window.MDHubCache.normalizePath("/docs/" + path.replace(/^\/+/, "")); window.MDHubCache.getPendingEdit(normalizedPath).then(function (pending) { if (pending && pending.conflict) { showConflict(pending.conflict, pending.content || getEditorValue()); } }).catch(function () {}); } function scheduleSave() { setStatus("Unsaved"); if (saveTimer) { clearTimeout(saveTimer); } saveTimer = setTimeout(function () { saveNow().catch(function () { setStatus("Queued"); }); }, 2000); } function doneHref() { return doneLink ? doneLink.getAttribute("href") : "/docs/" + path.replace(/\.md$/, ""); } function clearScheduledSave() { if (saveTimer) { clearTimeout(saveTimer); saveTimer = null; } } function leaveEditorNow() { clearScheduledSave(); suppressBeforeUnload = true; window.location.href = doneHref(); } function saveThenLeaveEditor() { var href = doneHref(); if (currentConflict) { setStatus("Conflict"); if (conflictNotice) { conflictNotice.scrollIntoView({ behavior: "smooth", block: "center" }); } return; } clearScheduledSave(); if (isSaving) { waitForSaveThenNavigate(href); return; } setStatus("Saving"); saveNow() .then(function (result) { if (result === "saved" || result === "unchanged") { suppressBeforeUnload = true; window.location.href = href; return; } if (result === "conflict") { setStatus("Conflict"); return; } setStatus("Queued"); if (window.confirm("Could not save. Leave without saving?")) { suppressBeforeUnload = true; window.location.href = href; } }) .catch(function () { setStatus("Queued"); if (window.confirm("Could not save. Leave without saving?")) { suppressBeforeUnload = true; window.location.href = href; } }); } function loadCodeMirror() { if (codeMirrorReadyPromise) { return codeMirrorReadyPromise; } codeMirrorReadyPromise = Promise.all([ import("/static/codemirror.bundle.js"), ]).then(function (modules) { var bundle = modules[0]; codeMirrorModules = { cm: bundle, markdown: bundle, vim: bundle, }; registerVimCommands(); return codeMirrorModules; }); return codeMirrorReadyPromise; } function registerVimCommands() { var Vim = codeMirrorModules && codeMirrorModules.vim && codeMirrorModules.vim.Vim; if (!Vim) { return; } Vim.defineEx("write", "w", function () { saveNow().catch(function () { setStatus("Queued"); }); }); Vim.defineEx("quit", "q", function () { leaveEditorNow(); }); Vim.defineEx("wq", "wq", function () { saveThenLeaveEditor(); }); Vim.defineEx("xit", "x", function () { saveThenLeaveEditor(); }); Vim.map("", ":q"); } function codeMirrorTheme() { var EditorView = codeMirrorModules.cm.EditorView; var isDark = window.matchMedia("(prefers-color-scheme: dark)").matches; return EditorView.theme({ "&": { height: "100%", minHeight: "60vh", backgroundColor: "var(--panel)", color: "var(--text)", fontSize: "14px", }, "&.cm-focused": { outline: "none", }, ".cm-scroller": { fontFamily: "'Iosevka', 'JetBrains Mono', ui-monospace, SFMono-Regular, monospace", lineHeight: "22px", }, ".cm-content": { padding: "12px 0", caretColor: "var(--accent)", }, ".cm-line": { padding: "0 14px", }, ".cm-gutters": { backgroundColor: "var(--panel-strong)", borderRight: "1px solid var(--border)", color: "var(--muted)", }, ".cm-activeLine, .cm-activeLineGutter": { backgroundColor: isDark ? "oklch(0.24 0.012 55)" : "#f0ede5", }, ".cm-selectionBackground, &.cm-focused .cm-selectionBackground": { backgroundColor: "color-mix(in srgb, var(--accent) 22%, transparent)", }, ".cm-cursor": { borderLeftColor: "var(--accent)", }, ".cm-panel": { backgroundColor: "var(--panel-strong)", borderTop: "1px solid var(--border)", color: "var(--text)", fontFamily: "'Iosevka', 'JetBrains Mono', ui-monospace, SFMono-Regular, monospace", }, ".cm-vim-panel input": { color: "var(--text)", }, }, { dark: isDark }); } function codeMirrorHighlighting() { var cm = codeMirrorModules.cm; var tags = codeMirrorModules.cm.tags; var urlColor = window.matchMedia("(prefers-color-scheme: dark)").matches ? "oklch(0.78 0.14 190)" : "#047c78"; return cm.syntaxHighlighting(cm.HighlightStyle.define([ { tag: [tags.url, tags.link], color: urlColor, textDecoration: "underline", textUnderlineOffset: "2px", }, ])); } function createCodeMirrorEditor(mount, initialValue, options) { var cm = codeMirrorModules.cm; var markdownModule = codeMirrorModules.markdown; var vimModule = codeMirrorModules.vim; var onChange = options && options.onChange ? options.onChange : function () {}; var extensions = [ vimModule.vim({ status: true }), cm.basicSetup, markdownModule.markdown({ base: markdownModule.markdownLanguage, }), cm.EditorView.lineWrapping, codeMirrorTheme(), codeMirrorHighlighting(), cm.EditorView.updateListener.of(function (update) { if (update.docChanged) { onChange(update.state.doc.toString()); } }), ]; mount.replaceChildren(); mount.classList.add("codemirror-mounted"); var view = new cm.EditorView({ doc: initialValue, extensions: extensions, parent: mount, }); return { view: view, getValue: function () { return view.state.doc.toString(); }, setValue: function (value) { if (view.state.doc.toString() === value) { return; } view.dispatch({ changes: { from: 0, to: view.state.doc.length, insert: value }, }); }, focus: function () { view.focus(); }, hasTextFocus: function () { return view.hasFocus; }, destroy: function () { view.destroy(); }, }; } function ensureResolutionEditor() { if (!conflictResolution || !conflictResolutionMount) { return; } loadCodeMirror().then(function () { if (resolutionEditor) { return; } resolutionEditor = createCodeMirrorEditor(conflictResolutionMount, conflictResolution.value || resolvedConflictContent, { onChange: function (value) { if (suppressResolutionEvents) { return; } resolvedConflictContent = value; conflictResolution.value = value; if (conflictApply) { conflictApply.disabled = false; } setConflictMessage("Manual resolution updated. Apply it to save the resolved document."); }, }); conflictResolution.hidden = true; resolutionEditor.focus(); }).catch(function () { conflictResolution.hidden = false; }); } if (codeMirrorContainer) { loadCodeMirror().then(function () { editor = createCodeMirrorEditor(codeMirrorContainer, textarea.value, { onChange: function (value) { textarea.value = value; scheduleSave(); }, }); textarea.hidden = true; editor.focus(); }).catch(function () { textarea.addEventListener("input", scheduleSave); }); } else { textarea.addEventListener("input", scheduleSave); } form.addEventListener("submit", function (event) { event.preventDefault(); saveNow().catch(function () { setStatus("Queued"); }); }); if (conflictApply) { conflictApply.addEventListener("click", function () { applyResolvedConflict().catch(function () { setStatus("Queued"); setConflictMessage("Could not save the resolved document. It remains queued locally."); }); }); } if (conflictDismiss) { conflictDismiss.addEventListener("click", function () { hideConflict(); }); } if (conflictResolution) { conflictResolution.addEventListener("input", function () { resolvedConflictContent = conflictResolution.value; if (conflictApply) { conflictApply.disabled = false; } setConflictMessage("Manual resolution updated. Apply it to save the resolved document."); }); } if (conflictUseServer) { conflictUseServer.addEventListener("click", function () { if (!currentConflict) { return; } setResolutionContent(currentConflict.currentContent || "", "Using the server copy as the resolution. You can edit it before applying."); }); } if (conflictUseLocal) { conflictUseLocal.addEventListener("click", function () { setResolutionContent(currentConflictLocalContent, "Using your queued edit as the resolution. You can edit it before applying."); }); } if (conflictUseBoth) { conflictUseBoth.addEventListener("click", function () { if (!currentConflict) { return; } setResolutionContent( buildCombinedResolution(currentConflict.currentContent || "", currentConflictLocalContent), "Using both versions as the resolution. Edit the combined content before applying." ); }); } restorePendingConflict(); if (doneLink) { doneLink.addEventListener("click", function (event) { event.preventDefault(); saveThenLeaveEditor(); }); } function waitForSaveThenNavigate(href) { var checkInterval = setInterval(function () { if (!isSaving) { clearInterval(checkInterval); if (currentConflict) { setStatus("Conflict"); if (conflictNotice) { conflictNotice.scrollIntoView({ behavior: "smooth", block: "center" }); } return; } suppressBeforeUnload = true; window.location.href = href; } }, 100); // Timeout after 10 seconds setTimeout(function () { clearInterval(checkInterval); }, 10000); } // Warn on browser unload if there are unsaved changes window.addEventListener("beforeunload", function (e) { if (suppressBeforeUnload) { return; } if (getEditorValue() !== lastSavedValue || currentConflict) { e.preventDefault(); e.returnValue = ""; } }); function editorSurfaceHasFocus(target) { if (!target) { return false; } if (target === textarea || target.closest("[data-codemirror-mount], [data-conflict-codemirror-mount], [data-document-editor], .cm-editor")) { return true; } return Boolean(editor && editor.hasTextFocus && editor.hasTextFocus()); } document.addEventListener("keydown", function (e) { if ((e.metaKey || e.ctrlKey) && e.key === "s") { e.preventDefault(); saveNow().catch(function () { setStatus("Queued"); }); return; } if (e.metaKey && e.key === "Enter") { e.preventDefault(); saveThenLeaveEditor(); return; } if (!editorSurfaceHasFocus(e.target)) { return; } }, true); })();