This commit is contained in:
Mateusz Gruszczyński
2026-07-24 15:30:53 +02:00
parent 2c46314425
commit 041f586261
10 changed files with 205 additions and 64 deletions
+1
View File
@@ -11,3 +11,4 @@ README.md
Dockerfile* Dockerfile*
docker-compose*.yml docker-compose*.yml
migrate/ migrate/
scripts/*.txt
+2 -1
View File
@@ -13,4 +13,5 @@ data/files/*
venv venv
.venv .venv
migrate/etherpad-dry-run-report.json migrate/etherpad-dry-run-report.json
data/garge data/garage
scripts/*.txt
Generated
+1 -1
View File
@@ -2433,7 +2433,7 @@ dependencies = [
[[package]] [[package]]
name = "rustpad" name = "rustpad"
version = "0.0.15" version = "0.0.17"
dependencies = [ dependencies = [
"argon2", "argon2",
"aws-config", "aws-config",
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "rustpad" name = "rustpad"
version = "0.0.16" version = "0.0.17"
edition = "2024" edition = "2024"
rust-version = "1.94" rust-version = "1.94"
description = "Collaborative Markdown notepad built with Axum, WebSockets and SQLite, PostgreSQL and MySQL" description = "Collaborative Markdown notepad built with Axum, WebSockets and SQLite, PostgreSQL and MySQL"
+49
View File
@@ -0,0 +1,49 @@
# scripts/random_text_generator.py
import random
import string
import sys
from pathlib import Path
def anonymize_text(text: str) -> str:
result = []
for character in text:
if character.islower():
result.append(random.choice(string.ascii_lowercase))
elif character.isupper():
result.append(random.choice(string.ascii_uppercase))
elif character.isdigit():
result.append(random.choice(string.digits))
else:
result.append(character)
return "".join(result)
def main() -> None:
if len(sys.argv) != 2:
print("Usage: python scripts/random_text_generator.py <input_file>")
sys.exit(1)
input_path = Path(sys.argv[1])
if not input_path.is_file():
print(f"File not found: {input_path}")
sys.exit(1)
source_text = input_path.read_text(encoding="utf-8")
anonymized_text = anonymize_text(source_text)
output_path = input_path.with_name(
f"{input_path.stem}_anonymized{input_path.suffix}"
)
output_path.write_text(anonymized_text, encoding="utf-8")
print(f"Anonymized file saved to: {output_path}")
if __name__ == "__main__":
main()
+67 -36
View File
@@ -2201,7 +2201,7 @@ dialog::backdrop {
content: attr(data-source-line); content: attr(data-source-line);
position: absolute; position: absolute;
top: 0; top: 0;
left: -50px; left: var(--preview-line-left, -50px);
width: 32px; width: 32px;
color: #596270; color: #596270;
text-align: right; text-align: right;
@@ -2304,70 +2304,80 @@ dialog::backdrop {
color: #b9c2cf; color: #b9c2cf;
} }
.markdown-body .task-list { /* Task-list layout. */
margin: 0;
padding: 0;
list-style: none
}
.markdown-body .task-list-item { .markdown-body .task-list-item {
position: relative; position: relative;
display: grid; display: grid;
grid-template-columns: 1em minmax(0, 1fr); grid-template-columns: 1em minmax(0, 1fr);
grid-template-rows: 1.32em; grid-template-rows: auto auto;
column-gap: .45em; column-gap: .45em;
align-items: center; align-items: start;
min-height: 1.32em; min-height: 1.32em;
margin: 0; margin: 0;
padding: 0; padding: 0;
line-height: 1.32 line-height: 1.32;
} list-style: none;
.markdown-body .task-list-item::before {
position: absolute;
top: 0;
right: calc(100% + 18px);
width: 32px;
height: 1.32em;
line-height: 1.32em;
transform: none
} }
.markdown-body .task-checkbox { .markdown-body .task-checkbox {
grid-column: 1; grid-column: 1;
grid-row: 1; grid-row: 1;
align-self: start;
appearance: none;
box-sizing: border-box;
flex: 0 0 1em;
width: 1em; width: 1em;
min-width: 1em;
max-width: 1em;
height: 1em; height: 1em;
margin: 0; min-height: 1em;
align-self: center; max-height: 1em;
accent-color: var(--accent); margin: .14em 0 0;
cursor: pointer padding: 0;
border: 1px solid #707887;
border-radius: 2px;
background: var(--surface-2);
box-shadow: none;
cursor: pointer;
} }
.markdown-body .task-list-item>span { .markdown-body .task-checkbox:checked {
border-color: var(--accent);
background-color: var(--accent);
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12'%3E%3Cpath fill='none' stroke='white' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' d='M2 6.5 4.7 9 10 3'/%3E%3C/svg%3E");
background-position: center;
background-repeat: no-repeat;
background-size: .78em .78em;
}
.markdown-body .task-checkbox:focus {
outline: none;
}
.markdown-body .task-checkbox:focus-visible {
box-shadow: 0 0 0 2px rgba(124, 104, 238, .3);
}
.markdown-body .task-list-item > .list-item-content {
grid-column: 2; grid-column: 2;
grid-row: 1; grid-row: 1;
display: block; display: block;
min-width: 0; min-width: 0;
margin: 0; margin: 0;
padding: 0; padding: 0;
line-height: 1.32 line-height: inherit;
} }
.hide-preview-line-numbers .markdown-body .task-list-item::before { .markdown-body .task-list-item > ul,
display: none .markdown-body .task-list-item > ol {
grid-column: 2;
grid-row: 2;
min-width: 0;
} }
.compact-editor .markdown-body .task-list-item { .compact-editor .markdown-body .task-list-item {
grid-template-rows: 1.24em;
min-height: 1.24em; min-height: 1.24em;
line-height: 1.24 line-height: 1.24;
}
.compact-editor .markdown-body .task-list-item::before,
.compact-editor .markdown-body .task-list-item>span {
height: 1.24em;
line-height: 1.24
} }
.markdown-body .footnotes { .markdown-body .footnotes {
@@ -3805,3 +3815,24 @@ dialog::backdrop {
.markdown-body .markdown-toc .toc-level-4, .markdown-body .markdown-toc .toc-level-4,
.markdown-body .markdown-toc .toc-level-5, .markdown-body .markdown-toc .toc-level-5,
.markdown-body .markdown-toc .toc-level-6 { margin-left: 3em; } .markdown-body .markdown-toc .toc-level-6 { margin-left: 3em; }
/* Nested Markdown lists keep markers and source-line numbers in separate gutters. */
.markdown-body ul,
.markdown-body ol {
padding-left: 1.75em;
}
.markdown-body .list-source-line::before {
right: auto;
}
.markdown-body .list-item-content {
min-width: 0;
overflow-wrap: anywhere;
word-break: break-word;
}
.markdown-body .contains-task-items > .task-list-item {
list-style: none;
}
+77 -18
View File
@@ -92,6 +92,63 @@ function normalizeLanguage(value) {
const attrs = (line, editable = false, prefix = "", suffix = "", lineOffset = 0) => ` class="preview-source-line${editable ? " preview-editable" : ""}" data-source-line="${line + lineOffset + 1}"${editable ? ` contenteditable="true" spellcheck="true" data-source-prefix="${escapeHtml(prefix)}" data-source-suffix="${escapeHtml(suffix)}"` : ""}`; const attrs = (line, editable = false, prefix = "", suffix = "", lineOffset = 0) => ` class="preview-source-line${editable ? " preview-editable" : ""}" data-source-line="${line + lineOffset + 1}"${editable ? ` contenteditable="true" spellcheck="true" data-source-prefix="${escapeHtml(prefix)}" data-source-suffix="${escapeHtml(suffix)}"` : ""}`;
const isPlainText = line => !/[`*_~^=\[\]<>|:#]/.test(line) && !/^\s*(?:[-+*>]|\d+\.)\s/.test(line); const isPlainText = line => !/[`*_~^=\[\]<>|:#]/.test(line) && !/^\s*(?:[-+*>]|\d+\.)\s/.test(line);
function listLine(line) {
const match = line.match(/^(\s*)([-*+]|(\d+)\.)\s+(?:\[([ xX])\]\s+)?(.+)$/);
if (!match) return null;
const indent = match[1].replace(/\t/g, " ").length;
return {
indent,
whitespace: match[1],
type: match[3] ? "ol" : "ul",
number: match[3] ? Number(match[3]) : null,
checked: match[4] == null ? null : match[4].toLowerCase() === "x",
text: match[5],
marker: match[3] ? `${match[3]}. ` : `${match[2]} `
};
}
function renderList(lines, start, lineOffset = 0, baseIndent = null, forcedType = null, depth = 0) {
const first = listLine(lines[start]);
if (!first) return null;
const indent = baseIndent == null ? first.indent : baseIndent;
const type = forcedType || first.type;
let index = start;
let body = "";
let hasTask = false;
while (index < lines.length) {
const item = listLine(lines[index]);
if (!item || item.indent < indent || item.indent !== indent || item.type !== type) break;
const sourceLine = index + lineOffset + 1;
const prefix = `${item.whitespace}${item.marker}${item.checked == null ? "" : `[${item.checked ? "x" : " "}] `}`;
const valueAttr = "";
const taskClass = item.checked == null ? "" : " task-list-item";
hasTask ||= item.checked != null;
const gutterOffset = `${(depth + 1) * 1.75}em`;
body += `<li class="preview-source-line list-source-line${taskClass}" data-source-line="${sourceLine}"${valueAttr} style="--list-gutter-offset:${gutterOffset}">`;
if (item.checked != null) {
body += `<input type="checkbox" class="task-checkbox" data-source-line="${sourceLine}"${item.checked ? " checked" : ""}>`;
}
body += `<span class="preview-editable list-item-content" contenteditable="true" spellcheck="true" data-source-line="${sourceLine}" data-source-prefix="${escapeHtml(prefix)}">${inline(item.text)}</span>`;
index++;
while (index < lines.length) {
const nested = listLine(lines[index]);
if (!nested || nested.indent <= indent) break;
const rendered = renderList(lines, index, lineOffset, nested.indent, nested.type, depth + 1);
if (!rendered) break;
body += rendered.html;
index = rendered.end;
}
body += `</li>`;
}
const classAttr = hasTask ? ` class="contains-task-items"` : "";
return {html: `<${type}${classAttr}>${body}</${type}>`, end: index};
}
function splitTableRow(line) { function splitTableRow(line) {
let value = line.trim(); let value = line.trim();
if (value.startsWith("|")) value = value.slice(1); if (value.startsWith("|")) value = value.slice(1);
@@ -138,8 +195,20 @@ function collectHeadings(lines) {
return headings; return headings;
} }
export function alignPreviewLineNumbers(root) {
if (!root) return;
const styles = getComputedStyle(root);
const targetLeft = parseFloat(styles.paddingLeft || "0") - 50;
const rootLeft = root.getBoundingClientRect().left;
root.querySelectorAll(".preview-source-line").forEach(line => {
const lineLeft = line.getBoundingClientRect().left - rootLeft;
line.style.setProperty("--preview-line-left", `${targetLeft - lineLeft}px`);
});
}
export function renderMarkdown(source, lineOffset = 0) { export function renderMarkdown(source, lineOffset = 0) {
let html = "", inCode = false, fence = "", language = "", codeLineStart = null, code = [], codeStart = 0, list = null; let html = "", inCode = false, fence = "", language = "", codeLineStart = null, code = [], codeStart = 0;
const lines = String(source).split("\n"); const lines = String(source).split("\n");
const headings = collectHeadings(lines); const headings = collectHeadings(lines);
const headingByLine = new Map(headings.map(item => [item.index, item])); const headingByLine = new Map(headings.map(item => [item.index, item]));
@@ -159,7 +228,7 @@ export function renderMarkdown(source, lineOffset = 0) {
lines[i] = ""; lines[i] = "";
} }
const closeList = () => { if (list) { html += `</${list}>`; list = null; } }; const closeList = () => {};
const closeCode = () => { const closeCode = () => {
const body = escapeHtml(code.join("\n")); const body = escapeHtml(code.join("\n"));
const lang = normalizeLanguage(language); const lang = normalizeLanguage(language);
@@ -261,29 +330,19 @@ export function renderMarkdown(source, lineOffset = 0) {
} }
const heading = line.match(/^\s{0,4}(#{1,6})\s+(.+?)(?:\s+\{#([A-Za-z][\w:.-]*)\})?\s*$/); const heading = line.match(/^\s{0,4}(#{1,6})\s+(.+?)(?:\s+\{#([A-Za-z][\w:.-]*)\})?\s*$/);
const task = line.match(/^(\s*)[-*+]\s+\[([ xX])\]\s+(.+)$/); const listItem = listLine(line);
const ul = line.match(/^(\s*)[-*+]\s+(.+)$/);
const ol = line.match(/^(\s*)\d+\.\s+(.+)$/);
if (heading) { if (listItem) {
const rendered = renderList(lines, index, lineOffset, listItem.indent, listItem.type);
html += rendered.html;
index = rendered.end - 1;
} else if (heading) {
closeList(); closeList();
const n = heading[1].length; const n = heading[1].length;
const resolved = headingByLine.get(index); const resolved = headingByLine.get(index);
const id = resolved?.id || heading[3] || headingSlug(heading[2]); const id = resolved?.id || heading[3] || headingSlug(heading[2]);
const suffix = heading[3] ? ` {#${heading[3]}}` : ""; const suffix = heading[3] ? ` {#${heading[3]}}` : "";
html += `<h${n} id="${escapeHtml(id)}"${attrs(index, true, `${heading[1]} `, suffix, lineOffset)}>${inline(heading[2])}</h${n}>`; html += `<h${n} id="${escapeHtml(id)}"${attrs(index, true, `${heading[1]} `, suffix, lineOffset)}>${inline(heading[2])}</h${n}>`;
} else if (task) {
if (list !== "ul") { closeList(); html += `<ul class="task-list">`; list = "ul"; }
const checked = task[2].toLowerCase() === "x";
html += `<li class="preview-source-line task-list-item" data-source-line="${index + lineOffset + 1}"><input type="checkbox" class="task-checkbox" data-source-line="${index + lineOffset + 1}"${checked ? " checked" : ""}><span class="preview-editable" contenteditable="true" spellcheck="true" data-source-line="${index + lineOffset + 1}" data-source-prefix="${escapeHtml(`${task[1]}- [${checked ? "x" : " "}] `)}">${inline(task[3])}</span></li>`;
} else if (ul || ol) {
const type = ul ? "ul" : "ol";
if (list !== type) { closeList(); html += `<${type}>`; list = type; }
const match = ul || ol;
const indentWidth = match[1].replace(/\t/g, " ").length;
const prefix = ul ? `${match[1]}- ` : `${match[1]}${(line.match(/^\s*(\d+)\./)||[])[1] || 1}. `;
const indentStyle = indentWidth ? ` style="margin-left:${Math.min(indentWidth, 24) * 0.45}em"` : "";
html += `<li${attrs(index, true, prefix, "", lineOffset)}${indentStyle}>${inline(match[2])}</li>`;
} else { } else {
closeList(); closeList();
const definition = index + 1 < lines.length && /^:\s+/.test(lines[index + 1]); const definition = index + 1 < lines.length && /^:\s+/.test(lines[index + 1]);
+2 -2
View File
@@ -4,7 +4,7 @@ installGlobalDiagnostics();
import { api } from "@rustpad/api"; import { api } from "@rustpad/api";
import { copyText } from "@rustpad/clipboard"; import { copyText } from "@rustpad/clipboard";
import { applyFormat, bindFormatShortcuts } from "@rustpad/editor-format"; import { applyFormat, bindFormatShortcuts } from "@rustpad/editor-format";
import { renderMarkdown } from "@rustpad/markdown"; import { alignPreviewLineNumbers, renderMarkdown } from "@rustpad/markdown";
import { prepareImageFile } from "./image-upload.js"; import { prepareImageFile } from "./image-upload.js";
import { getNickname, getAccessToken, getAuthToken, setAccessToken } from "@rustpad/session"; import { getNickname, getAccessToken, getAuthToken, setAccessToken } from "@rustpad/session";
import { bindIdentityDialog } from "./auth-ui.js"; import { bindIdentityDialog } from "./auth-ui.js";
@@ -132,7 +132,7 @@ function replaceTableCell(line,index,value){
const cells=body.split("|").map(cell=>cell.trim());while(cells.length<=index)cells.push("");cells[index]=value.replace(/\|/g,"&#124;"); const cells=body.split("|").map(cell=>cell.trim());while(cells.length<=index)cells.push("");cells[index]=value.replace(/\|/g,"&#124;");
return `${leading?"| ":""}${cells.join(" | ")}${trailing?" |":""}`; 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)=>`<div class="preview-source-line preview-editable" data-source-line="${index+1}" contenteditable="true" spellcheck="true">${escapeHtml(line)||"<br>"}</div>`).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 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)=>`<div class="preview-source-line preview-editable" data-source-line="${index+1}" contenteditable="true" spellcheck="true">${escapeHtml(line)||"<br>"}</div>`).join("");document.querySelector("#preview-label").textContent="Text preview · editable";}alignPreviewLineNumbers(preview);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 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 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,accessToken,nickname,color:currentUserColor()||null,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"})}`;},onPresence:updatePresence,onLatency:updateLatency,onChat:appendChatMessage,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 connect(){socket?.stop();socket=new NoteSocket({workspaceSlug,noteSlug,password,accessToken,nickname,color:currentUserColor()||null,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"})}`;},onPresence:updatePresence,onLatency:updateLatency,onChat:appendChatMessage,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();}
+2 -2
View File
@@ -4,7 +4,7 @@ installGlobalDiagnostics();
import { api } from "@rustpad/api"; import { api } from "@rustpad/api";
import { copyText } from "@rustpad/clipboard"; import { copyText } from "@rustpad/clipboard";
import { applyFormat, bindFormatShortcuts } from "@rustpad/editor-format"; import { applyFormat, bindFormatShortcuts } from "@rustpad/editor-format";
import { renderMarkdown } from "@rustpad/markdown"; import { alignPreviewLineNumbers, renderMarkdown } from "@rustpad/markdown";
import { prepareImageFile } from "./image-upload.js"; import { prepareImageFile } from "./image-upload.js";
import { getNickname, getAuthToken, getAccessToken, setAccessToken } from "@rustpad/session"; import { getNickname, getAuthToken, getAccessToken, setAccessToken } from "@rustpad/session";
import { bindIdentityDialog } from "./auth-ui.js"; import { bindIdentityDialog } from "./auth-ui.js";
@@ -128,7 +128,7 @@ function replaceTableCell(line,index,value){
const cells=body.split("|").map(cell=>cell.trim());while(cells.length<=index)cells.push("");cells[index]=value.replace(/\|/g,"&#124;"); const cells=body.split("|").map(cell=>cell.trim());while(cells.length<=index)cells.push("");cells[index]=value.replace(/\|/g,"&#124;");
return `${leading?"| ":""}${cells.join(" | ")}${trailing?" |":""}`; 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)=>`<div class="preview-source-line preview-editable" data-source-line="${index+1}" contenteditable="true" spellcheck="true">${escapeHtml(line)||"<br>"}</div>`).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 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)=>`<div class="preview-source-line preview-editable" data-source-line="${index+1}" contenteditable="true" spellcheck="true">${escapeHtml(line)||"<br>"}</div>`).join("");document.querySelector("#preview-label").textContent="Text preview · editable";}alignPreviewLineNumbers(preview);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 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 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();}
+2 -2
View File
@@ -3,7 +3,7 @@ installGlobalDiagnostics();
import { api } from "@rustpad/api"; import { api } from "@rustpad/api";
import { copyText } from "@rustpad/clipboard"; import { copyText } from "@rustpad/clipboard";
import { renderMarkdown } from "@rustpad/markdown"; import { alignPreviewLineNumbers, renderMarkdown } from "@rustpad/markdown";
const token=location.pathname.split("/").filter(Boolean)[1]; const token=location.pathname.split("/").filter(Boolean)[1];
const content=document.querySelector("#public-content"); const content=document.querySelector("#public-content");
@@ -16,7 +16,7 @@ function lockPublicContent(allowTaskUpdates){
content.querySelectorAll('.preview-editable').forEach(node=>node.classList.remove('preview-editable')); content.querySelectorAll('.preview-editable').forEach(node=>node.classList.remove('preview-editable'));
content.querySelectorAll('.task-checkbox').forEach(box=>{box.disabled=!allowTaskUpdates;box.title=allowTaskUpdates?'Update this task':'Task updates are disabled by the owner';}); content.querySelectorAll('.task-checkbox').forEach(box=>{box.disabled=!allowTaskUpdates;box.title=allowTaskUpdates?'Update this task':'Task updates are disabled by the owner';});
} }
async function initialize(){try{const page=await api(`/api/public/${encodeURIComponent(token)}`);document.querySelector("#public-title").textContent=page.title;document.querySelector("#public-meta").textContent=`Updated: ${new Date(page.updated_at).toLocaleString("en-US")}${page.allow_task_updates?" · tasks can be updated":""}`;document.title=`${page.title} · RustPad`;content.innerHTML=renderMarkdown(page.content);lockPublicContent(page.allow_task_updates);await Promise.all([renderMermaid(),renderCodeHighlight()]);}catch(error){content.innerHTML=`<p class="error">${String(error.message)}</p>`;}} async function initialize(){try{const page=await api(`/api/public/${encodeURIComponent(token)}`);document.querySelector("#public-title").textContent=page.title;document.querySelector("#public-meta").textContent=`Updated: ${new Date(page.updated_at).toLocaleString("en-US")}${page.allow_task_updates?" · tasks can be updated":""}`;document.title=`${page.title} · RustPad`;content.innerHTML=renderMarkdown(page.content);alignPreviewLineNumbers(content);lockPublicContent(page.allow_task_updates);await Promise.all([renderMermaid(),renderCodeHighlight()]);}catch(error){content.innerHTML=`<p class="error">${String(error.message)}</p>`;}}
content.addEventListener("change",async event=>{const box=event.target.closest(".task-checkbox");if(!box||box.disabled)return;const previous=!box.checked;box.disabled=true;try{const page=await api(`/api/public/${encodeURIComponent(token)}/tasks`,{method:"POST",body:JSON.stringify({source_line:Number(box.dataset.sourceLine),checked:box.checked})});document.querySelector("#public-meta").textContent=`Updated: ${new Date(page.updated_at).toLocaleString("en-US")} · tasks can be updated`;toast("Task saved");}catch(error){box.checked=previous;toast(error.message);}finally{box.disabled=false;}}); content.addEventListener("change",async event=>{const box=event.target.closest(".task-checkbox");if(!box||box.disabled)return;const previous=!box.checked;box.disabled=true;try{const page=await api(`/api/public/${encodeURIComponent(token)}/tasks`,{method:"POST",body:JSON.stringify({source_line:Number(box.dataset.sourceLine),checked:box.checked})});document.querySelector("#public-meta").textContent=`Updated: ${new Date(page.updated_at).toLocaleString("en-US")} · tasks can be updated`;toast("Task saved");}catch(error){box.checked=previous;toast(error.message);}finally{box.disabled=false;}});
lineNumbersToggle.addEventListener("change",()=>{document.body.classList.toggle("hide-preview-line-numbers",!lineNumbersToggle.checked);}); lineNumbersToggle.addEventListener("change",()=>{document.body.classList.toggle("hide-preview-line-numbers",!lineNumbersToggle.checked);});
document.querySelector("#copy-public-link").addEventListener("click",async()=>{try{await copyText(location.href);toast("Link copied");}catch(error){toast(error.message);}}); document.querySelector("#copy-public-link").addEventListener("click",async()=>{try{await copyText(location.href);toast("Link copied");}catch(error){toast(error.message);}});