use cssparser::Parser;
use selectors::matching::QuirksMode;
use style::computed_values::box_sizing::T as BoxSizing;
use style::computed_values::position::T as Position;
use style::parser::ParserContext;
use style::properties::declaration_block::{Importance, parse_style_attribute};
use style::properties::{
ComputedValues, NonCustomPropertyId, PropertyDeclaration, PropertyDeclarationBlock,
PropertyDeclarationId, PropertyId, ShorthandId, SourcePropertyDeclaration,
parse_one_declaration_into,
};
use style::servo_arc::Arc as ServoArc;
use style::stylesheets::supports_rule::parse_condition_or_declaration;
use style::stylesheets::{CssRuleType, Origin, OriginSet, UrlExtraData};
use style::stylist::RegisterCustomPropertyResult;
use style::values::computed::length::CSSPixelLength;
use style::values::computed::{GridTemplateAreas, GridTemplateComponent, LengthPercentage};
use style::values::generics::position::{Inset as GenericInset, PreferredRatio};
use style::values::resolved;
use style::values::specified::box_::{DisplayInside, DisplayOutside};
use style_traits::{CssStringWriter, ParsingMode, ToCss};
use blitz_traits::node_id::NodeId;
use url::Url;
use crate::BaseDocument;
use crate::layout::replaced::is_replaced_element;
use crate::local_name;
fn format_px(px: f32) -> String {
CSSPixelLength::new(px).to_css_string()
}
fn resolve_inset<P>(val: &GenericInset<P, LengthPercentage>, basis: f32) -> Option<f32> {
match val {
GenericInset::LengthPercentage(lp) => Some(lp.resolve(CSSPixelLength::new(basis)).px()),
_ => None,
}
}
fn format_matrix_component(v: f64) -> String {
let rounded = (v * 1e6).round() / 1e6;
let rounded = if rounded == 0.0 { 0.0 } else { rounded };
format!("{rounded}")
}
fn format_matrix(m: &euclid::default::Transform3D<f64>, is_3d: bool) -> String {
let c = format_matrix_component;
if is_3d {
format!(
"matrix3d({}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {})",
c(m.m11),
c(m.m12),
c(m.m13),
c(m.m14),
c(m.m21),
c(m.m22),
c(m.m23),
c(m.m24),
c(m.m31),
c(m.m32),
c(m.m33),
c(m.m34),
c(m.m41),
c(m.m42),
c(m.m43),
c(m.m44)
)
} else {
format!(
"matrix({}, {}, {}, {}, {}, {})",
c(m.m11),
c(m.m12),
c(m.m21),
c(m.m22),
c(m.m41),
c(m.m42)
)
}
}
pub fn css_property_is_supported(name: &str) -> bool {
matches!(
PropertyId::parse_enabled_for_all_content(name),
Ok(property_id) if !matches!(property_id, PropertyId::Custom(_))
)
}
pub fn parse_transform_matrix(value: &str) -> Option<([f64; 16], bool)> {
use style::properties::longhands::transform;
let url_data = UrlExtraData(ServoArc::new(Url::parse("about:blank").unwrap()));
let context = ParserContext::new(
Origin::Author,
&url_data,
Some(CssRuleType::Style),
ParsingMode::DEFAULT,
QuirksMode::NoQuirks,
Default::default(),
None,
None,
Default::default(),
);
let mut parser = Parser::new(value);
let transform = parser
.parse_entirely(|t| transform::parse(&context, t))
.ok()?;
let (m, is_3d) = transform.to_transform_3d_matrix_f64(None).ok()?;
Some((
[
m.m11, m.m12, m.m13, m.m14, m.m21, m.m22, m.m23, m.m24, m.m31, m.m32, m.m33, m.m34,
m.m41, m.m42, m.m43, m.m44,
],
!is_3d,
))
}
pub fn resolved_style_property_names() -> &'static [&'static str] {
static NAMES: std::sync::OnceLock<Vec<&'static str>> = std::sync::OnceLock::new();
NAMES.get_or_init(|| {
let mut names: Vec<&'static str> = NonCustomPropertyId::iter()
.filter_map(|id| id.as_longhand())
.map(|longhand| longhand.name())
.filter(|name| PropertyId::parse_enabled_for_all_content(name).is_ok())
.collect();
names.sort_unstable();
names
})
}
impl BaseDocument {
pub fn css_declaration_is_valid(&self, property: &str, value: &str) -> bool {
let Ok(property_id) = PropertyId::parse_enabled_for_all_content(property) else {
return false;
};
let mut declarations = SourcePropertyDeclaration::default();
parse_one_declaration_into(
&mut declarations,
property_id,
value,
Origin::Author,
&self.url.url_extra_data(),
None,
ParsingMode::DEFAULT,
QuirksMode::NoQuirks,
CssRuleType::Style,
)
.is_ok()
}
fn parse_style_attr_block(&self, style_attr: &str) -> PropertyDeclarationBlock {
parse_style_attribute(
style_attr,
&self.url.url_extra_data(),
None,
QuirksMode::NoQuirks,
CssRuleType::Style,
)
}
pub fn style_attr_serialize(&self, style_attr: &str) -> String {
let block = self.parse_style_attr_block(style_attr);
let mut css = CssStringWriter::new();
let _ = block.to_css(&mut css);
css
}
pub fn style_attr_get_property(&self, style_attr: &str, property: &str) -> String {
let Ok(property_id) = PropertyId::parse_enabled_for_all_content(property) else {
return String::new();
};
let block = self.parse_style_attr_block(style_attr);
let mut css = CssStringWriter::new();
let _ = block.property_value_to_css(&property_id, &mut css);
css
}
pub fn style_attr_set_property(
&self,
style_attr: &str,
property: &str,
value: &str,
important: bool,
) -> Option<String> {
let property_id = PropertyId::parse_enabled_for_all_content(property).ok()?;
let mut block = self.parse_style_attr_block(style_attr);
if value.trim().is_empty() {
if let Some(first_declaration) = block.first_declaration_to_remove(&property_id) {
block.remove_property(&property_id, first_declaration);
}
} else {
let mut source = SourcePropertyDeclaration::default();
parse_one_declaration_into(
&mut source,
property_id,
value,
Origin::Author,
&self.url.url_extra_data(),
None,
ParsingMode::DEFAULT,
QuirksMode::NoQuirks,
CssRuleType::Style,
)
.ok()?;
let importance = if important {
Importance::Important
} else {
Importance::Normal
};
block.extend(source.drain(), importance);
}
let mut css = CssStringWriter::new();
let _ = block.to_css(&mut css);
Some(css)
}
pub fn style_attr_remove_property(
&self,
style_attr: &str,
property: &str,
) -> Option<(String, String)> {
let property_id = PropertyId::parse_enabled_for_all_content(property).ok()?;
let mut block = self.parse_style_attr_block(style_attr);
let mut removed_value = CssStringWriter::new();
let _ = block.property_value_to_css(&property_id, &mut removed_value);
if let Some(first_declaration) = block.first_declaration_to_remove(&property_id) {
block.remove_property(&property_id, first_declaration);
}
let mut css = CssStringWriter::new();
let _ = block.to_css(&mut css);
Some((css, removed_value))
}
pub fn css_supports_condition(&self, condition: &str) -> bool {
let mut parser = Parser::new(condition);
let Ok(condition) = parser.parse_entirely(parse_condition_or_declaration) else {
return false;
};
let url_data = self.url.url_extra_data();
let context = ParserContext::new(
Origin::Author,
&url_data,
Some(CssRuleType::Style),
ParsingMode::DEFAULT,
QuirksMode::NoQuirks,
Default::default(),
None,
None,
Default::default(),
);
condition.eval(&context)
}
pub fn register_custom_property(
&mut self,
name: &str,
syntax: &str,
inherits: bool,
initial_value: Option<&str>,
) -> RegisterCustomPropertyResult {
let url_data = self.url.url_extra_data();
let result =
self.stylist
.register_custom_property(&url_data, name, syntax, inherits, initial_value);
if matches!(result, RegisterCustomPropertyResult::SuccessfullyRegistered) {
self.stylist
.force_stylesheet_origins_dirty(OriginSet::all());
}
result
}
pub fn resolved_style_value(&self, node_id: NodeId, property_name: &str) -> String {
let Some(node) = self.get_node(node_id) else {
return String::new();
};
if !node.flags.is_in_document() {
return String::new();
}
let undisplayed_styles;
let stored_styles = node.primary_styles();
let styles: &ComputedValues = match &stored_styles {
Some(styles) => styles,
None => match self.resolve_undisplayed_style(node_id) {
Some(styles) => {
undisplayed_styles = styles;
&undisplayed_styles
}
None => return String::new(),
},
};
let display = styles.clone_display();
let is_non_atomic_inline = display.outside() == DisplayOutside::Inline
&& display.inside() == DisplayInside::Flow
&& !node.flags.is_inline_root()
&& node.element_data().is_none_or(|data| {
let tag = &data.name.local;
!(is_replaced_element(tag)
|| *tag == local_name!("input")
|| *tag == local_name!("textarea")
|| *tag == local_name!("button"))
});
let generates_box =
node.flags.is_in_document() && !display.is_none() && stored_styles.is_some();
let has_layout_box = generates_box && !is_non_atomic_inline;
let Ok(property_id) = PropertyId::parse_enabled_for_all_content(property_name) else {
return String::new();
};
let property_name = match property_id.as_shorthand() {
Err(PropertyDeclarationId::Longhand(longhand)) => {
longhand.to_physical(styles.writing_mode).name()
}
_ => property_name,
};
let zoom = styles.effective_zoom.value();
let format_used_px = |px: f32| format_px(px / zoom);
match property_name {
"margin" | "margin-block" | "margin-inline" | "padding" | "padding-block"
| "padding-inline" | "inset" | "inset-block" | "inset-inline"
if has_layout_box =>
{
if let Ok(shorthand) = property_id.as_shorthand() {
let sides: Vec<String> = shorthand
.longhands()
.map(|longhand| self.resolved_style_value(node_id, longhand.name()))
.collect();
return serialize_sides(&sides);
}
}
"grid-template-columns" | "grid-template-rows"
if display.inside() == DisplayInside::Grid =>
{
if let Some(info) =
node.element_data()
.and_then(|data| match &data.detailed_layout_info {
taffy::DetailedLayoutInfo::Grid(info) => Some(info),
_ => None,
})
{
return if property_name == "grid-template-columns" {
info.grid_template_columns(zoom)
} else {
info.grid_template_rows(zoom)
};
}
}
"grid-template" if display.inside() == DisplayInside::Grid => {
let pos_styles = styles.get_position();
let rows_are_none =
matches!(pos_styles.grid_template_rows, GridTemplateComponent::None);
let columns_are_none = matches!(
pos_styles.grid_template_columns,
GridTemplateComponent::None
);
let info = node
.element_data()
.and_then(|data| match &data.detailed_layout_info {
taffy::DetailedLayoutInfo::Grid(info) => Some(info),
_ => None,
});
if let Some(info) = info
&& (rows_are_none || columns_are_none)
&& matches!(pos_styles.grid_template_areas, GridTemplateAreas::None)
{
let rows = if rows_are_none {
info.grid_template_rows(zoom)
} else {
pos_styles.grid_template_rows.to_css_string()
};
let columns = if columns_are_none {
info.grid_template_columns(zoom)
} else {
pos_styles.grid_template_columns.to_css_string()
};
return format!("{rows} / {columns}");
}
}
"width" | "height" if has_layout_box => {
let layout = node.final_layout();
let border_box = styles.get_position().box_sizing == BoxSizing::BorderBox;
let size = if property_name == "width" {
if border_box {
layout.size.width
} else {
layout.size.width
- layout.border.left
- layout.border.right
- layout.padding.left
- layout.padding.right
}
} else if border_box {
layout.size.height
} else {
layout.size.height
- layout.border.top
- layout.border.bottom
- layout.padding.top
- layout.padding.bottom
};
return format_used_px(size.max(0.0));
}
"margin-top" | "margin-right" | "margin-bottom" | "margin-left" if has_layout_box => {
let layout = node.final_layout();
let margin = match property_name {
"margin-top" => layout.margin.top,
"margin-right" => layout.margin.right,
"margin-bottom" => layout.margin.bottom,
"margin-left" => layout.margin.left,
_ => unreachable!(),
};
return format_used_px(margin);
}
"padding-top" | "padding-right" | "padding-bottom" | "padding-left"
if has_layout_box =>
{
let layout = self.physical_unrounded_geometry(node_id).1;
let padding = match property_name {
"padding-top" => layout.padding.top,
"padding-right" => layout.padding.right,
"padding-bottom" => layout.padding.bottom,
"padding-left" => layout.padding.left,
_ => unreachable!(),
};
return format_used_px(padding);
}
"transform-origin" | "perspective-origin" if has_layout_box => {
let layout = self.physical_unrounded_geometry(node_id).1;
let resolve = |value: &LengthPercentage, basis: f32| {
format_used_px(value.resolve(CSSPixelLength::new(basis)).px())
};
let box_styles = styles.get_box();
return if property_name == "transform-origin" {
let origin = &box_styles.transform_origin;
let x = resolve(&origin.horizontal, layout.size.width);
let y = resolve(&origin.vertical, layout.size.height);
if origin.depth.px() == 0.0 {
format!("{x} {y}")
} else {
format!("{x} {y} {}", format_used_px(origin.depth.px()))
}
} else {
let origin = &box_styles.perspective_origin;
let x = resolve(&origin.horizontal, layout.size.width);
let y = resolve(&origin.vertical, layout.size.height);
format!("{x} {y}")
};
}
"top" | "right" | "bottom" | "left" if has_layout_box => {
let position = styles.clone_position();
let parent_layout = node
.containing_block()
.and_then(|id| self.get_node(id))
.map(|parent| *parent.final_layout());
match position {
Position::Relative => {
let (cb_width, cb_height) = parent_layout
.map(|pl| {
(
pl.size.width
- pl.border.left
- pl.border.right
- pl.padding.left
- pl.padding.right,
pl.size.height
- pl.border.top
- pl.border.bottom
- pl.padding.top
- pl.padding.bottom,
)
})
.unwrap_or((0.0, 0.0));
let pos_styles = styles.get_position();
let is_vertical = matches!(property_name, "top" | "bottom");
let basis = if is_vertical { cb_height } else { cb_width };
let (start, end) = if is_vertical {
(
resolve_inset(&pos_styles.top, basis),
resolve_inset(&pos_styles.bottom, basis),
)
} else {
(
resolve_inset(&pos_styles.left, basis),
resolve_inset(&pos_styles.right, basis),
)
};
let is_start = matches!(property_name, "top" | "left");
let used = match (start, end) {
(Some(start), Some(end)) => {
if is_start {
start
} else {
end
}
}
(Some(start), None) => {
if is_start {
start
} else {
-start
}
}
(None, Some(end)) => {
if is_start {
-end
} else {
end
}
}
(None, None) => 0.0,
};
return format_used_px(used);
}
Position::Absolute | Position::Fixed => {
if let Some(pl) = parent_layout {
let cb_width = pl.size.width - pl.border.left - pl.border.right;
let cb_height = pl.size.height - pl.border.top - pl.border.bottom;
let pos_styles = styles.get_position();
let (inset, basis) = match property_name {
"top" => (&pos_styles.top, cb_height),
"bottom" => (&pos_styles.bottom, cb_height),
"left" => (&pos_styles.left, cb_width),
"right" => (&pos_styles.right, cb_width),
_ => unreachable!(),
};
if let Some(value) = resolve_inset(inset, basis) {
return format_used_px(value);
}
let layout = node.final_layout();
let margin_box_top =
layout.location.y - layout.margin.top - pl.border.top;
let margin_box_left =
layout.location.x - layout.margin.left - pl.border.left;
let margin_box_width =
layout.margin.left + layout.size.width + layout.margin.right;
let margin_box_height =
layout.margin.top + layout.size.height + layout.margin.bottom;
let used = match property_name {
"top" => margin_box_top,
"bottom" => cb_height - margin_box_top - margin_box_height,
"left" => margin_box_left,
"right" => cb_width - margin_box_left - margin_box_width,
_ => unreachable!(),
};
return format_used_px(used);
}
}
_ => {}
}
}
"min-width" | "min-height" => {
let pos_styles = styles.get_position();
let is_auto = if property_name == "min-width" {
pos_styles.min_width.is_auto()
} else {
pos_styles.min_height.is_auto()
};
let has_aspect_ratio =
!matches!(pos_styles.aspect_ratio.ratio, PreferredRatio::None);
let preserves_auto =
generates_box && (has_aspect_ratio || self.is_flex_or_grid_item(node_id));
if is_auto && !preserves_auto {
return format_px(0.0);
}
}
"transform" if has_layout_box => {
let transform = &styles.get_box().transform;
if !transform.0.is_empty() {
let layout = node.final_layout();
let reference_box = euclid::Rect::new(
euclid::Point2D::new(CSSPixelLength::new(0.0), CSSPixelLength::new(0.0)),
euclid::Size2D::new(
CSSPixelLength::new(layout.size.width),
CSSPixelLength::new(layout.size.height),
),
);
if let Ok((matrix, is_3d)) =
transform.to_transform_3d_matrix_f64(Some(&reference_box))
{
return format_matrix(&matrix, is_3d);
}
}
}
_ => {}
}
let css = match property_id.as_shorthand() {
Ok(shorthand) => serialize_resolved_shorthand(styles, shorthand),
Err(declaration_id) => styles.computed_value_to_string(declaration_id),
};
canonicalize_computed_alignment(property_name, css)
}
fn is_flex_or_grid_item(&self, node_id: NodeId) -> bool {
let mut parent_id = self.get_node(node_id).and_then(|node| node.parent);
while let Some(parent) = parent_id.and_then(|id| self.get_node(id)) {
let Some(parent_styles) = parent.primary_styles() else {
return false;
};
let display = parent_styles.clone_display();
if display.is_contents() {
parent_id = parent.parent;
continue;
}
return matches!(display.inside(), DisplayInside::Flex | DisplayInside::Grid);
}
false
}
}
fn serialize_sides(sides: &[String]) -> String {
let len = match sides {
[start, end] if start == end => 1,
[top, right, bottom, left] if right == left => {
if top != bottom {
3
} else if top != right {
2
} else {
1
}
}
_ => sides.len(),
};
sides[..len].join(" ")
}
fn serialize_resolved_shorthand(styles: &ComputedValues, shorthand: ShorthandId) -> String {
if shorthand == ShorthandId::All {
return String::new();
}
let declarations: Vec<PropertyDeclaration> = shorthand
.longhands()
.map(|longhand| {
let mut context = resolved::Context {
style: styles,
for_property: PropertyId::NonCustom(longhand.into()),
current_longhand: Some(longhand),
};
styles.computed_or_resolved_declaration(longhand, Some(&mut context))
})
.collect();
let declaration_refs: Vec<&PropertyDeclaration> = declarations.iter().collect();
let mut css = CssStringWriter::new();
let _ = shorthand.longhands_to_css(&declaration_refs, &mut css);
css
}
fn canonicalize_computed_alignment(property_name: &str, value: String) -> String {
const ALIGNMENT_PROPERTIES: [&str; 9] = [
"align-content",
"align-items",
"align-self",
"justify-content",
"justify-items",
"justify-self",
"place-content",
"place-items",
"place-self",
];
if !ALIGNMENT_PROPERTIES.contains(&property_name) || !value.contains("flex-") {
return value;
}
value
.replace("flex-start", "flow-start")
.replace("flex-end", "flow-end")
}