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_PPr, CT_Shd};
use rdocx_oxml::shared::{
ST_Border, ST_Jc, ST_PageOrientation, ST_SectionType, ST_TabJc, ST_TabLeader,
};
use rdocx_oxml::text::{
BreakType, CT_P, CT_R, CommentRangeMarker, HyperlinkSpan, RunContent, hyperlink_revision_index,
};
use rdocx_oxml::units::Twips;
use crate::run::{Run, RunRef};
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(i32),
CommentRangeEnd(i32),
BookmarkStart {
id: Option<i32>,
name: Option<&'a str>,
},
BookmarkEnd {
id: Option<i32>,
},
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 {
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,
}
}
}
#[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,
}
}
}
pub struct Paragraph<'a> {
pub(crate) inner: &'a mut CT_P,
}
impl<'a> Paragraph<'a> {
pub fn text(&self) -> String {
self.inner.text()
}
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 }];
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_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) {
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);
}
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.runs.len()
}
pub fn run(&self, index: usize) -> Option<RunRef<'_>> {
self.inner.runs.get(index).map(|inner| RunRef { inner })
}
pub fn run_mut(&mut self, index: usize) -> Option<Run<'_>> {
self.inner.runs.get_mut(index).map(|inner| Run { inner })
}
pub fn runs(&self) -> impl Iterator<Item = RunRef<'_>> {
self.inner.runs.iter().map(|r| RunRef { inner: r })
}
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 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(CT_Shd {
val: "clear".to_string(),
color: Some("auto".to_string()),
fill: Some(fill_color.to_string()),
});
}
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()),
};
self.ensure_ppr().borders = Some(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 set_border_bottom(&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()),
};
let borders = self
.ensure_ppr()
.borders
.get_or_insert_with(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) {
self.ensure_ppr().outline_lvl = 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());
}
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(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.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, .. } => {
ParagraphItemRef::CommentRangeStart(*id)
}
CommentRangeMarker::End { id, .. } => {
ParagraphItemRef::CommentRangeEnd(*id)
}
});
}
}
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(),
}
} else {
ParagraphItemRef::BookmarkEnd { id: bookmark.id() }
}
} 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.runs.len()
}
pub fn run(&self, index: usize) -> Option<RunRef<'_>> {
self.inner.runs.get(index).map(|inner| RunRef { inner })
}
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.runs.iter().map(|r| RunRef { inner: r })
}
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())
}
}
#[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());
}
}