1use pdfboss_write::Color;
4
5use crate::parse::{parse_sheet, StyleError};
6use crate::style::{Declared, Edges, Element, TextStyle};
7
8pub(crate) const DEFAULT_CSS: &str = "\
9body { font-family: helvetica; font-size: 11pt; line-height: 1.4; color: #000; margin: 72pt; text-align: left; }\n\
10h1 { font-size: 2em; font-weight: bold; margin-top: 18pt; margin-bottom: 9pt; }\n\
11h2 { font-size: 1.6em; font-weight: bold; margin-top: 14pt; margin-bottom: 7pt; }\n\
12h3 { font-size: 1.3em; font-weight: bold; margin-top: 12pt; margin-bottom: 6pt; }\n\
13h4 { font-size: 1.15em; font-weight: bold; margin-top: 11pt; margin-bottom: 6pt; }\n\
14h5 { font-size: 1em; font-weight: bold; margin-top: 11pt; margin-bottom: 6pt; }\n\
15h6 { font-size: 0.9em; font-weight: bold; margin-top: 11pt; margin-bottom: 6pt; }\n\
16p { margin-bottom: 8pt; }\n\
17code { font-family: courier; background-color: #f0f0f0; }\n\
18pre { font-family: courier; font-size: 0.9em; background-color: #f0f0f0; margin-top: 8pt; margin-bottom: 8pt; padding: 8pt; }\n\
19blockquote { margin-left: 24pt; margin-top: 8pt; margin-bottom: 8pt; color: #555; font-style: italic; }\n\
20ul { margin-top: 4pt; margin-bottom: 8pt; }\n\
21ol { margin-top: 4pt; margin-bottom: 8pt; }\n\
22li { margin-bottom: 2pt; }\n\
23table { margin-top: 8pt; margin-bottom: 8pt; }\n\
24th { font-weight: bold; background-color: #e8e8e8; padding: 4pt; }\n\
25td { padding: 4pt; }\n\
26a { color: #0645ad; text-decoration: underline; }\n\
27del { text-decoration: line-through; }\n\
28hr { margin-top: 12pt; margin-bottom: 12pt; color: #999; }\n";
29
30#[derive(Debug)]
32pub struct Theme {
33 decls: Vec<Declared>,
34}
35
36impl Theme {
37 pub fn default_theme() -> Theme {
39 let rules = parse_sheet(DEFAULT_CSS).expect("the built-in default theme parses");
40 Theme::from_rules(Theme::empty(), rules)
41 }
42
43 pub fn parse(css: &str) -> Result<Theme, StyleError> {
46 Ok(Theme::from_rules(Theme::default_theme(), parse_sheet(css)?))
47 }
48
49 fn empty() -> Theme {
51 Theme {
52 decls: vec![Declared::default(); Element::ALL.len()],
53 }
54 }
55
56 fn from_rules(mut theme: Theme, rules: Vec<crate::parse::Rule>) -> Theme {
58 for rule in rules {
59 for element in &rule.elements {
60 theme.decls[*element as usize].merge(&rule.declared);
61 }
62 }
63 theme
64 }
65
66 pub fn declared(&self, e: Element) -> &Declared {
69 &self.decls[e as usize]
70 }
71
72 pub fn base(&self) -> TextStyle {
75 TextStyle::base().apply(self.declared(Element::Body))
76 }
77
78 pub fn margin(&self, e: Element) -> Edges {
80 let d = self.declared(e);
81 Edges {
82 top: d.margin[0].unwrap_or(0.0),
83 right: d.margin[1].unwrap_or(0.0),
84 bottom: d.margin[2].unwrap_or(0.0),
85 left: d.margin[3].unwrap_or(0.0),
86 }
87 }
88
89 pub fn padding(&self, e: Element) -> Edges {
91 let d = self.declared(e);
92 Edges {
93 top: d.padding[0].unwrap_or(0.0),
94 right: d.padding[1].unwrap_or(0.0),
95 bottom: d.padding[2].unwrap_or(0.0),
96 left: d.padding[3].unwrap_or(0.0),
97 }
98 }
99
100 pub fn background(&self, e: Element) -> Option<Color> {
102 self.declared(e).background
103 }
104}
105
106#[cfg(test)]
107mod tests {
108 use super::*;
109
110 #[test]
111 fn default_theme_parses_and_covers_body() {
112 let theme = Theme::default_theme();
113 let base = theme.base();
114 assert_eq!(base.size, 11.0);
115 assert_eq!(base.family, crate::style::FontFamily::Helvetica);
116 assert_eq!(theme.margin(Element::Body).left, 72.0);
117 }
118
119 #[test]
120 fn user_theme_overlays_defaults() {
121 let theme = Theme::parse("h1 { color: #c00; }").unwrap();
122 let h1 = theme.base().apply(theme.declared(Element::H1));
123 assert_eq!(h1.color, Color::Rgb(0.8, 0.0, 0.0));
124 assert!(
125 h1.bold,
126 "default h1 bold survives an overlay that only sets color"
127 );
128 assert_eq!(h1.size, 22.0, "default h1 2em of body 11pt survives");
129 }
130
131 #[test]
132 fn later_rule_wins() {
133 let theme = Theme::parse("p { color: #111; }\np { color: #222; }").unwrap();
134 let p = theme.base().apply(theme.declared(Element::P));
135 let expected = 0x22 as f32 / 255.0;
136 assert_eq!(p.color, Color::Rgb(expected, expected, expected));
137 }
138
139 #[test]
140 fn parse_error_location_passes_through() {
141 let e = Theme::parse("h1 { font-size: 12px; }").unwrap_err();
142 assert_eq!(e.line, 1);
143 }
144}