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::Text(data) => {
127 if data.content.chars().all(|c| c.is_ascii_whitespace()) {
128 println!("{id} #text: <whitespace>");
129 } else {
130 let content = data.content.trim();
131 if content.len() > 10 {
132 println!(
133 "#text {id}: {}...",
134 content
135 .split_at(content.char_indices().take(10).last().unwrap().0)
136 .0
137 .escape_default()
138 )
139 } else {
140 println!("#text {id}: {}", data.content.trim().escape_default())
141 }
142 }
143 }
144
145 NodeData::Comment { .. } => println!("<!-- COMMENT {id} -->"),
146
147 NodeData::ShadowRoot(data) => println!("{id} #shadow-root ({:?})", data.mode),
148
149 NodeData::AnonymousBlock(_) => println!("{id} AnonymousBlock"),
150
151 NodeData::Element(data) => {
152 print!("<{} {id}", data.name.local);
153 for attr in data.attrs.iter() {
154 print!(" {}=\"{}\"", attr.name.local, attr.value);
155 }
156 if !node.children.is_empty() {
157 println!(">");
158 } else {
159 println!("/>");
160 }
161 } }
168
169 if !node.children.is_empty() {
170 for child_id in node.children.iter() {
171 walk_tree(indent + 2, node.with(*child_id));
172 }
173
174 if let NodeData::Element(data) = &node.data {
175 println!("{}</{}>", " ".repeat(indent), data.name.local);
176 }
177 }
178}
179
180#[cfg(feature = "svg")]
182pub(crate) fn parse_svg_image(source: &[u8]) -> Result<crate::node::SvgImageData, usvg::Error> {
183 let options = usvg::Options {
184 fontdb: Arc::clone(&*FONT_DB),
185 ..Default::default()
186 };
187 let tree = usvg::Tree::from_data(source, &options)?;
188 Ok(crate::node::SvgImageData {
189 tree: Arc::new(tree),
190 })
191}
192
193pub trait ToColorColor {
194 fn as_color_color(&self) -> Color;
196}
197impl ToColorColor for AbsoluteColor {
198 fn as_color_color(&self) -> Color {
199 Color::new(
200 *self
201 .to_color_space(style::color::ColorSpace::Srgb)
202 .raw_components(),
203 )
204 }
205}
206
207pub(crate) fn absolute_color_to_svg_css(color: &AbsoluteColor) -> String {
211 let [red, green, blue, alpha] = color.as_color_color().components;
212 let channel = |value: f32| (value.clamp(0.0, 1.0) * 255.0).round() as u8;
213 format!(
214 "rgba({}, {}, {}, {})",
215 channel(red),
216 channel(green),
217 channel(blue),
218 alpha.clamp(0.0, 1.0)
219 )
220}
221
222#[cfg(all(test, feature = "svg"))]
223mod svg_tests {
224 use super::parse_svg_image;
225
226 #[test]
227 fn missing_height_is_computed_from_width_and_viewbox_ratio() {
228 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>"#;
229 let svg = parse_svg_image(src).unwrap();
230 assert_eq!(svg.intrinsic_width(), Some(200.0));
231 assert_eq!(svg.intrinsic_height(), None);
232 assert_eq!(svg.viewbox_aspect_ratio(), Some(1.0));
233 assert_eq!(svg.tree.size().width(), 200.0);
234 assert_eq!(svg.tree.size().height(), 200.0);
235 assert_eq!(svg.intrinsic_size(), (200.0, 200.0));
236 }
237
238 #[test]
239 fn viewbox_only_has_no_intrinsic_dimensions() {
240 let src = br#"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 485 58"></svg>"#;
241 let svg = parse_svg_image(src).unwrap();
242 assert_eq!(svg.intrinsic_width(), None);
243 assert_eq!(svg.intrinsic_height(), None);
244 assert!((svg.aspect_ratio() - (485.0 / 58.0)).abs() < 1e-3);
246 }
247
248 #[test]
249 fn absolute_dimensions_are_intrinsic() {
250 let src = br#"<svg xmlns="http://www.w3.org/2000/svg" width="24" height="16" viewBox="0 0 48 32"></svg>"#;
251 let svg = parse_svg_image(src).unwrap();
252 assert_eq!(svg.intrinsic_width(), Some(24.0));
253 assert_eq!(svg.intrinsic_height(), Some(16.0));
254 }
255
256 #[test]
257 fn percentage_dimensions_are_not_intrinsic() {
258 let src = br#"<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="50%" viewBox="0 0 200 100"></svg>"#;
259 let svg = parse_svg_image(src).unwrap();
260 assert_eq!(svg.intrinsic_width(), None);
261 assert_eq!(svg.intrinsic_height(), None);
262 }
263
264 #[test]
265 fn unit_lengths_are_intrinsic() {
266 let src = br#"<svg xmlns="http://www.w3.org/2000/svg" width="24px" height="1.5em" viewBox="0 0 48 32"></svg>"#;
267 let svg = parse_svg_image(src).unwrap();
268 assert!(svg.intrinsic_width().is_some());
269 assert!(svg.intrinsic_height().is_some());
270 }
271
272 #[test]
273 fn non_numeric_dimensions_are_not_intrinsic() {
274 let src = br#"<svg xmlns="http://www.w3.org/2000/svg" width="auto" height="foo" viewBox="0 0 200 100"></svg>"#;
275 let svg = parse_svg_image(src).unwrap();
276 assert_eq!(svg.intrinsic_width(), None);
277 assert_eq!(svg.intrinsic_height(), None);
278 }
279}
280
281#[macro_export]
284macro_rules! qual_name {
285 ($local:tt $(, $ns:ident)?) => {
286 $crate::QualName {
287 prefix: None,
288 ns: $crate::ns!($($ns)?),
289 local: $crate::local_name!($local),
290 }
291 };
292}
293
294#[cfg(feature = "svg")]
302pub(crate) fn restore_svg_attribute_case(markup: &str) -> String {
303 const CAMEL_CASED: &[&str] = &[
306 "viewBox",
307 "preserveAspectRatio",
308 "baseProfile",
309 "clipPath",
310 "clipPathUnits",
311 "diffuseConstant",
312 "edgeMode",
313 "filterUnits",
314 "glyphRef",
315 "gradientTransform",
316 "gradientUnits",
317 "kernelMatrix",
318 "kernelUnitLength",
319 "keyPoints",
320 "keySplines",
321 "keyTimes",
322 "lengthAdjust",
323 "limitingConeAngle",
324 "markerHeight",
325 "markerUnits",
326 "markerWidth",
327 "maskContentUnits",
328 "maskUnits",
329 "numOctaves",
330 "pathLength",
331 "patternContentUnits",
332 "patternTransform",
333 "patternUnits",
334 "pointsAtX",
335 "pointsAtY",
336 "pointsAtZ",
337 "primitiveUnits",
338 "refX",
339 "refY",
340 "repeatCount",
341 "repeatDur",
342 "requiredExtensions",
343 "requiredFeatures",
344 "specularConstant",
345 "specularExponent",
346 "spreadMethod",
347 "startOffset",
348 "stdDeviation",
349 "stitchTiles",
350 "surfaceScale",
351 "systemLanguage",
352 "tableValues",
353 "targetX",
354 "targetY",
355 "textLength",
356 "xChannelSelector",
357 "yChannelSelector",
358 "zoomAndPan",
359 ];
360
361 let mut output = markup.to_owned();
362 for name in CAMEL_CASED {
363 let lowered = name.to_ascii_lowercase();
364 if lowered == *name {
365 continue;
366 }
367 let needle = format!(" {lowered}=");
370 if output.contains(&needle) {
371 output = output.replace(&needle, &format!(" {name}="));
372 }
373 }
374 output
375}
376
377#[cfg(all(test, feature = "svg"))]
378mod svg_attribute_case_tests {
379 use super::{parse_svg_image, restore_svg_attribute_case};
380
381 #[test]
384 fn a_lowercased_view_box_loses_the_declared_aspect_ratio() {
385 let lowered = r#"<svg viewbox="0 0 744 221" xmlns="http://www.w3.org/2000/svg"><path d="M0 0h10v10H0z"/></svg>"#;
386 let parsed = parse_svg_image(lowered.as_bytes()).unwrap();
387 let ratio = parsed.tree.size().width() / parsed.tree.size().height();
388 assert!(
389 (ratio - 3.367).abs() > 0.1,
390 "lowercased viewbox unexpectedly honoured: {ratio}"
391 );
392 }
393
394 #[test]
395 fn restoring_the_case_recovers_the_view_box() {
396 let lowered = r#"<svg viewbox="0 0 744 221" preserveaspectratio="xMidYMid meet" xmlns="http://www.w3.org/2000/svg"><path d="M0 0h10v10H0z"/></svg>"#;
397 let restored = restore_svg_attribute_case(lowered);
398 assert!(restored.contains("viewBox="));
399 assert!(restored.contains("preserveAspectRatio="));
400
401 let parsed = parse_svg_image(restored.as_bytes()).unwrap();
402 let size = parsed.tree.size();
403 assert_eq!(size.width().round(), 744.0);
404 assert_eq!(size.height().round(), 221.0);
405 }
406
407 #[test]
408 fn attribute_names_without_capitals_are_left_alone() {
409 let markup = r#"<svg width="10" height="10" xmlns="http://www.w3.org/2000/svg"/>"#;
410 assert_eq!(restore_svg_attribute_case(markup), markup);
411 }
412}