use quick_xml::events::{BytesEnd, BytesStart, Event};
use quick_xml::{Reader, Writer, XmlVersion};
use crate::error::{OxmlError, Result};
use crate::namespace::{W_NS, matches_local_name};
use crate::shared::{ST_Border, ST_TabJc, ST_TabLeader};
use crate::units::Twips;
const MAX_TAB_XML_DEPTH: usize = 64;
#[derive(Debug, Clone, PartialEq)]
pub struct CT_BorderEdge {
pub val: ST_Border,
pub sz: Option<u32>,
pub space: Option<u32>,
pub color: Option<String>,
}
impl CT_BorderEdge {
pub fn new(val: ST_Border) -> Self {
CT_BorderEdge {
val,
sz: None,
space: None,
color: None,
}
}
pub fn from_xml_attrs(e: &BytesStart) -> Result<Self> {
let mut val = ST_Border::None;
let mut sz = None;
let mut space = None;
let mut color = None;
for attr in e.attributes() {
let attr = attr?;
let key = attr.key.as_ref();
let v = std::str::from_utf8(&attr.value)?;
if matches_local_name(key, b"val") {
val = ST_Border::from_str(v)?;
} else if matches_local_name(key, b"sz") {
sz = Some(v.parse()?);
} else if matches_local_name(key, b"space") {
space = Some(v.parse()?);
} else if matches_local_name(key, b"color") {
color = Some(v.to_string());
}
}
Ok(CT_BorderEdge {
val,
sz,
space,
color,
})
}
pub fn write_xml_attrs(&self, e: &mut BytesStart) {
let mut buf = itoa::Buffer::new();
e.push_attribute(("w:val", self.val.to_str()));
if let Some(sz) = self.sz {
e.push_attribute(("w:sz", buf.format(sz)));
}
if let Some(space) = self.space {
e.push_attribute(("w:space", buf.format(space)));
}
if let Some(ref color) = self.color {
e.push_attribute(("w:color", color.as_str()));
}
}
pub fn to_xml<W: std::io::Write>(
&self,
writer: &mut Writer<W>,
tag: &str,
) -> crate::error::Result<()> {
let mut e = BytesStart::new(tag);
self.write_xml_attrs(&mut e);
writer.write_event(Event::Empty(e))?;
Ok(())
}
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct CT_PBdr {
pub top: Option<CT_BorderEdge>,
pub bottom: Option<CT_BorderEdge>,
pub left: Option<CT_BorderEdge>,
pub right: Option<CT_BorderEdge>,
pub between: Option<CT_BorderEdge>,
pub bar: Option<CT_BorderEdge>,
}
impl CT_PBdr {
pub fn from_xml(reader: &mut Reader<&[u8]>) -> Result<Self> {
let mut bdr = CT_PBdr::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") {
bdr.top = Some(edge);
} else if matches_local_name(name.as_ref(), b"bottom") {
bdr.bottom = Some(edge);
} else if matches_local_name(name.as_ref(), b"left")
|| matches_local_name(name.as_ref(), b"start")
{
bdr.left = Some(edge);
} else if matches_local_name(name.as_ref(), b"right")
|| matches_local_name(name.as_ref(), b"end")
{
bdr.right = Some(edge);
} else if matches_local_name(name.as_ref(), b"between") {
bdr.between = Some(edge);
} else if matches_local_name(name.as_ref(), b"bar") {
bdr.bar = Some(edge);
}
}
Ok(Event::End(ref e)) if matches_local_name(e.name().as_ref(), b"pBdr") => {
break;
}
Ok(Event::Eof) => break,
Err(e) => return Err(e.into()),
_ => {}
}
buf.clear();
}
Ok(bdr)
}
pub fn to_xml<W: std::io::Write>(&self, writer: &mut Writer<W>) -> Result<()> {
writer.write_event(Event::Start(BytesStart::new("w:pBdr")))?;
if let Some(ref edge) = self.top {
let mut e = BytesStart::new("w:top");
edge.write_xml_attrs(&mut e);
writer.write_event(Event::Empty(e))?;
}
if let Some(ref edge) = self.left {
let mut e = BytesStart::new("w:left");
edge.write_xml_attrs(&mut e);
writer.write_event(Event::Empty(e))?;
}
if let Some(ref edge) = self.bottom {
let mut e = BytesStart::new("w:bottom");
edge.write_xml_attrs(&mut e);
writer.write_event(Event::Empty(e))?;
}
if let Some(ref edge) = self.right {
let mut e = BytesStart::new("w:right");
edge.write_xml_attrs(&mut e);
writer.write_event(Event::Empty(e))?;
}
if let Some(ref edge) = self.between {
let mut e = BytesStart::new("w:between");
edge.write_xml_attrs(&mut e);
writer.write_event(Event::Empty(e))?;
}
if let Some(ref edge) = self.bar {
let mut e = BytesStart::new("w:bar");
edge.write_xml_attrs(&mut e);
writer.write_event(Event::Empty(e))?;
}
writer.write_event(Event::End(BytesEnd::new("w:pBdr")))?;
Ok(())
}
pub fn is_empty(&self) -> bool {
self.top.is_none()
&& self.bottom.is_none()
&& self.left.is_none()
&& self.right.is_none()
&& self.between.is_none()
&& self.bar.is_none()
}
}
#[derive(Debug, Clone)]
pub struct CT_TabStop {
pub val: ST_TabJc,
pub pos: Twips,
pub leader: Option<ST_TabLeader>,
pub source_occurrence: Option<usize>,
}
impl PartialEq for CT_TabStop {
fn eq(&self, other: &Self) -> bool {
self.val == other.val && self.pos == other.pos && self.leader == other.leader
}
}
impl CT_TabStop {
pub fn new(val: ST_TabJc, pos: Twips) -> Self {
CT_TabStop {
val,
pos,
leader: None,
source_occurrence: None,
}
}
pub fn from_xml_attrs(e: &BytesStart) -> Result<Self> {
Self::from_xml_attrs_with_prefixes(e, &["w".to_string()])
}
pub fn from_xml_attrs_with_prefixes(e: &BytesStart, word_prefixes: &[String]) -> Result<Self> {
let prefixes = word_prefixes_at(e, word_prefixes)?;
let mut val = ST_TabJc::Left;
let mut pos = Twips(0);
let mut leader = None;
for attr in e.attributes() {
let attr = attr?;
let key = attr.key.as_ref();
let v = std::str::from_utf8(&attr.value)?;
if is_word_attribute(key, b"val", &prefixes) {
val = ST_TabJc::from_str(v)?;
} else if is_word_attribute(key, b"pos", &prefixes) {
pos = Twips(v.parse()?);
} else if is_word_attribute(key, b"leader", &prefixes) {
leader = Some(ST_TabLeader::from_str(v)?);
}
}
Ok(CT_TabStop {
val,
pos,
leader,
source_occurrence: None,
})
}
}
fn word_prefixes_at(start: &BytesStart<'_>, inherited: &[String]) -> Result<Vec<String>> {
let mut prefixes = inherited.to_vec();
for attribute in start.attributes() {
let attribute = attribute?;
let name = attribute.key.as_ref();
let prefix = if name == b"xmlns" {
b"".as_slice()
} else if let Some(prefix) = name.strip_prefix(b"xmlns:") {
prefix
} else {
continue;
};
let prefix = std::str::from_utf8(prefix)?.to_string();
prefixes.retain(|candidate| candidate != &prefix);
let value =
attribute.decoded_and_normalized_value(XmlVersion::Implicit1_0, start.decoder())?;
if value.as_bytes() == W_NS.as_bytes() {
prefixes.push(prefix);
}
}
Ok(prefixes)
}
fn is_word_name(name: &[u8], word_prefixes: &[String]) -> bool {
let Some(separator) = name.iter().position(|byte| *byte == b':') else {
return word_prefixes.iter().any(String::is_empty);
};
word_prefixes
.iter()
.any(|prefix| prefix.as_bytes() == &name[..separator])
}
fn is_word_element(name: &[u8], local: &[u8], word_prefixes: &[String]) -> bool {
matches_local_name(name, local) && is_word_name(name, word_prefixes)
}
fn is_word_attribute(key: &[u8], local: &[u8], word_prefixes: &[String]) -> bool {
let Some(separator) = key.iter().position(|byte| *byte == b':') else {
return false;
};
key.get(separator + 1..) == Some(local)
&& word_prefixes
.iter()
.any(|prefix| prefix.as_bytes() == &key[..separator])
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct CT_Tabs {
pub tabs: Vec<CT_TabStop>,
}
impl CT_Tabs {
pub fn from_xml(reader: &mut Reader<&[u8]>) -> Result<Self> {
Self::from_xml_with_prefixes(reader, &["w".to_string()])
}
pub fn from_xml_with_prefixes(
reader: &mut Reader<&[u8]>,
word_prefixes: &[String],
) -> Result<Self> {
let mut tabs = Vec::new();
let mut buf = Vec::new();
let mut scopes = vec![word_prefixes.to_vec()];
loop {
match reader.read_event_into(&mut buf) {
Ok(Event::Empty(ref e)) => {
let prefixes = word_prefixes_at(e, scopes.last().expect("outer scope"))?;
if scopes.len() == 1 && is_word_element(e.name().as_ref(), b"tab", &prefixes) {
let mut tab = CT_TabStop::from_xml_attrs_with_prefixes(e, &prefixes)?;
tab.source_occurrence = Some(tabs.len());
tabs.push(tab);
}
}
Ok(Event::Start(ref e)) => {
if scopes.len() >= MAX_TAB_XML_DEPTH {
return Err(OxmlError::InvalidValue(format!(
"tab XML depth exceeds {MAX_TAB_XML_DEPTH}"
)));
}
let prefixes = word_prefixes_at(e, scopes.last().expect("outer scope"))?;
if scopes.len() == 1 && is_word_element(e.name().as_ref(), b"tab", &prefixes) {
let mut tab = CT_TabStop::from_xml_attrs_with_prefixes(e, &prefixes)?;
tab.source_occurrence = Some(tabs.len());
tabs.push(tab);
}
scopes.push(prefixes);
}
Ok(Event::End(ref e)) => {
if scopes.len() == 1 && is_word_element(e.name().as_ref(), b"tabs", &scopes[0])
{
break;
}
if scopes.len() > 1 {
scopes.pop();
}
}
Ok(Event::Eof) => break,
Err(e) => return Err(e.into()),
_ => {}
}
buf.clear();
}
Ok(CT_Tabs { tabs })
}
pub fn to_xml<W: std::io::Write>(&self, writer: &mut Writer<W>) -> Result<()> {
if self.tabs.is_empty() {
return Ok(());
}
writer.write_event(Event::Start(BytesStart::new("w:tabs")))?;
let mut buf = itoa::Buffer::new();
for tab in &self.tabs {
let mut e = BytesStart::new("w:tab");
e.push_attribute(("w:val", tab.val.to_str()));
e.push_attribute(("w:pos", buf.format(tab.pos.0)));
if let Some(leader) = tab.leader {
e.push_attribute(("w:leader", leader.to_str()));
}
writer.write_event(Event::Empty(e))?;
}
writer.write_event(Event::End(BytesEnd::new("w:tabs")))?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn round_trip_borders() {
let bdr = CT_PBdr {
top: Some(CT_BorderEdge {
val: ST_Border::Single,
sz: Some(4),
space: Some(1),
color: Some("000000".to_string()),
}),
bottom: Some(CT_BorderEdge {
val: ST_Border::Double,
sz: Some(6),
space: Some(2),
color: Some("FF0000".to_string()),
}),
..Default::default()
};
let mut output = Vec::new();
let mut writer = Writer::new(&mut output);
bdr.to_xml(&mut writer).unwrap();
let xml = String::from_utf8(output).unwrap();
let full = xml.to_string();
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"pBdr") => {
break;
}
_ => {}
}
buf.clear();
}
let parsed = CT_PBdr::from_xml(&mut reader).unwrap();
assert_eq!(parsed.top.as_ref().unwrap().val, ST_Border::Single);
assert_eq!(parsed.top.as_ref().unwrap().sz, Some(4));
assert_eq!(parsed.bottom.as_ref().unwrap().val, ST_Border::Double);
assert!(parsed.left.is_none());
}
#[test]
fn round_trip_tabs() {
let tabs = CT_Tabs {
tabs: vec![
CT_TabStop {
val: ST_TabJc::Left,
pos: Twips(720),
leader: None,
source_occurrence: None,
},
CT_TabStop {
val: ST_TabJc::Center,
pos: Twips(4320),
leader: Some(ST_TabLeader::Dot),
source_occurrence: None,
},
CT_TabStop {
val: ST_TabJc::Right,
pos: Twips(8640),
leader: Some(ST_TabLeader::Hyphen),
source_occurrence: None,
},
],
};
let mut output = Vec::new();
let mut writer = Writer::new(&mut output);
tabs.to_xml(&mut writer).unwrap();
let xml = String::from_utf8(output).unwrap();
let mut reader = Reader::from_str(&xml);
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"tabs") => {
break;
}
_ => {}
}
buf.clear();
}
let parsed = CT_Tabs::from_xml(&mut reader).unwrap();
assert_eq!(parsed.tabs.len(), 3);
assert_eq!(parsed.tabs[0].val, ST_TabJc::Left);
assert_eq!(parsed.tabs[0].pos, Twips(720));
assert_eq!(parsed.tabs[1].val, ST_TabJc::Center);
assert_eq!(parsed.tabs[1].leader, Some(ST_TabLeader::Dot));
assert_eq!(parsed.tabs[2].val, ST_TabJc::Right);
assert_eq!(parsed.tabs[0].source_occurrence, Some(0));
assert_eq!(parsed.tabs[1].source_occurrence, Some(1));
}
#[test]
fn namespace_aware_tabs_ignore_foreign_same_local_children() {
let xml = r#"<q:tabs xmlns:q="http://schemas.openxmlformats.org/wordprocessingml/2006/main" xmlns:ext="urn:producer"><ext:tab ext:val="right" ext:pos="99"/><q:tab q:val="left" q:pos="720"/></q:tabs>"#;
let mut reader = Reader::from_str(xml);
let mut buf = Vec::new();
loop {
match reader.read_event_into(&mut buf) {
Ok(Event::Start(_)) => break,
Ok(Event::Eof) => panic!("missing tabs start"),
_ => {}
}
buf.clear();
}
let parsed = CT_Tabs::from_xml_with_prefixes(&mut reader, &["q".to_string()]).unwrap();
assert_eq!(parsed.tabs.len(), 1);
assert_eq!(parsed.tabs[0].pos, Twips(720));
assert_eq!(parsed.tabs[0].source_occurrence, Some(0));
let mut constructed = CT_TabStop::new(ST_TabJc::Left, Twips(720));
assert_eq!(parsed.tabs[0], constructed);
constructed.source_occurrence = Some(99);
assert_eq!(parsed.tabs[0], constructed);
}
#[test]
fn namespace_aware_tabs_track_shadows_and_expanded_tab_elements() {
let xml = r#"<q:tabs xmlns:q="http://schemas.openxmlformats.org/wordprocessingml/2006/main"><foreign xmlns="urn:producer" xmlns:q="urn:producer"><q:tab q:val="right" q:pos="99"/><q:tabs><q:tab q:val="right" q:pos="100"/></q:tabs></foreign><q:tab q:val="left" q:pos="720"></q:tab><q:tab q:val="right" q:pos="1440"/></q:tabs>"#;
let mut reader = Reader::from_str(xml);
let mut buf = Vec::new();
loop {
match reader.read_event_into(&mut buf) {
Ok(Event::Start(_)) => break,
Ok(Event::Eof) => panic!("missing tabs start"),
event => {
event.unwrap();
}
}
buf.clear();
}
let parsed = CT_Tabs::from_xml_with_prefixes(&mut reader, &["q".to_string()]).unwrap();
assert_eq!(parsed.tabs.len(), 2);
assert_eq!(parsed.tabs[0].pos, Twips(720));
assert_eq!(parsed.tabs[0].source_occurrence, Some(0));
assert_eq!(parsed.tabs[1].pos, Twips(1440));
assert_eq!(parsed.tabs[1].source_occurrence, Some(1));
let default_xml = r#"<tabs xmlns="http://schemas.openxmlformats.org/wordprocessingml/2006/main"><foreign xmlns="urn:producer"><tab val="right" pos="99"/><tabs><tab val="right" pos="100"/></tabs></foreign><tab xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" w:val="center" w:pos="2160"></tab></tabs>"#;
let mut reader = Reader::from_str(default_xml);
let mut buf = Vec::new();
loop {
match reader.read_event_into(&mut buf) {
Ok(Event::Start(_)) => break,
Ok(Event::Eof) => panic!("missing default tabs start"),
event => {
event.unwrap();
}
}
buf.clear();
}
let parsed = CT_Tabs::from_xml_with_prefixes(&mut reader, &[String::new()]).unwrap();
assert_eq!(parsed.tabs.len(), 1);
assert_eq!(parsed.tabs[0].val, ST_TabJc::Center);
assert_eq!(parsed.tabs[0].pos, Twips(2160));
assert_eq!(parsed.tabs[0].source_occurrence, Some(0));
}
#[test]
fn namespace_aware_tabs_reject_deep_distinct_aliases_normally() {
let mut xml = format!(r#"<q:tabs xmlns:q="{W_NS}">"#);
for depth in 0..MAX_TAB_XML_DEPTH {
xml.push_str(&format!(r#"<n{depth}:unknown xmlns:n{depth}="{W_NS}">"#));
}
for depth in (0..MAX_TAB_XML_DEPTH).rev() {
xml.push_str(&format!("</n{depth}:unknown>"));
}
xml.push_str("</q:tabs>");
let mut reader = Reader::from_str(&xml);
let mut buf = Vec::new();
loop {
match reader.read_event_into(&mut buf) {
Ok(Event::Start(_)) => break,
Ok(Event::Eof) => panic!("missing tabs start"),
event => {
event.unwrap();
}
}
buf.clear();
}
let error = CT_Tabs::from_xml_with_prefixes(&mut reader, &["q".to_string()])
.expect_err("deep alias nesting must be bounded");
assert!(error.to_string().contains("tab XML depth exceeds 64"));
}
#[test]
fn border_edge_all_styles_round_trip() {
let styles = [
ST_Border::None,
ST_Border::Single,
ST_Border::Thick,
ST_Border::Double,
ST_Border::Dotted,
ST_Border::Dashed,
ST_Border::DotDash,
ST_Border::Wave,
];
for &style in &styles {
let bdr = CT_PBdr {
top: Some(CT_BorderEdge {
val: style,
sz: Some(8),
space: Some(0),
color: Some("FF00FF".to_string()),
}),
..Default::default()
};
let mut output = Vec::new();
let mut writer = Writer::new(&mut output);
bdr.to_xml(&mut writer).unwrap();
let xml = String::from_utf8(output).unwrap();
let mut reader = Reader::from_str(&xml);
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"pBdr") => {
break;
}
_ => {}
}
buf.clear();
}
let parsed = CT_PBdr::from_xml(&mut reader).unwrap();
let top = parsed.top.as_ref().unwrap();
assert_eq!(
top.val, style,
"Border style round-trip failed for {style:?}"
);
assert_eq!(top.sz, Some(8));
assert_eq!(top.color.as_deref(), Some("FF00FF"));
}
}
}