${snippet}
Page could not be loaded
${escapeHtml(e.message)}
/* * Copyright (C) 2026 Mateusz Gruszczyński @linuxiarz.pl * Source-Available Code / Dual-Licensed. * * Free for non-commercial and evaluation use under terms of BSL/GPLv3. * Commercial or production use requires a valid paid license. * See LICENSE file in repository root for details. */ import { installGlobalDiagnostics, logInfo } from "@rustpad/logger"; installGlobalDiagnostics(); import { applyAuthorshipEdit, authorshipOwners, lineAuthors, mapSelectionThroughEdit, parseAuthorship, renderAuthorshipLayer, replaceAuthorshipOwner, serializeAuthorship, syncAuthorshipLayer } from "@rustpad/authorship"; import { copyText } from "@rustpad/clipboard"; import { lineFromHash, lineLink, lineStartOffset } from "@rustpad/line-links"; import { applyFormat, bindFormatShortcuts } from "@rustpad/editor-format"; import { bindEmojiPicker } from "@rustpad/emoji-picker"; import { alignPreviewLineNumbers, renderMarkdown, setMarkdownFiles, unresolvedMarkdownFileAliases } from "@rustpad/markdown"; import { getNickname, getGuestId, getAuthToken, getAccessToken, setAccessToken } from "@rustpad/session"; import { bindIdentityDialog, validateCurrentSession } from "@rustpad/auth-ui"; import { bindNoteFiles } from "@rustpad/note-files"; import { currentShareUrl, readEditorState, writeEditorState } from "@rustpad/url-state"; import { toast } from "@rustpad/toast"; import { getTheme } from "@rustpad/theme"; export function startNoteEditor(adapter) { const editor = document.querySelector("#editor"), preview = document.querySelector("#preview"), editorWorkspace = document.querySelector("#editor-workspace"), gutter = document.querySelector("#line-gutter"), ownerLabels = document.querySelector("#owner-labels"), authorshipLayer = document.querySelector("#authorship-layer"); const modeToggle = document.querySelector("#mode-toggle"), passwordDialog = document.querySelector("#password-dialog"), identityDialog = document.querySelector("#identity-dialog"); const accessLevel = document.querySelector("#access-level"), roomDetails = document.querySelector("#room-details"), roomUsers = document.querySelector("#room-users"), roomCount = document.querySelector("#room-count"), socketLatency = document.querySelector("#socket-latency"), mobileConnectionDetails = document.querySelector("#mobile-connection-details"), chatMessages = document.querySelector("#chat-messages"), chatForm = document.querySelector("#chat-form"), chatInput = document.querySelector("#chat-input"), chatUnread = document.querySelector("#chat-unread"), mobileChatUnread = document.querySelector("#mobile-chat-unread"), connectionNotice = document.querySelector("#connection-notice"), connectionNoticeTitle = document.querySelector("#connection-notice-title"), connectionNoticeMessage = document.querySelector("#connection-notice-message"); let unreadChat = 0; const compactToggle = document.querySelector("#compact-toggle"), lineLinksToggle = document.querySelector("#line-links-toggle"), authorshipColorsToggle = document.querySelector("#authorship-colors-toggle"), authorshipColorsLabel = document.querySelector("#authorship-colors-label"), publicPageEnabled = document.querySelector("#public-page-enabled"), publicTaskUpdates = document.querySelector("#public-task-updates"), unprotectPublicPage = document.querySelector("#unprotect-public-page"), participantBadges = document.querySelector("#participant-badges"), fontFamily = document.querySelector("#font-family"), fontSize = document.querySelector("#font-size"), currentUser = document.querySelector("#current-user"), userColorPicker = document.querySelector("#user-color-picker"), mobileColorPicker = document.querySelector("#mobile-color-picker"), useGlobalColorButton = document.querySelector("#use-global-color"); const mobileFontFamily = document.querySelector("#mobile-font-family"), mobileFontSize = document.querySelector("#mobile-font-size"), mobileLineToggle = document.querySelector("#mobile-line-numbers-toggle"), mobilePreviewLineToggle = document.querySelector("#mobile-preview-line-numbers-toggle"), mobileCompactToggle = document.querySelector("#mobile-compact-toggle"), mobileLineLinksToggle = document.querySelector("#mobile-line-links-toggle"); const shareToken = new URLSearchParams(location.search).get("share"); const notePreferenceKey = name => `rustpad:${name}:${location.pathname}`; let accessToken = shareToken || getAccessToken(adapter.access.kind, adapter.access.key), password = "", nickname = getNickname(), info, socket, saveTimer, applyingRemote = false, applyingHistory = false, resourceUnlocked = false, uiState = readEditorState(), authorship = parseAuthorship("", "[]"), previousContent = "", globalColor = "", noteColor = "", presenceUsers = [], authorshipMode = "simple", authorshipColorsEnabled = true, lastRevealedLineHash = ""; let editorSettingsSaveTimer, editorSettingsSaveInFlight = false, pendingPersonalSettingsSave = false, pendingAuthorshipSettingsSave = false, connectionNoticeTimer = 0, connectionWasInterrupted = false; const editHistory = { entries: [], index: -1, lastKind: "", lastRecordedAt: 0, snapshot() { return { content: editor.value, ownerMap: serializeAuthorship(authorship, editor.value.length), selectionStart: editor.selectionStart, selectionEnd: editor.selectionEnd, selectionDirection: editor.selectionDirection, scrollTop: editor.scrollTop, scrollLeft: editor.scrollLeft, }; }, reset() { this.entries = [this.snapshot()]; this.index = 0; this.lastKind = ""; this.lastRecordedAt = 0; }, syncCurrent() { if (this.index < 0) { this.reset(); return; } this.entries[this.index] = this.snapshot(); }, record(inputType = "") { const snapshot = this.snapshot(); if (this.index < 0) { this.entries = [snapshot]; this.index = 0; return; } if (this.entries[this.index]?.content === snapshot.content) { this.entries[this.index] = snapshot; return; } if (this.index < this.entries.length - 1) this.entries.splice(this.index + 1); const kind = inputType === "insertText" || inputType === "insertCompositionText" ? "typing" : inputType === "deleteContentBackward" || inputType === "deleteContentForward" ? "deleting" : "action"; const now = Date.now(); const merge = kind !== "action" && kind === this.lastKind && now - this.lastRecordedAt < 900 && this.index > 0; if (merge) this.entries[this.index] = snapshot; else { this.entries.push(snapshot); this.index += 1; if (this.entries.length > 100) { this.entries.shift(); this.index -= 1; } } this.lastKind = kind; this.lastRecordedAt = now; }, move(offset) { const nextIndex = this.index + offset; if (nextIndex < 0 || nextIndex >= this.entries.length) return false; this.index = nextIndex; this.lastKind = ""; this.lastRecordedAt = 0; restoreHistorySnapshot(this.entries[this.index]); return true; }, undo() { return this.move(-1); }, redo() { return this.move(1); }, }; const compactLayoutQuery = window.matchMedia("(max-width: 1499px)"); const singlePaneQuery = window.matchMedia("(max-width: 760px)"); let compactView = uiState.view === "preview" ? "preview" : "edit"; let refreshFilesForAliases = () => { }; let aliasRefreshTimer = 0; let lastUnresolvedAliasKey = ""; let markdownFileSignature = ""; function updateMarkdownFiles(files, { rerender = false } = {}) { const normalized = (Array.isArray(files) ? files : []).map(file => ({ filename: String(file?.filename || ""), url: String(file?.url || ""), mime_type: String(file?.mime_type || ""), })).sort((left, right) => left.filename.localeCompare(right.filename)); const nextSignature = JSON.stringify(normalized); const changed = nextSignature !== markdownFileSignature; markdownFileSignature = nextSignature; setMarkdownFiles(normalized); if (rerender && changed) render(); } function scheduleAliasFileRefresh(content) { const key = unresolvedMarkdownFileAliases(content).sort().join("\u0000"); if (!key) { lastUnresolvedAliasKey = ""; return; } if (key === lastUnresolvedAliasKey) return; lastUnresolvedAliasKey = key; clearTimeout(aliasRefreshTimer); aliasRefreshTimer = window.setTimeout(() => refreshFilesForAliases(), 200); } const lineToggle = document.querySelector("#line-numbers-toggle"), previewLineToggle = document.querySelector("#preview-line-numbers-toggle"); lineToggle.checked = localStorage.getItem(notePreferenceKey("line-numbers")) !== "off"; previewLineToggle.checked = localStorage.getItem(notePreferenceKey("preview-line-numbers")) === "on"; compactToggle.checked = localStorage.getItem(notePreferenceKey("compact")) !== "off"; lineLinksToggle.checked = localStorage.getItem(notePreferenceKey("line-links")) === "on"; fontFamily.value = localStorage.getItem(notePreferenceKey("font-family")) || "mono"; fontSize.value = localStorage.getItem(notePreferenceKey("font-size")) || "14"; authorshipColorsToggle.checked = authorshipColorsEnabled; function syncMobileEditorControls() { if (mobileFontFamily) mobileFontFamily.value = fontFamily.value; if (mobileFontSize) mobileFontSize.value = fontSize.value; if (mobileLineToggle) mobileLineToggle.checked = lineToggle.checked; if (mobilePreviewLineToggle) mobilePreviewLineToggle.checked = previewLineToggle.checked; if (mobileCompactToggle) mobileCompactToggle.checked = compactToggle.checked; if (mobileLineLinksToggle) mobileLineLinksToggle.checked = lineLinksToggle.checked; } syncMobileEditorControls(); function updateAuthorshipControls() { const canManage = info?.can_manage_authorship === true; authorshipColorsToggle.checked = authorshipColorsEnabled; authorshipColorsToggle.disabled = !canManage; authorshipColorsLabel.textContent = authorshipColorsEnabled ? "Colors on" : "Colors off"; document.querySelectorAll("[data-authorship-mode]").forEach(button => { button.classList.toggle("active", button.dataset.authorshipMode === authorshipMode); button.disabled = !canManage; }); const controls = document.querySelector(".authorship-controls"); if (controls) controls.title = canManage ? "Global authorship settings" : "Only the owner can change authorship settings"; } function defaultColorFor(name) { let h = 0; for (const c of name || "?") h = (h * 31 + c.charCodeAt(0)) % 360; return `hsl(${h} 70% 62%)`; } function ownerParts(owner) { const raw = String(owner || ""); const split = raw.lastIndexOf("\u001f"); return split < 0 ? { name: raw, color: "" } : { name: raw.slice(0, split), color: raw.slice(split + 1) }; } function ownerName(owner) { return ownerParts(owner).name; } function colorFor(owner) { const parts = ownerParts(owner); const ownColor = parts.name === nickname ? currentUserColor() : ""; return /^#[0-9a-f]{6}$/i.test(ownColor) ? ownColor : /^#[0-9a-f]{6}$/i.test(parts.color) ? parts.color : defaultColorFor(parts.name); } const guestColorKey = `rustpad:guest-color:${adapter.access.kind}:${adapter.access.key}`; function readGuestColor() { return sessionStorage.getItem(guestColorKey) || ""; } function writeGuestColor(color) { if (color) sessionStorage.setItem(guestColorKey, color); else sessionStorage.removeItem(guestColorKey); } function globalUserColor() { return globalColor || ""; } function noteUserColor() { return noteColor || ""; } function currentUserColor() { return noteUserColor() || globalUserColor(); } function currentOwner() { const color = currentUserColor(); return color ? `${nickname}\u001f${color}` : nickname; } function adoptCurrentOwnerAliases(model, contentLength) { const color = currentUserColor(); if (!getAuthToken() || !/^#[0-9a-f]{6}$/i.test(color)) return model; const replacement = currentOwner(); return replaceAuthorshipOwner(model, owner => { const parts = ownerParts(owner); return /^#[0-9a-f]{6}$/i.test(parts.color) && parts.color.toLowerCase() === color.toLowerCase(); }, replacement, contentLength); } function updateCurrentUser() { const color = currentUserColor() || defaultColorFor(nickname); const pickerColor = /^#[0-9a-f]{6}$/i.test(color) ? color : "#7c6cff"; const overridden = Boolean(noteUserColor()); currentUser.querySelector(".user-chip__name").textContent = nickname; currentUser.style.setProperty("--owner", color); currentUser.title = overridden ? "Note color override" : "Global profile color"; userColorPicker.value = pickerColor; if (mobileColorPicker) mobileColorPicker.value = pickerColor; useGlobalColorButton.hidden = !overridden; document.querySelector(".mobile-editor-bubble")?.style.setProperty("--owner", color); } function sessionHeaders() { return accessToken && accessToken !== "cookie" ? { Authorization: `Bearer ${accessToken}` } : {}; } function accountHeaders() { return {}; } async function loadNoteInfo() { info = await adapter.loadInfo(sessionHeaders()); globalColor = info.global_color || ""; noteColor = info.note_color || ""; if (getAuthToken()) { const colors = await adapter.loadColor(accountHeaders()); globalColor = colors.global_color || ""; noteColor = colors.note_color || ""; } else { noteColor = readGuestColor(); } updateMarkdownFiles(info.files || []); if (info.personal_editor_settings) { compactToggle.checked = info.compact_view !== false; lineToggle.checked = info.editor_line_numbers !== false; previewLineToggle.checked = info.preview_line_numbers === true; lineLinksToggle.checked = info.line_links === true; if (["mono", "system", "serif", "arial", "georgia"].includes(info.font_family)) fontFamily.value = info.font_family; if (["14", "16", "18", "20", "22"].includes(String(info.font_size))) fontSize.value = String(info.font_size); } authorshipMode = info.authorship_mode === "full" ? "full" : "simple"; authorshipColorsEnabled = info.colors_enabled !== false; updateAuthorshipControls(); syncMobileEditorControls(); updateCurrentUser(); return info; } function updatePresence(users) { const entries = Array.isArray(users) ? users : []; presenceUsers = entries.map(entry => typeof entry === "string" ? { name: entry, color: "" } : entry || {}); roomCount.textContent = `${entries.length} ${entries.length === 1 ? "user" : "users"}`; roomUsers.replaceChildren(...presenceUsers.map(user => { const li = document.createElement("li"), dot = document.createElement("span"), label = document.createElement("span"); li.className = "room-user"; dot.className = "room-user__dot"; dot.style.setProperty("--owner", /^#[0-9a-f]{6}$/i.test(user.color || "") ? user.color : defaultColorFor(user.name)); label.textContent = user.name || "Guest"; li.title = label.textContent; li.append(dot, label); return li; })); if (!entries.length) { const li = document.createElement("li"); li.textContent = "No active users"; roomUsers.append(li); } renderGutter(); } function updateLatency(ms) { const text = Number.isFinite(ms) ? `${ms} ms` : "— ms"; socketLatency.textContent = text; const mobileLatency = document.querySelector("#mobile-socket-latency"); if (mobileLatency) mobileLatency.textContent = text; } function setDiagnosticField(name, value) { document.querySelectorAll(`[data-connection-diagnostic="${name}"]`).forEach(node => { node.textContent = value; }); } function formatDiagnosticDuration(milliseconds) { const seconds = Math.max(0, Math.floor(Number(milliseconds || 0) / 1000)); if (seconds < 60) return `${seconds}s`; const minutes = Math.floor(seconds / 60); if (minutes < 60) return `${minutes}m ${seconds % 60}s`; const hours = Math.floor(minutes / 60); return `${hours}h ${minutes % 60}m`; } function renderConnectionDiagnostics(snapshot = {}) { const server = snapshot.server || {}; const runtime = snapshot.runtime || {}; const latency = runtime.latency || {}; const client = server.client || {}; const quality = latency.quality || (runtime.state === "open" ? "measuring" : runtime.state || "waiting"); const qualityLabel = quality.charAt(0).toUpperCase() + quality.slice(1); setDiagnosticField("quality", qualityLabel); setDiagnosticField("latency", Number.isFinite(latency.current) ? `${latency.current} ms · avg ${latency.average} ms · ${latency.minimum}–${latency.maximum} ms` : "Waiting for heartbeat"); setDiagnosticField("jitter", Number.isFinite(latency.jitter) ? `${latency.jitter} ms` : "—"); setDiagnosticField("uptime", runtime.authenticated_at ? formatDiagnosticDuration(runtime.uptime_ms) : runtime.last_connection_uptime_ms ? `last ${formatDiagnosticDuration(runtime.last_connection_uptime_ms)}` : "—"); setDiagnosticField("reconnects", `${runtime.total_reconnects || 0}${runtime.reconnect_attempt ? ` · attempt ${runtime.reconnect_attempt}` : ""}`); const scheme = client.request_scheme ? `${client.request_scheme.toUpperCase()} / ` : ""; setDiagnosticField("transport", `${scheme}${server.transport || "WebSocket"}`); setDiagnosticField("heartbeat", server.heartbeat_interval_ms ? `${Math.round(server.heartbeat_interval_ms / 1000)}s ping · ${Math.round(server.heartbeat_timeout_ms / 1000)}s timeout` : "Waiting for server policy"); const network = runtime.network || {}; const networkParts = [network.effective_type || client.effective_type]; if (Number.isFinite(network.downlink_mbps ?? client.downlink_mbps)) networkParts.push(`${network.downlink_mbps ?? client.downlink_mbps} Mb/s`); if (Number.isFinite(network.rtt_ms ?? client.network_rtt_ms)) networkParts.push(`system RTT ${Math.round(network.rtt_ms ?? client.network_rtt_ms)} ms`); if ((network.save_data ?? client.save_data) === true) networkParts.push("data saver"); setDiagnosticField("network", networkParts.filter(Boolean).join(" · ") || (runtime.online === false ? "Offline" : "Not exposed by browser")); const clientParts = [client.platform, client.timezone, client.language || client.accept_language, client.id ? `id ${client.id}` : null, client.user_agent]; setDiagnosticField("client", clientParts.filter(Boolean).join(" · ") || "Waiting for server data"); setDiagnosticField("server", server.server_version ? `RustPad ${server.server_version} · connection ${server.connection_id}` : "Waiting for server data"); const lastEvent = runtime.last_close ? `Closed ${runtime.last_close.code}${runtime.last_close.reason ? `: ${runtime.last_close.reason}` : ""}` : runtime.last_message_at ? `Message ${new Date(runtime.last_message_at).toLocaleTimeString()}` : "No messages yet"; const traffic = `${formatBytes(runtime.bytes_received)} received · ${formatBytes(runtime.bytes_sent)} sent`; const buffered = runtime.buffered_amount ? ` · ${formatBytes(runtime.buffered_amount)} buffered` : ""; setDiagnosticField("last-event", `${lastEvent} · ${runtime.visibility || document.visibilityState} · ${traffic}${buffered}`); for (const details of [document.querySelector("#connection-details"), document.querySelector("#mobile-connection-details")]) { if (!details) continue; details.classList.remove("is-quality-excellent", "is-quality-good", "is-quality-degraded", "is-quality-poor"); if (["excellent", "good", "degraded", "poor"].includes(quality)) details.classList.add(`is-quality-${quality}`); } } function appendLinkifiedText(container, value) { const text = String(value || ""); const urlPattern = /https?:\/\/[^\s<>{}\[\]"'`]+/gi; let index = 0; for (const match of text.matchAll(urlPattern)) { const start = match.index ?? 0; if (start > index) container.append(document.createTextNode(text.slice(index, start))); let raw = match[0], trail = ""; while (/[),.!?:;]$/.test(raw)) { trail = raw.slice(-1) + trail; raw = raw.slice(0, -1); } try { const url = new URL(raw); if (url.protocol === "http:" || url.protocol === "https:") { const link = document.createElement("a"); link.href = url.href; link.textContent = raw; link.target = "_blank"; link.rel = "noopener noreferrer"; container.append(link); } else container.append(document.createTextNode(raw)); } catch { container.append(document.createTextNode(raw)); } if (trail) container.append(document.createTextNode(trail)); index = start + match[0].length; } if (index < text.length) container.append(document.createTextNode(text.slice(index))); } function appendChatMessage(message) { const empty = chatMessages.querySelector(".chat-empty"); empty?.remove(); const row = document.createElement("p"); row.className = "chat-message"; const author = document.createElement("strong"); author.textContent = message.sender; const text = document.createElement("span"); appendLinkifiedText(text, message.text); row.append(author, text); chatMessages.append(row); while (chatMessages.children.length > 100) chatMessages.firstElementChild.remove(); chatMessages.scrollTop = chatMessages.scrollHeight; if (message.sender !== nickname && !roomDetails.open) { unreadChat++; chatUnread.hidden = false; chatUnread.textContent = unreadChat > 99 ? "99+" : String(unreadChat); if (mobileChatUnread) { mobileChatUnread.hidden = false; mobileChatUnread.textContent = chatUnread.textContent; } const oldTitle = document.title; if (!document.title.startsWith("● ")) document.title = `● ${oldTitle}`; if (document.hidden && Notification.permission === "granted") new Notification(`${message.sender} wrote in RustPad`, { body: message.text.slice(0, 160), tag: "rustpad-room-chat" }); } } function clearUnread() { unreadChat = 0; chatUnread.hidden = true; chatUnread.textContent = ""; if (mobileChatUnread) { mobileChatUnread.hidden = true; mobileChatUnread.textContent = ""; } document.title = document.title.replace(/^● /, ""); } function setStatus(kind, text) { const className = `status__dot${kind ? ` is-${kind}` : ""}`; document.querySelector("#status-dot").className = className; document.querySelector("#status-text").textContent = text; const mobileDot = document.querySelector("#mobile-status-dot"); const mobileText = document.querySelector("#mobile-status-text"); if (mobileDot) mobileDot.className = className; if (mobileText) mobileText.textContent = text; } function showConnectionNotice(title, message, restored = false) { clearTimeout(connectionNoticeTimer); connectionNoticeTitle.textContent = title; connectionNoticeMessage.textContent = message; connectionNotice.hidden = false; connectionNotice.classList.toggle("is-restored", restored); requestAnimationFrame(() => connectionNotice.classList.add("is-visible")); if (restored) connectionNoticeTimer = window.setTimeout(() => { connectionNotice.classList.remove("is-visible", "is-restored"); connectionNoticeTimer = window.setTimeout(() => { connectionNotice.hidden = true; }, 220); }, 1800); } function hideConnectionNotice() { clearTimeout(connectionNoticeTimer); connectionNotice.classList.remove("is-visible", "is-restored"); connectionNotice.hidden = true; } function handleSocketStatus(status, details = {}) { if (status === "online") { setStatus("online", "Connected"); if (connectionWasInterrupted || details.restored) showConnectionNotice("Connection restored", "Live editing is active again.", true); connectionWasInterrupted = false; return; } if (status === "reconnecting") { connectionWasInterrupted = true; setStatus("offline", "Reconnecting…"); showConnectionNotice("Connection interrupted", details.message || "Trying to reconnect automatically."); return; } setStatus(null, "Connecting…"); } function updateAddressLabel() { document.querySelector(adapter.addressSelector).textContent = `${location.pathname}${location.search}`; } async function renderMermaid() { const nodes = preview.querySelectorAll(".mermaid"); if (!nodes.length) return; try { const { default: mermaid } = await import("https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs"); mermaid.initialize({ startOnLoad: false, theme: getTheme() === "dark" ? "dark" : "default", securityLevel: "strict" }); await mermaid.run({ nodes: [...nodes] }); } catch { nodes.forEach(n => n.insertAdjacentHTML("beforebegin", '
Failed to load Mermaid.
')); } } async function renderCodeHighlight() { const nodes = preview.querySelectorAll('pre code[class^="language-"]:not(.language-mermaid)'); if (!nodes.length) return; try { const hljs = await import("https://cdn.jsdelivr.net/npm/highlight.js@11.11.1/+esm"); nodes.forEach(node => { const lines = node.querySelectorAll(".code-line"); if (!lines.length) { hljs.default.highlightElement(node); return; } const language = [...node.classList].find(name => name.startsWith("language-"))?.slice(9); lines.forEach(line => { try { line.innerHTML = hljs.default.highlight(line.textContent, { language, ignoreIllegals: true }).value; } catch { line.innerHTML = hljs.default.highlightAuto(line.textContent).value; } }); node.classList.add("hljs"); }); } catch { } } function renderParticipantBadges(owners) { if (!participantBadges) return; const people = new Map(); for (const owner of owners) people.set(ownerName(owner), { name: ownerName(owner), compactName: "", color: colorFor(owner) }); for (const user of presenceUsers) { const name = user.name || "Guest"; const color = /^#[0-9a-f]{6}$/i.test(user.color || "") ? user.color : defaultColorFor(name); people.set(name, { name, compactName: user.compact_name || name, color }); } participantBadges.hidden = authorshipMode !== "simple" || people.size < 2; const compact = people.size > 4; participantBadges.replaceChildren(...[...people.values()].map(person => { const badge = document.createElement("span"); badge.className = "participant-badge"; badge.style.setProperty("--owner", person.color); badge.textContent = compact && person.compactName ? person.compactName : person.name; badge.title = person.name; return badge; })); } function syncOwnerLabels() { ownerLabels.querySelectorAll(".owner-label-group[data-content-top]").forEach(group => { group.style.top = `${Number(group.dataset.contentTop) - editor.scrollTop}px`; }); } function syncEditorLayers() { gutter.scrollTop = editor.scrollTop; syncAuthorshipLayer(authorshipLayer, editor); syncOwnerLabels(); } function renderGutter() { document.body.classList.toggle("hide-editor-line-numbers", !lineToggle.checked); document.body.classList.toggle("hide-preview-line-numbers", !previewLineToggle.checked); document.body.classList.toggle("line-links-enabled", lineLinksToggle.checked); gutter.setAttribute("aria-hidden", String(!lineLinksToggle.checked)); const lineCount = Math.max(1, (editor.value.match(/\n/g) || []).length + 1); const lines = Array.from({ length: lineCount }); const owners = authorshipOwners(authorship); const showAuthorship = authorshipColorsEnabled && owners.length > 0; const authorsByLine = showAuthorship ? lineAuthors(editor.value, authorship) : []; const full = authorshipMode === "full"; authorshipLayer.hidden = !showAuthorship; ownerLabels.hidden = !full || !showAuthorship; renderParticipantBadges(authorshipColorsEnabled ? owners : []); document.querySelectorAll("[data-authorship-mode]").forEach(button => button.classList.toggle("active", button.dataset.authorshipMode === authorshipMode)); editorWorkspace.dataset.authorshipMode = authorshipMode; const style = getComputedStyle(editor), lineHeight = parseFloat(style.lineHeight) || 29, fontSize = parseFloat(style.fontSize) || 14, paddingTop = parseFloat(style.paddingTop) || 24, paddingBottom = parseFloat(style.paddingBottom) || 24; gutter.style.paddingTop = `${paddingTop}px`; gutter.style.paddingBottom = `${paddingBottom}px`; gutter.style.lineHeight = `${lineHeight}px`; gutter.innerHTML = lines.map((_, i) => ``).join(""); ownerLabels.style.setProperty("--editor-line-height", `${lineHeight}px`); ownerLabels.style.setProperty("--editor-rendered-font-size", `${fontSize}px`); if (full) { let previousAuthorSignature = null; ownerLabels.innerHTML = lines.map((_, i) => { const authors = authorsByLine[i] || []; if (!authors.length) return ""; const top = paddingTop + i * lineHeight + lineHeight / 2; const signature = authors.map(owner => ownerName(owner)).sort((a, b) => a.localeCompare(b)).join("\u0000"); if (signature === previousAuthorSignature) return ""; previousAuthorSignature = signature; const badges = authors.map(owner => `${escapeHtml(ownerName(owner))}`).join(""); return `${badges}`; }).join(""); } else ownerLabels.replaceChildren(); if (showAuthorship) renderAuthorshipLayer(authorshipLayer, editor, authorship, colorFor); else authorshipLayer.replaceChildren(); const linkedLine = lineFromHash(location.hash, lineCount); gutter.querySelector(`[data-line="${linkedLine}"]`)?.classList.add("is-linked"); syncEditorLayers(); } function revealLinkedLine() { if (!location.hash || location.hash === lastRevealedLineHash) return; const lineCount = Math.max(1, (editor.value.match(/\n/g) || []).length + 1); const line = lineFromHash(location.hash, lineCount); if (!line) return; const offset = lineStartOffset(editor.value, line); if (offset == null) return; const style = getComputedStyle(editor); const lineHeight = parseFloat(style.lineHeight) || 29; const paddingTop = parseFloat(style.paddingTop) || 24; const lineTop = paddingTop + (line - 1) * lineHeight; const lineBottom = lineTop + lineHeight; if (lineTop < editor.scrollTop || lineBottom > editor.scrollTop + editor.clientHeight) { editor.scrollTop = Math.max(0, lineTop - Math.max(lineHeight, editor.clientHeight * 0.25)); } editor.setSelectionRange(offset, offset); lastRevealedLineHash = location.hash; gutter.querySelectorAll(".line-number-button.is-linked").forEach(button => button.classList.remove("is-linked")); gutter.querySelector(`[data-line="${line}"]`)?.classList.add("is-linked"); syncEditorLayers(); } function escapeHtml(v) { return String(v).replace(/[&<>"']/g, c => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c])); } function formatDate(value) { const raw = String(value ?? "").trim(); let normalized = raw; if (/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}(?:\.\d+)?[+-]\d{2}$/.test(normalized)) normalized = normalized.replace(" ", "T") + ":00"; else if (/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}(?:\.\d+)?[+-]\d{2}:\d{2}$/.test(normalized)) normalized = normalized.replace(" ", "T"); else if (/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}(?:\.\d+)?$/.test(normalized)) normalized = normalized.replace(" ", "T") + "Z"; const date = new Date(normalized); return Number.isNaN(date.getTime()) ? raw : date.toLocaleString("pl-PL", { year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", second: "2-digit" }); } function previewNodeMarkdownParts(current) { if (current.nodeType !== Node.ELEMENT_NODE) return { open: "", close: "", atomic: null }; const tag = current.tagName.toLowerCase(); if (tag === "strong" || tag === "b") return { open: "**", close: "**", atomic: null }; if (tag === "em" || tag === "i") return { open: "*", close: "*", atomic: null }; if (tag === "s" || tag === "del") return { open: "~~", close: "~~", atomic: null }; if (tag === "mark") return { open: "==", close: "==", atomic: null }; if (tag === "code") return { open: "`", close: "`", atomic: null }; if (tag === "sub") return { open: "~", close: "~", atomic: null }; if (tag === "sup" && !current.classList.contains("footnote-ref")) return { open: "^", close: "^", atomic: null }; const fileAlias = current.getAttribute("data-file-alias"); const fileName = current.getAttribute("data-file-name"); if (tag === "a" && fileAlias === "file" && fileName) return { open: `[file=${fileName},`, close: "]", atomic: null }; if (tag === "a") return { open: "[", close: `](${current.getAttribute("href") || "#"})`, atomic: null }; if (tag === "img") { const alt = current.getAttribute("alt") || ""; if (fileAlias === "image" && fileName) { const safeAlt = alt.replace(/\]/g, ")").replace(/[\r\n]+/g, " "); return { open: "", close: "", atomic: `[image=${fileName},${safeAlt}]` }; } const src = current.getAttribute("src") || ""; const title = current.getAttribute("title"); return { open: "", close: "", atomic: `}"` : ""})` }; } if (tag === "br") return { open: "", close: "", atomic: "\n" }; return { open: "", close: "", atomic: null }; } function previewNodeMarkdown(current) { if (current.nodeType === Node.TEXT_NODE) return (current.nodeValue || "").replace(/\u00a0/g, " "); if (current.nodeType !== Node.ELEMENT_NODE) return ""; const parts = previewNodeMarkdownParts(current); if (parts.atomic !== null) return parts.atomic; const body = [...current.childNodes].map(previewNodeMarkdown).join(""); return `${parts.open}${body}${parts.close}`; } function markdownFromPreview(node) { return [...node.childNodes].map(previewNodeMarkdown).join(""); } function markdownPointOffset(root, container, offset) { let result = 0; let found = false; const contains = (parent, child) => parent === child || (parent.nodeType === Node.ELEMENT_NODE && parent.contains(child)); const walk = (current, isRoot = false) => { if (found) return; if (current === container) { if (current.nodeType === Node.TEXT_NODE) result += Math.max(0, Math.min(offset, (current.nodeValue || "").length)); else if (current.nodeType === Node.ELEMENT_NODE) { const parts = isRoot ? { open: "", close: "", atomic: null } : previewNodeMarkdownParts(current); if (parts.atomic !== null) result += offset > 0 ? parts.atomic.length : 0; else { result += parts.open.length; const children = [...current.childNodes]; for (let index = 0; index < Math.min(offset, children.length); index++) result += previewNodeMarkdown(children[index]).length; } } found = true; return; } if (current.nodeType === Node.TEXT_NODE) { result += (current.nodeValue || "").length; return; } if (current.nodeType !== Node.ELEMENT_NODE) return; const parts = isRoot ? { open: "", close: "", atomic: null } : previewNodeMarkdownParts(current); if (parts.atomic !== null) { result += parts.atomic.length; return; } result += parts.open.length; for (const child of current.childNodes) { if (contains(child, container)) { walk(child); return; } result += previewNodeMarkdown(child).length; } result += parts.close.length; }; walk(root, true); return found ? result : 0; } function previewCaretOffset(target) { const selection = window.getSelection(); if (!selection?.rangeCount) return 0; const range = selection.getRangeAt(0); if (!target.contains(range.startContainer)) return 0; const prefix = range.cloneRange(); prefix.selectNodeContents(target); prefix.setEnd(range.startContainer, range.startOffset); return prefix.toString().length; } function placePreviewCaret(target, offset) { const walker = document.createTreeWalker(target, NodeFilter.SHOW_TEXT); let remaining = Math.max(0, offset), node; while ((node = walker.nextNode())) { if (remaining <= node.nodeValue.length) { const range = document.createRange(); range.setStart(node, remaining); range.collapse(true); const selection = window.getSelection(); selection.removeAllRanges(); selection.addRange(range); return; } remaining -= node.nodeValue.length; } const range = document.createRange(); range.selectNodeContents(target); range.collapse(false); const selection = window.getSelection(); selection.removeAllRanges(); selection.addRange(range); } function deactivatePreviewEdit(target) { if (!target) return; target.removeAttribute("contenteditable"); target.removeAttribute("spellcheck"); target.classList.remove("preview-editable--active"); } function activatePreviewEdit(target, offset = null) { if (!target) return; const active = preview.querySelector('.preview-editable[contenteditable="true"]'); if (active && active !== target) { const targetIndex = [...preview.querySelectorAll(".preview-editable")].indexOf(target); active.blur(); target = preview.querySelectorAll(".preview-editable")[targetIndex]; if (!target) return; } target.setAttribute("contenteditable", "true"); target.setAttribute("spellcheck", "true"); target.classList.add("preview-editable--active"); target.focus({ preventScroll: true }); placePreviewCaret(target, offset == null ? previewCaretOffset(target) : offset); } function movePreviewCaret(target, direction) { const editables = [...preview.querySelectorAll(".preview-editable")]; const index = editables.indexOf(target); if (index < 0 || !editables[index + direction]) return false; const offset = previewCaretOffset(target); target.blur(); const next = [...preview.querySelectorAll(".preview-editable")][index + direction]; if (!next) return false; activatePreviewEdit(next, offset); next.scrollIntoView({ block: "nearest" }); return true; } function continueIndentation(event) { if (event.key !== "Enter" || event.shiftKey || event.ctrlKey || event.metaKey || event.altKey) return; const start = editor.selectionStart, end = editor.selectionEnd; const lineStart = editor.value.lastIndexOf("\n", start - 1) + 1; const current = editor.value.slice(lineStart, start); const indent = (current.match(/^[ \t]*/) || [""])[0]; if (!indent) return; event.preventDefault(); editor.setRangeText(`\n${indent}`, start, end, "end"); editor.dispatchEvent(new Event("input", { bubbles: true })); } function formatBytes(bytes) { const value = Math.max(0, Number(bytes) || 0), units = ["B", "KB", "MB", "GB", "TB"]; let size = value, index = 0; while (size >= 1024 && index < units.length - 1) { size /= 1024; index++; } return `${index === 0 ? Math.round(size) : size.toFixed(size >= 10 ? 1 : 2)} ${units[index]}`; } function replaceTableCell(line, index, value) { const leading = line.trimStart().startsWith("|"), trailing = line.trimEnd().endsWith("|"); let body = line.trim(); if (leading) body = body.slice(1); if (trailing) body = body.slice(0, -1); const cells = body.split("|").map(cell => cell.trim()); while (cells.length <= index) cells.push(""); cells[index] = value.replace(/\|/g, "|"); return `${leading ? "| " : ""}${cells.join(" | ")}${trailing ? " |" : ""}`; } function sourceLineBounds(lineIndex) { const lines = editor.value.split("\n"); if (lineIndex < 0 || lineIndex >= lines.length) return null; let start = 0; for (let index = 0; index < lineIndex; index++) start += lines[index].length + 1; return { start, end: start + lines[lineIndex].length, text: lines[lineIndex], lineIndex }; } function tableCellBounds(line, cellIndex) { const first = line.search(/\S|$/); const trailingWhitespace = (line.match(/\s*$/) || [""])[0].length; let bodyStart = first; let bodyEnd = line.length - trailingWhitespace; if (line[bodyStart] === "|") bodyStart++; if (bodyEnd > bodyStart && line[bodyEnd - 1] === "|") bodyEnd--; const body = line.slice(bodyStart, bodyEnd); const segments = []; let segmentStart = 0; for (let index = 0; index <= body.length; index++) { if (index === body.length || body[index] === "|") { const raw = body.slice(segmentStart, index); const left = (raw.match(/^\s*/) || [""])[0].length; const right = (raw.match(/\s*$/) || [""])[0].length; segments.push({ start: bodyStart + segmentStart + left, end: bodyStart + index - right }); segmentStart = index + 1; } } return segments[cellIndex] || { start: bodyStart, end: bodyStart }; } function editableSourceBounds(target) { const lineIndex = Number(target?.dataset.sourceLine) - 1; const line = sourceLineBounds(lineIndex); if (!line) return null; if (target.dataset.tableCell !== undefined) { const cell = tableCellBounds(line.text, Number(target.dataset.tableCell)); return { start: line.start + cell.start, end: line.start + cell.end, line }; } const prefix = target.dataset.rawSourceEdit === "true" ? "" : (target.dataset.sourcePrefix || ""); const suffix = target.dataset.rawSourceEdit === "true" ? "" : (target.dataset.sourceSuffix || ""); return { start: Math.min(line.end, line.start + prefix.length), end: Math.max(line.start, line.end - suffix.length), line, }; } function editableAtBoundary(container, offset, preferPrevious = false) { const element = container.nodeType === Node.ELEMENT_NODE ? container : container.parentElement; const direct = element?.closest?.(".preview-editable"); if (direct && preview.contains(direct)) return direct; if (container.nodeType !== Node.ELEMENT_NODE) return null; const children = [...container.childNodes]; const candidate = preferPrevious ? children[Math.max(0, offset - 1)] : children[Math.min(offset, children.length - 1)]; const candidates = candidate ? [candidate] : []; for (const node of candidates) { const candidateElement = node.nodeType === Node.ELEMENT_NODE ? node : node.parentElement; if (candidateElement?.matches?.(".preview-editable")) return candidateElement; const nested = candidateElement?.querySelectorAll?.(".preview-editable"); if (nested?.length) return preferPrevious ? nested[nested.length - 1] : nested[0]; } const source = element?.closest?.(".preview-source-line"); const nested = source?.querySelectorAll?.(".preview-editable"); return nested?.length ? (preferPrevious ? nested[nested.length - 1] : nested[0]) : null; } function previewPointSourceOffset(container, offset, preferPrevious = false) { const target = editableAtBoundary(container, offset, preferPrevious); const bounds = editableSourceBounds(target); if (!target || !bounds) return null; let bodyOffset; if (target === container || target.contains(container)) bodyOffset = markdownPointOffset(target, container, offset); else bodyOffset = preferPrevious ? bounds.end - bounds.start : 0; return { offset: Math.max(bounds.start, Math.min(bounds.end, bounds.start + bodyOffset)), target, bounds, }; } function previewSelectionSourceRange({ expandWholeLines = false } = {}) { const selection = window.getSelection(); if (!selection?.rangeCount || selection.isCollapsed) return null; const range = selection.getRangeAt(0); const startInside = range.startContainer === preview || preview.contains(range.startContainer); const endInside = range.endContainer === preview || preview.contains(range.endContainer); if (!startInside || !endInside) return null; const startPoint = previewPointSourceOffset(range.startContainer, range.startOffset, false); const endPoint = previewPointSourceOffset(range.endContainer, range.endOffset, true); if (!startPoint || !endPoint) return null; let start = Math.min(startPoint.offset, endPoint.offset); let end = Math.max(startPoint.offset, endPoint.offset); if (expandWholeLines && startPoint.bounds.line.lineIndex !== endPoint.bounds.line.lineIndex) { if (start === startPoint.bounds.start) start = startPoint.bounds.line.start; if (end === endPoint.bounds.end) { end = endPoint.bounds.line.end; if (end < editor.value.length && editor.value[end] === "\n") end++; } } return { start, end }; } function deletePreviewSelection() { const range = previewSelectionSourceRange({ expandWholeLines: true }); if (!range || range.end <= range.start) return false; editor.setRangeText("", range.start, range.end, "end"); editor.dispatchEvent(new Event("input", { bubbles: true })); return true; } function previewShortcutFormat(event) { const primary = event.ctrlKey || event.metaKey; if (primary && !event.shiftKey && event.key.toLowerCase() === "b") return "bold"; if (primary && !event.shiftKey && event.key.toLowerCase() === "i") return "italic"; if (primary && event.shiftKey && event.key.toLowerCase() === "x") return "strike"; if (primary && !event.shiftKey && event.key.toLowerCase() === "k") return "link"; if (primary && event.shiftKey && event.key === "7") return "number"; if (primary && event.shiftKey && event.key === "8") return "bullet"; if (primary && event.shiftKey && event.key === "9") return "task"; if (event.altKey && /^[1-4]$/.test(event.key)) return `heading${event.key}`; return null; } function syncPreviewScroll() { if (activeView() !== "split") return; const editorRange = Math.max(0, editor.scrollHeight - editor.clientHeight); const previewRange = Math.max(0, preview.scrollHeight - preview.clientHeight); const ratio = editorRange > 0 ? editor.scrollTop / editorRange : 0; preview.scrollTop = ratio * previewRange; } function render() { if (uiState.mode === "markdown") { preview.classList.remove("preview--raw"); preview.innerHTML = renderMarkdown(editor.value); scheduleAliasFileRefresh(editor.value); document.querySelector("#preview-label").textContent = "Preview (mermaid / markdown)"; renderMermaid(); renderCodeHighlight(); } else { preview.classList.add("preview--raw"); preview.innerHTML = editor.value.split("\n").map((line, index) => `${escapeHtml(e.message)}
Loading…
'; try { const revisions = await adapter.loadHistory(accessToken); list.innerHTML = revisions.length ? revisions.map((r, i) => { const snippet = escapeHtml(r.content.trim().split("\n").slice(0, 3).join(" · ").slice(0, 150) || "Empty note"); const author = r.author || "Unknown author"; return `${snippet}
No history yet.
'; for (const r of revisions) { list.querySelector(`[data-preview="${r.id}"]`)?.addEventListener("click", () => { const el = list.querySelector(`#preview-${r.id}`); el.hidden = !el.hidden; el.textContent = r.content; }); list.querySelector(`[data-revision="${r.id}"]`)?.addEventListener("click", async () => { await adapter.restoreRevision(r.id, accessToken); toast("Version restored"); }); } } catch (e) { list.innerHTML = `${escapeHtml(e.message)}
`; } }); document.querySelector("#close-history").addEventListener("click", () => { historyPanel.classList.remove("open"); historyPanel.setAttribute("aria-hidden", "true"); document.body.classList.remove("history-open"); }); const deleteNoteButton = document.querySelector("#delete-note"); if (deleteNoteButton && adapter.deleteNote) deleteNoteButton.addEventListener("click", async () => { try { await adapter.deleteNote(info, accessToken); } catch (error) { toast(error.message); } }); window.addEventListener("error", event => { setStatus("offline", "Application error"); console.error(event.error || event.message); }); window.addEventListener("unhandledrejection", event => { setStatus("offline", "Application error"); console.error(event.reason); }); initialize(); }