1mod error;
2pub mod style;
3
4use cairo::{Context, ImageSurface};
5use error::CssError;
6use lazy_static::lazy_static;
7use rayon::prelude::*;
8use regex::Regex;
9use std::{collections::HashMap, ops::Deref};
10use style::{Parseable, Style};
11
12lazy_static! {
13 static ref RE: Regex =
14 Regex::new(r"(?P<selector>\S+)\s*\{\s*(?P<properties>[^}]+)\s*\}").unwrap();
15 static ref PROPERTY_RE: Regex =
16 Regex::new(r"(?P<property>[\w-]+):\s*(?P<value>[^;]+);").unwrap();
17}
18
19pub fn parse<T>(css: T) -> Result<Vec<Style>, CssError<'static>>
20where
21 T: AsRef<str>,
22{
23 let split = css
24 .as_ref()
25 .split_inclusive('}')
26 .filter_map(|selector| {
27 let selector = selector.trim();
28 if selector.is_empty() {
29 return None;
30 }
31 Some(selector)
32 })
33 .collect::<Vec<&str>>();
34
35 let all_selector = split.iter().find_map(|s| {
36 let mut properties: HashMap<Box<str>, String> = HashMap::new();
37
38 for cap in RE.captures_iter(s) {
39 if &cap["selector"] == "*" {
40 for property_cap in PROPERTY_RE.captures_iter(&cap["properties"]) {
41 properties.insert(
42 property_cap["property"].into(),
43 property_cap["value"].to_string(),
44 );
45 }
46 return Some(properties);
47 }
48 }
49 None
50 });
51
52 Ok(split
53 .par_iter()
54 .filter_map(|s| {
55 let mut properties = HashMap::with_capacity(split.len() - 1);
56
57 if let Some(cap) = RE.captures_iter(s).next() {
58 let selector = cap["selector"].to_string();
59 PROPERTY_RE
60 .captures_iter(&cap["properties"])
61 .for_each(|property_cap| {
62 properties.insert(
63 property_cap["property"].into(),
64 property_cap["value"].to_string(),
65 );
66 });
67 return Some((selector, properties));
68 }
69
70 None
71 })
72 .map(|(selector, properties)| Style::new(selector, &properties, all_selector.as_ref()))
73 .collect::<Vec<Style>>())
74}
75
76pub fn render<T>(css: T) -> Result<HashMap<String, Vec<u8>>, CssError<'static>>
77where
78 T: Parseable,
79{
80 let mut styles = css.parse()?;
81
82 styles
83 .par_iter_mut()
84 .map(|style| {
85 let name = &style.selector;
86
87 let mut width = style.width;
88 let mut height = style.height;
89 let mut position = 0;
90
91 let mut text_width = 0;
92
93 if let Some(content) = &style.content {
94 if content.is_empty() {
95 style.content = None;
96 }
97 }
98 if let Some(content) = &style.content {
99 let surface = ImageSurface::create(cairo::Format::ARgb32, 0, 0)
100 .map_err(|_| CssError::ContentError("Failed to create cairo surface"))?;
101 let context = Context::new(&surface)
102 .map_err(|_| CssError::ContentError("Failed to create cairo context"))?;
103 let font = &style.font;
104
105 context.select_font_face(font.family.deref(), font.style, font.weight);
106 context.set_font_size(font.size);
107 let extents = context
108 .text_extents(content.deref())
109 .map_err(|_| CssError::ContentError(""))?;
110
111 if width.is_none() {
112 width = Some(extents.width() as i32);
113 }
114 if height.is_none() {
115 height = Some(extents.height() as i32);
116 }
117 text_width = extents.width() as i32;
118 position = extents.y_bearing().abs() as i32;
119 }
120
121 let margin = style.margin;
122 let padding = style.padding;
123
124 let width = width.unwrap_or(5);
125 let height = height.unwrap_or(5);
126
127 let surface = ImageSurface::create(
128 cairo::Format::ARgb32,
129 width + margin[1] + margin[3] + padding[1] + padding[3],
130 height + margin[0] + margin[2] + padding[0] + padding[2],
131 )
132 .map_err(|_| CssError::ContentError("Failed to create cairo surface"))?;
133 let mut img =
134 Vec::with_capacity(surface.width() as usize * surface.height() as usize * 4);
135
136 let context = Context::new(&surface)
137 .map_err(|_| CssError::ContentError("Failed to create cairo context"))?;
138
139 context.set_source_rgba(
140 style.background_color[0],
141 style.background_color[1],
142 style.background_color[2],
143 style.background_color[3],
144 );
145 draw_rectangle(
146 &context,
147 margin[3] as f64,
148 margin[0] as f64,
149 width as f64 + padding[1] as f64 + padding[3] as f64,
150 height as f64 + padding[0] as f64 + padding[2] as f64,
151 style.border_radius,
152 );
153 context
154 .fill_preserve()
155 .map_err(|_| CssError::ContentError("Failed to paint the surface"))?;
156
157 if let Some(text) = &style.content {
158 let font = &style.font;
159 context.select_font_face(font.family.deref(), font.style, font.weight);
160 context.set_font_size(font.size);
161 context.set_source_rgba(font.color[0], font.color[1], font.color[2], 1.0);
162 match font.text_align.deref() {
163 "center" => {
164 context.move_to(
165 (width / 2 - text_width / 2) as f64 + padding[3] as f64,
166 position as f64 + padding[0] as f64,
167 );
168 }
169 "right" => {
170 context.move_to(width as f64 - text_width as f64, position as f64);
171 }
172 "left" => {
173 context.move_to(
174 0.0 + padding[3] as f64 + margin[3] as f64,
175 position as f64 + padding[0] as f64 + margin[0] as f64,
176 );
177 }
178 _ => return Err(CssError::ContentError("Invalid text-align")),
179 }
180 _ = context.show_text(text.deref());
181 }
182
183 surface
184 .write_to_png(&mut img)
185 .map_err(|_| CssError::ContentError("Failed to write cairo surface as PNG"))?;
186
187 Ok((name.clone(), img))
188 })
189 .collect::<Result<HashMap<_, _>, CssError>>()
190}
191
192fn draw_rectangle(context: &Context, x: f64, y: f64, width: f64, height: f64, border_radius: f64) {
193 let border_radius = match border_radius > 20. {
194 true => 20. / 3.33,
195 false => border_radius / 3.33,
196 };
197 let degrees = std::f64::consts::PI / 180.0;
198
199 context.new_sub_path();
200 context.arc(
201 x + width - border_radius,
202 y + border_radius,
203 border_radius,
204 -90.0 * degrees,
205 0.0 * degrees,
206 );
207 context.arc(
208 x + width - border_radius,
209 y + height - border_radius,
210 border_radius,
211 0.0 * degrees,
212 90.0 * degrees,
213 );
214 context.arc(
215 x + border_radius,
216 y + height - border_radius,
217 border_radius,
218 90.0 * degrees,
219 180.0 * degrees,
220 );
221 context.arc(
222 x + border_radius,
223 y + border_radius,
224 border_radius,
225 180.0 * degrees,
226 270.0 * degrees,
227 );
228 context.close_path();
229}