Files
wordcraft/crates/engine/tests/it/odt_fields.rs
T
Claude 0b6b88f414 Gap 19: ODT custom document properties and user fields as DOCPROPERTY
ODT custom document properties (`meta:user-defined` in `meta.xml`) were
dropped, and the fields showing them or a user field were read as
plain text.
- Properties: `meta:user-defined` values (string, number, true/false,
  date) are now WordCraft's custom document properties, and so are
  user field declarations (`text:user-field-decl`).
- Fields: `text:user-defined` and `text:user-field-get` are Word's
  `DOCPROPERTY` fields, which WordCraft updates from those properties.
- Writing: custom properties go back into `meta.xml`, and a
  `DOCPROPERTY` naming one is written as `text:user-defined`.

A LibreOffice document with a custom property and a user field
round-trips pixel-identical, valid against the schema. Its user field
comes back as a document property.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
2026-11-01 20:30:48 +00:00

100 lines
5.7 KiB
Rust

//! Fields in an ODT file update in WordCraft as they do in Word: a reference to a bookmark shows
//! the bookmarked text (a bookmark over text, `text:bookmark-start`…`-end`, not a point). The
//! ODT is original and synthetic, shaped like LibreOffice's.
use std::io::Write;
use serde_json::json;
use wordcraft_engine::{Session, io};
const NS: &str = r#"xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0" xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0" xmlns:style="urn:oasis:names:tc:opendocument:xmlns:style:1.0" xmlns:fo="urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0""#;
fn odt(body: &str) -> Vec<u8> {
let content = format!(r#"<office:document-content {NS}><office:body><office:text>{body}</office:text></office:body></office:document-content>"#);
let mut z = zip::ZipWriter::new(std::io::Cursor::new(Vec::new()));
let o = zip::write::SimpleFileOptions::default().compression_method(zip::CompressionMethod::Stored);
for (name, data) in [("mimetype", "application/vnd.oasis.opendocument.text"), ("content.xml", content.as_str())] {
z.start_file(name, o).unwrap();
z.write_all(data.as_bytes()).unwrap();
}
z.finish().unwrap().into_inner()
}
fn text(s: &Session) -> Vec<String> {
s.doc.body.iter().filter_map(|b| b.as_para()).map(|p| p.text.replace(wordcraft_doc::para::OBJ, "")).collect()
}
/// The displayed results of the fields in the body.
fn results(s: &Session) -> Vec<String> {
s.doc
.body
.iter()
.filter_map(|b| b.as_para())
.flat_map(|p| p.objects.iter())
.filter_map(|o| match o {
wordcraft_doc::para::InlineObject::Field { result, .. } => Some(result.clone()),
_ => None,
})
.collect()
}
#[test]
fn a_reference_to_a_bookmark_updates_to_its_text() {
let doc = io::open_bytes(
"refs.odt",
&odt(r#"<text:h text:outline-level="1"><text:bookmark-start text:name="__RefHeading___Toc1"/>Planning ahead<text:bookmark-end text:name="__RefHeading___Toc1"/></text:h><text:p>See <text:bookmark-ref text:reference-format="text" text:ref-name="__RefHeading___Toc1">Planning</text:bookmark-ref>.</text:p>"#),
)
.unwrap();
let mut s = Session::new(doc);
assert_eq!(results(&s), ["Planning"]);
s.run("references.updateFields", &json!({"all": true})).unwrap();
assert_eq!(results(&s), ["Planning ahead"], "{:?}", text(&s));
// And after a save in ODT and opening again.
let again = io::open_bytes("refs.odt", &io::save_bytes("refs.odt", &s.doc).unwrap()).unwrap();
let mut s2 = Session::new(again);
s2.run("references.updateFields", &json!({"all": true})).unwrap();
assert_eq!(results(&s2), ["Planning ahead"]);
}
#[test]
fn captions_chapters_and_dates_update_as_words() {
// Stale results, as a file can hold them: they are recomputed.
let doc = io::open_bytes(
"fields.odt",
&odt(r#"<text:h text:outline-level="1">Methods</text:h>
<text:p>Figure <text:sequence text:ref-name="refFigure0" text:name="Figure" text:formula="ooow:Figure+1" style:num-format="1">7</text:sequence>: a cat</text:p>
<text:p>Figure <text:sequence text:ref-name="refFigure1" text:name="Figure" text:formula="ooow:Figure+1" style:num-format="1">9</text:sequence>: a dog</text:p>
<text:p>See <text:sequence-ref text:reference-format="category-and-value" text:ref-name="refFigure1">Figure 9</text:sequence-ref> in <text:chapter text:display="name" text:outline-level="1">Old</text:chapter>.</text:p>"#),
)
.unwrap();
let mut s = Session::new(doc);
s.run("references.updateFields", &json!({"all": true})).unwrap();
assert_eq!(results(&s), ["1", "2", "Figure 2", "Methods"], "{:?}", text(&s));
}
#[test]
fn a_flat_document_opens_and_saves_by_its_extension() {
let fodt = r#"<?xml version="1.0" encoding="UTF-8"?><office:document xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0" xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0" office:version="1.3" office:mimetype="application/vnd.oasis.opendocument.text"><office:body><office:text><text:p>Flat words.</text:p></office:text></office:body></office:document>"#;
let doc = io::open_bytes("plain.fodt", fodt.as_bytes()).unwrap();
let s = Session::new(doc);
assert_eq!(text(&s), ["Flat words."]);
let saved = io::save_bytes("plain.fodt", &s.doc).unwrap();
assert!(saved.starts_with(b"<?xml"));
assert_eq!(text(&Session::new(io::open_bytes("plain.fodt", &saved).unwrap())), ["Flat words."]);
}
#[test]
fn a_user_field_updates_from_its_property() {
// An ODT user field opens as `DOCPROPERTY`: updating fields shows the property's value.
let content = r#"<office:document-content xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0" xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0"><office:body><office:text><text:user-field-decls><text:user-field-decl text:name="Owner" office:value-type="string" office:string-value="Ada"/></text:user-field-decls><text:p>By <text:user-field-get text:name="Owner">stale</text:user-field-get>.</text:p></office:text></office:body></office:document-content>"#;
let mut z = zip::ZipWriter::new(std::io::Cursor::new(Vec::new()));
let o = zip::write::SimpleFileOptions::default().compression_method(zip::CompressionMethod::Stored);
for (name, data) in [("mimetype", "application/vnd.oasis.opendocument.text"), ("content.xml", content)] {
z.start_file(name, o).unwrap();
std::io::Write::write_all(&mut z, data.as_bytes()).unwrap();
}
let mut s = Session::new(io::open_bytes("user.odt", &z.finish().unwrap().into_inner()).unwrap());
s.run("references.updateFields", &json!({"all": true})).unwrap();
assert_eq!(results(&s), ["Ada"]);
}