From 041f586261540bcaeae5ab2eb91c5196a04b362f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Fri, 24 Jul 2026 15:30:53 +0200 Subject: [PATCH] fixes --- .dockerignore | 3 +- .gitignore | 3 +- Cargo.lock | 2 +- Cargo.toml | 2 +- scripts/random_text_generator.py | 49 +++++++++++++++ static/css/styles.css | 103 ++++++++++++++++++++----------- static/js/markdown.js | 95 ++++++++++++++++++++++------ static/js/note.js | 4 +- static/js/pad.js | 4 +- static/js/public.js | 4 +- 10 files changed, 205 insertions(+), 64 deletions(-) create mode 100644 scripts/random_text_generator.py diff --git a/.dockerignore b/.dockerignore index f7b7195..5e6f628 100644 --- a/.dockerignore +++ b/.dockerignore @@ -10,4 +10,5 @@ rustpad.db-wal README.md Dockerfile* docker-compose*.yml -migrate/ \ No newline at end of file +migrate/ +scripts/*.txt \ No newline at end of file diff --git a/.gitignore b/.gitignore index bb02a5e..8e181e9 100644 --- a/.gitignore +++ b/.gitignore @@ -13,4 +13,5 @@ data/files/* venv .venv migrate/etherpad-dry-run-report.json -data/garge \ No newline at end of file +data/garage +scripts/*.txt \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index fc78018..34b1f09 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2433,7 +2433,7 @@ dependencies = [ [[package]] name = "rustpad" -version = "0.0.15" +version = "0.0.17" dependencies = [ "argon2", "aws-config", diff --git a/Cargo.toml b/Cargo.toml index 6b51a38..21a76e6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rustpad" -version = "0.0.16" +version = "0.0.17" edition = "2024" rust-version = "1.94" description = "Collaborative Markdown notepad built with Axum, WebSockets and SQLite, PostgreSQL and MySQL" diff --git a/scripts/random_text_generator.py b/scripts/random_text_generator.py new file mode 100644 index 0000000..f4b0e20 --- /dev/null +++ b/scripts/random_text_generator.py @@ -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 ") + 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() \ No newline at end of file diff --git a/static/css/styles.css b/static/css/styles.css index 6c3679b..a50ee27 100644 --- a/static/css/styles.css +++ b/static/css/styles.css @@ -2201,7 +2201,7 @@ dialog::backdrop { content: attr(data-source-line); position: absolute; top: 0; - left: -50px; + left: var(--preview-line-left, -50px); width: 32px; color: #596270; text-align: right; @@ -2304,70 +2304,80 @@ dialog::backdrop { color: #b9c2cf; } -.markdown-body .task-list { - margin: 0; - padding: 0; - list-style: none -} - +/* Task-list layout. */ .markdown-body .task-list-item { position: relative; display: grid; grid-template-columns: 1em minmax(0, 1fr); - grid-template-rows: 1.32em; + grid-template-rows: auto auto; column-gap: .45em; - align-items: center; + align-items: start; min-height: 1.32em; margin: 0; padding: 0; - line-height: 1.32 -} - -.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 + line-height: 1.32; + list-style: none; } .markdown-body .task-checkbox { grid-column: 1; grid-row: 1; + align-self: start; + appearance: none; + box-sizing: border-box; + flex: 0 0 1em; width: 1em; + min-width: 1em; + max-width: 1em; height: 1em; - margin: 0; - align-self: center; - accent-color: var(--accent); - cursor: pointer + min-height: 1em; + max-height: 1em; + margin: .14em 0 0; + 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-row: 1; display: block; min-width: 0; margin: 0; padding: 0; - line-height: 1.32 + line-height: inherit; } -.hide-preview-line-numbers .markdown-body .task-list-item::before { - display: none +.markdown-body .task-list-item > ul, +.markdown-body .task-list-item > ol { + grid-column: 2; + grid-row: 2; + min-width: 0; } .compact-editor .markdown-body .task-list-item { - grid-template-rows: 1.24em; min-height: 1.24em; - 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 + line-height: 1.24; } .markdown-body .footnotes { @@ -3805,3 +3815,24 @@ dialog::backdrop { .markdown-body .markdown-toc .toc-level-4, .markdown-body .markdown-toc .toc-level-5, .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; +} + diff --git a/static/js/markdown.js b/static/js/markdown.js index 17652e8..8f4ed75 100644 --- a/static/js/markdown.js +++ b/static/js/markdown.js @@ -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 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 += `
  • `; + if (item.checked != null) { + body += ``; + } + body += `${inline(item.text)}`; + 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 += `
  • `; + } + + const classAttr = hasTask ? ` class="contains-task-items"` : ""; + return {html: `<${type}${classAttr}>${body}`, end: index}; +} + function splitTableRow(line) { let value = line.trim(); if (value.startsWith("|")) value = value.slice(1); @@ -138,8 +195,20 @@ function collectHeadings(lines) { 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) { - 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 headings = collectHeadings(lines); const headingByLine = new Map(headings.map(item => [item.index, item])); @@ -159,7 +228,7 @@ export function renderMarkdown(source, lineOffset = 0) { lines[i] = ""; } - const closeList = () => { if (list) { html += ``; list = null; } }; + const closeList = () => {}; const closeCode = () => { const body = escapeHtml(code.join("\n")); 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 task = line.match(/^(\s*)[-*+]\s+\[([ xX])\]\s+(.+)$/); - const ul = line.match(/^(\s*)[-*+]\s+(.+)$/); - const ol = line.match(/^(\s*)\d+\.\s+(.+)$/); + const listItem = listLine(line); - 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(); const n = heading[1].length; const resolved = headingByLine.get(index); const id = resolved?.id || heading[3] || headingSlug(heading[2]); const suffix = heading[3] ? ` {#${heading[3]}}` : ""; html += `${inline(heading[2])}`; - } else if (task) { - if (list !== "ul") { closeList(); html += `