import { installGlobalDiagnostics, logInfo } from "./logger.js";
installGlobalDiagnostics();
import { api } from "@rustpad/api";
import { copyText } from "@rustpad/clipboard";
import { applyFormat, bindFormatShortcuts } from "@rustpad/editor-format";
import { renderMarkdown } from "@rustpad/markdown";
import { prepareImageFile } from "./image-upload.js";
import { getNickname, getPassword, getAuthToken, setPassword } from "@rustpad/session";
import { bindIdentityDialog } from "./auth-ui.js";
import { NoteSocket } from "@rustpad/socket";
import { askConfirm } from "./modal.js";
import { currentShareUrl, readEditorState, writeEditorState } from "@rustpad/url-state";
const parts=location.pathname.split("/").filter(Boolean), workspaceSlug=parts[1], noteSlug=parts[3];
const editor=document.querySelector("#editor"), preview=document.querySelector("#preview"), editorWorkspace=document.querySelector("#editor-workspace"), gutter=document.querySelector("#line-gutter"), ownerLabels=document.querySelector("#owner-labels");
const modeToggle=document.querySelector("#mode-toggle"), passwordDialog=document.querySelector("#password-dialog"), identityDialog=document.querySelector("#identity-dialog");
const compactToggle=document.querySelector("#compact-toggle"), publicTaskUpdates=document.querySelector("#public-task-updates"), fontFamily=document.querySelector("#font-family"), fontSize=document.querySelector("#font-size");
let password=getPassword(workspaceSlug), nickname=getNickname(), info, socket, saveTimer, applyingRemote=false, uiState=readEditorState(), owners=[];
const lineToggle=document.querySelector("#line-numbers-toggle"); lineToggle.checked=localStorage.getItem("rustpad:line-numbers")!=="off";
compactToggle.checked=localStorage.getItem("rustpad:compact")!=="off";
fontFamily.value=localStorage.getItem("rustpad:font-family")||"mono";
fontSize.value=localStorage.getItem("rustpad:font-size")||"14";
function colorFor(name){let h=0;for(const c of name||"?")h=(h*31+c.charCodeAt(0))%360;return `hsl(${h} 70% 62%)`;}
function toast(text){const el=document.querySelector("#toast");el.textContent=text;el.classList.add("visible");setTimeout(()=>el.classList.remove("visible"),1800);}
function setStatus(kind,text){document.querySelector("#status-dot").className=`status__dot${kind?` is-${kind}`:""}`;document.querySelector("#status-text").textContent=text;}
function updateAddressLabel(){document.querySelector("#note-url").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:"dark",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=>hljs.default.highlightElement(node));}catch{}}
function renderGutter(){
const lineCount=Math.max(1,(editor.value.match(/\n/g)||[]).length+1);
const lines=Array.from({length:lineCount});
owners=owners.slice(0,lineCount);
while(owners.length`${i+1}
`).join("");
ownerLabels.style.setProperty("--editor-line-height",`${lineHeight}px`);
ownerLabels.innerHTML=lines.map((_,i)=>{
const owner=owners[i]||"";
if(!owner)return "";
const top=paddingTop+i*lineHeight-editor.scrollTop;
const label=owner!==owners[i-1]?`${escapeHtml(owner)} `:"";
return ` ${label}`;
}).join("");
document.body.classList.toggle("hide-line-numbers",!lineToggle.checked);
}
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 markdownFromPreview(node){
const walk=current=>{
if(current.nodeType===Node.TEXT_NODE)return current.nodeValue||"";
if(current.nodeType!==Node.ELEMENT_NODE)return "";
const tag=current.tagName.toLowerCase(),body=[...current.childNodes].map(walk).join("");
if(tag==="strong"||tag==="b")return `**${body}**`;
if(tag==="em"||tag==="i")return `*${body}*`;
if(tag==="s"||tag==="del")return `~~${body}~~`;
if(tag==="mark")return `==${body}==`;
if(tag==="code")return "`"+body+"`";
if(tag==="sub")return `~${body}~`;
if(tag==="sup"&&!current.classList.contains("footnote-ref"))return `^${body}^`;
if(tag==="a")return `[${body}](${current.getAttribute("href")||"#"})`;
if(tag==="br")return " ";
return body;
};
return [...node.childNodes].map(walk).join("").replace(/\n/g," ").trim();
}
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 render(){if(uiState.mode==="markdown"){preview.classList.remove("preview--raw");preview.innerHTML=renderMarkdown(editor.value);document.querySelector("#preview-label").textContent="Markdown + Mermaid preview · text and headings are editable";renderMermaid();renderCodeHighlight();}else{preview.classList.add("preview--raw");preview.innerHTML=editor.value.split("\n").map((line,index)=>`${escapeHtml(line)||" "}
`).join("");document.querySelector("#preview-label").textContent="Text preview · editable";}document.querySelector("#characters").textContent=`${editor.value.length} characters`;document.querySelector("#words").textContent=`${editor.value.trim()?editor.value.trim().split(/\s+/).length:0} words`;renderGutter();}
function applyUi({write=false,replace=false}={}){editorWorkspace.className=`workspace view-${uiState.view} editor-workspace-font-${fontFamily.value}`;editorWorkspace.style.setProperty("--editor-font-size",`${fontSize.value}px`);document.body.classList.toggle("compact-editor",compactToggle.checked);document.querySelectorAll("[data-view]").forEach(b=>{const a=b.dataset.view===uiState.view;b.classList.toggle("active",a);b.setAttribute("aria-pressed",String(a));});const markdown=uiState.mode==="markdown";modeToggle.classList.toggle("active",markdown);modeToggle.textContent=markdown?"Markdown":"Text";render();if(write)writeEditorState(uiState,{replace});updateAddressLabel();}
function applyRemote(content,ownerMap){if(content===editor.value&&ownerMap==null)return;const start=editor.selectionStart,end=editor.selectionEnd;applyingRemote=true;editor.value=content;try{owners=JSON.parse(ownerMap||"[]");}catch{owners=[];}editor.setSelectionRange(Math.min(start,content.length),Math.min(end,content.length));applyingRemote=false;render();}
function connect(){socket?.stop();socket=new NoteSocket({workspaceSlug,noteSlug,password,nickname,sessionToken:getAuthToken(),onStatus:s=>setStatus(s==="online"?"online":s==="offline"?"offline":null,s==="online"?"Connected":s==="offline"?"Reconnecting…":"Connecting…"),onAuthenticated:m=>{if(passwordDialog.open)passwordDialog.close();applyRemote(m.content,m.owner_map);editor.focus();},onDocument:m=>{applyRemote(m.content,m.owner_map);document.querySelector("#save-state").textContent=`${m.author?`${m.author} · `:""}${new Date(m.updated_at).toLocaleTimeString("en-US",{hour:"2-digit",minute:"2-digit"})}`;},onError:m=>{document.querySelector("#password-error").textContent=m;if(/nickname|session|account/i.test(m)){if(!identityDialog.open)identityDialog.showModal();}else if(info?.protected&&!passwordDialog.open)passwordDialog.showModal();}});socket.connect();}
function formatBytes(bytes){const value=Number(bytes)||0;if(value<1024)return `${value} B`;if(value<1024*1024)return `${(value/1024).toFixed(1)} KB`;return `${(value/1024/1024).toFixed(1)} MB`;}
async function loadFiles({open=false}={}){
try{
const files=await api(`/api/workspaces/${encodeURIComponent(workspaceSlug)}/notes/${encodeURIComponent(noteSlug)}/files`,{method:"PUT",body:JSON.stringify({password:password||null})});
document.querySelector("#footer-files").textContent=`${files.length} ${files.length===1?"file":"files"}`;
const list=document.querySelector("#files-list");
list.innerHTML=files.length?files.map(file=>`${escapeHtml(file.filename)}
${formatBytes(file.size_bytes)} · ${escapeHtml(file.mime_type)} · ${file.is_attached?"in note":"removed from content"}
Link Markdown ${info?.protected&&password?`Delete `:""}
Copy
`).join(""):'No files uploaded.
';
if(open&&!document.querySelector("#files-dialog").open)document.querySelector("#files-dialog").showModal();
}catch(error){toast(error.message);}
}
bindIdentityDialog({dialog:identityDialog,onIdentity:async value=>{nickname=value;identityDialog.close();document.querySelector("#current-user").textContent=nickname;document.querySelector("#current-user").style.setProperty("--owner",colorFor(nickname));if(info.protected&&!password)passwordDialog.showModal();else{loadFiles();connect();}}});
identityDialog.addEventListener("close",()=>{if(!nickname)queueMicrotask(()=>{if(!identityDialog.open)identityDialog.showModal();});});
async function initialize(){try{info=await api(`/api/workspaces/${encodeURIComponent(workspaceSlug)}/notes/${encodeURIComponent(noteSlug)}`);document.querySelector("#back-workspace").href=`/w/${encodeURIComponent(workspaceSlug)}`;document.title=`${info.title} · ${info.workspace_title}`;publicTaskUpdates.checked=Boolean(info.allow_public_task_updates);applyUi({write:true,replace:true});if(!nickname){identityDialog.showModal();return;}document.querySelector("#current-user").textContent=nickname;document.querySelector("#current-user").style.setProperty("--owner",colorFor(nickname));document.querySelector("#delete-note").hidden=info.note_protected;if(info.protected&&!password)passwordDialog.showModal();else{loadFiles();connect();}}catch(e){document.body.innerHTML=`Note not found ${escapeHtml(e.message)}
`;}}
document.querySelectorAll("[data-view]").forEach(b=>b.addEventListener("click",()=>{uiState={...uiState,view:b.dataset.view};applyUi({write:true});}));modeToggle.addEventListener("click",()=>{uiState={...uiState,mode:uiState.mode==="markdown"?"text":"markdown"};applyUi({write:true});});lineToggle.addEventListener("change",()=>{localStorage.setItem("rustpad:line-numbers",lineToggle.checked?"on":"off");renderGutter();});compactToggle.addEventListener("change",()=>{localStorage.setItem("rustpad:compact",compactToggle.checked?"on":"off");applyUi();});fontFamily.addEventListener("change",()=>{localStorage.setItem("rustpad:font-family",fontFamily.value);applyUi();});fontSize.addEventListener("change",()=>{localStorage.setItem("rustpad:font-size",fontSize.value);applyUi();});
window.addEventListener("popstate",()=>{uiState=readEditorState();applyUi();});window.addEventListener("rustpad:urlchange",updateAddressLabel);document.querySelector("#copy-link").addEventListener("click",async()=>{try{await copyText(currentShareUrl(uiState));toast("Link copied");}catch(e){toast(e.message);}});document.querySelectorAll("[data-format]").forEach(b=>b.addEventListener("click",()=>{applyFormat(editor,b.dataset.format);b.closest("details")?.removeAttribute("open");}));bindFormatShortcuts(editor);document.querySelector("#shortcuts-button").addEventListener("click",()=>document.querySelector("#shortcuts-dialog").showModal());document.querySelector("#close-shortcuts").addEventListener("click",()=>document.querySelector("#shortcuts-dialog").close());preview.addEventListener("change",event=>{const checkbox=event.target.closest(".task-checkbox");if(!checkbox)return;const lineIndex=Number(checkbox.dataset.sourceLine)-1;const lines=editor.value.split("\n");if(lineIndex<0||lineIndex>=lines.length)return;lines[lineIndex]=lines[lineIndex].replace(/^(\s*[-*+]\s+\[)[ xX](\])/,`$1${checkbox.checked?"x":" "}$2`);editor.value=lines.join("\n");editor.dispatchEvent(new Event("input",{bubbles:true}));});preview.addEventListener("keydown",event=>{const target=event.target.closest(".preview-editable");if(!target)return;if(event.key==="Enter"){event.preventDefault();target.blur();}});preview.addEventListener("blur",event=>{const target=event.target.closest(".preview-editable");if(!target)return;const lineIndex=Number(target.dataset.sourceLine)-1;if(lineIndex<0)return;const lines=editor.value.split("\n");const value=markdownFromPreview(target);let next;if(target.dataset.tableCell!==undefined)next=replaceTableCell(lines[lineIndex],Number(target.dataset.tableCell),value);else{const prefix=target.dataset.sourcePrefix||"",suffix=target.dataset.sourceSuffix||"";next=prefix+value+suffix;}if(lines[lineIndex]===next)return;lines[lineIndex]=next;editor.value=lines.join("\n");editor.setSelectionRange(editor.value.length,editor.value.length);editor.dispatchEvent(new Event("input",{bubbles:true}));},{capture:true});
publicTaskUpdates.addEventListener("change",async()=>{publicTaskUpdates.disabled=true;try{await api(`/api/workspaces/${encodeURIComponent(workspaceSlug)}/notes/${encodeURIComponent(noteSlug)}/publish`,{method:"POST",body:JSON.stringify({password:password||null,allow_task_updates:publicTaskUpdates.checked})});toast(publicTaskUpdates.checked?"Public task updates enabled":"Public task updates disabled");}catch(error){publicTaskUpdates.checked=!publicTaskUpdates.checked;toast(error.message);}finally{publicTaskUpdates.disabled=false;}});document.querySelector("#publish-page").addEventListener("click",async()=>{try{const result=await api(`/api/workspaces/${encodeURIComponent(workspaceSlug)}/notes/${encodeURIComponent(noteSlug)}/publish`,{method:"POST",body:JSON.stringify({password:password||null,allow_task_updates:publicTaskUpdates.checked})});const url=new URL(result.url,location.origin).href;await copyText(url);toast("Page link copied");window.open(url,"_blank","noopener");}catch(error){toast(error.message);}});
editor.addEventListener("scroll",()=>{gutter.scrollTop=editor.scrollTop;renderGutter();});editor.addEventListener("input",()=>{const newLines=editor.value.split("\n").length;const cursorLine=editor.value.slice(0,editor.selectionStart).split("\n").length-1;while(owners.lengthsocket?.update(editor.value,JSON.stringify(owners)),250);});
document.querySelector("#password-form").addEventListener("submit",e=>{e.preventDefault();password=document.querySelector("#open-password").value;setPassword(workspaceSlug,password);document.querySelector("#password-error").textContent="";loadFiles();connect();});
const historyPanel=document.querySelector("#history-panel");document.querySelector("#history-button").addEventListener("click",async()=>{historyPanel.classList.add("open");historyPanel.setAttribute("aria-hidden","false");document.body.classList.add("history-open");const list=document.querySelector("#history-list");list.innerHTML='Loading…
';try{const revisions=await api(`/api/workspaces/${encodeURIComponent(workspaceSlug)}/notes/${encodeURIComponent(noteSlug)}/history`,{method:"POST",body:JSON.stringify({password:password||null})});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 `${escapeHtml(author)} ${formatDate(r.created_at)}
${snippet}
Preview Restore
`;}).join(""):'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 api(`/api/workspaces/${encodeURIComponent(workspaceSlug)}/notes/${encodeURIComponent(noteSlug)}/restore`,{method:"POST",body:JSON.stringify({password:password||null,revision_id:r.id})});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");});
document.querySelector("#upload-button").addEventListener("click",()=>document.querySelector("#file-input").click());document.querySelector("#file-input").addEventListener("change",async e=>{let file=e.target.files[0];if(!file)return;if(file.type.startsWith("image/")){file=await prepareImageFile(file);if(!file){e.target.value="";return;}}const form=new FormData();form.append("password",password||"");form.append("file",file);try{const result=await api(`/api/workspaces/${encodeURIComponent(workspaceSlug)}/notes/${encodeURIComponent(noteSlug)}/files`,{method:"POST",body:form,headers:{}});const image=file.type.startsWith("image/");const text=image?``:`[${file.name}](${result.url})`;editor.setRangeText(text,editor.selectionStart,editor.selectionEnd,"end");editor.dispatchEvent(new Event("input"));toast("File uploaded");loadFiles();}catch(err){toast(err.message);}e.target.value="";});
document.querySelector("#files-button").addEventListener("click",()=>loadFiles({open:true}));
document.querySelector("#footer-files").addEventListener("click",()=>loadFiles({open:true}));
document.querySelector("#close-files").addEventListener("click",()=>document.querySelector("#files-dialog").close());
document.querySelector("#files-list").addEventListener("click",async event=>{
const showButton=event.target.closest("[data-show-file-code]");
if(showButton){
const row=showButton.closest(".file-row"), panel=row.querySelector(".file-code"), output=panel.querySelector("textarea");
const absolute=new URL(showButton.dataset.url,location.origin).href;
let text=absolute;
if(showButton.dataset.showFileCode==="markdown")text=showButton.dataset.mime?.startsWith("image/")?``:`[${showButton.dataset.name}](${absolute})`;
if(showButton.dataset.showFileCode==="html")text=(showButton.dataset.mime||"").startsWith("image/")?` `:`${showButton.dataset.name} `;
output.value=text;panel.hidden=false;output.focus();output.select();return;
}
const copyButton=event.target.closest("[data-copy-generated]");
if(copyButton){try{await copyText(copyButton.closest(".file-code").querySelector("textarea").value);toast("Copied");}catch(error){toast(error.message);}return;}
const deleteButton=event.target.closest("[data-delete-file]");
if(deleteButton){
if(!await askConfirm(`Delete file "${deleteButton.dataset.fileName}" permanently?`,{title:"Delete file",confirmText:"Delete",danger:true}))return;
try{await api(`/api/workspaces/${encodeURIComponent(workspaceSlug)}/notes/${encodeURIComponent(noteSlug)}/files/${encodeURIComponent(deleteButton.dataset.deleteFile)}`,{method:"DELETE",body:JSON.stringify({password:password||null})});toast("File deleted");await loadFiles();}catch(error){toast(error.message);}return;
}
});
document.querySelector("#delete-note").addEventListener("click",async()=>{if(!await askConfirm(`Delete note “${info.title}”? This cannot be undone.`,{title:"Delete note",confirmText:"Delete",danger:true}))return;try{await api(`/api/workspaces/${encodeURIComponent(workspaceSlug)}/notes/${encodeURIComponent(noteSlug)}`,{method:"DELETE",body:JSON.stringify({password:password||null})});location.assign(`/w/${encodeURIComponent(workspaceSlug)}`);}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();