use quick_xml::events::{BytesEnd, BytesStart, Event};
use quick_xml::{Reader, Writer};
use crate::borders::CT_BorderEdge;
use crate::error::Result;
use crate::namespace::matches_local_name;
use crate::numbering::word_prefixes_at;
use crate::properties::{CT_Shd, get_val_attr, is_word_element};
use crate::raw_xml::{capture_element, capture_empty_element};
#[cfg(test)]
use crate::shared::ST_Border;
use crate::shared::ST_Jc;
use crate::text::CT_P;
use crate::units::Twips;
fn write_extras_at<W: std::io::Write>(
writer: &mut Writer<W>,
extra_xml: &[(usize, Vec<u8>)],
pos: usize,
) -> Result<()> {
for (at, raw) in extra_xml {
if *at == pos {
writer.get_mut().write_all(raw)?;
}
}
Ok(())
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct CT_TblBorders {
pub top: Option<CT_BorderEdge>,
pub bottom: Option<CT_BorderEdge>,
pub left: Option<CT_BorderEdge>,
pub right: Option<CT_BorderEdge>,
pub inside_h: Option<CT_BorderEdge>,
pub inside_v: Option<CT_BorderEdge>,
}
impl CT_TblBorders {
pub fn from_xml(reader: &mut Reader<&[u8]>) -> Result<Self> {
let mut borders = CT_TblBorders::default();
let mut buf = Vec::new();
loop {
match reader.read_event_into(&mut buf) {
Ok(Event::Empty(ref e)) => {
let name = e.name();
let edge = CT_BorderEdge::from_xml_attrs(e)?;
if matches_local_name(name.as_ref(), b"top") {
borders.top = Some(edge);
} else if matches_local_name(name.as_ref(), b"bottom") {
borders.bottom = Some(edge);
} else if matches_local_name(name.as_ref(), b"left")
|| matches_local_name(name.as_ref(), b"start")
{
borders.left = Some(edge);
} else if matches_local_name(name.as_ref(), b"right")
|| matches_local_name(name.as_ref(), b"end")
{
borders.right = Some(edge);
} else if matches_local_name(name.as_ref(), b"insideH") {
borders.inside_h = Some(edge);
} else if matches_local_name(name.as_ref(), b"insideV") {
borders.inside_v = Some(edge);
}
}
Ok(Event::End(ref e))
if matches_local_name(e.name().as_ref(), b"tblBorders")
|| matches_local_name(e.name().as_ref(), b"tcBorders") =>
{
break;
}
Ok(Event::Eof) => break,
Err(e) => return Err(e.into()),
_ => {}
}
buf.clear();
}
Ok(borders)
}
pub fn to_xml<W: std::io::Write>(&self, writer: &mut Writer<W>, tag: &str) -> Result<()> {
writer.write_event(Event::Start(BytesStart::new(tag)))?;
if let Some(ref e) = self.top {
e.to_xml(writer, "w:top")?;
}
if let Some(ref e) = self.left {
e.to_xml(writer, "w:left")?;
}
if let Some(ref e) = self.bottom {
e.to_xml(writer, "w:bottom")?;
}
if let Some(ref e) = self.right {
e.to_xml(writer, "w:right")?;
}
if let Some(ref e) = self.inside_h {
e.to_xml(writer, "w:insideH")?;
}
if let Some(ref e) = self.inside_v {
e.to_xml(writer, "w:insideV")?;
}
writer.write_event(Event::End(BytesEnd::new(tag)))?;
Ok(())
}
pub fn is_empty(&self) -> bool {
self.top.is_none()
&& self.bottom.is_none()
&& self.left.is_none()
&& self.right.is_none()
&& self.inside_h.is_none()
&& self.inside_v.is_none()
}
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct CT_TblCellMar {
pub top: Option<Twips>,
pub bottom: Option<Twips>,
pub left: Option<Twips>,
pub right: Option<Twips>,
}
impl CT_TblCellMar {
fn parse_edge(e: &BytesStart) -> Result<Option<Twips>> {
for attr in e.attributes() {
let attr = attr?;
if matches_local_name(attr.key.as_ref(), b"w") {
let val: i32 = std::str::from_utf8(&attr.value)?.parse()?;
return Ok(Some(Twips(val)));
}
}
Ok(None)
}
pub fn from_xml(reader: &mut Reader<&[u8]>) -> Result<Self> {
let mut mar = CT_TblCellMar::default();
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"top") {
mar.top = Self::parse_edge(e)?;
} else if matches_local_name(name.as_ref(), b"bottom") {
mar.bottom = Self::parse_edge(e)?;
} else if matches_local_name(name.as_ref(), b"left")
|| matches_local_name(name.as_ref(), b"start")
{
mar.left = Self::parse_edge(e)?;
} else if matches_local_name(name.as_ref(), b"right")
|| matches_local_name(name.as_ref(), b"end")
{
mar.right = Self::parse_edge(e)?;
}
}
Ok(Event::End(ref e)) if matches_local_name(e.name().as_ref(), b"tblCellMar") => {
break;
}
Ok(Event::Eof) => break,
Err(e) => return Err(e.into()),
_ => {}
}
buf.clear();
}
Ok(mar)
}
pub fn to_xml<W: std::io::Write>(&self, writer: &mut Writer<W>) -> Result<()> {
writer.write_event(Event::Start(BytesStart::new("w:tblCellMar")))?;
fn write_edge<W: std::io::Write>(
writer: &mut Writer<W>,
tag: &str,
val: Twips,
) -> Result<()> {
let mut buf = itoa::Buffer::new();
let mut e = BytesStart::new(tag);
e.push_attribute(("w:w", buf.format(val.0)));
e.push_attribute(("w:type", "dxa"));
writer.write_event(Event::Empty(e))?;
Ok(())
}
if let Some(t) = self.top {
write_edge(writer, "w:top", t)?;
}
if let Some(l) = self.left {
write_edge(writer, "w:left", l)?;
}
if let Some(b) = self.bottom {
write_edge(writer, "w:bottom", b)?;
}
if let Some(r) = self.right {
write_edge(writer, "w:right", r)?;
}
writer.write_event(Event::End(BytesEnd::new("w:tblCellMar")))?;
Ok(())
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct CT_TblWidth {
pub w: i32,
pub width_type: String,
}
impl CT_TblWidth {
pub fn dxa(twips: i32) -> Self {
CT_TblWidth {
w: twips,
width_type: "dxa".to_string(),
}
}
pub fn pct(fiftieths: i32) -> Self {
CT_TblWidth {
w: fiftieths,
width_type: "pct".to_string(),
}
}
pub fn auto() -> Self {
CT_TblWidth {
w: 0,
width_type: "auto".to_string(),
}
}
pub fn from_xml_attrs(e: &BytesStart) -> Result<Self> {
let mut w = 0;
let mut width_type = "dxa".to_string();
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"w") {
w = val.parse().unwrap_or(0);
} else if matches_local_name(key, b"type") {
width_type = val.to_string();
}
}
Ok(CT_TblWidth { w, width_type })
}
pub fn write_xml<W: std::io::Write>(&self, writer: &mut Writer<W>, tag: &str) -> Result<()> {
let mut buf = itoa::Buffer::new();
let mut e = BytesStart::new(tag);
e.push_attribute(("w:w", buf.format(self.w)));
e.push_attribute(("w:type", self.width_type.as_str()));
writer.write_event(Event::Empty(e))?;
Ok(())
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct CT_TblGridCol {
pub width: Twips,
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct CT_TblPr {
pub style_id: Option<String>,
pub width: Option<CT_TblWidth>,
pub jc: Option<ST_Jc>,
pub borders: Option<CT_TblBorders>,
pub cell_margin: Option<CT_TblCellMar>,
pub layout: Option<String>,
pub indent: Option<CT_TblWidth>,
pub shading: Option<CT_Shd>,
pub look: Option<CT_TblLook>,
}
#[derive(Debug, Clone, PartialEq, Default)]
#[allow(non_snake_case)]
pub struct CT_TblLook {
pub val: Option<String>,
pub first_row: Option<bool>,
pub last_row: Option<bool>,
pub first_column: Option<bool>,
pub last_column: Option<bool>,
pub no_h_band: Option<bool>,
pub no_v_band: Option<bool>,
}
fn parse_ooxml_bool(value: &str) -> Option<bool> {
match value {
"1" | "true" | "on" => Some(true),
"0" | "false" | "off" => Some(false),
_ => None,
}
}
fn ooxml_bool_str(value: bool) -> &'static str {
if value { "1" } else { "0" }
}
#[allow(non_snake_case)]
impl CT_TblLook {
pub fn from_xml_attrs(e: &BytesStart) -> Result<Self> {
let mut look = CT_TblLook::default();
for attr in e.attributes().flatten() {
let value = std::str::from_utf8(&attr.value)?;
let key = attr.key.as_ref();
if matches_local_name(key, b"val") {
look.val = Some(value.to_string());
} else if matches_local_name(key, b"firstRow") {
look.first_row = parse_ooxml_bool(value);
} else if matches_local_name(key, b"lastRow") {
look.last_row = parse_ooxml_bool(value);
} else if matches_local_name(key, b"firstColumn") {
look.first_column = parse_ooxml_bool(value);
} else if matches_local_name(key, b"lastColumn") {
look.last_column = parse_ooxml_bool(value);
} else if matches_local_name(key, b"noHBand") {
look.no_h_band = parse_ooxml_bool(value);
} else if matches_local_name(key, b"noVBand") {
look.no_v_band = parse_ooxml_bool(value);
}
}
Ok(look)
}
pub fn to_xml<W: std::io::Write>(&self, writer: &mut Writer<W>) -> Result<()> {
let mut e = BytesStart::new("w:tblLook");
if let Some(ref val) = self.val {
e.push_attribute(("w:val", val.as_str()));
}
for (name, value) in [
("w:firstRow", self.first_row),
("w:lastRow", self.last_row),
("w:firstColumn", self.first_column),
("w:lastColumn", self.last_column),
("w:noHBand", self.no_h_band),
("w:noVBand", self.no_v_band),
] {
if let Some(value) = value {
e.push_attribute((name, ooxml_bool_str(value)));
}
}
writer.write_event(Event::Empty(e))?;
Ok(())
}
}
#[allow(non_snake_case)]
impl CT_TblPr {
pub fn from_xml(reader: &mut Reader<&[u8]>) -> Result<Self> {
let mut pr = CT_TblPr::default();
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"tblStyle") {
pr.style_id = get_val_attr(e)?;
} else if matches_local_name(name.as_ref(), b"tblW") {
pr.width = Some(CT_TblWidth::from_xml_attrs(e)?);
} else if matches_local_name(name.as_ref(), b"jc") {
if let Some(val) = get_val_attr(e)? {
pr.jc = Some(ST_Jc::from_str(&val)?);
}
} else if matches_local_name(name.as_ref(), b"tblLayout") {
if let Some(val) = get_val_attr(e)? {
pr.layout = Some(val);
}
} else if matches_local_name(name.as_ref(), b"tblInd") {
pr.indent = Some(CT_TblWidth::from_xml_attrs(e)?);
} else if matches_local_name(name.as_ref(), b"shd") {
pr.shading = Some(CT_Shd::from_xml_attrs(e)?);
} else if matches_local_name(name.as_ref(), b"tblLook") {
pr.look = Some(CT_TblLook::from_xml_attrs(e)?);
}
}
Ok(Event::Start(ref e)) => {
let name = e.name();
if matches_local_name(name.as_ref(), b"tblBorders") {
pr.borders = Some(CT_TblBorders::from_xml(reader)?);
} else if matches_local_name(name.as_ref(), b"tblCellMar") {
pr.cell_margin = Some(CT_TblCellMar::from_xml(reader)?);
} else {
reader.read_to_end_into(name, &mut Vec::new())?;
}
}
Ok(Event::End(ref e)) if matches_local_name(e.name().as_ref(), b"tblPr") => {
break;
}
Ok(Event::Eof) => break,
Err(e) => return Err(e.into()),
_ => {}
}
buf.clear();
}
Ok(pr)
}
pub fn to_xml<W: std::io::Write>(&self, writer: &mut Writer<W>) -> Result<()> {
writer.write_event(Event::Start(BytesStart::new("w:tblPr")))?;
if let Some(ref style_id) = self.style_id {
let mut e = BytesStart::new("w:tblStyle");
e.push_attribute(("w:val", style_id.as_str()));
writer.write_event(Event::Empty(e))?;
}
if let Some(ref width) = self.width {
width.write_xml(writer, "w:tblW")?;
}
if let Some(jc) = self.jc {
let mut e = BytesStart::new("w:jc");
e.push_attribute(("w:val", jc.to_str()));
writer.write_event(Event::Empty(e))?;
}
if let Some(ref indent) = self.indent {
indent.write_xml(writer, "w:tblInd")?;
}
if let Some(ref borders) = self.borders
&& !borders.is_empty()
{
borders.to_xml(writer, "w:tblBorders")?;
}
if let Some(ref shd) = self.shading {
shd.write_xml(writer, "w:shd")?;
}
if let Some(ref layout) = self.layout {
let mut e = BytesStart::new("w:tblLayout");
e.push_attribute(("w:type", layout.as_str()));
writer.write_event(Event::Empty(e))?;
}
if let Some(ref cell_margin) = self.cell_margin {
cell_margin.to_xml(writer)?;
}
if let Some(ref look) = self.look {
look.to_xml(writer)?;
}
writer.write_event(Event::End(BytesEnd::new("w:tblPr")))?;
Ok(())
}
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct CT_TblGrid {
pub columns: Vec<CT_TblGridCol>,
}
#[allow(non_snake_case)]
impl CT_TblGrid {
pub fn from_xml(reader: &mut Reader<&[u8]>) -> Result<Self> {
let mut columns = Vec::new();
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"gridCol") {
let mut width = Twips(0);
for attr in e.attributes() {
let attr = attr?;
if matches_local_name(attr.key.as_ref(), b"w") {
width = Twips(std::str::from_utf8(&attr.value)?.parse()?);
}
}
columns.push(CT_TblGridCol { width });
}
}
Ok(Event::End(ref e)) if matches_local_name(e.name().as_ref(), b"tblGrid") => {
break;
}
Ok(Event::Eof) => break,
Err(e) => return Err(e.into()),
_ => {}
}
buf.clear();
}
Ok(CT_TblGrid { columns })
}
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:tblGrid")))?;
for col in &self.columns {
let mut e = BytesStart::new("w:gridCol");
e.push_attribute(("w:w", buf.format(col.width.0)));
writer.write_event(Event::Empty(e))?;
}
writer.write_event(Event::End(BytesEnd::new("w:tblGrid")))?;
Ok(())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum VMerge {
Restart,
Continue,
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct CT_TrPr {
pub height: Option<Twips>,
pub height_rule: Option<String>,
pub header: Option<bool>,
pub jc: Option<ST_Jc>,
pub cant_split: Option<bool>,
pub cnf_style: Option<String>,
}
#[allow(non_snake_case)]
impl CT_TrPr {
pub fn from_xml(reader: &mut Reader<&[u8]>) -> Result<Self> {
let mut pr = CT_TrPr::default();
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"trHeight") {
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"val") {
pr.height = Some(Twips(val.parse()?));
} else if matches_local_name(key, b"hRule") {
pr.height_rule = Some(val.to_string());
}
}
} else if matches_local_name(name.as_ref(), b"tblHeader") {
pr.header = Some(true);
} else if matches_local_name(name.as_ref(), b"jc") {
if let Some(val) = get_val_attr(e)? {
pr.jc = Some(ST_Jc::from_str(&val)?);
}
} else if matches_local_name(name.as_ref(), b"cnfStyle") {
pr.cnf_style = get_val_attr(e)?;
} else if matches_local_name(name.as_ref(), b"cantSplit") {
pr.cant_split = Some(true);
}
}
Ok(Event::End(ref e)) if matches_local_name(e.name().as_ref(), b"trPr") => {
break;
}
Ok(Event::Eof) => break,
Err(e) => return Err(e.into()),
_ => {}
}
buf.clear();
}
Ok(pr)
}
pub fn to_xml<W: std::io::Write>(&self, writer: &mut Writer<W>) -> Result<()> {
if self.is_empty() {
return Ok(());
}
writer.write_event(Event::Start(BytesStart::new("w:trPr")))?;
if let Some(ref cnf) = self.cnf_style {
let mut e = BytesStart::new("w:cnfStyle");
e.push_attribute(("w:val", cnf.as_str()));
writer.write_event(Event::Empty(e))?;
}
if let Some(ref cant_split) = self.cant_split
&& *cant_split
{
writer.write_event(Event::Empty(BytesStart::new("w:cantSplit")))?;
}
if let Some(height) = self.height {
let mut buf = itoa::Buffer::new();
let mut e = BytesStart::new("w:trHeight");
e.push_attribute(("w:val", buf.format(height.0)));
if let Some(ref rule) = self.height_rule {
e.push_attribute(("w:hRule", rule.as_str()));
}
writer.write_event(Event::Empty(e))?;
}
if let Some(true) = self.header {
writer.write_event(Event::Empty(BytesStart::new("w:tblHeader")))?;
}
if let Some(jc) = self.jc {
let mut e = BytesStart::new("w:jc");
e.push_attribute(("w:val", jc.to_str()));
writer.write_event(Event::Empty(e))?;
}
writer.write_event(Event::End(BytesEnd::new("w:trPr")))?;
Ok(())
}
fn is_empty(&self) -> bool {
self.height.is_none()
&& self.header.is_none()
&& self.jc.is_none()
&& self.cant_split.is_none()
&& self.cnf_style.is_none()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ST_VerticalJc {
Top,
Center,
Bottom,
}
impl ST_VerticalJc {
pub fn from_str(s: &str) -> Self {
match s {
"center" => Self::Center,
"bottom" => Self::Bottom,
_ => Self::Top,
}
}
pub fn to_str(self) -> &'static str {
match self {
Self::Top => "top",
Self::Center => "center",
Self::Bottom => "bottom",
}
}
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct CT_TcPr {
pub width: Option<CT_TblWidth>,
pub grid_span: Option<u32>,
pub v_merge: Option<VMerge>,
pub borders: Option<CT_TblBorders>,
pub shading: Option<CT_Shd>,
pub v_align: Option<ST_VerticalJc>,
pub no_wrap: Option<bool>,
pub text_direction: Option<String>,
pub cnf_style: Option<String>,
}
#[allow(non_snake_case)]
impl CT_TcPr {
pub fn from_xml(reader: &mut Reader<&[u8]>) -> Result<Self> {
let mut pr = CT_TcPr::default();
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"tcW") {
pr.width = Some(CT_TblWidth::from_xml_attrs(e)?);
} else if matches_local_name(name.as_ref(), b"gridSpan") {
if let Some(val) = get_val_attr(e)? {
pr.grid_span = Some(val.parse()?);
}
} else if matches_local_name(name.as_ref(), b"vMerge") {
if let Some(val) = get_val_attr(e)? {
pr.v_merge = Some(if val == "restart" {
VMerge::Restart
} else {
VMerge::Continue
});
} else {
pr.v_merge = Some(VMerge::Continue);
}
} else if matches_local_name(name.as_ref(), b"vAlign") {
if let Some(val) = get_val_attr(e)? {
pr.v_align = Some(ST_VerticalJc::from_str(&val));
}
} else if matches_local_name(name.as_ref(), b"shd") {
pr.shading = Some(CT_Shd::from_xml_attrs(e)?);
} else if matches_local_name(name.as_ref(), b"cnfStyle") {
pr.cnf_style = get_val_attr(e)?;
} else if matches_local_name(name.as_ref(), b"noWrap") {
pr.no_wrap = Some(true);
} else if matches_local_name(name.as_ref(), b"textDirection")
&& let Some(val) = get_val_attr(e)?
{
pr.text_direction = Some(val);
}
}
Ok(Event::Start(ref e)) => {
let name = e.name();
if matches_local_name(name.as_ref(), b"tcBorders") {
pr.borders = Some(CT_TblBorders::from_xml(reader)?);
} else {
reader.read_to_end_into(name, &mut Vec::new())?;
}
}
Ok(Event::End(ref e)) if matches_local_name(e.name().as_ref(), b"tcPr") => {
break;
}
Ok(Event::Eof) => break,
Err(e) => return Err(e.into()),
_ => {}
}
buf.clear();
}
Ok(pr)
}
pub fn to_xml<W: std::io::Write>(&self, writer: &mut Writer<W>) -> Result<()> {
if self.is_empty() {
return Ok(());
}
writer.write_event(Event::Start(BytesStart::new("w:tcPr")))?;
if let Some(ref cnf) = self.cnf_style {
let mut e = BytesStart::new("w:cnfStyle");
e.push_attribute(("w:val", cnf.as_str()));
writer.write_event(Event::Empty(e))?;
}
if let Some(ref width) = self.width {
width.write_xml(writer, "w:tcW")?;
}
if let Some(grid_span) = self.grid_span
&& grid_span > 1
{
let mut buf = itoa::Buffer::new();
let mut e = BytesStart::new("w:gridSpan");
e.push_attribute(("w:val", buf.format(grid_span)));
writer.write_event(Event::Empty(e))?;
}
if let Some(ref vm) = self.v_merge {
let mut e = BytesStart::new("w:vMerge");
match vm {
VMerge::Restart => e.push_attribute(("w:val", "restart")),
VMerge::Continue => {} }
writer.write_event(Event::Empty(e))?;
}
if let Some(ref borders) = self.borders
&& !borders.is_empty()
{
borders.to_xml(writer, "w:tcBorders")?;
}
if let Some(ref shd) = self.shading {
shd.write_xml(writer, "w:shd")?;
}
if let Some(true) = self.no_wrap {
writer.write_event(Event::Empty(BytesStart::new("w:noWrap")))?;
}
if let Some(ref va) = self.v_align {
let mut e = BytesStart::new("w:vAlign");
e.push_attribute(("w:val", va.to_str()));
writer.write_event(Event::Empty(e))?;
}
if let Some(ref td) = self.text_direction {
let mut e = BytesStart::new("w:textDirection");
e.push_attribute(("w:val", td.as_str()));
writer.write_event(Event::Empty(e))?;
}
writer.write_event(Event::End(BytesEnd::new("w:tcPr")))?;
Ok(())
}
fn is_empty(&self) -> bool {
self.width.is_none()
&& self.grid_span.is_none()
&& self.v_merge.is_none()
&& self.borders.is_none()
&& self.shading.is_none()
&& self.v_align.is_none()
&& self.no_wrap.is_none()
&& self.text_direction.is_none()
&& self.cnf_style.is_none()
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum CellContent {
Paragraph(CT_P),
Table(CT_Tbl),
}
#[derive(Debug, Clone, PartialEq)]
pub struct CT_Tc {
pub properties: Option<CT_TcPr>,
pub content: Vec<CellContent>,
pub extra_xml: Vec<(usize, Vec<u8>)>,
}
#[allow(non_snake_case)]
impl CT_Tc {
pub fn new() -> Self {
CT_Tc {
properties: None,
content: vec![CellContent::Paragraph(CT_P::new())],
extra_xml: Vec::new(),
}
}
pub fn paragraphs(&self) -> Vec<&CT_P> {
self.content
.iter()
.filter_map(|c| match c {
CellContent::Paragraph(p) => Some(p),
CellContent::Table(_) => None,
})
.collect()
}
pub fn paragraphs_mut(&mut self) -> Vec<&mut CT_P> {
self.content
.iter_mut()
.filter_map(|c| match c {
CellContent::Paragraph(p) => Some(p),
CellContent::Table(_) => None,
})
.collect()
}
pub fn text(&self) -> String {
self.paragraphs()
.iter()
.map(|p| p.text())
.collect::<Vec<_>>()
.join("\n")
}
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 properties = None;
let mut content = Vec::new();
let mut extra_xml = Vec::new();
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 matches_local_name(name.as_ref(), b"tcPr") {
properties = Some(CT_TcPr::from_xml(reader)?);
} else if is_word_element(name.as_ref(), b"p", &prefixes) {
content.push(CellContent::Paragraph(CT_P::from_xml_with_prefixes(
reader, &prefixes,
)?));
} else if is_word_element(name.as_ref(), b"tbl", &prefixes) {
content.push(CellContent::Table(CT_Tbl::from_xml_with_prefixes(
reader, &prefixes,
)?));
} else {
extra_xml.push((content.len(), capture_element(reader, e)?));
}
}
Ok(Event::Empty(ref e)) => {
let name = e.name();
if !matches_local_name(name.as_ref(), b"tcPr") {
extra_xml.push((content.len(), capture_empty_element(e)?));
}
}
Ok(Event::End(ref e)) if matches_local_name(e.name().as_ref(), b"tc") => {
break;
}
Ok(Event::Eof) => break,
Err(e) => return Err(e.into()),
_ => {}
}
buf.clear();
}
Ok(CT_Tc {
properties,
content,
extra_xml,
})
}
pub fn to_xml<W: std::io::Write>(&self, writer: &mut Writer<W>) -> Result<()> {
writer.write_event(Event::Start(BytesStart::new("w:tc")))?;
if let Some(ref props) = self.properties {
props.to_xml(writer)?;
}
for (idx, item) in self.content.iter().enumerate() {
write_extras_at(writer, &self.extra_xml, idx)?;
match item {
CellContent::Paragraph(p) => p.to_xml(writer)?,
CellContent::Table(tbl) => tbl.to_xml(writer)?,
}
}
write_extras_at(writer, &self.extra_xml, self.content.len())?;
writer.write_event(Event::End(BytesEnd::new("w:tc")))?;
Ok(())
}
}
impl Default for CT_Tc {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct CT_Row {
pub properties: Option<CT_TrPr>,
pub cells: Vec<CT_Tc>,
pub extra_xml: Vec<(usize, Vec<u8>)>,
}
#[allow(non_snake_case)]
impl CT_Row {
pub fn new() -> Self {
CT_Row {
properties: None,
cells: Vec::new(),
extra_xml: Vec::new(),
}
}
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 properties = None;
let mut cells = Vec::new();
let mut extra_xml = Vec::new();
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 matches_local_name(name.as_ref(), b"trPr") {
properties = Some(CT_TrPr::from_xml(reader)?);
} else if is_word_element(name.as_ref(), b"tc", &prefixes) {
cells.push(CT_Tc::from_xml_with_prefixes(reader, &prefixes)?);
} else {
extra_xml.push((cells.len(), capture_element(reader, e)?));
}
}
Ok(Event::Empty(ref e)) => {
let name = e.name();
if !matches_local_name(name.as_ref(), b"trPr") {
extra_xml.push((cells.len(), capture_empty_element(e)?));
}
}
Ok(Event::End(ref e)) if matches_local_name(e.name().as_ref(), b"tr") => {
break;
}
Ok(Event::Eof) => break,
Err(e) => return Err(e.into()),
_ => {}
}
buf.clear();
}
Ok(CT_Row {
properties,
cells,
extra_xml,
})
}
pub fn to_xml<W: std::io::Write>(&self, writer: &mut Writer<W>) -> Result<()> {
writer.write_event(Event::Start(BytesStart::new("w:tr")))?;
if let Some(ref props) = self.properties {
props.to_xml(writer)?;
}
for (idx, cell) in self.cells.iter().enumerate() {
write_extras_at(writer, &self.extra_xml, idx)?;
cell.to_xml(writer)?;
}
write_extras_at(writer, &self.extra_xml, self.cells.len())?;
writer.write_event(Event::End(BytesEnd::new("w:tr")))?;
Ok(())
}
}
impl Default for CT_Row {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct CT_Tbl {
pub properties: Option<CT_TblPr>,
pub grid: Option<CT_TblGrid>,
pub rows: Vec<CT_Row>,
pub extra_xml: Vec<(usize, Vec<u8>)>,
}
#[allow(non_snake_case)]
impl CT_Tbl {
pub fn new() -> Self {
CT_Tbl {
properties: None,
grid: None,
rows: Vec::new(),
extra_xml: Vec::new(),
}
}
pub fn from_xml(reader: &mut Reader<&[u8]>) -> Result<Self> {
Self::from_xml_with_prefixes(reader, &["w".to_string()])
}
pub(crate) fn from_xml_with_prefixes(
reader: &mut Reader<&[u8]>,
word_prefixes: &[String],
) -> Result<Self> {
let mut properties = None;
let mut grid = None;
let mut rows = Vec::new();
let mut extra_xml = Vec::new();
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 matches_local_name(name.as_ref(), b"tblPr") {
properties = Some(CT_TblPr::from_xml(reader)?);
} else if matches_local_name(name.as_ref(), b"tblGrid") {
grid = Some(CT_TblGrid::from_xml(reader)?);
} else if is_word_element(name.as_ref(), b"tr", &prefixes) {
rows.push(CT_Row::from_xml_with_prefixes(reader, &prefixes)?);
} else {
extra_xml.push((rows.len(), capture_element(reader, e)?));
}
}
Ok(Event::Empty(ref e)) => {
let name = e.name();
if !matches_local_name(name.as_ref(), b"tblPr")
&& !matches_local_name(name.as_ref(), b"tblGrid")
{
extra_xml.push((rows.len(), capture_empty_element(e)?));
}
}
Ok(Event::End(ref e)) if matches_local_name(e.name().as_ref(), b"tbl") => {
break;
}
Ok(Event::Eof) => break,
Err(e) => return Err(e.into()),
_ => {}
}
buf.clear();
}
Ok(CT_Tbl {
properties,
grid,
rows,
extra_xml,
})
}
pub fn to_xml<W: std::io::Write>(&self, writer: &mut Writer<W>) -> Result<()> {
writer.write_event(Event::Start(BytesStart::new("w:tbl")))?;
if let Some(ref props) = self.properties {
props.to_xml(writer)?;
}
if let Some(ref grid) = self.grid {
grid.to_xml(writer)?;
}
for (idx, row) in self.rows.iter().enumerate() {
write_extras_at(writer, &self.extra_xml, idx)?;
row.to_xml(writer)?;
}
write_extras_at(writer, &self.extra_xml, self.rows.len())?;
writer.write_event(Event::End(BytesEnd::new("w:tbl")))?;
Ok(())
}
}
impl Default for CT_Tbl {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn parse_table(xml: &str) -> CT_Tbl {
let full = format!("<w:tbl>{xml}</w:tbl>");
let mut reader = Reader::from_str(&full);
reader.config_mut().trim_text(true);
let mut buf = Vec::new();
loop {
match reader.read_event_into(&mut buf) {
Ok(Event::Start(ref e)) if matches_local_name(e.name().as_ref(), b"tbl") => break,
_ => {}
}
buf.clear();
}
CT_Tbl::from_xml(&mut reader).unwrap()
}
#[test]
fn parse_simple_table() {
let tbl = parse_table(
r#"<w:tblPr><w:tblW w:w="5000" w:type="dxa"/></w:tblPr>
<w:tblGrid><w:gridCol w:w="2500"/><w:gridCol w:w="2500"/></w:tblGrid>
<w:tr>
<w:tc><w:p><w:r><w:t>A1</w:t></w:r></w:p></w:tc>
<w:tc><w:p><w:r><w:t>B1</w:t></w:r></w:p></w:tc>
</w:tr>
<w:tr>
<w:tc><w:p><w:r><w:t>A2</w:t></w:r></w:p></w:tc>
<w:tc><w:p><w:r><w:t>B2</w:t></w:r></w:p></w:tc>
</w:tr>"#,
);
assert_eq!(tbl.rows.len(), 2);
assert_eq!(tbl.rows[0].cells.len(), 2);
assert_eq!(tbl.rows[0].cells[0].text(), "A1");
assert_eq!(tbl.rows[1].cells[1].text(), "B2");
let grid = tbl.grid.unwrap();
assert_eq!(grid.columns.len(), 2);
assert_eq!(grid.columns[0].width, Twips(2500));
let pr = tbl.properties.unwrap();
assert_eq!(pr.width.as_ref().unwrap().w, 5000);
}
#[test]
fn aliased_table_cell_paragraph_properties_keep_root_scope() {
let xml = format!(
r#"<q:tbl xmlns:q="{}" xmlns:ext="urn:producer"><q:tr><q:tc><ext:p><ext:pPr><ext:jc ext:val="right"/></ext:pPr></ext:p><q:p><q:pPr><ext:jc ext:val="right"/><q:jc q:val="center"/></q:pPr><q:r><q:t>Cell</q:t></q:r></q:p></q:tc></q:tr></q:tbl>"#,
crate::namespace::W_NS
);
let mut reader = Reader::from_str(&xml);
let mut buf = Vec::new();
let table = loop {
match reader.read_event_into(&mut buf) {
Ok(Event::Start(ref element)) if element.local_name().as_ref() == b"tbl" => {
let prefixes = word_prefixes_at(element, &[]).unwrap();
break CT_Tbl::from_xml_with_prefixes(&mut reader, &prefixes).unwrap();
}
Ok(Event::Eof) => panic!("missing table"),
event => {
event.unwrap();
}
}
buf.clear();
};
let paragraphs = table.rows[0].cells[0].paragraphs();
assert_eq!(paragraphs.len(), 1);
assert_eq!(paragraphs[0].text(), "Cell");
assert_eq!(
paragraphs[0].properties.as_ref().unwrap().jc,
Some(ST_Jc::Center)
);
}
#[test]
fn default_namespace_table_cell_properties_keep_root_scope() {
let xml = format!(
r#"<tbl xmlns="{0}" xmlns:w="{0}" xmlns:ext="urn:producer"><tr><tc><ext:p><ext:pPr><ext:jc ext:val="right"/></ext:pPr></ext:p><p><pPr><ext:jc ext:val="right"/><jc w:val="center"/></pPr><r><t>Cell</t></r></p></tc></tr></tbl>"#,
crate::namespace::W_NS
);
let mut reader = Reader::from_str(&xml);
let mut buf = Vec::new();
let table = loop {
match reader.read_event_into(&mut buf) {
Ok(Event::Start(ref element)) if element.local_name().as_ref() == b"tbl" => {
let prefixes = word_prefixes_at(element, &[]).unwrap();
break CT_Tbl::from_xml_with_prefixes(&mut reader, &prefixes).unwrap();
}
Ok(Event::Eof) => panic!("missing table"),
event => {
event.unwrap();
}
}
buf.clear();
};
let paragraphs = table.rows[0].cells[0].paragraphs();
assert_eq!(paragraphs.len(), 1);
assert_eq!(paragraphs[0].text(), "Cell");
assert_eq!(
paragraphs[0].properties.as_ref().unwrap().jc,
Some(ST_Jc::Center)
);
}
#[test]
fn parse_cell_merge() {
let tbl = parse_table(
r#"<w:tblGrid><w:gridCol w:w="2500"/><w:gridCol w:w="2500"/></w:tblGrid>
<w:tr>
<w:tc>
<w:tcPr><w:gridSpan w:val="2"/></w:tcPr>
<w:p><w:r><w:t>Merged</w:t></w:r></w:p>
</w:tc>
</w:tr>
<w:tr>
<w:tc>
<w:tcPr><w:vMerge w:val="restart"/></w:tcPr>
<w:p><w:r><w:t>VM Start</w:t></w:r></w:p>
</w:tc>
<w:tc><w:p/></w:tc>
</w:tr>
<w:tr>
<w:tc>
<w:tcPr><w:vMerge/></w:tcPr>
<w:p/>
</w:tc>
<w:tc><w:p/></w:tc>
</w:tr>"#,
);
assert_eq!(
tbl.rows[0].cells[0].properties.as_ref().unwrap().grid_span,
Some(2)
);
assert_eq!(
tbl.rows[1].cells[0].properties.as_ref().unwrap().v_merge,
Some(VMerge::Restart)
);
assert_eq!(
tbl.rows[2].cells[0].properties.as_ref().unwrap().v_merge,
Some(VMerge::Continue)
);
}
#[test]
fn parse_table_borders() {
let tbl = parse_table(
r#"<w:tblPr>
<w:tblBorders>
<w:top w:val="single" w:sz="4" w:color="000000"/>
<w:bottom w:val="single" w:sz="4" w:color="000000"/>
<w:left w:val="single" w:sz="4" w:color="000000"/>
<w:right w:val="single" w:sz="4" w:color="000000"/>
<w:insideH w:val="single" w:sz="4" w:color="000000"/>
<w:insideV w:val="single" w:sz="4" w:color="000000"/>
</w:tblBorders>
</w:tblPr>
<w:tblGrid><w:gridCol w:w="5000"/></w:tblGrid>
<w:tr><w:tc><w:p/></w:tc></w:tr>"#,
);
let borders = tbl.properties.unwrap().borders.unwrap();
assert_eq!(borders.top.unwrap().val, ST_Border::Single);
assert_eq!(borders.inside_h.unwrap().val, ST_Border::Single);
assert_eq!(borders.inside_v.unwrap().val, ST_Border::Single);
}
#[test]
fn parse_cell_shading() {
let tbl = parse_table(
r#"<w:tblGrid><w:gridCol w:w="5000"/></w:tblGrid>
<w:tr>
<w:tc>
<w:tcPr><w:shd w:val="clear" w:fill="FFFF00"/></w:tcPr>
<w:p/>
</w:tc>
</w:tr>"#,
);
let shd = tbl.rows[0].cells[0]
.properties
.as_ref()
.unwrap()
.shading
.as_ref()
.unwrap();
assert_eq!(shd.fill, Some("FFFF00".to_string()));
}
#[test]
fn parse_row_properties() {
let tbl = parse_table(
r#"<w:tblGrid><w:gridCol w:w="5000"/></w:tblGrid>
<w:tr>
<w:trPr>
<w:trHeight w:val="720" w:hRule="exact"/>
<w:tblHeader/>
</w:trPr>
<w:tc><w:p/></w:tc>
</w:tr>"#,
);
let tr_pr = tbl.rows[0].properties.as_ref().unwrap();
assert_eq!(tr_pr.height, Some(Twips(720)));
assert_eq!(tr_pr.height_rule, Some("exact".to_string()));
assert_eq!(tr_pr.header, Some(true));
}
#[test]
fn round_trip_table() {
let mut tbl = CT_Tbl::new();
tbl.properties = Some(CT_TblPr {
width: Some(CT_TblWidth::dxa(9000)),
borders: Some(CT_TblBorders {
top: Some(CT_BorderEdge {
val: ST_Border::Single,
sz: Some(4),
space: Some(0),
color: Some("000000".to_string()),
}),
bottom: Some(CT_BorderEdge {
val: ST_Border::Single,
sz: Some(4),
space: Some(0),
color: Some("000000".to_string()),
}),
..Default::default()
}),
..Default::default()
});
tbl.grid = Some(CT_TblGrid {
columns: vec![
CT_TblGridCol { width: Twips(4500) },
CT_TblGridCol { width: Twips(4500) },
],
});
let mut row = CT_Row::new();
let mut cell1 = CT_Tc::new();
cell1.paragraphs_mut()[0].add_run("Hello");
let mut cell2 = CT_Tc::new();
cell2.paragraphs_mut()[0].add_run("World");
row.cells.push(cell1);
row.cells.push(cell2);
tbl.rows.push(row);
let mut output = Vec::new();
let mut writer = Writer::new(&mut output);
tbl.to_xml(&mut writer).unwrap();
let xml = String::from_utf8(output).unwrap();
let parsed = parse_table(
xml.strip_prefix("<w:tbl>")
.unwrap()
.strip_suffix("</w:tbl>")
.unwrap(),
);
assert_eq!(parsed.rows.len(), 1);
assert_eq!(parsed.rows[0].cells.len(), 2);
assert_eq!(parsed.rows[0].cells[0].text(), "Hello");
assert_eq!(parsed.rows[0].cells[1].text(), "World");
let grid = parsed.grid.unwrap();
assert_eq!(grid.columns.len(), 2);
assert_eq!(grid.columns[0].width, Twips(4500));
let borders = parsed.properties.unwrap().borders.unwrap();
assert!(borders.top.is_some());
assert!(borders.bottom.is_some());
}
#[test]
fn nested_table_xml_round_trip() {
use crate::text::CT_P;
let mut outer_cell = CT_Tc::new();
outer_cell.paragraphs_mut()[0].add_run("Before table");
let mut nested_tbl = CT_Tbl::new();
nested_tbl.grid = Some(CT_TblGrid {
columns: vec![CT_TblGridCol { width: Twips(2000) }],
});
let mut nested_row = CT_Row::new();
let mut nested_cell = CT_Tc::new();
nested_cell.paragraphs_mut()[0].add_run("Nested content");
nested_row.cells.push(nested_cell);
nested_tbl.rows.push(nested_row);
outer_cell.content.push(CellContent::Table(nested_tbl));
let mut after = CT_P::new();
after.add_run("After table");
outer_cell.content.push(CellContent::Paragraph(after));
let mut output = Vec::new();
let mut writer = Writer::new(&mut output);
outer_cell.to_xml(&mut writer).unwrap();
let xml = String::from_utf8(output).unwrap();
assert!(xml.contains("<w:tbl>"));
assert!(xml.contains("Nested content"));
let inner_xml = xml
.strip_prefix("<w:tc>")
.unwrap()
.strip_suffix("</w:tc>")
.unwrap();
let full_xml = format!(
"<w:tc xmlns:w=\"http://schemas.openxmlformats.org/wordprocessingml/2006/main\">{inner_xml}</w:tc>"
);
let mut reader = Reader::from_str(&full_xml);
reader.config_mut().trim_text(true);
loop {
match reader.read_event() {
Ok(Event::Start(e)) if e.local_name().as_ref() == b"tc" => break,
_ => {}
}
}
let parsed = CT_Tc::from_xml(&mut reader).unwrap();
assert_eq!(parsed.paragraphs().len(), 2);
assert_eq!(parsed.paragraphs()[0].text(), "Before table");
assert_eq!(parsed.paragraphs()[1].text(), "After table");
let tables: Vec<_> = parsed
.content
.iter()
.filter_map(|c| match c {
CellContent::Table(t) => Some(t),
_ => None,
})
.collect();
assert_eq!(tables.len(), 1);
assert_eq!(tables[0].rows.len(), 1);
assert_eq!(tables[0].rows[0].cells[0].text(), "Nested content");
}
#[test]
fn paragraphs_method_backward_compat() {
let mut cell = CT_Tc::new();
assert_eq!(cell.paragraphs().len(), 1);
cell.paragraphs_mut()[0].add_run("First");
let nested = CT_Tbl::new();
cell.content.push(CellContent::Table(nested));
let mut p = CT_P::new();
p.add_run("Second");
cell.content.push(CellContent::Paragraph(p));
assert_eq!(cell.paragraphs().len(), 2);
assert_eq!(cell.paragraphs()[0].text(), "First");
assert_eq!(cell.paragraphs()[1].text(), "Second");
assert_eq!(cell.text(), "First\nSecond");
}
fn table_to_xml(tbl: &CT_Tbl) -> String {
let mut output = Vec::new();
let mut writer = Writer::new(&mut output);
tbl.to_xml(&mut writer).unwrap();
String::from_utf8(output).unwrap()
}
#[test]
fn unknown_table_children_round_trip() {
const GRID: &str = r#"<w:tblGrid><w:gridCol w:w="4675"/></w:tblGrid>"#;
for (label, inner) in [
(
"row wrapped in a content control",
format!(
r#"{GRID}<w:sdt><w:sdtContent><w:tr><w:tc><w:p><w:r><w:t>x</w:t></w:r></w:p></w:tc></w:tr></w:sdtContent></w:sdt>"#
),
),
(
"cell wrapped in a content control",
format!(
r#"{GRID}<w:tr><w:sdt><w:sdtContent><w:tc><w:p><w:r><w:t>x</w:t></w:r></w:p></w:tc></w:sdtContent></w:sdt></w:tr>"#
),
),
(
"paragraph wrapped in a content control",
format!(
r#"{GRID}<w:tr><w:tc><w:sdt><w:sdtContent><w:p><w:r><w:t>x</w:t></w:r></w:p></w:sdtContent></w:sdt></w:tc></w:tr>"#
),
),
(
"bookmark at table level",
format!(
r#"{GRID}<w:bookmarkStart w:id="1" w:name="b"/><w:tr><w:tc><w:p><w:r><w:t>x</w:t></w:r></w:p></w:tc></w:tr>"#
),
),
(
"bookmark at row level",
format!(
r#"{GRID}<w:tr><w:bookmarkStart w:id="1" w:name="b"/><w:tc><w:p><w:r><w:t>x</w:t></w:r></w:p></w:tc></w:tr>"#
),
),
] {
let tbl = parse_table(&inner);
let xml = table_to_xml(&tbl);
assert_eq!(
xml,
format!("<w:tbl>{inner}</w:tbl>"),
"{label} was not preserved"
);
}
}
#[test]
fn table_style_conditional_formatting_round_trips() {
let inner = concat!(
r#"<w:tblPr><w:tblStyle w:val="GridTable4-Accent1"/>"#,
r#"<w:tblLook w:val="04A0" w:firstRow="1" w:lastRow="0" w:firstColumn="1" w:lastColumn="0" w:noHBand="0" w:noVBand="1"/>"#,
r#"</w:tblPr><w:tblGrid><w:gridCol w:w="4675"/></w:tblGrid>"#,
r#"<w:tr><w:trPr><w:cnfStyle w:val="100000000000"/></w:trPr>"#,
r#"<w:tc><w:tcPr><w:cnfStyle w:val="001000000000"/></w:tcPr><w:p/></w:tc>"#,
r#"</w:tr>"#,
);
let tbl = parse_table(inner);
let look = tbl
.properties
.as_ref()
.and_then(|p| p.look.as_ref())
.expect("tblLook should be parsed");
assert_eq!(look.val.as_deref(), Some("04A0"));
assert_eq!(look.first_row, Some(true));
assert_eq!(look.last_row, Some(false));
assert_eq!(look.first_column, Some(true));
assert_eq!(look.no_v_band, Some(true));
assert_eq!(
tbl.rows[0]
.properties
.as_ref()
.and_then(|p| p.cnf_style.as_deref()),
Some("100000000000")
);
assert_eq!(
tbl.rows[0].cells[0]
.properties
.as_ref()
.and_then(|p| p.cnf_style.as_deref()),
Some("001000000000")
);
assert_eq!(
table_to_xml(&tbl),
format!("<w:tbl>{inner}</w:tbl>"),
"the whole thing must survive a write"
);
}
#[test]
fn properties_holding_only_cnf_style_are_still_written() {
let tbl = parse_table(concat!(
r#"<w:tblGrid><w:gridCol w:w="100"/></w:tblGrid>"#,
r#"<w:tr><w:trPr><w:cnfStyle w:val="100000000000"/></w:trPr>"#,
r#"<w:tc><w:tcPr><w:cnfStyle w:val="001000000000"/></w:tcPr><w:p/></w:tc></w:tr>"#,
));
let xml = table_to_xml(&tbl);
assert!(
xml.contains(r#"<w:trPr><w:cnfStyle w:val="100000000000"/></w:trPr>"#),
"{xml}"
);
assert!(
xml.contains(r#"<w:tcPr><w:cnfStyle w:val="001000000000"/></w:tcPr>"#),
"{xml}"
);
}
#[test]
fn tbl_look_accepts_either_boolean_spelling() {
let tbl = parse_table(concat!(
r#"<w:tblPr><w:tblLook w:firstRow="true" w:lastRow="false" w:noVBand="1"/></w:tblPr>"#,
r#"<w:tblGrid><w:gridCol w:w="100"/></w:tblGrid>"#,
));
let look = tbl
.properties
.as_ref()
.and_then(|p| p.look.as_ref())
.unwrap();
assert_eq!(look.first_row, Some(true));
assert_eq!(look.last_row, Some(false));
assert_eq!(look.no_v_band, Some(true));
}
#[test]
fn self_closing_table_properties_are_not_reordered() {
let tbl = parse_table(
r#"<w:tblPr/><w:tblGrid><w:gridCol w:w="100"/></w:tblGrid><w:tr><w:tc><w:p/></w:tc></w:tr>"#,
);
assert!(tbl.extra_xml.is_empty(), "tblPr must not be captured");
let xml = table_to_xml(&tbl);
assert!(
!xml.contains("</w:tr><w:tblPr/>"),
"tblPr must never follow the rows: {xml}"
);
}
}