fix(export): unify title-injection logic across DOCX/ODT and PDF; remove drift
This commit is contained in:
parent
4ce0a5ebfb
commit
3afb43d4ed
3 changed files with 57 additions and 29 deletions
|
|
@ -56,6 +56,42 @@ export const CALLOUT_TITLE_CLASS = "callout-title";
|
|||
// into [[wikilinks]]; the wikilink plugin turns [[text]] into it on parse.
|
||||
export const CHAPTER_LINK_PREFIX = "folio:chapter/";
|
||||
|
||||
// ---- Chapter presentation helpers ----------------------------------------
|
||||
// Shared by every assembled/exported rendering of a chapter (Full Book view,
|
||||
// PDF print, DOCX/ODT) so "does this chapter already carry its own title?"
|
||||
// has exactly one answer everywhere. Before this lived in two files with
|
||||
// different rules, DOCX and PDF could disagree about injecting the title.
|
||||
|
||||
/** Concatenated plain text of a node subtree (atom leaves contribute nothing). */
|
||||
export function nativeNodeText(node: FolioNode): string {
|
||||
if (node.text) return node.text;
|
||||
let t = "";
|
||||
for (const c of node.content ?? []) t += nativeNodeText(c);
|
||||
return t;
|
||||
}
|
||||
|
||||
/** Whether any top-level heading in the content already matches the title
|
||||
* (case/space-insensitive). If true, renderers must not inject a duplicate
|
||||
* title heading ahead of the content. */
|
||||
export function nativeHasTitleHeading(content: FolioNode[], title: string): boolean {
|
||||
const t = title.trim().toLowerCase();
|
||||
if (!t) return false;
|
||||
for (const n of content) {
|
||||
if (n.type === "heading" && nativeNodeText(n).trim().toLowerCase() === t) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Top-level nodes for one chapter, injecting a title heading when the
|
||||
* content does not already provide one. */
|
||||
export function buildChapterNativeContent(content: FolioNode[], title: string): FolioNode[] {
|
||||
if (nativeHasTitleHeading(content, title)) return content.slice();
|
||||
return [
|
||||
{ type: "heading", attrs: { level: 1 }, content: [{ type: "text", text: title }] },
|
||||
...content,
|
||||
];
|
||||
}
|
||||
|
||||
// Each entry migrates the document FROM its key version TO key+1.
|
||||
// Entries are additive and must be non-destructive.
|
||||
const MIGRATIONS: Record<number, (doc: FolioChapterDoc) => FolioChapterDoc> = {};
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ import {
|
|||
CALLOUT_TITLE_CLASS,
|
||||
CHAPTER_LINK_PREFIX,
|
||||
} from "./native-doc.js";
|
||||
import { nativeHasTitleHeading } from "./native-doc.js";
|
||||
import type { FolioMark, FolioNode } from "./native-doc.js";
|
||||
|
||||
const MONO_FONT = "Consolas";
|
||||
|
|
@ -192,14 +193,13 @@ function nodeText(node: FolioNode): string {
|
|||
return t;
|
||||
}
|
||||
|
||||
// Whether the chapter's native content already opens with a heading matching
|
||||
// its title, in which case we must not inject a duplicate title heading.
|
||||
// Whether to prepend a title heading. Delegates to the shared predicate so
|
||||
// DOCX/ODT answer identically to the Full Book view and PDF print (the old
|
||||
// local copy only inspected the FIRST node, so a chapter whose self-title
|
||||
// heading sat deeper in the document got a duplicate injected here while the
|
||||
// other paths correctly skipped it).
|
||||
function needsTitleInjection(content: FolioNode[], title: string): boolean {
|
||||
const t = title.trim().toLowerCase();
|
||||
if (!t) return false;
|
||||
const first = content[0];
|
||||
if (first && first.type === "heading" && nodeText(first).trim().toLowerCase() === t) return false;
|
||||
return true;
|
||||
return !nativeHasTitleHeading(content, title);
|
||||
}
|
||||
|
||||
function headingLevel(level: number) {
|
||||
|
|
|
|||
|
|
@ -5,8 +5,18 @@
|
|||
// These helpers are kept free of DOM/IPC so they can be unit-tested directly
|
||||
// and reused by the renderer (book.ts).
|
||||
import type { FolioNode } from "../main/native-doc.js";
|
||||
import {
|
||||
nativeNodeText,
|
||||
nativeHasTitleHeading,
|
||||
buildChapterNativeContent,
|
||||
} from "../main/native-doc.js";
|
||||
import type { WikilinkTarget } from "./wikilinks.js";
|
||||
|
||||
// The title-heading helpers are shared with the DOCX/ODT exporter and live
|
||||
// next to the schema in native-doc.ts. Re-exported so existing consumers
|
||||
// (and the fullbook test entry) keep their import paths.
|
||||
export { nativeNodeText, nativeHasTitleHeading, buildChapterNativeContent };
|
||||
|
||||
export interface FullBookChapterInput {
|
||||
id: string;
|
||||
title: string;
|
||||
|
|
@ -22,33 +32,15 @@ export interface FullBookAssembly {
|
|||
|
||||
// Concatenate the plain text of a native node tree (used for word/char counts).
|
||||
// Atom leaf nodes (chapterBreak, image, horizontal rule, …) carry no text.
|
||||
export function nativeNodeText(node: FolioNode): string {
|
||||
if (node.text) return node.text;
|
||||
let t = "";
|
||||
for (const c of node.content ?? []) t += nativeNodeText(c);
|
||||
return t;
|
||||
}
|
||||
// (Implementation lives in native-doc.ts; re-exported above.)
|
||||
|
||||
// Whether the native content already opens with a heading matching its title,
|
||||
// in which case we must not inject a duplicate title heading.
|
||||
export function nativeHasTitleHeading(content: FolioNode[], title: string): boolean {
|
||||
const t = title.trim().toLowerCase();
|
||||
if (!t) return false;
|
||||
for (const n of content) {
|
||||
if (n.type === "heading" && nativeNodeText(n).trim().toLowerCase() === t) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
// (Implementation lives in native-doc.ts; re-exported above.)
|
||||
|
||||
// Build the top-level nodes for one chapter in the Full Book, injecting a title
|
||||
// Top-level nodes for one chapter in the Full Book, injecting a title
|
||||
// heading when the content does not already provide one.
|
||||
export function buildChapterNativeContent(content: FolioNode[], title: string): FolioNode[] {
|
||||
if (nativeHasTitleHeading(content, title)) return content.slice();
|
||||
return [
|
||||
{ type: "heading", attrs: { level: 1 }, content: [{ type: "text", text: title }] },
|
||||
...content,
|
||||
];
|
||||
}
|
||||
// (Implementation lives in native-doc.ts; re-exported above.)
|
||||
|
||||
// A ChapterBreak anchor marking where a chapter begins in the assembled view.
|
||||
export function chapterBreakNode(id: string, title: string, first: boolean): FolioNode {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue