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
145
tests/run-print-test.mjs
Normal file
145
tests/run-print-test.mjs
Normal 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("☒") && html.includes("done task") && html.includes("☐") && 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.");
|
||||
Loading…
Add table
Add a link
Reference in a new issue