diff --git a/Cargo.lock b/Cargo.lock index ff9c5f6..458cb2e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1332,7 +1332,7 @@ dependencies = [ [[package]] name = "rustpad" -version = "0.0.2" +version = "0.0.3" dependencies = [ "argon2", "axum", diff --git a/src/api.rs b/src/api.rs index d597d4f..13ae536 100644 --- a/src/api.rs +++ b/src/api.rs @@ -768,6 +768,7 @@ async fn serve_token_file(state: &SharedState, token: &str, filename: &str) -> R HeaderValue::from_str(mime.as_ref()).unwrap_or_else(|_| HeaderValue::from_static("application/octet-stream")), ); response.headers_mut().insert(header::X_CONTENT_TYPE_OPTIONS, HeaderValue::from_static("nosniff")); + response.headers_mut().insert(header::CACHE_CONTROL, HeaderValue::from_static("public, max-age=600")); Ok(response) } diff --git a/src/db.rs b/src/db.rs index 949ce2a..6b3b2ed 100644 --- a/src/db.rs +++ b/src/db.rs @@ -404,6 +404,23 @@ struct PublishedPageRow { updated_at: String, } + +#[derive(Debug, Clone, FromRow)] +struct PostgresPublishedPageRow { + token: String, + pad_id: Option, + note_id: Option, + allow_task_updates: bool, + title: String, + content: String, + updated_at: String, +} + +impl From for PublishedPage { + fn from(value: PostgresPublishedPageRow) -> Self { + Self { token: value.token, pad_id: value.pad_id, note_id: value.note_id, allow_task_updates: value.allow_task_updates, title: value.title, content: value.content, updated_at: value.updated_at } + } +} impl From for PublishedPage { fn from(value: PublishedPageRow) -> Self { Self { @@ -453,40 +470,47 @@ pub async fn publish_note(pool: &Database, note_id: i64) -> Result Result, sqlx::Error> { + if pool.kind() == DatabaseKind::Postgres { + return Ok(sqlx::query_as::<_, PostgresPublishedPageRow>(queries::Q021_POSTGRES) + .bind(token).fetch_optional(pool.pool()).await?.map(Into::into)); + } Ok(sqlx::query_as::<_, PublishedPageRow>(queries::get(pool.kind(), queries::Q021)) - .bind(token) - .fetch_optional(pool.pool()) - .await? - .map(Into::into)) + .bind(token).fetch_optional(pool.pool()).await?.map(Into::into)) } pub async fn pad_public_task_updates(pool: &Database, pad_id: i64) -> Result { + if pool.kind() == DatabaseKind::Postgres { + return Ok(sqlx::query_scalar::<_, bool>(queries::Q044_POSTGRES) + .bind(pad_id).fetch_optional(pool.pool()).await?.unwrap_or(false)); + } let value = sqlx::query_scalar::<_, i64>(queries::get(pool.kind(), queries::Q044)) - .bind(pad_id) - .fetch_optional(pool.pool()) - .await? - .unwrap_or(0); + .bind(pad_id).fetch_optional(pool.pool()).await?.unwrap_or(0); Ok(value != 0) } pub async fn note_public_task_updates(pool: &Database, note_id: i64) -> Result { + if pool.kind() == DatabaseKind::Postgres { + return Ok(sqlx::query_scalar::<_, bool>(queries::Q045_POSTGRES) + .bind(note_id).fetch_optional(pool.pool()).await?.unwrap_or(false)); + } let value = sqlx::query_scalar::<_, i64>(queries::get(pool.kind(), queries::Q045)) - .bind(note_id) - .fetch_optional(pool.pool()) - .await? - .unwrap_or(0); + .bind(note_id).fetch_optional(pool.pool()).await?.unwrap_or(0); Ok(value != 0) } pub async fn set_pad_public_task_updates(pool: &Database, pad_id: i64, allow: bool) -> Result<(), sqlx::Error> { publish_pad(pool, pad_id).await?; - sqlx::query(queries::get(pool.kind(), queries::Q040)).bind(if allow { 1i64 } else { 0i64 }).bind(pad_id).execute(pool.pool()).await?; + let mut query = sqlx::query(queries::get(pool.kind(), queries::Q040)); + query = if pool.kind() == DatabaseKind::Postgres { query.bind(allow) } else { query.bind(if allow { 1i64 } else { 0i64 }) }; + query.bind(pad_id).execute(pool.pool()).await?; Ok(()) } pub async fn set_note_public_task_updates(pool: &Database, note_id: i64, allow: bool) -> Result<(), sqlx::Error> { publish_note(pool, note_id).await?; - sqlx::query(queries::get(pool.kind(), queries::Q041)).bind(if allow { 1i64 } else { 0i64 }).bind(note_id).execute(pool.pool()).await?; + let mut query = sqlx::query(queries::get(pool.kind(), queries::Q041)); + query = if pool.kind() == DatabaseKind::Postgres { query.bind(allow) } else { query.bind(if allow { 1i64 } else { 0i64 }) }; + query.bind(note_id).execute(pool.pool()).await?; Ok(()) } diff --git a/src/queries.rs b/src/queries.rs index bea45d7..5dd191f 100644 --- a/src/queries.rs +++ b/src/queries.rs @@ -73,3 +73,7 @@ pub const Q043: &str = "UPDATE notes SET content = ?, updated_at = CURRENT_TIMES pub const Q044: &str = "SELECT CASE WHEN allow_task_updates THEN 1 ELSE 0 END FROM published_pages WHERE pad_id = ?"; pub const Q045: &str = "SELECT CASE WHEN allow_task_updates THEN 1 ELSE 0 END FROM published_pages WHERE note_id = ?"; + +pub const Q021_POSTGRES: &str = "SELECT pp.token, pp.pad_id, pp.note_id, pp.allow_task_updates, COALESCE(p.title, n.title) AS title, COALESCE(p.content, n.content) AS content, COALESCE(p.updated_at, n.updated_at) AS updated_at FROM published_pages pp LEFT JOIN pads p ON p.id = pp.pad_id LEFT JOIN notes n ON n.id = pp.note_id WHERE pp.token = $1"; +pub const Q044_POSTGRES: &str = "SELECT allow_task_updates FROM published_pages WHERE pad_id = $1"; +pub const Q045_POSTGRES: &str = "SELECT allow_task_updates FROM published_pages WHERE note_id = $1"; diff --git a/static/js/editor-format.js b/static/js/editor-format.js index a2ff0a6..bbd4563 100644 --- a/static/js/editor-format.js +++ b/static/js/editor-format.js @@ -62,6 +62,7 @@ export function bindFormatShortcuts(editor) { editor.addEventListener("keydown", event => { const primary = event.ctrlKey || event.metaKey; let format = null; + if (primary && !event.shiftKey && event.key.toLowerCase() === "z") return; // native textarea undo if (primary && !event.shiftKey && event.key.toLowerCase() === "b") format = "bold"; else if (primary && !event.shiftKey && event.key.toLowerCase() === "i") format = "italic"; else if (primary && event.shiftKey && event.key.toLowerCase() === "x") format = "strike"; diff --git a/static/js/markdown.js b/static/js/markdown.js index f376855..c803561 100644 --- a/static/js/markdown.js +++ b/static/js/markdown.js @@ -115,13 +115,13 @@ export function renderMarkdown(source) { closeList(); const headers = splitTableRow(line); html += `
`; - headers.forEach((cell, i) => html += ``); + headers.forEach((cell, i) => html += ``); html += ``; index += 2; while (index < lines.length && lines[index].includes("|") && lines[index].trim()) { const cells = splitTableRow(lines[index]); html += ``; - headers.forEach((_, i) => html += ``); + headers.forEach((_, i) => html += ``); html += ``; index++; } @@ -144,11 +144,11 @@ export function renderMarkdown(source) { } else if (task) { if (list !== "ul") { closeList(); html += `
    `; list = "ul"; } const checked = task[2].toLowerCase() === "x"; - html += `
  • ${inline(task[3])}
  • `; + html += `
  • ${inline(task[3])}
  • `; } else if (ul || ol) { const type = ul ? "ul" : "ol"; if (list !== type) { closeList(); html += `<${type}>`; list = type; } - html += `${inline((ul || ol)[1])}`; + html += `${inline((ul || ol)[1])}`; } else { closeList(); const definition = index + 1 < lines.length && /^:\s+/.test(lines[index + 1]); @@ -160,8 +160,8 @@ export function renderMarkdown(source) { } html += ``; } else if (/^---+$/.test(line.trim())) html += ``; - else if (line.startsWith("> ")) html += `${inline(line.slice(2))}`; - else if (line.trim()) html += `${inline(line)}

    `; + else if (line.startsWith("> ")) html += ` ")}>${inline(line.slice(2))}`; + else if (line.trim()) html += `${inline(line)}

    `; else html += `
    `; } } diff --git a/static/js/note.js b/static/js/note.js index 9b648d4..fe594cf 100644 --- a/static/js/note.js +++ b/static/js/note.js @@ -41,6 +41,31 @@ function renderGutter(){ 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();} @@ -59,7 +84,7 @@ async function loadFiles({open=false}={}){ 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.querySelector("#identity-form").addEventListener("submit",e=>{e.preventDefault();nickname=document.querySelector("#nickname").value.trim();setNickname(nickname);identityDialog.close();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();}}); 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 prefix=target.dataset.sourcePrefix||"",suffix=target.dataset.sourceSuffix||"";const next=prefix+(target.innerText||"").replace(/\n/g," ")+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}); +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();}); diff --git a/static/js/pad.js b/static/js/pad.js index a8a8600..89e3d16 100644 --- a/static/js/pad.js +++ b/static/js/pad.js @@ -41,6 +41,31 @@ function renderGutter(){ 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();} @@ -57,7 +82,7 @@ function connect(){socket?.stop();socket=new PadSocket({slug,password,nickname,o async function initialize(){try{info=await api(`/api/pads/${encodeURIComponent(slug)}`);document.title=`${info.title} · RustPad`;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));if(info.protected&&!password)passwordDialog.showModal();else{loadFiles();connect();}}catch(e){document.body.innerHTML=`

    Note not found

    ${escapeHtml(e.message)}

    `;}} document.querySelector("#identity-form").addEventListener("submit",e=>{e.preventDefault();nickname=document.querySelector("#nickname").value.trim();setNickname(nickname);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();}}); 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 prefix=target.dataset.sourcePrefix||"",suffix=target.dataset.sourceSuffix||"";const next=prefix+(target.innerText||"").replace(/\n/g," ")+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}); +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/pads/${encodeURIComponent(slug)}/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/pads/${encodeURIComponent(slug)}/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;sessionStorage.setItem(`rustpad:pad:${slug}:password`,password);document.querySelector("#password-error").textContent="";connect();}); diff --git a/static/note.html b/static/note.html index 5989037..4f19465 100644 --- a/static/note.html +++ b/static/note.html @@ -1,5 +1,5 @@ __NOTE_TITLE__ · RustPad
    __WORKSPACE_TITLE__

    __NOTE_TITLE__

    More
    Editor
    Markdown preview
    · · Changes are saved automatically
    -

    Keyboard shortcuts

    Use Ctrl on Windows/Linux or Cmd on macOS.

    Ctrl/Cmd+BBoldCtrl/Cmd+IItalicCtrl/Cmd+Shift+XStrikethroughCtrl/Cmd+KLinkCtrl/Cmd+Shift+7Numbered listCtrl/Cmd+Shift+8Bullet listCtrl/Cmd+Shift+9Task listAlt+1…4Headings H1–H4

    Note files

    Copy a direct link or ready Markdown/HTML code.

    What should we call you?

    Your name will be shown next to changes and remembered on this device.

    +

    Keyboard shortcuts

    Use Ctrl on Windows/Linux or Cmd on macOS.

    Ctrl/Cmd+ZUndoCtrl/Cmd+BBoldCtrl/Cmd+IItalicCtrl/Cmd+Shift+XStrikethroughCtrl/Cmd+KLinkCtrl/Cmd+Shift+7Numbered listCtrl/Cmd+Shift+8Bullet listCtrl/Cmd+Shift+9Task listAlt+1…4Headings H1–H4

    Note files

    Copy a direct link or ready Markdown/HTML code.

    What should we call you?

    Your name will be shown next to changes and remembered on this device.

    Protected workspace

    Back
    diff --git a/static/pad.html b/static/pad.html index f64c2aa..05ab18e 100644 --- a/static/pad.html +++ b/static/pad.html @@ -1,5 +1,5 @@ __PAD_TITLE__ · RustPad
    RustPad

    __PAD_TITLE__

    More
    Editor
    Markdown preview
    · · Changes are saved automatically
    -

    Keyboard shortcuts

    Use Ctrl on Windows/Linux or Cmd on macOS.

    Ctrl/Cmd+BBoldCtrl/Cmd+IItalicCtrl/Cmd+Shift+XStrikethroughCtrl/Cmd+KLinkCtrl/Cmd+Shift+7Numbered listCtrl/Cmd+Shift+8Bullet listCtrl/Cmd+Shift+9Task listAlt+1…4Headings H1–H4

    Note files

    Copy a direct link or ready Markdown/HTML code.

    What should we call you?

    Your name will be shown next to changes and remembered on this device.

    +

    Keyboard shortcuts

    Use Ctrl on Windows/Linux or Cmd on macOS.

    Ctrl/Cmd+ZUndoCtrl/Cmd+BBoldCtrl/Cmd+IItalicCtrl/Cmd+Shift+XStrikethroughCtrl/Cmd+KLinkCtrl/Cmd+Shift+7Numbered listCtrl/Cmd+Shift+8Bullet listCtrl/Cmd+Shift+9Task listAlt+1…4Headings H1–H4

    Note files

    Copy a direct link or ready Markdown/HTML code.

    What should we call you?

    Your name will be shown next to changes and remembered on this device.

    Protected note

    Back
${inline(cell)}${inline(cell)}
${inline(cells[i] || "")}${inline(cells[i] || "")}