1use pdfboss_write::{Color, Standard14};
6
7#[derive(Clone, Copy, Debug, PartialEq, Eq)]
9pub enum Element {
10 Body,
12 H1,
14 H2,
16 H3,
18 H4,
20 H5,
22 H6,
24 P,
26 Code,
28 Pre,
30 Blockquote,
32 Ul,
34 Ol,
36 Li,
38 Table,
40 Th,
42 Td,
44 A,
46 Del,
48 Hr,
50}
51
52impl Element {
53 pub const ALL: [Element; 20] = [
56 Element::Body,
57 Element::H1,
58 Element::H2,
59 Element::H3,
60 Element::H4,
61 Element::H5,
62 Element::H6,
63 Element::P,
64 Element::Code,
65 Element::Pre,
66 Element::Blockquote,
67 Element::Ul,
68 Element::Ol,
69 Element::Li,
70 Element::Table,
71 Element::Th,
72 Element::Td,
73 Element::A,
74 Element::Del,
75 Element::Hr,
76 ];
77
78 pub fn name(self) -> &'static str {
80 match self {
81 Element::Body => "body",
82 Element::H1 => "h1",
83 Element::H2 => "h2",
84 Element::H3 => "h3",
85 Element::H4 => "h4",
86 Element::H5 => "h5",
87 Element::H6 => "h6",
88 Element::P => "p",
89 Element::Code => "code",
90 Element::Pre => "pre",
91 Element::Blockquote => "blockquote",
92 Element::Ul => "ul",
93 Element::Ol => "ol",
94 Element::Li => "li",
95 Element::Table => "table",
96 Element::Th => "th",
97 Element::Td => "td",
98 Element::A => "a",
99 Element::Del => "del",
100 Element::Hr => "hr",
101 }
102 }
103
104 pub fn from_name(name: &str) -> Option<Element> {
107 Element::ALL
108 .into_iter()
109 .find(|element| element.name() == name)
110 }
111}
112
113#[derive(Clone, Copy, Debug, PartialEq, Eq)]
115pub enum FontFamily {
116 Helvetica,
118 Times,
120 Courier,
122}
123
124#[derive(Clone, Copy, Debug, PartialEq)]
126pub enum Align {
127 Left,
129 Center,
131 Right,
133}
134
135#[derive(Clone, Copy, Debug, PartialEq)]
137pub enum Decoration {
138 None,
140 Underline,
142 LineThrough,
144}
145
146#[derive(Clone, Copy, Debug, PartialEq)]
148pub enum FontSize {
149 Pt(f32),
151 Em(f32),
153}
154
155#[derive(Clone, Copy, Debug, Default, PartialEq)]
157pub struct Edges {
158 pub top: f32,
160 pub right: f32,
162 pub bottom: f32,
164 pub left: f32,
166}
167
168#[derive(Clone, Debug, Default, PartialEq)]
172pub struct Declared {
173 pub family: Option<FontFamily>,
175 pub size: Option<FontSize>,
177 pub bold: Option<bool>,
179 pub italic: Option<bool>,
181 pub color: Option<Color>,
183 pub background: Option<Color>,
185 pub margin: [Option<f32>; 4],
187 pub padding: [Option<f32>; 4],
189 pub line_height: Option<f32>,
191 pub align: Option<Align>,
193 pub decoration: Option<Decoration>,
195}
196
197impl Declared {
198 pub fn merge(&mut self, other: &Declared) {
202 if other.family.is_some() {
203 self.family = other.family;
204 }
205 if other.size.is_some() {
206 self.size = other.size;
207 }
208 if other.bold.is_some() {
209 self.bold = other.bold;
210 }
211 if other.italic.is_some() {
212 self.italic = other.italic;
213 }
214 if other.color.is_some() {
215 self.color = other.color;
216 }
217 if other.background.is_some() {
218 self.background = other.background;
219 }
220 for side in 0..4 {
221 if other.margin[side].is_some() {
222 self.margin[side] = other.margin[side];
223 }
224 if other.padding[side].is_some() {
225 self.padding[side] = other.padding[side];
226 }
227 }
228 if other.line_height.is_some() {
229 self.line_height = other.line_height;
230 }
231 if other.align.is_some() {
232 self.align = other.align;
233 }
234 if other.decoration.is_some() {
235 self.decoration = other.decoration;
236 }
237 }
238}
239
240#[derive(Clone, Copy, Debug, PartialEq)]
243pub struct TextStyle {
244 pub family: FontFamily,
246 pub size: f32,
248 pub bold: bool,
250 pub italic: bool,
252 pub color: Color,
254 pub line_height: f32,
256 pub align: Align,
258 pub decoration: Decoration,
260}
261
262impl TextStyle {
263 pub fn base() -> TextStyle {
266 TextStyle {
267 family: FontFamily::Helvetica,
268 size: 11.0,
269 bold: false,
270 italic: false,
271 color: Color::BLACK,
272 line_height: 1.4,
273 align: Align::Left,
274 decoration: Decoration::None,
275 }
276 }
277
278 pub fn apply(&self, d: &Declared) -> TextStyle {
282 TextStyle {
283 family: d.family.unwrap_or(self.family),
284 size: match d.size {
285 Some(FontSize::Pt(pt)) => pt,
286 Some(FontSize::Em(em)) => em * self.size,
287 None => self.size,
288 },
289 bold: d.bold.unwrap_or(self.bold),
290 italic: d.italic.unwrap_or(self.italic),
291 color: d.color.unwrap_or(self.color),
292 line_height: d.line_height.unwrap_or(self.line_height),
293 align: d.align.unwrap_or(self.align),
294 decoration: d.decoration.unwrap_or(self.decoration),
295 }
296 }
297
298 pub fn font(&self) -> Standard14 {
301 match (self.family, self.bold, self.italic) {
302 (FontFamily::Helvetica, false, false) => Standard14::Helvetica,
303 (FontFamily::Helvetica, true, false) => Standard14::HelveticaBold,
304 (FontFamily::Helvetica, false, true) => Standard14::HelveticaOblique,
305 (FontFamily::Helvetica, true, true) => Standard14::HelveticaBoldOblique,
306 (FontFamily::Times, false, false) => Standard14::TimesRoman,
307 (FontFamily::Times, true, false) => Standard14::TimesBold,
308 (FontFamily::Times, false, true) => Standard14::TimesItalic,
309 (FontFamily::Times, true, true) => Standard14::TimesBoldItalic,
310 (FontFamily::Courier, false, false) => Standard14::Courier,
311 (FontFamily::Courier, true, false) => Standard14::CourierBold,
312 (FontFamily::Courier, false, true) => Standard14::CourierOblique,
313 (FontFamily::Courier, true, true) => Standard14::CourierBoldOblique,
314 }
315 }
316}
317
318#[cfg(test)]
319mod tests {
320 use super::*;
321
322 #[test]
323 fn element_from_name_maps_every_selector() {
324 assert_eq!(Element::from_name("h1"), Some(Element::H1));
325 assert_eq!(Element::from_name("blockquote"), Some(Element::Blockquote));
326 assert_eq!(Element::from_name("div"), None);
327 for element in Element::ALL {
328 assert!(Element::from_name(element.name()).is_some());
329 }
330 }
331
332 #[test]
333 fn font_resolution_covers_all_twelve_faces() {
334 let style = TextStyle {
335 family: FontFamily::Times,
336 bold: true,
337 italic: true,
338 ..TextStyle::base()
339 };
340 assert_eq!(style.font(), Standard14::TimesBoldItalic);
341 let style = TextStyle {
342 family: FontFamily::Courier,
343 ..TextStyle::base()
344 };
345 assert_eq!(style.font(), Standard14::Courier);
346 }
347
348 #[test]
349 fn apply_overlays_only_declared_fields() {
350 let declared = Declared {
351 size: Some(FontSize::Em(2.0)),
352 bold: Some(true),
353 ..Declared::default()
354 };
355 let applied = TextStyle::base().apply(&declared);
356 assert_eq!(applied.size, 22.0);
357 assert!(applied.bold);
358 assert_eq!(applied.family, FontFamily::Helvetica);
359 }
360
361 #[test]
362 fn merge_keeps_earlier_fields_the_later_rule_leaves_unset() {
363 let mut first = Declared {
364 bold: Some(true),
365 ..Declared::default()
366 };
367 let second = Declared {
368 italic: Some(true),
369 ..Declared::default()
370 };
371 first.merge(&second);
372 assert_eq!(first.bold, Some(true));
373 assert_eq!(first.italic, Some(true));
374 }
375}