Replace markdown editor with native Tiptap editor and add Home navigation
- Migrate renderer from md-editor/md-format to a Tiptap-based native editor (native-doc pipeline: native-doc, markdown-to-native, native-to-markdown, native-assembly, migration, backup) - Use pointer-event-based chapter reordering with grab/grabbing cursors to avoid the Wayland prohibited-drop cursor while preserving reordering - Add a persistent Home button that closes the open book and returns to the Folio library/welcome screen - Update app icons, package deps, theme styles, and tests
This commit is contained in:
parent
0df26d4ce6
commit
786a65ae2a
48 changed files with 5762 additions and 3999 deletions
258
docs/architecture-stage-1.md
Normal file
258
docs/architecture-stage-1.md
Normal file
|
|
@ -0,0 +1,258 @@
|
|||
# Folio Document Architecture — Stage 1 Plan
|
||||
|
||||
**Scope:** Architecture only. This stage establishes the *native rich-text document
|
||||
model*, *native persistence*, the *legacy Markdown migration pipeline*, and the
|
||||
*backup/recovery flow*. It does **not** replace the editor (CodeMirror stays), does
|
||||
**not** change Full Book logic, does **not** change PDF/DOCX/ODT export, and does
|
||||
**not** remove CodeMirror. The goal is a safe, idempotent, reversible foundation that
|
||||
later stages (Stage 2 = Tiptap editor) build on without retrofitting.
|
||||
|
||||
---
|
||||
|
||||
## 1. Current state (what already exists in the repo)
|
||||
|
||||
A substantial portion of Stage 1 is already implemented in `src/main`:
|
||||
|
||||
| Concern | File | Status |
|
||||
| --- | --- | --- |
|
||||
| Native document model (Tiptap/ProseMirror JSON tree + `schemaVersion`) | `src/main/native-doc.ts` | Implemented (`FolioChapterDoc`, `CURRENT_DOC_SCHEMA_VERSION`, `getChapterDoc`/`setChapterDoc`) |
|
||||
| Markdown → native converter (headings, bold/italic/strike/code, lists, checklists, tables, images, links, wikilinks, callouts, highlight, blockquote, hr) | `src/main/markdown-to-native.ts` | Implemented |
|
||||
| Migration pipeline (explicit, idempotent, `.md`-preserving) | `src/main/migration.ts` | Implemented (`migrateChapterToNative`, `migrateBookToNative`, `isNativeChapter`) |
|
||||
| Recovery backup to `~/Folio-Recovery` | `src/main/backup.ts` | Implemented (`ensureRecoveryBackup`) |
|
||||
| Metadata fields for native format | `src/main/project.ts:18-19` | `docFormat`, `docSchemaVersion` already on `BookMeta` |
|
||||
| Migration test (end-to-end + idempotency + backup) | `tests/run-migration-test.mjs` | Passing |
|
||||
|
||||
**Gaps that this plan must close (the actual Stage 1 work):**
|
||||
|
||||
1. **No `native → Markdown` serializer.** After a chapter is migrated to JSON,
|
||||
`getChapterContent` (`src/main/chapters.ts:19`) and `export.ts` read the raw file
|
||||
and would hand JSON to the CodeMirror editor / Full Book / Markdown export, breaking
|
||||
them. Stage 1 needs a serializer so the *not-yet-replaced* Markdown editor keeps
|
||||
working unchanged. This is the bridge that Stage 2 removes.
|
||||
2. **No explicit migration trigger wired end-to-end.** `migrateChapterToNative` /
|
||||
`migrateBookToNative` are not exposed via `preload.ts`, `index.ts`, or any UI in
|
||||
`book.ts`. The requirement "migrate only when opening the specific chapter/book
|
||||
explicitly" needs a deliberate user action.
|
||||
3. **Content access layer is not format-aware.** Reads/writes throughout assume `.md`.
|
||||
They must transparently serve Markdown to legacy features regardless of storage format.
|
||||
|
||||
---
|
||||
|
||||
## 2. Document model → persistence → editor integration points
|
||||
|
||||
### 2.1 Document model (`src/main/native-doc.ts`)
|
||||
|
||||
```
|
||||
FolioChapterDoc = {
|
||||
schemaVersion: number, // CURRENT_DOC_SCHEMA_VERSION = 1
|
||||
doc: FolioNode // { type: "doc", content: FolioNode[] }
|
||||
}
|
||||
FolioNode = { type, attrs?, content?, text?, marks? }
|
||||
FolioMark = { type, attrs? } // bold, italic, strike, code, underline,
|
||||
// highlight, textColor, link, ...
|
||||
```
|
||||
|
||||
- `doc.type === "doc"` is the ProseMirror/Tiptap top node. This is **the** source of
|
||||
truth for a migrated chapter, not Markdown.
|
||||
- `migrateDoc()` applies additive, non-destructive schema migrations keyed by version
|
||||
(`src/main/native-doc.ts:54`). Bump `CURRENT_DOC_SCHEMA_VERSION` only for breaking
|
||||
shape changes.
|
||||
- Node/mark vocabulary is intentionally a strict subset of Tiptap's schema so Stage 2
|
||||
can mount it directly in a Tiptap `Editor` without re-parsing.
|
||||
|
||||
### 2.2 Persistence layout (per book)
|
||||
|
||||
```
|
||||
<book>/
|
||||
folio.json # BookMeta: chapterOrder, chapters{id->entry},
|
||||
# docFormat:"folio-doc", docSchemaVersion
|
||||
chapters/
|
||||
<id>.md # legacy (NEVER deleted/overwritten by migration)
|
||||
<id>.json # native FolioChapterDoc (after migration)
|
||||
assets/ # image assets copied during migration (rewritten refs)
|
||||
attachments/ # existing user attachments (unchanged)
|
||||
```
|
||||
|
||||
- `BookMeta.chapters[id].file` is the single pointer to a chapter's storage file.
|
||||
Migration repoints it from `chapters/<id>.md` to `chapters/<id>.json`
|
||||
(`src/main/migration.ts:107`). Until then it stays `.md`.
|
||||
- `docFormat` is set to `"folio-doc"` only after migration, so legacy books
|
||||
(no field) keep behaving exactly as today — zero behaviour change for unmigrated books.
|
||||
|
||||
### 2.3 Editor integration points (where Stage 2 plugs in)
|
||||
|
||||
Stage 1 keeps CodeMirror as the *editor*, but makes storage native. The seam is a
|
||||
**content adapter** between IPC and the editor:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────┐
|
||||
IPC (main) │ ChapterContentAdapter (new, renderer) │
|
||||
getChapter --->│ - if entry.file ends with .md -> text │
|
||||
Content │ - if .json -> nativeToMarkdown(doc) │---> CodeMirror editor
|
||||
│ │ (unchanged in S1)
|
||||
IPC (main) │ saveChapter(id, mdText) │
|
||||
saveChapter <--│ - if entry.file is .json -> │<--- editor onChange
|
||||
│ markdownToNative(mdText) -> setDoc │
|
||||
│ - if .md -> write text (legacy path) │
|
||||
└─────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
- **Read path:** `getChapterContent` becomes format-aware (see §3). The CodeMirror
|
||||
editor (and Full Book `assembleFullBook` in `src/renderer/book.ts:680`, and
|
||||
`exportMarkdown`) continue to receive Markdown.
|
||||
- **Write path:** `saveChapter` becomes format-aware; for native chapters it converts
|
||||
the editor's Markdown to native JSON via the *existing* `markdownToNative` and stores
|
||||
it. Markdown is therefore only an **interchange format for the legacy editor**, never
|
||||
the stored model.
|
||||
- **Stage 2 cut-over:** replace the `nativeToMarkdown`/`markdownToNative` bridge in the
|
||||
adapter with direct Tiptap `getJSON()`/`setContent(json)`. The model and persistence
|
||||
layers need no further change.
|
||||
|
||||
---
|
||||
|
||||
## 3. Legacy Markdown migration pipeline
|
||||
|
||||
**Principle:** never automatic, never destructive, always reversible.
|
||||
|
||||
```
|
||||
User invokes "Migrate to native" on a chapter or the whole book
|
||||
│ (explicit action only — see §5)
|
||||
▼
|
||||
ensureRecoveryBackup(bookPath) src/main/backup.ts:20
|
||||
│ copies entire <book> to ~/Folio-Recovery/<book>-<ts>
|
||||
│ (session-guarded: once per process run per book)
|
||||
▼
|
||||
For each requested chapter id:
|
||||
entry = meta.chapters[id]
|
||||
if entry.file ends with ".json":
|
||||
→ return { status: "already-native" } (idempotent, .md untouched)
|
||||
md = getChapterContent(bookPath, id)
|
||||
doc = markdownToNative(md, { resolveImage, targets })
|
||||
│ • resolveImage() copies local images into assets/ and rewrites
|
||||
│ src to "assets/<name>"; external/data/blob URIs kept as-is
|
||||
│ (src/main/migration.ts:44)
|
||||
│ • targets enable [[wikilink]] → folio:chapter/<id>
|
||||
▼
|
||||
setChapterDoc(bookPath, id, doc) writes chapters/<id>.json
|
||||
entry.file = "chapters/<id>.json"
|
||||
meta.docFormat = "folio-doc"
|
||||
meta.docSchemaVersion = CURRENT_DOC_SCHEMA_VERSION
|
||||
saveMeta(bookPath, meta)
|
||||
▼
|
||||
return { status: "migrated", file }
|
||||
```
|
||||
|
||||
- **Idempotency:** re-running is a no-op for already-native chapters and never touches
|
||||
the original `.md` (`tests/run-migration-test.mjs:257`).
|
||||
- **Safety:** the original `.md` is never read-for-write; migration only *adds* a new
|
||||
`.json` and repoints metadata. The `.md` remains on disk as a fallback/recovery copy.
|
||||
- **Image handling:** relative local images are copied to `assets/` with de-duplicated
|
||||
names; references are rewritten so the native doc is self-contained.
|
||||
|
||||
---
|
||||
|
||||
## 4. Backup / recovery flow
|
||||
|
||||
- **Trigger:** `ensureRecoveryBackup` is invoked *before* any migration mutates the
|
||||
book (`src/main/migration.ts:90`, `:123`).
|
||||
- **Location:** external to the book — `~/Folio-Recovery/<BookName>-<ISO-timestamp>`
|
||||
(`src/main/backup.ts:12`, `:26`). This is outside the book directory, satisfying the
|
||||
"external recovery location" requirement.
|
||||
- **Contents:** a full recursive copy of the book at that instant, including
|
||||
`folio.json` and every `chapters/<id>.md` — so recovery = "open the backup folder
|
||||
instead of the migrated book."
|
||||
- **Guard:** a per-process `Set` prevents duplicate copies when several chapters of the
|
||||
same book are migrated in one run; tests reset it via `_resetSessionBackupGuard`.
|
||||
- **Recovery procedure (documented for users):** close the book, delete or rename the
|
||||
migrated book folder, move the desired `~/Folio-Recovery/<book>-<ts>` folder back to
|
||||
its original path, and open it. No tooling required; pure file operations.
|
||||
|
||||
---
|
||||
|
||||
## 5. Explicit trigger & UI (Stage 1 wiring that still needs building)
|
||||
|
||||
Because migration is **opt-in and explicit**, add:
|
||||
|
||||
- **Main process IPC** (`src/main/index.ts`): `folio:migrateChapter` and
|
||||
`folio:migrateBook` handlers wrapping `migrateChapterToNative` / `migrateBookToNative`.
|
||||
- **Preload bridge** (`src/main/preload.ts`): expose `migrateChapter(id)` /
|
||||
`migrateBook()` invoking those channels.
|
||||
- **Renderer API type** (`src/renderer/book.ts` `FolioAPI`): declare the two new calls.
|
||||
- **UI affordance** (`src/renderer/book.ts`): a "Migrate to native format" action
|
||||
(e.g., in the chapter row context menu / book menu). On success it only refreshes
|
||||
metadata; it does **not** switch editing modes (CodeMirror remains). A confirmation
|
||||
dialog is shown because migration repoints storage.
|
||||
|
||||
> Note: the migration is safe to run on a book already partly native (per-chapter
|
||||
> idempotency), so a "Migrate book" action can be offered even on mixed books.
|
||||
|
||||
---
|
||||
|
||||
## 6. Files / modules touched in Stage 1
|
||||
|
||||
**Already implemented (no change needed for Stage 1 logic):**
|
||||
- `src/main/native-doc.ts` — model + read/write + schema migration
|
||||
- `src/main/markdown-to-native.ts` — Markdown → native
|
||||
- `src/main/migration.ts` — pipeline (explicit, idempotent)
|
||||
- `src/main/backup.ts` — recovery backup
|
||||
- `src/main/project.ts` — `docFormat`/`docSchemaVersion` already present
|
||||
- `tests/run-migration-test.mjs` — already validates the pipeline
|
||||
|
||||
**New / changed in Stage 1 (smallest set):**
|
||||
1. `src/main/native-to-markdown.ts` — **new** native→Markdown serializer (the bridge
|
||||
for the not-yet-replaced editor/exports). Mirror of `markdown-to-native.ts`.
|
||||
2. `src/main/chapters.ts` — make `getChapterContent` / `setChapterContent`
|
||||
format-aware (route `.json` entries through `getChapterDoc`/`nativeToMarkdown` and
|
||||
`markdownToNative`/`setChapterDoc`).
|
||||
3. `src/main/export.ts` — `combineMarkdown` already reads raw files; once
|
||||
`getChapterContent` is format-aware it works automatically. Verify `buildZip` copies
|
||||
the correct `entry.file` (it already uses `ch.file`, so JSON is bundled correctly).
|
||||
4. `src/main/index.ts` — add `folio:migrateChapter` / `folio:migrateBook` IPC handlers.
|
||||
5. `src/main/preload.ts` — expose the two migration calls.
|
||||
6. `src/renderer/book.ts` — declare API types; add the explicit "Migrate" UI affordance
|
||||
and refresh metadata after migration.
|
||||
|
||||
**Untouched in Stage 1 (explicit non-goals):** `src/renderer/md-editor.ts`,
|
||||
`src/renderer/md-format.ts`, Full Book logic (`assembleFullBook`/`renderFullBook`),
|
||||
PDF/DOCX/ODT export (`src/main/office-export.ts`), CodeMirror removal, theme system
|
||||
(`src/renderer/theme.ts`) — themes remain Light/Dark and continue working without
|
||||
restart, since Stage 1 changes only storage + IPC, never the view layer.
|
||||
|
||||
---
|
||||
|
||||
## 7. Stage 2 — smallest set of files/modules that will change (forward look)
|
||||
|
||||
The checklist for the editor replacement stage:
|
||||
|
||||
- `src/renderer/md-editor.ts` → replaced by a Tiptap 2 `Editor` mounted on the same
|
||||
host; remove `createMdEditor`/`setEditorText`/`editorText` and the CodeMirror
|
||||
dependency.
|
||||
- `src/renderer/book.ts` → swap the content adapter's Markdown bridge for direct
|
||||
Tiptap `editor.getJSON()` / `editor.commands.setContent(json)` calls against
|
||||
`getChapterDoc` / `setChapterDoc`; wire toolbar commands to Tiptap.
|
||||
- `src/renderer/md-format.ts` → replaced by Tiptap extension commands / marks.
|
||||
- `src/main/native-to-markdown.ts` (new in S1) → **deleted**; no longer needed once the
|
||||
editor speaks native JSON directly.
|
||||
- `src/main/markdown-to-native.ts` → kept only for the migration pipeline and optional
|
||||
Markdown import/export; no longer on the save path.
|
||||
- `package.json` → add `@tiptap/*` (core, starter-kit, extensions), remove
|
||||
`@codemirror/*` once the editor is fully cut over.
|
||||
- Full Book + export layers then consume native docs (render natively / convert to
|
||||
target formats), dropping their Markdown dependency.
|
||||
|
||||
---
|
||||
|
||||
## 8. Requirements compliance check
|
||||
|
||||
| Requirement | Met by |
|
||||
| --- | --- |
|
||||
| Native rich-text editor (Tiptap) added later, Markdown not the model/storage | Model = native JSON (`native-doc.ts`); storage = `.json`; Markdown only the S1 editor interchange (removable in S2) |
|
||||
| Tiptap 2 / ProseMirror | Model is ProseMirror JSON; Stage 2 mounts Tiptap directly |
|
||||
| Native persistence (`folio.json`, `chapters/<id>.json`, `assets/`) | `native-doc.ts`, `project.ts`, `migration.ts` |
|
||||
| Never delete/overwrite `.md` | `migration.ts` only adds `.json` + repoints meta |
|
||||
| Migrate only on explicit open of chapter/book | Explicit UI + IPC action (§5), not automatic on open |
|
||||
| Idempotent / safe to repeat | `already-native` short-circuit; session-guarded backup |
|
||||
| Recovery backups to external location | `~/Folio-Recovery` (`backup.ts`) |
|
||||
| Markdown optional compat/export only | Exports read via format-aware `getChapterContent`; core features store native |
|
||||
| Themes Light/Dark, no restart | Untouched in Stage 1 (view layer unchanged) |
|
||||
| Existing functionality unchanged | CodeMirror/Full Book/exports receive Markdown via adapter; unmigrated books are bit-for-bit identical |
|
||||
Loading…
Add table
Add a link
Reference in a new issue