feat: spellcheck menu, typewriter mode, file associations, updater hardening

English spellcheck suggestions with Add-to-dictionary in the native context menu plus a Settings toggle. Typewriter caret-centering mode. OS file associations for .md/.txt and Open File wording. Updater: release URL surfaced, single progress listener, two-byte tarball check, installer wait guard. Search/installer test suites registered; chapter-list layout polish; ignore KDE .directory files.
This commit is contained in:
avi 2026-09-19 16:38:17 -05:00
commit 542a545c0d
17 changed files with 445 additions and 73 deletions

41
tests/run-search-test.mjs Normal file
View file

@ -0,0 +1,41 @@
// Standalone test for the whole-book search helpers (src/renderer/book/ui.ts).
// These pure functions power the Search modal: Markdown flattening, literal
// matching (phrases included), and result snippets.
// Run with: node tests/run-search-test.mjs
import { bundleForTest } from "./helpers/bundle.mjs";
const { mod: ui, cleanupBundle } = await bundleForTest({
name: "folio-search-ui",
entryPoints: ["src/renderer/book/ui.ts"],
});
const { findMatches, markdownToPlain, makeSnippet, moveInList, escapeHtml } = ui;
let failures = 0;
function ok(cond, msg) {
if (cond) console.log(" ok -", msg);
else { failures++; console.error("FAIL -", msg); }
}
// markdown -> searchable plain text
ok(markdownToPlain("# Title\nSome **bold** text") === "Title\nSome bold text", "headings/emphasis flattened");
ok(markdownToPlain("[link](http://x)") === "link", "links flattened to text");
ok(markdownToPlain("`code`") === "code", "inline code flattened");
// literal matching incl. phrases, case handling
ok(JSON.stringify(findMatches("hello world hello", "hello", true)) === "[0,12]", "finds all occurrences");
ok(findMatches("Hello", "hello", true).length === 0, "match-case respected");
ok(findMatches("Hello", "hello", false).length === 1, "case-insensitive match");
ok(JSON.stringify(findMatches("the quick brown fox", "quick brown", false)) === "[4]", "phrase search works");
ok(findMatches("a.b", "a.b", false).length === 1, "regex chars treated literally");
// snippets highlight the match and escape HTML
const snip = makeSnippet("say <hi> & bye", 4, 4);
ok(snip.includes("<mark>") && snip.includes("&lt;hi&gt;"), "snippet highlights + escapes");
// shared reorder math used by drag + keyboard reorder
ok(JSON.stringify(moveInList([1, 2, 3], 0, 2)) === "[2,3,1]", "moveInList moves item");
ok(escapeHtml("<a>&") === "&lt;a&gt;&amp;", "escapeHtml works");
cleanupBundle();
if (failures) { console.error(`${failures} search check(s) FAILED`); process.exit(1); }
console.log("All search checks passed.");