//! Saving as Word 2003 XML (#1081): documents built in code, written as WordprocessingML 2003 //! and read back with the 2003 reader. use std::sync::Arc; use wordcraft_doc::numbering::ListKind; use wordcraft_doc::para::NoteKind; use wordcraft_doc::props::{Align, CharProps, NumRef, Rgb, TextColor}; use wordcraft_doc::table::{Cell, Table}; use wordcraft_doc::{Block, Comment, Document, InlineObject, Paragraph, PartKind, Revision, RevisionKind, para_block}; use wordcraft_docx::{XmlKind, read_xml, sniff_xml, write_wordml2003}; fn tiny_png() -> Vec { let img = image::RgbaImage::from_fn(3, 2, |x, y| image::Rgba([x as u8 * 80, y as u8 * 120, 200, 255])); let mut out = std::io::Cursor::new(Vec::new()); img.write_to(&mut out, image::ImageFormat::Png).unwrap(); out.into_inner() } fn paras(doc: &Document) -> Vec<&Paragraph> { doc.body.iter().filter_map(|b| b.as_para()).collect() } fn part_text(doc: &Document, id: Option) -> String { id.and_then(|i| doc.parts.get(&i)) .map(|p| p.blocks.iter().filter_map(|b| b.as_para()).map(|p| p.plain_text()).collect::>().join("\n")) .unwrap_or_default() } fn sample() -> Document { let mut d = Document::new(); d.core.title = "Garden & more".into(); d.core.creator = "Ada".into(); let key = d.add_media(tiny_png(), "png"); let num = d.numbering.add_list(ListKind::Numbered); let head = d.add_part(PartKind::Header, vec![para_block(Paragraph::with_text("Head", CharProps::default()))]); let foot = d.add_part(PartKind::Footnote, vec![para_block(Paragraph::with_text("Sown in May.", CharProps::default()))]); let com = d.add_part(PartKind::Comment, vec![para_block(Paragraph::with_text("Too short?", CharProps::default()))]); d.comments.insert(3, Comment { author: "Bo".into(), part: com, ..Default::default() }); d.revisions.push(Revision { kind: RevisionKind::Insert, author: "Ada".into(), date: String::new(), move_name: None }); d.last_section.headers.default = Some(head); let mut p = Paragraph::new(); for (t, c) in [ ("Plain ", CharProps::default()), ( "loud", CharProps { bold: Some(true), italic: Some(true), size: Some(14.0), color: Some(TextColor::Rgb(Rgb(255, 0, 0))), ..Default::default() }, ), (" link", CharProps { link: Some("https://example.org/beans?a=1&b=2".into()), ..Default::default() }), ] { let off = p.len(); p.insert_text(off, t, &c).unwrap(); } p.props.align = Some(Align::Center); p.props.indent_left = Some(36.0); p.props.space_after = Some(12.0); p.insert_object(0, InlineObject::CommentStart { id: 3 }, &CharProps::default()).unwrap(); let end = p.len(); p.insert_object(end, InlineObject::CommentEnd { id: 3 }, &CharProps::default()).unwrap(); let at = p.text.find("loud").unwrap(); p.insert_object(at, InlineObject::NoteRef { kind: NoteKind::Footnote, id: foot, custom: String::new() }, &CharProps::default()).unwrap(); let mut blocks = vec![para_block(p)]; let mut li = Paragraph::with_text("listed", CharProps { ins: Some(0), ..Default::default() }); li.props.numbering = Some(NumRef { num, level: 0 }); blocks.push(para_block(li)); let mut img = Paragraph::new(); img.insert_object( 0, InlineObject::Image { media: key, w: 30.0, h: 20.0, alt: "dot".into(), float: Default::default(), crop: [0.0; 4], ole: None }, &CharProps::default(), ) .unwrap(); blocks.push(para_block(img)); let mut t = Table::new(2, 2, 300.0); t.rows[0].cells[0] = Cell::with_text("A1"); t.rows[0].cells[1] = Cell::with_text("B1"); t.rows[1].cells[0].props.shading = Some(Rgb(0, 128, 0)); blocks.push(Arc::new(Block::Table(t))); blocks.push(para_block(Paragraph::new())); d.body = blocks; d } #[test] fn round_trips_through_the_2003_reader() { let d = sample(); let xml = write_wordml2003(&d).expect("write"); let text = std::str::from_utf8(&xml).expect("utf-8"); assert!(text.contains("") && text.contains("") && text.contains(" & more"); assert_eq!(back.core.creator, "Ada"); let ps = paras(&back); assert_eq!(ps[0].plain_text(), "Plain loud link"); assert_eq!(ps[0].props.align, Some(Align::Center)); assert_eq!(ps[0].props.indent_left, Some(36.0)); assert_eq!(ps[0].props.space_after, Some(12.0)); let c = ps[0].props_of_char(ps[0].text.find("loud").unwrap()); assert_eq!((c.bold, c.italic, c.size), (Some(true), Some(true), Some(14.0))); assert!(c.color.is_some()); assert_eq!(ps[0].props_at(ps[0].text.find("link").unwrap()).link.as_deref(), Some("https://example.org/beans?a=1&b=2")); let note = ps[0].objects.iter().find_map(|o| match o { InlineObject::NoteRef { id, .. } => Some(*id), _ => None, }); assert_eq!(part_text(&back, note), "Sown in May."); let comment = back.comments.values().next().expect("comment"); assert_eq!((comment.author.as_str(), part_text(&back, Some(comment.part)).as_str()), ("Bo", "Too short?")); let listed = ps[1]; assert!(listed.props.numbering.is_some(), "list paragraph keeps its list"); let ins = listed.props_of_char(0).ins.and_then(|i| back.revisions.get(i as usize)); assert_eq!(ins.map(|r| r.author.as_str()), Some("Ada")); let pic = ps.iter().flat_map(|p| p.objects.iter()).find(|o| matches!(o, InlineObject::Image { .. })); assert!(matches!(pic, Some(InlineObject::Image { w, h, alt, .. }) if *w == 30.0 && *h == 20.0 && alt == "dot")); assert_eq!(back.media.len(), 1); let table = back.body.iter().find_map(|b| match b.as_ref() { Block::Table(t) => Some(t), _ => None, }); let table = table.expect("table"); assert_eq!(table.rows.len(), 2); assert_eq!(table.rows[0].cells[1].blocks.first().and_then(|b| b.as_para()).map(|p| p.plain_text()).as_deref(), Some("B1")); assert_eq!(table.rows[1].cells[0].props.shading, Some(Rgb(0, 128, 0))); assert_eq!(part_text(&back, back.last_section.headers.default), "Head"); } /// Features 2003 XML can't carry drop out without an error, and the text stays. #[test] fn control_characters_and_unsupported_content_do_not_break_the_file() { let mut d = Document::new(); let mut p = Paragraph::with_text("a\u{1}b\u{FFFE}c <&> \"q\"", CharProps { size: Some(f32::NAN), ..Default::default() }); p.insert_object( 0, InlineObject::Opaque { format: "docx".into(), xml: "raw".into(), text: "raw".into() }, &CharProps::default(), ) .unwrap(); d.body = vec![para_block(p)]; let xml = write_wordml2003(&d).expect("write"); assert!(read_xml(&xml).is_ok()); let text = String::from_utf8(xml).expect("utf-8"); assert!(!text.contains('\u{1}') && !text.contains('\u{FFFE}')); assert!(!text.contains("