1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
use serde::{Deserialize, Serialize, Serializer};
use std::io::Write;
use crate::documents::BuildXML;
use crate::types::ThemeColor;
use crate::xml_builder::*;
/// A `<w:color>` color property.
///
/// `val` is an `RRGGBB` hex string (or `auto`). The optional theme fields let
/// the color reference a document-theme color instead of being locked to the
/// hex value: when present they emit `w:themeColor` / `w:themeShade` /
/// `w:themeTint`, and Word resolves the color from the active theme while
/// `val` stays as a fallback for renderers that ignore themes.
///
/// ```
/// use docx_rs::{Color, ThemeColor};
///
/// // Plain hex color (unchanged behavior).
/// let c = Color::new("2E74B5");
///
/// // Theme-aware color: "use accent1, shaded to BF", with 2E74B5 as fallback.
/// let c = Color::new("2E74B5")
/// .theme_color(ThemeColor::Accent1)
/// .theme_shade("BF");
/// ```
#[derive(Deserialize, Debug, Clone, PartialEq)]
pub struct Color {
val: String,
#[serde(default)]
theme_color: Option<String>,
#[serde(default)]
theme_shade: Option<String>,
#[serde(default)]
theme_tint: Option<String>,
}
impl Color {
/// Creates a color from an `RRGGBB` hex string (or `auto`).
pub fn new(val: impl Into<String>) -> Color {
Color {
val: val.into(),
theme_color: None,
theme_shade: None,
theme_tint: None,
}
}
/// Sets the theme color reference (`w:themeColor`).
pub fn theme_color(mut self, theme_color: ThemeColor) -> Color {
// Stored as the serialized ST_ThemeColor token (e.g. "accent1") so the
// builder and reader share a single string representation; the typed
// `ThemeColor` enum is the ergonomic boundary for callers.
self.theme_color = Some(theme_color.to_string());
self
}
/// Sets the theme shade modifier (`w:themeShade`), a hex byte such as `"BF"`.
pub fn theme_shade(mut self, theme_shade: impl Into<String>) -> Color {
self.theme_shade = Some(theme_shade.into());
self
}
/// Sets the theme tint modifier (`w:themeTint`), a hex byte such as `"99"`.
pub fn theme_tint(mut self, theme_tint: impl Into<String>) -> Color {
self.theme_tint = Some(theme_tint.into());
self
}
}
impl BuildXML for Color {
fn build_to<W: Write>(
&self,
stream: crate::xml::writer::EventWriter<W>,
) -> crate::xml::writer::Result<crate::xml::writer::EventWriter<W>> {
XMLBuilder::from(stream)
.color_with_theme(
&self.val,
self.theme_color.as_deref(),
self.theme_shade.as_deref(),
self.theme_tint.as_deref(),
)?
.into_inner()
}
}
impl Serialize for Color {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
// Preserve the existing JSON shape (a bare hex string). Theme metadata
// is not represented in JSON to avoid a breaking format change; it is
// fully round-tripped through the docx/XML path, which is what consumers
// generating documents rely on.
serializer.serialize_str(&self.val)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(test)]
use pretty_assertions::assert_eq;
use std::str;
#[test]
fn test_build() {
let c = Color::new("FFFFFF");
let b = c.build();
assert_eq!(str::from_utf8(&b).unwrap(), r#"<w:color w:val="FFFFFF" />"#);
}
#[test]
fn test_build_with_theme_color() {
let c = Color::new("2E74B5")
.theme_color(ThemeColor::Accent1)
.theme_shade("BF");
let b = c.build();
assert_eq!(
str::from_utf8(&b).unwrap(),
r#"<w:color w:val="2E74B5" w:themeColor="accent1" w:themeShade="BF" />"#
);
}
#[test]
fn test_build_with_theme_tint() {
let c = Color::new("2E74B5")
.theme_color(ThemeColor::Accent2)
.theme_tint("99");
let b = c.build();
assert_eq!(
str::from_utf8(&b).unwrap(),
r#"<w:color w:val="2E74B5" w:themeColor="accent2" w:themeTint="99" />"#
);
}
}