Export notes as Markdown or PDF

The reason to build this is not a missing feature — it is that trusting a
program you maintain alone with years of notes should not mean the notes
can only ever be read by that program.

Reachable from a note's toolbar, a notebook's context menu, and Account →
Alle Notizen exportieren.

Markdown carries over everything with an equivalent: headings, lists,
quotes, code, tables, and task state as `- [x]`. A link to another note in
the same export becomes a relative link to its file; a link to a note
outside it degrades to the plain title rather than leaving a dead
reference. Notes with images or a drawing arrive as a ZIP so the files
travel along, and a multi-note export gets a README index.

PDF goes through the browser's print dialog rather than a bundled PDF
library — the browser already has a good one, and it is the only format
where drawings sit inside the document. Markdown gets them as a PNG
beside the text, with a line saying so, because dropping them silently
would be the wrong kind of quiet.

Two decisions worth recording:

- the ZIP writer is eighty lines here rather than a dependency. Note text
  is small and images are already compressed, so compression bought
  nothing, and stored entries are a handful of fixed-layout records.
- Markdown escaping only covers what changes meaning mid-line, plus a
  leading list or heading marker. Escaping every character Markdown ever
  uses turns a sentence into `Hallo\. Alles gut\!`, which defeats the
  point of choosing a readable format.

42 unit tests on the serialiser and the archive format, plus a browser
suite that unpacks a real download and reads the bytes back.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
marcandClaude Opus 5 committed 2026-08-08 14:46:53 +02:00
1 parent 311e992496
commit 2611d87f8c
14 files changed
+1707 -2

No files matched your search

+31
View File
@@ -17,6 +17,7 @@ import { EditNotebookModal, CreateNotebookModal } from "./ui/NotebookModals.jsx"
import { AccountMenu } from "./ui/AccountMenu.jsx";
import { AuthScreen } from "./ui/AuthScreen.jsx";
import { AdminPanel } from "./ui/AdminPanel.jsx";
import { ExportDialog } from "./ui/ExportDialog.jsx";
import { Sidebar } from "./ui/Sidebar.jsx";
import { Dashboard } from "./features/notebooks/Dashboard.jsx";
import { NoteList } from "./features/notes/NoteList.jsx";
@@ -51,6 +52,8 @@ export default function App() {
const [editingNb, setEditingNb] = useState(null);
const [showAccount, setShowAccount] = useState(false);
const [showAdmin, setShowAdmin] = useState(false);
// { notes, label } — set while the export dialog is up.
const [exporting, setExporting] = useState(null);
const [historyFor, setHistoryFor] = useState(null);
const [ctxMenu, setCtxMenu] = useState(null);
const [confirmDialog, setConfirmDialog] = useState(null);
@@ -318,6 +321,15 @@ export default function App() {
y: e.clientY,
items: [
{ label: "Edit", icon: I.edit, action: () => setEditingNb(nb) },
{
label: "Export...",
icon: I.export,
action: () =>
setExporting({
label: nb.name,
notes: activeNotes.filter((n) => n.folderId === nb.id),
}),
},
{ divider: true },
{ label: "Delete Notebook", icon: I.trash, danger: true, action: () => deleteNotebook(nb.id) },
],
@@ -357,6 +369,11 @@ export default function App() {
action: () => data.toggleStar(note.id),
},
...moveSubmenu(note),
{
label: "Export...",
icon: I.export,
action: () => setExporting({ label: note.title, notes: [note] }),
},
{ divider: true },
{ label: "Move to Trash", icon: I.trash, danger: true, action: () => trashNote(note.id) },
];
@@ -492,6 +509,7 @@ export default function App() {
onRestore={() => restoreNote(activeNote)}
onPermanentDelete={() => permanentDeleteNote(activeNote)}
onOpenHistory={() => setHistoryFor(activeNote)}
onExport={() => noteMeta && setExporting({ label: noteMeta.title, notes: [noteMeta] })}
onCreateNote={createNote}
/>
)}
@@ -522,9 +540,22 @@ export default function App() {
setShowAccount(false);
setShowAdmin(true);
}}
onExportAll={() => {
setShowAccount(false);
setExporting({ label: "Alle Notizen", notes: activeNotes });
}}
dark={dark}
/>
)}
{exporting && (
<ExportDialog
notes={exporting.notes}
notebooks={notebooks}
label={exporting.label}
dark={dark}
onClose={() => setExporting(null)}
/>
)}
{showAdmin && user?.isAdmin && (
<AdminPanel currentUser={user} onClose={() => setShowAdmin(false)} dark={dark} />
)}