Page-break between chapters and numeric title fallback

Every chapter after the first now starts on a new page via a
Chapter_20_Break heading style (fo:break-before="page"); the first
chapter keeps the plain heading so there is no blank leading page.

Chapter titles now resolve as: manual override > "# Title:" header >
the chapter's 1-based position in the left pane (e.g. "3."). The old
"# heading" / filename fallbacks are replaced by this numbering, and the
same logic drives the chapter-title hint.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
landon
2026-07-26 14:51:21 -05:00
parent 7271e5e2ef
commit 6680c001bf
3 changed files with 81 additions and 52 deletions
+28 -3
View File
@@ -389,6 +389,9 @@ fn styles_xml() -> String {
<style:text-properties fo:font-size="14pt" fo:font-weight="bold"/>
</style:style>
{headings}
<style:style style:name="Chapter_20_Break" style:display-name="Chapter Break" style:family="paragraph" style:parent-style-name="Heading_20_1" style:next-style-name="Text_20_body" style:default-outline-level="1">
<style:paragraph-properties fo:break-before="page"/>
</style:style>
<style:style style:name="Chapter_20_Caption" style:display-name="Chapter Caption" style:family="paragraph" style:parent-style-name="Standard" style:next-style-name="Text_20_body" style:class="text">
<style:paragraph-properties fo:margin-top="0in" fo:margin-bottom="0.2in"/>
<style:text-properties fo:font-style="italic" fo:font-size="12pt" fo:color="#666666"/>
@@ -447,11 +450,17 @@ fn styles_xml() -> String {
fn content_xml(chapters: &[Chapter]) -> String {
let mut body = String::new();
for chapter in chapters {
for (i, chapter) in chapters.iter().enumerate() {
// Chapter heading (always level 1); the document's own headings are
// shifted down by one so they nest beneath it.
// shifted down by one so they nest beneath it. Every chapter after the
// first starts on a new page via a heading style with a page break.
let heading_style = if i == 0 {
"Heading_20_1"
} else {
"Chapter_20_Break"
};
body.push_str(&format!(
"<text:h text:style-name=\"Heading_20_1\" text:outline-level=\"1\">{}</text:h>",
"<text:h text:style-name=\"{heading_style}\" text:outline-level=\"1\">{}</text:h>",
esc(&chapter.title)
));
if let Some(slug) = chapter.slug.as_deref().map(str::trim).filter(|s| !s.is_empty()) {
@@ -593,10 +602,26 @@ Done.";
.unwrap();
assert!(content.contains(">Chapter One</text:h>"));
assert!(content.contains(">Chapter Two</text:h>"));
// The first chapter uses the plain heading; later chapters get a page break.
assert!(content.contains(
"<text:h text:style-name=\"Heading_20_1\" text:outline-level=\"1\">Chapter One</text:h>"
));
assert!(content.contains(
"<text:h text:style-name=\"Chapter_20_Break\" text:outline-level=\"1\">Chapter Two</text:h>"
));
// The slug renders as an italic caption paragraph under the first chapter.
assert!(content.contains(
"<text:p text:style-name=\"Chapter_20_Caption\">\
<text:span text:style-name=\"T_italic\">the opening</text:span></text:p>"
));
// The page-break style is defined in styles.xml.
let mut styles = String::new();
zip.by_name("styles.xml")
.unwrap()
.read_to_string(&mut styles)
.unwrap();
assert!(styles.contains("style:name=\"Chapter_20_Break\""));
assert!(styles.contains("fo:break-before=\"page\""));
}
}