1#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
14pub struct Length(pub f32);
15
16impl Length {
17 pub const fn points(self) -> f32 {
19 self.0
20 }
21
22 pub fn parse(text: &str) -> Option<Self> {
28 let text = text.trim();
29 let split = text.len().checked_sub(2)?;
30 if !text.is_char_boundary(split) {
31 return None;
32 }
33 let (number, unit) = text.split_at(split);
34 let value: f32 = number.trim().parse().ok()?;
35 let points = match unit {
36 "pt" => value,
37 "in" => value * 72.0,
38 "cm" => value * 72.0 / 2.54,
39 "mm" => value * 72.0 / 25.4,
40 "pc" => value * 12.0,
42 "px" => value * 0.75,
45 _ => return None,
46 };
47 Some(Self(points))
48 }
49}
50
51#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
56pub struct Percent(pub f32);
57
58impl Percent {
59 pub const fn fraction(self) -> f32 {
61 self.0 / 100.0
62 }
63
64 pub fn parse(text: &str) -> Option<Self> {
66 let value: f32 = text.trim().strip_suffix('%')?.trim().parse().ok()?;
67 Some(Self(value))
68 }
69}
70
71#[derive(Debug, Clone, Copy, PartialEq)]
74pub enum Measure {
75 Absolute(Length),
77 Relative(Percent),
79}
80
81impl Measure {
82 pub fn parse(text: &str) -> Option<Self> {
84 if let Some(percent) = Percent::parse(text) {
85 return Some(Self::Relative(percent));
86 }
87 Length::parse(text).map(Self::Absolute)
88 }
89
90 pub fn resolve(self, reference: f32) -> f32 {
92 match self {
93 Self::Absolute(length) => length.points(),
94 Self::Relative(percent) => reference * percent.fraction(),
95 }
96 }
97}
98
99#[derive(Debug, Clone, Copy, PartialEq, Eq)]
105pub struct Color {
106 pub r: u8,
108 pub g: u8,
110 pub b: u8,
112}
113
114impl Color {
115 pub fn parse(text: &str) -> Option<Self> {
120 let hex = text.trim().strip_prefix('#')?;
121 if hex.len() != 6 {
122 return None;
123 }
124 Some(Self {
125 r: u8::from_str_radix(&hex[0..2], 16).ok()?,
126 g: u8::from_str_radix(&hex[2..4], 16).ok()?,
127 b: u8::from_str_radix(&hex[4..6], 16).ok()?,
128 })
129 }
130}
131
132pub(crate) fn boolean(text: &str) -> Option<bool> {
134 match text.trim() {
135 "true" => Some(true),
136 "false" => Some(false),
137 _ => None,
138 }
139}
140
141#[cfg(test)]
142mod tests {
143 use super::*;
144
145 #[test]
146 fn lengths_convert_to_points() {
147 assert_eq!(Length::parse("12pt"), Some(Length(12.0)));
148 assert_eq!(Length::parse("1in"), Some(Length(72.0)));
149 assert_eq!(Length::parse("1pc"), Some(Length(12.0)));
150 let cm = Length::parse("2.54cm").unwrap();
151 assert!((cm.points() - 72.0).abs() < 0.01);
152 let mm = Length::parse("25.4mm").unwrap();
153 assert!((mm.points() - 72.0).abs() < 0.01);
154 }
155
156 #[test]
157 fn a_length_needs_a_unit() {
158 assert_eq!(Length::parse("12"), None);
159 assert_eq!(Length::parse(""), None);
160 assert_eq!(Length::parse("pt"), None);
161 }
162
163 #[test]
164 fn a_multibyte_tail_is_not_split_through() {
165 assert_eq!(Length::parse("12µm"), None);
168 }
169
170 #[test]
171 fn colours_and_percentages() {
172 assert_eq!(
173 Color::parse("#ff8000"),
174 Some(Color {
175 r: 255,
176 g: 128,
177 b: 0
178 })
179 );
180 assert_eq!(Color::parse("transparent"), None);
181 assert_eq!(Color::parse("#abc"), None);
182 assert_eq!(Percent::parse("150%").map(Percent::fraction), Some(1.5));
183 }
184}