use quick_xml::events::{BytesDecl, BytesEnd, BytesStart, Event};
use quick_xml::{Reader, Writer, XmlVersion};
use crate::borders::CT_BorderEdge;
use crate::content_control::{CT_Sdt, SdtOwner};
use crate::error::{OxmlError, Result};
use crate::header_footer::{HdrFtrRef, HdrFtrType};
use crate::namespace::{W_NS, matches_local_name};
use crate::numbering::{
local_namespace_overrides, merged_owner_bindings, namespace_bindings, word_prefixes_at,
};
use crate::properties::{
get_word_val_attr, is_word_attribute, is_word_element, parse_integer_measurement,
};
use crate::raw_xml::{capture_element, capture_empty_element};
use crate::revision::{CT_Revision, RevisionKind};
use crate::shared::{ST_PageOrientation, ST_SectionType};
use crate::table::{CT_Tbl, ST_VerticalJc};
use crate::text::{
CT_P, ROOT_MC_BINDING, ROOT_R_BINDING, ROOT_WP_BINDING, capture_root_attribute_record,
declare_w14_on_part_root, is_root_attribute_record, push_root_attribute_record,
root_binding_scope,
};
use crate::units::Twips;
struct WordNamespaceBinding {
uri: Vec<u8>,
depth: usize,
}
struct WordAdmissionFrame {
admitted: bool,
word: bool,
legacy_run_prefix: Option<Vec<u8>>,
mc: bool,
vml: bool,
owner: Option<SdtOwner>,
anchor_depth: Option<usize>,
namespace_undo: Vec<(Vec<u8>, Option<WordNamespaceBinding>)>,
mc_capture_depth: Option<usize>,
qualified_drawing_depth: Option<usize>,
control: bool,
cost: usize,
table_depth: usize,
local: Vec<u8>,
}
fn restore_namespace_depths(
depths: &mut std::collections::HashMap<Vec<u8>, WordNamespaceBinding>,
undo: Vec<(Vec<u8>, Option<WordNamespaceBinding>)>,
) {
for (prefix, previous) in undo.into_iter().rev() {
if let Some(previous) = previous {
depths.insert(prefix, previous);
} else {
depths.remove(&prefix);
}
}
}
fn decoded_namespace_is(uri: &[u8], expected: &str) -> bool {
uri == expected.as_bytes()
|| std::str::from_utf8(uri)
.ok()
.and_then(|uri| quick_xml::escape::unescape(uri).ok())
.is_some_and(|uri| uri == expected)
}
fn captured_namespace_is(
binding: Option<&WordNamespaceBinding>,
capture_depth: usize,
expected: &str,
) -> bool {
binding.is_some_and(|binding| {
if binding.depth < capture_depth {
decoded_namespace_is(&binding.uri, expected)
} else {
binding.uri == expected.as_bytes()
}
})
}
pub(crate) fn admitted_word_child(parent: &[u8], child: &[u8]) -> bool {
match parent {
b"document" => child == b"body",
b"body" | b"hdr" | b"ftr" | b"comment" | b"footnote" | b"endnote" | b"docPartBody"
| b"txbxContent" | b"tc" => matches!(child, b"p" | b"tbl" | b"sdt"),
b"comments" => child == b"comment",
b"footnotes" => child == b"footnote",
b"endnotes" => child == b"endnote",
b"glossaryDocument" => child == b"docParts",
b"docParts" => child == b"docPart",
b"docPart" => child == b"docPartBody",
b"tbl" => matches!(child, b"tr" | b"sdt"),
b"tr" => matches!(child, b"tc" | b"sdt"),
b"p" => matches!(
child,
b"r" | b"sdt"
| b"hyperlink"
| b"fldSimple"
| b"ruby"
| b"ins"
| b"del"
| b"moveFrom"
| b"moveTo"
),
b"r" => matches!(child, b"drawing" | b"pict" | b"object"),
b"sdt" => child == b"sdtContent",
b"sdtContent" => true,
b"hyperlink" => matches!(child, b"r" | b"ins" | b"del" | b"moveFrom" | b"moveTo"),
b"fldSimple" | b"rt" | b"rubyBase" => child == b"r",
b"ruby" => matches!(child, b"rt" | b"rubyBase"),
b"ins" | b"del" | b"moveFrom" | b"moveTo" => matches!(
child,
b"r" | b"sdt" | b"hyperlink" | b"ins" | b"del" | b"moveFrom" | b"moveTo"
),
_ => false,
}
}
fn word_owner_cost(path_cost: usize, increment: usize) -> Result<usize> {
path_cost.checked_add(increment).filter(|cost| *cost <= 256).ok_or_else(|| {
OxmlError::InvalidValue(
"Word owner nesting exceeds weighted limit 256 (content control 1, typed text box 16)".into(),
)
})
}
pub(crate) enum WordOwner<'a> {
Body(&'a CT_Body),
Paragraph(&'a CT_P),
Run(&'a crate::text::CT_R),
Control(&'a CT_Sdt),
Table(&'a CT_Tbl),
Row(&'a crate::table::CT_Row),
Cell(&'a crate::table::CT_Tc),
Field(&'a crate::text::Field),
Revision(&'a CT_Revision),
Drawing(&'a crate::drawing::CT_Drawing),
Anchor(&'a crate::drawing::CT_Anchor),
}
pub(crate) fn admit_word_model(root: WordOwner<'_>) -> Result<()> {
use crate::content_control::SdtContent;
use crate::table::CellContent;
use crate::text::RunContent;
let mut pending = vec![(root, 0usize, 0usize)];
while let Some((owner, cost, table_depth)) = pending.pop() {
let mut push = |child| pending.push((child, cost, table_depth));
match owner {
WordOwner::Body(body) => {
for child in &body.content {
match child {
BodyContent::Paragraph(p) => push(WordOwner::Paragraph(p)),
BodyContent::Table(t) => push(WordOwner::Table(t)),
BodyContent::ContentControl(s) => push(WordOwner::Control(s)),
BodyContent::RawXml(_) => {}
}
}
}
WordOwner::Paragraph(p) => {
for run in &p.runs {
push(WordOwner::Run(run));
}
for (_, _, _, control) in &p.content_controls {
push(WordOwner::Control(control));
}
for (_, _, revision) in &p.revisions {
push(WordOwner::Revision(revision));
}
for ruby in &p.rubies {
for run in &ruby.ruby_text {
push(WordOwner::Run(run));
}
}
}
WordOwner::Run(run) => {
for child in &run.content {
match child {
RunContent::Drawing(d) => push(WordOwner::Drawing(d)),
RunContent::Field(f) => push(WordOwner::Field(f)),
_ => {}
}
}
}
WordOwner::Control(control) => {
let cost = word_owner_cost(cost, 1)?;
for run in control.original_resource_runs() {
pending.push((WordOwner::Run(run), cost, table_depth));
}
for (_, revision) in &control.revisions {
pending.push((WordOwner::Revision(revision), cost, table_depth));
}
for child in &control.content {
let child = match child {
SdtContent::Paragraph(p) => WordOwner::Paragraph(p),
SdtContent::Table(t) => WordOwner::Table(t),
SdtContent::Row(r) => WordOwner::Row(r),
SdtContent::Cell(c) => WordOwner::Cell(c),
SdtContent::Run(r) => WordOwner::Run(r),
SdtContent::ContentControl(s) => WordOwner::Control(s),
SdtContent::RawXml(_) => continue,
};
pending.push((child, cost, table_depth));
}
}
WordOwner::Table(table) => {
if table_depth >= crate::table::MAX_RECOGNIZED_TABLE_NESTING {
return Err(OxmlError::InvalidValue(
"recognized model nesting exceeds table limit".into(),
));
}
let table_depth = table_depth + 1;
for row in &table.rows {
pending.push((WordOwner::Row(row), cost, table_depth));
}
for (_, _, control) in &table.content_controls {
pending.push((WordOwner::Control(control), cost, table_depth));
}
}
WordOwner::Row(row) => {
for cell in &row.cells {
push(WordOwner::Cell(cell));
}
for (_, _, control) in &row.content_controls {
push(WordOwner::Control(control));
}
}
WordOwner::Cell(cell) => {
for child in &cell.content {
match child {
CellContent::Paragraph(p) => push(WordOwner::Paragraph(p)),
CellContent::Table(t) => push(WordOwner::Table(t)),
CellContent::ContentControl(s) => push(WordOwner::Control(s)),
}
}
}
WordOwner::Field(field) => {
for child in field.resource_owner_children() {
push(child);
}
}
WordOwner::Revision(revision) => {
if let Some(p) = revision.content_paragraph() {
push(WordOwner::Paragraph(p));
}
if let crate::revision::RevisionContent::Runs(runs) = revision.content() {
for run in runs {
push(WordOwner::Run(run));
}
}
for (_, revision) in revision.nested_revisions() {
push(WordOwner::Revision(revision));
}
}
WordOwner::Drawing(drawing) => {
if let Some(anchor) = &drawing.anchor {
push(WordOwner::Anchor(anchor));
}
}
WordOwner::Anchor(anchor) => {
if let Some(shape) = &anchor.shape
&& (shape.text_body.is_some() || !shape.text.is_empty())
{
let cost = word_owner_cost(cost, 16)?;
if let Some(body) = &shape.text_body {
pending.push((WordOwner::Body(body), cost, table_depth));
}
for p in &shape.text {
pending.push((WordOwner::Paragraph(p), cost, table_depth));
}
}
}
}
}
Ok(())
}
pub(crate) fn admit_word_xml(
xml: &[u8],
prefixes: &[String],
consumed_owner: Option<&[u8]>,
) -> Result<()> {
debug_assert!(consumed_owner.is_none_or(|owner| matches!(owner, b"p" | b"r")));
let mut bindings = namespace_bindings(prefixes);
for prefix in prefixes.iter().filter(|prefix| !prefix.starts_with('\0')) {
if !bindings.iter().any(|(name, _)| name == prefix) {
bindings.push((prefix.clone(), W_NS.to_owned()));
}
}
let mut source = b"<rdocxAdmission".to_vec();
for (prefix, namespace) in bindings {
source.extend_from_slice(b" xmlns");
if !prefix.is_empty() {
source.push(b':');
source.extend_from_slice(prefix.as_bytes());
}
source.extend_from_slice(b"=\"");
source.extend_from_slice(quick_xml::escape::escape(&namespace).as_bytes());
source.push(b'"');
}
source.push(b'>');
source.extend_from_slice(xml);
source.extend_from_slice(b"</rdocxAdmission>");
let mut reader = Reader::from_reader(source.as_slice());
let mut stack: Vec<WordAdmissionFrame> = Vec::new();
let mut controls = 0usize;
let mut path_cost = 0usize;
let mut namespace_depths: std::collections::HashMap<Vec<u8>, WordNamespaceBinding> =
std::collections::HashMap::new();
let mut buffer = Vec::new();
loop {
let event = reader.read_event_into(&mut buffer)?;
match event {
Event::Start(ref element) | Event::Empty(ref element) => {
let local = element.local_name();
let parent_admitted = stack.last().is_none_or(|frame| frame.admitted);
let parent_owner = stack.last().and_then(|frame| frame.owner);
let in_control_content = stack
.last()
.is_some_and(|frame| frame.local == b"sdtContent");
let mut namespace_undo = Vec::new();
for attribute in element.attributes() {
let attribute = attribute?;
let key = attribute.key.as_ref();
let prefix = if key == b"xmlns" {
Some(b"".as_slice())
} else {
key.strip_prefix(b"xmlns:")
};
if let Some(prefix) = prefix {
let prefix = prefix.to_vec();
let uri = attribute.value.as_ref();
const XML_URI: &[u8] = b"http://www.w3.org/XML/1998/namespace";
if prefix == b"xmlns"
|| prefix == b"xml" && uri != XML_URI
|| prefix != b"xml" && uri == XML_URI
|| uri == b"http://www.w3.org/2000/xmlns/"
{
return Err(OxmlError::InvalidValue(
"reserved XML namespace binding".into(),
));
}
let previous = namespace_depths.insert(
prefix.clone(),
WordNamespaceBinding {
uri: uri.to_vec(),
depth: stack.len(),
},
);
namespace_undo.push((prefix, previous));
}
}
let name = element.name();
let name = name.as_ref();
let prefix = name
.iter()
.position(|byte| *byte == b':')
.map_or(b"".as_slice(), |separator| &name[..separator]);
let binding = namespace_depths.get(prefix);
let namespace_uri =
binding.map_or(b"".as_slice(), |binding| binding.uri.as_slice());
let virtual_owner = stack.is_empty() && consumed_owner.is_some();
let fallback_run = consumed_owner == Some(b"p".as_slice())
&& binding.is_none()
&& local.as_ref() == b"r"
&& stack.last().is_some_and(|parent| {
parent.admitted && parent.word && parent.local == b"p"
});
let legacy_run_prefix = if fallback_run {
Some(prefix.to_vec())
} else {
stack
.last()
.and_then(|parent| parent.legacy_run_prefix.clone())
};
let legacy_word = binding.is_none() && legacy_run_prefix.as_deref() == Some(prefix);
let word = virtual_owner
|| legacy_word
|| !stack.is_empty() && decoded_namespace_is(namespace_uri, W_NS);
let owned_control_child = !in_control_content
|| !word
|| local.as_ref() == b"sdt"
|| parent_owner.is_some_and(|owner: SdtOwner| owner.owns(local.as_ref()));
let qualified_control_owner =
!word || local.as_ref() != b"sdt" || parent_owner.is_some() || stack.len() == 1;
let mut admitted = parent_admitted && owned_control_child && qualified_control_owner
&& ((stack.is_empty() && local.as_ref() == b"rdocxAdmission")
|| (word && matches!(local.as_ref(),
b"document" | b"body" | b"p" | b"r" | b"tbl" | b"tr" | b"tc"
| b"sdt" | b"sdtContent" | b"hdr" | b"ftr" | b"comments" | b"comment"
| b"footnotes" | b"footnote" | b"endnotes" | b"endnote"
| b"glossaryDocument" | b"docParts" | b"docPart" | b"docPartBody"
| b"hyperlink" | b"fldSimple" | b"ins" | b"del" | b"moveFrom" | b"moveTo"
| b"drawing" | b"pict" | b"object" | b"txbxContent" | b"ruby" | b"rt" | b"rubyBase"))
|| (decoded_namespace_is(namespace_uri, crate::namespace::MC_NS)
&& matches!(local.as_ref(), b"AlternateContent" | b"Choice" | b"Fallback"))
|| (namespace_uri == b"http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing"
&& local.as_ref() == b"anchor")
|| (namespace_uri == b"http://schemas.openxmlformats.org/drawingml/2006/main"
&& matches!(local.as_ref(), b"graphic" | b"graphicData"))
|| (namespace_uri == b"http://schemas.microsoft.com/office/word/2010/wordprocessingShape"
&& matches!(local.as_ref(), b"wsp" | b"txbx"))
|| (namespace_uri == b"urn:schemas-microsoft-com:vml"
&& matches!(local.as_ref(), b"shape" | b"group" | b"textbox" | b"rect" | b"roundrect" | b"oval" | b"line" | b"polyline" | b"curve" | b"arc")));
if word
&& let Some(parent) = stack.last()
&& parent.word
{
admitted &= admitted_word_child(&parent.local, local.as_ref());
}
if namespace_uri
== b"http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing"
{
admitted &= stack.len() == 1
|| stack
.last()
.is_some_and(|parent| parent.word && parent.local == b"drawing");
}
let mc = !stack.is_empty()
&& decoded_namespace_is(namespace_uri, crate::namespace::MC_NS);
let mut mc_capture_depth = stack.last().and_then(|frame| frame.mc_capture_depth);
let mut qualified_drawing_depth = None;
let vml = !stack.is_empty() && namespace_uri == b"urn:schemas-microsoft-com:vml";
if mc && let Some(parent) = stack.last() {
admitted &= if local.as_ref() == b"AlternateContent" {
(stack.len() == 1 || parent.word && parent.local == b"r")
&& captured_namespace_is(binding, stack.len(), crate::namespace::MC_NS)
} else {
parent.mc
&& parent.local == b"AlternateContent"
&& mc_capture_depth.is_some_and(|depth| {
captured_namespace_is(binding, depth, crate::namespace::MC_NS)
})
};
if local.as_ref() == b"AlternateContent" {
mc_capture_depth = admitted.then_some(stack.len());
}
}
if word
&& let Some(parent) = stack.last()
&& parent.mc
{
admitted &= match (parent.local.as_slice(), local.as_ref()) {
(b"Choice", b"drawing") => mc_capture_depth.is_some_and(|depth| {
captured_namespace_is(binding, depth, W_NS) || legacy_word
}),
(b"Choice" | b"Fallback", b"pict") => true,
_ => false,
};
if admitted && local.as_ref() == b"drawing" {
qualified_drawing_depth = Some(stack.len());
}
}
if vml && let Some(parent) = stack.last() {
let shape = matches!(
local.as_ref(),
b"shape"
| b"group"
| b"rect"
| b"roundrect"
| b"oval"
| b"line"
| b"polyline"
| b"curve"
| b"arc"
);
admitted &= parent.word
&& matches!(parent.local.as_slice(), b"pict" | b"object")
&& shape
|| parent.vml && parent.local == b"group" && shape
|| parent.vml
&& matches!(
parent.local.as_slice(),
b"shape"
| b"rect"
| b"roundrect"
| b"oval"
| b"line"
| b"polyline"
| b"curve"
| b"arc"
)
&& local.as_ref() == b"textbox";
}
if word && matches!(local.as_ref(), b"ruby" | b"rt" | b"rubyBase") {
for attribute in element.attributes() {
let attribute = attribute?;
let key = attribute.key.as_ref();
if key != b"xmlns" && !key.starts_with(b"xmlns:") {
admitted = false;
}
}
}
let parent_anchor = stack.last().and_then(|frame| frame.anchor_depth);
if parent_anchor.is_none()
&& matches!(
namespace_uri,
b"http://schemas.openxmlformats.org/drawingml/2006/main"
| b"http://schemas.microsoft.com/office/word/2010/wordprocessingShape"
)
{
admitted = false;
}
let direct_drawing = stack
.last()
.is_some_and(|frame| frame.word && frame.local == b"drawing" && frame.admitted);
if direct_drawing && local.as_ref() == b"anchor" {
admitted = stack.last().unwrap().qualified_drawing_depth.is_none_or(|depth|
captured_namespace_is(binding, depth, "http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing"));
}
let mut anchor_depth =
(admitted && local.as_ref() == b"anchor").then_some(stack.len());
let typed_anchor_body = parent_anchor.is_some_and(|depth| {
local.as_ref() == b"txbxContent"
&& (captured_namespace_is(binding, depth, W_NS) || legacy_word)
});
if let Some(parent_anchor) = parent_anchor {
admitted = !matches!(
local.as_ref(),
b"srcRect"
| b"positionH"
| b"positionV"
| b"spPr"
| b"blip"
| b"wrapNone"
| b"wrapSquare"
| b"wrapTight"
| b"wrapThrough"
| b"wrapTopAndBottom"
) && !(captured_namespace_is(
binding,
parent_anchor,
"http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing",
) && local.as_ref() == b"docPr");
anchor_depth = (admitted && !typed_anchor_body).then_some(parent_anchor);
}
let control =
admitted && anchor_depth.is_none() && word && local.as_ref() == b"sdt";
let typed_vml_body = admitted
&& word
&& local.as_ref() == b"txbxContent"
&& stack.last().is_some_and(|frame| frame.local == b"textbox");
let textbox = matches!(event, Event::Start(_))
&& admitted
&& (typed_anchor_body || typed_vml_body);
let cost = usize::from(control) + 16 * usize::from(textbox);
if control && controls == 256 {
return Err(OxmlError::InvalidValue(
"content-control nesting exceeds inclusive limit 256".into(),
));
}
let next_cost = word_owner_cost(path_cost, cost)?;
let table_depth = stack.last().map_or(0, |frame| frame.table_depth);
let table_depth =
if admitted && anchor_depth.is_none() && word && local.as_ref() == b"tbl" {
if table_depth >= crate::table::MAX_RECOGNIZED_TABLE_NESTING {
return Err(OxmlError::InvalidValue(
"recognized model nesting exceeds table limit".into(),
));
}
table_depth + 1
} else {
table_depth
};
if matches!(event, Event::Start(_)) {
let owner = if !admitted || anchor_depth.is_some() {
None
} else if word {
match consumed_owner
.filter(|_| virtual_owner)
.unwrap_or(local.as_ref())
{
b"p" | b"r" | b"hyperlink" | b"fldSimple" => Some(SdtOwner::Inline),
b"body" | b"hdr" | b"ftr" | b"comment" | b"footnote" | b"endnote"
| b"docPartBody" | b"txbxContent" => Some(SdtOwner::Body),
b"tbl" => Some(SdtOwner::Table),
b"tr" => Some(SdtOwner::Row),
b"tc" => Some(SdtOwner::Cell),
b"sdt" => Some(parent_owner.unwrap_or(SdtOwner::Standalone)),
b"sdtContent" | b"ins" | b"del" | b"moveFrom" | b"moveTo" => {
parent_owner
}
_ => None,
}
} else if namespace_uri == crate::namespace::MC_NS.as_bytes() {
parent_owner
} else {
None
};
stack.push(WordAdmissionFrame {
control,
admitted,
word,
legacy_run_prefix: admitted.then_some(legacy_run_prefix).flatten(),
mc,
vml,
owner,
anchor_depth,
namespace_undo,
mc_capture_depth,
qualified_drawing_depth,
cost,
table_depth,
local: consumed_owner
.filter(|_| virtual_owner)
.unwrap_or(local.as_ref())
.to_vec(),
});
controls += usize::from(control);
path_cost = next_cost;
} else {
restore_namespace_depths(&mut namespace_depths, namespace_undo);
}
}
Event::End(_) => {
if let Some(frame) = stack.pop() {
controls -= usize::from(frame.control);
path_cost -= frame.cost;
restore_namespace_depths(&mut namespace_depths, frame.namespace_undo);
}
}
Event::Eof => break,
_ => {}
}
buffer.clear();
}
Ok(())
}
pub(crate) fn admit_word_reader(
input: &Reader<&[u8]>,
prefixes: &[String],
owner: &[u8],
namespace: Option<&str>,
) -> Result<()> {
let mut reader = input.clone();
reader.config_mut().check_end_names = true;
let xml = *input.get_ref();
let offset = input.buffer_position();
let mut depth = 0usize;
loop {
let before = reader.buffer_position();
match reader.read_event()? {
Event::Start(_) => depth += 1,
Event::End(_) if depth > 0 => depth -= 1,
Event::End(end) => {
let name = end.name();
if end.local_name().as_ref() != owner {
return Err(OxmlError::InvalidValue(
"remaining XML closes a different owner".into(),
));
}
if let Some(namespace) = namespace {
let bindings = namespace_bindings(prefixes);
let qualified = if namespace == W_NS {
is_word_element(name.as_ref(), owner, prefixes)
} else {
name.as_ref()
.iter()
.position(|byte| *byte == b':')
.is_some_and(|at| {
bindings.iter().any(|(prefix, uri)| {
prefix.as_bytes() == &name.as_ref()[..at] && uri == namespace
})
})
};
if !qualified {
return Err(OxmlError::InvalidValue(
"remaining XML closes a different qualified owner".into(),
));
}
let mut source = vec![b'<'];
source.extend_from_slice(name.as_ref());
source.push(b'>');
source.extend_from_slice(&xml[..(reader.buffer_position() - offset) as usize]);
return admit_word_xml(&source, prefixes, None);
}
return admit_word_xml(&xml[..(before - offset) as usize], prefixes, Some(owner));
}
Event::Eof => {
return Err(OxmlError::InvalidValue(
"remaining XML has no owning close".into(),
));
}
_ => {}
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum BodyContent {
Paragraph(CT_P),
Table(CT_Tbl),
ContentControl(CT_Sdt),
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>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct CT_PageNumberType {
pub start: Option<u32>,
#[doc(hidden)]
pub raw_xml: Option<Vec<u8>>,
#[doc(hidden)]
pub parsed_start: Option<u32>,
#[doc(hidden)]
pub start_attribute_name: Option<Vec<u8>>,
#[doc(hidden)]
pub start_insertion_prefix: Option<Vec<u8>>,
#[doc(hidden)]
pub reserved_insertion_prefixes: Vec<Vec<u8>>,
}
impl CT_PageNumberType {
pub fn new(start: u32) -> Self {
Self {
start: Some(start),
raw_xml: None,
parsed_start: None,
start_attribute_name: None,
start_insertion_prefix: None,
reserved_insertion_prefixes: Vec::new(),
}
}
}
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, Copy, PartialEq, Eq)]
pub enum ST_PageBorderZOrder {
Front,
Back,
}
impl ST_PageBorderZOrder {
pub fn from_str(value: &str) -> Option<Self> {
match value {
"front" => Some(Self::Front),
"back" => Some(Self::Back),
_ => None,
}
}
pub fn to_str(self) -> &'static str {
match self {
Self::Front => "front",
Self::Back => "back",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ST_PageBorderDisplay {
AllPages,
FirstPage,
NotFirstPage,
}
impl ST_PageBorderDisplay {
pub fn from_str(value: &str) -> Option<Self> {
match value {
"allPages" => Some(Self::AllPages),
"firstPage" => Some(Self::FirstPage),
"notFirstPage" => Some(Self::NotFirstPage),
_ => None,
}
}
pub fn to_str(self) -> &'static str {
match self {
Self::AllPages => "allPages",
Self::FirstPage => "firstPage",
Self::NotFirstPage => "notFirstPage",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ST_PageBorderOffset {
Page,
Text,
}
impl ST_PageBorderOffset {
pub fn from_str(value: &str) -> Option<Self> {
match value {
"page" => Some(Self::Page),
"text" => Some(Self::Text),
_ => None,
}
}
pub fn to_str(self) -> &'static str {
match self {
Self::Page => "page",
Self::Text => "text",
}
}
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct CT_PageBorders {
pub z_order: Option<ST_PageBorderZOrder>,
pub display: Option<ST_PageBorderDisplay>,
pub offset_from: Option<ST_PageBorderOffset>,
pub top: Option<CT_BorderEdge>,
pub left: Option<CT_BorderEdge>,
pub bottom: Option<CT_BorderEdge>,
pub right: Option<CT_BorderEdge>,
pub extra_attributes: Vec<(String, String)>,
pub extra_xml: Vec<Vec<u8>>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ST_LineNumberRestart {
NewPage,
NewSection,
Continuous,
}
impl ST_LineNumberRestart {
pub fn from_str(value: &str) -> Option<Self> {
match value {
"newPage" => Some(Self::NewPage),
"newSection" => Some(Self::NewSection),
"continuous" => Some(Self::Continuous),
_ => None,
}
}
pub fn to_str(self) -> &'static str {
match self {
Self::NewPage => "newPage",
Self::NewSection => "newSection",
Self::Continuous => "continuous",
}
}
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct CT_LineNumber {
pub count_by: Option<u32>,
pub start: Option<u32>,
pub distance: Option<Twips>,
pub restart: Option<ST_LineNumberRestart>,
pub extra_attributes: Vec<(String, String)>,
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct CT_PaperSource {
pub first: Option<u32>,
pub other: Option<u32>,
pub extra_attributes: Vec<(String, String)>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[allow(non_camel_case_types)]
pub enum ST_DocGrid {
Default,
Lines,
LinesAndChars,
SnapToChars,
}
impl ST_DocGrid {
pub fn from_str(value: &str) -> Option<Self> {
match value {
"default" => Some(Self::Default),
"lines" => Some(Self::Lines),
"linesAndChars" => Some(Self::LinesAndChars),
"snapToChars" => Some(Self::SnapToChars),
_ => None,
}
}
pub fn to_str(self) -> &'static str {
match self {
Self::Default => "default",
Self::Lines => "lines",
Self::LinesAndChars => "linesAndChars",
Self::SnapToChars => "snapToChars",
}
}
pub fn snaps_lines(self) -> bool {
matches!(self, Self::Lines | Self::LinesAndChars | Self::SnapToChars)
}
pub fn snaps_characters(self) -> bool {
matches!(self, Self::LinesAndChars | Self::SnapToChars)
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
#[allow(non_camel_case_types)]
pub struct CT_DocGrid {
pub grid_type: Option<ST_DocGrid>,
pub line_pitch: Option<Twips>,
pub char_space: Option<i32>,
pub extra_attributes: Vec<(String, String)>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct CT_NoteProperties {
pub pos: Option<String>,
pub num_fmt: Option<String>,
pub num_start: Option<u32>,
pub num_restart: Option<String>,
pub special_references: Vec<i32>,
#[doc(hidden)]
pub extra_xml: Vec<(usize, Vec<u8>)>,
}
#[doc(hidden)]
#[derive(Debug, Clone, PartialEq)]
pub struct CT_SectPrReferenceAnchor {
reference: HdrFtrRef,
source_occurrence: usize,
}
impl CT_SectPrReferenceAnchor {
fn at(references: &[HdrFtrRef], index: usize) -> Option<Self> {
let reference = references.get(index)?.clone();
let source_occurrence = references[..index]
.iter()
.filter(|candidate| **candidate == reference)
.count();
Some(Self {
reference,
source_occurrence,
})
}
fn resolve(&self, references: &[HdrFtrRef]) -> Option<usize> {
references
.iter()
.enumerate()
.filter(|(_, reference)| **reference == self.reference)
.nth(self.source_occurrence)
.map(|(index, _)| index)
}
}
#[doc(hidden)]
#[derive(Debug, Clone, PartialEq)]
pub enum CT_SectPrRawPosition {
Schema { slot: usize, occurrence: usize },
Header {
preceding: Option<CT_SectPrReferenceAnchor>,
following: Option<CT_SectPrReferenceAnchor>,
},
Footer {
preceding: Option<CT_SectPrReferenceAnchor>,
following: Option<CT_SectPrReferenceAnchor>,
},
}
#[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 page_number: Option<CT_PageNumberType>,
pub footnote_pr: Option<Box<CT_NoteProperties>>,
pub endnote_pr: Option<Box<CT_NoteProperties>>,
pub paper_source: Option<Box<CT_PaperSource>>,
pub page_borders: Option<Box<CT_PageBorders>>,
pub line_numbers: Option<Box<CT_LineNumber>>,
pub vertical_alignment: Option<ST_VerticalJc>,
pub text_direction: Option<String>,
pub doc_grid: Option<Box<CT_DocGrid>>,
pub title_pg: Option<bool>,
pub header_refs: Vec<HdrFtrRef>,
pub footer_refs: Vec<HdrFtrRef>,
pub extra_xml: Vec<Vec<u8>>,
#[doc(hidden)]
pub extra_xml_positions: Vec<CT_SectPrRawPosition>,
pub change: Option<CT_Revision>,
}
#[allow(non_snake_case)]
impl CT_SectPr {
fn empty() -> Self {
Self {
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,
page_number: None,
footnote_pr: None,
endnote_pr: None,
paper_source: None,
page_borders: None,
line_numbers: None,
vertical_alignment: None,
text_direction: None,
doc_grid: None,
title_pg: None,
header_refs: Vec::new(),
footer_refs: Vec::new(),
extra_xml: Vec::new(),
extra_xml_positions: Vec::new(),
change: None,
}
}
#[doc(hidden)]
pub fn raw_position_is_root_attributes(position: &CT_SectPrRawPosition, raw: &[u8]) -> bool {
matches!(
position,
CT_SectPrRawPosition::Schema {
slot: usize::MAX,
occurrence: usize::MAX
}
) && is_root_attribute_record(raw)
}
fn from_empty_root(root: &BytesStart<'_>, word_prefixes: &[String]) -> Result<Self> {
let mut section = Self::empty();
if let Some(record) = capture_root_attribute_record(root, word_prefixes)? {
section.extra_xml.push(record);
section
.extra_xml_positions
.push(CT_SectPrRawPosition::Schema {
slot: usize::MAX,
occurrence: usize::MAX,
});
}
Ok(section)
}
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,
page_number: None,
footnote_pr: None,
endnote_pr: None,
paper_source: None,
page_borders: None,
line_numbers: None,
vertical_alignment: None,
text_direction: None,
doc_grid: None,
title_pg: None,
header_refs: Vec::new(),
footer_refs: Vec::new(),
extra_xml: Vec::new(),
extra_xml_positions: Vec::new(),
change: None,
}
}
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,
page_number: None,
footnote_pr: None,
endnote_pr: None,
paper_source: None,
page_borders: None,
line_numbers: None,
vertical_alignment: None,
text_direction: None,
doc_grid: None,
title_pg: None,
header_refs: Vec::new(),
footer_refs: Vec::new(),
extra_xml: Vec::new(),
extra_xml_positions: Vec::new(),
change: None,
}
}
pub fn from_xml(reader: &mut Reader<&[u8]>) -> Result<Self> {
Self::from_xml_with_prefixes(reader, &["w".to_owned()])
}
pub(crate) fn from_xml_with_prefixes(
reader: &mut Reader<&[u8]>,
word_prefixes: &[String],
) -> Result<Self> {
Self::from_xml_with_prefixes_and_owner_bindings(reader, word_prefixes, &[])
}
pub(crate) fn from_xml_with_prefixes_and_owner_bindings(
reader: &mut Reader<&[u8]>,
word_prefixes: &[String],
owner_bindings: &[(String, String)],
) -> Result<Self> {
Self::from_xml_with_prefixes_owner_bindings_and_root(
reader,
word_prefixes,
owner_bindings,
None,
)
}
#[doc(hidden)]
pub fn from_xml_fragment_checked(xml: &[u8]) -> Result<Self> {
admit_word_xml(xml, &[], None)?;
crate::text::validate_word_owner_fragment(xml, b"sectPr", true)?;
let mut reader = Reader::from_reader(xml);
let mut buffer = Vec::new();
loop {
match reader.read_event_into(&mut buffer)? {
Event::Start(root) => {
let prefixes = word_prefixes_at(&root, &[])?;
let bindings = local_namespace_overrides(&root, &prefixes)?;
return Self::from_xml_with_prefixes_owner_bindings_and_root(
&mut reader,
&prefixes,
&bindings,
Some(&root),
);
}
Event::Empty(root) => {
let prefixes = word_prefixes_at(&root, &[])?;
return Self::from_empty_root(&root, &prefixes);
}
_ => {}
}
buffer.clear();
}
}
pub(crate) fn from_xml_with_prefixes_owner_bindings_and_root(
reader: &mut Reader<&[u8]>,
word_prefixes: &[String],
owner_bindings: &[(String, String)],
root: Option<&BytesStart<'_>>,
) -> Result<Self> {
let raw_owner_bindings =
crate::text::bindings_outside_retained_root(owner_bindings, root, word_prefixes)?;
let mut sect = Self::empty();
let mut change_raw_index = 0usize;
let mut raw_position = (0usize, 0usize);
let mut header_count = 0usize;
let mut footer_count = 0usize;
let mut buf = Vec::new();
loop {
match reader.read_event_into(&mut buf) {
Ok(Event::Empty(ref e)) => {
let name = e.name();
let prefixes = word_prefixes_at(e, word_prefixes)?;
if is_word_element(name.as_ref(), b"pgSz", &prefixes) {
Self::parse_page_size_attributes(&mut sect, e, &prefixes)?;
raw_position = (4, 0);
} else if is_word_element(name.as_ref(), b"pgMar", &prefixes) {
Self::parse_page_margin_attributes(&mut sect, e, &prefixes)?;
raw_position = (5, 0);
} else if is_word_element(name.as_ref(), b"type", &prefixes) {
if let Some(val) = get_word_val_attr(e, &prefixes)? {
sect.section_type = ST_SectionType::from_str(&val).ok();
}
raw_position = (3, 0);
} else if is_word_element(name.as_ref(), b"pgNumType", &prefixes) {
let raw = crate::text::raw_with_external_bindings(
&capture_empty_element(e)?,
&raw_owner_bindings,
)?;
if sect.page_number.is_none() {
sect.page_number =
Some(Self::parse_page_number(e, &prefixes, owner_bindings, raw)?);
} else {
sect.push_extra_xml(raw, 6, 1);
}
raw_position = (6, 1);
} else if is_word_element(name.as_ref(), b"cols", &prefixes) {
sect.columns = Some(Self::parse_cols_empty(e, &prefixes)?);
raw_position = (7, 0);
} else if is_word_element(name.as_ref(), b"footnotePr", &prefixes) {
if sect.footnote_pr.is_none() && e.attributes().next().is_none() {
sect.footnote_pr = Some(Box::default());
} else {
let raw = crate::text::raw_with_external_bindings(
&capture_empty_element(e)?,
&raw_owner_bindings,
)?;
sect.push_extra_xml(raw, 2, 0);
}
raw_position = (2, 0);
} else if is_word_element(name.as_ref(), b"endnotePr", &prefixes) {
if sect.endnote_pr.is_none() && e.attributes().next().is_none() {
sect.endnote_pr = Some(Box::default());
} else {
let raw = crate::text::raw_with_external_bindings(
&capture_empty_element(e)?,
&raw_owner_bindings,
)?;
sect.push_extra_xml(raw, 2, 1);
}
raw_position = (2, 1);
} else if is_word_element(name.as_ref(), b"paperSrc", &prefixes) {
if sect.paper_source.is_none() {
sect.paper_source =
Some(Box::new(Self::parse_paper_source(e, &prefixes)?));
} else {
let raw = crate::text::raw_with_external_bindings(
&capture_empty_element(e)?,
&raw_owner_bindings,
)?;
sect.push_extra_xml(raw, 5, 1);
}
raw_position = (5, 1);
} else if is_word_element(name.as_ref(), b"pgBorders", &prefixes) {
if sect.page_borders.is_none() {
sect.page_borders =
Some(Box::new(Self::parse_page_border_attrs(e, &prefixes)?));
} else {
let raw = crate::text::raw_with_external_bindings(
&capture_empty_element(e)?,
&raw_owner_bindings,
)?;
sect.push_extra_xml(raw, 5, 2);
}
raw_position = (5, 2);
} else if is_word_element(name.as_ref(), b"lnNumType", &prefixes) {
if sect.line_numbers.is_none() {
sect.line_numbers =
Some(Box::new(Self::parse_line_number(e, &prefixes)?));
} else {
let raw = crate::text::raw_with_external_bindings(
&capture_empty_element(e)?,
&raw_owner_bindings,
)?;
sect.push_extra_xml(raw, 5, 3);
}
raw_position = (5, 3);
} else if is_word_element(name.as_ref(), b"vAlign", &prefixes) {
let typed = sole_val_attribute(e, &prefixes)?
.filter(|value| ST_VerticalJc::from_str(value).to_str() == value)
.map(|value| ST_VerticalJc::from_str(&value));
match typed {
Some(value) if sect.vertical_alignment.is_none() => {
sect.vertical_alignment = Some(value);
}
_ => {
let raw = crate::text::raw_with_external_bindings(
&capture_empty_element(e)?,
&raw_owner_bindings,
)?;
sect.push_extra_xml(raw, 7, 0);
}
}
raw_position = (7, 1);
} else if is_word_element(name.as_ref(), b"textDirection", &prefixes) {
match sole_val_attribute(e, &prefixes)? {
Some(value) if sect.text_direction.is_none() => {
sect.text_direction = Some(value);
}
_ => {
let raw = crate::text::raw_with_external_bindings(
&capture_empty_element(e)?,
&raw_owner_bindings,
)?;
sect.push_extra_xml(raw, 8, 0);
}
}
raw_position = (8, 1);
} else if is_word_element(name.as_ref(), b"docGrid", &prefixes) {
if sect.doc_grid.is_none() {
sect.doc_grid = Some(Box::new(Self::parse_doc_grid(e, &prefixes)?));
} else {
let raw = crate::text::raw_with_external_bindings(
&capture_empty_element(e)?,
&raw_owner_bindings,
)?;
sect.push_extra_xml(raw, 8, 2);
}
raw_position = (8, 2);
} else if is_word_element(name.as_ref(), b"headerReference", &prefixes) {
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 is_word_attribute(key, b"type", &prefixes) {
hdr_type = HdrFtrType::from_str(val);
} else if attribute_in_namespace(
key,
b"id",
crate::namespace::R_NS,
&prefixes,
) {
rel_id = val.to_string();
}
}
if !rel_id.is_empty() {
sect.header_refs.push(HdrFtrRef {
hdr_ftr_type: hdr_type,
rel_id,
});
header_count += 1;
}
raw_position = (0, header_count);
} else if is_word_element(name.as_ref(), b"footerReference", &prefixes) {
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 is_word_attribute(key, b"type", &prefixes) {
ftr_type = HdrFtrType::from_str(val);
} else if attribute_in_namespace(
key,
b"id",
crate::namespace::R_NS,
&prefixes,
) {
rel_id = val.to_string();
}
}
if !rel_id.is_empty() {
sect.footer_refs.push(HdrFtrRef {
hdr_ftr_type: ftr_type,
rel_id,
});
footer_count += 1;
}
raw_position = (1, footer_count);
} else if is_word_element(name.as_ref(), b"titlePg", &prefixes) {
sect.title_pg = Some(
get_word_val_attr(e, &prefixes)?
.is_none_or(|value| value != "0" && value != "false"),
);
raw_position = (8, 0);
} else if is_word_element(name.as_ref(), b"sectPrChange", &prefixes) {
let raw = crate::text::raw_with_external_bindings(
&capture_empty_element(e)?,
&raw_owner_bindings,
)?;
if let Some(revision) = CT_Revision::from_raw(raw.clone(), &prefixes) {
if let Some(previous) = sect.change.replace(revision) {
sect.extra_xml
.insert(change_raw_index, previous.into_raw_xml());
sect.extra_xml_positions.insert(
change_raw_index,
CT_SectPrRawPosition::Schema {
slot: 9,
occurrence: 0,
},
);
}
change_raw_index = sect.extra_xml.len();
} else {
sect.push_extra_xml(raw, 9, 0);
}
raw_position = (9, 0);
} else {
let position = Self::raw_child_schema_slot(name.as_ref(), &prefixes)
.unwrap_or(raw_position);
let raw = crate::text::raw_with_external_bindings(
&capture_empty_element(e)?,
&raw_owner_bindings,
)?;
sect.push_extra_xml(raw, position.0, position.1);
raw_position = position;
}
}
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"pgSz", &prefixes) {
let raw = crate::text::raw_with_external_bindings(
&capture_element(reader, e)?,
&raw_owner_bindings,
)?;
if raw_element_has_child_content(&raw)?
|| !page_size_attributes_are_modeled(e, &prefixes)?
{
sect.push_extra_xml(raw, 4, 0);
} else {
Self::parse_page_size_attributes(&mut sect, e, &prefixes)?;
}
raw_position = (4, 0);
} else if is_word_element(name.as_ref(), b"pgMar", &prefixes) {
let raw = crate::text::raw_with_external_bindings(
&capture_element(reader, e)?,
&raw_owner_bindings,
)?;
if raw_element_has_child_content(&raw)?
|| !page_margin_attributes_are_modeled(e, &prefixes)?
{
sect.push_extra_xml(raw, 5, 0);
} else {
Self::parse_page_margin_attributes(&mut sect, e, &prefixes)?;
}
raw_position = (5, 0);
} else if is_word_element(name.as_ref(), b"headerReference", &prefixes) {
let raw = crate::text::raw_with_external_bindings(
&capture_element(reader, e)?,
&raw_owner_bindings,
)?;
if raw_element_has_child_content(&raw)?
|| !story_reference_attributes_are_modeled(e, &prefixes)?
{
sect.push_extra_xml(raw, 0, header_count);
} else if let Some(reference) = Self::parse_story_reference(e, &prefixes)? {
sect.header_refs.push(reference);
header_count += 1;
}
raw_position = (0, header_count);
} else if is_word_element(name.as_ref(), b"footerReference", &prefixes) {
let raw = crate::text::raw_with_external_bindings(
&capture_element(reader, e)?,
&raw_owner_bindings,
)?;
if raw_element_has_child_content(&raw)?
|| !story_reference_attributes_are_modeled(e, &prefixes)?
{
sect.push_extra_xml(raw, 1, footer_count);
} else if let Some(reference) = Self::parse_story_reference(e, &prefixes)? {
sect.footer_refs.push(reference);
footer_count += 1;
}
raw_position = (1, footer_count);
} else if is_word_element(name.as_ref(), b"cols", &prefixes) {
sect.columns = Some(Self::parse_cols_start(reader, e, &prefixes)?);
raw_position = (7, 0);
} else if is_word_element(name.as_ref(), b"footnotePr", &prefixes) {
if sect.footnote_pr.is_none() && e.attributes().next().is_none() {
sect.footnote_pr = Some(Box::new(Self::parse_note_properties(
reader,
&prefixes,
owner_bindings,
b"footnotePr",
)?));
} else {
let raw = crate::text::raw_with_external_bindings(
&capture_element(reader, e)?,
&raw_owner_bindings,
)?;
sect.push_extra_xml(raw, 2, 0);
}
raw_position = (2, 0);
} else if is_word_element(name.as_ref(), b"endnotePr", &prefixes) {
if sect.endnote_pr.is_none() && e.attributes().next().is_none() {
sect.endnote_pr = Some(Box::new(Self::parse_note_properties(
reader,
&prefixes,
owner_bindings,
b"endnotePr",
)?));
} else {
let raw = crate::text::raw_with_external_bindings(
&capture_element(reader, e)?,
&raw_owner_bindings,
)?;
sect.push_extra_xml(raw, 2, 1);
}
raw_position = (2, 1);
} else if is_word_element(name.as_ref(), b"pgBorders", &prefixes) {
if sect.page_borders.is_none() {
sect.page_borders = Some(Box::new(Self::parse_page_borders(
reader,
e,
&prefixes,
owner_bindings,
)?));
} else {
let raw = crate::text::raw_with_external_bindings(
&capture_element(reader, e)?,
&raw_owner_bindings,
)?;
sect.push_extra_xml(raw, 5, 2);
}
raw_position = (5, 2);
} else if is_word_element(name.as_ref(), b"paperSrc", &prefixes) {
let raw = crate::text::raw_with_external_bindings(
&capture_element(reader, e)?,
&raw_owner_bindings,
)?;
sect.push_extra_xml(raw, 5, 0);
raw_position = (5, 1);
} else if is_word_element(name.as_ref(), b"lnNumType", &prefixes) {
let raw = crate::text::raw_with_external_bindings(
&capture_element(reader, e)?,
&raw_owner_bindings,
)?;
sect.push_extra_xml(raw, 5, 2);
raw_position = (5, 3);
} else if is_word_element(name.as_ref(), b"vAlign", &prefixes) {
let raw = crate::text::raw_with_external_bindings(
&capture_element(reader, e)?,
&raw_owner_bindings,
)?;
sect.push_extra_xml(raw, 7, 0);
raw_position = (7, 1);
} else if is_word_element(name.as_ref(), b"textDirection", &prefixes) {
let raw = crate::text::raw_with_external_bindings(
&capture_element(reader, e)?,
&raw_owner_bindings,
)?;
sect.push_extra_xml(raw, 8, 0);
raw_position = (8, 1);
} else if is_word_element(name.as_ref(), b"docGrid", &prefixes) {
let raw = crate::text::raw_with_external_bindings(
&capture_element(reader, e)?,
&raw_owner_bindings,
)?;
sect.push_extra_xml(raw, 8, 1);
raw_position = (8, 2);
} else if is_word_element(name.as_ref(), b"pgNumType", &prefixes) {
let raw = crate::text::raw_with_external_bindings(
&capture_element(reader, e)?,
&raw_owner_bindings,
)?;
if sect.page_number.is_none() {
sect.page_number =
Some(Self::parse_page_number(e, &prefixes, owner_bindings, raw)?);
} else {
sect.push_extra_xml(raw, 6, 1);
}
raw_position = (6, 1);
} else if is_word_element(name.as_ref(), b"sectPrChange", &prefixes) {
let raw = crate::text::raw_with_external_bindings(
&capture_element(reader, e)?,
&raw_owner_bindings,
)?;
if let Some(revision) = CT_Revision::from_raw(raw.clone(), &prefixes) {
if let Some(previous) = sect.change.replace(revision) {
sect.extra_xml
.insert(change_raw_index, previous.into_raw_xml());
sect.extra_xml_positions.insert(
change_raw_index,
CT_SectPrRawPosition::Schema {
slot: 9,
occurrence: 0,
},
);
}
change_raw_index = sect.extra_xml.len();
} else {
sect.push_extra_xml(raw, 9, 0);
}
raw_position = (9, 0);
} else {
let position = Self::raw_child_schema_slot(name.as_ref(), &prefixes)
.unwrap_or(raw_position);
let raw = crate::text::raw_with_external_bindings(
&capture_element(reader, e)?,
&raw_owner_bindings,
)?;
sect.push_extra_xml(raw, position.0, position.1);
raw_position = position;
}
}
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();
}
if let Some(record) = root
.map(|root| capture_root_attribute_record(root, word_prefixes))
.transpose()?
.flatten()
{
sect.extra_xml.push(record);
sect.extra_xml_positions.push(CT_SectPrRawPosition::Schema {
slot: usize::MAX,
occurrence: usize::MAX,
});
}
sect.bind_story_reference_positions();
Ok(sect)
}
fn parse_page_size_attributes(
sect: &mut Self,
element: &BytesStart<'_>,
word_prefixes: &[String],
) -> Result<()> {
for attribute in element.attributes() {
let attribute = attribute?;
let key = attribute.key.as_ref();
let value = std::str::from_utf8(&attribute.value)?;
let measurement = || parse_integer_measurement(element.name().as_ref(), key, value);
if is_word_attribute(key, b"w", word_prefixes) {
sect.page_width = Some(Twips(measurement()?));
} else if is_word_attribute(key, b"h", word_prefixes) {
sect.page_height = Some(Twips(measurement()?));
} else if is_word_attribute(key, b"orient", word_prefixes) {
sect.orientation = ST_PageOrientation::from_str(value).ok();
}
}
Ok(())
}
fn parse_page_margin_attributes(
sect: &mut Self,
element: &BytesStart<'_>,
word_prefixes: &[String],
) -> Result<()> {
for attribute in element.attributes() {
let attribute = attribute?;
let key = attribute.key.as_ref();
let field = if is_word_attribute(key, b"top", word_prefixes) {
&mut sect.margin_top
} else if is_word_attribute(key, b"right", word_prefixes)
|| is_word_attribute(key, b"end", word_prefixes)
{
&mut sect.margin_right
} else if is_word_attribute(key, b"bottom", word_prefixes) {
&mut sect.margin_bottom
} else if is_word_attribute(key, b"left", word_prefixes)
|| is_word_attribute(key, b"start", word_prefixes)
{
&mut sect.margin_left
} else if is_word_attribute(key, b"gutter", word_prefixes) {
&mut sect.gutter
} else if is_word_attribute(key, b"header", word_prefixes) {
&mut sect.header_distance
} else if is_word_attribute(key, b"footer", word_prefixes) {
&mut sect.footer_distance
} else {
continue;
};
let value = std::str::from_utf8(&attribute.value)?;
*field = Some(Twips(parse_integer_measurement(
element.name().as_ref(),
key,
value,
)?));
}
Ok(())
}
fn parse_story_reference(
element: &BytesStart<'_>,
word_prefixes: &[String],
) -> Result<Option<HdrFtrRef>> {
let mut hdr_ftr_type = HdrFtrType::Default;
let mut relationship_id = None;
for attribute in element.attributes() {
let attribute = attribute?;
let value = attribute
.decoded_and_normalized_value(XmlVersion::Implicit1_0, element.decoder())?
.into_owned();
if is_word_attribute(attribute.key.as_ref(), b"type", word_prefixes) {
hdr_ftr_type = HdrFtrType::from_str(&value);
} else if attribute_in_namespace(
attribute.key.as_ref(),
b"id",
crate::namespace::R_NS,
word_prefixes,
) {
relationship_id = Some(value);
}
}
Ok(relationship_id.map(|rel_id| HdrFtrRef {
hdr_ftr_type,
rel_id,
}))
}
fn parse_page_number(
e: &BytesStart<'_>,
word_prefixes: &[String],
owner_bindings: &[(String, String)],
raw_xml: Vec<u8>,
) -> Result<CT_PageNumberType> {
let mut start = None;
let mut start_attribute_name = None;
for attr in e.attributes() {
let attr = attr?;
if is_word_attribute(attr.key.as_ref(), b"start", word_prefixes) {
start = attr
.decoded_and_normalized_value(XmlVersion::Implicit1_0, e.decoder())?
.parse()
.ok();
start_attribute_name = Some(attr.key.as_ref().to_vec());
}
}
let element_name = e.name();
let element_prefix = element_name
.as_ref()
.iter()
.position(|byte| *byte == b':')
.map(|separator| &element_name.as_ref()[..separator]);
let start_insertion_prefix = element_prefix
.filter(|prefix| {
!prefix.is_empty()
&& word_prefixes
.iter()
.any(|candidate| candidate.as_bytes() == *prefix)
})
.or_else(|| {
word_prefixes
.iter()
.find(|prefix| !prefix.is_empty() && !prefix.starts_with('\0'))
.map(String::as_bytes)
})
.map(<[u8]>::to_vec);
let mut reserved_insertion_prefixes = owner_bindings
.iter()
.map(|(prefix, _)| prefix.as_bytes().to_vec())
.collect::<Vec<_>>();
reserved_insertion_prefixes.extend(
namespace_bindings(word_prefixes)
.into_iter()
.map(|(prefix, _)| prefix.into_bytes()),
);
reserved_insertion_prefixes.sort();
reserved_insertion_prefixes.dedup();
Ok(CT_PageNumberType {
start,
raw_xml: Some(raw_xml),
parsed_start: start,
start_attribute_name,
start_insertion_prefix,
reserved_insertion_prefixes,
})
}
fn push_extra_xml(&mut self, raw: Vec<u8>, slot: usize, occurrence: usize) {
self.extra_xml.push(raw);
self.extra_xml_positions
.push(CT_SectPrRawPosition::Schema { slot, occurrence });
}
fn bind_story_reference_positions(&mut self) {
for position in &mut self.extra_xml_positions {
let (slot, occurrence) = match position {
CT_SectPrRawPosition::Schema { slot, occurrence } => (*slot, *occurrence),
_ => continue,
};
let (references, is_header) = match slot {
0 => (&self.header_refs, true),
1 => (&self.footer_refs, false),
_ => continue,
};
let preceding = occurrence
.checked_sub(1)
.and_then(|index| CT_SectPrReferenceAnchor::at(references, index));
let following = CT_SectPrReferenceAnchor::at(references, occurrence);
*position = if is_header {
CT_SectPrRawPosition::Header {
preceding,
following,
}
} else {
CT_SectPrRawPosition::Footer {
preceding,
following,
}
};
}
}
fn raw_child_schema_slot(name: &[u8], word_prefixes: &[String]) -> Option<(usize, usize)> {
let local = name.rsplit(|byte| *byte == b':').next().unwrap_or(name);
if !is_word_element(name, local, word_prefixes) {
return None;
}
match local {
b"formProt" => Some((7, 0)),
b"noEndnote" => Some((7, 1)),
b"bidi" | b"rtlGutter" => Some((8, 1)),
b"printerSettings" => Some((8, 2)),
_ => None,
}
}
fn parse_doc_grid(e: &BytesStart, word_prefixes: &[String]) -> Result<CT_DocGrid> {
let mut grid = CT_DocGrid::default();
for attribute in e.attributes() {
let attribute = attribute?;
let key = attribute.key.as_ref();
let value = std::str::from_utf8(&attribute.value)?;
let modeled = if is_word_attribute(key, b"type", word_prefixes) {
match ST_DocGrid::from_str(value) {
Some(grid_type) => {
grid.grid_type = Some(grid_type);
true
}
None => false,
}
} else if is_word_attribute(key, b"linePitch", word_prefixes) {
match value.parse() {
Ok(parsed) => {
grid.line_pitch = Some(Twips(parsed));
true
}
Err(_) => false,
}
} else if is_word_attribute(key, b"charSpace", word_prefixes) {
match value.parse() {
Ok(parsed) => {
grid.char_space = Some(parsed);
true
}
Err(_) => false,
}
} else {
false
};
if !modeled {
grid.extra_attributes
.push((std::str::from_utf8(key)?.to_owned(), value.to_owned()));
}
}
Ok(grid)
}
fn parse_paper_source(e: &BytesStart, word_prefixes: &[String]) -> Result<CT_PaperSource> {
let mut source = CT_PaperSource::default();
for attribute in e.attributes() {
let attribute = attribute?;
let key = attribute.key.as_ref();
let value = std::str::from_utf8(&attribute.value)?;
if is_word_attribute(key, b"first", word_prefixes) {
store_unsigned_attribute(
value,
key,
&mut source.first,
&mut source.extra_attributes,
)?;
} else if is_word_attribute(key, b"other", word_prefixes) {
store_unsigned_attribute(
value,
key,
&mut source.other,
&mut source.extra_attributes,
)?;
} else {
source
.extra_attributes
.push((std::str::from_utf8(key)?.to_owned(), value.to_owned()));
}
}
Ok(source)
}
fn parse_line_number(e: &BytesStart, word_prefixes: &[String]) -> Result<CT_LineNumber> {
let mut numbering = CT_LineNumber::default();
for attribute in e.attributes() {
let attribute = attribute?;
let key = attribute.key.as_ref();
let value = std::str::from_utf8(&attribute.value)?;
if is_word_attribute(key, b"countBy", word_prefixes) {
store_unsigned_attribute(
value,
key,
&mut numbering.count_by,
&mut numbering.extra_attributes,
)?;
} else if is_word_attribute(key, b"start", word_prefixes) {
store_unsigned_attribute(
value,
key,
&mut numbering.start,
&mut numbering.extra_attributes,
)?;
} else if is_word_attribute(key, b"distance", word_prefixes) {
match value.parse() {
Ok(parsed) => numbering.distance = Some(Twips(parsed)),
Err(_) => numbering
.extra_attributes
.push((std::str::from_utf8(key)?.to_owned(), value.to_owned())),
}
} else if is_word_attribute(key, b"restart", word_prefixes) {
match ST_LineNumberRestart::from_str(value) {
Some(parsed) => numbering.restart = Some(parsed),
None => numbering
.extra_attributes
.push((std::str::from_utf8(key)?.to_owned(), value.to_owned())),
}
} else {
numbering
.extra_attributes
.push((std::str::from_utf8(key)?.to_owned(), value.to_owned()));
}
}
Ok(numbering)
}
fn parse_page_border_attrs(e: &BytesStart, word_prefixes: &[String]) -> Result<CT_PageBorders> {
let mut borders = CT_PageBorders::default();
for attribute in e.attributes() {
let attribute = attribute?;
let key = attribute.key.as_ref();
let value = std::str::from_utf8(&attribute.value)?;
let retained = if is_word_attribute(key, b"zOrder", word_prefixes) {
borders.z_order = ST_PageBorderZOrder::from_str(value);
borders.z_order.is_none()
} else if is_word_attribute(key, b"display", word_prefixes) {
borders.display = ST_PageBorderDisplay::from_str(value);
borders.display.is_none()
} else if is_word_attribute(key, b"offsetFrom", word_prefixes) {
borders.offset_from = ST_PageBorderOffset::from_str(value);
borders.offset_from.is_none()
} else {
true
};
if retained {
borders
.extra_attributes
.push((std::str::from_utf8(key)?.to_owned(), value.to_owned()));
}
}
Ok(borders)
}
fn parse_page_borders(
reader: &mut Reader<&[u8]>,
e: &BytesStart,
word_prefixes: &[String],
owner_bindings: &[(String, String)],
) -> Result<CT_PageBorders> {
let mut borders = Self::parse_page_border_attrs(e, word_prefixes)?;
let mut buf = Vec::new();
loop {
match reader.read_event_into(&mut buf) {
Ok(Event::Empty(ref child)) => {
let name = child.name();
let prefixes = word_prefixes_at(child, word_prefixes)?;
let edge = |child: &BytesStart| {
CT_BorderEdge::from_xml_attrs_with_prefixes(child, &prefixes)
};
let slot = if is_word_element(name.as_ref(), b"top", &prefixes) {
Some(&mut borders.top)
} else if is_word_element(name.as_ref(), b"left", &prefixes) {
Some(&mut borders.left)
} else if is_word_element(name.as_ref(), b"bottom", &prefixes) {
Some(&mut borders.bottom)
} else if is_word_element(name.as_ref(), b"right", &prefixes) {
Some(&mut borders.right)
} else {
None
};
if let Some(slot) = slot {
if slot.is_some() {
return Err(OxmlError::InvalidValue(
"duplicate recognized page border edge".into(),
));
}
*slot = Some(edge(child)?);
} else {
borders
.extra_xml
.push(crate::text::raw_with_external_bindings(
&capture_empty_element(child)?,
owner_bindings,
)?);
}
}
Ok(Event::Start(ref child)) => {
let prefixes = word_prefixes_at(child, word_prefixes)?;
let name = child.name();
let slot = if is_word_element(name.as_ref(), b"top", &prefixes) {
Some(&mut borders.top)
} else if is_word_element(name.as_ref(), b"left", &prefixes) {
Some(&mut borders.left)
} else if is_word_element(name.as_ref(), b"bottom", &prefixes) {
Some(&mut borders.bottom)
} else if is_word_element(name.as_ref(), b"right", &prefixes) {
Some(&mut borders.right)
} else {
None
};
let mut edge = if slot.is_some() {
Some(CT_BorderEdge::from_xml_attrs_with_prefixes(
child, &prefixes,
)?)
} else {
None
};
let mut bindings = owner_bindings.to_vec();
for (prefix, uri) in namespace_bindings(&prefixes) {
bindings.retain(|(name, _)| name != &prefix);
bindings.push((prefix, uri));
}
for prefix in prefixes.iter().filter(|prefix| !prefix.starts_with('\0')) {
if !bindings.iter().any(|(name, _)| name == prefix) {
bindings.push((
prefix.clone(),
"http://schemas.openxmlformats.org/wordprocessingml/2006/main"
.into(),
));
}
}
let raw = crate::text::raw_with_external_bindings(
&capture_element(reader, child)?,
if slot.is_some() {
&bindings
} else {
owner_bindings
},
)?;
if let Some(slot) = slot {
if slot.is_some() {
return Err(OxmlError::InvalidValue(
"duplicate recognized page border edge".into(),
));
}
let mut edge = edge.take().unwrap();
edge.source_xml = Some(raw);
*slot = Some(edge);
} else {
borders.extra_xml.push(raw);
}
}
Ok(Event::End(ref end))
if matches_local_name(end.name().as_ref(), b"pgBorders") =>
{
break;
}
Ok(Event::Eof) => break,
Err(error) => return Err(error.into()),
_ => {}
}
buf.clear();
}
Ok(borders)
}
pub(crate) fn parse_note_properties(
reader: &mut Reader<&[u8]>,
word_prefixes: &[String],
owner_bindings: &[(String, String)],
tag: &[u8],
) -> Result<CT_NoteProperties> {
let mut properties = CT_NoteProperties::default();
let mut slot = 0usize;
let mut buf = Vec::new();
loop {
match reader.read_event_into(&mut buf) {
Ok(Event::Empty(ref child)) => {
let name = child.name();
let prefixes = word_prefixes_at(child, word_prefixes)?;
let modeled = if is_word_element(name.as_ref(), b"pos", &prefixes) {
slot = 1;
sole_val_attribute(child, &prefixes)?
.map(|value| properties.pos = Some(value))
} else if is_word_element(name.as_ref(), b"numFmt", &prefixes) {
slot = 2;
sole_val_attribute(child, &prefixes)?
.map(|value| properties.num_fmt = Some(value))
} else if is_word_element(name.as_ref(), b"numStart", &prefixes) {
slot = 3;
sole_val_attribute(child, &prefixes)?
.and_then(|value| value.parse().ok())
.map(|value| properties.num_start = Some(value))
} else if is_word_element(name.as_ref(), b"numRestart", &prefixes) {
slot = 4;
sole_val_attribute(child, &prefixes)?
.map(|value| properties.num_restart = Some(value))
} else if is_word_element(
name.as_ref(),
if matches_local_name(tag, b"footnotePr") {
b"footnote".as_slice()
} else {
b"endnote".as_slice()
},
&prefixes,
) {
slot = 5;
child
.attributes()
.filter_map(|attribute| attribute.ok())
.find(|attribute| {
is_word_attribute(attribute.key.as_ref(), b"id", &prefixes)
})
.and_then(|attribute| {
std::str::from_utf8(&attribute.value)
.ok()
.and_then(|value| value.parse::<i32>().ok())
})
.map(|id| properties.special_references.push(id))
} else {
None
};
if modeled.is_none() {
properties.extra_xml.push((
slot,
crate::text::raw_with_external_bindings(
&capture_empty_element(child)?,
owner_bindings,
)?,
));
}
}
Ok(Event::Start(ref child)) => {
properties.extra_xml.push((
slot,
crate::text::raw_with_external_bindings(
&capture_element(reader, child)?,
owner_bindings,
)?,
));
}
Ok(Event::End(ref end)) if matches_local_name(end.name().as_ref(), tag) => break,
Ok(Event::Eof) => break,
Err(error) => return Err(error.into()),
_ => {}
}
buf.clear();
}
Ok(properties)
}
fn parse_cols_attrs(e: &BytesStart, word_prefixes: &[String]) -> 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 is_word_attribute(key, b"num", word_prefixes) {
cols.num = Some(val_str.parse()?);
} else if is_word_attribute(key, b"space", word_prefixes) {
cols.space = Some(Twips(parse_integer_measurement(
e.name().as_ref(),
key,
val_str,
)?));
} else if is_word_attribute(key, b"equalWidth", word_prefixes) {
cols.equal_width = Some(val_str == "1" || val_str == "true");
} else if is_word_attribute(key, b"sep", word_prefixes) {
cols.sep = Some(val_str == "1" || val_str == "true");
}
}
Ok(cols)
}
fn parse_cols_empty(e: &BytesStart, word_prefixes: &[String]) -> Result<CT_Columns> {
Self::parse_cols_attrs(e, word_prefixes)
}
fn parse_cols_start(
reader: &mut Reader<&[u8]>,
e: &BytesStart,
word_prefixes: &[String],
) -> Result<CT_Columns> {
let mut cols = Self::parse_cols_attrs(e, word_prefixes)?;
let mut buf = Vec::new();
loop {
match reader.read_event_into(&mut buf) {
Ok(Event::Empty(ref e)) => {
let prefixes = word_prefixes_at(e, word_prefixes)?;
if !is_word_element(e.name().as_ref(), b"col", &prefixes) {
buf.clear();
continue;
}
let mut width = None;
let mut space = None;
for attr in e.attributes() {
let attr = attr?;
let key = attr.key.as_ref();
let value = std::str::from_utf8(&attr.value)?;
let measurement =
|| parse_integer_measurement(e.name().as_ref(), key, value);
if is_word_attribute(key, b"w", &prefixes) {
width = Some(Twips(measurement()?));
} else if is_word_attribute(key, b"space", &prefixes) {
space = Some(Twips(measurement()?));
}
}
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();
let mut root = BytesStart::new("w:sectPr");
for (raw, position) in self.extra_xml.iter().zip(&self.extra_xml_positions) {
if Self::raw_position_is_root_attributes(position, raw) {
push_root_attribute_record(&mut root, raw, None)?;
}
}
writer.write_event(Event::Start(root))?;
let ordered_raw = self.extra_xml_positions.len() == self.extra_xml.len();
if ordered_raw {
self.write_story_reference_boundary(writer, &self.header_refs, true, 0)?;
}
for (index, hdr) in self.header_refs.iter().enumerate() {
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))?;
if ordered_raw {
self.write_story_reference_boundary(writer, &self.header_refs, true, index + 1)?;
}
}
if ordered_raw {
self.write_story_reference_boundary(writer, &self.footer_refs, false, 0)?;
}
for (index, ftr) in self.footer_refs.iter().enumerate() {
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 ordered_raw {
self.write_story_reference_boundary(writer, &self.footer_refs, false, index + 1)?;
}
}
if let Some(footnote_pr) = &self.footnote_pr {
footnote_pr.to_xml(writer, "w:footnotePr")?;
}
if ordered_raw {
self.write_raw_position(writer, 2, 0)?;
}
if let Some(endnote_pr) = &self.endnote_pr {
endnote_pr.to_xml(writer, "w:endnotePr")?;
}
if ordered_raw {
self.write_raw_position(writer, 2, 1)?;
}
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 ordered_raw {
self.write_raw_position(writer, 3, 0)?;
}
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 ordered_raw {
self.write_raw_position(writer, 4, 0)?;
}
if self.margin_top.is_some()
|| self.margin_right.is_some()
|| self.margin_bottom.is_some()
|| self.margin_left.is_some()
|| self.gutter.is_some()
|| self.header_distance.is_some()
|| self.footer_distance.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 ordered_raw {
self.write_raw_position(writer, 5, 0)?;
}
if let Some(paper_source) = &self.paper_source {
let mut e = BytesStart::new("w:paperSrc");
push_retained_attributes(&mut e, &paper_source.extra_attributes);
if let Some(first) = paper_source.first {
e.push_attribute(("w:first", buf.format(first)));
}
if let Some(other) = paper_source.other {
e.push_attribute(("w:other", buf.format(other)));
}
writer.write_event(Event::Empty(e))?;
}
if ordered_raw {
self.write_raw_position(writer, 5, 1)?;
}
if let Some(page_borders) = &self.page_borders {
page_borders.to_xml(writer)?;
}
if ordered_raw {
self.write_raw_position(writer, 5, 2)?;
}
if let Some(line_numbers) = &self.line_numbers {
let mut e = BytesStart::new("w:lnNumType");
push_retained_attributes(&mut e, &line_numbers.extra_attributes);
if let Some(count_by) = line_numbers.count_by {
e.push_attribute(("w:countBy", buf.format(count_by)));
}
if let Some(start) = line_numbers.start {
e.push_attribute(("w:start", buf.format(start)));
}
if let Some(distance) = line_numbers.distance {
e.push_attribute(("w:distance", buf.format(distance.0)));
}
if let Some(restart) = line_numbers.restart {
e.push_attribute(("w:restart", restart.to_str()));
}
writer.write_event(Event::Empty(e))?;
}
if ordered_raw {
self.write_raw_position(writer, 5, 3)?;
}
if let Some(page_number) = &self.page_number {
page_number.to_xml(writer)?;
}
if ordered_raw {
self.write_raw_position(writer, 6, 1)?;
}
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 ordered_raw {
self.write_raw_position(writer, 7, 0)?;
}
if let Some(vertical_alignment) = self.vertical_alignment {
let mut e = BytesStart::new("w:vAlign");
e.push_attribute(("w:val", vertical_alignment.to_str()));
writer.write_event(Event::Empty(e))?;
}
if ordered_raw {
self.write_raw_position(writer, 7, 1)?;
}
if let Some(title_pg) = self.title_pg {
let mut e = BytesStart::new("w:titlePg");
if !title_pg {
e.push_attribute(("w:val", "0"));
}
writer.write_event(Event::Empty(e))?;
}
if ordered_raw {
self.write_raw_position(writer, 8, 0)?;
}
if let Some(text_direction) = &self.text_direction {
let mut e = BytesStart::new("w:textDirection");
e.push_attribute(("w:val", text_direction.as_str()));
writer.write_event(Event::Empty(e))?;
}
let mut write_doc_grid = |writer: &mut Writer<W>| -> Result<()> {
if let Some(doc_grid) = &self.doc_grid {
let mut e = BytesStart::new("w:docGrid");
push_retained_attributes(&mut e, &doc_grid.extra_attributes);
if let Some(grid_type) = doc_grid.grid_type {
e.push_attribute(("w:type", grid_type.to_str()));
}
if let Some(line_pitch) = doc_grid.line_pitch {
e.push_attribute(("w:linePitch", buf.format(line_pitch.0)));
}
if let Some(char_space) = doc_grid.char_space {
e.push_attribute(("w:charSpace", buf.format(char_space)));
}
writer.write_event(Event::Empty(e))?;
}
Ok(())
};
if ordered_raw {
self.write_raw_position(writer, 8, 1)?;
write_doc_grid(writer)?;
self.write_raw_position(writer, 8, 2)?;
self.write_raw_position(writer, 9, 0)?;
} else {
write_doc_grid(writer)?;
for raw in &self.extra_xml {
if is_root_attribute_record(raw) {
continue;
}
writer.get_mut().write_all(raw)?;
}
}
if let Some(change) = &self.change {
change.write_xml(writer)?;
}
writer.write_event(Event::End(BytesEnd::new("w:sectPr")))?;
Ok(())
}
fn write_raw_position<W: std::io::Write>(
&self,
writer: &mut Writer<W>,
slot: usize,
occurrence: usize,
) -> Result<()> {
for (position, raw) in self.extra_xml_positions.iter().zip(&self.extra_xml) {
if matches!(
position,
CT_SectPrRawPosition::Schema {
slot: candidate_slot,
occurrence: candidate_occurrence,
} if (*candidate_slot, *candidate_occurrence) == (slot, occurrence)
) {
writer.get_mut().write_all(raw)?;
}
}
Ok(())
}
fn write_story_reference_boundary<W: std::io::Write>(
&self,
writer: &mut Writer<W>,
references: &[HdrFtrRef],
header: bool,
boundary: usize,
) -> Result<()> {
for (position, raw) in self.extra_xml_positions.iter().zip(&self.extra_xml) {
let anchored = match position {
CT_SectPrRawPosition::Header {
preceding,
following,
} if header => Some((preceding, following)),
CT_SectPrRawPosition::Footer {
preceding,
following,
} if !header => Some((preceding, following)),
CT_SectPrRawPosition::Schema { slot, occurrence }
if (*slot == 0 && header) || (*slot == 1 && !header) =>
{
if (*occurrence).min(references.len()) == boundary {
writer.get_mut().write_all(raw)?;
}
None
}
_ => None,
};
let Some((preceding, following)) = anchored else {
continue;
};
let resolved = following
.as_ref()
.and_then(|reference| reference.resolve(references))
.or_else(|| {
preceding
.as_ref()
.and_then(|reference| reference.resolve(references).map(|index| index + 1))
})
.unwrap_or(references.len());
if resolved == boundary {
writer.get_mut().write_all(raw)?;
}
}
Ok(())
}
}
impl CT_PageBorders {
fn to_xml<W: std::io::Write>(&self, writer: &mut Writer<W>) -> Result<()> {
let mut e = BytesStart::new("w:pgBorders");
push_retained_attributes(&mut e, &self.extra_attributes);
if let Some(z_order) = self.z_order {
e.push_attribute(("w:zOrder", z_order.to_str()));
}
if let Some(display) = self.display {
e.push_attribute(("w:display", display.to_str()));
}
if let Some(offset_from) = self.offset_from {
e.push_attribute(("w:offsetFrom", offset_from.to_str()));
}
if self.top.is_none()
&& self.left.is_none()
&& self.bottom.is_none()
&& self.right.is_none()
&& self.extra_xml.is_empty()
{
writer.write_event(Event::Empty(e))?;
return Ok(());
}
writer.write_event(Event::Start(e))?;
for (edge, tag) in [
(&self.top, "w:top"),
(&self.left, "w:left"),
(&self.bottom, "w:bottom"),
(&self.right, "w:right"),
] {
if let Some(edge) = edge {
edge.to_xml(writer, tag)?;
}
}
for raw in &self.extra_xml {
writer.get_mut().write_all(raw)?;
}
writer.write_event(Event::End(BytesEnd::new("w:pgBorders")))?;
Ok(())
}
}
impl CT_NoteProperties {
pub(crate) fn to_xml<W: std::io::Write>(
&self,
writer: &mut Writer<W>,
tag: &str,
) -> Result<()> {
if self.pos.is_none()
&& self.num_fmt.is_none()
&& self.num_start.is_none()
&& self.num_restart.is_none()
&& self.special_references.is_empty()
&& self.extra_xml.is_empty()
{
writer.write_event(Event::Empty(BytesStart::new(tag)))?;
return Ok(());
}
writer.write_event(Event::Start(BytesStart::new(tag)))?;
self.write_retained_children(writer, 0)?;
if let Some(pos) = &self.pos {
let mut e = BytesStart::new("w:pos");
e.push_attribute(("w:val", pos.as_str()));
writer.write_event(Event::Empty(e))?;
}
self.write_retained_children(writer, 1)?;
if let Some(num_fmt) = &self.num_fmt {
let mut e = BytesStart::new("w:numFmt");
e.push_attribute(("w:val", num_fmt.as_str()));
writer.write_event(Event::Empty(e))?;
}
self.write_retained_children(writer, 2)?;
if let Some(num_start) = self.num_start {
let mut buf = itoa::Buffer::new();
let mut e = BytesStart::new("w:numStart");
e.push_attribute(("w:val", buf.format(num_start)));
writer.write_event(Event::Empty(e))?;
}
self.write_retained_children(writer, 3)?;
if let Some(num_restart) = &self.num_restart {
let mut e = BytesStart::new("w:numRestart");
e.push_attribute(("w:val", num_restart.as_str()));
writer.write_event(Event::Empty(e))?;
}
self.write_retained_children(writer, 4)?;
let child_tag = if tag.ends_with("footnotePr") {
"w:footnote"
} else {
"w:endnote"
};
for id in &self.special_references {
let mut buf = itoa::Buffer::new();
let mut child = BytesStart::new(child_tag);
child.push_attribute(("w:id", buf.format(*id)));
writer.write_event(Event::Empty(child))?;
}
self.write_retained_children(writer, 5)?;
writer.write_event(Event::End(BytesEnd::new(tag)))?;
Ok(())
}
fn write_retained_children<W: std::io::Write>(
&self,
writer: &mut Writer<W>,
slot: usize,
) -> Result<()> {
for (candidate, raw) in &self.extra_xml {
if *candidate == slot {
writer.get_mut().write_all(raw)?;
}
}
Ok(())
}
}
impl CT_PageNumberType {
fn to_xml<W: std::io::Write>(&self, writer: &mut Writer<W>) -> Result<()> {
if let Some(raw) = &self.raw_xml {
if self.start == self.parsed_start {
writer.get_mut().write_all(raw)?;
return Ok(());
}
writer.get_mut().write_all(&replace_page_number_start(
raw,
self.start,
self.start_attribute_name.as_deref(),
self.start_insertion_prefix.as_deref(),
&self.reserved_insertion_prefixes,
)?)?;
return Ok(());
}
let mut element = BytesStart::new("w:pgNumType");
if let Some(start) = self.start {
let mut buf = itoa::Buffer::new();
element.push_attribute(("w:start", buf.format(start)));
}
writer.write_event(Event::Empty(element))?;
Ok(())
}
}
fn store_unsigned_attribute(
value: &str,
key: &[u8],
field: &mut Option<u32>,
retained: &mut Vec<(String, String)>,
) -> Result<()> {
match value.parse() {
Ok(parsed) => *field = Some(parsed),
Err(_) => retained.push((std::str::from_utf8(key)?.to_owned(), value.to_owned())),
}
Ok(())
}
fn sole_val_attribute(e: &BytesStart<'_>, word_prefixes: &[String]) -> Result<Option<String>> {
let mut value = None;
for attribute in e.attributes() {
let attribute = attribute?;
if !is_word_attribute(attribute.key.as_ref(), b"val", word_prefixes) {
return Ok(None);
}
value = Some(
attribute
.decoded_and_normalized_value(XmlVersion::Implicit1_0, e.decoder())?
.into_owned(),
);
}
Ok(value)
}
fn push_retained_attributes(e: &mut BytesStart<'_>, retained: &[(String, String)]) {
for (name, value) in retained {
e.push_attribute(quick_xml::events::attributes::Attribute {
key: quick_xml::name::QName(name.as_bytes()),
value: std::borrow::Cow::Borrowed(value.as_bytes()),
});
}
}
fn replace_page_number_start(
raw: &[u8],
start: Option<u32>,
attribute_name: Option<&[u8]>,
insertion_prefix: Option<&[u8]>,
reserved_prefixes: &[Vec<u8>],
) -> Result<Vec<u8>> {
let replacement = start.map(|value| value.to_string());
let mut output = raw.to_vec();
let Some((tag_end, attributes)) = start_tag_attributes(raw) else {
return Ok(output);
};
if let Some(name) = attribute_name
&& let Some((_, value, full)) = attributes
.iter()
.find(|(candidate, _, _)| &raw[candidate.clone()] == name)
{
if let Some(ref value_text) = replacement {
output.splice(value.clone(), value_text.bytes());
} else {
output.drain(full.clone());
}
return Ok(output);
}
if let Some(value) = replacement {
let insert_at = raw[..tag_end]
.iter()
.rposition(|byte| !byte.is_ascii_whitespace() && *byte != b'/')
.map_or(tag_end, |index| index + 1);
let inserted = if let Some(prefix) = insertion_prefix {
format!(" {}:start=\"{value}\"", String::from_utf8_lossy(prefix))
} else {
let prefix = unused_page_number_prefix(raw, tag_end, &attributes, reserved_prefixes);
format!(" xmlns:{prefix}=\"{W_NS}\" {prefix}:start=\"{value}\"")
};
output.splice(insert_at..insert_at, inserted.bytes());
}
Ok(output)
}
fn unused_page_number_prefix(
raw: &[u8],
tag_end: usize,
attributes: &[AttributeSpans],
reserved_prefixes: &[Vec<u8>],
) -> String {
for suffix in 0usize.. {
let prefix = if suffix == 0 {
"rdocxWord".to_owned()
} else {
format!("rdocxWord{suffix}")
};
let declaration = format!("xmlns:{prefix}");
let qualified = format!("{prefix}:");
let used = reserved_prefixes
.iter()
.any(|candidate| candidate == prefix.as_bytes())
|| attributes.iter().any(|(name, _, _)| {
&raw[name.clone()] == declaration.as_bytes()
|| raw[name.clone()].starts_with(qualified.as_bytes())
})
|| raw[1..tag_end].starts_with(qualified.as_bytes());
if !used {
return prefix;
}
}
unreachable!("the finite start tag cannot use every generated prefix")
}
fn attribute_in_namespace(
name: &[u8],
local_name: &[u8],
namespace: &str,
scope: &[String],
) -> bool {
let Some(separator) = name.iter().position(|byte| *byte == b':') else {
return false;
};
if name.get(separator + 1..) != Some(local_name) {
return false;
}
let prefix = &name[..separator];
scope.iter().any(|binding| {
binding
.strip_prefix('\0')
.and_then(|binding| binding.split_once('\0'))
.is_some_and(|(candidate, value)| candidate.as_bytes() == prefix && value == namespace)
})
}
fn page_size_attributes_are_modeled(
element: &BytesStart<'_>,
word_prefixes: &[String],
) -> Result<bool> {
for attribute in element.attributes() {
let attribute = attribute?;
let key = attribute.key.as_ref();
if namespace_declaration(key) {
continue;
}
let value = std::str::from_utf8(&attribute.value)?;
if is_word_attribute(key, b"w", word_prefixes)
|| is_word_attribute(key, b"h", word_prefixes)
{
if parse_integer_measurement::<i32>(element.name().as_ref(), key, value).is_err() {
return Ok(false);
}
} else if is_word_attribute(key, b"orient", word_prefixes) {
if ST_PageOrientation::from_str(value).is_err() {
return Ok(false);
}
} else {
return Ok(false);
}
}
Ok(true)
}
fn page_margin_attributes_are_modeled(
element: &BytesStart<'_>,
word_prefixes: &[String],
) -> Result<bool> {
for attribute in element.attributes() {
let attribute = attribute?;
let key = attribute.key.as_ref();
if namespace_declaration(key) {
continue;
}
let modeled = [
b"top".as_slice(),
b"right".as_slice(),
b"end".as_slice(),
b"bottom".as_slice(),
b"left".as_slice(),
b"start".as_slice(),
b"gutter".as_slice(),
b"header".as_slice(),
b"footer".as_slice(),
]
.iter()
.any(|local| is_word_attribute(key, local, word_prefixes));
let value = std::str::from_utf8(&attribute.value)?;
if !modeled
|| parse_integer_measurement::<i32>(element.name().as_ref(), key, value).is_err()
{
return Ok(false);
}
}
Ok(true)
}
pub(crate) fn checked_section_child_slot(local: &[u8]) -> Option<(u8, bool)> {
let slot = match local {
b"headerReference" => return Some((0, true)),
b"footerReference" => return Some((1, true)),
b"footnotePr" => 2,
b"endnotePr" => 3,
b"type" => 4,
b"pgSz" => 5,
b"pgMar" => 6,
b"paperSrc" => 7,
b"pgBorders" => 8,
b"lnNumType" => 9,
b"pgNumType" => 10,
b"cols" => 11,
b"vAlign" => 12,
b"titlePg" => 13,
b"textDirection" => 14,
b"docGrid" => 15,
b"sectPrChange" => 16,
_ => return None,
};
Some((slot, false))
}
pub(crate) fn validate_checked_section_child(
element: &BytesStart<'_>,
prefixes: &[String],
) -> Result<bool> {
let name = element.local_name();
let local = name.as_ref();
if matches!(
local,
b"p" | b"r" | b"tbl" | b"tr" | b"tc" | b"pPr" | b"rPr" | b"sectPr"
) {
return Err(OxmlError::InvalidValue(
"element XML section has a known child with the wrong owner".into(),
));
}
if matches!(local, b"headerReference" | b"footerReference") {
if !story_reference_attributes_are_modeled(element, prefixes)? {
return Err(OxmlError::InvalidValue(
"element XML section reference has unsupported or invalid owner attributes".into(),
));
}
return Ok(true);
}
for attribute in element.attributes() {
let attribute = attribute?;
let key = attribute.key.as_ref();
let value =
attribute.decoded_and_normalized_value(XmlVersion::Implicit1_0, element.decoder())?;
let invalid = match local {
b"type" if is_word_attribute(key, b"val", prefixes) => {
ST_SectionType::from_str(&value).is_err()
}
b"pgSz" if is_word_attribute(key, b"orient", prefixes) => {
ST_PageOrientation::from_str(&value).is_err()
}
b"titlePg" if is_word_attribute(key, b"val", prefixes) => {
!matches!(value.as_ref(), "true" | "false" | "on" | "off" | "1" | "0")
}
b"vAlign" if is_word_attribute(key, b"val", prefixes) => {
!matches!(value.as_ref(), "top" | "center" | "bottom" | "both")
}
b"pgSz"
if is_word_attribute(key, b"w", prefixes)
|| is_word_attribute(key, b"h", prefixes) =>
{
parse_integer_measurement::<i32>(element.name().as_ref(), key, &value).is_err()
}
_ => false,
};
if invalid {
return Err(OxmlError::InvalidValue(
"element XML section property has an invalid modeled value".into(),
));
}
}
Ok(matches!(
local,
b"type"
| b"pgSz"
| b"pgMar"
| b"paperSrc"
| b"lnNumType"
| b"pgNumType"
| b"vAlign"
| b"titlePg"
| b"textDirection"
| b"docGrid"
))
}
fn story_reference_attributes_are_modeled(
element: &BytesStart<'_>,
word_prefixes: &[String],
) -> Result<bool> {
let mut type_count = 0usize;
let mut relationship_count = 0usize;
for attribute in element.attributes() {
let attribute = attribute?;
let key = attribute.key.as_ref();
if namespace_declaration(key) {
continue;
}
let value = std::str::from_utf8(&attribute.value)?;
if is_word_attribute(key, b"type", word_prefixes) {
type_count += 1;
if type_count > 1 || !matches!(value, "default" | "first" | "even") {
return Ok(false);
}
} else if attribute_in_namespace(key, b"id", crate::namespace::R_NS, word_prefixes) {
relationship_count += 1;
if relationship_count > 1 || value.is_empty() {
return Ok(false);
}
} else {
return Ok(false);
}
}
Ok(relationship_count == 1)
}
fn namespace_declaration(name: &[u8]) -> bool {
name == b"xmlns" || name.starts_with(b"xmlns:")
}
fn raw_element_has_child_content(raw: &[u8]) -> Result<bool> {
let mut reader = Reader::from_reader(raw);
reader.config_mut().trim_text(false);
let mut buffer = Vec::new();
let mut depth = 0usize;
loop {
match reader.read_event_into(&mut buffer)? {
Event::Start(_) => {
if depth > 0 {
return Ok(true);
}
depth += 1;
}
Event::Empty(_) if depth > 0 => return Ok(true),
Event::Text(text)
if depth > 0 && text.as_ref().iter().any(|byte| !byte.is_ascii_whitespace()) =>
{
return Ok(true);
}
Event::CData(text)
if depth > 0 && text.as_ref().iter().any(|byte| !byte.is_ascii_whitespace()) =>
{
return Ok(true);
}
Event::GeneralRef(_) if depth > 0 => return Ok(true),
Event::End(_) => depth = depth.saturating_sub(1),
Event::Eof => return Ok(false),
_ => {}
}
buffer.clear();
}
}
type AttributeSpans = (
std::ops::Range<usize>,
std::ops::Range<usize>,
std::ops::Range<usize>,
);
fn start_tag_attributes(raw: &[u8]) -> Option<(usize, Vec<AttributeSpans>)> {
if raw.first() != Some(&b'<') {
return None;
}
let mut quote = None;
let tag_end = raw
.iter()
.enumerate()
.find_map(|(index, byte)| match (*byte, quote) {
(b'\'' | b'"', None) => {
quote = Some(*byte);
None
}
(value, Some(open)) if value == open => {
quote = None;
None
}
(b'>', None) => Some(index),
_ => None,
})?;
let mut cursor = 1usize;
while cursor < tag_end && !raw[cursor].is_ascii_whitespace() && raw[cursor] != b'/' {
cursor += 1;
}
let mut attributes = Vec::new();
while cursor < tag_end {
let full_start = cursor;
while cursor < tag_end && raw[cursor].is_ascii_whitespace() {
cursor += 1;
}
if cursor == tag_end || raw[cursor] == b'/' {
break;
}
let name_start = cursor;
while cursor < tag_end
&& !raw[cursor].is_ascii_whitespace()
&& raw[cursor] != b'='
&& raw[cursor] != b'/'
{
cursor += 1;
}
let name_end = cursor;
while cursor < tag_end && raw[cursor].is_ascii_whitespace() {
cursor += 1;
}
if raw.get(cursor) != Some(&b'=') {
return None;
}
cursor += 1;
while cursor < tag_end && raw[cursor].is_ascii_whitespace() {
cursor += 1;
}
let open_quote = *raw.get(cursor)?;
if open_quote != b'\'' && open_quote != b'"' {
return None;
}
cursor += 1;
let value_start = cursor;
while cursor < tag_end && raw[cursor] != open_quote {
cursor += 1;
}
if cursor == tag_end {
return None;
}
let value_end = cursor;
cursor += 1;
attributes.push((
name_start..name_end,
value_start..value_end,
full_start..cursor,
));
}
Some((tag_end, attributes))
}
#[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> {
let mut paragraphs = Vec::new();
for content in &self.content {
match content {
BodyContent::Paragraph(paragraph) => paragraphs.push(paragraph),
BodyContent::ContentControl(sdt) => {
sdt.collect_paragraphs(SdtOwner::Body, &mut paragraphs)
}
BodyContent::Table(_) | BodyContent::RawXml(_) => {}
}
}
paragraphs.into_iter()
}
#[doc(hidden)]
pub fn accepted_paragraph_joins_next(&self, index: usize) -> bool {
let Some(BodyContent::Paragraph(paragraph)) = self.content.get(index) else {
return false;
};
let Some(properties) = paragraph.properties.as_ref() else {
return false;
};
let Some(mark) = properties.rpr.as_ref() else {
return false;
};
let moved_away = || {
mark.revision_xml.iter().any(|raw| {
let mut reader = Reader::from_reader(raw.as_slice());
matches!(
reader.read_event(),
Ok(Event::Start(start) | Event::Empty(start))
if word_prefixes_at(&start, &["w".to_owned()]).is_ok_and(|prefixes| {
is_word_element(start.name().as_ref(), b"moveFrom", &prefixes)
})
)
})
};
let removed = mark
.revision_markers
.iter()
.any(|marker| marker.kind() == RevisionKind::Deletion)
|| moved_away();
removed
&& properties.sect_pr.is_none()
&& matches!(self.content.get(index + 1), Some(BodyContent::Paragraph(_)))
}
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> {
let mut tables = Vec::new();
for content in &self.content {
match content {
BodyContent::Table(table) => tables.push(table),
BodyContent::ContentControl(sdt) => sdt.collect_tables(SdtOwner::Body, &mut tables),
BodyContent::Paragraph(_) | BodyContent::RawXml(_) => {}
}
}
tables.into_iter()
}
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.find_paragraph_indices(text).into_iter().next()
}
pub fn find_paragraph_indices(&self, text: &str) -> Vec<usize> {
let mut direct = Vec::new();
let mut enclosing = Vec::new();
for (index, content) in self.content.iter().enumerate() {
match content {
BodyContent::Paragraph(paragraph) if paragraph.text().contains(text) => {
direct.push(index);
}
BodyContent::ContentControl(sdt) => {
let mut paragraphs = Vec::new();
sdt.collect_paragraphs(SdtOwner::Body, &mut paragraphs);
if paragraphs
.iter()
.any(|paragraph| paragraph.text().contains(text))
{
enclosing.push(index);
}
}
_ => {}
}
}
direct.extend(enclosing);
direct
}
pub fn content_controls(&self) -> Vec<&CT_Sdt> {
let mut controls = Vec::new();
for content in &self.content {
match content {
BodyContent::Paragraph(paragraph) => paragraph.collect_controls(&mut controls),
BodyContent::Table(table) => table.collect_controls(&mut controls),
BodyContent::ContentControl(sdt) => {
controls.push(sdt);
sdt.collect_controls(SdtOwner::Body, &mut controls);
}
BodyContent::RawXml(_) => {}
}
}
controls
}
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> {
admit_word_reader(reader, &["w".to_owned()], b"body", Some(W_NS))?;
Self::from_xml_with_prefixes_and_owner_bindings(reader, &["w".to_string()], &[])
}
pub(crate) fn from_xml_with_prefixes_and_owner_bindings(
reader: &mut Reader<&[u8]>,
word_prefixes: &[String],
owner_bindings: &[(String, String)],
) -> Result<Self> {
Self::from_xml_with_prefixes_and_owner_bindings_until(
reader,
word_prefixes,
owner_bindings,
b"body",
)
}
pub(crate) fn from_xml_with_prefixes_and_owner_bindings_until(
reader: &mut Reader<&[u8]>,
word_prefixes: &[String],
owner_bindings: &[(String, String)],
end_local_name: &[u8],
) -> 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_and_root(reader, &prefixes, Some(e))?,
));
} else if is_word_element(name.as_ref(), b"tbl", &prefixes) {
let local_bindings = local_namespace_overrides(e, word_prefixes)?;
let table_bindings = merged_owner_bindings(owner_bindings, &local_bindings);
let mut table = CT_Tbl::from_xml_with_prefixes_and_owner_bindings_at_depth(
reader,
&prefixes,
&table_bindings,
0,
Some(e),
)?;
table.retain_root_attributes(e, &prefixes)?;
content.push(BodyContent::Table(table));
} else if is_word_element(name.as_ref(), b"sdt", &prefixes) {
let raw = crate::text::raw_with_external_bindings(
&capture_element(reader, e)?,
owner_bindings,
)?;
if let Some(sdt) = CT_Sdt::from_body_raw(&raw, &prefixes) {
content.push(BodyContent::ContentControl(sdt));
} else {
content.push(BodyContent::RawXml(raw));
}
} else if is_word_element(name.as_ref(), b"sectPr", &prefixes) {
if sect_pr.is_none() {
let local_bindings = local_namespace_overrides(e, word_prefixes)?;
let section_bindings =
merged_owner_bindings(owner_bindings, &local_bindings);
sect_pr =
Some(CT_SectPr::from_xml_with_prefixes_owner_bindings_and_root(
reader,
&prefixes,
§ion_bindings,
Some(e),
)?);
} else {
content.push(BodyContent::RawXml(
crate::text::raw_with_external_bindings(
&capture_element(reader, e)?,
owner_bindings,
)?,
));
}
} else {
content.push(BodyContent::RawXml(
crate::text::raw_with_external_bindings(
&capture_element(reader, e)?,
owner_bindings,
)?,
));
}
}
Ok(Event::Empty(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_empty_root(e, &prefixes)?));
} else if is_word_element(name.as_ref(), b"tbl", &prefixes) {
content.push(BodyContent::Table(CT_Tbl::new()));
} else if is_word_element(name.as_ref(), b"sectPr", &prefixes) {
if sect_pr.is_none() {
sect_pr = Some(CT_SectPr::from_empty_root(e, &prefixes)?);
} else {
content.push(BodyContent::RawXml(
crate::text::raw_with_external_bindings(
&capture_empty_element(e)?,
owner_bindings,
)?,
));
}
} else {
content.push(BodyContent::RawXml(
crate::text::raw_with_external_bindings(
&capture_empty_element(e)?,
owner_bindings,
)?,
));
}
}
Ok(Event::End(ref e))
if is_word_element(e.name().as_ref(), end_local_name, word_prefixes) =>
{
break;
}
Ok(Event::Eof) => {
let end = String::from_utf8_lossy(end_local_name);
return Err(OxmlError::MissingElement(format!("w:{end} end")));
}
Err(e) => return Err(e.into()),
_ => {}
}
buf.clear();
}
Ok(CT_Body { content, sect_pr })
}
#[doc(hidden)]
pub fn validate_resource_limits(&self) -> Result<()> {
admit_word_model(WordOwner::Body(self))
}
pub fn to_xml<W: std::io::Write>(&self, writer: &mut Writer<W>) -> Result<()> {
admit_word_model(WordOwner::Body(self))?;
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::ContentControl(sdt) => sdt.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>>,
#[doc(hidden)]
pub background_extra_xml: Vec<Vec<u8>>,
#[doc(hidden)]
pub root_attributes: Vec<(String, String)>,
}
#[allow(non_snake_case)]
impl CT_Document {
pub fn set_background_color(&mut self, color: &str) -> Result<()> {
if color.len() != 6 || !color.bytes().all(|byte| byte.is_ascii_hexdigit()) {
return Err(OxmlError::InvalidValue(
"page color must be six hexadecimal digits".to_owned(),
));
}
let mut prefixes = vec!["w".to_owned()];
for (name, namespace) in &self.extra_namespaces {
if namespace == W_NS {
prefixes.push(name.strip_prefix("xmlns:").unwrap_or("").to_owned());
}
}
self.background_xml = Some(match &self.background_xml {
Some(raw) => {
crate::settings::rewrite_word_root_attribute(raw, &prefixes, b"color", color)?
}
None => format!("<w:background xmlns:w=\"{W_NS}\" w:color=\"{color}\"/>").into_bytes(),
});
Ok(())
}
pub fn new() -> Self {
CT_Document {
body: CT_Body::new(),
extra_namespaces: Vec::new(),
background_xml: None,
background_extra_xml: Vec::new(),
root_attributes: Vec::new(),
}
}
pub fn from_xml(xml: &[u8]) -> Result<Self> {
admit_word_xml(xml, &["w".to_owned()], None)?;
let mut reader = Reader::from_reader(xml);
reader.config_mut().trim_text(false);
let mut body = None;
let mut extra_namespaces = Vec::new();
let mut background_xml = None;
let mut background_extra_xml = Vec::new();
let mut root_attributes = Vec::new();
let mut buf = Vec::new();
let mut word_prefixes = Vec::new();
let mut document_open = false;
let mut document_closed = false;
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 is_word_element(name.as_ref(), b"document", &prefixes) {
if document_open || document_closed {
return Err(OxmlError::UnexpectedElement("w:document".to_owned()));
}
for attr in e.attributes().flatten() {
let key = attr.key.as_ref();
let is_namespace = key.starts_with(b"xmlns:") || key == b"xmlns";
if is_namespace && known_ns.contains(&key) {
continue;
}
let key_str = std::str::from_utf8(key).unwrap_or("").to_string();
let val_str = attr
.decoded_and_normalized_value(XmlVersion::Implicit1_0, e.decoder())?
.into_owned();
if is_namespace {
extra_namespaces.push((key_str, val_str));
} else {
root_attributes.push((key_str, val_str));
}
}
document_open = true;
word_prefixes = prefixes;
} else if !document_open || document_closed {
return Err(OxmlError::UnexpectedElement(
String::from_utf8_lossy(name.as_ref()).into_owned(),
));
} else if is_word_element(name.as_ref(), b"body", &prefixes) {
if !document_open || document_closed || body.is_some() {
return Err(OxmlError::UnexpectedElement("w:body".to_owned()));
}
let owner_bindings = local_namespace_overrides(e, &word_prefixes)?;
body = Some(CT_Body::from_xml_with_prefixes_and_owner_bindings(
&mut reader,
&prefixes,
&owner_bindings,
)?);
} else if is_word_element(name.as_ref(), b"background", &prefixes) {
background_xml = Some(capture_element(&mut reader, e)?);
} else if matches_local_name(name.as_ref(), b"background") {
background_extra_xml.push(capture_element(&mut reader, e)?);
} else {
reader.read_to_end_into(name, &mut Vec::new())?;
}
}
Ok(Event::Empty(ref e)) => {
let prefixes = word_prefixes_at(e, &word_prefixes)?;
if is_word_element(e.name().as_ref(), b"document", &prefixes)
|| !document_open
|| document_closed
{
return Err(OxmlError::UnexpectedElement(
String::from_utf8_lossy(e.name().as_ref()).into_owned(),
));
} else if is_word_element(e.name().as_ref(), b"body", &prefixes) {
if body.is_some() {
return Err(OxmlError::UnexpectedElement("w:body".to_owned()));
}
body = Some(CT_Body::new());
} else if is_word_element(e.name().as_ref(), b"background", &prefixes) {
background_xml = Some(capture_empty_element(e)?);
} else if matches_local_name(e.name().as_ref(), b"background") {
background_extra_xml.push(capture_empty_element(e)?);
}
}
Ok(Event::End(ref e))
if is_word_element(e.name().as_ref(), b"document", &word_prefixes) =>
{
if !document_open {
return Err(OxmlError::UnexpectedElement("w:document end".to_owned()));
}
document_open = false;
document_closed = true;
}
Ok(Event::Text(ref text)) if !text.as_ref().iter().all(u8::is_ascii_whitespace) => {
return Err(OxmlError::UnexpectedElement(
"text outside w:body".to_owned(),
));
}
Ok(Event::Eof) => {
if document_open || !document_closed {
return Err(OxmlError::MissingElement("w:document end".to_owned()));
}
break;
}
Err(e) => return Err(e.into()),
_ => {}
}
buf.clear();
}
Ok(CT_Document {
body: body.ok_or_else(|| OxmlError::MissingElement("w:body".to_owned()))?,
extra_namespaces,
background_xml,
background_extra_xml,
root_attributes,
})
}
#[doc(hidden)]
pub fn validate_resource_limits(&self) -> Result<()> {
admit_word_model(WordOwner::Body(&self.body))
}
pub fn to_xml(&self) -> Result<Vec<u8>> {
self.validate_resource_limits()?;
let wp_ns = "http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing";
let wp_root_is_canonical = self
.extra_namespaces
.iter()
.find(|(name, _)| name == "xmlns:wp")
.is_none_or(|(_, namespace)| namespace == wp_ns);
let _binding_scope = root_binding_scope(
ROOT_R_BINDING
| ROOT_MC_BINDING
| if wp_root_is_canonical {
ROOT_WP_BINDING
} else {
0
},
);
let mut writer = Writer::new(Vec::new());
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 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.iter().chain(&self.root_attributes) {
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);
}
for raw in &self.background_extra_xml {
writer.get_mut().extend_from_slice(raw);
}
self.body.to_xml(&mut writer)?;
writer.write_event(Event::End(BytesEnd::new("w:document")))?;
let mut xml = writer.into_inner();
declare_w14_on_part_root(&mut xml)?;
Ok(xml)
}
}
impl Default for CT_Document {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
#[test]
fn fx211_textbox_measurement_control() {
let Ok(depth) = std::env::var("FX211_MEASURE_DEPTH") else {
return;
};
let depth: usize = depth.parse().unwrap();
let control_depth: usize = std::env::var("FX211_MEASURE_CONTROL_DEPTH")
.unwrap_or_else(|_| "0".into())
.parse()
.unwrap();
let mixed = std::env::var("FX211_MEASURE_MIXED").is_ok();
let start = "<w:p><w:r><w:drawing><wp:anchor><wp:extent cx=\"10\" cy=\"20\"/><wp:docPr id=\"1\" name=\"measured\"/><a:graphic><a:graphicData><wps:wsp><wps:txbx><w:txbxContent>";
let end = "</w:txbxContent></wps:txbx></wps:wsp></a:graphicData></a:graphic></wp:anchor></w:drawing></w:r></w:p>";
let inner = "<w:p><w:r><w:t>leaf</w:t></w:r></w:p>";
let mut xml = format!(
"<w:document xmlns:w=\"{}\" xmlns:wp=\"http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing\" xmlns:a=\"http://schemas.openxmlformats.org/drawingml/2006/main\" xmlns:wps=\"http://schemas.microsoft.com/office/word/2010/wordprocessingShape\"><w:body>",
crate::namespace::W_NS
);
xml.push_str(&"<w:sdt><w:sdtContent>".repeat(control_depth));
for _ in 0..depth {
xml.push_str(start);
if mixed {
xml.push_str("<w:sdt><w:sdtContent>");
}
}
xml.push_str(inner);
for _ in 0..depth {
if mixed {
xml.push_str("</w:sdtContent></w:sdt>");
}
xml.push_str(end);
}
xml.push_str(&"</w:sdtContent></w:sdt>".repeat(control_depth));
xml.push_str("</w:body></w:document>");
println!(
"measurement depth={depth} mixed={mixed} inputBytes={}",
xml.len()
);
crate::drawing::FX211_DRAWING_READS.with(|reads| reads.set(0));
let timer = std::time::Instant::now();
let parsed = super::CT_Document::from_xml(xml.as_bytes())
.expect("accepted measurement fixture must construct typed owners");
assert_eq!(
crate::drawing::FX211_DRAWING_READS.with(std::cell::Cell::get),
depth,
"each no-field drawing is constructed once"
);
let mut controls: Vec<_> = parsed
.body
.content
.iter()
.filter_map(|item| match item {
super::BodyContent::ContentControl(control) => Some(control),
_ => None,
})
.collect();
let mut typed_controls = 0;
while let Some(control) = controls.pop() {
typed_controls += 1;
for child in &control.content {
if let crate::content_control::SdtContent::ContentControl(control) = child {
controls.push(control);
}
}
}
assert_eq!(
typed_controls, control_depth,
"measurement must exercise outer typed controls"
);
let mut bodies = vec![&parsed.body];
let mut typed_boxes = 0;
while let Some(body) = bodies.pop() {
for paragraph in body.paragraphs() {
for run in paragraph.runs() {
for content in &run.content {
if let crate::text::RunContent::Drawing(drawing) = content
&& let Some(body) = drawing
.anchor
.as_ref()
.and_then(|anchor| anchor.shape.as_ref())
.and_then(|shape| shape.text_body.as_ref())
{
typed_boxes += 1;
bodies.push(body);
}
}
}
}
}
assert_eq!(
typed_boxes, depth,
"measurement must exercise actual typed text boxes"
);
println!(
"parsed typedBoxes={typed_boxes} elapsed={:?}",
timer.elapsed()
);
let clone = parsed.clone();
println!("cloned elapsed={:?}", timer.elapsed());
let saved = clone.to_xml().unwrap();
println!("saved bytes={} elapsed={:?}", saved.len(), timer.elapsed());
let reopened = super::CT_Document::from_xml(&saved).unwrap();
assert_eq!(
crate::drawing::FX211_DRAWING_READS.with(std::cell::Cell::get),
depth * 2,
"each reopened no-field drawing is constructed once"
);
println!("reopened elapsed={:?}", timer.elapsed());
drop(reopened);
drop(clone);
drop(parsed);
println!("dropped elapsed={:?}", timer.elapsed());
}
#[test]
fn fx211_mixed_table_measurement_control() {
let Ok(depth) = std::env::var("FX211_TABLE_DEPTH") else {
return;
};
let depth: usize = depth.parse().unwrap();
let controls = std::env::var("FX211_TABLE_CONTROL").is_ok();
let boxes = std::env::var("FX211_TABLE_BOX").is_ok();
let outer_controls: usize = std::env::var("FX211_TABLE_OUTER_CONTROL_DEPTH")
.unwrap_or_else(|_| "0".into())
.parse()
.unwrap();
let outer_boxes: usize = std::env::var("FX211_TABLE_OUTER_BOX_DEPTH")
.unwrap_or_else(|_| "0".into())
.parse()
.unwrap();
let mut start = String::new();
let mut end = String::new();
if boxes {
start.push_str("<w:p><w:r><w:drawing><wp:anchor><wp:docPr id=\"1\"/><w:txbxContent>");
end.push_str("</w:txbxContent></wp:anchor></w:drawing></w:r></w:p>");
}
if controls {
start.push_str("<w:sdt><w:sdtContent>");
end.insert_str(0, "</w:sdtContent></w:sdt>");
}
start.push_str("<w:tbl><w:tr><w:tc>");
end.insert_str(0, "</w:tc></w:tr></w:tbl>");
let outer_start = format!(
"{}{}",
"<w:p><w:r><w:drawing><wp:anchor><wp:docPr id=\"1\"/><w:txbxContent>"
.repeat(outer_boxes),
"<w:sdt><w:sdtContent>".repeat(outer_controls)
);
let outer_end = format!(
"{}{}",
"</w:sdtContent></w:sdt>".repeat(outer_controls),
"</w:txbxContent></wp:anchor></w:drawing></w:r></w:p>".repeat(outer_boxes)
);
let xml = format!(
"<w:document xmlns:w=\"{}\" xmlns:wp=\"http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing\"><w:body>{}{}<w:p><w:r><w:t>leaf</w:t></w:r></w:p>{}{}</w:body></w:document>",
crate::namespace::W_NS,
outer_start,
start.repeat(depth),
end.repeat(depth),
outer_end
);
if std::env::var("FX211_TABLE_PREFLIGHT_REFUSAL_ONLY").is_ok() {
let error = super::admit_word_xml(xml.as_bytes(), &[], None).unwrap_err();
assert!(error.to_string().contains("weighted limit 256"));
println!("preflight-only refused weighted mixed path; no native construction");
return;
}
let timer = std::time::Instant::now();
let parsed = super::CT_Document::from_xml(xml.as_bytes()).unwrap();
enum Measured<'a> {
Body(&'a super::CT_Body),
Table(&'a crate::table::CT_Tbl),
Cell(&'a crate::table::CT_Tc),
Control(&'a crate::content_control::CT_Sdt),
Paragraph(&'a crate::text::CT_P),
}
let mut pending = vec![Measured::Body(&parsed.body)];
let (mut actual_tables, mut actual_controls, mut actual_boxes) = (0, 0, 0);
while let Some(owner) = pending.pop() {
match owner {
Measured::Body(body) => {
for child in &body.content {
pending.push(match child {
super::BodyContent::Paragraph(p) => Measured::Paragraph(p),
super::BodyContent::Table(t) => Measured::Table(t),
super::BodyContent::ContentControl(s) => Measured::Control(s),
super::BodyContent::RawXml(_) => {
panic!("measurement owner became opaque")
}
});
}
}
Measured::Table(table) => {
actual_tables += 1;
for row in &table.rows {
for cell in &row.cells {
pending.push(Measured::Cell(cell));
}
}
}
Measured::Cell(cell) => {
for child in &cell.content {
pending.push(match child {
crate::table::CellContent::Paragraph(p) => Measured::Paragraph(p),
crate::table::CellContent::Table(t) => Measured::Table(t),
crate::table::CellContent::ContentControl(s) => Measured::Control(s),
});
}
}
Measured::Control(control) => {
actual_controls += 1;
for child in &control.content {
pending.push(match child {
crate::content_control::SdtContent::Paragraph(p) => {
Measured::Paragraph(p)
}
crate::content_control::SdtContent::Table(t) => Measured::Table(t),
crate::content_control::SdtContent::ContentControl(s) => {
Measured::Control(s)
}
_ => panic!("unexpected measured control owner"),
});
}
}
Measured::Paragraph(p) => {
for run in &p.runs {
for child in &run.content {
if let crate::text::RunContent::Drawing(drawing) = child {
let body = drawing
.anchor
.as_ref()
.unwrap()
.shape
.as_ref()
.unwrap()
.text_body
.as_ref()
.unwrap();
actual_boxes += 1;
pending.push(Measured::Body(body));
}
}
}
}
}
}
assert_eq!(actual_tables, depth);
assert_eq!(
actual_controls,
outer_controls + if controls { depth } else { 0 }
);
assert_eq!(actual_boxes, outer_boxes + if boxes { depth } else { 0 });
println!(
"parsed tables={actual_tables} controls={actual_controls} boxes={actual_boxes} inputBytes={} elapsed={:?}",
xml.len(),
timer.elapsed()
);
parsed.validate_resource_limits().unwrap();
let cloned = parsed.clone();
let saved = cloned.to_xml().unwrap();
let reopened = super::CT_Document::from_xml(&saved).unwrap();
assert_eq!(reopened.to_xml().unwrap(), saved);
drop(reopened);
drop(cloned);
drop(parsed);
println!("dropped elapsed={:?}", timer.elapsed());
}
fn fx211_control_xml(depth: usize) -> String {
format!(
"{}<w:p><w:r><w:t>accepted</w:t></w:r></w:p>{}",
"<w:sdt><w:sdtContent>".repeat(depth),
"</w:sdtContent></w:sdt>".repeat(depth)
)
}
#[test]
fn fx211_weighted_source_boundary_and_actual_anchor_routes() {
let start = "<w:p><w:r><w:drawing><wp:anchor><wp:extent cx=\"10\" cy=\"20\"/><wp:docPr id=\"1\" name=\"bounded\"/><x:wrapper><w:txbxContent>";
let end = "</w:txbxContent></x:wrapper></wp:anchor></w:drawing></w:r></w:p>";
let wrap = |depth: usize, controls: usize| {
format!(
"<w:document xmlns:w=\"{}\" xmlns:wp=\"http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing\" xmlns:x=\"urn:producer\"><w:body>{}{}<w:p><w:r><w:t>leaf</w:t></w:r></w:p>{}{}</w:body></w:document>",
crate::namespace::W_NS,
"<w:sdt><w:sdtContent>".repeat(controls),
start.repeat(depth),
end.repeat(depth),
"</w:sdtContent></w:sdt>".repeat(controls)
)
};
for (boxes, controls) in [(16, 0), (1, 240), (8, 128), (0, 256)] {
super::admit_word_xml(wrap(boxes, controls).as_bytes(), &[], None).unwrap();
}
for (boxes, controls) in [(17, 0), (1, 241), (8, 129), (2000, 0), (2000, 1)] {
let xml = wrap(boxes, controls);
let error = super::CT_Document::from_xml(xml.as_bytes()).unwrap_err();
assert!(error.to_string().contains("weighted limit 256"));
}
let sibling = wrap(16, 0).replace(
"</w:body>",
&format!("{}<w:p/>{}</w:body>", start.repeat(8), end.repeat(8)),
);
super::admit_word_xml(sibling.as_bytes(), &[], None).unwrap();
assert!(super::word_owner_cost(usize::MAX, 1).is_err());
let opaque = wrap(0, 0).replace("<w:p><w:r><w:t>leaf</w:t></w:r></w:p>", &format!("<w:p><w:r><w:drawing><wp:anchor><wp:docPr id=\"1\"/><x:spPr><w:txbxContent>{}</w:txbxContent></x:spPr></wp:anchor></w:drawing></w:r></w:p>", fx211_control_xml(257)));
super::admit_word_xml(opaque.as_bytes(), &[], None).unwrap();
}
#[test]
fn fx211_opaque_qualified_wrong_owner_edges_preserve_source() {
let deep = fx211_control_xml(257);
for payload in [
format!(
"<w:drawing><wp:anchor><wp:docPr id=\"1\"/><w:txbxContent>{deep}</w:txbxContent></wp:anchor></w:drawing>"
),
format!("<w:p><w:tbl><w:tr><w:tc>{deep}</w:tc></w:tr></w:tbl></w:p>"),
format!("<w:p><w:r><w:tbl><w:tr><w:tc>{deep}</w:tc></w:tr></w:tbl></w:r></w:p>"),
] {
let xml = format!(
"<w:document xmlns:w=\"{}\" xmlns:wp=\"http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing\"><w:body>{payload}</w:body></w:document>",
crate::namespace::W_NS
);
let parsed = super::CT_Document::from_xml(xml.as_bytes()).unwrap();
let saved = parsed.to_xml().unwrap();
assert!(std::str::from_utf8(&saved).unwrap().contains(&payload));
super::CT_Document::from_xml(&saved).unwrap();
}
}
#[test]
fn fx211_ruby_and_vml_owner_resource_routes() {
let anchor = "<w:drawing><wp:anchor><wp:docPr id=\"1\"/><w:txbxContent><w:p><w:r><w:t>box</w:t></w:r></w:p></w:txbxContent></wp:anchor></w:drawing>";
let ruby = format!(
"<w:p><w:ruby><w:rt><w:r>{anchor}</w:r></w:rt><w:rubyBase><w:r><w:t>base</w:t></w:r></w:rubyBase></w:ruby></w:p>"
);
let vml = "<w:p><w:r><w:pict><v:rect><v:textbox><w:txbxContent><w:p><w:r><w:t>VML</w:t></w:r></w:p></w:txbxContent></v:textbox></v:rect></w:pict></w:r></w:p>";
let wrap = |payload: &str, controls: usize| {
format!(
"<w:document xmlns:w=\"{}\" xmlns:wp=\"http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing\" xmlns:v=\"urn:schemas-microsoft-com:vml\" xmlns:x=\"urn:producer\"><w:body>{}{payload}{}</w:body></w:document>",
crate::namespace::W_NS,
"<w:sdt><w:sdtContent>".repeat(controls),
"</w:sdtContent></w:sdt>".repeat(controls)
)
};
for payload in [&ruby, vml] {
super::admit_word_xml(wrap(payload, 240).as_bytes(), &[], None).unwrap();
assert!(super::admit_word_xml(wrap(payload, 241).as_bytes(), &[], None).is_err());
}
let parsed = super::CT_Document::from_xml(wrap(&ruby, 0).as_bytes()).unwrap();
let paragraph = parsed.body.paragraphs().next().unwrap();
assert_eq!(paragraph.rubies.len(), 1);
assert!(
matches!(¶graph.rubies[0].ruby_text[0].content[0], crate::text::RunContent::Drawing(d) if d.anchor.as_ref().unwrap().shape.as_ref().unwrap().text_body.is_some())
);
let opaque = format!(
"<w:p><w:ruby x:opaque=\"kept\"><w:rt><w:r><w:drawing><wp:anchor><wp:docPr id=\"1\"/><w:txbxContent>{}</w:txbxContent></wp:anchor></w:drawing></w:r></w:rt><w:rubyBase><w:r><w:t>base</w:t></w:r></w:rubyBase></w:ruby></w:p>",
fx211_control_xml(257)
);
let parsed = super::CT_Document::from_xml(wrap(&opaque, 0).as_bytes()).unwrap();
assert!(
std::str::from_utf8(&parsed.to_xml().unwrap())
.unwrap()
.contains(&opaque)
);
}
#[test]
fn fx211_public_reader_owner_refusal_keeps_cursor() {
let start = "<w:p><w:r><w:drawing><wp:anchor xmlns:wp=\"http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing\"><wp:docPr id=\"1\"/><w:txbxContent>";
let end = "</w:txbxContent></wp:anchor></w:drawing></w:r></w:p>";
let deep = format!("{}<w:p/><!---->{}", start.repeat(17), end.repeat(17));
let run = deep
.strip_prefix("<w:p>")
.unwrap()
.strip_suffix("</w:p>")
.unwrap();
let drawing = run
.strip_prefix("<w:r>")
.unwrap()
.strip_suffix("</w:r>")
.unwrap();
let anchor = drawing
.strip_prefix("<w:drawing>")
.unwrap()
.strip_suffix("</w:drawing>")
.unwrap();
for (owner, content) in [
("body", deep.clone()),
("p", run.to_owned()),
("r", drawing.to_owned()),
("drawing", anchor.to_owned()),
("tbl", format!("<w:tr><w:tc>{deep}</w:tc></w:tr>")),
("tr", format!("<w:tc>{deep}</w:tc>")),
("tc", deep.clone()),
("sdt", format!("<w:sdtContent>{deep}</w:sdtContent>")),
] {
let source = format!(
"<w:{owner} xmlns:w=\"{}\">{content}</w:{owner}><w:p/>",
crate::namespace::W_NS
);
let mut reader = quick_xml::Reader::from_reader(source.as_bytes());
let quick_xml::events::Event::Start(start) = reader.read_event().unwrap() else {
panic!("expected start");
};
let before = reader.buffer_position();
let remaining = *reader.get_ref();
let result = match owner {
"body" => super::CT_Body::from_xml(&mut reader).map(|_| ()),
"p" => crate::text::CT_P::from_xml(&mut reader).map(|_| ()),
"r" => crate::text::CT_R::from_xml(&mut reader).map(|_| ()),
"drawing" => crate::drawing::CT_Drawing::from_xml(&mut reader).map(|_| ()),
"tbl" => crate::table::CT_Tbl::from_xml(&mut reader).map(|_| ()),
"tr" => crate::table::CT_Row::from_xml(&mut reader).map(|_| ()),
"tc" => crate::table::CT_Tc::from_xml(&mut reader).map(|_| ()),
"sdt" => crate::content_control::CT_Sdt::from_xml(&mut reader, &start).map(|_| ()),
_ => unreachable!(),
};
let error = result.unwrap_err();
assert!(
error.to_string().contains("weighted limit 256"),
"{owner}: {error}"
);
assert_eq!(reader.buffer_position(), before, "{owner}");
assert_eq!(*reader.get_ref(), remaining, "{owner}");
}
let source = format!(
"{}<w:p/>",
anchor.replacen(
"<wp:anchor ",
&format!("<wp:anchor xmlns:w=\"{}\" ", crate::namespace::W_NS),
1
)
);
let mut reader = quick_xml::NsReader::from_reader(source.as_bytes());
let mut buffer = Vec::new();
let quick_xml::events::Event::Start(start) = reader.read_event_into(&mut buffer).unwrap()
else {
panic!("expected anchor");
};
let before = reader.buffer_position();
assert!(
crate::drawing::CT_Anchor::from_xml(&mut reader, &start)
.unwrap_err()
.to_string()
.contains("weighted limit 256")
);
assert_eq!(reader.buffer_position(), before);
}
#[test]
fn fx211_decoded_word_namespace_resource_accounting() {
let namespace = crate::namespace::W_NS.replacen('/', "/", 1);
let wrap = |depth| {
format!(
"<w:document xmlns:w=\"{namespace}\"><w:body>{}</w:body></w:document>",
fx211_control_xml(depth)
)
};
let parsed = super::CT_Document::from_xml(wrap(2).as_bytes()).unwrap();
let super::BodyContent::ContentControl(control) = &parsed.body.content[0] else {
panic!("expected decoded qualified control");
};
assert!(matches!(
&control.content[0],
crate::content_control::SdtContent::ContentControl(_)
));
println!("actual decoded qualified controls=2");
assert!(super::admit_word_xml(wrap(257).as_bytes(), &[], None).is_err());
}
#[test]
fn fx211_direct_foreign_anchor_actual_owned_route() {
let start = "<w:p><w:r><w:drawing><x:anchor><wp:docPr id=\"1\"/><w:txbxContent>";
let end = "</w:txbxContent></x:anchor></w:drawing></w:r></w:p>";
let wrap = |depth| {
format!(
"<w:document xmlns:w=\"{}\" xmlns:wp=\"http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing\" xmlns:x=\"urn:producer\"><w:body>{}<w:p/><!---->{}</w:body></w:document>",
crate::namespace::W_NS,
start.repeat(depth),
end.repeat(depth)
)
};
let parsed = super::CT_Document::from_xml(wrap(1).as_bytes()).unwrap();
let paragraph = parsed.body.paragraphs().next().unwrap();
let crate::text::RunContent::Drawing(drawing) = ¶graph.runs[0].content[0] else {
panic!("expected drawing");
};
assert!(
drawing
.anchor
.as_ref()
.unwrap()
.shape
.as_ref()
.unwrap()
.text_body
.is_some()
);
println!("actual direct legacy foreign anchor typed boxes=1");
assert!(super::admit_word_xml(wrap(17).as_bytes(), &[], None).is_err());
}
#[test]
fn fx211_captured_namespace_origin_and_sibling_scope() {
let encoded_word = crate::namespace::W_NS.replacen('/', "/", 1);
let encoded_mc = crate::namespace::MC_NS.replacen('/', "/", 1);
let encoded_wp =
"http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing";
let deep = fx211_control_xml(257);
let anchor = |binding: &str| {
format!(
"<wp:anchor {binding}><wp:docPr id=\"1\"/><w:txbxContent>{deep}</w:txbxContent></wp:anchor>"
)
};
let wrap = |content: &str, bindings: &str| {
format!(
"<w:document xmlns:w=\"{}\" xmlns:wp=\"http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing\" xmlns:mc=\"{}\" {bindings}><w:body>{content}</w:body></w:document>",
crate::namespace::W_NS,
crate::namespace::MC_NS
)
};
let opaque = format!(
"<w:p><w:r><w:drawing>{}</w:drawing></w:r></w:p>",
anchor(&format!("xmlns:w=\"{encoded_word}\""))
);
let source = wrap(&opaque, "");
let parsed = super::CT_Document::from_xml(source.as_bytes()).unwrap();
let paragraph = parsed.body.paragraphs().next().unwrap();
let crate::text::RunContent::Drawing(drawing) = ¶graph.runs[0].content[0] else {
panic!("expected drawing");
};
assert!(drawing.anchor.as_ref().unwrap().shape.as_ref().is_none());
assert!(
std::str::from_utf8(&parsed.to_xml().unwrap())
.unwrap()
.contains(&opaque)
);
let inherited = wrap(
&opaque.replace(&format!(" xmlns:w=\"{encoded_word}\""), ""),
"",
)
.replace(
&format!("xmlns:w=\"{}\"", crate::namespace::W_NS),
&format!("xmlns:w=\"{encoded_word}\""),
);
assert!(super::admit_word_xml(inherited.as_bytes(), &[], None).is_err());
let alternate = |binding: &str, shape: &str| {
format!(
"<w:p><w:r><mc:AlternateContent {binding}><mc:Choice Requires=\"wp\"><w:drawing>{shape}</w:drawing></mc:Choice></mc:AlternateContent></w:r></w:p>"
)
};
let own_encoded_mc = alternate(&format!("xmlns:mc=\"{encoded_mc}\""), &anchor(""));
let parsed = super::CT_Document::from_xml(wrap(&own_encoded_mc, "").as_bytes()).unwrap();
assert!(
std::str::from_utf8(&parsed.to_xml().unwrap())
.unwrap()
.contains(&own_encoded_mc)
);
let own_encoded_wp = alternate("", &anchor(&format!("xmlns:wp=\"{encoded_wp}\"")));
let parsed = super::CT_Document::from_xml(wrap(&own_encoded_wp, "").as_bytes()).unwrap();
assert!(
std::str::from_utf8(&parsed.to_xml().unwrap())
.unwrap()
.contains(&own_encoded_wp)
);
let inherited_wp = wrap(&alternate("", &anchor("")), "").replace(
"xmlns:wp=\"http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing\"",
&format!("xmlns:wp=\"{encoded_wp}\""),
);
assert!(super::admit_word_xml(inherited_wp.as_bytes(), &[], None).is_err());
let sibling = wrap(
&format!("<w:p xmlns:w=\"urn:producer\"/>{}<w:p>{deep}</w:p>", opaque),
"",
);
assert!(super::admit_word_xml(sibling.as_bytes(), &[], None).is_err());
}
#[test]
fn fx211_anchor_opaque_table_wrappers_do_not_consume_modeled_limit() {
let anchor = format!(
"<wp:anchor xmlns:w=\"{}\" xmlns:wp=\"http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing\"><wp:docPr id=\"1\"/>{}<w:p/><z:opaque xmlns:z=\"urn:opaque\">retained marker</z:opaque>{}</wp:anchor>",
crate::namespace::W_NS,
"<w:tbl>".repeat(33),
"</w:tbl>".repeat(33)
);
let drawing_source = format!("<w:drawing>{anchor}</w:drawing>");
let mut reader = quick_xml::Reader::from_str(&drawing_source);
assert!(matches!(
reader.read_event().unwrap(),
quick_xml::events::Event::Start(_)
));
let drawing = crate::drawing::CT_Drawing::from_xml_with_prefixes(
&mut reader,
&["w".into()],
&["w".into()],
)
.unwrap();
assert!(drawing.anchor.as_ref().unwrap().shape.is_none());
super::admit_word_model(super::WordOwner::Drawing(&drawing)).unwrap();
let mut saved_drawing = Vec::new();
drawing
.to_xml(&mut quick_xml::Writer::new(&mut saved_drawing))
.unwrap();
assert!(
saved_drawing
.windows(anchor.len())
.any(|bytes| bytes == anchor.as_bytes())
);
println!(
"actual native anchor opaque table wrappers=33 typed tables=0 exact source retained"
);
let wrap = |drawing: &str| {
format!(
"<w:document xmlns:w=\"{}\"><w:body><w:p><w:r>{drawing}</w:r></w:p></w:body></w:document>",
crate::namespace::W_NS
)
};
let source = wrap(&drawing_source);
super::admit_word_xml(source.as_bytes(), &[], None).unwrap();
let parsed = super::CT_Document::from_xml(source.as_bytes()).unwrap();
let saved = parsed.to_xml().unwrap();
assert!(
saved
.windows(anchor.len())
.any(|bytes| bytes == anchor.as_bytes())
);
let reopened = super::CT_Document::from_xml(&saved).unwrap();
assert_eq!(reopened.to_xml().unwrap(), saved);
let typed_anchor = format!(
"<wp:anchor xmlns:w=\"{}\" xmlns:wp=\"http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing\"><wp:docPr id=\"1\"/><w:txbxContent>{}<w:p/>{}</w:txbxContent></wp:anchor>",
crate::namespace::W_NS,
"<w:tbl><w:tr><w:tc>".repeat(33),
"</w:tc></w:tr></w:tbl>".repeat(33)
);
assert!(
super::admit_word_xml(
wrap(&format!("<w:drawing>{typed_anchor}</w:drawing>")).as_bytes(),
&[],
None
)
.is_err()
);
}
#[test]
fn fx211_table_source_limit_is_inclusive_and_survives_owner_captures() {
let wrap = |payload: &str| {
format!(
"<w:document xmlns:w=\"{}\" xmlns:wp=\"http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing\"><w:body>{payload}</w:body></w:document>",
crate::namespace::W_NS
)
};
let table = |depth: usize| {
format!(
"{}<w:p/>{}",
"<w:tbl><w:tr><w:tc>".repeat(depth),
"</w:tc></w:tr></w:tbl>".repeat(depth)
)
};
for depth in [32, 33] {
let payload = table(depth);
let result = super::admit_word_xml(wrap(&payload).as_bytes(), &[], None);
assert_eq!(result.is_ok(), depth == 32, "source table depth {depth}");
let controlled = format!(
"{}<w:sdt><w:sdtContent>{}</w:sdtContent></w:sdt>{}",
"<w:tbl><w:tr><w:tc>".repeat(depth - 1),
table(1),
"</w:tc></w:tr></w:tbl>".repeat(depth - 1)
);
assert_eq!(
super::admit_word_xml(wrap(&controlled).as_bytes(), &[], None).is_ok(),
depth == 32
);
let boxed = controlled
.replace(
"<w:sdt><w:sdtContent>",
"<w:p><w:r><w:drawing><wp:anchor><wp:docPr id=\"1\"/><w:txbxContent>",
)
.replace(
"</w:sdtContent></w:sdt>",
"</w:txbxContent></wp:anchor></w:drawing></w:r></w:p>",
);
assert_eq!(
super::admit_word_xml(wrap(&boxed).as_bytes(), &[], None).is_ok(),
depth == 32
);
}
super::admit_word_xml(
wrap(&format!("{}{}", table(32), table(32))).as_bytes(),
&[],
None,
)
.unwrap();
let foreign = format!(
"<x:opaque xmlns:x=\"urn:opaque\">{}</x:opaque><w:p/>",
table(33)
);
super::admit_word_xml(wrap(&foreign).as_bytes(), &[], None).unwrap();
}
#[test]
fn fx211_table_32_actual_model_lifecycle_and_33_writer_refusal() {
let mut table = crate::table::CT_Tbl::new();
for _ in 1..32 {
let mut outer = crate::table::CT_Tbl::new();
let mut row = crate::table::CT_Row::new();
let mut cell = crate::table::CT_Tc::new();
cell.content = vec![crate::table::CellContent::Table(table)];
row.cells = vec![cell];
outer.rows = vec![row];
table = outer;
}
let count_tables = |root: &crate::table::CT_Tbl| {
let mut pending = vec![root];
let mut count = 0;
while let Some(table) = pending.pop() {
count += 1;
for row in &table.rows {
for cell in &row.cells {
for child in &cell.content {
if let crate::table::CellContent::Table(table) = child {
pending.push(table);
}
}
}
}
}
count
};
assert_eq!(count_tables(&table), 32);
super::admit_word_model(super::WordOwner::Table(&table)).unwrap();
let mut model = super::CT_Document::from_xml(
format!(
"<w:document xmlns:w=\"{}\"><w:body/></w:document>",
crate::namespace::W_NS
)
.as_bytes(),
)
.unwrap();
model.body.content = vec![super::BodyContent::Table(table)];
let clone = model.clone();
let saved = clone.to_xml().unwrap();
let reopened = super::CT_Document::from_xml(&saved).unwrap();
reopened.validate_resource_limits().unwrap();
let super::BodyContent::Table(reopened_table) = &reopened.body.content[0] else {
panic!("reopened table");
};
assert_eq!(count_tables(reopened_table), 32);
assert_eq!(reopened.to_xml().unwrap(), saved);
println!("actual authored32 clone/save/source-parse/reopen qualified");
drop(reopened);
drop(clone);
let super::BodyContent::Table(inner) = model.body.content.remove(0) else {
panic!("table");
};
let mut table = crate::table::CT_Tbl::new();
let mut row = crate::table::CT_Row::new();
let mut cell = crate::table::CT_Tc::new();
cell.content = vec![crate::table::CellContent::Table(inner)];
row.cells = vec![cell];
table.rows = vec![row];
assert!(super::admit_word_model(super::WordOwner::Table(&table)).is_err());
let mut output = vec![9, 8, 7];
assert!(
table
.to_xml(&mut quick_xml::Writer::new(&mut output))
.is_err()
);
assert_eq!(output, vec![9, 8, 7]);
}
#[test]
fn fx211_authored_table_guard_refuses_existing_limit_without_serializing() {
let mut outcomes = Vec::new();
for depth in [33, 128] {
let mut table = crate::table::CT_Tbl::new();
for _ in 1..depth {
let mut outer = crate::table::CT_Tbl::new();
let mut row = crate::table::CT_Row::new();
let mut cell = crate::table::CT_Tc::new();
cell.content = vec![crate::table::CellContent::Table(table)];
row.cells = vec![cell];
outer.rows = vec![row];
table = outer;
}
let result = super::admit_word_model(super::WordOwner::Table(&table));
println!("authored tables={depth} guarded result={result:?}");
let mut pending = vec![table];
while let Some(table) = pending.pop() {
for row in table.rows {
for cell in row.cells {
for child in cell.content {
if let crate::table::CellContent::Table(table) = child {
pending.push(table);
}
}
}
}
}
outcomes.push(result);
}
for result in outcomes {
assert!(
result.is_err(),
"authored table limit must apply before recursion"
);
}
}
#[test]
fn fx211_authored_model_refusal_precedes_writer_output() {
let wrap = |payload: &str| {
format!(
"<w:document xmlns:w=\"{}\"><w:body>{payload}</w:body></w:document>",
crate::namespace::W_NS
)
};
let mut accepted =
super::CT_Document::from_xml(wrap(&fx211_control_xml(1)).as_bytes()).unwrap();
let super::BodyContent::ContentControl(template) = &accepted.body.content[0] else {
panic!("expected template");
};
let template = template.clone();
let mut inner = template.clone();
for _ in 1..256 {
let mut outer = template.clone();
outer.content = vec![crate::content_control::SdtContent::ContentControl(inner)];
inner = outer;
}
accepted.body.content = vec![super::BodyContent::ContentControl(inner)];
println!("authored256 constructed iteratively");
accepted.validate_resource_limits().unwrap();
println!("authored256 borrowed admission passed");
let mut outer =
super::CT_Document::from_xml(wrap(&fx211_control_xml(1)).as_bytes()).unwrap();
let super::BodyContent::ContentControl(control) = &mut outer.body.content[0] else {
panic!("expected control");
};
let super::BodyContent::ContentControl(inner) = accepted.body.content.remove(0) else {
panic!("expected inner control");
};
control.content = vec![crate::content_control::SdtContent::ContentControl(inner)];
assert!(outer.validate_resource_limits().is_err());
assert!(outer.to_xml().is_err());
let mut bytes = vec![1, 2, 3];
assert!(
outer
.body
.to_xml(&mut quick_xml::Writer::new(&mut bytes))
.is_err()
);
assert_eq!(bytes, vec![1, 2, 3]);
}
#[test]
fn fx211_resource_admission_inclusive_control_boundary_and_sibling_reset() {
let accepted = fx211_control_xml(256);
let refused = fx211_control_xml(257);
super::admit_word_xml(accepted.as_bytes(), &["w".into()], None).unwrap();
assert!(super::admit_word_xml(refused.as_bytes(), &["w".into()], None).is_err());
let siblings = format!("<w:body>{accepted}{accepted}</w:body>");
super::admit_word_xml(siblings.as_bytes(), &["w".into()], None).unwrap();
assert!(super::admit_word_xml(refused.as_bytes(), &["w".into()], None).is_err());
super::admit_word_xml(accepted.as_bytes(), &["w".into()], None).unwrap();
}
#[test]
fn fx211_resource_admission_respects_opaque_and_rebound_owner_contexts() {
let deep = fx211_control_xml(257);
for opaque in [
format!(
"<w:body xmlns:w=\"{}\"><x:opaque xmlns:x=\"urn:producer\">{deep}</x:opaque></w:body>",
crate::namespace::W_NS
),
format!(
"<w:body xmlns:w=\"{}\"><w:producerExtension>{deep}</w:producerExtension></w:body>",
crate::namespace::W_NS
),
format!(
"<w:body xmlns:w=\"{}\"><w:sdt xmlns:w=\"urn:producer\">{deep}</w:sdt></w:body>",
crate::namespace::W_NS
),
] {
super::admit_word_xml(opaque.as_bytes(), &[], None).unwrap();
}
let inline_opaque = format!(
"<w:p xmlns:w=\"{}\"><w:sdt><w:sdtContent><w:p>{deep}</w:p></w:sdtContent></w:sdt></w:p>",
crate::namespace::W_NS
);
super::admit_word_xml(inline_opaque.as_bytes(), &[], None).unwrap();
let alias = deep.replace("w:", "q:");
let qualified = format!(
"<q:body xmlns:q=\"{}\">{alias}</q:body>",
crate::namespace::W_NS
);
assert!(super::admit_word_xml(qualified.as_bytes(), &[], None).is_err());
let default = deep.replace("w:", "");
let qualified = format!(
"<body xmlns=\"{}\">{default}</body>",
crate::namespace::W_NS
);
assert!(super::admit_word_xml(qualified.as_bytes(), &[], None).is_err());
}
#[test]
fn fx211_inline_shape_is_opaque_but_anchor_body_is_typed() {
let payload = fx211_control_xml(257);
let drawing = |owner: &str, payload: &str| {
format!(
"<wp:{owner}><wp:extent cx=\"10\" cy=\"20\"/><wp:docPr id=\"1\" name=\"owned\"/><a:graphic><a:graphicData uri=\"urn:producer-route\"><wps:wsp><wps:txbx><w:txbxContent>{payload}</w:txbxContent></wps:txbx></wps:wsp></a:graphicData></a:graphic></wp:{owner}>"
)
};
let wrap = |drawing: &str| {
format!(
"<w:document xmlns:w=\"{}\" xmlns:wp=\"http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing\" xmlns:a=\"http://schemas.openxmlformats.org/drawingml/2006/main\" xmlns:wps=\"http://schemas.microsoft.com/office/word/2010/wordprocessingShape\"><w:body><w:p><w:r><w:drawing>{drawing}</w:drawing></w:r></w:p></w:body></w:document>",
crate::namespace::W_NS
)
};
let opaque = drawing("inline", &payload);
let source = wrap(&opaque);
let parsed = super::CT_Document::from_xml(source.as_bytes()).unwrap();
let saved = parsed.to_xml().unwrap();
let saved = std::str::from_utf8(&saved).unwrap();
assert!(
saved.contains(&opaque),
"opaque inline source remains exact"
);
let reopened = super::CT_Document::from_xml(saved.as_bytes()).unwrap();
assert_eq!(reopened.to_xml().unwrap(), saved.as_bytes());
assert!(
super::CT_Document::from_xml(wrap(&drawing("anchor", &payload)).as_bytes()).is_err()
);
let accepted = wrap(&drawing(
"anchor",
"<w:p><w:r><w:t>typed leaf</w:t></w:r></w:p>",
));
let parsed = super::CT_Document::from_xml(accepted.as_bytes()).unwrap();
let paragraph = parsed.body.paragraphs().next().unwrap();
let run = paragraph.runs().into_iter().next().unwrap();
let crate::text::RunContent::Drawing(drawing) = &run.content[0] else {
panic!("expected native drawing");
};
let body = drawing
.anchor
.as_ref()
.unwrap()
.shape
.as_ref()
.unwrap()
.text_body
.as_ref()
.unwrap();
assert_eq!(body.paragraphs().next().unwrap().text(), "typed leaf");
}
#[test]
fn fx211_resource_admission_refuses_before_related_story_construction() {
let deep = fx211_control_xml(257);
let wrap = |root: &str, payload: &str| {
format!(
"<w:{root} xmlns:w=\"{}\">{payload}</w:{root}>",
crate::namespace::W_NS
)
};
assert!(
super::CT_Document::from_xml(wrap("document", &wrap("body", &deep)).as_bytes())
.is_err()
);
assert!(crate::header_footer::CT_HdrFtr::from_xml(wrap("hdr", &deep).as_bytes()).is_err());
assert!(
crate::comments::CT_Comments::from_xml(
wrap("comments", &wrap("comment", &deep)).as_bytes()
)
.is_err()
);
assert!(
crate::footnotes::CT_Footnotes::from_xml(
wrap("footnotes", &wrap("footnote", &deep)).as_bytes()
)
.is_err()
);
assert!(
crate::footnotes::CT_Footnotes::from_xml(
wrap("endnotes", &wrap("endnote", &deep)).as_bytes()
)
.is_err()
);
assert!(
crate::glossary::CT_GlossaryDocument::from_xml(
wrap(
"glossaryDocument",
&wrap("docParts", &wrap("docPart", &wrap("docPartBody", &deep)))
)
.as_bytes()
)
.is_err()
);
}
#[test]
fn a_document_with_an_unmodelled_enumerated_value_still_opens() {
let cases: [(&str, &str); 8] = [
(
"ST_Jc",
r#"<w:pPr><w:jc w:val="thaiDistribute"/><w:keepNext/></w:pPr>"#,
),
(
"ST_Underline",
r#"<w:pPr><w:keepNext/></w:pPr><w:r><w:rPr><w:u w:val="wavyHeavy"/><w:b/></w:rPr><w:t>x</w:t></w:r>"#,
),
(
"ST_HighlightColor",
r#"<w:pPr><w:keepNext/></w:pPr><w:r><w:rPr><w:highlight w:val="chartreuse"/><w:b/></w:rPr><w:t>x</w:t></w:r>"#,
),
(
"ST_Border",
r#"<w:pPr><w:keepNext/><w:pBdr><w:top w:val="tribal4" w:sz="8"/></w:pBdr></w:pPr>"#,
),
(
"ST_TabJc",
r#"<w:pPr><w:keepNext/><w:tabs><w:tab w:val="numbering" w:pos="720"/></w:tabs></w:pPr>"#,
),
(
"ST_TabLeader",
r#"<w:pPr><w:keepNext/><w:tabs><w:tab w:val="left" w:pos="720" w:leader="middleDot"/></w:tabs></w:pPr>"#,
),
(
"ST_SectionType",
r#"<w:pPr><w:keepNext/><w:sectPr><w:type w:val="oddPage2"/></w:sectPr></w:pPr>"#,
),
(
"ST_PageOrientation",
r#"<w:pPr><w:keepNext/><w:sectPr><w:pgSz w:w="12240" w:h="15840" w:orient="sideways"/></w:sectPr></w:pPr>"#,
),
];
for (name, body) in cases {
let xml = format!(
r#"<?xml version="1.0"?>
<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:body><w:p>{body}</w:p></w:body></w:document>"#
);
let document = CT_Document::from_xml(xml.as_bytes()).unwrap_or_else(|e| {
panic!("{name}: an unmodelled value must not fail the open, got {e}")
});
let BodyContent::Paragraph(paragraph) = &document.body.content[0] else {
panic!("{name}: expected a paragraph");
};
let properties = paragraph
.properties
.as_ref()
.unwrap_or_else(|| panic!("{name}: properties survive"));
assert_eq!(
properties.keep_next,
Some(true),
"{name}: an unmodelled value must not cost the element its siblings"
);
}
}
#[test]
fn an_unmodelled_value_leaves_the_property_unset() {
let xml = br#"<?xml version="1.0"?>
<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:body><w:p><w:pPr><w:jc w:val="thaiDistribute"/></w:pPr></w:p></w:body></w:document>"#;
let document = CT_Document::from_xml(xml).expect("opens");
let BodyContent::Paragraph(paragraph) = &document.body.content[0] else {
panic!("expected a paragraph");
};
assert_eq!(paragraph.properties.as_ref().unwrap().jc, None);
}
use super::*;
#[test]
fn parses_empty_body_paragraph_and_first_section_properties_as_modeled_content() {
let xml = format!(
r#"<w:document xmlns:w="{W_NS}"><w:body><w:p/><w:sectPr/><w:sectPr><w:titlePg/></w:sectPr></w:body></w:document>"#
);
let document = CT_Document::from_xml(xml.as_bytes()).unwrap();
assert!(matches!(
document.body.content.as_slice(),
[BodyContent::Paragraph(_), BodyContent::RawXml(raw)]
if raw.windows(b"<w:titlePg/>".len())
.any(|window| window == b"<w:titlePg/>")
));
let section = document.body.sect_pr.expect("first section is retained");
assert_eq!(section.title_pg, None);
}
#[test]
fn start_end_section_leaves_keep_unsupported_attributes_opaque() {
let xml = format!(
r#"<w:document xmlns:w="{W_NS}" xmlns:r="{}" xmlns:x="urn:producer"><w:body><w:sectPr><w:headerReference r:id="rId1" x:flag="keep"></w:headerReference><w:pgSz w:w="12240" w:h="15840" x:flag="keep"></w:pgSz><w:pgMar w:top="720" x:flag="not-a-number"></w:pgMar></w:sectPr></w:body></w:document>"#,
crate::namespace::R_NS,
);
let document = CT_Document::from_xml(xml.as_bytes()).expect("document parses");
let section = document.body.sect_pr.expect("section is retained");
assert!(section.header_refs.is_empty());
assert_eq!(section.page_width, None);
assert_eq!(section.margin_top, None);
assert_eq!(section.extra_xml.len(), 3);
assert!(section.extra_xml.iter().all(|raw| {
raw.windows(b"x:flag=\"".len())
.any(|window| window == b"x:flag=\"")
}));
}
#[test]
fn expanded_section_children_preserve_modeled_facts_and_relationship_namespaces() {
let xml = format!(
r#"<w:document xmlns:w="{W_NS}" xmlns:q="{}" xmlns:x="urn:foreign"><w:body><w:sectPr><w:headerReference w:type="default" q:id="rId1"></w:headerReference><w:footerReference w:type="even" x:id="decoy"></w:footerReference><w:pgSz w:w="12240" w:h="15840"></w:pgSz><w:pgMar w:top="720"></w:pgMar></w:sectPr></w:body></w:document>"#,
crate::namespace::R_NS,
);
let document = CT_Document::from_xml(xml.as_bytes()).expect("document parses");
let section = document.body.sect_pr.expect("section is retained");
assert_eq!(section.page_width, Some(Twips(12240)));
assert_eq!(section.page_height, Some(Twips(15840)));
assert_eq!(section.margin_top, Some(Twips(720)));
assert_eq!(section.header_refs[0].rel_id, "rId1");
assert!(section.footer_refs.is_empty());
}
#[test]
fn document_reader_rejects_truncated_multiple_and_foreign_roots() {
let truncated = format!(r#"<w:document xmlns:w="{W_NS}"><w:body><w:p/>"#);
assert!(CT_Document::from_xml(truncated.as_bytes()).is_err());
let multiple_roots = format!(
r#"<w:document xmlns:w="{W_NS}"><w:body/></w:document><w:document xmlns:w="{W_NS}"><w:body/></w:document>"#
);
assert!(CT_Document::from_xml(multiple_roots.as_bytes()).is_err());
let foreign =
format!(r#"<x:document xmlns:x="urn:foreign" xmlns:w="{W_NS}"><x:body/></x:document>"#);
assert!(CT_Document::from_xml(foreign.as_bytes()).is_err());
let text_outside = format!(r#"outside<w:document xmlns:w="{W_NS}"><w:body/></w:document>"#);
assert!(CT_Document::from_xml(text_outside.as_bytes()).is_err());
let empty_body = format!(r#"<w:document xmlns:w="{W_NS}"><w:body/></w:document>"#);
assert_eq!(
CT_Document::from_xml(empty_body.as_bytes())
.expect("self-closing body parses")
.body,
CT_Body::new()
);
}
#[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 foreign_document_background_lookalikes_remain_untyped() {
for raw in [
r#"<ext:background ext:color="red"/>"#,
r#"<ext:background ext:color="red"><ext:payload/></ext:background>"#,
] {
let xml = format!(
r#"<w:document xmlns:w="{W_NS}" xmlns:ext="urn:producer">{raw}<w:body><w:p/></w:body></w:document>"#
);
let document = CT_Document::from_xml(xml.as_bytes()).expect("document opens");
assert!(document.background_xml.is_none());
let written = document.to_xml().expect("document writes");
assert!(
written
.windows(raw.len())
.any(|window| window == raw.as_bytes()),
"foreign background bytes survive exactly"
);
let reopened = CT_Document::from_xml(&written).expect("written document reopens");
assert!(reopened.background_xml.is_none());
assert_eq!(reopened.background_extra_xml, vec![raw.as_bytes().to_vec()]);
}
}
#[test]
fn root_attributes_survive_a_rewrite_after_the_namespace_declarations() {
let xml = format!(
r#"<w:document xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:w="{W_NS}" xmlns:w14="http://schemas.microsoft.com/office/word/2010/wordml" mc:Ignorable="w14" xmlns:x="urn:producer" x:root="a & b"><w:body><w:p/></w:body></w:document>"#
);
let parsed = CT_Document::from_xml(xml.as_bytes()).unwrap();
assert_eq!(
parsed.root_attributes,
[
("mc:Ignorable".to_owned(), "w14".to_owned()),
("x:root".to_owned(), "a & b".to_owned()),
]
);
let written = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
let root = &written[written.find("<w:document").unwrap()..];
let root = &root[..=root.find('>').unwrap()];
assert!(
root.ends_with(
r#" xmlns:w14="http://schemas.microsoft.com/office/word/2010/wordml" xmlns:x="urn:producer" mc:Ignorable="w14" x:root="a & b">"#
),
"{root}"
);
let reparsed = CT_Document::from_xml(written.as_bytes()).unwrap();
assert_eq!(reparsed.root_attributes, parsed.root_attributes);
assert_eq!(reparsed.to_xml().unwrap(), written.as_bytes());
}
#[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 body_owner_bindings_reach_direct_table_descendants_after_redundant_redeclaration() {
let xml = format!(
r#"<w:document xmlns:w="{W_NS}">
<w:body xmlns:ext="urn:body-owner">
<w:tbl xmlns:ext="urn:body-owner">
<w:tblPr>
<w:tblBorders>
<ext:diagonal ext:style="kept"/>
</w:tblBorders>
</w:tblPr>
<w:tblGrid><w:gridCol w:w="5000"/></w:tblGrid>
</w:tbl>
</w:body>
</w:document>"#
);
let document = CT_Document::from_xml(xml.as_bytes()).expect("document parses");
let written = String::from_utf8(document.to_xml().expect("document writes"))
.expect("document XML is UTF-8");
assert!(
written.contains(r#"<ext:diagonal ext:style="kept" xmlns:ext="urn:body-owner"/>"#),
"body binding survives a redundant table declaration: {written}"
);
let reopened = CT_Document::from_xml(written.as_bytes()).expect("document reopens");
let BodyContent::Table(table) = &reopened.body.content[0] else {
panic!("direct table reopens as typed content");
};
assert_eq!(
table
.properties
.as_ref()
.and_then(|properties| properties.borders.as_ref())
.expect("table borders reopen")
.extra_xml,
vec![br#"<ext:diagonal ext:style="kept" xmlns:ext="urn:body-owner"/>"#.to_vec()]
);
}
#[test]
fn body_owner_bindings_survive_body_content_control_table_projection() {
let xml = format!(
r#"<w:document xmlns:w="{W_NS}">
<w:body xmlns:ext="urn:body-control">
<w:sdt><w:sdtContent>
<w:tbl>
<w:tblPr>
<w:tblBorders><ext:diagonal ext:style="kept"/></w:tblBorders>
</w:tblPr>
<w:tblGrid><w:gridCol w:w="5000"/></w:tblGrid>
</w:tbl>
</w:sdtContent></w:sdt>
</w:body>
</w:document>"#
);
let document = CT_Document::from_xml(xml.as_bytes()).expect("document parses");
let written = String::from_utf8(document.to_xml().expect("document writes"))
.expect("document XML is UTF-8");
assert!(
written.contains(r#"xmlns:ext="urn:body-control""#),
"{written}"
);
assert!(written.contains("<ext:diagonal"), "{written}");
let reopened = CT_Document::from_xml(written.as_bytes()).expect("document reopens");
assert!(
matches!(reopened.body.content[0], BodyContent::ContentControl(_)),
"body control remains typed"
);
let rewritten = String::from_utf8(reopened.to_xml().expect("document rewrites"))
.expect("document XML is UTF-8");
assert!(
rewritten.contains(r#"xmlns:ext="urn:body-control""#),
"{rewritten}"
);
assert!(rewritten.contains("<ext:diagonal"), "{rewritten}");
}
#[test]
fn body_owner_bindings_survive_direct_raw_children() {
let xml = format!(
r#"<w:document xmlns:w="{W_NS}">
<w:body xmlns:ext="urn:body-raw">
<ext:block ext:value="kept"><ext:nested/></ext:block>
<ext:empty ext:value="kept"/>
</w:body>
</w:document>"#
);
let document = CT_Document::from_xml(xml.as_bytes()).expect("document parses");
let written = String::from_utf8(document.to_xml().expect("document writes"))
.expect("document XML is UTF-8");
assert!(
written.contains(
r#"<ext:block ext:value="kept" xmlns:ext="urn:body-raw"><ext:nested/></ext:block>"#
),
"body binding reaches a raw start child: {written}"
);
assert!(
written.contains(r#"<ext:empty ext:value="kept" xmlns:ext="urn:body-raw"/>"#),
"body binding reaches a raw empty child: {written}"
);
let reopened = CT_Document::from_xml(written.as_bytes()).expect("document reopens");
let rewritten = String::from_utf8(reopened.to_xml().expect("document rewrites"))
.expect("document XML is UTF-8");
assert!(
rewritten.contains(
r#"<ext:block ext:value="kept" xmlns:ext="urn:body-raw"><ext:nested/></ext:block>"#
),
"body binding survives a second serialization: {rewritten}"
);
assert!(
rewritten.contains(r#"<ext:empty ext:value="kept" xmlns:ext="urn:body-raw"/>"#),
"empty body child survives a second serialization: {rewritten}"
);
}
#[test]
fn self_closing_modeled_body_children_are_typed_by_namespace() {
let xml = format!(
r#"<document xmlns="{W_NS}" xmlns:q="{W_NS}" xmlns:ext="urn:producer"><body><p/><q:tbl/><ext:p ext:flag="keep"/><ext:body/><q:sectPr/></body></document>"#
);
let parsed = CT_Document::from_xml(xml.as_bytes()).unwrap();
assert!(matches!(parsed.body.content[0], BodyContent::Paragraph(_)));
assert!(matches!(parsed.body.content[1], BodyContent::Table(_)));
let BodyContent::RawXml(raw) = &parsed.body.content[2] else {
panic!("foreign paragraph must remain raw");
};
assert_eq!(raw, br#"<ext:p ext:flag="keep"/>"#);
let BodyContent::RawXml(raw) = &parsed.body.content[3] else {
panic!("foreign body must remain raw");
};
assert_eq!(raw, br#"<ext:body/>"#);
assert!(parsed.body.sect_pr.is_some());
assert_eq!(parsed.body.content.len(), 4);
let written = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
assert!(written.contains("<w:p"), "{written}");
assert!(written.contains("<w:tbl>"));
assert!(written.contains(r#"<ext:p ext:flag="keep"/>"#));
assert!(written.contains("<ext:body/>"));
assert!(written.contains("<w:sectPr"));
let reparsed = CT_Document::from_xml(written.as_bytes()).unwrap();
assert_eq!(reparsed.body.content.len(), 4);
assert!(reparsed.body.sect_pr.is_some());
}
#[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 page_number_start_mutation_targets_only_the_parsed_attribute() {
let xml = format!(
r#"<w:document xmlns:w="{W_NS}" xmlns:q="{W_NS}" xmlns:x="urn:producer"><w:body><w:sectPr><q:pgNumType x:note="q:start='producer'>still producer" q:start = '3' x:tail="keep"/></w:sectPr></w:body></w:document>"#
);
let mut document = CT_Document::from_xml(xml.as_bytes()).unwrap();
document
.body
.sect_pr
.as_mut()
.unwrap()
.page_number
.as_mut()
.unwrap()
.start = Some(27);
let written = String::from_utf8(document.to_xml().unwrap()).unwrap();
assert!(
written.contains(r#"x:note="q:start='producer'>still producer""#),
"{written}"
);
assert!(written.contains("q:start = '27'"), "{written}");
assert!(written.contains(r#"x:tail="keep""#), "{written}");
}
#[test]
fn missing_page_number_start_uses_a_prefix_bound_to_word() {
for (page_number, expected, inherited) in [
(
format!(r#"<q:pgNumType xmlns:q="{W_NS}" xmlns:w="urn:producer"/>"#),
"q:start=\"27\"",
"",
),
(
format!(r#"<pgNumType xmlns="{W_NS}" xmlns:w="urn:producer"/>"#),
"rdocxWord:start=\"27\"",
"",
),
(
format!(
r#"<pgNumType xmlns="{W_NS}" xmlns:w="urn:producer" xmlns:x="urn:extension" x:type="rdocxWord:ProducerType"/>"#
),
"rdocxWord1:start=\"27\"",
r#" xmlns:rdocxWord="urn:producer""#,
),
] {
let xml = format!(
r#"<w:document xmlns:w="{W_NS}"{inherited}><w:body><w:sectPr>{page_number}</w:sectPr></w:body></w:document>"#
);
let mut document = CT_Document::from_xml(xml.as_bytes()).unwrap();
document
.body
.sect_pr
.as_mut()
.unwrap()
.page_number
.as_mut()
.unwrap()
.start = Some(27);
let written = document.to_xml().unwrap();
let written_text = String::from_utf8(written.clone()).unwrap();
assert!(written_text.contains(expected), "{written_text}");
assert!(!written_text.contains(r#" w:start="27""#), "{written_text}");
if !inherited.is_empty() {
assert!(
written_text.contains(r#"xmlns:rdocxWord="urn:producer""#),
"{written_text}"
);
assert!(
written_text.contains(r#"x:type="rdocxWord:ProducerType""#),
"{written_text}"
);
}
let reopened = CT_Document::from_xml(&written).unwrap();
assert_eq!(
reopened.body.sect_pr.unwrap().page_number.unwrap().start,
Some(27)
);
}
}
#[test]
fn duplicate_page_number_elements_preserve_source_order() {
for (page_numbers, expected_start) in [
(
r#"<w:pgNumType x:id="raw-first"/><w:pgNumType x:id="raw-second"/>"#,
None,
),
(
r#"<w:pgNumType w:start="12" x:id="typed-first"/><w:pgNumType w:start="27" x:id="typed-second"/>"#,
Some(12),
),
] {
let xml = format!(
r#"<w:document xmlns:w="{W_NS}" xmlns:x="urn:producer"><w:body><w:sectPr>{page_numbers}</w:sectPr></w:body></w:document>"#
);
let document = CT_Document::from_xml(xml.as_bytes()).unwrap();
assert_eq!(
document
.body
.sect_pr
.as_ref()
.unwrap()
.page_number
.as_ref()
.unwrap()
.start,
expected_start
);
let written = String::from_utf8(document.to_xml().unwrap()).unwrap();
let first = written.find("first").unwrap();
let second = written.find("second").unwrap();
assert!(first < second, "{written}");
}
}
#[test]
fn retained_children_keep_boundaries_between_repeated_story_references() {
let xml = format!(
r#"<w:document xmlns:w="{W_NS}" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:x="urn:producer"><w:body><w:sectPr><w:headerReference w:type="default" r:id="h1"/><x:betweenHeaders/><w:headerReference w:type="first" r:id="h2"/><w:footerReference w:type="default" r:id="f1"/><x:betweenFooters/><w:footerReference w:type="even" r:id="f2"/></w:sectPr></w:body></w:document>"#
);
let mut document = CT_Document::from_xml(xml.as_bytes()).unwrap();
let written = String::from_utf8(document.to_xml().unwrap()).unwrap();
let positions = ["h1", "betweenHeaders", "h2", "f1", "betweenFooters", "f2"]
.map(|marker| written.find(marker).unwrap());
assert!(
positions.windows(2).all(|pair| pair[0] < pair[1]),
"{written}"
);
let mut front_removed = document.clone();
front_removed
.body
.sect_pr
.as_mut()
.unwrap()
.header_refs
.remove(0);
let front_removed = String::from_utf8(front_removed.to_xml().unwrap()).unwrap();
assert!(
front_removed.find("betweenHeaders").unwrap() < front_removed.find("h2").unwrap(),
"{front_removed}"
);
let mut reordered = document.clone();
reordered
.body
.sect_pr
.as_mut()
.unwrap()
.header_refs
.swap(0, 1);
let reordered = String::from_utf8(reordered.to_xml().unwrap()).unwrap();
let reordered_positions =
["betweenHeaders", "h2", "h1"].map(|marker| reordered.find(marker).unwrap());
assert!(
reordered_positions.windows(2).all(|pair| pair[0] < pair[1]),
"{reordered}"
);
let section = document.body.sect_pr.as_mut().unwrap();
section.header_refs.clear();
section.footer_refs.clear();
let shortened = String::from_utf8(document.to_xml().unwrap()).unwrap();
assert!(shortened.contains("betweenHeaders"), "{shortened}");
assert!(shortened.contains("betweenFooters"), "{shortened}");
}
#[test]
fn retained_boundaries_use_occurrences_for_equal_story_references() {
fn positions(xml: &str, reference: &str, retained: &str) -> (Vec<usize>, usize) {
(
xml.match_indices(reference)
.map(|(index, _)| index)
.collect(),
xml.find(retained).unwrap(),
)
}
let xml = format!(
r#"<w:document xmlns:w="{W_NS}" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:x="urn:producer"><w:body><w:sectPr><w:headerReference w:type="default" r:id="sameHeader"/><x:betweenEqualHeaders/><w:headerReference w:type="default" r:id="sameHeader"/><w:footerReference w:type="default" r:id="sameFooter"/><x:betweenEqualFooters/><w:footerReference w:type="default" r:id="sameFooter"/></w:sectPr></w:body></w:document>"#
);
let document = CT_Document::from_xml(xml.as_bytes()).unwrap();
let unchanged = String::from_utf8(document.to_xml().unwrap()).unwrap();
let (headers, raw_header) = positions(&unchanged, "sameHeader", "betweenEqualHeaders");
let (footers, raw_footer) = positions(&unchanged, "sameFooter", "betweenEqualFooters");
assert!(
headers[0] < raw_header && raw_header < headers[1],
"{unchanged}"
);
assert!(
footers[0] < raw_footer && raw_footer < footers[1],
"{unchanged}"
);
let mut front_removed = document.clone();
front_removed
.body
.sect_pr
.as_mut()
.unwrap()
.header_refs
.remove(0);
let front_removed = String::from_utf8(front_removed.to_xml().unwrap()).unwrap();
let (headers, raw_header) = positions(&front_removed, "sameHeader", "betweenEqualHeaders");
assert!(headers[0] < raw_header, "{front_removed}");
let mut tail_removed = document.clone();
tail_removed
.body
.sect_pr
.as_mut()
.unwrap()
.footer_refs
.remove(1);
let tail_removed = String::from_utf8(tail_removed.to_xml().unwrap()).unwrap();
let (footers, raw_footer) = positions(&tail_removed, "sameFooter", "betweenEqualFooters");
assert!(footers[0] < raw_footer, "{tail_removed}");
let mut reordered = document.clone();
let section = reordered.body.sect_pr.as_mut().unwrap();
section.header_refs.swap(0, 1);
section.footer_refs.swap(0, 1);
let reordered = String::from_utf8(reordered.to_xml().unwrap()).unwrap();
let (headers, raw_header) = positions(&reordered, "sameHeader", "betweenEqualHeaders");
let (footers, raw_footer) = positions(&reordered, "sameFooter", "betweenEqualFooters");
assert!(
headers[0] < raw_header && raw_header < headers[1],
"{reordered}"
);
assert!(
footers[0] < raw_footer && raw_footer < footers[1],
"{reordered}"
);
}
#[test]
fn ambiguous_equal_reference_mutations_are_byte_stable() {
fn positions(xml: &str) -> (Vec<usize>, usize) {
(
xml.match_indices("sameHeader")
.map(|(index, _)| index)
.collect(),
xml.find("betweenEqualHeaders").unwrap(),
)
}
let xml = format!(
r#"<w:document xmlns:w="{W_NS}" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:x="urn:producer"><w:body><w:sectPr><w:headerReference w:type="default" r:id="sameHeader"/><x:betweenEqualHeaders/><w:headerReference w:type="default" r:id="sameHeader"/></w:sectPr></w:body></w:document>"#
);
let source = CT_Document::from_xml(xml.as_bytes()).unwrap();
let mut edited = source.clone();
let section = edited.body.sect_pr.as_mut().unwrap();
let _: &Vec<HdrFtrRef> = §ion.header_refs;
let old_allocations = [
section.header_refs[0].rel_id.as_ptr(),
section.header_refs[1].rel_id.as_ptr(),
];
let replacements = section
.header_refs
.iter()
.map(|reference| String::from_utf8(reference.rel_id.as_bytes().to_vec()).unwrap())
.collect::<Vec<_>>();
assert!(replacements.iter().all(|replacement| {
old_allocations
.iter()
.all(|allocation| replacement.as_ptr() != *allocation)
}));
for (reference, replacement) in section.header_refs.iter_mut().zip(replacements) {
reference.rel_id = replacement;
}
section.header_refs.swap(0, 1);
let written = String::from_utf8(edited.to_xml().unwrap()).unwrap();
let (references, retained) = positions(&written);
assert!(
references[0] < retained && retained < references[1],
"{written}"
);
assert_eq!(
written,
String::from_utf8(edited.clone().to_xml().unwrap()).unwrap()
);
let reopened = CT_Document::from_xml(written.as_bytes()).unwrap();
let reopened_written = String::from_utf8(reopened.to_xml().unwrap()).unwrap();
let (references, retained) = positions(&reopened_written);
assert!(
references[0] < retained && retained < references[1],
"{reopened_written}"
);
assert_eq!(
reopened_written,
String::from_utf8(reopened.clone().to_xml().unwrap()).unwrap()
);
let mut replaced_source = source.clone();
let section = replaced_source.body.sect_pr.as_mut().unwrap();
let retired = section.header_refs.remove(1);
drop(retired);
section.header_refs.insert(
0,
HdrFtrRef {
hdr_ftr_type: HdrFtrType::Default,
rel_id: "sameHeader".to_owned(),
},
);
let replaced_source = String::from_utf8(replaced_source.to_xml().unwrap()).unwrap();
let (references, retained) = positions(&replaced_source);
assert!(
references[0] < retained && retained < references[1],
"{replaced_source}"
);
let reopened = CT_Document::from_xml(replaced_source.as_bytes()).unwrap();
assert_eq!(
replaced_source,
String::from_utf8(reopened.to_xml().unwrap()).unwrap()
);
}
#[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);
let document = CT_Document::from_xml(
br#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"><w:body><w:sdt><w:sdtContent><w:p><w:r><w:t>Background</w:t></w:r></w:p></w:sdtContent></w:sdt><w:p><w:r><w:t>Background</w:t></w:r></w:p></w:body></w:document>"#,
)
.unwrap();
assert_eq!(document.body.find_paragraph_index("Background"), Some(1));
assert_eq!(
document.body.find_paragraph_indices("Background"),
vec![1, 0]
);
}
#[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));
}
}