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:
avi 2026-08-20 11:12:17 -05:00
commit 786a65ae2a
48 changed files with 5762 additions and 3999 deletions

145
tests/run-print-test.mjs Normal file
View file

@ -0,0 +1,145 @@
// Standalone test for the native -> print HTML renderer (print-format.ts).
// This is the path the PDF export uses (#printBook innerHTML). It bundles the
// renderer module and verifies that chapter breaks become print sections and
// that every rich-text feature survives the native -> HTML conversion.
// Run with: node tests/run-print-test.mjs
import esbuild from "esbuild";
import fs from "fs";
import os from "os";
import path from "path";
import url from "url";
const out = path.resolve(`tests/.tmp-folio-print-${process.pid}.mjs`);
await esbuild.build({
entryPoints: [path.resolve("src/renderer/print-format.ts")],
bundle: true,
platform: "node",
format: "esm",
external: ["electron"],
outfile: out,
write: true,
logLevel: "silent",
});
const mod = await import(url.pathToFileURL(out).href);
const renderPrintChaptersNative = mod.renderPrintChaptersNative;
let failures = 0;
const check = (name, ok) => {
console.log(`${ok ? "PASS" : "FAIL"} ${name}`);
if (!ok) failures += 1;
};
// --- native document with chapter breaks + every formatting feature ---------
const text = (t, marks = []) => ({ type: "text", text: t, marks });
const para = (content, attrs = {}) => ({ type: "paragraph", attrs, content });
const heading = (level, t) => ({ type: "heading", attrs: { level }, content: [text(t)] });
const listItem = (content) => ({ type: "listItem", content });
const doc = {
type: "doc",
content: [
{ type: "chapterBreak", attrs: { chapterId: "c1", title: "One", first: true } },
heading(1, "One"),
para([
text("bold", [{ type: "bold" }]),
text(" "),
text("italic", [{ type: "italic" }]),
text(" "),
text("underline", [{ type: "underline" }]),
text(" "),
text("hl", [{ type: "highlight" }]),
text(" "),
text("red", [{ type: "textColor", attrs: { color: "#e11d48" } }]),
text(" "),
text("mk", [{ type: "highlight", attrs: { color: "#fde047" } }]),
]),
para([text("center")], { textAlign: "center" }),
{ type: "bulletList", content: [listItem([para([text("bullet one")])])] },
{
type: "orderedList",
content: [
listItem([para([text("numbered one")])]),
listItem([para([text("numbered two")])]),
],
},
{
type: "taskList",
content: [
{ type: "taskItem", attrs: { checked: true }, content: [para([text("done task")])] },
{ type: "taskItem", attrs: { checked: false }, content: [para([text("todo task")])] },
],
},
{
type: "blockquote",
attrs: { callout: "note" },
content: [
para([text("Callout title")], { class: "callout-title" }),
para([text("callout body")]),
],
},
{ type: "codeBlock", attrs: { language: "js" }, content: [text("const answer = 42;")] },
{ type: "horizontalRule" },
{
type: "table",
content: [
{
type: "tableRow",
content: [
{ type: "tableHeader", content: [para([text("Head A")])] },
{ type: "tableHeader", content: [para([text("Head B")])] },
],
},
{
type: "tableRow",
content: [
{ type: "tableCell", content: [para([text("cell 1")])] },
{ type: "tableCell", content: [para([text("cell 2")])] },
],
},
],
},
{ type: "image", attrs: { src: "assets/pic.png", alt: "a picture" } },
para([text("Site", [{ type: "link", attrs: { href: "https://example.com" } }])]),
para([text("Go", [{ type: "link", attrs: { href: "folio:chapter/Two" } }])]),
{ type: "chapterBreak", attrs: { chapterId: "c2", title: "Two", first: false } },
heading(1, "Two"),
para([text("second chapter body")]),
],
};
const html = renderPrintChaptersNative(doc);
check("has two print-chapter sections", (html.match(/<section class="print-chapter">/g) || []).length === 2);
const iOne = html.indexOf("One");
const iTwo = html.indexOf("Two");
check("chapters appear in order", iOne >= 0 && iOne < iTwo);
check("first section contains chapter one", html.indexOf("<section") < iOne && html.indexOf("second chapter body") > iOne);
check("renders h1", /<h1>One<\/h1>/.test(html));
check("renders bold", /<strong>bold<\/strong>/.test(html));
check("renders italic", /<em>italic<\/em>/.test(html));
check("renders underline", /<u>underline<\/u>/.test(html));
check("renders highlight as mark", /<mark>hl<\/mark>/.test(html));
check("renders text color span", /<span style="color:#e11d48">red<\/span>/.test(html));
check("renders highlight color span", /<span style="background-color:#fde047">mk<\/span>/.test(html));
check("renders centered paragraph", /<p style="text-align:center">center<\/p>/.test(html));
check("renders bullet list", /<ul>[\s\S]*bullet one[\s\S]*<\/ul>/.test(html));
check("renders ordered list", /<ol>[\s\S]*numbered one[\s\S]*numbered two[\s\S]*<\/ol>/.test(html));
check("renders task list with checkboxes", html.includes("&#9746;") && html.includes("done task") && html.includes("&#9744;") && html.includes("todo task"));
check("renders callout", /<div class="callout callout-note">.*<p class="callout-title">Callout title<\/p>/s.test(html));
check("renders code block", /<pre><code>const answer = 42;<\/code><\/pre>/.test(html));
check("renders horizontal rule", /<hr>/.test(html));
check("renders table with header + cells", /<table class="folio-table">.*<th>Head A<\/th>.*<td>cell 1<\/td>.*<\/table>/s.test(html));
check("renders image", /<img src="assets\/pic.png" alt="a picture">/.test(html));
check("renders external link", /<a href="https:\/\/example.com">Site<\/a>/.test(html));
check("renders wikilink as internal anchor", /<a href="folio:chapter\/Two" class="wikilink internal">Go<\/a>/.test(html));
check("no raw wikilink brackets", !html.includes("[[Two"));
fs.rmSync(out, { force: true });
if (failures > 0) {
console.error(`\n${failures} check(s) failed.`);
process.exit(1);
}
console.log("\nAll print-format checks passed.");