mirror of
https://github.com/storytold/wordcraft.git
synced 2026-11-09 04:01:39 +00:00
docx: normalize TOCHeading gallery content controls with trailing FieldEnd paragraph (#5952)
* docx: normalize TOCHeading gallery content controls with trailing FieldEnd paragraph * test(docx): assert TOC entries survive round-trip in section-break TOC control test
This commit is contained in:
1 parent
f0fb31c0c9
commit
b8a7e2acdb
2 files changed
+58
-3
No files matched your search
@@ -83,13 +83,14 @@ pub(super) fn toc_control_to_toc(body: &mut Blocks) {
|
||||
}
|
||||
// An empty paragraph opening the control takes the field. It may end a section, as above
|
||||
// entries in columns (#3889).
|
||||
if engine.get(s).and_then(|b| b.as_para()).is_some_and(|p| p.is_empty()) {
|
||||
let empty_heading = engine.get(s).and_then(|b| b.as_para()).is_some_and(|p| p.is_empty());
|
||||
if empty_heading {
|
||||
field_to_heading(&mut engine, s + 1);
|
||||
} else {
|
||||
toc_field_to_heading(&mut engine);
|
||||
}
|
||||
if crate::write::written_toc_span(&engine) == Some((s, last)) {
|
||||
*body = toc;
|
||||
*body = if empty_heading { toc } else { engine };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,7 +98,7 @@ pub(super) fn toc_control_to_toc(body: &mut Blocks) {
|
||||
/// reproduce exactly. Other imported TOCs keep their explicit boundaries, independent of styles.
|
||||
pub(super) fn toc_field_to_heading(body: &mut Blocks) {
|
||||
for i in 1..body.len() {
|
||||
if body[i - 1].as_para().is_some_and(|p| p.props.style.as_deref() == Some("TOCHeading")) {
|
||||
if body.get(i - 1).and_then(|b| b.as_para()).is_some_and(|p| p.props.style.as_deref() == Some("TOCHeading")) {
|
||||
field_to_heading(body, i);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -342,3 +342,57 @@ fn toc_gallery_around_a_toc_whose_heading_is_in_a_block_control_round_trips() {
|
||||
assert!(xml.rfind("</w:sdt>").unwrap() < xml.rfind(">Chapter One<").unwrap(), "pass {pass}: {xml}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn word_toc_gallery_with_field_end_after_entries_round_trips_without_update() {
|
||||
let style = |s: &str, inner: &str| format!(r#"<w:p><w:pPr><w:pStyle w:val="{s}"/></w:pPr>{inner}</w:p>"#);
|
||||
let begin = r#"<w:r><w:fldChar w:fldCharType="begin"/></w:r><w:r><w:instrText xml:space="preserve"> TOC \o "1-3" \h \z \u </w:instrText></w:r><w:r><w:fldChar w:fldCharType="separate"/></w:r>"#;
|
||||
let content = format!(
|
||||
"{}{}{}<w:p><w:r><w:fldChar w:fldCharType=\"end\"/></w:r></w:p>",
|
||||
style("TOCHeading", &t("Contents")),
|
||||
style("TOC1", &format!("{begin}{}", t("Chapter One\t1"))),
|
||||
style("TOC2", &t("Section\t2"))
|
||||
);
|
||||
let pr = r#"<w:id w:val="-1234567"/><w:docPartObj><w:docPartGallery w:val="Table of Contents"/><w:docPartUnique/></w:docPartObj>"#;
|
||||
let mut bytes = package(&format!("{}{}", sdt(pr, &content), p(&t("Chapter One"))));
|
||||
for pass in 0..2 {
|
||||
let d = read(&bytes).unwrap();
|
||||
bytes = write(&d).unwrap();
|
||||
let xml = document_xml(&bytes);
|
||||
assert_eq!(xml.matches("<w:sdt>").count(), 1, "pass {pass}: {xml}");
|
||||
assert!(xml.contains(r#"<w:docPartGallery w:val="Table of Contents"/>"#), "pass {pass}: {xml}");
|
||||
assert!(xml.find("</w:sdt>").unwrap() < xml.rfind(">Chapter One<").unwrap(), "pass {pass}: {xml}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn range_toc_without_control_stays_unwrapped_and_section_break_toc_control_hands_over() {
|
||||
let style = |s: &str, inner: &str| format!(r#"<w:p><w:pPr><w:pStyle w:val="{s}"/></w:pPr>{inner}</w:p>"#);
|
||||
let begin = r#"<w:r><w:fldChar w:fldCharType="begin"/></w:r><w:r><w:instrText xml:space="preserve"> TOC \o "1-3" \h \z \u </w:instrText></w:r><w:r><w:fldChar w:fldCharType="separate"/></w:r>"#;
|
||||
// A range TOC that was never in a content control (with FieldEnd in its own paragraph) must
|
||||
// not gain a Table of Contents w:sdt on save.
|
||||
let bare = format!(
|
||||
"{}{}{}<w:p><w:r><w:fldChar w:fldCharType=\"end\"/></w:r></w:p>{}",
|
||||
style("TOCHeading", &t("Contents")),
|
||||
style("TOC1", &format!("{begin}{}", t("Chapter One\t1"))),
|
||||
style("TOC2", &t("Section\t2")),
|
||||
p(&t("Chapter One")),
|
||||
);
|
||||
let d_bare = read(&package(&bare)).unwrap();
|
||||
let xml_bare = document_xml(&write(&d_bare).unwrap());
|
||||
assert_eq!(xml_bare.matches("<w:sdt>").count(), 0, "{xml_bare}");
|
||||
|
||||
// #3889: a Table of Contents control opening on an empty paragraph that ends a section above
|
||||
// the entries hands its control over on read so Update Table reuses the section-break paragraph.
|
||||
let sect_toc = format!(
|
||||
r#"<w:p><w:pPr><w:sectPr><w:pgSz w:w="11906" w:h="16838"/></w:sectPr></w:pPr></w:p>{}{}<w:p><w:r><w:fldChar w:fldCharType="end"/></w:r></w:p>"#,
|
||||
style("TOC1", &format!("{begin}{}", t("Chapter One\t1"))),
|
||||
style("TOC2", &t("Section\t2")),
|
||||
);
|
||||
let pr = r#"<w:id w:val="-1234567"/><w:docPartObj><w:docPartGallery w:val="Table of Contents"/><w:docPartUnique/></w:docPartObj>"#;
|
||||
let d_sect = read(&package(&format!("{}{}", sdt(pr, §_toc), p(&t("Chapter One"))))).unwrap();
|
||||
assert!(controls(&d_sect).is_empty());
|
||||
assert!(d_sect.body[0].as_para().unwrap().section.is_some());
|
||||
let xml_sect = document_xml(&write(&d_sect).unwrap());
|
||||
assert!(xml_sect.contains("Chapter One") && xml_sect.contains("Section"), "{xml_sect}");
|
||||
}
|
||||
Reference in new issue
Block a user