1use cssparser::Parser;
10use selectors::matching::QuirksMode;
11use style::computed_values::box_sizing::T as BoxSizing;
12use style::computed_values::position::T as Position;
13use style::parser::ParserContext;
14use style::properties::declaration_block::{Importance, parse_style_attribute};
15use style::properties::{
16 ComputedValues, NonCustomPropertyId, PropertyDeclaration, PropertyDeclarationBlock,
17 PropertyDeclarationId, PropertyId, ShorthandId, SourcePropertyDeclaration,
18 parse_one_declaration_into,
19};
20use style::servo_arc::Arc as ServoArc;
21use style::stylesheets::supports_rule::parse_condition_or_declaration;
22use style::stylesheets::{CssRuleType, Origin, OriginSet, UrlExtraData};
23use style::stylist::RegisterCustomPropertyResult;
24use style::values::computed::length::CSSPixelLength;
25use style::values::computed::{GridTemplateAreas, GridTemplateComponent, LengthPercentage};
26use style::values::generics::position::{Inset as GenericInset, PreferredRatio};
27use style::values::resolved;
28use style::values::specified::box_::{DisplayInside, DisplayOutside};
29use style_traits::{CssStringWriter, ParsingMode, ToCss};
30
31use blitz_traits::node_id::NodeId;
32use url::Url;
33
34use crate::BaseDocument;
35use crate::layout::replaced::is_replaced_element;
36use crate::local_name;
37
38fn format_px(px: f32) -> String {
40 CSSPixelLength::new(px).to_css_string()
41}
42
43fn resolve_inset<P>(val: &GenericInset<P, LengthPercentage>, basis: f32) -> Option<f32> {
46 match val {
47 GenericInset::LengthPercentage(lp) => Some(lp.resolve(CSSPixelLength::new(basis)).px()),
48 _ => None,
49 }
50}
51
52fn format_matrix_component(v: f64) -> String {
55 let rounded = (v * 1e6).round() / 1e6;
56 let rounded = if rounded == 0.0 { 0.0 } else { rounded };
58 format!("{rounded}")
59}
60
61fn format_matrix(m: &euclid::default::Transform3D<f64>, is_3d: bool) -> String {
64 let c = format_matrix_component;
65 if is_3d {
66 format!(
67 "matrix3d({}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {})",
68 c(m.m11),
69 c(m.m12),
70 c(m.m13),
71 c(m.m14),
72 c(m.m21),
73 c(m.m22),
74 c(m.m23),
75 c(m.m24),
76 c(m.m31),
77 c(m.m32),
78 c(m.m33),
79 c(m.m34),
80 c(m.m41),
81 c(m.m42),
82 c(m.m43),
83 c(m.m44)
84 )
85 } else {
86 format!(
87 "matrix({}, {}, {}, {}, {}, {})",
88 c(m.m11),
89 c(m.m12),
90 c(m.m21),
91 c(m.m22),
92 c(m.m41),
93 c(m.m42)
94 )
95 }
96}
97
98pub fn css_property_is_supported(name: &str) -> bool {
102 matches!(
103 PropertyId::parse_enabled_for_all_content(name),
104 Ok(property_id) if !matches!(property_id, PropertyId::Custom(_))
105 )
106}
107
108pub fn parse_transform_matrix(value: &str) -> Option<([f64; 16], bool)> {
117 use style::properties::longhands::transform;
118 let url_data = UrlExtraData(ServoArc::new(Url::parse("about:blank").unwrap()));
120 let context = ParserContext::new(
121 Origin::Author,
122 &url_data,
123 Some(CssRuleType::Style),
124 ParsingMode::DEFAULT,
125 QuirksMode::NoQuirks,
126 Default::default(),
127 None,
128 None,
129 Default::default(),
130 );
131 let mut parser = Parser::new(value);
132 let transform = parser
133 .parse_entirely(|t| transform::parse(&context, t))
134 .ok()?;
135 let (m, is_3d) = transform.to_transform_3d_matrix_f64(None).ok()?;
136 Some((
137 [
138 m.m11, m.m12, m.m13, m.m14, m.m21, m.m22, m.m23, m.m24, m.m31, m.m32, m.m33, m.m34,
139 m.m41, m.m42, m.m43, m.m44,
140 ],
141 !is_3d,
142 ))
143}
144
145pub fn resolved_style_property_names() -> &'static [&'static str] {
149 static NAMES: std::sync::OnceLock<Vec<&'static str>> = std::sync::OnceLock::new();
150 NAMES.get_or_init(|| {
151 let mut names: Vec<&'static str> = NonCustomPropertyId::iter()
152 .filter_map(|id| id.as_longhand())
153 .map(|longhand| longhand.name())
154 .filter(|name| PropertyId::parse_enabled_for_all_content(name).is_ok())
155 .collect();
156 names.sort_unstable();
157 names
158 })
159}
160
161impl BaseDocument {
162 pub fn css_declaration_is_valid(&self, property: &str, value: &str) -> bool {
166 let Ok(property_id) = PropertyId::parse_enabled_for_all_content(property) else {
167 return false;
168 };
169 let mut declarations = SourcePropertyDeclaration::default();
170 parse_one_declaration_into(
171 &mut declarations,
172 property_id,
173 value,
174 Origin::Author,
175 &self.url.url_extra_data(),
176 None,
177 ParsingMode::DEFAULT,
178 QuirksMode::NoQuirks,
179 CssRuleType::Style,
180 )
181 .is_ok()
182 }
183
184 fn parse_style_attr_block(&self, style_attr: &str) -> PropertyDeclarationBlock {
187 parse_style_attribute(
188 style_attr,
189 &self.url.url_extra_data(),
190 None,
191 QuirksMode::NoQuirks,
192 CssRuleType::Style,
193 )
194 }
195
196 pub fn style_attr_serialize(&self, style_attr: &str) -> String {
199 let block = self.parse_style_attr_block(style_attr);
200 let mut css = CssStringWriter::new();
201 let _ = block.to_css(&mut css);
202 css
203 }
204
205 pub fn style_attr_get_property(&self, style_attr: &str, property: &str) -> String {
209 let Ok(property_id) = PropertyId::parse_enabled_for_all_content(property) else {
210 return String::new();
211 };
212 let block = self.parse_style_attr_block(style_attr);
213 let mut css = CssStringWriter::new();
214 let _ = block.property_value_to_css(&property_id, &mut css);
215 css
216 }
217
218 pub fn style_attr_set_property(
225 &self,
226 style_attr: &str,
227 property: &str,
228 value: &str,
229 important: bool,
230 ) -> Option<String> {
231 let property_id = PropertyId::parse_enabled_for_all_content(property).ok()?;
232 let mut block = self.parse_style_attr_block(style_attr);
233
234 if value.trim().is_empty() {
235 if let Some(first_declaration) = block.first_declaration_to_remove(&property_id) {
236 block.remove_property(&property_id, first_declaration);
237 }
238 } else {
239 let mut source = SourcePropertyDeclaration::default();
240 parse_one_declaration_into(
241 &mut source,
242 property_id,
243 value,
244 Origin::Author,
245 &self.url.url_extra_data(),
246 None,
247 ParsingMode::DEFAULT,
248 QuirksMode::NoQuirks,
249 CssRuleType::Style,
250 )
251 .ok()?;
252 let importance = if important {
253 Importance::Important
254 } else {
255 Importance::Normal
256 };
257 block.extend(source.drain(), importance);
258 }
259
260 let mut css = CssStringWriter::new();
261 let _ = block.to_css(&mut css);
262 Some(css)
263 }
264
265 pub fn style_attr_remove_property(
272 &self,
273 style_attr: &str,
274 property: &str,
275 ) -> Option<(String, String)> {
276 let property_id = PropertyId::parse_enabled_for_all_content(property).ok()?;
277 let mut block = self.parse_style_attr_block(style_attr);
278
279 let mut removed_value = CssStringWriter::new();
280 let _ = block.property_value_to_css(&property_id, &mut removed_value);
281 if let Some(first_declaration) = block.first_declaration_to_remove(&property_id) {
282 block.remove_property(&property_id, first_declaration);
283 }
284
285 let mut css = CssStringWriter::new();
286 let _ = block.to_css(&mut css);
287 Some((css, removed_value))
288 }
289
290 pub fn css_supports_condition(&self, condition: &str) -> bool {
295 let mut parser = Parser::new(condition);
296 let Ok(condition) = parser.parse_entirely(parse_condition_or_declaration) else {
297 return false;
298 };
299
300 let url_data = self.url.url_extra_data();
301 let context = ParserContext::new(
302 Origin::Author,
303 &url_data,
304 Some(CssRuleType::Style),
305 ParsingMode::DEFAULT,
306 QuirksMode::NoQuirks,
307 Default::default(),
308 None,
309 None,
310 Default::default(),
311 );
312 condition.eval(&context)
313 }
314
315 pub fn register_custom_property(
319 &mut self,
320 name: &str,
321 syntax: &str,
322 inherits: bool,
323 initial_value: Option<&str>,
324 ) -> RegisterCustomPropertyResult {
325 let url_data = self.url.url_extra_data();
326 let result =
327 self.stylist
328 .register_custom_property(&url_data, name, syntax, inherits, initial_value);
329 if matches!(result, RegisterCustomPropertyResult::SuccessfullyRegistered) {
330 self.stylist
331 .force_stylesheet_origins_dirty(OriginSet::all());
332 }
333 result
334 }
335
336 pub fn resolved_style_value(&self, node_id: NodeId, property_name: &str) -> String {
344 let Some(node) = self.get_node(node_id) else {
345 return String::new();
346 };
347 if !node.flags.is_in_document() {
349 return String::new();
350 }
351 let undisplayed_styles;
354 let stored_styles = node.primary_styles();
355 let styles: &ComputedValues = match &stored_styles {
356 Some(styles) => styles,
357 None => match self.resolve_undisplayed_style(node_id) {
358 Some(styles) => {
359 undisplayed_styles = styles;
360 &undisplayed_styles
361 }
362 None => return String::new(),
363 },
364 };
365
366 let display = styles.clone_display();
367 let is_non_atomic_inline = display.outside() == DisplayOutside::Inline
371 && display.inside() == DisplayInside::Flow
372 && !node.flags.is_inline_root()
373 && node.element_data().is_none_or(|data| {
374 let tag = &data.name.local;
375 !(is_replaced_element(tag)
376 || *tag == local_name!("input")
377 || *tag == local_name!("textarea")
378 || *tag == local_name!("button"))
379 });
380 let generates_box =
381 node.flags.is_in_document() && !display.is_none() && stored_styles.is_some();
382 let has_layout_box = generates_box && !is_non_atomic_inline;
383
384 let Ok(property_id) = PropertyId::parse_enabled_for_all_content(property_name) else {
385 return String::new();
386 };
387 let property_name = match property_id.as_shorthand() {
389 Err(PropertyDeclarationId::Longhand(longhand)) => {
390 longhand.to_physical(styles.writing_mode).name()
391 }
392 _ => property_name,
393 };
394
395 let zoom = styles.effective_zoom.value();
397 let format_used_px = |px: f32| format_px(px / zoom);
398
399 match property_name {
401 "margin" | "margin-block" | "margin-inline" | "padding" | "padding-block"
403 | "padding-inline" | "inset" | "inset-block" | "inset-inline"
404 if has_layout_box =>
405 {
406 if let Ok(shorthand) = property_id.as_shorthand() {
407 let sides: Vec<String> = shorthand
408 .longhands()
409 .map(|longhand| self.resolved_style_value(node_id, longhand.name()))
410 .collect();
411 return serialize_sides(&sides);
412 }
413 }
414 "grid-template-columns" | "grid-template-rows"
415 if display.inside() == DisplayInside::Grid =>
416 {
417 if let Some(info) =
418 node.element_data()
419 .and_then(|data| match &data.detailed_layout_info {
420 taffy::DetailedLayoutInfo::Grid(info) => Some(info),
421 _ => None,
422 })
423 {
424 return if property_name == "grid-template-columns" {
425 info.grid_template_columns(zoom)
426 } else {
427 info.grid_template_rows(zoom)
428 };
429 }
430 }
431 "grid-template" if display.inside() == DisplayInside::Grid => {
435 let pos_styles = styles.get_position();
436 let rows_are_none =
437 matches!(pos_styles.grid_template_rows, GridTemplateComponent::None);
438 let columns_are_none = matches!(
439 pos_styles.grid_template_columns,
440 GridTemplateComponent::None
441 );
442 let info = node
443 .element_data()
444 .and_then(|data| match &data.detailed_layout_info {
445 taffy::DetailedLayoutInfo::Grid(info) => Some(info),
446 _ => None,
447 });
448 if let Some(info) = info
449 && (rows_are_none || columns_are_none)
450 && matches!(pos_styles.grid_template_areas, GridTemplateAreas::None)
451 {
452 let rows = if rows_are_none {
453 info.grid_template_rows(zoom)
454 } else {
455 pos_styles.grid_template_rows.to_css_string()
456 };
457 let columns = if columns_are_none {
458 info.grid_template_columns(zoom)
459 } else {
460 pos_styles.grid_template_columns.to_css_string()
461 };
462 return format!("{rows} / {columns}");
463 }
464 }
465 "width" | "height" if has_layout_box => {
466 let layout = node.final_layout();
469 let border_box = styles.get_position().box_sizing == BoxSizing::BorderBox;
470 let size = if property_name == "width" {
471 if border_box {
472 layout.size.width
473 } else {
474 layout.size.width
475 - layout.border.left
476 - layout.border.right
477 - layout.padding.left
478 - layout.padding.right
479 }
480 } else if border_box {
481 layout.size.height
482 } else {
483 layout.size.height
484 - layout.border.top
485 - layout.border.bottom
486 - layout.padding.top
487 - layout.padding.bottom
488 };
489 return format_used_px(size.max(0.0));
490 }
491 "margin-top" | "margin-right" | "margin-bottom" | "margin-left" if has_layout_box => {
492 let layout = node.final_layout();
495 let margin = match property_name {
496 "margin-top" => layout.margin.top,
497 "margin-right" => layout.margin.right,
498 "margin-bottom" => layout.margin.bottom,
499 "margin-left" => layout.margin.left,
500 _ => unreachable!(),
501 };
502 return format_used_px(margin);
503 }
504 "padding-top" | "padding-right" | "padding-bottom" | "padding-left"
505 if has_layout_box =>
506 {
507 let layout = self.physical_unrounded_geometry(node_id).1;
510 let padding = match property_name {
511 "padding-top" => layout.padding.top,
512 "padding-right" => layout.padding.right,
513 "padding-bottom" => layout.padding.bottom,
514 "padding-left" => layout.padding.left,
515 _ => unreachable!(),
516 };
517 return format_used_px(padding);
518 }
519 "transform-origin" | "perspective-origin" if has_layout_box => {
520 let layout = self.physical_unrounded_geometry(node_id).1;
522 let resolve = |value: &LengthPercentage, basis: f32| {
523 format_used_px(value.resolve(CSSPixelLength::new(basis)).px())
524 };
525 let box_styles = styles.get_box();
526 return if property_name == "transform-origin" {
527 let origin = &box_styles.transform_origin;
528 let x = resolve(&origin.horizontal, layout.size.width);
529 let y = resolve(&origin.vertical, layout.size.height);
530 if origin.depth.px() == 0.0 {
531 format!("{x} {y}")
532 } else {
533 format!("{x} {y} {}", format_used_px(origin.depth.px()))
534 }
535 } else {
536 let origin = &box_styles.perspective_origin;
537 let x = resolve(&origin.horizontal, layout.size.width);
538 let y = resolve(&origin.vertical, layout.size.height);
539 format!("{x} {y}")
540 };
541 }
542 "top" | "right" | "bottom" | "left" if has_layout_box => {
543 let position = styles.clone_position();
544 let parent_layout = node
545 .containing_block()
546 .and_then(|id| self.get_node(id))
547 .map(|parent| *parent.final_layout());
548
549 match position {
550 Position::Relative => {
555 let (cb_width, cb_height) = parent_layout
556 .map(|pl| {
557 (
558 pl.size.width
559 - pl.border.left
560 - pl.border.right
561 - pl.padding.left
562 - pl.padding.right,
563 pl.size.height
564 - pl.border.top
565 - pl.border.bottom
566 - pl.padding.top
567 - pl.padding.bottom,
568 )
569 })
570 .unwrap_or((0.0, 0.0));
571 let pos_styles = styles.get_position();
572 let is_vertical = matches!(property_name, "top" | "bottom");
573 let basis = if is_vertical { cb_height } else { cb_width };
574 let (start, end) = if is_vertical {
575 (
576 resolve_inset(&pos_styles.top, basis),
577 resolve_inset(&pos_styles.bottom, basis),
578 )
579 } else {
580 (
581 resolve_inset(&pos_styles.left, basis),
582 resolve_inset(&pos_styles.right, basis),
583 )
584 };
585 let is_start = matches!(property_name, "top" | "left");
586 let used = match (start, end) {
587 (Some(start), Some(end)) => {
588 if is_start {
589 start
590 } else {
591 end
592 }
593 }
594 (Some(start), None) => {
595 if is_start {
596 start
597 } else {
598 -start
599 }
600 }
601 (None, Some(end)) => {
602 if is_start {
603 -end
604 } else {
605 end
606 }
607 }
608 (None, None) => 0.0,
609 };
610 return format_used_px(used);
611 }
612 Position::Absolute | Position::Fixed => {
618 if let Some(pl) = parent_layout {
619 let cb_width = pl.size.width - pl.border.left - pl.border.right;
620 let cb_height = pl.size.height - pl.border.top - pl.border.bottom;
621
622 let pos_styles = styles.get_position();
623 let (inset, basis) = match property_name {
624 "top" => (&pos_styles.top, cb_height),
625 "bottom" => (&pos_styles.bottom, cb_height),
626 "left" => (&pos_styles.left, cb_width),
627 "right" => (&pos_styles.right, cb_width),
628 _ => unreachable!(),
629 };
630 if let Some(value) = resolve_inset(inset, basis) {
631 return format_used_px(value);
632 }
633
634 let layout = node.final_layout();
635 let margin_box_top =
636 layout.location.y - layout.margin.top - pl.border.top;
637 let margin_box_left =
638 layout.location.x - layout.margin.left - pl.border.left;
639 let margin_box_width =
640 layout.margin.left + layout.size.width + layout.margin.right;
641 let margin_box_height =
642 layout.margin.top + layout.size.height + layout.margin.bottom;
643 let used = match property_name {
644 "top" => margin_box_top,
645 "bottom" => cb_height - margin_box_top - margin_box_height,
646 "left" => margin_box_left,
647 "right" => cb_width - margin_box_left - margin_box_width,
648 _ => unreachable!(),
649 };
650 return format_used_px(used);
651 }
652 }
653 _ => {}
655 }
656 }
657 "min-width" | "min-height" => {
661 let pos_styles = styles.get_position();
662 let is_auto = if property_name == "min-width" {
663 pos_styles.min_width.is_auto()
664 } else {
665 pos_styles.min_height.is_auto()
666 };
667 let has_aspect_ratio =
668 !matches!(pos_styles.aspect_ratio.ratio, PreferredRatio::None);
669 let preserves_auto =
670 generates_box && (has_aspect_ratio || self.is_flex_or_grid_item(node_id));
671 if is_auto && !preserves_auto {
672 return format_px(0.0);
673 }
674 }
675 "transform" if has_layout_box => {
676 let transform = &styles.get_box().transform;
677 if !transform.0.is_empty() {
678 let layout = node.final_layout();
681 let reference_box = euclid::Rect::new(
682 euclid::Point2D::new(CSSPixelLength::new(0.0), CSSPixelLength::new(0.0)),
683 euclid::Size2D::new(
684 CSSPixelLength::new(layout.size.width),
685 CSSPixelLength::new(layout.size.height),
686 ),
687 );
688 if let Ok((matrix, is_3d)) =
689 transform.to_transform_3d_matrix_f64(Some(&reference_box))
690 {
691 return format_matrix(&matrix, is_3d);
692 }
693 }
694 }
695 _ => {}
696 }
697
698 let css = match property_id.as_shorthand() {
700 Ok(shorthand) => serialize_resolved_shorthand(styles, shorthand),
702 Err(declaration_id) => styles.computed_value_to_string(declaration_id),
703 };
704 canonicalize_computed_alignment(property_name, css)
705 }
706
707 fn is_flex_or_grid_item(&self, node_id: NodeId) -> bool {
711 let mut parent_id = self.get_node(node_id).and_then(|node| node.parent);
712 while let Some(parent) = parent_id.and_then(|id| self.get_node(id)) {
713 let Some(parent_styles) = parent.primary_styles() else {
714 return false;
715 };
716 let display = parent_styles.clone_display();
717 if display.is_contents() {
718 parent_id = parent.parent;
719 continue;
720 }
721 return matches!(display.inside(), DisplayInside::Flex | DisplayInside::Grid);
722 }
723 false
724 }
725}
726
727fn serialize_sides(sides: &[String]) -> String {
730 let len = match sides {
731 [start, end] if start == end => 1,
732 [top, right, bottom, left] if right == left => {
733 if top != bottom {
734 3
735 } else if top != right {
736 2
737 } else {
738 1
739 }
740 }
741 _ => sides.len(),
742 };
743 sides[..len].join(" ")
744}
745
746fn serialize_resolved_shorthand(styles: &ComputedValues, shorthand: ShorthandId) -> String {
751 if shorthand == ShorthandId::All {
753 return String::new();
754 }
755
756 let declarations: Vec<PropertyDeclaration> = shorthand
757 .longhands()
758 .map(|longhand| {
759 let mut context = resolved::Context {
760 style: styles,
761 for_property: PropertyId::NonCustom(longhand.into()),
762 current_longhand: Some(longhand),
763 };
764 styles.computed_or_resolved_declaration(longhand, Some(&mut context))
765 })
766 .collect();
767 let declaration_refs: Vec<&PropertyDeclaration> = declarations.iter().collect();
768
769 let mut css = CssStringWriter::new();
770 let _ = shorthand.longhands_to_css(&declaration_refs, &mut css);
771 css
772}
773
774fn canonicalize_computed_alignment(property_name: &str, value: String) -> String {
780 const ALIGNMENT_PROPERTIES: [&str; 9] = [
781 "align-content",
782 "align-items",
783 "align-self",
784 "justify-content",
785 "justify-items",
786 "justify-self",
787 "place-content",
788 "place-items",
789 "place-self",
790 ];
791 if !ALIGNMENT_PROPERTIES.contains(&property_name) || !value.contains("flex-") {
792 return value;
793 }
794 value
795 .replace("flex-start", "flow-start")
796 .replace("flex-end", "flow-end")
797}