(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-monaco-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 monacoContainer = document.querySelector("[data-monaco-mount]"); var monacoLoaded = false; var monacoReadyPromise = null; var monacoDarkTheme = "cairnquire-dark"; var monacoLightTheme = "cairnquire-light"; 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; 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; } isSaving = true; hideConflict(); setStatus("Saving"); if (!window.MDHubSync) { isSaving = false; setStatus("Queued"); return; } 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; } if (result.status === "conflict") { showConflict(result.conflict, value); isSaving = false; return; } setStatus("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 loadMonaco() { if (monacoReadyPromise) { return monacoReadyPromise; } if (monacoLoaded && window.monaco) { monacoReadyPromise = Promise.resolve(); return monacoReadyPromise; } monacoLoaded = true; var isDark = window.matchMedia("(prefers-color-scheme: dark)").matches; monacoReadyPromise = new Promise(function (resolve) { var script = document.createElement("script"); script.src = "https://cdn.jsdelivr.net/npm/monaco-editor@0.52.2/min/vs/loader.js"; script.onload = function () { require.config({ paths: { vs: "https://cdn.jsdelivr.net/npm/monaco-editor@0.52.2/min/vs" }, }); require(["vs/editor/editor.main"], function () { monaco.editor.defineTheme(monacoDarkTheme, { base: "vs-dark", inherit: true, rules: [ { token: "comment", foreground: "8B9DAF", fontStyle: "italic" }, { token: "keyword", foreground: "E8943A" }, { token: "string", foreground: "C9A96E" }, { token: "number", foreground: "C9A96E" }, { token: "type", foreground: "E8943A" }, { token: "tag", foreground: "E8943A" }, { token: "attribute.name", foreground: "D4A05A" }, { token: "attribute.value", foreground: "C9A96E" }, { token: "delimiter", foreground: "B0B8C4" }, { token: "variable", foreground: "D4D8DE" }, ], colors: { "editor.background": "#1E1E1E", "editor.foreground": "#E8ECF0", "editor.lineHighlightBackground": "#282828", "editor.selectionBackground": "#3A3A3A", "editorLineNumber.foreground": "#555555", "editorLineNumber.activeForeground": "#E8943A", "editor.inactiveSelectionBackground": "#333333", "editorCursor.foreground": "#E8943A", "editorIndentGuide.background": "#2A2A2A", "editorIndentGuide.activeBackground": "#3A3A3A", "editorWhitespace.foreground": "#2A2A2A", "editorGutter.background": "#1E1E1E", "editorOverviewRuler.border": "#1E1E1E", "scrollbarSlider.background": "#3A3A3A88", "scrollbarSlider.hoverBackground": "#4A4A4A88", "scrollbarSlider.activeBackground": "#5A5A5A88", "minimap.background": "#1E1E1E", }, }); monaco.editor.defineTheme(monacoLightTheme, { base: "vs", inherit: true, rules: [ { token: "comment", foreground: "6A7B8C", fontStyle: "italic" }, { token: "keyword", foreground: "C67A2A" }, { token: "string", foreground: "8A6D3B" }, { token: "number", foreground: "8A6D3B" }, { token: "type", foreground: "C67A2A" }, { token: "tag", foreground: "C67A2A" }, { token: "attribute.name", foreground: "A87D3A" }, { token: "attribute.value", foreground: "8A6D3B" }, ], colors: { "editor.background": "#FCFAF5", "editor.foreground": "#1C2430", "editor.lineHighlightBackground": "#F0EDE5", "editor.selectionBackground": "#E8943A33", "editorLineNumber.foreground": "#AAAAAA", "editorLineNumber.activeForeground": "#C67A2A", "editorCursor.foreground": "#C67A2A", "editorIndentGuide.background": "#E8E5DD", "editorGutter.background": "#FCFAF5", }, }); if (monacoContainer) { editor = monaco.editor.create(monacoContainer, { value: textarea.value, language: "markdown", theme: isDark ? monacoDarkTheme : monacoLightTheme, fontFamily: "'Iosevka', 'JetBrains Mono', ui-monospace, SFMono-Regular, monospace", fontSize: 14, lineHeight: 22, minimap: { enabled: false }, wordWrap: "on", scrollBeyondLastLine: false, padding: { top: 12 }, renderLineHighlight: "line", smoothScrolling: true, cursorBlinking: "smooth", cursorSmoothCaretAnimation: "on", bracketPairColorization: { enabled: true }, automaticLayout: true, }); textarea.hidden = true; monacoContainer.classList.add("monaco-mounted"); editor.onDidChangeModelContent(function () { textarea.value = editor.getValue(); scheduleSave(); }); if (window.MDHubSync) { editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS, function () { saveNow().catch(function () { setStatus("Queued"); }); }); } var darkMQ = window.matchMedia("(prefers-color-scheme: dark)"); darkMQ.addEventListener("change", function (e) { monaco.editor.setTheme(e.matches ? monacoDarkTheme : monacoLightTheme); }); editor.focus(); } resolve(); }); }; document.head.appendChild(script); }); return monacoReadyPromise; } function ensureResolutionEditor() { if (!conflictResolution || !conflictResolutionMount) { return; } loadMonaco().then(function () { if (!window.monaco || resolutionEditor) { return; } var isDark = window.matchMedia("(prefers-color-scheme: dark)").matches; resolutionEditor = monaco.editor.create(conflictResolutionMount, { value: conflictResolution.value || resolvedConflictContent, language: "markdown", theme: isDark ? monacoDarkTheme : monacoLightTheme, fontFamily: "'Iosevka', 'JetBrains Mono', ui-monospace, SFMono-Regular, monospace", fontSize: 14, lineHeight: 22, minimap: { enabled: false }, wordWrap: "on", scrollBeyondLastLine: false, padding: { top: 12 }, renderLineHighlight: "line", smoothScrolling: true, cursorBlinking: "smooth", cursorSmoothCaretAnimation: "on", bracketPairColorization: { enabled: true }, automaticLayout: true, }); conflictResolution.hidden = true; conflictResolutionMount.classList.add("monaco-mounted"); resolutionEditor.onDidChangeModelContent(function () { if (suppressResolutionEvents) { return; } resolvedConflictContent = resolutionEditor.getValue(); conflictResolution.value = resolvedConflictContent; if (conflictApply) { conflictApply.disabled = false; } setConflictMessage("Manual resolution updated. Apply it to save the resolved document."); }); resolutionEditor.focus(); }).catch(function () { conflictResolution.hidden = false; }); } if (monacoContainer) { loadMonaco().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(); var doneLink = document.querySelector("[data-done-link]"); var isSaving = false; if (doneLink) { doneLink.addEventListener("click", function (event) { var currentValue = getEditorValue(); var href = doneLink.getAttribute("href"); // If there's an unresolved conflict, warn the user if (currentConflict) { event.preventDefault(); setStatus("Conflict"); if (conflictNotice) { conflictNotice.scrollIntoView({ behavior: "smooth", block: "center" }); } return; } // If nothing changed, allow normal navigation if (currentValue === lastSavedValue) { return; } // Unsaved changes — cancel any pending timer and save immediately event.preventDefault(); if (saveTimer) { clearTimeout(saveTimer); saveTimer = null; } if (isSaving) { // Already saving, just wait and then navigate waitForSaveThenNavigate(href); return; } isSaving = true; setStatus("Saving"); saveNow() .then(function () { isSaving = false; window.location.href = href; }) .catch(function () { isSaving = false; setStatus("Queued"); // If save fails, ask user if they want to leave anyway if (window.confirm("Could not save. Leave without saving?")) { window.location.href = href; } }); }); } function waitForSaveThenNavigate(href) { var checkInterval = setInterval(function () { if (!isSaving) { clearInterval(checkInterval); 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 (getEditorValue() !== lastSavedValue || currentConflict) { e.preventDefault(); e.returnValue = ""; } }); document.addEventListener("keydown", function (e) { if ((e.metaKey || e.ctrlKey) && e.key === "s") { e.preventDefault(); } }); })();