use quick_xml::events::{BytesDecl, BytesEnd, BytesStart, Event};
use quick_xml::{Reader, Writer};
use crate::error::Result;
use crate::header_footer::{HdrFtrRef, HdrFtrType};
use crate::namespace::{W_NS, matches_local_name};
use crate::numbering::word_prefixes_at;
use crate::properties::{get_val_attr, is_word_element};
use crate::raw_xml::{capture_element, capture_empty_element};
use crate::shared::{ST_PageOrientation, ST_SectionType};
use crate::table::CT_Tbl;
use crate::text::CT_P;
use crate::units::Twips;
#[derive(Debug, Clone, PartialEq)]
pub enum BodyContent {
Paragraph(CT_P),
Table(CT_Tbl),
RawXml(Vec<u8>),
}
#[derive(Debug, Clone, PartialEq)]
pub struct CT_Column {
pub width: Option<Twips>,
pub space: Option<Twips>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct CT_Columns {
pub num: Option<u32>,
pub space: Option<Twips>,
pub equal_width: Option<bool>,
pub sep: Option<bool>,
pub columns: Vec<CT_Column>,
}
impl Default for CT_Columns {
fn default() -> Self {
CT_Columns {
num: Some(1),
space: Some(Twips(720)),
equal_width: Some(true),
sep: None,
columns: Vec::new(),
}
}
}
#[derive(Debug, Clone, PartialEq)]
#[allow(non_snake_case)]
pub struct CT_SectPr {
pub page_width: Option<Twips>,
pub page_height: Option<Twips>,
pub orientation: Option<ST_PageOrientation>,
pub margin_top: Option<Twips>,
pub margin_right: Option<Twips>,
pub margin_bottom: Option<Twips>,
pub margin_left: Option<Twips>,
pub gutter: Option<Twips>,
pub header_distance: Option<Twips>,
pub footer_distance: Option<Twips>,
pub section_type: Option<ST_SectionType>,
pub columns: Option<CT_Columns>,
pub title_pg: Option<bool>,
pub header_refs: Vec<HdrFtrRef>,
pub footer_refs: Vec<HdrFtrRef>,
pub extra_xml: Vec<Vec<u8>>,
}
#[allow(non_snake_case)]
impl CT_SectPr {
pub fn default_letter() -> Self {
CT_SectPr {
page_width: Some(Twips(12240)), page_height: Some(Twips(15840)), orientation: Some(ST_PageOrientation::Portrait),
margin_top: Some(Twips(1440)), margin_right: Some(Twips(1440)), margin_bottom: Some(Twips(1440)), margin_left: Some(Twips(1440)), gutter: Some(Twips(0)),
header_distance: Some(Twips(720)),
footer_distance: Some(Twips(720)),
section_type: None,
columns: None,
title_pg: None,
header_refs: Vec::new(),
footer_refs: Vec::new(),
extra_xml: Vec::new(),
}
}
pub fn default_a4() -> Self {
CT_SectPr {
page_width: Some(Twips(11906)), page_height: Some(Twips(16838)), orientation: Some(ST_PageOrientation::Portrait),
margin_top: Some(Twips(1440)),
margin_right: Some(Twips(1440)),
margin_bottom: Some(Twips(1440)),
margin_left: Some(Twips(1440)),
gutter: Some(Twips(0)),
header_distance: Some(Twips(720)),
footer_distance: Some(Twips(720)),
section_type: None,
columns: None,
title_pg: None,
header_refs: Vec::new(),
footer_refs: Vec::new(),
extra_xml: Vec::new(),
}
}
pub fn from_xml(reader: &mut Reader<&[u8]>) -> Result<Self> {
let mut sect = CT_SectPr {
page_width: None,
page_height: None,
orientation: None,
margin_top: None,
margin_right: None,
margin_bottom: None,
margin_left: None,
gutter: None,
header_distance: None,
footer_distance: None,
section_type: None,
columns: None,
title_pg: None,
header_refs: Vec::new(),
footer_refs: Vec::new(),
extra_xml: Vec::new(),
};
let mut buf = Vec::new();
loop {
match reader.read_event_into(&mut buf) {
Ok(Event::Empty(ref e)) => {
let name = e.name();
if matches_local_name(name.as_ref(), b"pgSz") {
for attr in e.attributes() {
let attr = attr?;
let key = attr.key.as_ref();
let val_str = std::str::from_utf8(&attr.value)?;
if matches_local_name(key, b"w") {
sect.page_width = Some(Twips(val_str.parse()?));
} else if matches_local_name(key, b"h") {
sect.page_height = Some(Twips(val_str.parse()?));
} else if matches_local_name(key, b"orient") {
sect.orientation = Some(ST_PageOrientation::from_str(val_str)?);
}
}
} else if matches_local_name(name.as_ref(), b"pgMar") {
for attr in e.attributes() {
let attr = attr?;
let key = attr.key.as_ref();
let val: i32 = std::str::from_utf8(&attr.value)?.parse()?;
if matches_local_name(key, b"top") {
sect.margin_top = Some(Twips(val));
} else if matches_local_name(key, b"right")
|| matches_local_name(key, b"end")
{
sect.margin_right = Some(Twips(val));
} else if matches_local_name(key, b"bottom") {
sect.margin_bottom = Some(Twips(val));
} else if matches_local_name(key, b"left")
|| matches_local_name(key, b"start")
{
sect.margin_left = Some(Twips(val));
} else if matches_local_name(key, b"gutter") {
sect.gutter = Some(Twips(val));
} else if matches_local_name(key, b"header") {
sect.header_distance = Some(Twips(val));
} else if matches_local_name(key, b"footer") {
sect.footer_distance = Some(Twips(val));
}
}
} else if matches_local_name(name.as_ref(), b"type") {
if let Some(val) = get_val_attr(e)? {
sect.section_type = Some(ST_SectionType::from_str(&val)?);
}
} else if matches_local_name(name.as_ref(), b"cols") {
sect.columns = Some(Self::parse_cols_empty(e)?);
} else if matches_local_name(name.as_ref(), b"headerReference") {
let mut hdr_type = HdrFtrType::Default;
let mut rel_id = String::new();
for attr in e.attributes() {
let attr = attr?;
let key = attr.key.as_ref();
let val = std::str::from_utf8(&attr.value)?;
if matches_local_name(key, b"type") {
hdr_type = HdrFtrType::from_str(val);
} else if matches_local_name(key, b"id") {
rel_id = val.to_string();
}
}
if !rel_id.is_empty() {
sect.header_refs.push(HdrFtrRef {
hdr_ftr_type: hdr_type,
rel_id,
});
}
} else if matches_local_name(name.as_ref(), b"footerReference") {
let mut ftr_type = HdrFtrType::Default;
let mut rel_id = String::new();
for attr in e.attributes() {
let attr = attr?;
let key = attr.key.as_ref();
let val = std::str::from_utf8(&attr.value)?;
if matches_local_name(key, b"type") {
ftr_type = HdrFtrType::from_str(val);
} else if matches_local_name(key, b"id") {
rel_id = val.to_string();
}
}
if !rel_id.is_empty() {
sect.footer_refs.push(HdrFtrRef {
hdr_ftr_type: ftr_type,
rel_id,
});
}
} else if matches_local_name(name.as_ref(), b"titlePg") {
sect.title_pg = Some(true);
} else {
sect.extra_xml.push(capture_empty_element(e)?);
}
}
Ok(Event::Start(ref e)) => {
let name = e.name();
if matches_local_name(name.as_ref(), b"cols") {
sect.columns = Some(Self::parse_cols_start(reader, e)?);
} else {
sect.extra_xml.push(capture_element(reader, e)?);
}
}
Ok(Event::End(ref e)) if matches_local_name(e.name().as_ref(), b"sectPr") => {
break;
}
Ok(Event::Eof) => break,
Err(e) => return Err(e.into()),
_ => {}
}
buf.clear();
}
Ok(sect)
}
fn parse_cols_attrs(e: &BytesStart) -> Result<CT_Columns> {
let mut cols = CT_Columns::default();
for attr in e.attributes() {
let attr = attr?;
let key = attr.key.as_ref();
let val_str = std::str::from_utf8(&attr.value)?;
if matches_local_name(key, b"num") {
cols.num = Some(val_str.parse()?);
} else if matches_local_name(key, b"space") {
cols.space = Some(Twips(val_str.parse()?));
} else if matches_local_name(key, b"equalWidth") {
cols.equal_width = Some(val_str == "1" || val_str == "true");
} else if matches_local_name(key, b"sep") {
cols.sep = Some(val_str == "1" || val_str == "true");
}
}
Ok(cols)
}
fn parse_cols_empty(e: &BytesStart) -> Result<CT_Columns> {
Self::parse_cols_attrs(e)
}
fn parse_cols_start(reader: &mut Reader<&[u8]>, e: &BytesStart) -> Result<CT_Columns> {
let mut cols = Self::parse_cols_attrs(e)?;
let mut buf = Vec::new();
loop {
match reader.read_event_into(&mut buf) {
Ok(Event::Empty(ref e)) if matches_local_name(e.name().as_ref(), b"col") => {
let mut width = None;
let mut space = None;
for attr in e.attributes() {
let attr = attr?;
let key = attr.key.as_ref();
let val: i32 = std::str::from_utf8(&attr.value)?.parse()?;
if matches_local_name(key, b"w") {
width = Some(Twips(val));
} else if matches_local_name(key, b"space") {
space = Some(Twips(val));
}
}
cols.columns.push(CT_Column { width, space });
}
Ok(Event::End(ref e)) if matches_local_name(e.name().as_ref(), b"cols") => {
break;
}
Ok(Event::Eof) => break,
Err(e) => return Err(e.into()),
_ => {}
}
buf.clear();
}
Ok(cols)
}
pub fn to_xml<W: std::io::Write>(&self, writer: &mut Writer<W>) -> Result<()> {
let mut buf = itoa::Buffer::new();
writer.write_event(Event::Start(BytesStart::new("w:sectPr")))?;
for hdr in &self.header_refs {
let mut e = BytesStart::new("w:headerReference");
e.push_attribute(("w:type", hdr.hdr_ftr_type.to_str()));
e.push_attribute(("r:id", hdr.rel_id.as_str()));
writer.write_event(Event::Empty(e))?;
}
for ftr in &self.footer_refs {
let mut e = BytesStart::new("w:footerReference");
e.push_attribute(("w:type", ftr.hdr_ftr_type.to_str()));
e.push_attribute(("r:id", ftr.rel_id.as_str()));
writer.write_event(Event::Empty(e))?;
}
if let Some(st) = self.section_type {
let mut e = BytesStart::new("w:type");
e.push_attribute(("w:val", st.to_str()));
writer.write_event(Event::Empty(e))?;
}
if self.page_width.is_some() || self.page_height.is_some() || self.orientation.is_some() {
let mut e = BytesStart::new("w:pgSz");
if let Some(w) = self.page_width {
e.push_attribute(("w:w", buf.format(w.0)));
}
if let Some(h) = self.page_height {
e.push_attribute(("w:h", buf.format(h.0)));
}
if let Some(orient) = self.orientation
&& orient == ST_PageOrientation::Landscape
{
e.push_attribute(("w:orient", orient.to_str()));
}
writer.write_event(Event::Empty(e))?;
}
if self.margin_top.is_some()
|| self.margin_right.is_some()
|| self.margin_bottom.is_some()
|| self.margin_left.is_some()
{
let mut e = BytesStart::new("w:pgMar");
if let Some(t) = self.margin_top {
e.push_attribute(("w:top", buf.format(t.0)));
}
if let Some(r) = self.margin_right {
e.push_attribute(("w:right", buf.format(r.0)));
}
if let Some(b) = self.margin_bottom {
e.push_attribute(("w:bottom", buf.format(b.0)));
}
if let Some(l) = self.margin_left {
e.push_attribute(("w:left", buf.format(l.0)));
}
if let Some(g) = self.gutter {
e.push_attribute(("w:gutter", buf.format(g.0)));
}
if let Some(h) = self.header_distance {
e.push_attribute(("w:header", buf.format(h.0)));
}
if let Some(f) = self.footer_distance {
e.push_attribute(("w:footer", buf.format(f.0)));
}
writer.write_event(Event::Empty(e))?;
}
if let Some(ref cols) = self.columns {
if cols.columns.is_empty() {
let mut e = BytesStart::new("w:cols");
if let Some(num) = cols.num {
e.push_attribute(("w:num", buf.format(num)));
}
if let Some(space) = cols.space {
e.push_attribute(("w:space", buf.format(space.0)));
}
if let Some(eq) = cols.equal_width
&& !eq
{
e.push_attribute(("w:equalWidth", "0"));
}
if let Some(sep) = cols.sep
&& sep
{
e.push_attribute(("w:sep", "1"));
}
writer.write_event(Event::Empty(e))?;
} else {
let mut e = BytesStart::new("w:cols");
if let Some(num) = cols.num {
e.push_attribute(("w:num", buf.format(num)));
}
if let Some(eq) = cols.equal_width {
e.push_attribute(("w:equalWidth", if eq { "1" } else { "0" }));
}
if let Some(sep) = cols.sep
&& sep
{
e.push_attribute(("w:sep", "1"));
}
writer.write_event(Event::Start(e))?;
for col in &cols.columns {
let mut ce = BytesStart::new("w:col");
if let Some(w) = col.width {
ce.push_attribute(("w:w", buf.format(w.0)));
}
if let Some(s) = col.space {
ce.push_attribute(("w:space", buf.format(s.0)));
}
writer.write_event(Event::Empty(ce))?;
}
writer.write_event(Event::End(BytesEnd::new("w:cols")))?;
}
}
if let Some(true) = self.title_pg {
writer.write_event(Event::Empty(BytesStart::new("w:titlePg")))?;
}
for raw in &self.extra_xml {
writer.get_mut().write_all(raw)?;
}
writer.write_event(Event::End(BytesEnd::new("w:sectPr")))?;
Ok(())
}
}
#[derive(Debug, Clone, PartialEq)]
#[allow(non_snake_case)]
pub struct CT_Body {
pub content: Vec<BodyContent>,
pub sect_pr: Option<CT_SectPr>,
}
#[allow(non_snake_case)]
impl CT_Body {
pub fn new() -> Self {
CT_Body {
content: Vec::new(),
sect_pr: Some(CT_SectPr::default_letter()),
}
}
pub fn paragraphs(&self) -> impl Iterator<Item = &CT_P> {
self.content.iter().filter_map(|c| match c {
BodyContent::Paragraph(p) => Some(p),
_ => None,
})
}
pub fn paragraphs_mut(&mut self) -> impl Iterator<Item = &mut CT_P> {
self.content.iter_mut().filter_map(|c| match c {
BodyContent::Paragraph(p) => Some(p),
_ => None,
})
}
pub fn tables(&self) -> impl Iterator<Item = &CT_Tbl> {
self.content.iter().filter_map(|c| match c {
BodyContent::Table(t) => Some(t),
_ => None,
})
}
pub fn tables_mut(&mut self) -> impl Iterator<Item = &mut CT_Tbl> {
self.content.iter_mut().filter_map(|c| match c {
BodyContent::Table(t) => Some(t),
_ => None,
})
}
pub fn add_paragraph(&mut self, p: CT_P) {
self.content.push(BodyContent::Paragraph(p));
}
pub fn add_table(&mut self, tbl: CT_Tbl) {
self.content.push(BodyContent::Table(tbl));
}
pub fn content_count(&self) -> usize {
self.content.len()
}
pub fn insert_paragraph(&mut self, index: usize, p: CT_P) {
self.content.insert(index, BodyContent::Paragraph(p));
}
pub fn insert_table(&mut self, index: usize, tbl: CT_Tbl) {
self.content.insert(index, BodyContent::Table(tbl));
}
pub fn find_paragraph_index(&self, text: &str) -> Option<usize> {
self.content.iter().position(|c| match c {
BodyContent::Paragraph(p) => p.text().contains(text),
_ => false,
})
}
pub fn remove(&mut self, index: usize) -> Option<BodyContent> {
if index < self.content.len() {
Some(self.content.remove(index))
} else {
None
}
}
pub fn get(&self, index: usize) -> Option<&BodyContent> {
self.content.get(index)
}
pub fn get_mut(&mut self, index: usize) -> Option<&mut BodyContent> {
self.content.get_mut(index)
}
pub fn from_xml(reader: &mut Reader<&[u8]>) -> Result<Self> {
Self::from_xml_with_prefixes(reader, &["w".to_string()])
}
fn from_xml_with_prefixes(
reader: &mut Reader<&[u8]>,
word_prefixes: &[String],
) -> Result<Self> {
let mut content = Vec::new();
let mut sect_pr = None;
let mut buf = Vec::new();
loop {
match reader.read_event_into(&mut buf) {
Ok(Event::Start(ref e)) => {
let name = e.name();
let prefixes = word_prefixes_at(e, word_prefixes)?;
if is_word_element(name.as_ref(), b"p", &prefixes) {
content.push(BodyContent::Paragraph(CT_P::from_xml_with_prefixes(
reader, &prefixes,
)?));
} else if is_word_element(name.as_ref(), b"tbl", &prefixes) {
content.push(BodyContent::Table(CT_Tbl::from_xml_with_prefixes(
reader, &prefixes,
)?));
} else if matches_local_name(name.as_ref(), b"sectPr") {
sect_pr = Some(CT_SectPr::from_xml(reader)?);
} else {
content.push(BodyContent::RawXml(capture_element(reader, e)?));
}
}
Ok(Event::Empty(ref e)) => {
let name = e.name();
if !matches_local_name(name.as_ref(), b"body") {
content.push(BodyContent::RawXml(capture_empty_element(e)?));
}
}
Ok(Event::End(ref e)) if matches_local_name(e.name().as_ref(), b"body") => {
break;
}
Ok(Event::Eof) => break,
Err(e) => return Err(e.into()),
_ => {}
}
buf.clear();
}
Ok(CT_Body { content, sect_pr })
}
pub fn to_xml<W: std::io::Write>(&self, writer: &mut Writer<W>) -> Result<()> {
writer.write_event(Event::Start(BytesStart::new("w:body")))?;
for item in &self.content {
match item {
BodyContent::Paragraph(p) => p.to_xml(writer)?,
BodyContent::Table(t) => t.to_xml(writer)?,
BodyContent::RawXml(raw) => {
writer.get_mut().write_all(raw)?;
}
}
}
if let Some(ref sect) = self.sect_pr {
sect.to_xml(writer)?;
}
writer.write_event(Event::End(BytesEnd::new("w:body")))?;
Ok(())
}
}
impl Default for CT_Body {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, PartialEq)]
#[allow(non_snake_case)]
pub struct CT_Document {
pub body: CT_Body,
pub extra_namespaces: Vec<(String, String)>,
pub background_xml: Option<Vec<u8>>,
}
#[allow(non_snake_case)]
impl CT_Document {
pub fn new() -> Self {
CT_Document {
body: CT_Body::new(),
extra_namespaces: Vec::new(),
background_xml: None,
}
}
pub fn from_xml(xml: &[u8]) -> Result<Self> {
let mut reader = Reader::from_reader(xml);
reader.config_mut().trim_text(true);
let mut body = None;
let mut extra_namespaces = Vec::new();
let mut background_xml = None;
let mut buf = Vec::new();
let mut word_prefixes = Vec::new();
let known_ns: &[&[u8]] = &[b"xmlns:w", b"xmlns:r", b"xmlns:mc", b"xmlns"];
loop {
match reader.read_event_into(&mut buf) {
Ok(Event::Start(ref e)) => {
let name = e.name();
let prefixes = word_prefixes_at(e, &word_prefixes)?;
if matches_local_name(name.as_ref(), b"body") {
body = Some(CT_Body::from_xml_with_prefixes(&mut reader, &prefixes)?);
} else if matches_local_name(name.as_ref(), b"document") {
for attr in e.attributes().flatten() {
let key = attr.key.as_ref();
if (key.starts_with(b"xmlns:") || key == b"xmlns")
&& !known_ns.contains(&key)
{
let key_str = std::str::from_utf8(key).unwrap_or("").to_string();
let val_str =
std::str::from_utf8(&attr.value).unwrap_or("").to_string();
extra_namespaces.push((key_str, val_str));
}
}
word_prefixes = prefixes;
} else if matches_local_name(name.as_ref(), b"background") {
background_xml = Some(capture_element(&mut reader, e)?);
} else {
reader.read_to_end_into(name, &mut Vec::new())?;
}
}
Ok(Event::Empty(ref e)) => {
if matches_local_name(e.name().as_ref(), b"background") {
background_xml = Some(capture_empty_element(e)?);
}
}
Ok(Event::Eof) => break,
Err(e) => return Err(e.into()),
_ => {}
}
buf.clear();
}
Ok(CT_Document {
body: body.unwrap_or_default(),
extra_namespaces,
background_xml,
})
}
pub fn to_xml(&self) -> Result<Vec<u8>> {
let mut writer = Writer::new_with_indent(Vec::new(), b' ', 2);
writer.write_event(Event::Decl(BytesDecl::new(
"1.0",
Some("UTF-8"),
Some("yes"),
)))?;
let mut doc_start = BytesStart::new("w:document");
doc_start.push_attribute(("xmlns:w", W_NS));
doc_start.push_attribute((
"xmlns:r",
"http://schemas.openxmlformats.org/officeDocument/2006/relationships",
));
doc_start.push_attribute((
"xmlns:mc",
"http://schemas.openxmlformats.org/markup-compatibility/2006",
));
let wp_ns = "http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing";
let mut has_wp = false;
for (key, _) in &self.extra_namespaces {
if key == "xmlns:wp" {
has_wp = true;
break;
}
}
if !has_wp {
doc_start.push_attribute(("xmlns:wp", wp_ns));
}
for (key, val) in &self.extra_namespaces {
doc_start.push_attribute((key.as_str(), val.as_str()));
}
writer.write_event(Event::Start(doc_start))?;
if let Some(ref bg) = self.background_xml {
writer.get_mut().extend_from_slice(bg);
}
self.body.to_xml(&mut writer)?;
writer.write_event(Event::End(BytesEnd::new("w:document")))?;
Ok(writer.into_inner())
}
}
impl Default for CT_Document {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn round_trip_document() {
let mut doc = CT_Document::new();
let mut p = CT_P::new();
p.add_run("Hello World");
doc.body.add_paragraph(p);
let xml = doc.to_xml().unwrap();
let parsed = CT_Document::from_xml(&xml).unwrap();
let paras: Vec<_> = parsed.body.paragraphs().collect();
assert_eq!(paras.len(), 1);
assert_eq!(paras[0].text(), "Hello World");
}
#[test]
fn default_namespace_document_paragraph_properties_parse_in_scope() {
let xml = format!(
r#"<document xmlns="{W_NS}" xmlns:q="{W_NS}" xmlns:ext="urn:producer"><body><p><pPr><ext:jc ext:val="right"/><jc q:val="center"/></pPr><r><t>Scoped</t></r></p></body></document>"#
);
let parsed = CT_Document::from_xml(xml.as_bytes()).unwrap();
let paragraph = parsed.body.paragraphs().next().unwrap();
assert_eq!(paragraph.text(), "Scoped");
assert_eq!(
paragraph.properties.as_ref().unwrap().jc,
Some(crate::shared::ST_Jc::Center)
);
}
#[test]
fn round_trip_with_section() {
let doc = CT_Document::new();
let xml = doc.to_xml().unwrap();
let parsed = CT_Document::from_xml(&xml).unwrap();
assert!(parsed.body.sect_pr.is_some());
let sect = parsed.body.sect_pr.unwrap();
assert_eq!(sect.page_width, Some(Twips(12240)));
}
#[test]
fn round_trip_landscape() {
let mut doc = CT_Document::new();
let sect = doc.body.sect_pr.as_mut().unwrap();
sect.orientation = Some(ST_PageOrientation::Landscape);
sect.page_width = Some(Twips(15840)); sect.page_height = Some(Twips(12240));
let xml = doc.to_xml().unwrap();
let parsed = CT_Document::from_xml(&xml).unwrap();
let sect = parsed.body.sect_pr.unwrap();
assert_eq!(sect.orientation, Some(ST_PageOrientation::Landscape));
assert_eq!(sect.page_width, Some(Twips(15840)));
}
#[test]
fn round_trip_columns() {
let mut doc = CT_Document::new();
let sect = doc.body.sect_pr.as_mut().unwrap();
sect.columns = Some(CT_Columns {
num: Some(2),
space: Some(Twips(720)),
equal_width: Some(true),
sep: Some(true),
columns: Vec::new(),
});
let xml = doc.to_xml().unwrap();
let parsed = CT_Document::from_xml(&xml).unwrap();
let cols = parsed.body.sect_pr.unwrap().columns.unwrap();
assert_eq!(cols.num, Some(2));
assert_eq!(cols.space, Some(Twips(720)));
assert_eq!(cols.sep, Some(true));
}
#[test]
fn round_trip_section_type() {
let mut doc = CT_Document::new();
let sect = doc.body.sect_pr.as_mut().unwrap();
sect.section_type = Some(ST_SectionType::Continuous);
sect.title_pg = Some(true);
let xml = doc.to_xml().unwrap();
let parsed = CT_Document::from_xml(&xml).unwrap();
let sect = parsed.body.sect_pr.unwrap();
assert_eq!(sect.section_type, Some(ST_SectionType::Continuous));
assert_eq!(sect.title_pg, Some(true));
}
#[test]
fn insert_paragraph_at_beginning() {
let mut body = CT_Body::new();
let mut p1 = CT_P::new();
p1.add_run("First");
body.add_paragraph(p1);
let mut p0 = CT_P::new();
p0.add_run("Inserted");
body.insert_paragraph(0, p0);
assert_eq!(body.content_count(), 2);
match &body.content[0] {
BodyContent::Paragraph(p) => assert_eq!(p.text(), "Inserted"),
_ => panic!("expected paragraph"),
}
match &body.content[1] {
BodyContent::Paragraph(p) => assert_eq!(p.text(), "First"),
_ => panic!("expected paragraph"),
}
}
#[test]
fn insert_paragraph_in_middle() {
let mut body = CT_Body::new();
let mut p1 = CT_P::new();
p1.add_run("First");
body.add_paragraph(p1);
let mut p2 = CT_P::new();
p2.add_run("Third");
body.add_paragraph(p2);
let mut mid = CT_P::new();
mid.add_run("Middle");
body.insert_paragraph(1, mid);
assert_eq!(body.content_count(), 3);
let texts: Vec<_> = body.paragraphs().map(|p| p.text()).collect();
assert_eq!(texts, vec!["First", "Middle", "Third"]);
}
#[test]
fn find_paragraph_index_match() {
let mut body = CT_Body::new();
let mut p1 = CT_P::new();
p1.add_run("Hello World");
body.add_paragraph(p1);
let mut p2 = CT_P::new();
p2.add_run("INSERT_HERE");
body.add_paragraph(p2);
assert_eq!(body.find_paragraph_index("INSERT_HERE"), Some(1));
assert_eq!(body.find_paragraph_index("NONEXISTENT"), None);
}
#[test]
fn remove_content() {
let mut body = CT_Body::new();
let mut p1 = CT_P::new();
p1.add_run("First");
body.add_paragraph(p1);
let mut p2 = CT_P::new();
p2.add_run("Second");
body.add_paragraph(p2);
let removed = body.remove(0);
assert!(removed.is_some());
assert_eq!(body.content_count(), 1);
match &body.content[0] {
BodyContent::Paragraph(p) => assert_eq!(p.text(), "Second"),
_ => panic!("expected paragraph"),
}
assert!(body.remove(5).is_none());
}
#[test]
fn get_and_get_mut() {
let mut body = CT_Body::new();
let mut p = CT_P::new();
p.add_run("Test");
body.add_paragraph(p);
assert!(body.get(0).is_some());
assert!(body.get(1).is_none());
if let Some(BodyContent::Paragraph(p)) = body.get_mut(0) {
p.add_run(" Modified");
}
match body.get(0).unwrap() {
BodyContent::Paragraph(p) => assert_eq!(p.text(), "Test Modified"),
_ => panic!("expected paragraph"),
}
}
#[test]
fn sect_pr_section_type_and_orientation_round_trip() {
let mut doc = CT_Document::new();
let sect = doc.body.sect_pr.as_mut().unwrap();
sect.section_type = Some(ST_SectionType::NextPage);
sect.orientation = Some(ST_PageOrientation::Landscape);
sect.page_width = Some(Twips(15840));
sect.page_height = Some(Twips(12240));
let xml = doc.to_xml().unwrap();
let parsed = CT_Document::from_xml(&xml).unwrap();
let sect2 = parsed.body.sect_pr.unwrap();
assert_eq!(sect2.section_type, Some(ST_SectionType::NextPage));
assert_eq!(sect2.orientation, Some(ST_PageOrientation::Landscape));
assert_eq!(sect2.page_width, Some(Twips(15840)));
assert_eq!(sect2.page_height, Some(Twips(12240)));
}
#[test]
fn sect_pr_all_section_types() {
for section_type in [
ST_SectionType::NextPage,
ST_SectionType::Continuous,
ST_SectionType::EvenPage,
ST_SectionType::OddPage,
] {
let mut doc = CT_Document::new();
let sect = doc.body.sect_pr.as_mut().unwrap();
sect.section_type = Some(section_type);
let xml = doc.to_xml().unwrap();
let parsed = CT_Document::from_xml(&xml).unwrap();
let sect2 = parsed.body.sect_pr.unwrap();
assert_eq!(
sect2.section_type,
Some(section_type),
"section type round-trip failed for {section_type:?}"
);
}
}
#[test]
fn sect_pr_in_paragraph_ppr_round_trip() {
let mut doc = CT_Document::new();
let mut p = CT_P::new();
p.add_run("Section break paragraph");
let mut ppr = crate::properties::CT_PPr::default();
let mut sect = CT_SectPr::default_letter();
sect.section_type = Some(ST_SectionType::NextPage);
sect.orientation = Some(ST_PageOrientation::Landscape);
sect.page_width = Some(Twips(15840));
sect.page_height = Some(Twips(12240));
ppr.sect_pr = Some(sect);
p.properties = Some(ppr);
doc.body.add_paragraph(p);
let xml = doc.to_xml().unwrap();
let parsed = CT_Document::from_xml(&xml).unwrap();
let paras: Vec<_> = parsed.body.paragraphs().collect();
assert_eq!(paras.len(), 1);
let ppr2 = paras[0].properties.as_ref().unwrap();
let sect2 = ppr2.sect_pr.as_ref().unwrap();
assert_eq!(sect2.section_type, Some(ST_SectionType::NextPage));
assert_eq!(sect2.orientation, Some(ST_PageOrientation::Landscape));
}
}