1use crate::node::{Node, NodeData};
2use color::{AlphaColor, Srgb};
3use keyboard_types::Modifiers;
4use std::borrow::Cow;
5use style::color::AbsoluteColor;
6
7#[cfg(target_os = "macos")]
8pub(crate) const ACTION_MOD: Modifiers = Modifiers::SUPER;
9#[cfg(not(target_os = "macos"))]
10pub(crate) const ACTION_MOD: Modifiers = Modifiers::CONTROL;
11
12pub(crate) fn has_clipboard_modifier(modifiers: Modifiers) -> bool {
19 modifiers.contains(ACTION_MOD) || modifiers.contains(Modifiers::CONTROL)
20}
21
22#[cfg(test)]
23mod shortcut_tests {
24 use super::*;
25
26 #[test]
27 fn clipboard_accepts_control_and_the_platform_action_modifier() {
28 assert!(has_clipboard_modifier(Modifiers::CONTROL));
29 assert!(has_clipboard_modifier(ACTION_MOD));
30 assert!(!has_clipboard_modifier(Modifiers::SHIFT));
31 }
32}
33
34pub type Color = AlphaColor<Srgb>;
35
36pub fn decode_font_bytes(bytes: &[u8]) -> Cow<'_, [u8]> {
40 if bytes.len() < 4 {
41 return Cow::Borrowed(bytes);
42 }
43 match &bytes[0..4] {
44 #[cfg(feature = "woff")]
45 b"wOFF" => wuff::decompress_woff1(bytes)
46 .map(Cow::Owned)
47 .unwrap_or_else(|_| {
48 #[cfg(feature = "tracing")]
49 tracing::warn!("Failed to decompress woff1 font");
50 Cow::Borrowed(bytes)
51 }),
52 #[cfg(feature = "woff")]
53 b"wOF2" => wuff::decompress_woff2(bytes)
54 .map(Cow::Owned)
55 .unwrap_or_else(|_| {
56 #[cfg(feature = "tracing")]
57 tracing::warn!("Failed to decompress woff2 font");
58 Cow::Borrowed(bytes)
59 }),
60 _ => Cow::Borrowed(bytes),
61 }
62}
63
64#[cfg(feature = "svg")]
65use std::sync::{Arc, LazyLock};
66#[cfg(feature = "svg")]
67use usvg::fontdb;
68#[cfg(feature = "svg")]
69pub(crate) static FONT_DB: LazyLock<Arc<fontdb::Database>> = LazyLock::new(|| {
70 let mut db = fontdb::Database::new();
71 db.load_system_fonts();
72 Arc::new(db)
73});
74
75#[derive(Clone, Copy, Debug)]
78pub enum ImageLayerKind {
79 Background,
80 Mask,
81}
82
83impl ImageLayerKind {
84 pub fn image_type(self, idx: usize) -> ImageType {
85 match self {
86 Self::Background => ImageType::Background(idx),
87 Self::Mask => ImageType::Mask(idx),
88 }
89 }
90}
91
92#[derive(Clone, Copy, Debug)]
93pub enum ImageType {
94 Image,
95 Background(usize),
96 Mask(usize),
97}
98
99#[derive(Clone, Debug, Copy, Eq, PartialEq)]
101pub struct Point<T> {
102 pub x: T,
104 pub y: T,
106}
107
108impl Point<f64> {
109 pub const ZERO: Self = Point { x: 0.0, y: 0.0 };
110}
111
112pub fn walk_tree(indent: usize, node: &Node) {
114 if let NodeData::Text(data) = &node.data {
116 if data.content.chars().all(|c| c.is_ascii_whitespace()) {
117 return;
118 }
119 }
120
121 print!("{}", " ".repeat(indent));
122 let id = node.id;
123 match &node.data {
124 NodeData::Document(_) => println!("#Document {id}"),
125
126 NodeData::DocumentFragment => println!("#DocumentFragment {id}"),
127
128 NodeData::Text(data) => {
129 if data.content.chars().all(|c| c.is_ascii_whitespace()) {
130 println!("{id} #text: <whitespace>");
131 } else {
132 let content = data.content.trim();
133 if content.len() > 10 {
134 println!(
135 "#text {id}: {}...",
136 content
137 .split_at(content.char_indices().take(10).last().unwrap().0)
138 .0
139 .escape_default()
140 )
141 } else {
142 println!("#text {id}: {}", data.content.trim().escape_default())
143 }
144 }
145 }
146
147 NodeData::Comment { .. } => println!("<!-- COMMENT {id} -->"),
148
149 NodeData::ShadowRoot(data) => println!("{id} #shadow-root ({:?})", data.mode),
150
151 NodeData::AnonymousBlock(_) => println!("{id} AnonymousBlock"),
152
153 NodeData::Element(data) => {
154 print!("<{} {id}", data.name.local);
155 for attr in data.attrs.iter() {
156 print!(" {}=\"{}\"", attr.name.local, attr.value);
157 }
158 if !node.children.is_empty() {
159 println!(">");
160 } else {
161 println!("/>");
162 }
163 } }
170
171 if !node.children.is_empty() {
172 for child_id in node.children.iter() {
173 walk_tree(indent + 2, node.with(*child_id));
174 }
175
176 if let NodeData::Element(data) = &node.data {
177 println!("{}</{}>", " ".repeat(indent), data.name.local);
178 }
179 }
180}
181
182#[cfg(feature = "svg")]
184pub(crate) fn parse_svg_image(source: &[u8]) -> Result<crate::node::SvgImageData, usvg::Error> {
185 let options = usvg::Options {
186 fontdb: Arc::clone(&*FONT_DB),
187 ..Default::default()
188 };
189 let tree = usvg::Tree::from_data(source, &options)?;
190 Ok(crate::node::SvgImageData {
191 tree: Arc::new(tree),
192 })
193}
194
195pub trait ToColorColor {
196 fn as_color_color(&self) -> Color;
198}
199impl ToColorColor for AbsoluteColor {
200 fn as_color_color(&self) -> Color {
201 Color::new(
202 *self
203 .to_color_space(style::color::ColorSpace::Srgb)
204 .raw_components(),
205 )
206 }
207}
208
209pub(crate) fn absolute_color_to_svg_css(color: &AbsoluteColor) -> String {
213 let [red, green, blue, alpha] = color.as_color_color().components;
214 let channel = |value: f32| (value.clamp(0.0, 1.0) * 255.0).round() as u8;
215 format!(
216 "rgba({}, {}, {}, {})",
217 channel(red),
218 channel(green),
219 channel(blue),
220 alpha.clamp(0.0, 1.0)
221 )
222}
223
224#[cfg(all(test, feature = "svg"))]
225mod svg_tests {
226 use super::parse_svg_image;
227
228 #[test]
229 fn missing_height_is_computed_from_width_and_viewbox_ratio() {
230 let src = br#"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" width="200"><rect width="100%" height="100%" fill="green"/></svg>"#;
231 let svg = parse_svg_image(src).unwrap();
232 assert_eq!(svg.intrinsic_width(), Some(200.0));
233 assert_eq!(svg.intrinsic_height(), None);
234 assert_eq!(svg.viewbox_aspect_ratio(), Some(1.0));
235 assert_eq!(svg.tree.size().width(), 200.0);
236 assert_eq!(svg.tree.size().height(), 200.0);
237 assert_eq!(svg.intrinsic_size(), (200.0, 200.0));
238 }
239
240 #[test]
241 fn viewbox_only_has_no_intrinsic_dimensions() {
242 let src = br#"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 485 58"></svg>"#;
243 let svg = parse_svg_image(src).unwrap();
244 assert_eq!(svg.intrinsic_width(), None);
245 assert_eq!(svg.intrinsic_height(), None);
246 assert!((svg.aspect_ratio() - (485.0 / 58.0)).abs() < 1e-3);
248 }
249
250 #[test]
251 fn absolute_dimensions_are_intrinsic() {
252 let src = br#"<svg xmlns="http://www.w3.org/2000/svg" width="24" height="16" viewBox="0 0 48 32"></svg>"#;
253 let svg = parse_svg_image(src).unwrap();
254 assert_eq!(svg.intrinsic_width(), Some(24.0));
255 assert_eq!(svg.intrinsic_height(), Some(16.0));
256 }
257
258 #[test]
259 fn percentage_dimensions_are_not_intrinsic() {
260 let src = br#"<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="50%" viewBox="0 0 200 100"></svg>"#;
261 let svg = parse_svg_image(src).unwrap();
262 assert_eq!(svg.intrinsic_width(), None);
263 assert_eq!(svg.intrinsic_height(), None);
264 }
265
266 #[test]
267 fn unit_lengths_are_intrinsic() {
268 let src = br#"<svg xmlns="http://www.w3.org/2000/svg" width="24px" height="1.5em" viewBox="0 0 48 32"></svg>"#;
269 let svg = parse_svg_image(src).unwrap();
270 assert!(svg.intrinsic_width().is_some());
271 assert!(svg.intrinsic_height().is_some());
272 }
273
274 #[test]
275 fn non_numeric_dimensions_are_not_intrinsic() {
276 let src = br#"<svg xmlns="http://www.w3.org/2000/svg" width="auto" height="foo" viewBox="0 0 200 100"></svg>"#;
277 let svg = parse_svg_image(src).unwrap();
278 assert_eq!(svg.intrinsic_width(), None);
279 assert_eq!(svg.intrinsic_height(), None);
280 }
281}
282
283#[macro_export]
286macro_rules! qual_name {
287 ($local:tt $(, $ns:ident)?) => {
288 $crate::QualName {
289 prefix: None,
290 ns: $crate::ns!($($ns)?),
291 local: $crate::local_name!($local),
292 }
293 };
294}
295
296#[cfg(feature = "svg")]
304pub(crate) fn restore_svg_attribute_case(markup: &str) -> String {
305 const CAMEL_CASED: &[&str] = &[
308 "viewBox",
309 "preserveAspectRatio",
310 "baseProfile",
311 "clipPath",
312 "clipPathUnits",
313 "diffuseConstant",
314 "edgeMode",
315 "filterUnits",
316 "glyphRef",
317 "gradientTransform",
318 "gradientUnits",
319 "kernelMatrix",
320 "kernelUnitLength",
321 "keyPoints",
322 "keySplines",
323 "keyTimes",
324 "lengthAdjust",
325 "limitingConeAngle",
326 "markerHeight",
327 "markerUnits",
328 "markerWidth",
329 "maskContentUnits",
330 "maskUnits",
331 "numOctaves",
332 "pathLength",
333 "patternContentUnits",
334 "patternTransform",
335 "patternUnits",
336 "pointsAtX",
337 "pointsAtY",
338 "pointsAtZ",
339 "primitiveUnits",
340 "refX",
341 "refY",
342 "repeatCount",
343 "repeatDur",
344 "requiredExtensions",
345 "requiredFeatures",
346 "specularConstant",
347 "specularExponent",
348 "spreadMethod",
349 "startOffset",
350 "stdDeviation",
351 "stitchTiles",
352 "surfaceScale",
353 "systemLanguage",
354 "tableValues",
355 "targetX",
356 "targetY",
357 "textLength",
358 "xChannelSelector",
359 "yChannelSelector",
360 "zoomAndPan",
361 ];
362
363 let mut output = markup.to_owned();
364 for name in CAMEL_CASED {
365 let lowered = name.to_ascii_lowercase();
366 if lowered == *name {
367 continue;
368 }
369 let needle = format!(" {lowered}=");
372 if output.contains(&needle) {
373 output = output.replace(&needle, &format!(" {name}="));
374 }
375 }
376 output
377}
378
379#[cfg(all(test, feature = "svg"))]
380mod svg_attribute_case_tests {
381 use super::{parse_svg_image, restore_svg_attribute_case};
382
383 #[test]
386 fn a_lowercased_view_box_loses_the_declared_aspect_ratio() {
387 let lowered = r#"<svg viewbox="0 0 744 221" xmlns="http://www.w3.org/2000/svg"><path d="M0 0h10v10H0z"/></svg>"#;
388 let parsed = parse_svg_image(lowered.as_bytes()).unwrap();
389 let ratio = parsed.tree.size().width() / parsed.tree.size().height();
390 assert!(
391 (ratio - 3.367).abs() > 0.1,
392 "lowercased viewbox unexpectedly honoured: {ratio}"
393 );
394 }
395
396 #[test]
397 fn restoring_the_case_recovers_the_view_box() {
398 let lowered = r#"<svg viewbox="0 0 744 221" preserveaspectratio="xMidYMid meet" xmlns="http://www.w3.org/2000/svg"><path d="M0 0h10v10H0z"/></svg>"#;
399 let restored = restore_svg_attribute_case(lowered);
400 assert!(restored.contains("viewBox="));
401 assert!(restored.contains("preserveAspectRatio="));
402
403 let parsed = parse_svg_image(restored.as_bytes()).unwrap();
404 let size = parsed.tree.size();
405 assert_eq!(size.width().round(), 744.0);
406 assert_eq!(size.height().round(), 221.0);
407 }
408
409 #[test]
410 fn attribute_names_without_capitals_are_left_alone() {
411 let markup = r#"<svg width="10" height="10" xmlns="http://www.w3.org/2000/svg"/>"#;
412 assert_eq!(restore_svg_attribute_case(markup), markup);
413 }
414}