use rdocx_oxml::borders::CT_BorderEdge;
use rdocx_oxml::drawing::{CT_Drawing, CT_Inline};
use rdocx_oxml::properties::{
CT_EastAsianLayout, CT_FitText, CT_RPr, CT_Shd, ST_Em, ST_TextEffect,
};
use rdocx_oxml::shared::{ST_HighlightColor, ST_Underline};
use rdocx_oxml::text::{BreakType, CT_R, CT_Text, Field, RunContent, SpecialCharacter};
use rdocx_oxml::units::{HalfPoint, Twips};
use crate::{Error, Length, Result};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BreakKind {
Line,
Page,
Column,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DrawingKind {
Image,
Shape,
Other,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DrawingRelationshipKind {
Embedded,
Linked,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FieldKind {
Simple,
Complex,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RunFontSlot {
Ascii,
HighAnsi,
EastAsia,
ComplexScript,
}
impl RunFontSlot {
fn explicit_mut(self, rpr: &mut CT_RPr) -> &mut Option<String> {
match self {
RunFontSlot::Ascii => &mut rpr.font_ascii,
RunFontSlot::HighAnsi => &mut rpr.font_hansi,
RunFontSlot::EastAsia => &mut rpr.font_east_asia,
RunFontSlot::ComplexScript => &mut rpr.font_cs,
}
}
fn theme_mut(self, rpr: &mut CT_RPr) -> &mut Option<String> {
match self {
RunFontSlot::Ascii => &mut rpr.font_ascii_theme,
RunFontSlot::HighAnsi => &mut rpr.font_hansi_theme,
RunFontSlot::EastAsia => &mut rpr.font_east_asia_theme,
RunFontSlot::ComplexScript => &mut rpr.font_cs_theme,
}
}
fn explicit(self, rpr: &CT_RPr) -> Option<&str> {
match self {
RunFontSlot::Ascii => rpr.font_ascii.as_deref(),
RunFontSlot::HighAnsi => rpr.font_hansi.as_deref(),
RunFontSlot::EastAsia => rpr.font_east_asia.as_deref(),
RunFontSlot::ComplexScript => rpr.font_cs.as_deref(),
}
}
fn theme(self, rpr: &CT_RPr) -> Option<&str> {
match self {
RunFontSlot::Ascii => rpr.font_ascii_theme.as_deref(),
RunFontSlot::HighAnsi => rpr.font_hansi_theme.as_deref(),
RunFontSlot::EastAsia => rpr.font_east_asia_theme.as_deref(),
RunFontSlot::ComplexScript => rpr.font_cs_theme.as_deref(),
}
}
}
pub type RunProperties = CT_RPr;
#[derive(Debug, Clone, Copy)]
pub struct DrawingRef<'a> {
inner: &'a CT_Drawing,
}
impl DrawingRef<'_> {
pub fn kind(&self) -> DrawingKind {
if self.relationship_id().is_some() {
DrawingKind::Image
} else if self
.inner
.anchor
.as_ref()
.is_some_and(|anchor| anchor.shape.is_some())
{
DrawingKind::Shape
} else {
DrawingKind::Other
}
}
pub fn is_inline(&self) -> bool {
self.inner.inline.is_some()
}
pub fn is_anchor(&self) -> bool {
self.inner.anchor.is_some()
}
pub fn relationship_id(&self) -> Option<&str> {
self.inner
.inline
.as_ref()
.and_then(|inline| {
(!inline.embed_id.is_empty())
.then_some(inline.embed_id.as_str())
.or(inline.link_id.as_deref())
})
.or_else(|| {
self.inner.anchor.as_ref().and_then(|anchor| {
(!anchor.embed_id.is_empty())
.then_some(anchor.embed_id.as_str())
.or(anchor.link_id.as_deref())
})
})
}
pub fn relationship_kind(&self) -> Option<DrawingRelationshipKind> {
self.inner
.inline
.as_ref()
.map(|inline| inline.embed_id.is_empty() && inline.link_id.is_some())
.or_else(|| {
self.inner
.anchor
.as_ref()
.map(|anchor| anchor.embed_id.is_empty() && anchor.link_id.is_some())
})
.filter(|_| self.relationship_id().is_some())
.map(|linked| {
if linked {
DrawingRelationshipKind::Linked
} else {
DrawingRelationshipKind::Embedded
}
})
}
pub fn description(&self) -> Option<&str> {
self.inner
.inline
.as_ref()
.and_then(|inline| inline.description.as_deref())
.or_else(|| {
self.inner
.anchor
.as_ref()
.and_then(|anchor| anchor.description.as_deref())
})
}
pub fn name(&self) -> Option<&str> {
self.inner
.inline
.as_ref()
.and_then(|inline| inline.name.as_deref())
.or_else(|| {
self.inner
.anchor
.as_ref()
.and_then(|anchor| anchor.name.as_deref())
})
}
pub fn width(&self) -> Option<Length> {
self.inner
.inline
.as_ref()
.map(|inline| Length::emu(inline.extent_cx.0))
.or_else(|| {
self.inner
.anchor
.as_ref()
.map(|anchor| Length::emu(anchor.extent_cx.0))
})
}
pub fn height(&self) -> Option<Length> {
self.inner
.inline
.as_ref()
.map(|inline| Length::emu(inline.extent_cy.0))
.or_else(|| {
self.inner
.anchor
.as_ref()
.map(|anchor| Length::emu(anchor.extent_cy.0))
})
}
}
#[derive(Debug, Clone, Copy)]
pub struct FieldRef<'a> {
inner: &'a Field,
}
#[derive(Debug, Clone, Copy)]
pub struct FieldDisplaySegmentRef<'a> {
text: &'a str,
properties: Option<&'a RunProperties>,
}
impl FieldDisplaySegmentRef<'_> {
pub fn text(&self) -> &str {
self.text
}
pub fn properties(&self) -> Option<&RunProperties> {
self.properties
}
}
#[derive(Debug, Clone, Copy)]
pub struct LegacyHorizontalRuleRef<'a> {
raw_xml: &'a [u8],
}
impl LegacyHorizontalRuleRef<'_> {
pub fn raw_xml(&self) -> &[u8] {
self.raw_xml
}
}
impl FieldRef<'_> {
pub fn kind(&self) -> FieldKind {
if self.inner.is_complex() {
FieldKind::Complex
} else {
FieldKind::Simple
}
}
pub fn instruction(&self) -> &str {
&self.inner.instruction.raw
}
pub fn name(&self) -> &str {
&self.inner.instruction.name
}
pub fn cached_result(&self) -> &str {
&self.inner.cached_result
}
pub fn dirty(&self) -> Option<bool> {
self.inner.dirty
}
pub fn locked(&self) -> Option<bool> {
self.inner.locked()
}
pub fn has_unmodeled_semantic_attributes(&self) -> bool {
self.inner.has_unmodeled_semantic_attributes()
}
pub fn cached_display_segments(&self) -> Vec<FieldDisplaySegmentRef<'_>> {
self.inner
.cached_display_segments()
.into_iter()
.map(|(text, properties)| FieldDisplaySegmentRef { text, properties })
.collect()
}
}
#[derive(Debug, Clone, Copy)]
#[non_exhaustive]
pub enum RunItemRef<'a> {
Text(&'a str),
DeletedText(&'a str),
Tab,
Break(BreakKind),
Drawing(DrawingRef<'a>),
Field(FieldRef<'a>),
FootnoteReference(i32),
EndnoteReference(i32),
CommentReference(i32),
LegacyHorizontalRule(LegacyHorizontalRuleRef<'a>),
Symbol { font: &'a str, char_code: u16 },
SpecialCharacter(SpecialCharacter),
LastRenderedPageBreak(&'a [u8]),
UnsupportedXml(&'a [u8]),
}
fn classify_raw_run_item(raw_xml: &[u8], encoded_position: Option<usize>) -> RunItemRef<'_> {
if encoded_position.is_some_and(CT_R::raw_child_is_legacy_horizontal_rule) {
RunItemRef::LegacyHorizontalRule(LegacyHorizontalRuleRef { raw_xml })
} else if raw_is_last_rendered_page_break(raw_xml) {
RunItemRef::LastRenderedPageBreak(raw_xml)
} else {
RunItemRef::UnsupportedXml(raw_xml)
}
}
fn raw_is_last_rendered_page_break(raw_xml: &[u8]) -> bool {
const NAME: &[u8] = b"lastRenderedPageBreak";
let Some(body) = raw_xml.strip_prefix(b"<") else {
return false;
};
let end = body
.iter()
.position(|byte| matches!(byte, b' ' | b'\t' | b'\r' | b'\n' | b'/' | b'>'))
.unwrap_or(body.len());
let qualified = &body[..end];
let local = qualified
.rsplit(|byte| *byte == b':')
.next()
.unwrap_or(qualified);
local == NAME
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum UnderlineStyle {
None,
Single,
Double,
Thick,
Dotted,
Dash,
Wave,
Words,
}
impl UnderlineStyle {
fn to_code(self) -> i32 {
match self {
Self::None => 0,
Self::Single => 1,
Self::Words => 2,
Self::Double => 3,
Self::Dotted => 4,
Self::Thick => 6,
Self::Dash => 7,
Self::Wave => 11,
}
}
}
fn underline_from_code(code: i32) -> Option<ST_Underline> {
match code {
0 => Some(ST_Underline::None),
1 => Some(ST_Underline::Single),
2 => Some(ST_Underline::Words),
3 => Some(ST_Underline::Double),
4 => Some(ST_Underline::Dotted),
6 => Some(ST_Underline::Thick),
7 => Some(ST_Underline::Dash),
9 => Some(ST_Underline::DotDash),
10 => Some(ST_Underline::DotDotDash),
11 => Some(ST_Underline::Wave),
_ => None,
}
}
fn underline_to_code(value: ST_Underline) -> i32 {
match value {
ST_Underline::None => 0,
ST_Underline::Single => 1,
ST_Underline::Words => 2,
ST_Underline::Double => 3,
ST_Underline::Dotted => 4,
ST_Underline::Thick => 6,
ST_Underline::Dash => 7,
ST_Underline::DotDash => 9,
ST_Underline::DotDotDash => 10,
ST_Underline::Wave => 11,
}
}
pub struct Run<'a> {
pub(crate) inner: &'a mut CT_R,
}
impl<'a> Run<'a> {
pub fn text(&self) -> String {
self.inner.text()
}
pub fn set_text(&mut self, text: &str) {
self.inner
.replace_content(vec![RunContent::Text(CT_Text::new(text))]);
}
pub fn add_text(&mut self, text: &str) {
self.inner
.append_content(RunContent::Text(CT_Text::new(text)));
}
pub fn add_tab(&mut self) {
self.inner.append_content(RunContent::Tab);
}
pub fn add_break(&mut self, kind: BreakKind) {
let kind = match kind {
BreakKind::Line => BreakType::Line,
BreakKind::Page => BreakType::Page,
BreakKind::Column => BreakType::Column,
};
self.inner.append_content(RunContent::Break(kind));
}
pub fn add_picture(&mut self, relationship_id: &str, width: Length, height: Length) {
let inline = CT_Inline::new(relationship_id, width.to_emu(), height.to_emu());
self.inner
.append_content(RunContent::Drawing(CT_Drawing::inline(inline)));
}
pub fn add_field(&mut self, instruction: &str, cached_result: &str) -> Result<()> {
oxml_core::xml::reject_non_xml_characters("field instruction", instruction)?;
oxml_core::xml::reject_non_xml_characters("field result", cached_result)?;
let field = Field::new(instruction, cached_result);
if field.instruction.name.is_empty() {
return Err(Error::Other(
"field instruction must contain a field name".to_owned(),
));
}
self.inner.append_content(RunContent::Field(field));
Ok(())
}
pub fn add_field_value(&mut self, field: Field) -> Result<()> {
field.validate_for_attachment()?;
self.inner.append_content(RunContent::Field(field));
Ok(())
}
pub fn add_symbol(&mut self, symbol: char) {
let mut encoded = [0_u8; 4];
self.add_text(symbol.encode_utf8(&mut encoded));
}
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>) {
if val.is_none() && self.inner.properties.is_none() {
return;
}
let rpr = self.ensure_rpr();
rpr.bold = val;
rpr.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>) {
if val.is_none() && self.inner.properties.is_none() {
return;
}
let rpr = self.ensure_rpr();
rpr.italic = val;
rpr.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_style_value(Some(if val {
UnderlineStyle::Single
} else {
UnderlineStyle::None
}));
}
pub fn underline_style(mut self, style: UnderlineStyle) -> Self {
self.set_underline_style(style);
self
}
pub fn set_underline_style(&mut self, style: UnderlineStyle) {
self.set_underline_style_value(Some(style));
}
pub fn set_underline_style_value(&mut self, style: Option<UnderlineStyle>) {
let applied = self.set_underline_code_value(style.map(UnderlineStyle::to_code));
debug_assert!(applied);
}
pub fn set_underline_code_value(&mut self, code: Option<i32>) -> bool {
let underline = match code {
Some(code) => match underline_from_code(code) {
Some(underline) => Some(underline),
None => return false,
},
None => None,
};
if underline.is_none() && self.inner.properties.is_none() {
return true;
}
self.ensure_rpr().underline = underline;
true
}
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>) {
if pt.is_none() && self.inner.properties.is_none() {
return;
}
let hp = pt.map(HalfPoint::from_pt);
let rpr = self.ensure_rpr();
rpr.sz = hp;
rpr.sz_cs = hp;
}
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>) {
if name.is_none() && self.inner.properties.is_none() {
return;
}
let rpr = self.ensure_rpr();
rpr.font_ascii = name.map(str::to_owned);
rpr.font_hansi = name.map(str::to_owned);
rpr.font_east_asia = name.map(str::to_owned);
rpr.font_cs = name.map(str::to_owned);
rpr.font_ascii_theme = None;
rpr.font_hansi_theme = None;
rpr.font_east_asia_theme = None;
rpr.font_cs_theme = None;
}
pub fn set_slot_font(&mut self, slot: RunFontSlot, name: Option<&str>) {
if name.is_none() && self.inner.properties.is_none() {
return;
}
let rpr = self.ensure_rpr();
*slot.explicit_mut(rpr) = name.map(str::to_owned);
*slot.theme_mut(rpr) = None;
}
pub fn set_slot_theme_font(&mut self, slot: RunFontSlot, theme: Option<&str>) {
if theme.is_none() && self.inner.properties.is_none() {
return;
}
let rpr = self.ensure_rpr();
*slot.theme_mut(rpr) = theme.map(str::to_owned);
*slot.explicit_mut(rpr) = None;
}
pub fn set_font_hint(&mut self, hint: Option<&str>) {
if hint.is_none() && self.inner.properties.is_none() {
return;
}
self.ensure_rpr().font_hint = hint.map(str::to_owned);
}
pub fn language(mut self, language: &str) -> Self {
self.set_language(language);
self
}
pub fn set_language(&mut self, language: &str) {
self.set_language_value(Some(language));
}
pub fn set_language_value(&mut self, language: Option<&str>) {
if language.is_none() && self.inner.properties.is_none() {
return;
}
self.ensure_rpr().language = language.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>) {
if hex.is_none() && self.inner.properties.is_none() {
return;
}
let rpr = self.ensure_rpr();
rpr.color = hex.map(str::to_owned);
rpr.color_theme = None;
rpr.color_theme_tint = None;
rpr.color_theme_shade = None;
}
pub fn set_color_theme(&mut self, theme: Option<&str>, tint: Option<u8>, shade: Option<u8>) {
if theme.is_none() && self.inner.properties.is_none() {
return;
}
let rpr = self.ensure_rpr();
rpr.color_theme = theme.map(str::to_owned);
rpr.color_theme_tint = theme.and(tint);
rpr.color_theme_shade = theme.and(shade);
}
pub fn highlight(mut self, color: &str) -> Self {
self.set_highlight(color);
self
}
pub fn set_highlight(&mut self, color: &str) {
self.ensure_rpr().shading = Some(Box::new(CT_Shd {
val: "clear".to_string(),
color: Some("auto".to_string()),
fill: Some(color.to_string()),
..Default::default()
}));
}
pub fn set_highlight_value(&mut self, color: Option<&str>) -> bool {
match color {
Some(color) => {
let Ok(color) = ST_HighlightColor::from_str(color) else {
return false;
};
self.ensure_rpr().highlight = Some(color);
}
None => {
if let Some(properties) = self.inner.properties.as_mut() {
properties.highlight = None;
}
}
}
true
}
pub fn set_shading_value(&mut self, color: Option<&str>) {
match color {
Some(color) => self.set_highlight(color),
None => {
if let Some(properties) = self.inner.properties.as_mut() {
properties.shading = 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>) {
if val.is_none() && self.inner.properties.is_none() {
return;
}
self.ensure_rpr().strike = val;
}
pub fn double_strike(mut self, val: bool) -> Self {
self.set_double_strike(val);
self
}
pub fn set_double_strike(&mut self, val: bool) {
self.ensure_rpr().dstrike = Some(val);
}
pub fn all_caps(mut self, val: bool) -> Self {
self.set_all_caps(val);
self
}
pub fn set_all_caps(&mut self, val: bool) {
self.ensure_rpr().caps = Some(val);
}
pub fn small_caps(mut self, val: bool) -> Self {
self.set_small_caps(val);
self
}
pub fn set_small_caps(&mut self, val: bool) {
self.ensure_rpr().small_caps = Some(val);
}
pub fn superscript(mut self) -> Self {
self.set_superscript();
self
}
pub fn set_superscript(&mut self) {
self.ensure_rpr().vert_align = Some("superscript".to_string());
}
pub fn subscript(mut self) -> Self {
self.set_subscript();
self
}
pub fn set_subscript(&mut self) {
self.ensure_rpr().vert_align = Some("subscript".to_string());
}
pub fn character_spacing(mut self, spacing: Length) -> Self {
self.set_character_spacing(spacing);
self
}
pub fn set_character_spacing(&mut self, spacing: Length) {
self.ensure_rpr().spacing = Some(spacing.as_twips());
}
pub fn width_scale(mut self, percent: u32) -> Self {
self.set_width_scale(percent);
self
}
pub fn set_width_scale(&mut self, percent: u32) {
self.ensure_rpr().width_scale = Some(percent);
}
pub fn position(mut self, half_points: i32) -> Self {
self.set_position(half_points);
self
}
pub fn set_position(&mut self, half_points: i32) {
self.ensure_rpr().position = Some(half_points);
}
pub fn hidden(mut self, val: bool) -> Self {
self.set_hidden(val);
self
}
pub fn set_hidden(&mut self, val: bool) {
self.ensure_rpr().vanish = Some(val);
}
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_rpr().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_rpr().style_id = style_id.map(str::to_owned);
}
pub fn add_symbol_char(&mut self, font: &str, char_code: u16) {
self.inner.append_content(RunContent::Symbol {
font: font.to_owned(),
char_code,
});
}
pub fn add_special_character(&mut self, character: SpecialCharacter) {
self.inner
.append_content(RunContent::SpecialCharacter(character));
}
pub fn set_rtl_value(&mut self, val: Option<bool>) {
self.set_toggle(val, |rpr| &mut rpr.rtl);
}
pub fn set_complex_script_value(&mut self, val: Option<bool>) {
self.set_toggle(val, |rpr| &mut rpr.complex_script);
}
pub fn set_bold_cs_value(&mut self, val: Option<bool>) {
self.set_toggle(val, |rpr| &mut rpr.bold_cs);
}
pub fn set_italic_cs_value(&mut self, val: Option<bool>) {
self.set_toggle(val, |rpr| &mut rpr.italic_cs);
}
pub fn set_size_cs_value(&mut self, pt: Option<f64>) {
if pt.is_none() && self.inner.properties.is_none() {
return;
}
self.ensure_rpr().sz_cs = pt.map(HalfPoint::from_pt);
}
pub fn set_language_east_asia_value(&mut self, language: Option<&str>) {
if language.is_none() && self.inner.properties.is_none() {
return;
}
self.ensure_rpr().language_east_asia = language.map(str::to_owned);
}
pub fn set_language_bidi_value(&mut self, language: Option<&str>) {
if language.is_none() && self.inner.properties.is_none() {
return;
}
self.ensure_rpr().language_bidi = language.map(str::to_owned);
}
pub fn set_outline_value(&mut self, val: Option<bool>) {
self.set_toggle(val, |rpr| &mut rpr.outline);
}
pub fn set_shadow_value(&mut self, val: Option<bool>) {
self.set_toggle(val, |rpr| &mut rpr.shadow);
}
pub fn set_emboss_value(&mut self, val: Option<bool>) {
self.set_toggle(val, |rpr| &mut rpr.emboss);
}
pub fn set_imprint_value(&mut self, val: Option<bool>) {
self.set_toggle(val, |rpr| &mut rpr.imprint);
}
pub fn set_no_proof_value(&mut self, val: Option<bool>) {
self.set_toggle(val, |rpr| &mut rpr.no_proof);
}
pub fn set_snap_to_grid_value(&mut self, val: Option<bool>) {
self.set_toggle(val, |rpr| &mut rpr.snap_to_grid);
}
pub fn set_web_hidden_value(&mut self, val: Option<bool>) {
self.set_toggle(val, |rpr| &mut rpr.web_hidden);
}
pub fn set_spec_vanish_value(&mut self, val: Option<bool>) {
self.set_toggle(val, |rpr| &mut rpr.spec_vanish);
}
pub fn set_office_math_value(&mut self, val: Option<bool>) {
self.set_toggle(val, |rpr| &mut rpr.office_math);
}
pub fn set_kern_value(&mut self, pt: Option<f64>) {
if pt.is_none() && self.inner.properties.is_none() {
return;
}
self.ensure_rpr().kern = pt.map(HalfPoint::from_pt);
}
pub fn set_effect_value(&mut self, effect: Option<ST_TextEffect>) {
if effect.is_none() && self.inner.properties.is_none() {
return;
}
self.ensure_rpr().effect = effect;
}
pub fn set_emphasis_mark_value(&mut self, mark: Option<ST_Em>) {
if mark.is_none() && self.inner.properties.is_none() {
return;
}
self.ensure_rpr().emphasis_mark = mark;
}
pub fn set_character_border_value(&mut self, border: Option<CT_BorderEdge>) {
if border.is_none() && self.inner.properties.is_none() {
return;
}
self.ensure_rpr().border = border.map(Box::new);
}
pub fn set_fit_text_value(&mut self, fit_text: Option<CT_FitText>) {
if fit_text.is_none() && self.inner.properties.is_none() {
return;
}
self.ensure_rpr().fit_text = fit_text.map(Box::new);
}
pub fn set_east_asian_layout_value(&mut self, layout: Option<CT_EastAsianLayout>) {
if layout.is_none() && self.inner.properties.is_none() {
return;
}
self.ensure_rpr().east_asian_layout = layout.map(Box::new);
}
fn set_toggle(
&mut self,
val: Option<bool>,
slot: impl FnOnce(&mut CT_RPr) -> &mut Option<bool>,
) {
if val.is_none() && self.inner.properties.is_none() {
return;
}
*slot(self.ensure_rpr()) = val;
}
fn ensure_rpr(&mut self) -> &mut CT_RPr {
self.inner.ensure_properties()
}
}
pub struct RunRef<'a> {
pub(crate) inner: &'a CT_R,
}
impl<'a> RunRef<'a> {
pub fn text(&self) -> String {
self.inner.text()
}
pub fn items(&self) -> impl Iterator<Item = RunItemRef<'_>> {
let property_boundary = usize::from(self.inner.properties.is_some());
let mut items = Vec::with_capacity(self.inner.content.len() + self.inner.extra_xml.len());
let ordered_raw = self.inner.extra_xml_positions.len() == self.inner.extra_xml.len();
if ordered_raw && property_boundary > 0 {
items.extend(
self.inner
.extra_xml_positions
.iter()
.zip(&self.inner.extra_xml)
.filter(|(position, _)| {
!CT_R::raw_child_is_root_attributes(**position)
&& CT_R::raw_child_position(**position) == 0
})
.map(|(position, raw)| classify_raw_run_item(raw, Some(*position))),
);
}
for index in 0..=self.inner.content.len() {
let boundary = property_boundary + index;
if ordered_raw {
items.extend(
self.inner
.extra_xml_positions
.iter()
.zip(&self.inner.extra_xml)
.filter(|(position, _)| {
!CT_R::raw_child_is_root_attributes(**position)
&& CT_R::raw_child_position(**position) == boundary
})
.map(|(position, raw)| classify_raw_run_item(raw, Some(*position))),
);
}
if let Some(content) = self.inner.content.get(index) {
items.push(match content {
RunContent::Text(text) => RunItemRef::Text(&text.text),
RunContent::DeletedText(text) => RunItemRef::DeletedText(&text.text),
RunContent::Tab => RunItemRef::Tab,
RunContent::Break(kind) => RunItemRef::Break(match kind {
BreakType::Line => BreakKind::Line,
BreakType::Page => BreakKind::Page,
BreakType::Column => BreakKind::Column,
}),
RunContent::Drawing(drawing) => {
RunItemRef::Drawing(DrawingRef { inner: drawing })
}
RunContent::Field(field) => RunItemRef::Field(FieldRef { inner: field }),
RunContent::FootnoteRef { id, .. } => RunItemRef::FootnoteReference(*id),
RunContent::EndnoteRef { id, .. } => RunItemRef::EndnoteReference(*id),
RunContent::CommentReference { id, .. } => RunItemRef::CommentReference(*id),
RunContent::Symbol { font, char_code } => RunItemRef::Symbol {
font: font.as_str(),
char_code: *char_code,
},
RunContent::SpecialCharacter(character) => {
RunItemRef::SpecialCharacter(*character)
}
});
}
}
if !ordered_raw {
items.extend(
self.inner
.extra_xml
.iter()
.filter(|raw| !rdocx_oxml::text::is_root_attribute_record(raw))
.map(|raw| classify_raw_run_item(raw, None)),
);
}
items.into_iter()
}
pub fn footnote_id(&self) -> Option<i32> {
use rdocx_oxml::text::RunContent;
self.inner.content.iter().find_map(|c| match c {
RunContent::FootnoteRef { id, .. } => Some(*id),
_ => None,
})
}
pub fn is_bold(&self) -> bool {
self.bold_value().unwrap_or(false)
}
pub fn bold_value(&self) -> Option<bool> {
self.inner.properties.as_ref().and_then(|rpr| rpr.bold)
}
pub fn is_italic(&self) -> bool {
self.italic_value().unwrap_or(false)
}
pub fn italic_value(&self) -> Option<bool> {
self.inner.properties.as_ref().and_then(|rpr| rpr.italic)
}
pub fn is_strike(&self) -> bool {
self.strike_value().unwrap_or(false)
}
pub fn strike_value(&self) -> Option<bool> {
self.inner.properties.as_ref().and_then(|rpr| rpr.strike)
}
pub fn is_underline(&self) -> bool {
self.underline_code_value().is_some_and(|code| code != 0)
}
pub fn underline_code_value(&self) -> Option<i32> {
self.inner
.properties
.as_ref()
.and_then(|rpr| rpr.underline)
.map(underline_to_code)
}
pub fn size(&self) -> Option<f64> {
self.inner
.properties
.as_ref()
.and_then(|rpr| rpr.sz)
.map(|hp| hp.to_pt())
}
pub fn font_name(&self) -> Option<&str> {
self.inner
.properties
.as_ref()
.and_then(|rpr| rpr.font_ascii.as_deref())
}
pub fn language(&self) -> Option<&str> {
self.inner
.properties
.as_ref()
.and_then(|rpr| rpr.language.as_deref())
}
pub fn color(&self) -> Option<&str> {
self.inner
.properties
.as_ref()
.and_then(|rpr| rpr.color.as_deref())
}
pub fn character_spacing(&self) -> Option<Twips> {
self.inner.properties.as_ref().and_then(|rpr| rpr.spacing)
}
pub fn highlight(&self) -> Option<String> {
let rpr = self.inner.properties.as_ref()?;
if let Some(h) = rpr.highlight {
return Some(h.to_str().to_string());
}
rpr.shading.as_ref().and_then(|sh| sh.fill.clone())
}
pub fn highlight_color(&self) -> Option<&'static str> {
self.inner
.properties
.as_ref()
.and_then(|rpr| rpr.highlight)
.map(ST_HighlightColor::to_str)
}
pub fn shading_fill(&self) -> Option<&str> {
self.inner
.properties
.as_ref()
.and_then(|rpr| rpr.shading.as_ref())
.and_then(|shading| shading.fill.as_deref())
}
pub fn inline_image(&self) -> Option<(&str, Option<&str>)> {
use rdocx_oxml::text::RunContent;
for c in &self.inner.content {
if let RunContent::Drawing(d) = c
&& let Some(inline) = &d.inline
{
return Some((inline.embed_id.as_str(), inline.description.as_deref()));
}
}
None
}
pub fn position(&self) -> Option<i32> {
self.inner.properties.as_ref().and_then(|rpr| rpr.position)
}
pub fn vert_align(&self) -> Option<&str> {
self.inner
.properties
.as_ref()
.and_then(|rpr| rpr.vert_align.as_deref())
}
pub fn style_id(&self) -> Option<&str> {
self.inner
.properties
.as_ref()
.and_then(|rpr| rpr.style_id.as_deref())
}
pub fn slot_font(&self, slot: RunFontSlot) -> Option<&str> {
self.inner
.properties
.as_ref()
.and_then(|rpr| slot.explicit(rpr))
}
pub fn slot_theme_font(&self, slot: RunFontSlot) -> Option<&str> {
self.inner
.properties
.as_ref()
.and_then(|rpr| slot.theme(rpr))
}
pub fn font_hint(&self) -> Option<&str> {
self.property(|rpr| rpr.font_hint.as_deref())
}
pub fn color_theme(&self) -> Option<&str> {
self.property(|rpr| rpr.color_theme.as_deref())
}
pub fn color_theme_tint(&self) -> Option<u8> {
self.property(|rpr| rpr.color_theme_tint)
}
pub fn color_theme_shade(&self) -> Option<u8> {
self.property(|rpr| rpr.color_theme_shade)
}
pub fn rtl_value(&self) -> Option<bool> {
self.property(|rpr| rpr.rtl)
}
pub fn complex_script_value(&self) -> Option<bool> {
self.property(|rpr| rpr.complex_script)
}
pub fn bold_cs_value(&self) -> Option<bool> {
self.property(|rpr| rpr.bold_cs)
}
pub fn italic_cs_value(&self) -> Option<bool> {
self.property(|rpr| rpr.italic_cs)
}
pub fn size_cs(&self) -> Option<f64> {
self.property(|rpr| rpr.sz_cs).map(|size| size.to_pt())
}
pub fn language_east_asia(&self) -> Option<&str> {
self.property(|rpr| rpr.language_east_asia.as_deref())
}
pub fn language_bidi(&self) -> Option<&str> {
self.property(|rpr| rpr.language_bidi.as_deref())
}
pub fn outline_value(&self) -> Option<bool> {
self.property(|rpr| rpr.outline)
}
pub fn shadow_value(&self) -> Option<bool> {
self.property(|rpr| rpr.shadow)
}
pub fn emboss_value(&self) -> Option<bool> {
self.property(|rpr| rpr.emboss)
}
pub fn imprint_value(&self) -> Option<bool> {
self.property(|rpr| rpr.imprint)
}
pub fn no_proof_value(&self) -> Option<bool> {
self.property(|rpr| rpr.no_proof)
}
pub fn snap_to_grid_value(&self) -> Option<bool> {
self.property(|rpr| rpr.snap_to_grid)
}
pub fn web_hidden_value(&self) -> Option<bool> {
self.property(|rpr| rpr.web_hidden)
}
pub fn spec_vanish_value(&self) -> Option<bool> {
self.property(|rpr| rpr.spec_vanish)
}
pub fn office_math_value(&self) -> Option<bool> {
self.property(|rpr| rpr.office_math)
}
pub fn kern(&self) -> Option<f64> {
self.property(|rpr| rpr.kern).map(|kern| kern.to_pt())
}
pub fn effect(&self) -> Option<&ST_TextEffect> {
self.property(|rpr| rpr.effect.as_ref())
}
pub fn emphasis_mark(&self) -> Option<&ST_Em> {
self.property(|rpr| rpr.emphasis_mark.as_ref())
}
pub fn character_border(&self) -> Option<&CT_BorderEdge> {
self.property(|rpr| rpr.border.as_deref())
}
pub fn fit_text(&self) -> Option<&CT_FitText> {
self.property(|rpr| rpr.fit_text.as_deref())
}
pub fn east_asian_layout(&self) -> Option<&CT_EastAsianLayout> {
self.property(|rpr| rpr.east_asian_layout.as_deref())
}
fn property<T>(&self, read: impl FnOnce(&'a CT_RPr) -> Option<T>) -> Option<T> {
read(self.inner.properties.as_ref()?)
}
}
#[cfg(test)]
mod tests {
use super::*;
use rdocx_oxml::CT_Document;
use rdocx_oxml::document::BodyContent;
#[test]
fn ct_r_public_struct_literal_keeps_its_existing_shape() {
let run = CT_R {
properties: None,
content: Vec::new(),
extra_xml: Vec::new(),
extra_xml_positions: Vec::new(),
alt_drawings: Vec::new(),
};
assert!(run.content.is_empty());
}
fn run_with_raw(raw: &[u8]) -> CT_R {
let raw = std::str::from_utf8(raw).unwrap();
run_with_inner_xml(raw)
}
fn run_with_inner_xml(inner_xml: &str) -> CT_R {
let xml = format!(
r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"><w:body><w:p><w:r>{inner_xml}</w:r></w:p></w:body></w:document>"#
);
let mut document = CT_Document::from_xml(xml.as_bytes()).unwrap();
let BodyContent::Paragraph(mut paragraph) = document.body.content.remove(0) else {
panic!("expected paragraph");
};
paragraph.runs.remove(0)
}
fn run_from_paragraph_inner_xml(inner_xml: &str) -> CT_R {
let xml = format!(
r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"><w:body><w:p>{inner_xml}</w:p></w:body></w:document>"#
);
let mut document = CT_Document::from_xml(xml.as_bytes()).unwrap();
let BodyContent::Paragraph(mut paragraph) = document.body.content.remove(0) else {
panic!("expected paragraph");
};
paragraph.runs.remove(0)
}
#[test]
fn drawing_reader_classifies_image_relationships_and_shapes() {
let linked_run = run_with_inner_xml(concat!(
r#"<w:drawing xmlns:wp="http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing" "#,
r#"xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" "#,
r#"xmlns:pic="http://schemas.openxmlformats.org/drawingml/2006/picture" "#,
r#"xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">"#,
r#"<wp:inline><wp:extent cx="10" cy="20"/><wp:docPr id="1" name="Linked"/><a:graphic><a:graphicData uri="http://schemas.openxmlformats.org/drawingml/2006/picture"><pic:pic><pic:blipFill><a:blip r:link="rId7"/></pic:blipFill></pic:pic></a:graphicData></a:graphic></wp:inline></w:drawing>"#,
));
let linked_run = RunRef { inner: &linked_run };
let linked_items = linked_run.items().collect::<Vec<_>>();
let [RunItemRef::Drawing(linked)] = linked_items.as_slice() else {
panic!("expected one linked drawing");
};
assert_eq!(linked.kind(), DrawingKind::Image);
assert_eq!(linked.relationship_id(), Some("rId7"));
assert_eq!(
linked.relationship_kind(),
Some(DrawingRelationshipKind::Linked)
);
let embedded = CT_Drawing::inline(rdocx_oxml::drawing::CT_Inline::new("rId8", 10, 20));
let embedded = DrawingRef { inner: &embedded };
assert_eq!(embedded.kind(), DrawingKind::Image);
assert_eq!(
embedded.relationship_kind(),
Some(DrawingRelationshipKind::Embedded)
);
let mut anchor = rdocx_oxml::drawing::CT_Anchor::background("", 10, 20);
anchor.shape = Some(rdocx_oxml::drawing::CT_Shape::default());
let shape = CT_Drawing::anchor(anchor);
assert_eq!(DrawingRef { inner: &shape }.kind(), DrawingKind::Shape);
let filled_shape = run_with_inner_xml(concat!(
r#"<w:drawing xmlns:wp="http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing" "#,
r#"xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" "#,
r#"xmlns:wps="http://schemas.microsoft.com/office/word/2010/wordprocessingShape" "#,
r#"xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">"#,
r#"<wp:anchor><wp:docPr id="1" name="Shape"/><wps:wsp><wps:spPr><a:blipFill><a:blip r:embed="rIdFill"/></a:blipFill></wps:spPr></wps:wsp></wp:anchor></w:drawing>"#,
));
let filled_shape = DrawingRef {
inner: filled_shape
.content
.iter()
.find_map(|content| match content {
RunContent::Drawing(drawing) => Some(drawing),
_ => None,
})
.expect("filled shape drawing"),
};
assert_eq!(filled_shape.kind(), DrawingKind::Shape);
assert_eq!(filled_shape.relationship_id(), None);
let chart = CT_Drawing::inline(rdocx_oxml::drawing::CT_Inline::new_chart("rId9", 10, 20));
let chart = DrawingRef { inner: &chart };
assert_eq!(chart.kind(), DrawingKind::Other);
assert_eq!(chart.relationship_kind(), None);
}
#[test]
fn legacy_horizontal_rule_classification_is_namespace_aware() {
let cases = [
br#"<w:pict xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office"><v:rect o:hr="t"/></w:pict>"#.as_slice(),
br#"<word:pict xmlns:word="http://schemas.openxmlformats.org/wordprocessingml/2006/main" xmlns:shape="urn:schemas-microsoft-com:vml" xmlns:office="urn:schemas-microsoft-com:office:office"><shape:rect office:hr="true"></shape:rect></word:pict>"#.as_slice(),
br#"<pict xmlns="http://schemas.openxmlformats.org/wordprocessingml/2006/main" xmlns:o="urn:schemas-microsoft-com:office:office"><rect xmlns="urn:schemas-microsoft-com:vml" o:hr="t"/></pict>"#.as_slice(),
br#"<w:pict xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" xmlns:v="urn:foreign" xmlns:o="urn:schemas-microsoft-com:office:office"><v:rect xmlns:v="urn:schemas-microsoft-com:vml" o:hr="true"/></w:pict>"#.as_slice(),
];
for raw in cases {
let run = run_with_raw(raw);
let run = RunRef { inner: &run };
let item = run.items().next().unwrap();
let RunItemRef::LegacyHorizontalRule(rule) = item else {
panic!("namespace-qualified legacy rule was not classified");
};
assert_eq!(rule.raw_xml(), raw);
}
}
#[test]
fn ambiguous_or_foreign_vml_stays_unsupported() {
let cases = [
br#"<w:pict xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office"><v:rect o:hr="1"/></w:pict>"#.as_slice(),
br#"<w:pict xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office"><v:rect o:hr="false"/></w:pict>"#.as_slice(),
br#"<w:pict xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" xmlns:v="urn:schemas-microsoft-com:vml"><v:rect/></w:pict>"#.as_slice(),
br#"<x:pict xmlns:x="urn:foreign" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office"><v:rect o:hr="t"/></x:pict>"#.as_slice(),
br#"<w:pict xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" xmlns:x="urn:foreign" xmlns:o="urn:schemas-microsoft-com:office:office"><x:rect o:hr="t"/></w:pict>"#.as_slice(),
br#"<w:pict xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:x="urn:foreign"><v:rect x:hr="t"/></w:pict>"#.as_slice(),
br#"<w:pict xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office"><v:rect xmlns:v="urn:foreign" o:hr="t"/></w:pict>"#.as_slice(),
br#"<w:pict xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office"><v:rect o:hr="t"/><v:rect o:hr="t"/></w:pict>"#.as_slice(),
br#"<w:pict xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office"><v:rect o:hr="t">visible</v:rect></w:pict>"#.as_slice(),
br#"<w:pict xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office"><!-- ambiguous --><v:rect o:hr="t"/></w:pict>"#.as_slice(),
];
for raw in cases {
let run = run_with_raw(raw);
assert!(matches!(
RunRef { inner: &run }.items().next().unwrap(),
RunItemRef::UnsupportedXml(bytes) if bytes == raw
));
}
let malformed = br#"<w:pict xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office"><v:rect o:hr="t"></w:pict>"#;
let run = CT_R {
properties: None,
content: Vec::new(),
extra_xml: vec![malformed.to_vec()],
extra_xml_positions: vec![0],
alt_drawings: Vec::new(),
};
assert!(matches!(
RunRef { inner: &run }.items().next().unwrap(),
RunItemRef::UnsupportedXml(bytes) if bytes == malformed
));
}
#[test]
fn legacy_horizontal_rule_keeps_exact_raw_xml_and_item_order() {
let raw = br#"<w:pict xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office"><v:rect o:hr="t"/></w:pict>"#;
let run = run_with_inner_xml(&format!(
"<w:t>before</w:t>{}<w:t>after</w:t>",
std::str::from_utf8(raw).unwrap()
));
let run = RunRef { inner: &run };
let items = run.items().collect::<Vec<_>>();
assert!(matches!(items[0], RunItemRef::Text("before")));
let RunItemRef::LegacyHorizontalRule(rule) = items[1] else {
panic!("legacy rule did not retain its ordered item slot");
};
assert_eq!(rule.raw_xml(), raw);
assert!(matches!(items[2], RunItemRef::Text("after")));
}
#[test]
fn unordered_legacy_raw_children_follow_typed_run_content() {
let run = CT_R {
properties: None,
content: vec![RunContent::Text(CT_Text::new("typed"))],
extra_xml: vec![b"<x:raw/>".to_vec()],
extra_xml_positions: Vec::new(),
alt_drawings: Vec::new(),
};
let run = RunRef { inner: &run };
let items = run.items().collect::<Vec<_>>();
assert!(matches!(items[0], RunItemRef::Text("typed")));
assert!(matches!(items[1], RunItemRef::UnsupportedXml(b"<x:raw/>")));
}
#[test]
fn field_reader_reports_unmodeled_semantic_attributes() {
let supported = run_from_paragraph_inner_xml(
r#"<w:fldSimple w:instr="PAGE" w:dirty="false"><w:r><w:t>1</w:t></w:r></w:fldSimple>"#,
);
let unsupported = run_from_paragraph_inner_xml(
r#"<w:fldSimple w:instr="PAGE" w:fldLock="true"><w:r><w:t>1</w:t></w:r></w:fldSimple>"#,
);
let supported_run = RunRef { inner: &supported };
let RunItemRef::Field(supported) = supported_run.items().next().expect("supported field")
else {
panic!("expected field");
};
let unsupported_run = RunRef {
inner: &unsupported,
};
let RunItemRef::Field(unsupported) =
unsupported_run.items().next().expect("unsupported field")
else {
panic!("expected field");
};
assert!(!supported.has_unmodeled_semantic_attributes());
assert!(unsupported.has_unmodeled_semantic_attributes());
}
#[test]
fn field_reader_exposes_complex_cached_display_segments() {
let simple = run_from_paragraph_inner_xml(
r#"<w:fldSimple w:instr="PAGE"><w:r><w:t>1</w:t></w:r></w:fldSimple>"#,
);
let complex = run_from_paragraph_inner_xml(
r#"<w:r><w:fldChar w:fldCharType="begin"/></w:r><w:r><w:instrText>PAGE</w:instrText></w:r><w:r><w:fldChar w:fldCharType="separate"/></w:r><w:r><w:rPr><w:b/></w:rPr><w:t>1</w:t></w:r><w:r><w:rPr><w:i/></w:rPr><w:t>2</w:t></w:r><w:r><w:fldChar w:fldCharType="end"/></w:r>"#,
);
let simple_run = RunRef { inner: &simple };
let RunItemRef::Field(simple) = simple_run.items().next().unwrap() else {
panic!("expected simple field");
};
let complex_run = RunRef { inner: &complex };
let RunItemRef::Field(complex) = complex_run.items().next().unwrap() else {
panic!("expected complex field");
};
assert_eq!(simple.kind(), FieldKind::Simple);
assert_eq!(complex.kind(), FieldKind::Complex);
let segments = complex.cached_display_segments();
assert_eq!(segments.len(), 2);
assert_eq!(segments[0].text(), "1");
assert_eq!(segments[0].properties().unwrap().bold, Some(true));
assert_eq!(segments[1].text(), "2");
assert_eq!(segments[1].properties().unwrap().italic, Some(true));
}
#[test]
fn run_language_authoring_sets_and_clears_the_complete_word_language_value() {
let mut inner = CT_R::new("hyphenation");
{
let mut run = Run { inner: &mut inner };
run.set_language_value(Some("en-US"));
}
assert_eq!(
inner.properties.as_ref().unwrap().language.as_deref(),
Some("en-US")
);
{
let mut run = Run { inner: &mut inner };
run.set_language_value(None);
}
assert_eq!(inner.properties.as_ref().unwrap().language, None);
}
#[test]
fn named_highlight_and_shading_are_independent_direct_properties() {
let mut inner = CT_R::new("marked");
{
let mut run = Run { inner: &mut inner };
assert!(run.set_highlight_value(Some("darkBlue")));
run.set_shading_value(Some("FFFF00"));
}
let run = RunRef { inner: &inner };
assert_eq!(run.highlight_color(), Some("darkBlue"));
assert_eq!(run.shading_fill(), Some("FFFF00"));
{
let mut run = Run { inner: &mut inner };
assert!(!run.set_highlight_value(Some("FFFF00")));
}
let run = RunRef { inner: &inner };
assert_eq!(run.highlight_color(), Some("darkBlue"));
assert_eq!(run.shading_fill(), Some("FFFF00"));
{
let mut run = Run { inner: &mut inner };
assert!(run.set_highlight_value(None));
run.set_shading_value(None);
}
let run = RunRef { inner: &inner };
assert_eq!(run.highlight_color(), None);
assert_eq!(run.shading_fill(), None);
}
#[test]
fn style_highlight_and_shading_preserve_ordered_run_content() {
let mut inner = CT_R {
properties: None,
content: vec![
RunContent::Text(CT_Text::new("before")),
RunContent::Tab,
RunContent::Break(BreakType::Page),
RunContent::Field(Field::new("PAGE", "1")),
RunContent::Drawing(CT_Drawing::inline(CT_Inline::new("rId1", 1, 1))),
RunContent::Text(CT_Text::new("after")),
],
extra_xml: vec![b"<w:sym w:font=\"Wingdings\" w:char=\"F0B7\"/>".to_vec()],
extra_xml_positions: vec![5],
alt_drawings: Vec::new(),
};
let expected_content = inner.content.clone();
let expected_raw = inner.extra_xml.clone();
{
let mut run = Run { inner: &mut inner };
run.set_style_value(Some("Strong"));
assert!(run.set_highlight_value(Some("yellow")));
run.set_shading_value(Some("FFFF00"));
}
assert_eq!(inner.content, expected_content);
assert_eq!(inner.extra_xml, expected_raw);
assert_eq!(inner.extra_xml_positions, vec![6]);
}
}