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:
1 parent
311e992496
commit
2611d87f8c
14 files changed
+1707
-2
No files matched your search
@@ -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} />
|
||||
)}
|
||||
|
||||
Reference in new issue
Block a user