use rdocx_oxml::borders::{CT_BorderEdge, CT_PBdr, CT_TabStop, CT_Tabs};
use rdocx_oxml::document::CT_SectPr;
use rdocx_oxml::math::OfficeMath;
use rdocx_oxml::properties::{CT_FramePr, CT_PPr, CT_RPr, CT_Shd};
use rdocx_oxml::ruby::CT_Ruby;
use rdocx_oxml::shared::{
ST_Border, ST_Jc, ST_PageOrientation, ST_SectionType, ST_TabJc, ST_TabLeader, ST_Underline,
};
use rdocx_oxml::text::{
AcceptedRunPath, AcceptedRunPathSegment, BreakType, CT_P, CT_R, CommentRangeMarker,
HyperlinkSpan, RunContent, hyperlink_revision_index,
};
use rdocx_oxml::units::{HalfPoint, Twips};
use crate::run::{Run, RunRef};
use crate::table::TableConditionalFormatting;
use crate::{ContentControlRef, Length, RevisionRef};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Alignment {
Left,
Center,
Right,
Justify,
}
#[non_exhaustive]
pub enum ParagraphItemRef<'a> {
Run(RunRef<'a>),
Hyperlink(HyperlinkRef<'a>),
Equation(&'a OfficeMath),
ContentControl(ContentControlRef<'a>),
Revision(RevisionRef<'a>),
CommentRangeStart {
id: i32,
has_child_content: bool,
},
CommentRangeEnd {
id: i32,
has_child_content: bool,
},
BookmarkStart {
id: Option<i32>,
name: Option<&'a str>,
has_child_content: bool,
},
BookmarkEnd {
id: Option<i32>,
has_child_content: bool,
},
UnsupportedXml(&'a [u8]),
}
#[non_exhaustive]
pub enum HyperlinkItemRef<'a> {
Run(RunRef<'a>),
Revision(RevisionRef<'a>),
UnsupportedXml(&'a [u8]),
}
#[derive(Clone, Copy)]
pub struct HyperlinkRef<'a> {
paragraph: &'a CT_P,
index: usize,
}
impl<'a> HyperlinkRef<'a> {
fn inner(&self) -> &'a HyperlinkSpan {
&self.paragraph.hyperlinks[self.index]
}
pub fn relationship_id(&self) -> Option<&'a str> {
self.inner().rel_id.as_deref()
}
pub fn anchor(&self) -> Option<&'a str> {
self.inner().anchor.as_deref()
}
pub fn tooltip(&self) -> Option<&'a str> {
self.inner().tooltip.as_deref()
}
pub fn doc_location(&self) -> Option<&'a str> {
self.inner().doc_location.as_deref()
}
pub fn has_unmodeled_semantic_attributes(&self) -> bool {
self.inner()
.extra_attributes
.iter()
.any(|(name, _)| name != "xmlns" && !name.starts_with("xmlns:"))
}
pub fn text(&self) -> String {
let hyperlink = self.inner();
self.paragraph.runs[hyperlink.run_start..hyperlink.run_end]
.iter()
.map(CT_R::text)
.collect()
}
pub fn items(&self) -> impl Iterator<Item = HyperlinkItemRef<'a>> {
let hyperlink = self.inner();
let mut items = Vec::new();
for relative_index in 0..=hyperlink.run_end - hyperlink.run_start {
let run_index = hyperlink.run_start + relative_index;
let revisions = self
.paragraph
.revisions
.iter()
.filter(|(at, slot, _)| {
*at == run_index && hyperlink_revision_index(*slot) == Some(self.index)
})
.map(|(_, _, revision)| revision)
.collect::<Vec<_>>();
for revision_index in 0..=revisions.len() {
items.extend(
hyperlink
.extra_xml
.iter()
.filter(|(at, before, _)| {
*at == relative_index
&& (*before).min(revisions.len()) == revision_index
})
.map(|(_, _, raw)| HyperlinkItemRef::UnsupportedXml(raw.as_slice())),
);
if let Some(revision) = revisions.get(revision_index) {
items.push(HyperlinkItemRef::Revision(RevisionRef { inner: revision }));
}
}
if relative_index < hyperlink.run_end - hyperlink.run_start {
items.push(HyperlinkItemRef::Run(RunRef {
inner: &self.paragraph.runs[run_index],
}));
}
}
items.into_iter()
}
}
impl Alignment {
pub(crate) fn to_st_jc(self) -> ST_Jc {
match self {
Alignment::Left => ST_Jc::Left,
Alignment::Center => ST_Jc::Center,
Alignment::Right => ST_Jc::Right,
Alignment::Justify => ST_Jc::Both,
}
}
fn from_st_jc(jc: ST_Jc) -> Self {
match jc {
ST_Jc::Center => Alignment::Center,
ST_Jc::Right | ST_Jc::End => Alignment::Right,
ST_Jc::Both | ST_Jc::Distribute => Alignment::Justify,
_ => Alignment::Left,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BorderStyle {
None,
Single,
Thick,
Double,
Dotted,
Dashed,
DotDash,
Wave,
}
impl BorderStyle {
pub(crate) fn to_st(self) -> ST_Border {
match self {
Self::None => ST_Border::None,
Self::Single => ST_Border::Single,
Self::Thick => ST_Border::Thick,
Self::Double => ST_Border::Double,
Self::Dotted => ST_Border::Dotted,
Self::Dashed => ST_Border::Dashed,
Self::DotDash => ST_Border::DotDash,
Self::Wave => ST_Border::Wave,
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct ParagraphBorderRef<'a> {
inner: &'a CT_BorderEdge,
}
impl ParagraphBorderRef<'_> {
pub fn style(self) -> &'static str {
self.inner.val.to_str()
}
pub fn size_eighths_pt(self) -> Option<u32> {
self.inner.sz
}
pub fn space_points(self) -> Option<u32> {
self.inner.space
}
pub fn color(&self) -> Option<&str> {
self.inner.color.as_deref()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TabAlignment {
Left,
Center,
Right,
Decimal,
}
impl TabAlignment {
fn to_st(self) -> ST_TabJc {
match self {
Self::Left => ST_TabJc::Left,
Self::Center => ST_TabJc::Center,
Self::Right => ST_TabJc::Right,
Self::Decimal => ST_TabJc::Decimal,
}
}
fn from_st(value: ST_TabJc) -> Option<Self> {
match value {
ST_TabJc::Left => Some(Self::Left),
ST_TabJc::Center => Some(Self::Center),
ST_TabJc::Right => Some(Self::Right),
ST_TabJc::Decimal => Some(Self::Decimal),
ST_TabJc::Bar | ST_TabJc::Clear | ST_TabJc::Num => None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TabLeader {
None,
Dot,
Hyphen,
Underscore,
}
impl TabLeader {
fn to_st(self) -> ST_TabLeader {
match self {
Self::None => ST_TabLeader::None,
Self::Dot => ST_TabLeader::Dot,
Self::Hyphen => ST_TabLeader::Hyphen,
Self::Underscore => ST_TabLeader::Underscore,
}
}
fn from_st(value: ST_TabLeader) -> Option<Self> {
match value {
ST_TabLeader::None => Some(Self::None),
ST_TabLeader::Dot => Some(Self::Dot),
ST_TabLeader::Hyphen => Some(Self::Hyphen),
ST_TabLeader::Underscore => Some(Self::Underscore),
ST_TabLeader::Heavy | ST_TabLeader::MiddleDot => None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ParagraphBorderEdge {
Top,
Bottom,
Left,
Right,
Between,
Bar,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ParagraphTextDirection {
LeftToRightTopToBottom,
TopToBottomRightToLeft,
BottomToTopLeftToRight,
LeftToRightTopToBottomVertical,
TopToBottomRightToLeftVertical,
TopToBottomLeftToRightVertical,
}
impl ParagraphTextDirection {
fn from_str(value: &str) -> Option<Self> {
match value {
"lrTb" => Some(Self::LeftToRightTopToBottom),
"tbRl" => Some(Self::TopToBottomRightToLeft),
"btLr" => Some(Self::BottomToTopLeftToRight),
"lrTbV" => Some(Self::LeftToRightTopToBottomVertical),
"tbRlV" => Some(Self::TopToBottomRightToLeftVertical),
"tbLrV" => Some(Self::TopToBottomLeftToRightVertical),
_ => None,
}
}
fn to_str(self) -> &'static str {
match self {
Self::LeftToRightTopToBottom => "lrTb",
Self::TopToBottomRightToLeft => "tbRl",
Self::BottomToTopLeftToRight => "btLr",
Self::LeftToRightTopToBottomVertical => "lrTbV",
Self::TopToBottomRightToLeftVertical => "tbRlV",
Self::TopToBottomLeftToRightVertical => "tbLrV",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ParagraphTextAlignment {
Top,
Center,
Baseline,
Bottom,
Auto,
}
impl ParagraphTextAlignment {
fn from_str(value: &str) -> Option<Self> {
match value {
"top" => Some(Self::Top),
"center" => Some(Self::Center),
"baseline" => Some(Self::Baseline),
"bottom" => Some(Self::Bottom),
"auto" => Some(Self::Auto),
_ => None,
}
}
fn to_str(self) -> &'static str {
match self {
Self::Top => "top",
Self::Center => "center",
Self::Baseline => "baseline",
Self::Bottom => "bottom",
Self::Auto => "auto",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TextboxTightWrap {
None,
AllLines,
FirstAndLastLine,
FirstLineOnly,
LastLineOnly,
}
impl TextboxTightWrap {
fn from_str(value: &str) -> Option<Self> {
match value {
"none" => Some(Self::None),
"allLines" => Some(Self::AllLines),
"firstAndLastLine" => Some(Self::FirstAndLastLine),
"firstLineOnly" => Some(Self::FirstLineOnly),
"lastLineOnly" => Some(Self::LastLineOnly),
_ => None,
}
}
fn to_str(self) -> &'static str {
match self {
Self::None => "none",
Self::AllLines => "allLines",
Self::FirstAndLastLine => "firstAndLastLine",
Self::FirstLineOnly => "firstLineOnly",
Self::LastLineOnly => "lastLineOnly",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DropCap {
None,
Drop,
Margin,
}
impl DropCap {
fn from_str(value: &str) -> Option<Self> {
match value {
"none" => Some(Self::None),
"drop" => Some(Self::Drop),
"margin" => Some(Self::Margin),
_ => None,
}
}
fn to_str(self) -> &'static str {
match self {
Self::None => "none",
Self::Drop => "drop",
Self::Margin => "margin",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FrameWrap {
Auto,
NotBeside,
Around,
Tight,
Through,
None,
}
impl FrameWrap {
fn from_str(value: &str) -> Option<Self> {
match value {
"auto" => Some(Self::Auto),
"notBeside" => Some(Self::NotBeside),
"around" => Some(Self::Around),
"tight" => Some(Self::Tight),
"through" => Some(Self::Through),
"none" => Some(Self::None),
_ => None,
}
}
fn to_str(self) -> &'static str {
match self {
Self::Auto => "auto",
Self::NotBeside => "notBeside",
Self::Around => "around",
Self::Tight => "tight",
Self::Through => "through",
Self::None => "none",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FrameAnchor {
Margin,
Page,
Text,
}
impl FrameAnchor {
fn from_str(value: &str) -> Option<Self> {
match value {
"margin" => Some(Self::Margin),
"page" => Some(Self::Page),
"text" => Some(Self::Text),
_ => None,
}
}
fn to_str(self) -> &'static str {
match self {
Self::Margin => "margin",
Self::Page => "page",
Self::Text => "text",
}
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq)]
pub struct ParagraphFrame {
pub width: Option<Length>,
pub height: Option<Length>,
pub horizontal_space: Option<Length>,
pub vertical_space: Option<Length>,
pub horizontal_position: Option<Length>,
pub vertical_position: Option<Length>,
pub horizontal_anchor: Option<FrameAnchor>,
pub vertical_anchor: Option<FrameAnchor>,
pub wrap: Option<FrameWrap>,
pub drop_cap: Option<DropCap>,
pub drop_cap_lines: Option<u32>,
pub anchor_lock: Option<bool>,
}
impl ParagraphFrame {
fn from_ct(frame: &CT_FramePr) -> Self {
ParagraphFrame {
width: frame.w.map(|twips| Length::twips(twips.0)),
height: frame.h.map(|twips| Length::twips(twips.0)),
horizontal_space: frame.h_space.map(|twips| Length::twips(twips.0)),
vertical_space: frame.v_space.map(|twips| Length::twips(twips.0)),
horizontal_position: frame.x.map(|twips| Length::twips(twips.0)),
vertical_position: frame.y.map(|twips| Length::twips(twips.0)),
horizontal_anchor: frame.h_anchor.as_deref().and_then(FrameAnchor::from_str),
vertical_anchor: frame.v_anchor.as_deref().and_then(FrameAnchor::from_str),
wrap: frame.wrap.as_deref().and_then(FrameWrap::from_str),
drop_cap: frame.drop_cap.as_deref().and_then(DropCap::from_str),
drop_cap_lines: frame.lines,
anchor_lock: frame.anchor_lock,
}
}
fn apply_to(self, frame: &mut CT_FramePr) {
frame.w = self.width.map(Length::as_twips);
frame.h = self.height.map(Length::as_twips);
frame.h_space = self.horizontal_space.map(Length::as_twips);
frame.v_space = self.vertical_space.map(Length::as_twips);
frame.x = self.horizontal_position.map(Length::as_twips);
frame.y = self.vertical_position.map(Length::as_twips);
frame.h_anchor = self
.horizontal_anchor
.map(|anchor| anchor.to_str().to_owned());
frame.v_anchor = self
.vertical_anchor
.map(|anchor| anchor.to_str().to_owned());
frame.wrap = self.wrap.map(|wrap| wrap.to_str().to_owned());
frame.drop_cap = self.drop_cap.map(|drop_cap| drop_cap.to_str().to_owned());
frame.lines = self.drop_cap_lines;
frame.anchor_lock = self.anchor_lock;
}
}
#[derive(Debug, Clone, Copy)]
pub struct TabStopRef<'a> {
inner: &'a CT_TabStop,
}
impl TabStopRef<'_> {
pub fn alignment(self) -> Option<TabAlignment> {
TabAlignment::from_st(self.inner.val)
}
pub fn position(self) -> Length {
Length::twips(self.inner.pos.0)
}
pub fn leader(self) -> Option<TabLeader> {
self.inner.leader.and_then(TabLeader::from_st)
}
}
pub struct ParagraphMark<'a> {
inner: &'a mut CT_RPr,
}
impl ParagraphMark<'_> {
pub fn bold(mut self, val: bool) -> Self {
self.set_bold(val);
self
}
pub fn set_bold(&mut self, val: bool) {
self.set_bold_value(Some(val));
}
pub fn set_bold_value(&mut self, val: Option<bool>) {
self.inner.bold = val;
self.inner.bold_cs = val;
}
pub fn italic(mut self, val: bool) -> Self {
self.set_italic(val);
self
}
pub fn set_italic(&mut self, val: bool) {
self.set_italic_value(Some(val));
}
pub fn set_italic_value(&mut self, val: Option<bool>) {
self.inner.italic = val;
self.inner.italic_cs = val;
}
pub fn underline(mut self, val: bool) -> Self {
self.set_underline(val);
self
}
pub fn set_underline(&mut self, val: bool) {
self.set_underline_value(Some(val));
}
pub fn set_underline_value(&mut self, val: Option<bool>) {
self.inner.underline = val.map(|val| {
if val {
ST_Underline::Single
} else {
ST_Underline::None
}
});
}
pub fn strike(mut self, val: bool) -> Self {
self.set_strike(val);
self
}
pub fn set_strike(&mut self, val: bool) {
self.set_strike_value(Some(val));
}
pub fn set_strike_value(&mut self, val: Option<bool>) {
self.inner.strike = val;
}
pub fn size(mut self, pt: f64) -> Self {
self.set_size(pt);
self
}
pub fn set_size(&mut self, pt: f64) {
self.set_size_value(Some(pt));
}
pub fn set_size_value(&mut self, pt: Option<f64>) {
let half_points = pt.map(HalfPoint::from_pt);
self.inner.sz = half_points;
self.inner.sz_cs = half_points;
}
pub fn font(mut self, name: &str) -> Self {
self.set_font(name);
self
}
pub fn set_font(&mut self, name: &str) {
self.set_font_value(Some(name));
}
pub fn set_font_value(&mut self, name: Option<&str>) {
self.inner.font_ascii = name.map(str::to_owned);
self.inner.font_hansi = name.map(str::to_owned);
self.inner.font_east_asia = name.map(str::to_owned);
self.inner.font_cs = name.map(str::to_owned);
}
pub fn color(mut self, hex: &str) -> Self {
self.set_color(hex);
self
}
pub fn set_color(&mut self, hex: &str) {
self.set_color_value(Some(hex));
}
pub fn set_color_value(&mut self, hex: Option<&str>) {
self.inner.color = hex.map(str::to_owned);
}
}
#[derive(Debug, Clone, Copy)]
pub struct ParagraphMarkRef<'a> {
inner: Option<&'a CT_RPr>,
}
impl ParagraphMarkRef<'_> {
pub fn is_present(&self) -> bool {
self.inner.is_some()
}
pub fn bold_value(&self) -> Option<bool> {
self.inner.and_then(|rpr| rpr.bold)
}
pub fn italic_value(&self) -> Option<bool> {
self.inner.and_then(|rpr| rpr.italic)
}
pub fn underline_value(&self) -> Option<bool> {
self.inner
.and_then(|rpr| rpr.underline)
.map(|underline| underline != ST_Underline::None)
}
pub fn strike_value(&self) -> Option<bool> {
self.inner.and_then(|rpr| rpr.strike)
}
pub fn size(&self) -> Option<f64> {
self.inner.and_then(|rpr| rpr.sz).map(HalfPoint::to_pt)
}
pub fn font_name(&self) -> Option<&str> {
self.inner.and_then(|rpr| rpr.font_ascii.as_deref())
}
pub fn color(&self) -> Option<&str> {
self.inner.and_then(|rpr| rpr.color.as_deref())
}
}
pub struct Paragraph<'a> {
pub(crate) inner: &'a mut CT_P,
}
impl<'a> Paragraph<'a> {
pub fn text(&self) -> String {
self.inner.accepted_text()
}
pub fn set_text(&mut self, text: &str) -> crate::Result<()> {
let anchors = RangeAnchors::scan(self.inner)?;
let mut paragraph = CT_P::new();
paragraph.properties = self.inner.properties.clone();
paragraph.extra_xml = self
.inner
.extra_xml
.iter()
.filter(|(position, raw)| CT_P::raw_is_root_attributes(*position, raw))
.cloned()
.collect();
if !text.is_empty() {
let mut inner = CT_R::new("");
inner.content.clear();
let mut run = Run { inner: &mut inner };
let mut pending = String::new();
for character in text.chars() {
if !matches!(character, '\t' | '\n' | '\r') {
pending.push(character);
continue;
}
if !pending.is_empty() {
run.add_text(&pending);
pending.clear();
}
if character == '\t' {
run.add_tab();
} else {
run.add_break(crate::BreakKind::Line);
}
}
if !pending.is_empty() {
run.add_text(&pending);
}
paragraph.runs.push(inner);
}
let end = paragraph.runs.len();
for id in &anchors.comment_starts {
paragraph.comment_ranges.push(CommentRangeMarker::Start {
id: *id,
run_index: 0,
raw_before: 0,
has_child_content: false,
});
}
for (id, name) in &anchors.bookmark_starts {
if !paragraph.insert_bookmark_start(0, *id, name) {
return Err(crate::Error::Other(format!(
"bookmark {name} could not be kept in the paragraph"
)));
}
}
for raw in &anchors.permission_starts {
paragraph.extra_xml.push((0, raw.clone()));
}
for id in &anchors.bookmark_ends {
if !paragraph.insert_bookmark_end(end, *id) {
return Err(crate::Error::Other(format!(
"bookmark end {id} could not be kept in the paragraph"
)));
}
}
for raw in &anchors.permission_ends {
paragraph.extra_xml.push((end, raw.clone()));
}
let raw_before = paragraph
.extra_xml
.iter()
.filter(|(position, _)| *position == end)
.count();
for id in &anchors.comment_ends {
paragraph.comment_ranges.push(CommentRangeMarker::End {
id: *id,
run_index: end,
raw_before,
has_child_content: false,
});
}
for id in &anchors.comment_references {
let mut reference = CT_R::new("");
reference.content = vec![RunContent::CommentReference {
id: *id,
raw_before: 0,
}];
paragraph.runs.push(reference);
}
*self.inner = paragraph;
Ok(())
}
pub fn equations(&self) -> impl Iterator<Item = &OfficeMath> {
self.inner.equations.iter().map(|(_, _, equation)| equation)
}
pub fn equation(&self, index: usize) -> Option<&OfficeMath> {
self.inner
.equations
.get(index)
.map(|(_, _, equation)| equation)
}
pub fn equation_mut(&mut self, index: usize) -> Option<&mut OfficeMath> {
self.inner
.equations
.get_mut(index)
.map(|(_, _, equation)| equation)
}
pub fn add_equation(&mut self, equation: OfficeMath) -> crate::Result<()> {
let run_index = self.inner.runs.len();
let raw_before = self
.inner
.extra_xml
.iter()
.filter(|(position, _)| *position == run_index)
.count();
let raw = equation.to_xml()?;
self.inner.extra_xml.push((run_index, raw));
self.inner.equations.push((run_index, raw_before, equation));
Ok(())
}
pub fn add_footnote_ref(&mut self, id: i32) {
use rdocx_oxml::text::{CT_R, RunContent};
let mut r = CT_R::new("");
r.content = vec![RunContent::FootnoteRef {
id,
custom_mark: None,
}];
self.inner.runs.push(r);
}
pub fn add_run(&mut self, text: &str) -> Run<'_> {
self.inner.runs.push(CT_R::new(text));
Run {
inner: self.inner.runs.last_mut().unwrap(),
}
}
pub fn add_ruby(&mut self, base: &str, phonetic: &str) -> usize {
let base_start = self.inner.runs.len();
self.inner.runs.push(CT_R::new(base));
self.inner.rubies.push(CT_Ruby {
properties: None,
ruby_text: vec![CT_R::new(phonetic)],
base_start,
base_end: base_start + 1,
raw_xml: Vec::new(),
});
self.inner.rubies.len() - 1
}
pub fn rubies(&self) -> impl Iterator<Item = &CT_Ruby> {
self.inner.rubies.iter()
}
pub fn ruby(&self, index: usize) -> Option<&CT_Ruby> {
self.inner.rubies.get(index)
}
pub fn ruby_mut(&mut self, index: usize) -> Option<&mut CT_Ruby> {
self.inner.rubies.get_mut(index)
}
pub fn add_run_inheriting_mark(&mut self, text: &str) -> Run<'_> {
let properties = self
.inner
.properties
.as_ref()
.and_then(|properties| properties.rpr.clone());
let mut run = CT_R::new(text);
run.properties = properties;
self.inner.runs.push(run);
Run {
inner: self.inner.runs.last_mut().unwrap(),
}
}
pub fn add_line_break(&mut self) {
let mut run = CT_R::new("");
run.content = vec![RunContent::Break(BreakType::Line)];
self.inner.runs.push(run);
}
pub(crate) fn add_tab(&mut self) {
let mut run = CT_R::new("");
run.content = vec![RunContent::Tab];
self.inner.runs.push(run);
}
pub(crate) fn add_picture(&mut self, rel_id: &str, width: Length, height: Length) -> &mut CT_R {
use rdocx_oxml::drawing::{CT_Drawing, CT_Inline};
let inline = CT_Inline::new(rel_id, width.to_emu(), height.to_emu());
let drawing = CT_Drawing::inline(inline);
let mut run = CT_R::new("");
run.content = vec![RunContent::Drawing(drawing)];
self.inner.runs.push(run);
self.inner
.runs
.last_mut()
.expect("picture run was appended")
}
pub fn add_hyperlink(&mut self, text: &str, relationship_id: &str) -> Run<'_> {
self.add_hyperlink_with_tooltip(text, relationship_id, None)
}
pub fn add_hyperlink_with_tooltip(
&mut self,
text: &str,
relationship_id: &str,
tooltip: Option<&str>,
) -> Run<'_> {
let run_start = self.inner.runs.len();
self.inner.runs.push(CT_R::new(text));
self.inner.hyperlinks.push(HyperlinkSpan {
rel_id: Some(relationship_id.to_string()),
anchor: None,
tooltip: tooltip.map(str::to_owned),
doc_location: None,
run_start,
run_end: run_start + 1,
extra_attributes: Vec::new(),
extra_xml: Vec::new(),
preserved_raw_before: None,
});
Run {
inner: self.inner.runs.last_mut().unwrap(),
}
}
pub fn run_count(&self) -> usize {
self.inner.accepted_run_paths().len()
}
pub fn run(&self, index: usize) -> Option<RunRef<'_>> {
let path = self.inner.accepted_run_paths().get(index)?.clone();
self.inner.accepted_run(&path).map(|inner| RunRef { inner })
}
pub fn run_mut(&mut self, index: usize) -> Option<Run<'_>> {
let path = self.inner.accepted_run_paths().get(index)?.clone();
let [AcceptedRunPathSegment::Run(direct)] = path.segments() else {
return None;
};
self.inner.runs.get_mut(*direct).map(|inner| Run { inner })
}
#[doc(hidden)]
pub fn run_path(&self, index: usize) -> Option<AcceptedRunPath> {
self.inner.accepted_run_paths().get(index).cloned()
}
#[doc(hidden)]
pub fn edit_run(
&mut self,
path: &AcceptedRunPath,
edit: impl FnOnce(&mut Run<'_>),
) -> crate::Result<()> {
let mut replacement = self
.inner
.accepted_run(path)
.cloned()
.ok_or_else(|| crate::Error::Other("accepted run path is stale".to_owned()))?;
edit(&mut Run {
inner: &mut replacement,
});
if self.inner.replace_accepted_run(path, replacement)? {
Ok(())
} else {
Err(crate::Error::Other("accepted run path is stale".to_owned()))
}
}
pub fn split_run(&mut self, run_index: usize, offset: usize) -> crate::Result<usize> {
let mut paragraph = self.inner.clone();
let path = paragraph
.accepted_run_paths()
.get(run_index)
.cloned()
.ok_or_else(|| crate::Error::Other("run index out of range".to_owned()))?;
let boundary = paragraph
.split_accepted_run(&path, run_index, offset)
.map_err(|error| crate::Error::Other(error.to_string()))?;
*self.inner = paragraph;
Ok(boundary)
}
pub fn remove_run(&mut self, run_index: usize) -> crate::Result<()> {
let run_paths = self.inner.accepted_run_paths();
let path = run_paths.get(run_index).ok_or_else(|| {
crate::Error::Other(format!(
"run index {run_index} is out of range for a paragraph with {} runs",
run_paths.len()
))
})?;
self.inner.remove_accepted_run(path).map_err(|error| {
let reason = match error {
rdocx_oxml::OxmlError::InvalidValue(reason) => reason,
other => other.to_string(),
};
crate::Error::Other(format!("run {run_index} cannot be removed: {reason}"))
})
}
pub fn runs(&self) -> impl Iterator<Item = RunRef<'_>> {
self.inner
.accepted_bookmark_runs()
.into_iter()
.map(|inner| RunRef { inner })
}
pub fn alignment(mut self, align: Alignment) -> Self {
self.set_alignment(align);
self
}
pub fn set_alignment(&mut self, align: Alignment) {
self.set_alignment_value(Some(align));
}
pub fn set_alignment_value(&mut self, align: Option<Alignment>) {
if align.is_none() && self.inner.properties.is_none() {
return;
}
self.ensure_ppr().jc = align.map(Alignment::to_st_jc);
}
pub fn style(mut self, style_id: &str) -> Self {
self.set_style(style_id);
self
}
pub fn set_style(&mut self, style_id: &str) {
self.ensure_ppr().style_id = Some(style_id.to_string());
}
pub fn set_style_value(&mut self, style_id: Option<&str>) {
if style_id.is_none() && self.inner.properties.is_none() {
return;
}
self.ensure_ppr().style_id = style_id.map(str::to_owned);
}
pub fn numbering(mut self, num_id: u32, level: u32) -> Self {
let _ = self.set_numbering(num_id, level);
self
}
pub fn set_numbering(&mut self, num_id: u32, level: u32) -> bool {
self.set_numbering_value(Some((num_id, level)))
}
pub fn set_numbering_value(&mut self, numbering: Option<(u32, u32)>) -> bool {
if numbering.is_some_and(|(_, level)| level > 8) {
return false;
}
if numbering.is_none() && self.inner.properties.is_none() {
return true;
}
let ppr = self.ensure_ppr();
ppr.num_id = numbering.map(|(num_id, _)| num_id);
ppr.num_ilvl = numbering.map(|(_, level)| level);
true
}
pub fn space_before(mut self, length: Length) -> Self {
self.set_space_before(length);
self
}
pub fn set_space_before(&mut self, length: Length) {
self.set_space_before_value(Some(length));
}
pub fn set_space_before_value(&mut self, length: Option<Length>) {
if length.is_none() && self.inner.properties.is_none() {
return;
}
self.ensure_ppr().space_before = length.map(Length::as_twips);
}
pub fn space_after(mut self, length: Length) -> Self {
self.set_space_after(length);
self
}
pub fn set_space_after(&mut self, length: Length) {
self.set_space_after_value(Some(length));
}
pub fn set_space_after_value(&mut self, length: Option<Length>) {
if length.is_none() && self.inner.properties.is_none() {
return;
}
self.ensure_ppr().space_after = length.map(Length::as_twips);
}
pub fn indent_left(mut self, length: Length) -> Self {
self.set_indent_left(length);
self
}
pub fn set_indent_left(&mut self, length: Length) {
self.set_indent_left_value(Some(length));
}
pub fn set_indent_left_value(&mut self, length: Option<Length>) {
if length.is_none() && self.inner.properties.is_none() {
return;
}
self.ensure_ppr().ind_left = length.map(Length::as_twips);
}
pub fn indent_right(mut self, length: Length) -> Self {
self.set_indent_right(length);
self
}
pub fn set_indent_right(&mut self, length: Length) {
self.set_indent_right_value(Some(length));
}
pub fn set_indent_right_value(&mut self, length: Option<Length>) {
if length.is_none() && self.inner.properties.is_none() {
return;
}
self.ensure_ppr().ind_right = length.map(Length::as_twips);
}
pub fn first_line_indent(mut self, length: Length) -> Self {
self.set_first_line_indent(length);
self
}
pub fn set_first_line_indent(&mut self, length: Length) {
self.ensure_ppr().ind_first_line = Some(length.as_twips());
}
pub fn set_signed_first_line_indent_value(&mut self, length: Option<Length>) {
if length.is_none() && self.inner.properties.is_none() {
return;
}
let ppr = self.ensure_ppr();
match length.map(Length::to_twips) {
Some(twips) if twips < 0 => {
ppr.ind_first_line = None;
ppr.ind_hanging = Some(Twips(twips.saturating_abs()));
}
Some(twips) => {
ppr.ind_first_line = Some(Twips(twips));
ppr.ind_hanging = None;
}
None => {
ppr.ind_first_line = None;
ppr.ind_hanging = None;
}
}
}
pub fn hanging_indent(mut self, length: Length) -> Self {
self.set_hanging_indent(length);
self
}
pub fn set_hanging_indent(&mut self, length: Length) {
self.ensure_ppr().ind_hanging = Some(length.as_twips());
}
pub fn keep_with_next(mut self, val: bool) -> Self {
self.set_keep_with_next(val);
self
}
pub fn set_keep_with_next(&mut self, val: bool) {
self.set_keep_with_next_value(Some(val));
}
pub fn set_keep_with_next_value(&mut self, val: Option<bool>) {
if val.is_none() && self.inner.properties.is_none() {
return;
}
self.ensure_ppr().keep_next = val;
}
pub fn keep_together(mut self, val: bool) -> Self {
self.set_keep_together(val);
self
}
pub fn set_keep_together(&mut self, val: bool) {
self.set_keep_together_value(Some(val));
}
pub fn set_keep_together_value(&mut self, val: Option<bool>) {
if val.is_none() && self.inner.properties.is_none() {
return;
}
self.ensure_ppr().keep_lines = val;
}
pub fn page_break_before(mut self, val: bool) -> Self {
self.set_page_break_before(val);
self
}
pub fn set_page_break_before(&mut self, val: bool) {
self.set_page_break_before_value(Some(val));
}
pub fn set_page_break_before_value(&mut self, val: Option<bool>) {
if val.is_none() && self.inner.properties.is_none() {
return;
}
self.ensure_ppr().page_break_before = val;
}
pub fn widow_control(mut self, val: bool) -> Self {
self.set_widow_control(val);
self
}
pub fn set_widow_control(&mut self, val: bool) {
self.set_widow_control_value(Some(val));
}
pub fn set_widow_control_value(&mut self, val: Option<bool>) {
if val.is_none() && self.inner.properties.is_none() {
return;
}
self.ensure_ppr().widow_control = val;
}
pub fn line_spacing(mut self, pt: f64) -> Self {
self.set_line_spacing(pt);
self
}
pub fn set_line_spacing(&mut self, pt: f64) {
let ppr = self.ensure_ppr();
ppr.line_spacing = Some(Twips::from_pt(pt));
ppr.line_rule = Some("exact".to_string());
}
pub(crate) fn set_line_spacing_at_least(&mut self, pt: f64) {
let ppr = self.ensure_ppr();
ppr.line_spacing = Some(Twips::from_pt(pt));
ppr.line_rule = Some("atLeast".to_string());
}
pub fn clear_line_spacing(&mut self) {
if let Some(ppr) = self.inner.properties.as_mut() {
ppr.line_spacing = None;
ppr.line_rule = None;
}
}
pub fn line_spacing_multiple(mut self, multiple: f64) -> Self {
self.set_line_spacing_multiple(multiple);
self
}
pub fn set_line_spacing_multiple(&mut self, multiple: f64) {
let ppr = self.ensure_ppr();
ppr.line_spacing = Some(Twips((multiple * 240.0) as i32));
ppr.line_rule = Some("auto".to_string());
}
pub fn shading(mut self, fill_color: &str) -> Self {
self.set_shading(fill_color);
self
}
pub fn set_shading(&mut self, fill_color: &str) {
self.ensure_ppr().shading = Some(Box::new(CT_Shd {
val: "clear".to_string(),
color: Some("auto".to_string()),
fill: Some(fill_color.to_string()),
..Default::default()
}));
}
pub fn border_all(mut self, style: BorderStyle, size_eighths_pt: u32, color: &str) -> Self {
self.set_border_all(style, size_eighths_pt, color);
self
}
pub fn set_border_all(&mut self, style: BorderStyle, size_eighths_pt: u32, color: &str) {
let edge = CT_BorderEdge {
val: style.to_st(),
sz: Some(size_eighths_pt),
space: Some(1),
color: Some(color.to_string()),
extra_attributes: Vec::new(),
};
self.ensure_ppr().borders = Some(Box::new(CT_PBdr {
top: Some(edge.clone()),
bottom: Some(edge.clone()),
left: Some(edge.clone()),
right: Some(edge),
between: None,
bar: None,
}));
}
pub fn border_bottom(mut self, style: BorderStyle, size_eighths_pt: u32, color: &str) -> Self {
self.set_border_bottom(style, size_eighths_pt, color);
self
}
pub fn border_bottom_with_space(
mut self,
style: BorderStyle,
size_eighths_pt: u32,
space_points: u32,
color: &str,
) -> Self {
self.set_border_bottom_with_space(style, size_eighths_pt, space_points, color);
self
}
pub fn set_border_bottom(&mut self, style: BorderStyle, size_eighths_pt: u32, color: &str) {
self.set_border_bottom_with_space(style, size_eighths_pt, 1, color);
}
pub fn set_border_bottom_with_space(
&mut self,
style: BorderStyle,
size_eighths_pt: u32,
space_points: u32,
color: &str,
) {
let edge = CT_BorderEdge {
val: style.to_st(),
sz: Some(size_eighths_pt),
space: Some(space_points),
color: Some(color.to_string()),
extra_attributes: Vec::new(),
};
let borders = self
.ensure_ppr()
.borders
.get_or_insert_with(|| Box::new(CT_PBdr::default()));
borders.bottom = Some(edge);
}
pub fn add_tab_stop(mut self, alignment: TabAlignment, position: Length) -> Self {
self.set_add_tab_stop(alignment, position);
self
}
pub fn set_add_tab_stop(&mut self, alignment: TabAlignment, position: Length) {
let tabs = self
.ensure_ppr()
.tabs
.get_or_insert_with(|| CT_Tabs { tabs: Vec::new() });
tabs.tabs.push(CT_TabStop {
val: alignment.to_st(),
pos: position.as_twips(),
leader: None,
source_occurrence: None,
});
}
pub fn add_tab_stop_with_leader(
mut self,
alignment: TabAlignment,
position: Length,
leader: TabLeader,
) -> Self {
self.set_add_tab_stop_with_leader(alignment, position, leader);
self
}
pub fn set_add_tab_stop_with_leader(
&mut self,
alignment: TabAlignment,
position: Length,
leader: TabLeader,
) {
let tabs = self
.ensure_ppr()
.tabs
.get_or_insert_with(|| CT_Tabs { tabs: Vec::new() });
tabs.tabs.push(CT_TabStop {
val: alignment.to_st(),
pos: position.as_twips(),
leader: Some(leader.to_st()),
source_occurrence: None,
});
}
pub fn outline_level(mut self, level: u32) -> Self {
self.set_outline_level(level);
self
}
pub fn set_outline_level(&mut self, level: u32) {
let _ = self.set_outline_level_value(Some(level));
}
pub fn section_break(mut self, break_type: SectionBreak) -> Self {
self.set_section_break(break_type);
self
}
pub fn set_section_break(&mut self, break_type: SectionBreak) {
let sect = self.ensure_sect_pr();
sect.section_type = Some(break_type.to_st());
}
pub fn section_landscape(mut self) -> Self {
self.set_section_landscape();
self
}
pub fn set_section_landscape(&mut self) {
let sect = self.ensure_sect_pr();
sect.orientation = Some(ST_PageOrientation::Landscape);
sect.page_width = Some(Twips(15840)); sect.page_height = Some(Twips(12240)); }
pub fn section_portrait(mut self) -> Self {
self.set_section_portrait();
self
}
pub fn set_section_portrait(&mut self) {
let sect = self.ensure_sect_pr();
sect.orientation = Some(ST_PageOrientation::Portrait);
sect.page_width = Some(Twips(12240)); sect.page_height = Some(Twips(15840)); }
pub fn section_page_size(mut self, width: crate::Length, height: crate::Length) -> Self {
self.set_section_page_size(width, height);
self
}
pub fn set_section_page_size(&mut self, width: crate::Length, height: crate::Length) {
let sect = self.ensure_sect_pr();
sect.page_width = Some(width.as_twips());
sect.page_height = Some(height.as_twips());
}
pub fn indent_start(mut self, length: Length) -> Self {
self.set_indent_start(length);
self
}
pub fn set_indent_start(&mut self, length: Length) {
self.set_indent_start_value(Some(length));
}
pub fn set_indent_start_value(&mut self, length: Option<Length>) {
if length.is_none() && self.inner.properties.is_none() {
return;
}
self.ensure_ppr().ind_start = length.map(Length::as_twips);
}
pub fn indent_end(mut self, length: Length) -> Self {
self.set_indent_end(length);
self
}
pub fn set_indent_end(&mut self, length: Length) {
self.set_indent_end_value(Some(length));
}
pub fn set_indent_end_value(&mut self, length: Option<Length>) {
if length.is_none() && self.inner.properties.is_none() {
return;
}
self.ensure_ppr().ind_end = length.map(Length::as_twips);
}
pub fn set_hanging_indent_value(&mut self, length: Option<Length>) {
if length.is_none() && self.inner.properties.is_none() {
return;
}
self.ensure_ppr().ind_hanging = length.map(Length::as_twips);
}
pub fn mirror_indents(mut self, val: bool) -> Self {
self.set_mirror_indents(val);
self
}
pub fn set_mirror_indents(&mut self, val: bool) {
self.set_mirror_indents_value(Some(val));
}
pub fn set_mirror_indents_value(&mut self, val: Option<bool>) {
if val.is_none() && self.inner.properties.is_none() {
return;
}
self.ensure_ppr().mirror_indents = val;
}
pub fn adjust_right_indent(mut self, val: bool) -> Self {
self.set_adjust_right_indent(val);
self
}
pub fn set_adjust_right_indent(&mut self, val: bool) {
self.set_adjust_right_indent_value(Some(val));
}
pub fn set_adjust_right_indent_value(&mut self, val: Option<bool>) {
if val.is_none() && self.inner.properties.is_none() {
return;
}
self.ensure_ppr().adjust_right_ind = val;
}
pub fn set_kinsoku_value(&mut self, val: Option<bool>) {
if val.is_none() && self.inner.properties.is_none() {
return;
}
self.ensure_ppr().kinsoku = val;
}
pub fn set_word_wrap_value(&mut self, val: Option<bool>) {
if val.is_none() && self.inner.properties.is_none() {
return;
}
self.ensure_ppr().word_wrap = val;
}
pub fn set_overflow_punct_value(&mut self, val: Option<bool>) {
if val.is_none() && self.inner.properties.is_none() {
return;
}
self.ensure_ppr().overflow_punct = val;
}
pub fn set_top_line_punct_value(&mut self, val: Option<bool>) {
if val.is_none() && self.inner.properties.is_none() {
return;
}
self.ensure_ppr().top_line_punct = val;
}
pub fn set_auto_space_de_value(&mut self, val: Option<bool>) {
if val.is_none() && self.inner.properties.is_none() {
return;
}
self.ensure_ppr().auto_space_de = val;
}
pub fn set_auto_space_dn_value(&mut self, val: Option<bool>) {
if val.is_none() && self.inner.properties.is_none() {
return;
}
self.ensure_ppr().auto_space_dn = val;
}
pub fn set_snap_to_grid_value(&mut self, val: Option<bool>) {
if val.is_none() && self.inner.properties.is_none() {
return;
}
self.ensure_ppr().snap_to_grid = val;
}
pub fn space_before_auto(mut self, val: bool) -> Self {
self.set_space_before_auto(val);
self
}
pub fn set_space_before_auto(&mut self, val: bool) {
self.set_space_before_auto_value(Some(val));
}
pub fn set_space_before_auto_value(&mut self, val: Option<bool>) {
if val.is_none() && self.inner.properties.is_none() {
return;
}
self.ensure_ppr().before_autospacing = val;
}
pub fn space_after_auto(mut self, val: bool) -> Self {
self.set_space_after_auto(val);
self
}
pub fn set_space_after_auto(&mut self, val: bool) {
self.set_space_after_auto_value(Some(val));
}
pub fn set_space_after_auto_value(&mut self, val: Option<bool>) {
if val.is_none() && self.inner.properties.is_none() {
return;
}
self.ensure_ppr().after_autospacing = val;
}
pub fn contextual_spacing(mut self, val: bool) -> Self {
self.set_contextual_spacing(val);
self
}
pub fn set_contextual_spacing(&mut self, val: bool) {
self.set_contextual_spacing_value(Some(val));
}
pub fn set_contextual_spacing_value(&mut self, val: Option<bool>) {
if val.is_none() && self.inner.properties.is_none() {
return;
}
self.ensure_ppr().contextual_spacing = val;
}
pub fn border(
mut self,
edge: ParagraphBorderEdge,
style: BorderStyle,
size_eighths_pt: u32,
color: &str,
) -> Self {
self.set_border(edge, style, size_eighths_pt, color);
self
}
pub fn set_border(
&mut self,
edge: ParagraphBorderEdge,
style: BorderStyle,
size_eighths_pt: u32,
color: &str,
) {
self.set_border_value(edge, Some((style, size_eighths_pt, color)));
}
pub fn set_border_value(
&mut self,
edge: ParagraphBorderEdge,
border: Option<(BorderStyle, u32, &str)>,
) {
if border.is_none() && self.inner.properties.is_none() {
return;
}
let borders = self
.ensure_ppr()
.borders
.get_or_insert_with(|| Box::new(CT_PBdr::default()));
let slot = match edge {
ParagraphBorderEdge::Top => &mut borders.top,
ParagraphBorderEdge::Bottom => &mut borders.bottom,
ParagraphBorderEdge::Left => &mut borders.left,
ParagraphBorderEdge::Right => &mut borders.right,
ParagraphBorderEdge::Between => &mut borders.between,
ParagraphBorderEdge::Bar => &mut borders.bar,
};
match border {
Some((style, size_eighths_pt, color)) => {
let retained = slot
.take()
.map(|existing| existing.extra_attributes)
.unwrap_or_default();
*slot = Some(CT_BorderEdge {
val: style.to_st(),
sz: Some(size_eighths_pt),
space: Some(1),
color: Some(color.to_owned()),
extra_attributes: retained,
});
}
None => *slot = None,
}
if borders.is_empty() {
self.ensure_ppr().borders = None;
}
}
pub fn clear_borders(&mut self) {
if let Some(ppr) = self.inner.properties.as_mut() {
ppr.borders = None;
}
}
pub fn shading_pattern(mut self, pattern: &str, fill_color: &str, color: &str) -> Self {
self.set_shading_pattern(pattern, fill_color, color);
self
}
pub fn set_shading_pattern(&mut self, pattern: &str, fill_color: &str, color: &str) {
self.set_shading_value(Some((pattern, fill_color, color)));
}
pub fn set_shading_value(&mut self, shading: Option<(&str, &str, &str)>) {
if shading.is_none() && self.inner.properties.is_none() {
return;
}
self.ensure_ppr().shading = shading.map(|(pattern, fill_color, color)| {
Box::new(CT_Shd {
val: pattern.to_owned(),
color: Some(color.to_owned()),
fill: Some(fill_color.to_owned()),
..Default::default()
})
});
}
pub fn tab_stop(&self, index: usize) -> Option<TabStopRef<'_>> {
self.inner
.properties
.as_ref()?
.tabs
.as_ref()?
.tabs
.get(index)
.map(|inner| TabStopRef { inner })
}
pub fn set_tab_stop(
&mut self,
index: usize,
alignment: TabAlignment,
position: Length,
leader: Option<TabLeader>,
) -> bool {
let Some(tabs) = self
.inner
.properties
.as_mut()
.and_then(|ppr| ppr.tabs.as_mut())
else {
return false;
};
let Some(tab) = tabs.tabs.get_mut(index) else {
return false;
};
tab.val = alignment.to_st();
tab.pos = position.as_twips();
tab.leader = leader.map(TabLeader::to_st);
true
}
pub fn remove_tab_stop(&mut self, index: usize) -> bool {
let Some(tabs) = self
.inner
.properties
.as_mut()
.and_then(|ppr| ppr.tabs.as_mut())
else {
return false;
};
if index >= tabs.tabs.len() {
return false;
}
tabs.tabs.remove(index);
if tabs.tabs.is_empty() {
self.ensure_ppr().tabs = None;
}
true
}
pub fn clear_tab_stops(&mut self) {
if let Some(ppr) = self.inner.properties.as_mut() {
ppr.tabs = None;
}
}
pub fn suppress_line_numbers(mut self, val: bool) -> Self {
self.set_suppress_line_numbers(val);
self
}
pub fn set_suppress_line_numbers(&mut self, val: bool) {
self.set_suppress_line_numbers_value(Some(val));
}
pub fn set_suppress_line_numbers_value(&mut self, val: Option<bool>) {
if val.is_none() && self.inner.properties.is_none() {
return;
}
self.ensure_ppr().suppress_line_numbers = val;
}
pub fn suppress_auto_hyphens(mut self, val: bool) -> Self {
self.set_suppress_auto_hyphens(val);
self
}
pub fn set_suppress_auto_hyphens(&mut self, val: bool) {
self.set_suppress_auto_hyphens_value(Some(val));
}
pub fn set_suppress_auto_hyphens_value(&mut self, val: Option<bool>) {
if val.is_none() && self.inner.properties.is_none() {
return;
}
self.ensure_ppr().suppress_auto_hyphens = val;
}
pub fn frame(mut self, frame: ParagraphFrame) -> Self {
self.set_frame(frame);
self
}
pub fn set_frame(&mut self, frame: ParagraphFrame) {
self.set_frame_value(Some(frame));
}
pub fn set_frame_value(&mut self, frame: Option<ParagraphFrame>) {
if frame.is_none() && self.inner.properties.is_none() {
return;
}
let ppr = self.ensure_ppr();
match frame {
Some(frame) => frame.apply_to(
ppr.frame
.get_or_insert_with(|| Box::new(CT_FramePr::default())),
),
None => ppr.frame = None,
}
}
pub fn suppress_overlap(mut self, val: bool) -> Self {
self.set_suppress_overlap(val);
self
}
pub fn set_suppress_overlap(&mut self, val: bool) {
self.set_suppress_overlap_value(Some(val));
}
pub fn set_suppress_overlap_value(&mut self, val: Option<bool>) {
if val.is_none() && self.inner.properties.is_none() {
return;
}
self.ensure_ppr().suppress_overlap = val;
}
pub fn textbox_tight_wrap(mut self, wrap: TextboxTightWrap) -> Self {
self.set_textbox_tight_wrap(wrap);
self
}
pub fn set_textbox_tight_wrap(&mut self, wrap: TextboxTightWrap) {
self.set_textbox_tight_wrap_value(Some(wrap));
}
pub fn set_textbox_tight_wrap_value(&mut self, wrap: Option<TextboxTightWrap>) {
if wrap.is_none() && self.inner.properties.is_none() {
return;
}
self.ensure_ppr().textbox_tight_wrap = wrap.map(|wrap| wrap.to_str().to_owned());
}
pub fn set_outline_level_value(&mut self, level: Option<u32>) -> bool {
if level.is_some_and(|level| level > 9) {
return false;
}
if level.is_none() && self.inner.properties.is_none() {
return true;
}
self.ensure_ppr().outline_lvl = level;
true
}
pub fn right_to_left(mut self, val: bool) -> Self {
self.set_right_to_left(val);
self
}
pub fn set_right_to_left(&mut self, val: bool) {
self.set_right_to_left_value(Some(val));
}
pub fn set_right_to_left_value(&mut self, val: Option<bool>) {
if val.is_none() && self.inner.properties.is_none() {
return;
}
self.ensure_ppr().bidi = val;
}
pub fn text_direction(mut self, direction: ParagraphTextDirection) -> Self {
self.set_text_direction(direction);
self
}
pub fn set_text_direction(&mut self, direction: ParagraphTextDirection) {
self.set_text_direction_value(Some(direction));
}
pub fn set_text_direction_value(&mut self, direction: Option<ParagraphTextDirection>) {
if direction.is_none() && self.inner.properties.is_none() {
return;
}
self.ensure_ppr().text_direction = direction.map(|direction| direction.to_str().to_owned());
}
pub fn text_alignment(mut self, alignment: ParagraphTextAlignment) -> Self {
self.set_text_alignment(alignment);
self
}
pub fn set_text_alignment(&mut self, alignment: ParagraphTextAlignment) {
self.set_text_alignment_value(Some(alignment));
}
pub fn set_text_alignment_value(&mut self, alignment: Option<ParagraphTextAlignment>) {
if alignment.is_none() && self.inner.properties.is_none() {
return;
}
self.ensure_ppr().text_alignment = alignment.map(|alignment| alignment.to_str().to_owned());
}
pub fn set_div_id_value(&mut self, div_id: Option<u32>) {
if div_id.is_none() && self.inner.properties.is_none() {
return;
}
self.ensure_ppr().div_id = div_id;
}
pub fn set_conditional_formatting(&mut self, regions: Option<TableConditionalFormatting>) {
if regions.is_none() && self.inner.properties.is_none() {
return;
}
self.ensure_ppr().cnf_style = regions.map(|regions| regions.to_value());
}
pub fn mark(&mut self) -> ParagraphMark<'_> {
ParagraphMark {
inner: self.ensure_ppr().rpr.get_or_insert_with(CT_RPr::default),
}
}
pub fn clear_mark(&mut self) {
if let Some(ppr) = self.inner.properties.as_mut() {
ppr.rpr = None;
}
}
fn ensure_ppr(&mut self) -> &mut CT_PPr {
self.inner.properties.get_or_insert_with(CT_PPr::default)
}
fn ensure_sect_pr(&mut self) -> &mut CT_SectPr {
let ppr = self.ensure_ppr();
ppr.sect_pr.get_or_insert_with(CT_SectPr::default_letter)
}
}
#[derive(Default)]
struct RangeAnchors {
comment_starts: Vec<i32>,
comment_ends: Vec<i32>,
comment_references: Vec<i32>,
bookmark_starts: Vec<(i32, String)>,
bookmark_ends: Vec<i32>,
permission_starts: Vec<Vec<u8>>,
permission_ends: Vec<Vec<u8>>,
}
const PAIRED_RANGES: [&str; 6] = [
"moveFromRange",
"moveToRange",
"customXmlInsRange",
"customXmlDelRange",
"customXmlMoveFromRange",
"customXmlMoveToRange",
];
impl RangeAnchors {
fn scan(paragraph: &CT_P) -> crate::Result<Self> {
let spans_paragraphs = || {
crate::Error::Other(
"the paragraph holds part of a field that spans several paragraphs".to_owned(),
)
};
let mut xml = Vec::new();
paragraph.to_xml(&mut quick_xml::Writer::new(&mut xml))?;
let mut reader = quick_xml::Reader::from_reader(xml.as_slice());
let mut anchors = Self::default();
let mut fields: Vec<bool> = Vec::new();
let mut ranges: Vec<(&str, bool, Option<String>)> = Vec::new();
let mut buffer = Vec::new();
let mut skipped = Vec::new();
loop {
let before = reader.buffer_position() as usize;
let (element, empty) = match reader.read_event_into(&mut buffer) {
Ok(quick_xml::events::Event::Start(element)) => (element, false),
Ok(quick_xml::events::Event::Empty(element)) => (element, true),
Ok(quick_xml::events::Event::Eof) => break,
Ok(_) => {
buffer.clear();
continue;
}
Err(error) => {
return Err(crate::Error::Other(format!(
"paragraph anchor scan failed: {error}"
)));
}
};
let prefix = element.name().prefix().map(|prefix| prefix.into_inner());
let attribute = |local: &[u8]| {
element.attributes().flatten().find_map(|attribute| {
(attribute.key.local_name().as_ref() == local
&& attribute.key.prefix().map(|prefix| prefix.into_inner()) == prefix)
.then(|| {
attribute
.decoded_and_normalized_value(
quick_xml::XmlVersion::Implicit1_0,
element.decoder(),
)
.ok()
.map(|value| value.into_owned())
})
.flatten()
})
};
let id = attribute(b"id").and_then(|value| value.parse::<i32>().ok());
let push = |list: &mut Vec<i32>| {
if let Some(id) = id
&& !list.contains(&id)
{
list.push(id);
}
};
let local = element.local_name().as_ref().to_vec();
match local.as_slice() {
b"commentRangeStart" => push(&mut anchors.comment_starts),
b"commentRangeEnd" => push(&mut anchors.comment_ends),
b"commentReference" => push(&mut anchors.comment_references),
b"bookmarkEnd" => push(&mut anchors.bookmark_ends),
b"bookmarkStart" => {
if let (Some(id), Some(name)) = (id, attribute(b"name"))
&& !anchors
.bookmark_starts
.iter()
.any(|(known, _)| *known == id)
{
anchors.bookmark_starts.push((id, name));
}
}
b"permStart" | b"permEnd" => {
if !empty {
reader
.read_to_end_into(element.name(), &mut skipped)
.map_err(|error| {
crate::Error::Other(format!(
"paragraph anchor scan failed: {error}"
))
})?;
skipped.clear();
}
let raw = xml[before..reader.buffer_position() as usize].to_vec();
let list = if local == b"permStart" {
&mut anchors.permission_starts
} else {
&mut anchors.permission_ends
};
if !list.contains(&raw) {
list.push(raw);
}
}
b"fldChar" => match attribute(b"fldCharType").as_deref() {
Some("begin") => fields.push(false),
Some("separate") => {
*fields.last_mut().ok_or_else(spans_paragraphs)? = true;
}
Some("end") => {
fields.pop().ok_or_else(spans_paragraphs)?;
}
_ => {}
},
b"instrText" | b"delInstrText" if fields.is_empty() => {
return Err(spans_paragraphs());
}
_ => {
let name = String::from_utf8_lossy(&local);
let range = PAIRED_RANGES.iter().find_map(|family| {
let end = name.strip_prefix(family)?;
matches!(end, "Start" | "End").then_some((*family, end == "Start"))
});
if let Some((family, start)) = range {
ranges.push((family, start, attribute(b"id")));
}
}
}
buffer.clear();
}
if !fields.is_empty() {
return Err(spans_paragraphs());
}
for family in PAIRED_RANGES {
let ids = |start: bool| {
ranges
.iter()
.filter(|(known, is_start, _)| *known == family && *is_start == start)
.map(|(_, _, id)| id)
.collect::<std::collections::BTreeSet<_>>()
};
if ids(true) != ids(false) {
return Err(crate::Error::Other(format!(
"the paragraph holds one end of a w:{family} range whose other end lies \
in another paragraph"
)));
}
}
Ok(anchors)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SectionBreak {
NextPage,
Continuous,
EvenPage,
OddPage,
}
impl SectionBreak {
fn to_st(self) -> ST_SectionType {
match self {
SectionBreak::NextPage => ST_SectionType::NextPage,
SectionBreak::Continuous => ST_SectionType::Continuous,
SectionBreak::EvenPage => ST_SectionType::EvenPage,
SectionBreak::OddPage => ST_SectionType::OddPage,
}
}
}
pub struct ParagraphRef<'a> {
pub(crate) inner: &'a CT_P,
}
impl<'a> ParagraphRef<'a> {
pub fn text(&self) -> String {
self.inner.accepted_text()
}
pub fn equations(&self) -> impl Iterator<Item = &'a OfficeMath> {
self.inner.equations.iter().map(|(_, _, equation)| equation)
}
pub fn equation(&self, index: usize) -> Option<&'a OfficeMath> {
self.inner
.equations
.get(index)
.map(|(_, _, equation)| equation)
}
pub fn items(&self) -> impl Iterator<Item = ParagraphItemRef<'_>> {
let mut items = Vec::new();
let mut run_index = 0;
while run_index <= self.inner.runs.len() {
let extras = self
.inner
.extra_xml
.iter()
.filter(|(at, _)| *at == run_index)
.map(|(_, raw)| raw)
.collect::<Vec<_>>();
for raw_index in 0..=extras.len() {
let markers = self
.inner
.comment_ranges
.iter()
.filter(|marker| match marker {
CommentRangeMarker::Start {
run_index: at,
raw_before,
..
}
| CommentRangeMarker::End {
run_index: at,
raw_before,
..
} => *at == run_index && (*raw_before).min(extras.len()) == raw_index,
})
.collect::<Vec<_>>();
for marker_index in 0..=markers.len() {
items.extend(
self.inner
.content_controls
.iter()
.filter(|(at, raw_before, markers_before, _)| {
*at == run_index
&& (*raw_before).min(extras.len()) == raw_index
&& (*markers_before).min(markers.len()) == marker_index
})
.map(|(_, _, _, control)| {
ParagraphItemRef::ContentControl(ContentControlRef {
inner: control,
})
}),
);
if let Some(marker) = markers.get(marker_index) {
items.push(match marker {
CommentRangeMarker::Start {
id,
has_child_content,
..
} => ParagraphItemRef::CommentRangeStart {
id: *id,
has_child_content: *has_child_content,
},
CommentRangeMarker::End {
id,
has_child_content,
..
} => ParagraphItemRef::CommentRangeEnd {
id: *id,
has_child_content: *has_child_content,
},
});
}
}
if let Some(raw) = extras.get(raw_index) {
let equation = self
.inner
.equations
.iter()
.find_map(|(at, slot, equation)| {
(*at == run_index && *slot == raw_index).then_some(equation)
});
let revision = self
.inner
.revisions
.iter()
.find_map(|(at, slot, revision)| {
(*at == run_index
&& hyperlink_revision_index(*slot).is_none()
&& *slot == raw_index)
.then_some(revision)
});
let bookmark = self.inner.bookmark_markers.iter().find(|bookmark| {
bookmark.run_index() == run_index && bookmark.raw_before() == raw_index
});
let empty_hyperlink =
self.inner.hyperlinks.iter().enumerate().find(|(_, link)| {
link.run_start == run_index
&& link.run_end == run_index
&& link.preserved_raw_before == Some(raw_index)
});
items.push(if let Some(equation) = equation {
ParagraphItemRef::Equation(equation)
} else if let Some(revision) = revision {
ParagraphItemRef::Revision(RevisionRef { inner: revision })
} else if let Some(bookmark) = bookmark {
if bookmark.is_start() {
ParagraphItemRef::BookmarkStart {
id: bookmark.id(),
name: bookmark.name(),
has_child_content: bookmark.has_child_content(),
}
} else {
ParagraphItemRef::BookmarkEnd {
id: bookmark.id(),
has_child_content: bookmark.has_child_content(),
}
}
} else if let Some((index, _)) = empty_hyperlink {
ParagraphItemRef::Hyperlink(HyperlinkRef {
paragraph: self.inner,
index,
})
} else {
ParagraphItemRef::UnsupportedXml(raw.as_slice())
});
}
}
if run_index == self.inner.runs.len() {
break;
}
if let Some((index, hyperlink)) =
self.inner
.hyperlinks
.iter()
.enumerate()
.find(|(_, hyperlink)| {
hyperlink.run_start == run_index && hyperlink.run_end > run_index
})
{
items.push(ParagraphItemRef::Hyperlink(HyperlinkRef {
paragraph: self.inner,
index,
}));
run_index = hyperlink.run_end;
} else {
items.push(ParagraphItemRef::Run(RunRef {
inner: &self.inner.runs[run_index],
}));
run_index += 1;
}
}
items.into_iter()
}
pub fn run_count(&self) -> usize {
self.inner.accepted_run_paths().len()
}
pub fn run(&self, index: usize) -> Option<RunRef<'_>> {
let path = self.inner.accepted_run_paths().get(index)?.clone();
self.inner.accepted_run(&path).map(|inner| RunRef { inner })
}
#[doc(hidden)]
pub fn run_path(&self, index: usize) -> Option<AcceptedRunPath> {
self.inner.accepted_run_paths().get(index).cloned()
}
pub fn style_id(&self) -> Option<&str> {
self.inner
.properties
.as_ref()
.and_then(|ppr| ppr.style_id.as_deref())
}
pub fn is_page_break_before(&self) -> bool {
self.page_break_before_value().unwrap_or(false)
}
pub fn keep_with_next_value(&self) -> Option<bool> {
self.inner.properties.as_ref().and_then(|ppr| ppr.keep_next)
}
pub fn keep_together_value(&self) -> Option<bool> {
self.inner
.properties
.as_ref()
.and_then(|ppr| ppr.keep_lines)
}
pub fn page_break_before_value(&self) -> Option<bool> {
self.inner
.properties
.as_ref()
.and_then(|ppr| ppr.page_break_before)
}
pub fn widow_control_value(&self) -> Option<bool> {
self.inner
.properties
.as_ref()
.and_then(|ppr| ppr.widow_control)
}
pub fn line_spacing_multiple(&self) -> Option<f64> {
let ppr = self.inner.properties.as_ref()?;
let line = ppr.line_spacing?;
match ppr.line_rule.as_deref() {
None | Some("auto") => Some(line.0 as f64 / 240.0),
_ => None,
}
}
pub fn line_spacing(&self) -> Option<Length> {
let ppr = self.inner.properties.as_ref()?;
let line = ppr.line_spacing?;
match ppr.line_rule.as_deref() {
Some("exact") | Some("atLeast") => Some(Length::twips(line.0)),
_ => None,
}
}
pub fn hyperlink_spans(&self) -> Vec<(usize, usize, Option<&str>)> {
self.inner
.hyperlinks
.iter()
.map(|h| (h.run_start, h.run_end, h.rel_id.as_deref()))
.collect()
}
pub fn numbering(&self) -> Option<(u32, u32)> {
let ppr = self.inner.properties.as_ref()?;
Some((ppr.num_id?, ppr.num_ilvl.unwrap_or(0)))
}
pub fn alignment(&self) -> Option<Alignment> {
self.inner
.properties
.as_ref()
.and_then(|ppr| ppr.jc)
.map(Alignment::from_st_jc)
}
pub fn space_before(&self) -> Option<Length> {
self.inner
.properties
.as_ref()
.and_then(|ppr| ppr.space_before)
.map(|twips| Length::twips(twips.0))
}
pub fn space_after(&self) -> Option<Length> {
self.inner
.properties
.as_ref()
.and_then(|ppr| ppr.space_after)
.map(|twips| Length::twips(twips.0))
}
pub fn indent_left(&self) -> Option<Length> {
self.inner
.properties
.as_ref()
.and_then(|ppr| ppr.ind_left)
.map(|twips| Length::twips(twips.0))
}
pub fn indent_right(&self) -> Option<Length> {
self.inner
.properties
.as_ref()
.and_then(|ppr| ppr.ind_right)
.map(|twips| Length::twips(twips.0))
}
pub fn first_line_indent(&self) -> Option<Length> {
let ppr = self.inner.properties.as_ref()?;
if let Some(twips) = ppr.ind_first_line {
return Some(Length::twips(twips.0));
}
ppr.ind_hanging
.map(|twips| Length::twips(twips.0.saturating_neg()))
}
pub fn runs(&self) -> impl Iterator<Item = RunRef<'_>> {
self.inner
.accepted_bookmark_runs()
.into_iter()
.map(|inner| RunRef { inner })
}
pub fn rubies(&self) -> impl Iterator<Item = &CT_Ruby> {
self.inner.rubies.iter()
}
pub fn ruby(&self, index: usize) -> Option<&CT_Ruby> {
self.inner.rubies.get(index)
}
pub fn has_borders(&self) -> bool {
self.inner
.properties
.as_ref()
.and_then(|ppr| ppr.borders.as_ref())
.map(|b| !b.is_empty())
.unwrap_or(false)
}
pub fn bottom_border(&self) -> Option<ParagraphBorderRef<'_>> {
self.inner
.properties
.as_ref()?
.borders
.as_ref()?
.bottom
.as_ref()
.map(|inner| ParagraphBorderRef { inner })
}
pub fn border_count(&self) -> usize {
let Some(borders) = self
.inner
.properties
.as_ref()
.and_then(|properties| properties.borders.as_ref())
else {
return 0;
};
[
borders.top.as_ref(),
borders.bottom.as_ref(),
borders.left.as_ref(),
borders.right.as_ref(),
borders.between.as_ref(),
borders.bar.as_ref(),
]
.into_iter()
.flatten()
.count()
}
pub fn tab_stop_count(&self) -> usize {
self.inner
.properties
.as_ref()
.and_then(|ppr| ppr.tabs.as_ref())
.map(|t| t.tabs.len())
.unwrap_or(0)
}
pub fn shading_fill(&self) -> Option<&str> {
self.inner
.properties
.as_ref()
.and_then(|ppr| ppr.shading.as_ref())
.and_then(|shd| shd.fill.as_deref())
}
pub fn shading_pattern(&self) -> Option<&str> {
self.inner
.properties
.as_ref()
.and_then(|ppr| ppr.shading.as_ref())
.map(|shd| shd.val.as_str())
}
pub fn shading_color(&self) -> Option<&str> {
self.inner
.properties
.as_ref()
.and_then(|ppr| ppr.shading.as_ref())
.and_then(|shd| shd.color.as_deref())
}
pub fn indent_start(&self) -> Option<Length> {
self.inner
.properties
.as_ref()
.and_then(|ppr| ppr.ind_start)
.map(|twips| Length::twips(twips.0))
}
pub fn indent_end(&self) -> Option<Length> {
self.inner
.properties
.as_ref()
.and_then(|ppr| ppr.ind_end)
.map(|twips| Length::twips(twips.0))
}
pub fn hanging_indent(&self) -> Option<Length> {
self.inner
.properties
.as_ref()
.and_then(|ppr| ppr.ind_hanging)
.map(|twips| Length::twips(twips.0))
}
pub fn mirror_indents_value(&self) -> Option<bool> {
self.inner
.properties
.as_ref()
.and_then(|ppr| ppr.mirror_indents)
}
pub fn adjust_right_indent_value(&self) -> Option<bool> {
self.inner
.properties
.as_ref()
.and_then(|ppr| ppr.adjust_right_ind)
}
pub fn kinsoku_value(&self) -> Option<bool> {
self.inner.properties.as_ref().and_then(|ppr| ppr.kinsoku)
}
pub fn word_wrap_value(&self) -> Option<bool> {
self.inner.properties.as_ref().and_then(|ppr| ppr.word_wrap)
}
pub fn overflow_punct_value(&self) -> Option<bool> {
self.inner
.properties
.as_ref()
.and_then(|ppr| ppr.overflow_punct)
}
pub fn top_line_punct_value(&self) -> Option<bool> {
self.inner
.properties
.as_ref()
.and_then(|ppr| ppr.top_line_punct)
}
pub fn auto_space_de_value(&self) -> Option<bool> {
self.inner
.properties
.as_ref()
.and_then(|ppr| ppr.auto_space_de)
}
pub fn auto_space_dn_value(&self) -> Option<bool> {
self.inner
.properties
.as_ref()
.and_then(|ppr| ppr.auto_space_dn)
}
pub fn snap_to_grid_value(&self) -> Option<bool> {
self.inner
.properties
.as_ref()
.and_then(|ppr| ppr.snap_to_grid)
}
pub fn space_before_auto_value(&self) -> Option<bool> {
self.inner
.properties
.as_ref()
.and_then(|ppr| ppr.before_autospacing)
}
pub fn space_after_auto_value(&self) -> Option<bool> {
self.inner
.properties
.as_ref()
.and_then(|ppr| ppr.after_autospacing)
}
pub fn contextual_spacing_value(&self) -> Option<bool> {
self.inner
.properties
.as_ref()
.and_then(|ppr| ppr.contextual_spacing)
}
pub fn outline_level(&self) -> Option<u32> {
self.inner
.properties
.as_ref()
.and_then(|ppr| ppr.outline_lvl)
}
pub fn border(&self, edge: ParagraphBorderEdge) -> Option<ParagraphBorderRef<'_>> {
let borders = self.inner.properties.as_ref()?.borders.as_ref()?;
let inner = match edge {
ParagraphBorderEdge::Top => borders.top.as_ref(),
ParagraphBorderEdge::Bottom => borders.bottom.as_ref(),
ParagraphBorderEdge::Left => borders.left.as_ref(),
ParagraphBorderEdge::Right => borders.right.as_ref(),
ParagraphBorderEdge::Between => borders.between.as_ref(),
ParagraphBorderEdge::Bar => borders.bar.as_ref(),
}?;
Some(ParagraphBorderRef { inner })
}
pub fn tab_stop(&self, index: usize) -> Option<TabStopRef<'_>> {
self.inner
.properties
.as_ref()?
.tabs
.as_ref()?
.tabs
.get(index)
.map(|inner| TabStopRef { inner })
}
pub fn suppress_line_numbers_value(&self) -> Option<bool> {
self.inner
.properties
.as_ref()
.and_then(|ppr| ppr.suppress_line_numbers)
}
pub fn suppress_auto_hyphens_value(&self) -> Option<bool> {
self.inner
.properties
.as_ref()
.and_then(|ppr| ppr.suppress_auto_hyphens)
}
pub fn frame(&self) -> Option<ParagraphFrame> {
self.inner
.properties
.as_ref()?
.frame
.as_deref()
.map(ParagraphFrame::from_ct)
}
pub fn suppress_overlap_value(&self) -> Option<bool> {
self.inner
.properties
.as_ref()
.and_then(|ppr| ppr.suppress_overlap)
}
pub fn textbox_tight_wrap(&self) -> Option<TextboxTightWrap> {
self.inner
.properties
.as_ref()?
.textbox_tight_wrap
.as_deref()
.and_then(TextboxTightWrap::from_str)
}
pub fn right_to_left_value(&self) -> Option<bool> {
self.inner.properties.as_ref().and_then(|ppr| ppr.bidi)
}
pub fn text_direction(&self) -> Option<ParagraphTextDirection> {
self.inner
.properties
.as_ref()?
.text_direction
.as_deref()
.and_then(ParagraphTextDirection::from_str)
}
pub fn text_alignment(&self) -> Option<ParagraphTextAlignment> {
self.inner
.properties
.as_ref()?
.text_alignment
.as_deref()
.and_then(ParagraphTextAlignment::from_str)
}
pub fn div_id(&self) -> Option<u32> {
self.inner.properties.as_ref().and_then(|ppr| ppr.div_id)
}
pub fn conditional_formatting(&self) -> Option<TableConditionalFormatting> {
self.inner
.properties
.as_ref()?
.cnf_style
.as_deref()
.and_then(TableConditionalFormatting::from_value)
}
pub fn mark(&self) -> ParagraphMarkRef<'_> {
ParagraphMarkRef {
inner: self
.inner
.properties
.as_ref()
.and_then(|ppr| ppr.rpr.as_ref()),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn hyperlink_reader_exposes_modeled_and_unmodeled_attributes() {
let mut paragraph = CT_P::new();
paragraph.add_run("linked");
paragraph.hyperlinks.push(HyperlinkSpan {
rel_id: Some("rId1".to_owned()),
anchor: None,
tooltip: Some("Open link".to_owned()),
doc_location: Some("section-two".to_owned()),
run_start: 0,
run_end: 1,
extra_attributes: vec![("w:history".to_owned(), "1".to_owned())],
extra_xml: Vec::new(),
preserved_raw_before: None,
});
let hyperlink = HyperlinkRef {
paragraph: ¶graph,
index: 0,
};
assert_eq!(hyperlink.tooltip(), Some("Open link"));
assert_eq!(hyperlink.doc_location(), Some("section-two"));
assert!(hyperlink.has_unmodeled_semantic_attributes());
}
#[test]
fn bottom_border_accepts_an_explicit_content_gap() {
let mut inner = CT_P::new();
let mut paragraph = Paragraph { inner: &mut inner };
paragraph.set_border_bottom_with_space(BorderStyle::Single, 14, 6, "auto");
let paragraph = ParagraphRef { inner: &inner };
let border = paragraph.bottom_border().expect("bottom border");
assert_eq!(border.style(), "single");
assert_eq!(border.size_eighths_pt(), Some(14));
assert_eq!(border.space_points(), Some(6));
assert_eq!(border.color(), Some("auto"));
}
const W: &str = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
fn fragment(children: &str) -> CT_P {
CT_P::from_xml_fragment(format!(r#"<w:p xmlns:w="{W}">{children}</w:p>"#).as_bytes())
.unwrap()
}
fn serialized(paragraph: &CT_P) -> String {
let mut xml = Vec::new();
paragraph
.to_xml(&mut quick_xml::Writer::new(&mut xml))
.unwrap();
String::from_utf8(xml).unwrap()
}
#[test]
fn set_text_keeps_properties_and_moves_every_anchor_to_the_new_run() {
let mut inner = fragment(concat!(
r#"<w:pPr><w:pStyle w:val="Heading1"/><w:jc w:val="center"/></w:pPr>"#,
r#"<w:bookmarkStart w:id="1" w:name="_Toc1"/>"#,
r#"<w:hyperlink w:anchor="x"><w:commentRangeStart w:id="7"/>"#,
r#"<w:r><w:rPr><w:b/></w:rPr><w:t>old</w:t></w:r></w:hyperlink>"#,
r#"<w:r><w:t xml:space="preserve"> text</w:t></w:r>"#,
r#"<w:commentRangeEnd w:id="7"/><w:r><w:commentReference w:id="7"/></w:r>"#,
r#"<w:bookmarkEnd w:id="1"/><w:proofErr w:type="spellStart"/>"#,
r#"<w:r><w:fldChar w:fldCharType="begin"/></w:r>"#,
r#"<w:r><w:instrText> PAGE </w:instrText></w:r>"#,
r#"<w:r><w:fldChar w:fldCharType="separate"/></w:r>"#,
r#"<w:r><w:t>1</w:t></w:r><w:r><w:fldChar w:fldCharType="end"/></w:r>"#,
));
let mut paragraph = Paragraph { inner: &mut inner };
paragraph.set_text("new\ttext\nline").unwrap();
assert_eq!(paragraph.text(), "new\ttext\nline");
assert_eq!(paragraph.run_count(), 2);
let xml = serialized(&inner);
let children = xml
.split_once("</w:pPr>")
.and_then(|(_, rest)| rest.strip_suffix("</w:p>"))
.unwrap();
assert_eq!(
children,
concat!(
r#"<w:commentRangeStart w:id="7"/><w:bookmarkStart w:id="1" w:name="_Toc1"/>"#,
r#"<w:r><w:t>new</w:t><w:tab/><w:t>text</w:t><w:br/><w:t>line</w:t></w:r>"#,
r#"<w:bookmarkEnd w:id="1"/><w:commentRangeEnd w:id="7"/>"#,
r#"<w:r><w:commentReference w:id="7"/></w:r>"#,
)
);
assert!(xml.contains(r#"<w:pStyle w:val="Heading1"/><w:jc w:val="center"/>"#));
assert!(inner.hyperlinks.is_empty());
}
#[test]
fn set_text_with_empty_text_leaves_no_run_and_a_carriage_return_breaks() {
let mut inner = fragment(r#"<w:r><w:t>old</w:t></w:r>"#);
let mut paragraph = Paragraph { inner: &mut inner };
paragraph.set_text("").unwrap();
assert_eq!(paragraph.run_count(), 0);
assert_eq!(serialized(&inner), "<w:p/>");
let mut paragraph = Paragraph { inner: &mut inner };
paragraph.set_text(" a\r\nb ").unwrap();
assert_eq!(paragraph.text(), " a\n\nb ");
assert_eq!(
serialized(&inner),
concat!(
r#"<w:p><w:r><w:t xml:space="preserve"> a</w:t><w:br/><w:br/>"#,
r#"<w:t xml:space="preserve">b </w:t></w:r></w:p>"#,
)
);
}
#[test]
fn set_text_rejects_one_end_of_a_field_that_spans_paragraphs() {
for children in [
concat!(
r#"<w:r><w:fldChar w:fldCharType="begin"/></w:r>"#,
r#"<w:r><w:instrText> TOC \o "1-3" </w:instrText></w:r>"#,
r#"<w:r><w:fldChar w:fldCharType="separate"/></w:r><w:r><w:t>Entry</w:t></w:r>"#,
),
r#"<w:r><w:t>Entry</w:t></w:r><w:r><w:fldChar w:fldCharType="end"/></w:r>"#,
concat!(
r#"<w:r><w:instrText> \h </w:instrText></w:r>"#,
r#"<w:r><w:fldChar w:fldCharType="separate"/></w:r><w:r><w:t>Entry</w:t></w:r>"#,
),
r#"<w:r><w:fldChar w:fldCharType="separate"/></w:r><w:r><w:t>Entry</w:t></w:r>"#,
r#"<w:r><w:instrText> \z </w:instrText></w:r>"#,
concat!(
r#"<w:r><w:fldChar w:fldCharType="end"/></w:r>"#,
r#"<w:r><w:fldChar w:fldCharType="begin"/></w:r>"#,
),
] {
let mut inner = fragment(children);
let before = serialized(&inner);
let error = Paragraph { inner: &mut inner }
.set_text("replaced")
.unwrap_err();
assert!(error.to_string().contains("field"), "{error}");
assert_eq!(serialized(&inner), before);
}
}
#[test]
fn set_text_keeps_permission_ranges_and_drops_only_paired_revision_ranges() {
let mut inner = fragment(concat!(
r#"<w:permStart w:id="5" w:edGrp="everyone"/><w:r><w:t>open</w:t></w:r>"#,
r#"<w:permEnd w:id="4"/>"#,
r#"<w:moveFromRangeStart w:id="8" w:author="Ada" w:name="move1"/>"#,
r#"<w:moveFrom w:id="9" w:author="Ada"><w:r><w:t>moved</w:t></w:r></w:moveFrom>"#,
r#"<w:moveFromRangeEnd w:id="8"/>"#,
));
let mut paragraph = Paragraph { inner: &mut inner };
paragraph.set_text("typed").unwrap();
let xml = serialized(&inner);
let children = xml
.strip_prefix("<w:p>")
.and_then(|rest| rest.strip_suffix("</w:p>"))
.unwrap();
assert_eq!(
children,
concat!(
r#"<w:permStart w:id="5" w:edGrp="everyone"/>"#,
r#"<w:r><w:t>typed</w:t></w:r><w:permEnd w:id="4"/>"#,
)
);
for children in [
r#"<w:moveToRangeStart w:id="3" w:author="Ada" w:name="move1"/><w:r><w:t>a</w:t></w:r>"#,
r#"<w:r><w:t>a</w:t></w:r><w:moveFromRangeEnd w:id="3"/>"#,
r#"<w:customXmlInsRangeStart w:id="6" w:author="Ada"/><w:r><w:t>a</w:t></w:r>"#,
] {
let mut inner = fragment(children);
let before = serialized(&inner);
let error = Paragraph { inner: &mut inner }
.set_text("replaced")
.unwrap_err();
assert!(error.to_string().contains("range"), "{error}");
assert_eq!(serialized(&inner), before);
}
}
#[test]
fn set_text_keeps_comments_and_bookmarks_through_a_package_round_trip() {
use crate::{RunPosition, RunRange};
let position = |body_index, run_index| RunPosition {
body_index,
run_index,
};
let mut document = crate::Document::new();
document.add_paragraph("Review ").add_run("this sentence.");
document.add_paragraph("Second paragraph.");
let range = |start, end| RunRange { start, end };
document
.add_comment(range(position(0, 1), position(0, 2)), "Ada", None, "inside")
.unwrap();
document
.add_comment(range(position(0, 1), position(1, 1)), "Ada", None, "across")
.unwrap();
document
.add_bookmark("target", range(position(0, 0), position(0, 2)))
.unwrap();
document
.paragraph_mut(0)
.unwrap()
.set_text("Rewritten")
.unwrap();
let reopened = crate::Document::from_bytes(&document.to_bytes().unwrap()).unwrap();
assert_eq!(reopened.paragraph(0).unwrap().text(), "Rewritten");
assert_eq!(reopened.paragraph(1).unwrap().text(), "Second paragraph.");
let comments = reopened
.comments()
.iter()
.map(|comment| comment.text())
.collect::<Vec<_>>();
assert_eq!(comments, ["inside", "across"]);
let bookmarks = reopened.bookmarks();
assert_eq!(bookmarks.len(), 1);
assert_eq!(bookmarks[0].name(), Some("target"));
assert_eq!(bookmarks[0].issue(), None);
assert_eq!(
bookmarks[0].range(),
Some(range(position(0, 0), position(0, 1)))
);
let body = serialized(reopened.paragraph(0).unwrap().inner);
for anchor in [
"commentRangeStart",
"commentRangeEnd",
"commentReference",
"bookmarkStart",
"bookmarkEnd",
] {
assert!(body.contains(anchor), "{anchor} missing from {body}");
}
}
}