Fix migration/backup partial-failure residue, theme selector fallback, dialog promise orphaning, print table multi-block cells

This commit is contained in:
avi 2026-08-24 19:53:27 -05:00
commit a3f366ca60
5 changed files with 47 additions and 4 deletions

View file

@ -48,7 +48,18 @@ export function ensureRecoveryBackup(bookPath: string): string | null {
const dest = path.join(getRecoveryRoot(), `${bookName}-${stamp}`); const dest = path.join(getRecoveryRoot(), `${bookName}-${stamp}`);
fs.mkdirSync(getRecoveryRoot(), { recursive: true }); fs.mkdirSync(getRecoveryRoot(), { recursive: true });
fs.cpSync(key, dest, { recursive: true }); try {
fs.cpSync(key, dest, { recursive: true });
} catch (err) {
// A half-copied directory is worse than none — it looks like a good
// backup. Remove the partial copy, then surface the failure.
try {
fs.rmSync(dest, { recursive: true, force: true });
} catch {
// best-effort cleanup only
}
throw err;
}
sessionBackedUp.add(key); sessionBackedUp.add(key);
return dest; return dest;

View file

@ -107,7 +107,19 @@ export function migrateChapterToNative(
entry.file = newFile; entry.file = newFile;
meta.docFormat = "folio-doc"; meta.docFormat = "folio-doc";
meta.docSchemaVersion = CURRENT_DOC_SCHEMA_VERSION; meta.docSchemaVersion = CURRENT_DOC_SCHEMA_VERSION;
saveMeta(bookPath, meta); try {
saveMeta(bookPath, meta);
} catch (err) {
// Meta write failed: roll the orphan chapter JSON back so the book
// stays consistent with folio.json (still pointing at the .md). A
// later retry simply rewrites it.
try {
fs.rmSync(path.join(bookPath, newFile), { force: true });
} catch {
// best-effort cleanup only
}
throw err;
}
return { status: "migrated", id, file: newFile }; return { status: "migrated", id, file: newFile };
} catch (err) { } catch (err) {

View file

@ -17,6 +17,10 @@ let currentMode: Mode = "prompt";
let resolver: ((value: string | null) => void) | null = null; let resolver: ((value: string | null) => void) | null = null;
function open(mode: Mode, title: string, body: string, value: string): Promise<string | null> { function open(mode: Mode, title: string, body: string, value: string): Promise<string | null> {
// If a previous dialog was somehow still open (menu accelerators can open
// one while another is visible), settle its promise as cancelled so the
// awaiting caller isn't stranded forever.
close(null);
currentMode = mode; currentMode = mode;
titleEl.textContent = title; titleEl.textContent = title;
msgEl.textContent = body; msgEl.textContent = body;

View file

@ -139,7 +139,18 @@ function tableToHtml(node: FolioNode): string {
const cells = (row.content ?? []) const cells = (row.content ?? [])
.map((cell) => { .map((cell) => {
const tag = cell.type === "tableHeader" ? "th" : "td"; const tag = cell.type === "tableHeader" ? "th" : "td";
return `<${tag}>${inlineToHtml((cell.content?.[0]?.content ?? []) as FolioNode[])}</${tag}>`; const blocks = cell.content ?? [];
// A cell can hold multiple blocks (Tiptap allows multi-paragraph
// cells); render them all instead of silently dropping every block
// after the first. The common single-paragraph cell keeps its old
// bare-inline shape so existing table styling is untouched.
let inner: string;
if (blocks.length === 1 && blocks[0].type === "paragraph") {
inner = inlineToHtml(blocks[0].content);
} else {
inner = blocks.map(blockToHtml).join("");
}
return `<${tag}>${inner}</${tag}>`;
}) })
.join(""); .join("");
return `<tr>${cells}</tr>`; return `<tr>${cells}</tr>`;

View file

@ -14,9 +14,14 @@ export function selector(): HTMLSelectElement {
// theme, which replaced it as Folio's default light appearance. // theme, which replaced it as Folio's default light appearance.
const LIGHT = "light"; const LIGHT = "light";
// Themes with a real entry in themes.css + the selector. Anything else that
// might linger in saved settings (a removed theme, a typo) falls back to
// Light so the <select> never desyncs from its <option> list.
const KNOWN = new Set(["light", "dark-mode", "neon", "meridian"]);
function normalizeTheme(id: unknown): string { function normalizeTheme(id: unknown): string {
if (typeof id !== "string" || !id || id === "default") return LIGHT; if (typeof id !== "string" || !id || id === "default") return LIGHT;
return id; return KNOWN.has(id) ? id : LIGHT;
} }
export function applyTheme(id: string): void { export function applyTheme(id: string): void {