v0.2.92
This commit is contained in:
1 parent
5cb6960b04
commit
0d7ffea17a
13 files changed
+17
-135
No files matched your search
Generated
+1
-1
@@ -2674,7 +2674,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustpad"
|
||||
version = "0.2.91"
|
||||
version = "0.2.92"
|
||||
dependencies = [
|
||||
"argon2",
|
||||
"aws-config",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "rustpad"
|
||||
version = "0.2.91"
|
||||
version = "0.2.92"
|
||||
edition = "2024"
|
||||
rust-version = "1.94"
|
||||
description = "Collaborative Markdown notepad built with Axum, WebSockets and SQLite, PostgreSQL and MySQL"
|
||||
|
||||
@@ -1,78 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<link rel="stylesheet" href="/static/css/styles.css">
|
||||
<link rel="stylesheet" href="/static/libs/rustpad-player/player.css">
|
||||
<script>window.__RUSTPAD_CONFIG__={assetVersion:"test"};</script>
|
||||
<script type="importmap">
|
||||
{"imports":{
|
||||
"@rustpad/i18n":"/static/js/i18n.js",
|
||||
"@rustpad/toast":"/static/js/toast.js",
|
||||
"@rustpad/vendor-libs":"/static/js/vendor-libs.js",
|
||||
"@rustpad/markdown":"/static/js/markdown.js",
|
||||
"@rustpad/emoji-data":"/static/js/emoji-data.js",
|
||||
"@rustpad/image-alias":"/static/js/image-alias.js"
|
||||
}}
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="toast"></div>
|
||||
<button id="pdf">PDF</button>
|
||||
<dialog id="dlg">
|
||||
<form id="form" method="dialog">
|
||||
<select id="pdf-page-format"><option value="screen-16-9" selected>screen</option></select>
|
||||
<select id="pdf-page-margin"><option value="12" selected>12</option></select>
|
||||
<input id="pdf-include-title" type="checkbox">
|
||||
<input id="pdf-preserve-colors" type="checkbox">
|
||||
<button data-pdf-cancel type="button">cancel</button>
|
||||
<button type="submit">go</button>
|
||||
</form>
|
||||
</dialog>
|
||||
<div id="preview" class="preview markdown-body"></div>
|
||||
<script type="module">
|
||||
import { renderMarkdown } from "@rustpad/markdown";
|
||||
import { bindPdfExport } from "/static/js/pdf-export.js";
|
||||
|
||||
const source = await (await fetch('/TEST_NOTE_FEATURES.txt')).text();
|
||||
const preview = document.querySelector('#preview');
|
||||
preview.innerHTML = renderMarkdown(source);
|
||||
|
||||
// Replace raw Mermaid source blocks with representative SVGs so pagination
|
||||
// exercises the same tall/wide atomic visuals as the real rendered preview.
|
||||
[...preview.querySelectorAll('.mermaid')].forEach((node, index) => {
|
||||
const tall = index === 2 || index === 3 || index === 4;
|
||||
const height = tall ? 1500 : 520;
|
||||
node.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 900 ${height}" width="900" height="${height}"><rect width="900" height="${height}" fill="#efeae2"/><rect x="90" y="60" width="720" height="180" rx="16" fill="#202323"/><text x="450" y="160" text-anchor="middle" font-size="56" fill="white">Mermaid ${index + 1}</text><line x1="450" y1="240" x2="450" y2="${height - 100}" stroke="#777" stroke-width="6"/></svg>`;
|
||||
});
|
||||
|
||||
let popupFrame = null;
|
||||
window.open = () => {
|
||||
popupFrame = document.createElement('iframe');
|
||||
popupFrame.id = 'print-frame';
|
||||
popupFrame.style.cssText = 'position:absolute;left:-10000px;top:0;width:1400px;height:900px;border:0';
|
||||
document.body.appendChild(popupFrame);
|
||||
const win = popupFrame.contentWindow;
|
||||
win.focus = () => {};
|
||||
win.print = async () => {
|
||||
const html = win.document.documentElement.outerHTML;
|
||||
await fetch('/capture', { method: 'POST', headers: {'Content-Type':'text/html'}, body: html });
|
||||
document.documentElement.dataset.captureDone = '1';
|
||||
};
|
||||
return win;
|
||||
};
|
||||
|
||||
const dialog = document.querySelector('#dlg');
|
||||
bindPdfExport({
|
||||
button: document.querySelector('#pdf'),
|
||||
dialog,
|
||||
form: document.querySelector('#form'),
|
||||
preview,
|
||||
title: () => 'TEST_NOTE_FEATURES',
|
||||
});
|
||||
|
||||
dialog.showModal();
|
||||
document.querySelector('#form').dispatchEvent(new Event('submit', { bubbles: true, cancelable: true }));
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,24 +0,0 @@
|
||||
<!doctype html><html lang="en"><head><meta charset="utf-8">
|
||||
<link rel="stylesheet" href="file:///mnt/data/rustpad_pdf_fix/static/css/styles.css">
|
||||
<link rel="stylesheet" href="file:///mnt/data/rustpad_pdf_fix/static/libs/rustpad-player/player.css">
|
||||
<script>window.__RUSTPAD_CONFIG__={assetVersion:"test"};</script>
|
||||
<script type="importmap">{"imports": {"@rustpad/i18n": "file:///mnt/data/rustpad_pdf_fix/static/js/i18n.js", "@rustpad/toast": "file:///mnt/data/rustpad_pdf_fix/static/js/toast.js", "@rustpad/vendor-libs": "file:///mnt/data/rustpad_pdf_fix/static/js/vendor-libs.js", "@rustpad/markdown": "file:///mnt/data/rustpad_pdf_fix/static/js/markdown.js", "@rustpad/emoji-data": "file:///mnt/data/rustpad_pdf_fix/static/js/emoji-data.js", "@rustpad/image-alias": "file:///mnt/data/rustpad_pdf_fix/static/js/image-alias.js"}}</script></head><body>
|
||||
<div id="toast"></div><button id="pdf">PDF</button><dialog id="dlg"><form id="form" method="dialog">
|
||||
<select id="pdf-page-format"><option value="screen-16-9" selected>screen</option></select>
|
||||
<select id="pdf-page-margin"><option value="12" selected>12</option></select>
|
||||
<input id="pdf-include-title" type="checkbox"><input id="pdf-preserve-colors" type="checkbox">
|
||||
<button data-pdf-cancel type="button">cancel</button><button type="submit">go</button></form></dialog>
|
||||
<div id="preview" class="preview markdown-body"></div>
|
||||
<script type="module">
|
||||
import { renderMarkdown } from "@rustpad/markdown";
|
||||
import { bindPdfExport } from "file:///mnt/data/rustpad_pdf_fix/static/js/pdf-export.js";
|
||||
const source="# RustPad Complete Feature Demonstration {#rustpad-complete-test}\n\nThis note is an extensive test document for the RustPad editor, Markdown renderer, preview editor, file handling, media players, collaboration tools, and mobile interface.\n\nUse **Markdown mode** and begin in **Split view**. Later, follow the manual interface checklist near the end of this note.\n\n## Preparation before testing\n\nUpload files with these exact names so every local attachment example can resolve:\n\n- `TEST_IMAGE.png` \u2014 any PNG or JPEG image renamed to this filename.\n- `TEST_VIDEO.mp4` \u2014 a short MP4 video.\n- `TEST_DOCUMENT.pdf` \u2014 any PDF document.\n- `TEST_ARCHIVE.zip` \u2014 any ZIP archive.\n\nUntil the files are uploaded, aliases such as `[image=TEST_IMAGE.png,...]` remain visible as plain text. This is expected.\n\nThe generated table of contents below should include only headings after the `[TOC]` marker, nest lower-level headings under their nearest parent, and avoid adding a second numeric sequence to headings that are already numbered.\n\n[TOC]\n\n---\n\n# 1. Headings and navigation {#headings-and-navigation}\n\n## Heading level 2\n\n### Heading level 3\n\n#### Heading level 4\n\n##### Heading level 5\n\n###### Heading level 6\n\nThe toolbar exposes H1\u2013H4, while the renderer also supports H5 and H6 entered manually.\n\n## Custom heading identifier {#custom-anchor-example}\n\nThis heading uses the explicit identifier `custom-anchor-example`.\n\n[Jump to the custom heading](#custom-anchor-example)\n\n[Return to the top](#rustpad-complete-test)\n\n## Repeated heading\n\nThe first repeated heading should receive the generated anchor `repeated-heading`.\n\n## Repeated heading\n\nThe second repeated heading should receive a unique generated anchor such as `repeated-heading-2`.\n\n---\n\n# 2. Paragraphs, wrapping, and plain text\n\nThis is a normal paragraph. It should wrap naturally when the editor or preview becomes narrow. Resize the browser window and verify that no horizontal page overflow is introduced by ordinary text.\n\nThis deliberately long sentence tests wrapping, compact mode, synchronized scrolling, font changes, mobile layout, and preview editing while preserving a readable paragraph across narrow and wide screens without forcing the entire interface beyond the available viewport width.\n\nA blank source line creates visible paragraph spacing.\n\nThe next line contains characters that should be escaped rather than executed:\n\n<script>alert(\"This must never execute\")</script>\n\nHTML other than the supported `<details>` and `<summary>` block should be displayed safely as text.\n\n---\n\n# 3. Inline formatting\n\n**Bold text**\n\n*Italic text*\n\n~~Strikethrough text~~\n\n==Highlighted text==\n\nH~2~O uses subscript.\n\nE = mc^2^ uses superscript.\n\nUse `cargo test`, `npm run build`, and `SELECT * FROM notes;` as inline code.\n\nFormatting can appear in one sentence: **bold**, *italic*, ~~deleted~~, ==important==, H~2~O, x^2^, and `inline code`.\n\nSelect one or more lines in the editor and press a formatting button. Multi-line selection should apply or remove the wrapper on each non-empty selected line.\n\n---\n\n# 4. Emoji support\n\nShortcodes should render as emoji:\n\n:smile: :rocket: :white_check_mark: :warning: :heart: :fire: :computer: :memo: :bug: :tada:\n\nThe **Emoji** toolbar menu should also:\n\n- search by emoji name or keyword,\n- switch between categories,\n- insert the selected emoji at the current cursor position,\n- close after insertion.\n\nDirect Unicode emoji should remain unchanged: \ud83d\ude00 \ud83d\ude80 \u2705 \u26a0\ufe0f \u2764\ufe0f \ud83d\udd25 \ud83e\uddea \ud83d\udcce \ud83c\udfac.\n\n---\n\n# 5. Links and automatic link detection\n\n[Example link](https://example.com)\n\n[Example link with a title](https://example.com \"Open example.com\")\n\n[Relative link to the RustPad home page](/)\n\n[Internal link to the media section](#media-and-attachments)\n\nA plain URL should become clickable automatically:\n\nhttps://example.com/documentation\n\nTrailing punctuation should not become part of the URL: https://example.com/testing.\n\nA mail address using the mailto scheme should become clickable:\n\nmailto:qa@example.com\n\nA deliberately unsafe URL should be neutralized instead of executed:\n\n[Blocked JavaScript URL](javascript:alert%281%29)\n\nA protocol-relative URL should also be rejected:\n\n[Blocked protocol-relative URL](//example.com/path)\n\n---\n\n# 6. Blockquotes\n\n> RustPad renders a quoted line as a blockquote.\n\n> This second source line is another quote and can be edited directly in Preview mode.\n\n> **Formatting**, `inline code`, links such as [RustPad home](/), and :memo: emoji work inside a quote.\n\n---\n\n# 7. Unordered, ordered, nested, and task lists\n\n## Unordered list\n\n- First unordered item\n- Second unordered item with **bold text**\n- Third unordered item with a [link](https://example.com)\n+ A plus sign is Line truncated
|
||||
const preview=document.querySelector('#preview'); preview.innerHTML=renderMarkdown(source);
|
||||
[...preview.querySelectorAll('.mermaid')].forEach((node,index)=>{
|
||||
const tall=index===2||index===3||index===4; const height=tall?1500:520;
|
||||
node.innerHTML=`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 900 ${height}" width="900" height="${height}"><rect width="900" height="${height}" fill="#efeae2"/><rect x="90" y="60" width="720" height="180" rx="16" fill="#202323"/><text x="450" y="160" text-anchor="middle" font-size="56" fill="white">Mermaid ${index+1}</text><line x1="450" y1="240" x2="450" y2="${height-100}" stroke="#777" stroke-width="6"/></svg>`;
|
||||
});
|
||||
window.open=()=>{ const frame=document.createElement('iframe'); frame.style.cssText='position:absolute;left:-10000px;top:0;width:1400px;height:900px;border:0'; document.body.appendChild(frame); const win=frame.contentWindow; win.focus=()=>{}; win.print=()=>{ window.__CAPTURE_HTML__=win.document.documentElement.outerHTML; document.documentElement.dataset.captureDone='1'; }; return win; };
|
||||
const dialog=document.querySelector('#dlg'); bindPdfExport({button:document.querySelector('#pdf'),dialog,form:document.querySelector('#form'),preview,title:()=> 'TEST_NOTE_FEATURES'});
|
||||
dialog.showModal(); document.querySelector('#form').dispatchEvent(new Event('submit',{bubbles:true,cancelable:true}));
|
||||
</script></body></html>
|
||||
+8
-1
@@ -1,6 +1,10 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Mateusz Gruszczyński @linuxiarz.pl
|
||||
* Source-Available Code / Dual-Licensed.
|
||||
*
|
||||
* Free for non-commercial and evaluation use under terms of BSL/GPLv3.
|
||||
* Commercial or production use requires a valid paid license.
|
||||
* See LICENSE file in repository root for details.
|
||||
*/
|
||||
|
||||
use std::sync::OnceLock;
|
||||
@@ -129,7 +133,10 @@ mod tests {
|
||||
#[test]
|
||||
fn metadata_has_unique_codes() {
|
||||
let metadata = metadata();
|
||||
let mut codes = metadata.iter().map(|item| item.code.as_str()).collect::<Vec<_>>();
|
||||
let mut codes = metadata
|
||||
.iter()
|
||||
.map(|item| item.code.as_str())
|
||||
.collect::<Vec<_>>();
|
||||
let len = codes.len();
|
||||
codes.sort_unstable();
|
||||
codes.dedup();
|
||||
|
||||
+1
-3
@@ -253,8 +253,6 @@
|
||||
</form>
|
||||
</dialog>
|
||||
<script>
|
||||
// If initial account scripts/network never finish, offer a local retry rather
|
||||
// than leaving the footer placeholder indefinitely (without blocking home).
|
||||
setTimeout(() => {
|
||||
const slot = document.getElementById("footer-account-slot");
|
||||
if (slot?.dataset.state !== "loading") return;
|
||||
@@ -262,7 +260,7 @@
|
||||
slot.setAttribute("aria-busy", "false");
|
||||
const retry = document.getElementById("footer-account-retry");
|
||||
if (retry) retry.hidden = false;
|
||||
}, 15000);
|
||||
}, 10000);
|
||||
</script>
|
||||
<div id="toast" class="toast-region" aria-live="polite" aria-label="Notifications" aria-relevant="additions removals">
|
||||
</div>
|
||||
|
||||
@@ -4,9 +4,6 @@
|
||||
* See LICENSE.md for terms.
|
||||
*/
|
||||
|
||||
// Preserve existing nodes. Remove stale entries first: otherwise deleting a
|
||||
// user near the start of a 1000-person list would move every following node.
|
||||
// `desired` must contain each node at most once.
|
||||
export function reconcileChildren(container, desired) {
|
||||
const retained = new Set(desired);
|
||||
for (const node of [...container.childNodes]) {
|
||||
|
||||
@@ -7,7 +7,5 @@
|
||||
* See LICENSE file in repository root for details.
|
||||
*/
|
||||
|
||||
// Generated from Unicode Emoji 17.0 emoji-test.txt.
|
||||
// Native glyphs use the platform emoji font (Apple style on Apple devices).
|
||||
export { EMOJI_GROUPS } from "@rustpad/emoji-groups";
|
||||
export { EMOJI_SHORTCODES } from "@rustpad/emoji-shortcodes";
|
||||
@@ -13,8 +13,6 @@ const MAX_RESULTS = 8;
|
||||
const MENTION_SUFFIX = /[\p{L}\p{M}\p{N}_.+-]{0,64}$/u;
|
||||
const MENTION_BOUNDARY = /[\p{L}\p{N}_@]/u;
|
||||
|
||||
// A suggestion is offered only for an @word being typed at a collapsed caret.
|
||||
// Do not interpret addresses such as john@example.org as mentions.
|
||||
export function mentionAtCaret(value, caret) {
|
||||
if (caret < 1 || caret > value.length) return null;
|
||||
const lineStart = value.lastIndexOf("\n", caret - 1) + 1;
|
||||
@@ -69,8 +67,6 @@ export function createMentionAutocomplete(editor) {
|
||||
menu.hidden = true;
|
||||
shell.append(menu);
|
||||
|
||||
// A single-line offscreen mirror measures the caret's horizontal position,
|
||||
// including proportional fonts and tabs, without relying on text estimates.
|
||||
const mirror = document.createElement("div");
|
||||
mirror.className = "mention-caret-mirror";
|
||||
mirror.setAttribute("aria-hidden", "true");
|
||||
@@ -158,7 +154,6 @@ export function createMentionAutocomplete(editor) {
|
||||
editor.focus();
|
||||
const inserted = `@${user.nickname}${separator}`;
|
||||
editor.setRangeText(inserted, start, end, "end");
|
||||
// Use the normal input pipeline: live collaboration, authorship and undo/redo.
|
||||
editor.dispatchEvent(new InputEvent("input", { bubbles: true, inputType: "insertReplacementText", data: inserted }));
|
||||
}
|
||||
|
||||
@@ -189,7 +184,6 @@ export function createMentionAutocomplete(editor) {
|
||||
const label = document.createElement("span");
|
||||
label.textContent = `@${user.nickname}`;
|
||||
button.append(dot, label);
|
||||
// Keep textarea selection intact while clicking with a mouse.
|
||||
button.addEventListener("mousedown", event => event.preventDefault());
|
||||
button.addEventListener("click", () => accept(index));
|
||||
menu.append(button);
|
||||
|
||||
@@ -11,10 +11,6 @@ import { t } from "@rustpad/i18n";
|
||||
import { normalizeMentionUsers } from "@rustpad/mention-autocomplete";
|
||||
|
||||
function escapeRegex(value) { return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); }
|
||||
|
||||
// Find real @mentions in the source, not arbitrary text that resembles an email
|
||||
// address or an @username inside a code fence, inline code, link or URL.
|
||||
// Offsets use UTF-16 code units, just like textarea.setSelectionRange().
|
||||
export function collectMentionOccurrences(source, authorizedUsers, { markdown = true } = {}) {
|
||||
const members = normalizeMentionUsers(authorizedUsers);
|
||||
if (!members.length) return [];
|
||||
@@ -39,7 +35,6 @@ export function collectMentionOccurrences(source, authorizedUsers, { markdown =
|
||||
|
||||
const excluded = [];
|
||||
if (markdown) {
|
||||
// The Markdown renderer stashes these constructs before highlighting mentions.
|
||||
const hidden = [
|
||||
/`[^`]+`/g,
|
||||
/!?\[[^\]]*\]\([^\s)]+(?:\s+["'][^"']*["'])?\)/g,
|
||||
@@ -90,8 +85,6 @@ export function createMentionNavigator({ container, list, previewContainer, prev
|
||||
if (!entries.has(mention.key)) entries.set(mention.key, []);
|
||||
entries.get(mention.key).push(mention);
|
||||
}
|
||||
// Keep a reference to each visible mention, so the jump can target the
|
||||
// exact chip even if several mentions are on the same source line.
|
||||
if (markdown) {
|
||||
const visible = new Map();
|
||||
for (const node of preview.querySelectorAll(".markdown-mention")) {
|
||||
@@ -174,7 +167,6 @@ export function createMentionNavigator({ container, list, previewContainer, prev
|
||||
}
|
||||
if (view !== "edit") {
|
||||
navigatePreview(mention);
|
||||
// Split view scroll synchronization may run after the textarea scroll event.
|
||||
if (view === "split") requestAnimationFrame(() => {
|
||||
if (lastJump.get(key) === mention.start) navigatePreview(mention);
|
||||
});
|
||||
|
||||
@@ -62,7 +62,7 @@ export function startNoteEditor(adapter) {
|
||||
if (!bootLoading || bootFinishing) return;
|
||||
bootFinishing = true;
|
||||
bootLoading.complete("access");
|
||||
bootLoading.skip("files"); // Identity/password dialog is ready to use.
|
||||
bootLoading.skip("files");
|
||||
bootLoading.ready();
|
||||
}
|
||||
let accessToken = shareToken || getAccessToken(adapter.access.kind, adapter.access.key), password = "", nickname = getNickname(), info, socket, saveTimer, applyingRemote = false, applyingHistory = false, resourceUnlocked = false, uiState = readEditorState(), authorship = parseAuthorship("", "[]"), previousContent = "", globalColor = "", noteColor = "", presenceUsers = [], authorshipMode = "simple", authorshipColorsEnabled = true, lastRevealedLineHash = "", lastRevealedHeadingHash = "", flushRequested = false, accountSession = null;
|
||||
@@ -291,8 +291,7 @@ export function startNoteEditor(adapter) {
|
||||
setMarkdownMentions([]);
|
||||
mentionAutocomplete.setUsers([]);
|
||||
mentionNavigator.setUsers([]);
|
||||
// Session validation can finish before loadNoteInfo(); avoid a premature
|
||||
// request (and a second request when note info becomes available).
|
||||
|
||||
if (!accountSession || !info) { render(); return; }
|
||||
try {
|
||||
const params = new URLSearchParams({ kind: adapter.access.kind, slug: adapter.access.key });
|
||||
|
||||
@@ -35,7 +35,7 @@ export function clearResourceAccessState() {
|
||||
if (key?.startsWith("rustpad:access:")) localStorage.removeItem(key);
|
||||
}
|
||||
}
|
||||
// Remove plaintext passwords saved by the previous frontend version.
|
||||
|
||||
for (let i = localStorage.length - 1; i >= 0; i--) {
|
||||
const key = localStorage.key(i);
|
||||
if (key?.startsWith("rustpad:workspace:") && key.endsWith(":password")) localStorage.removeItem(key);
|
||||
@@ -43,8 +43,7 @@ for (let i = localStorage.length - 1; i >= 0; i--) {
|
||||
|
||||
const NICKNAME_KEY = "rustpad:nickname";
|
||||
const NICKNAME_COOKIE = "rustpad_nickname";
|
||||
// A session cookie is shared by tabs, but disappears when the browser session
|
||||
// ends. The nickname is not an authentication secret.
|
||||
|
||||
function getCookie(name) {
|
||||
const prefix = `${name}=`;
|
||||
const item = document.cookie.split("; ").find(value => value.startsWith(prefix));
|
||||
@@ -79,7 +78,6 @@ export function getGuestId() {
|
||||
}
|
||||
|
||||
const AUTH_STATE_KEY = "rustpad:auth-state";
|
||||
// Remove browser-readable account tokens created by older frontend versions.
|
||||
localStorage.removeItem("rustpad:auth-token");
|
||||
sessionStorage.removeItem("rustpad:auth-token");
|
||||
export function getAuthToken() { return localStorage.getItem(AUTH_STATE_KEY) ? "cookie" : ""; }
|
||||
|
||||
+2
-1
@@ -38,7 +38,8 @@
|
||||
<a class="text-button" href="/">Back to home</a>
|
||||
</form>
|
||||
</dialog>
|
||||
<div id="toast" class="toast-region" aria-live="polite" aria-label="Notifications" aria-relevant="additions removals"></div>
|
||||
<div id="toast" class="toast-region" aria-live="polite" aria-label="Notifications" aria-relevant="additions removals">
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
Reference in new issue
Block a user