///|
/// CSS Computed Values
/// Resolves cascaded values to computed values with inheritance
///|
/// Computed values context for resolving relative units
pub(all) struct ComputeContext {
/// Parent's computed style (for inheritance)
parent_style : @style.Style?
/// Root font size (for rem units)
root_font_size : Double
/// Current font size (for em units)
font_size : Double
/// Viewport width (for vw units)
viewport_width : Double
/// Viewport height (for vh units)
viewport_height : Double
/// CSS Custom Properties (variables) - inherited from parent and local
custom_properties : Map[String, String]
}
///|
let internal_effective_color_scheme_key = "--crater-effective-color-scheme"
///|
fn view_to_string(v : StringView) -> String {
let sb = StringBuilder::new()
sb.write_stringview(v)
sb.to_string()
}
///|
/// Allocation-free prefix test for the hot cascade/compute path.
/// MoonBit's `String::has_prefix` runs a Boyer-Moore-Horspool search that
/// allocates a skip table (~850 bytes) per call; comparing UTF-16 code units
/// directly avoids that. This runs per declaration (the custom-property `--`
/// test) for every element, so the per-call allocation showed up in profiling.
fn starts_with(s : String, prefix : String) -> Bool {
let pl = prefix.length()
if pl > s.length() {
return false
}
for i in 0.. Bool {
let pl = prefix.length()
if pl > s.length() {
return false
}
for i in 0.. ComputeContext {
{
parent_style: None,
root_font_size: 16.0,
font_size: 16.0,
viewport_width: 1920.0,
viewport_height: 1080.0,
custom_properties: {},
}
}
///|
fn ch_unit_ratio(ctx : ComputeContext) -> Double {
match ctx.parent_style {
Some(style) =>
if style.font_family.to_lower().contains("ahem") {
1.0
} else {
0.5
}
None => 0.5
}
}
///|
pub fn ComputeContext::with_parent(parent : @style.Style) -> ComputeContext {
{
parent_style: Some(parent),
root_font_size: 16.0,
font_size: 16.0,
viewport_width: 1920.0,
viewport_height: 1080.0,
custom_properties: {},
}
}
///|
/// Create context with parent style and inherited custom properties
pub fn ComputeContext::with_parent_and_vars(
parent : @style.Style,
parent_vars : Map[String, String],
) -> ComputeContext {
// Copy parent vars for inheritance (custom properties inherit by default)
let vars : Map[String, String] = {}
for k, v in parent_vars {
vars[k] = v
}
{
parent_style: Some(parent),
root_font_size: 16.0,
font_size: 16.0,
viewport_width: 1920.0,
viewport_height: 1080.0,
custom_properties: vars,
}
}
///|
/// Create context with custom viewport dimensions
pub fn ComputeContext::with_viewport(
viewport_width : Double,
viewport_height : Double,
) -> ComputeContext {
{
parent_style: None,
root_font_size: 16.0,
font_size: 16.0,
viewport_width,
viewport_height,
custom_properties: {},
}
}
///|
/// Mutable style builder for collecting computed values
priv struct StyleBuilder {
mut display : @types.Display
mut position : @types.Position
mut float : @types.CssFloat
mut clear : @types.Clear
mut box_sizing : @types.BoxSizing
mut overflow_x : @types.Overflow
mut overflow_y : @types.Overflow
mut scroll_snap_type : @style.ScrollSnapType
mut scroll_snap_align_x : @style.ScrollSnapAlign
mut scroll_snap_align_y : @style.ScrollSnapAlign
mut width : @types.Dimension
mut height : @types.Dimension
mut min_width : @types.Dimension
mut min_height : @types.Dimension
mut max_width : @types.Dimension
mut max_height : @types.Dimension
mut margin_top : @types.Dimension
mut margin_right : @types.Dimension
mut margin_bottom : @types.Dimension
mut margin_left : @types.Dimension
mut margin_trim : @style.MarginTrim
mut padding_top : @types.Dimension
mut padding_right : @types.Dimension
mut padding_bottom : @types.Dimension
mut padding_left : @types.Dimension
mut border_top : @types.Dimension
mut border_right : @types.Dimension
mut border_bottom : @types.Dimension
mut border_left : @types.Dimension
mut border_style_top : @style.BorderStyle
mut border_style_right : @style.BorderStyle
mut border_style_bottom : @style.BorderStyle
mut border_style_left : @style.BorderStyle
mut border_color_top : @types.Color
mut border_color_right : @types.Color
mut border_color_bottom : @types.Color
mut border_color_left : @types.Color
mut flex_direction : @types.FlexDirection
mut flex_wrap : @types.FlexWrap
mut justify_content : @types.Alignment
mut align_items : @types.Alignment
mut align_content : @types.Alignment
mut justify_content_unsafe : Bool
mut align_content_unsafe : Bool
mut align_self : @types.AlignSelf
mut justify_self : @types.AlignSelf
mut justify_items : @types.Alignment
mut flex_grow : Double
mut flex_shrink : Double
mut flex_basis : @types.Dimension
mut order : Int
mut row_gap : @types.Dimension
mut column_gap : @types.Dimension
mut column_gap_is_normal : Bool
mut column_count : Int?
mut column_width : @types.Dimension
mut column_fill : @style.ColumnFill
mut aspect_ratio : Double?
mut inset_top : @types.Dimension
mut inset_right : @types.Dimension
mut inset_bottom : @types.Dimension
mut inset_left : @types.Dimension
mut grid_auto_flow : @types.GridAutoFlow
mut grid_template_columns : Array[@types.TrackSizingFunction]
mut grid_template_rows : Array[@types.TrackSizingFunction]
mut grid_auto_rows : Array[@types.TrackSizingFunction]
mut grid_auto_columns : Array[@types.TrackSizingFunction]
// Named lines declared in grid-template-columns/rows (index = grid line)
mut grid_template_column_line_names : Array[Array[String]]
mut grid_template_row_line_names : Array[Array[String]]
// Axis kind: Explicit track list (default), subgrid, or masonry.
mut grid_template_columns_kind : @types.GridTemplateKind
mut grid_template_rows_kind : @types.GridTemplateKind
// Grid template areas (container)
mut grid_template_areas : Array[String]
// Grid item placement
mut grid_column_start : @types.GridPlacement
mut grid_column_end : @types.GridPlacement
mut grid_row_start : @types.GridPlacement
mut grid_row_end : @types.GridPlacement
// Grid area name (item)
mut grid_area : String?
// Font properties
mut font_size : Double
mut font_weight : Double
mut font_family : String
mut line_height : Double
// Text wrapping and direction
mut text_align : @style.TextAlign
mut white_space : @style.WhiteSpace
mut text_overflow : @style.TextOverflow
mut writing_mode : @style.WritingMode
mut direction : @style.Direction
// Vertical alignment for inline elements
mut vertical_align : @style.VerticalAlign
// Clip (legacy)
mut clip : @types.ClipRect
mut clip_path : @style.ClipPath
// Paint properties
mut visibility : @style.Visibility
mut pointer_events : @style.PointerEvents
mut z_index : @style.ZIndex
mut opacity : Double
// Color properties
mut color : @types.Color
mut background_color : @types.Color
mut background_image : @types.BackgroundImage
mut box_shadows : Array[@style.BoxShadow]
// Containment
mut contain : @style.Contain
// Fragmentation
mut break_before : @style.BreakBefore
mut break_after : @style.BreakBefore
mut break_inside : @style.BreakInside
mut column_span : @style.ColumnSpan
mut contain_intrinsic_inline_size : Double?
mut contain_intrinsic_block_size : Double?
// Transform
mut transform : @style.Transform
mut individual_transform : @style.Transform
// Animation / transition properties (parallel longhand lists).
mut animation_name : Array[String]
mut animation_duration : Array[Double]
mut animation_timing_function : Array[@types.Easing]
mut animation_delay : Array[Double]
mut animation_iteration_count : Array[@types.AnimationIterationCount]
mut animation_direction : Array[@types.AnimationDirection]
mut animation_fill_mode : Array[@types.AnimationFillMode]
mut animation_play_state : Array[@types.AnimationPlayState]
mut transition_property : Array[String]
mut transition_duration : Array[Double]
mut transition_timing_function : Array[@types.Easing]
mut transition_delay : Array[Double]
// CSS filter / backdrop-filter presence (not none). Layout uses this only
// to know that the element establishes a containing block for abspos.
mut has_filter : Bool
// Zoom (affects layout scaling)
mut zoom : Double
// Table properties
mut border_spacing : Double
mut border_spacing_vertical : Double
mut border_collapse : @style.BorderCollapse
mut table_layout : @style.TableLayout
mut caption_side : @style.CaptionSide
// Table cell properties (not mutable via CSS, set from HTML attributes)
rowspan : Int
colspan : Int
// Border radius
mut border_top_left_radius : Double
mut border_top_right_radius : Double
mut border_bottom_right_radius : Double
mut border_bottom_left_radius : Double
mut text_decoration_underline : Bool
mut text_decoration_line_through : Bool
mut text_decoration_overline : Bool
mut letter_spacing : Double
mut word_spacing : Double
}
///|
fn StyleBuilder::new() -> StyleBuilder {
{
display: @types.Display::Block,
position: @types.Position::Static,
float: @types.CssFloat::None,
clear: @types.Clear::None,
box_sizing: @types.BoxSizing::ContentBox,
overflow_x: @types.Overflow::Visible,
overflow_y: @types.Overflow::Visible,
scroll_snap_type: @style.ScrollSnapType::none(),
scroll_snap_align_x: @style.ScrollSnapAlign::None,
scroll_snap_align_y: @style.ScrollSnapAlign::None,
width: @types.Dimension::Auto,
height: @types.Dimension::Auto,
min_width: @types.Dimension::Auto,
min_height: @types.Dimension::Auto,
max_width: @types.Dimension::Auto,
max_height: @types.Dimension::Auto,
margin_top: @types.Dimension::Length(0.0),
margin_right: @types.Dimension::Length(0.0),
margin_bottom: @types.Dimension::Length(0.0),
margin_left: @types.Dimension::Length(0.0),
margin_trim: @style.MarginTrim::None,
padding_top: @types.Dimension::Length(0.0),
padding_right: @types.Dimension::Length(0.0),
padding_bottom: @types.Dimension::Length(0.0),
padding_left: @types.Dimension::Length(0.0),
border_top: @types.Dimension::Length(0.0),
border_right: @types.Dimension::Length(0.0),
border_bottom: @types.Dimension::Length(0.0),
border_left: @types.Dimension::Length(0.0),
border_style_top: @style.BorderStyle::None,
border_style_right: @style.BorderStyle::None,
border_style_bottom: @style.BorderStyle::None,
border_style_left: @style.BorderStyle::None,
border_color_top: @types.Color::transparent(),
border_color_right: @types.Color::transparent(),
border_color_bottom: @types.Color::transparent(),
border_color_left: @types.Color::transparent(),
flex_direction: @types.FlexDirection::Row,
flex_wrap: @types.FlexWrap::NoWrap,
justify_content: @types.Alignment::FlexStart,
align_items: @types.Alignment::Stretch,
align_content: @types.Alignment::Stretch,
justify_content_unsafe: false,
align_content_unsafe: false,
align_self: @types.AlignSelf::Auto,
justify_self: @types.AlignSelf::Auto,
justify_items: @types.Alignment::Stretch,
flex_grow: 0.0,
flex_shrink: 1.0,
flex_basis: @types.Dimension::Auto,
order: 0,
row_gap: @types.Dimension::Length(0.0),
column_gap: @types.Dimension::Length(0.0),
column_gap_is_normal: true,
column_count: None,
column_width: @types.Dimension::Auto,
column_fill: @style.ColumnFill::Balance,
aspect_ratio: None,
inset_top: @types.Dimension::Auto,
inset_right: @types.Dimension::Auto,
inset_bottom: @types.Dimension::Auto,
inset_left: @types.Dimension::Auto,
grid_auto_flow: @types.GridAutoFlow::Row,
grid_template_columns: [],
grid_template_rows: [],
grid_auto_rows: [],
grid_auto_columns: [],
grid_template_column_line_names: [],
grid_template_row_line_names: [],
grid_template_columns_kind: @types.Explicit,
grid_template_rows_kind: @types.Explicit,
grid_template_areas: [],
grid_column_start: @types.GridPlacement::Auto,
grid_column_end: @types.GridPlacement::Auto,
grid_row_start: @types.GridPlacement::Auto,
grid_row_end: @types.GridPlacement::Auto,
grid_area: None,
font_size: 16.0,
font_weight: 400.0,
font_family: "",
line_height: 16.0,
text_align: @style.TextAlign::Start,
white_space: @style.WhiteSpace::Normal,
text_overflow: @style.TextOverflow::Clip,
writing_mode: @style.WritingMode::HorizontalTb,
direction: @style.Direction::Ltr,
vertical_align: @style.VerticalAlign::Baseline,
clip: @types.ClipRect::Auto,
clip_path: @style.ClipPath::none(),
visibility: @style.Visibility::Visible,
pointer_events: @style.PointerEvents::Auto,
z_index: @style.ZIndex::Auto,
opacity: 1.0,
color: @types.Color::black(),
background_color: @types.Color::transparent(),
background_image: @types.None,
box_shadows: [],
contain: @style.Contain::none(),
break_before: @style.BreakBefore::Auto,
break_after: @style.BreakBefore::Auto,
break_inside: @style.BreakInside::Auto,
column_span: @style.ColumnSpan::None,
contain_intrinsic_inline_size: None,
contain_intrinsic_block_size: None,
transform: @style.Transform::none(),
individual_transform: @style.Transform::none(),
animation_name: [],
animation_duration: [],
animation_timing_function: [],
animation_delay: [],
animation_iteration_count: [],
animation_direction: [],
animation_fill_mode: [],
animation_play_state: [],
transition_property: [],
transition_duration: [],
transition_timing_function: [],
transition_delay: [],
has_filter: false,
zoom: 1.0,
border_spacing: 0.0,
border_spacing_vertical: 0.0,
border_collapse: @style.BorderCollapse::Separate,
table_layout: @style.TableLayout::Auto,
caption_side: @style.CaptionSide::Top,
rowspan: 1,
colspan: 1,
border_top_left_radius: 0.0,
border_top_right_radius: 0.0,
border_bottom_right_radius: 0.0,
border_bottom_left_radius: 0.0,
text_decoration_underline: false,
text_decoration_line_through: false,
text_decoration_overline: false,
letter_spacing: 0.0,
word_spacing: 0.0,
}
}
///|
/// Create a StyleBuilder from an existing Style
fn StyleBuilder::from_style(style : @style.Style) -> StyleBuilder {
{
display: style.display,
position: style.position,
float: style.float,
clear: style.clear,
box_sizing: style.box_sizing,
overflow_x: style.overflow_x,
overflow_y: style.overflow_y,
scroll_snap_type: style.scroll_snap_type,
scroll_snap_align_x: style.scroll_snap_align_x,
scroll_snap_align_y: style.scroll_snap_align_y,
width: style.width,
height: style.height,
min_width: style.min_width,
min_height: style.min_height,
max_width: style.max_width,
max_height: style.max_height,
margin_top: style.margin.top,
margin_right: style.margin.right,
margin_bottom: style.margin.bottom,
margin_left: style.margin.left,
margin_trim: style.margin_trim,
padding_top: style.padding.top,
padding_right: style.padding.right,
padding_bottom: style.padding.bottom,
padding_left: style.padding.left,
border_top: style.border.top,
border_right: style.border.right,
border_bottom: style.border.bottom,
border_left: style.border.left,
border_style_top: style.border_style.top,
border_style_right: style.border_style.right,
border_style_bottom: style.border_style.bottom,
border_style_left: style.border_style.left,
border_color_top: style.border_color.top,
border_color_right: style.border_color.right,
border_color_bottom: style.border_color.bottom,
border_color_left: style.border_color.left,
flex_direction: style.flex_direction,
flex_wrap: style.flex_wrap,
justify_content: style.justify_content,
align_items: style.align_items,
align_content: style.align_content,
justify_content_unsafe: style.justify_content_unsafe,
align_content_unsafe: style.align_content_unsafe,
align_self: style.align_self,
justify_self: style.justify_self,
justify_items: style.justify_items,
flex_grow: style.flex_grow,
flex_shrink: style.flex_shrink,
flex_basis: style.flex_basis,
order: style.order,
row_gap: style.row_gap,
column_gap: style.column_gap,
column_gap_is_normal: style.column_gap_is_normal,
column_count: style.column_count,
column_width: style.column_width,
column_fill: style.column_fill,
aspect_ratio: style.aspect_ratio,
inset_top: style.inset.top,
inset_right: style.inset.right,
inset_bottom: style.inset.bottom,
inset_left: style.inset.left,
grid_auto_flow: style.grid_auto_flow,
grid_template_columns: style.grid_template_columns,
grid_template_rows: style.grid_template_rows,
grid_auto_rows: style.grid_auto_rows,
grid_auto_columns: style.grid_auto_columns,
grid_template_column_line_names: style.grid_template_column_line_names,
grid_template_row_line_names: style.grid_template_row_line_names,
grid_template_columns_kind: style.grid_template_columns_kind,
grid_template_rows_kind: style.grid_template_rows_kind,
grid_template_areas: style.grid_template_areas,
grid_column_start: style.grid_column.start,
grid_column_end: style.grid_column.end,
grid_row_start: style.grid_row.start,
grid_row_end: style.grid_row.end,
grid_area: style.grid_area,
font_size: style.font_size,
font_weight: style.font_weight,
font_family: style.font_family,
line_height: style.line_height,
text_align: style.text_align,
white_space: style.white_space,
text_overflow: style.text_overflow,
writing_mode: style.writing_mode,
direction: style.direction,
vertical_align: style.vertical_align,
clip: style.clip,
clip_path: style.clip_path,
visibility: style.visibility,
pointer_events: style.pointer_events,
z_index: style.z_index,
opacity: style.opacity,
color: style.color,
background_color: style.background_color,
background_image: style.background_image,
box_shadows: style.box_shadows,
contain: style.contain,
break_before: style.break_before,
break_after: style.break_after,
break_inside: style.break_inside,
column_span: style.column_span,
contain_intrinsic_inline_size: style.contain_intrinsic_inline_size,
contain_intrinsic_block_size: style.contain_intrinsic_block_size,
transform: style.transform,
individual_transform: style.individual_transform,
animation_name: style.animation_name,
animation_duration: style.animation_duration,
animation_timing_function: style.animation_timing_function,
animation_delay: style.animation_delay,
animation_iteration_count: style.animation_iteration_count,
animation_direction: style.animation_direction,
animation_fill_mode: style.animation_fill_mode,
animation_play_state: style.animation_play_state,
transition_property: style.transition_property,
transition_duration: style.transition_duration,
transition_timing_function: style.transition_timing_function,
transition_delay: style.transition_delay,
has_filter: style.has_filter,
zoom: style.zoom,
border_spacing: style.border_spacing,
border_spacing_vertical: style.border_spacing_vertical,
border_collapse: style.border_collapse,
table_layout: style.table_layout,
caption_side: style.caption_side,
rowspan: style.rowspan,
colspan: style.colspan,
border_top_left_radius: style.border_top_left_radius,
border_top_right_radius: style.border_top_right_radius,
border_bottom_right_radius: style.border_bottom_right_radius,
border_bottom_left_radius: style.border_bottom_left_radius,
text_decoration_underline: style.text_decoration_underline,
text_decoration_line_through: style.text_decoration_line_through,
text_decoration_overline: style.text_decoration_overline,
letter_spacing: style.letter_spacing,
word_spacing: style.word_spacing,
}
}
///|
fn StyleBuilder::build(self : StyleBuilder) -> @style.Style {
{
display: self.display,
position: self.position,
float: self.float,
clear: self.clear,
box_sizing: self.box_sizing,
overflow_x: self.overflow_x,
overflow_y: self.overflow_y,
scroll_snap_type: self.scroll_snap_type,
scroll_snap_align_x: self.scroll_snap_align_x,
scroll_snap_align_y: self.scroll_snap_align_y,
width: self.width,
height: self.height,
min_width: self.min_width,
min_height: self.min_height,
max_width: self.max_width,
max_height: self.max_height,
margin: {
top: self.margin_top,
right: self.margin_right,
bottom: self.margin_bottom,
left: self.margin_left,
},
margin_trim: self.margin_trim,
padding: {
top: self.padding_top,
right: self.padding_right,
bottom: self.padding_bottom,
left: self.padding_left,
},
border: {
top: self.border_top,
right: self.border_right,
bottom: self.border_bottom,
left: self.border_left,
},
border_style: {
top: self.border_style_top,
right: self.border_style_right,
bottom: self.border_style_bottom,
left: self.border_style_left,
},
border_color: {
top: self.border_color_top,
right: self.border_color_right,
bottom: self.border_color_bottom,
left: self.border_color_left,
},
flex_direction: self.flex_direction,
flex_wrap: self.flex_wrap,
justify_content: self.justify_content,
align_items: self.align_items,
align_content: self.align_content,
justify_content_unsafe: self.justify_content_unsafe,
align_content_unsafe: self.align_content_unsafe,
align_self: self.align_self,
flex_grow: self.flex_grow,
flex_shrink: self.flex_shrink,
flex_basis: self.flex_basis,
order: self.order,
row_gap: self.row_gap,
column_gap: self.column_gap,
column_gap_is_normal: self.column_gap_is_normal,
column_count: self.column_count,
column_width: self.column_width,
column_fill: self.column_fill,
aspect_ratio: self.aspect_ratio,
inset: {
top: self.inset_top,
right: self.inset_right,
bottom: self.inset_bottom,
left: self.inset_left,
},
grid_template_rows: self.grid_template_rows,
grid_template_columns: self.grid_template_columns,
grid_auto_rows: self.grid_auto_rows,
grid_auto_columns: self.grid_auto_columns,
grid_auto_flow: self.grid_auto_flow,
grid_template_column_line_names: self.grid_template_column_line_names,
grid_template_row_line_names: self.grid_template_row_line_names,
grid_template_columns_kind: self.grid_template_columns_kind,
grid_template_rows_kind: self.grid_template_rows_kind,
grid_template_areas: self.grid_template_areas,
grid_row: { start: self.grid_row_start, end: self.grid_row_end },
grid_column: { start: self.grid_column_start, end: self.grid_column_end },
grid_area: self.grid_area,
justify_items: self.justify_items,
justify_self: self.justify_self,
font_size: self.font_size,
font_weight: self.font_weight,
font_family: self.font_family,
line_height: self.line_height,
text_align: self.text_align,
white_space: self.white_space,
text_overflow: self.text_overflow,
writing_mode: self.writing_mode,
direction: self.direction,
vertical_align: self.vertical_align,
clip: self.clip,
clip_path: self.clip_path,
visibility: self.visibility,
pointer_events: self.pointer_events,
z_index: self.z_index,
opacity: self.opacity,
color: self.color,
background_color: self.background_color,
background_image: self.background_image,
box_shadows: self.box_shadows,
contain: self.contain,
break_before: self.break_before,
break_after: self.break_after,
break_inside: self.break_inside,
column_span: self.column_span,
contain_intrinsic_inline_size: self.contain_intrinsic_inline_size,
contain_intrinsic_block_size: self.contain_intrinsic_block_size,
transform: self.transform,
individual_transform: self.individual_transform,
animation_name: self.animation_name,
animation_duration: self.animation_duration,
animation_timing_function: self.animation_timing_function,
animation_delay: self.animation_delay,
animation_iteration_count: self.animation_iteration_count,
animation_direction: self.animation_direction,
animation_fill_mode: self.animation_fill_mode,
animation_play_state: self.animation_play_state,
transition_property: self.transition_property,
transition_duration: self.transition_duration,
transition_timing_function: self.transition_timing_function,
transition_delay: self.transition_delay,
has_filter: self.has_filter,
zoom: self.zoom,
border_spacing: self.border_spacing,
border_spacing_vertical: self.border_spacing_vertical,
border_collapse: self.border_collapse,
table_layout: self.table_layout,
caption_side: self.caption_side,
rowspan: self.rowspan,
colspan: self.colspan,
border_top_left_radius: self.border_top_left_radius,
border_top_right_radius: self.border_top_right_radius,
border_bottom_right_radius: self.border_bottom_right_radius,
border_bottom_left_radius: self.border_bottom_left_radius,
text_decoration_underline: self.text_decoration_underline,
text_decoration_line_through: self.text_decoration_line_through,
text_decoration_overline: self.text_decoration_overline,
letter_spacing: self.letter_spacing,
word_spacing: self.word_spacing,
}
}
///|
/// Resolve a CSS keyword value to actual value
fn resolve_keyword(
property : String,
value : @cascade.PropertyValue,
ctx : ComputeContext,
) -> String {
match value {
@cascade.PropertyValue::Value(v) => v
@cascade.PropertyValue::Inherit =>
// Get value from parent, or use initial if no parent
get_inherited_value(property, ctx)
@cascade.PropertyValue::Initial => initial_value(property)
@cascade.PropertyValue::Unset =>
// For inherited properties, acts like inherit
// For non-inherited properties, acts like initial
if is_inherited(property) {
get_inherited_value(property, ctx)
} else {
initial_value(property)
}
@cascade.PropertyValue::Revert =>
// Revert to user-agent stylesheet (we use initial as fallback)
initial_value(property)
@cascade.PropertyValue::RevertLayer =>
// Revert to previous cascade layer (we use initial as fallback)
initial_value(property)
}
}
///|
/// Get inherited value from parent or initial value
fn get_inherited_value(property : String, ctx : ComputeContext) -> String {
match ctx.parent_style {
Some(parent) => get_style_value_as_string(property, parent)
None => initial_value(property)
}
}
///|
/// Get a style property value as string representation
fn get_style_value_as_string(property : String, style : @style.Style) -> String {
match property {
"display" =>
match style.display {
@types.Display::Block => "block"
@types.Display::Inline => "inline"
@types.Display::InlineBlock => "inline-block"
@types.Display::Flex => "flex"
@types.Display::InlineFlex => "inline-flex"
@types.Display::Grid => "grid"
@types.Display::InlineGrid => "inline-grid"
@types.Display::Table => "table"
@types.Display::InlineTable => "inline-table"
@types.Display::TableRow => "table-row"
@types.Display::TableCell => "table-cell"
@types.Display::TableCaption => "table-caption"
@types.Display::TableRowGroup => "table-row-group"
@types.Display::TableHeaderGroup => "table-header-group"
@types.Display::TableFooterGroup => "table-footer-group"
@types.Display::TableColumn => "table-column"
@types.Display::TableColumnGroup => "table-column-group"
@types.Display::None => "none"
@types.Display::Contents => "contents"
@types.Display::FlowRoot => "flow-root"
}
"position" =>
match style.position {
@types.Position::Static => "static"
@types.Position::Relative => "relative"
@types.Position::Absolute => "absolute"
@types.Position::Fixed => "fixed"
}
"flex-direction" =>
match style.flex_direction {
@types.FlexDirection::Row => "row"
@types.FlexDirection::RowReverse => "row-reverse"
@types.FlexDirection::Column => "column"
@types.FlexDirection::ColumnReverse => "column-reverse"
}
"flex-wrap" =>
match style.flex_wrap {
@types.FlexWrap::NoWrap => "nowrap"
@types.FlexWrap::Wrap => "wrap"
@types.FlexWrap::WrapReverse => "wrap-reverse"
}
"width" => dimension_to_string(style.width)
"height" => dimension_to_string(style.height)
"min-width" => dimension_to_string(style.min_width)
"min-height" => dimension_to_string(style.min_height)
"max-width" => dimension_to_string(style.max_width)
"max-height" => dimension_to_string(style.max_height)
"margin-top" => dimension_to_string(style.margin.top)
"margin-right" => dimension_to_string(style.margin.right)
"margin-bottom" => dimension_to_string(style.margin.bottom)
"margin-left" => dimension_to_string(style.margin.left)
"margin-trim" =>
match style.margin_trim {
@style.MarginTrim::None => "none"
@style.MarginTrim::BlockStart => "block-start"
@style.MarginTrim::BlockEnd => "block-end"
@style.MarginTrim::Block => "block"
@style.MarginTrim::InlineStart => "inline-start"
@style.MarginTrim::InlineEnd => "inline-end"
@style.MarginTrim::Inline => "inline"
}
"padding-top" => dimension_to_string(style.padding.top)
"padding-right" => dimension_to_string(style.padding.right)
"padding-bottom" => dimension_to_string(style.padding.bottom)
"padding-left" => dimension_to_string(style.padding.left)
"column-count" =>
match style.column_count {
Some(v) => v.to_string()
None => "auto"
}
"column-width" => dimension_to_string(style.column_width)
"column-fill" =>
match style.column_fill {
@style.ColumnFill::Balance => "balance"
@style.ColumnFill::Auto => "auto"
}
"break-before" =>
match style.break_before {
@style.BreakBefore::Auto => "auto"
@style.BreakBefore::Column => "column"
@style.BreakBefore::Avoid => "avoid"
}
"break-after" =>
match style.break_after {
@style.BreakBefore::Auto => "auto"
@style.BreakBefore::Column => "column"
@style.BreakBefore::Avoid => "avoid"
}
"break-inside" | "page-break-inside" =>
match style.break_inside {
@style.BreakInside::Auto => "auto"
@style.BreakInside::Avoid => "avoid"
}
"column-span" =>
match style.column_span {
@style.ColumnSpan::None => "none"
@style.ColumnSpan::All => "all"
}
"flex-grow" => style.flex_grow.to_string()
"flex-shrink" => style.flex_shrink.to_string()
"flex-basis" => dimension_to_string(style.flex_basis)
"color" => style.color.to_hex()
"background-color" => style.background_color.to_hex()
"caption-side" =>
match style.caption_side {
@style.CaptionSide::Top => "top"
@style.CaptionSide::Bottom => "bottom"
}
"border-collapse" =>
match style.border_collapse {
@style.BorderCollapse::Separate => "separate"
@style.BorderCollapse::Collapse => "collapse"
}
"border-spacing" =>
if style.border_spacing == style.border_spacing_vertical {
style.border_spacing.to_string() + "px"
} else {
style.border_spacing.to_string() +
"px " +
style.border_spacing_vertical.to_string() +
"px"
}
"writing-mode" =>
match style.writing_mode {
@style.WritingMode::HorizontalTb => "horizontal-tb"
@style.WritingMode::VerticalRl => "vertical-rl"
@style.WritingMode::VerticalLr => "vertical-lr"
}
"direction" =>
match style.direction {
@style.Direction::Ltr => "ltr"
@style.Direction::Rtl => "rtl"
}
"font-size" => style.font_size.to_string() + "px"
"font-weight" => style.font_weight.to_string()
"line-height" => style.line_height.to_string() + "px"
"text-decoration" | "text-decoration-line" =>
text_decoration_to_string(
style.text_decoration_underline,
style.text_decoration_line_through,
style.text_decoration_overline,
)
"text-overflow" =>
match style.text_overflow {
@style.TextOverflow::Clip => "clip"
@style.TextOverflow::Ellipsis => "ellipsis"
}
"text-align" =>
match style.text_align {
@style.TextAlign::Start => "start"
@style.TextAlign::End => "end"
@style.TextAlign::Left => "left"
@style.TextAlign::Right => "right"
@style.TextAlign::Center => "center"
@style.TextAlign::Justify => "justify"
}
"white-space" =>
match style.white_space {
@style.WhiteSpace::Normal => "normal"
@style.WhiteSpace::Nowrap => "nowrap"
@style.WhiteSpace::Pre => "pre"
@style.WhiteSpace::PreWrap => "pre-wrap"
@style.WhiteSpace::PreLine => "pre-line"
}
"pointer-events" =>
match style.pointer_events {
@style.PointerEvents::Auto => "auto"
@style.PointerEvents::None => "none"
}
"contain-intrinsic-inline-size" =>
match style.contain_intrinsic_inline_size {
Some(v) => v.to_string() + "px"
None => "none"
}
"contain-intrinsic-block-size" =>
match style.contain_intrinsic_block_size {
Some(v) => v.to_string() + "px"
None => "none"
}
_ => initial_value(property)
}
}
///|
/// Convert dimension to string
fn dimension_to_string(dim : @types.Dimension) -> String {
match dim {
@types.Dimension::Auto => "auto"
@types.Dimension::Length(n) =>
if n == 0.0 {
"0"
} else {
n.to_string() + "px"
}
@types.Dimension::Percent(n) => (n * 100.0).to_string() + "%"
@types.Dimension::MinContent => "min-content"
@types.Dimension::MaxContent => "max-content"
@types.Dimension::FitContent(n) => "fit-content(" + n.to_string() + "px)"
@types.Dimension::Calc(px, pct) => {
let pct_part = (pct * 100.0).to_string() + "%"
if px < 0.0 {
"calc(" + pct_part + " - " + (-px).to_string() + "px)"
} else {
"calc(" + pct_part + " + " + px.to_string() + "px)"
}
}
@types.Dimension::MathFn(op, args) => {
let name = match op {
@types.MathOp::Min => "min("
@types.MathOp::Max => "max("
@types.MathOp::Clamp => "clamp("
}
let parts = args.map(fn(a) { math_arg_to_string(a.0, a.1) })
name + parts.join(", ") + ")"
}
}
}
///|
/// Serialize a single min()/max()/clamp() argument from its linear (px, pct)
/// form back to CSS, mirroring dimension_to_string's length/percent/calc tiers.
fn math_arg_to_string(px : Double, pct : Double) -> String {
if pct.abs() < 0.0001 {
if px == 0.0 {
"0"
} else {
px.to_string() + "px"
}
} else if px.abs() < 0.0001 {
(pct * 100.0).to_string() + "%"
} else {
let pct_part = (pct * 100.0).to_string() + "%"
if px < 0.0 {
"calc(" + pct_part + " - " + (-px).to_string() + "px)"
} else {
"calc(" + pct_part + " + " + px.to_string() + "px)"
}
}
}
///|
fn text_decoration_to_string(
underline : Bool,
line_through : Bool,
overline : Bool,
) -> String {
let values : Array[String] = []
if underline {
values.push("underline")
}
if line_through {
values.push("line-through")
}
if overline {
values.push("overline")
}
if values.is_empty() {
"none"
} else {
let buf = StringBuilder::new()
for i, value in values {
if i > 0 {
buf.write_string(" ")
}
buf.write_string(value)
}
buf.to_string()
}
}
///|
/// Compute a Style from cascaded values
pub fn compute(
cascaded : @cascade.CascadedValues,
ctx : ComputeContext,
) -> @style.Style {
let builder = StyleBuilder::new()
// First pass: collect custom properties (--*)
// Custom properties are stored in the context for var() resolution
cascaded.each(fn(prop, decl) {
if starts_with(prop, "--") {
let value = resolve_keyword(prop, decl.value, ctx)
// Store custom property in context
ctx.custom_properties[prop] = value
}
})
seed_effective_color_scheme(cascaded, ctx)
seed_direction(builder, cascaded, ctx)
// Seed writing-mode before applying logical properties.
// Logical sizing must follow the element's effective writing mode.
seed_writing_mode(builder, cascaded, ctx)
// Seed font metrics before resolving em/ch/ex units in other properties.
let font_ctx = seed_font_metrics(builder, cascaded, ctx)
// Second pass: apply regular properties with var() resolution
for item in regular_declarations_in_source_order(cascaded) {
let (prop, decl) = item
let value = resolve_keyword(prop, decl.value, font_ctx)
apply_property(builder, prop, value, font_ctx)
}
// Handle inheritance for inherited properties
apply_inheritance(builder, cascaded, ctx)
builder.build()
}
///|
/// Compute a Style and return updated custom properties
/// Used when building a style tree to pass custom properties to children
pub fn compute_with_vars(
cascaded : @cascade.CascadedValues,
ctx : ComputeContext,
) -> (@style.Style, Map[String, String]) {
let builder = StyleBuilder::new()
// First pass: collect custom properties (--*)
cascaded.each(fn(prop, decl) {
if starts_with(prop, "--") {
let value = resolve_keyword(prop, decl.value, ctx)
ctx.custom_properties[prop] = value
}
})
seed_effective_color_scheme(cascaded, ctx)
// Seed writing-mode before applying logical properties.
seed_direction(builder, cascaded, ctx)
seed_writing_mode(builder, cascaded, ctx)
let font_ctx = seed_font_metrics(builder, cascaded, ctx)
// Second pass: apply regular properties
for item in regular_declarations_in_source_order(cascaded) {
let (prop, decl) = item
let value = resolve_keyword(prop, decl.value, font_ctx)
apply_property(builder, prop, value, font_ctx)
}
// Handle inheritance
apply_inheritance(builder, cascaded, ctx)
// Return both the style and the custom properties for children
(builder.build(), ctx.custom_properties)
}
///|
fn seed_effective_color_scheme(
cascaded : @cascade.CascadedValues,
ctx : ComputeContext,
) -> Unit {
let inherited = match
ctx.custom_properties.get(internal_effective_color_scheme_key) {
Some(value) => value
None => "light"
}
let mut scheme = inherited
match cascaded.get_value("color-scheme") {
Some(raw_value) => {
let resolved = if raw_value.contains("var(") {
resolve_all_vars(raw_value, ctx)
} else {
raw_value
}
match first_color_scheme_keyword(resolved) {
Some(value) => scheme = value
None => ()
}
}
None => ()
}
ctx.custom_properties[internal_effective_color_scheme_key] = scheme
}
///|
fn first_color_scheme_keyword(value : String) -> String? {
let normalized = value.to_lower()
let tokens = normalized.split(" ")
for token in tokens {
let t = token.to_owned().trim()
if t == "light" || t == "dark" {
return Some(t.to_owned())
}
}
None
}
///|
fn seed_direction(
builder : StyleBuilder,
cascaded : @cascade.CascadedValues,
ctx : ComputeContext,
) -> Unit {
match ctx.parent_style {
Some(parent_style) => builder.direction = parent_style.direction
None => ()
}
match cascaded.get("direction") {
Some(decl) => {
let value = resolve_keyword("direction", decl.value, ctx)
builder.direction = parse_direction(value)
}
None => ()
}
}
///|
/// Seed writing-mode on the builder before logical properties are applied.
/// Uses inherited writing-mode as default, then overrides with explicit declaration.
fn seed_writing_mode(
builder : StyleBuilder,
cascaded : @cascade.CascadedValues,
ctx : ComputeContext,
) -> Unit {
match ctx.parent_style {
Some(parent_style) => builder.writing_mode = parent_style.writing_mode
None => ()
}
match cascaded.get("writing-mode") {
Some(decl) => {
let value = resolve_keyword("writing-mode", decl.value, ctx)
builder.writing_mode = parse_writing_mode(value)
}
None => ()
}
}
///|
fn is_preseeded_font_property(property : String) -> Bool {
property == "font" ||
property == "font-size" ||
property == "line-height" ||
property == "font-weight" ||
property == "font-family"
}
///|
/// Return regular declarations in CSS source order. CascadedValues is a map,
/// so applying it directly loses relative shorthand/longhand order.
fn regular_declarations_in_source_order(
cascaded : @cascade.CascadedValues,
) -> Array[(String, @cascade.Declaration)] {
let declarations : Array[(String, @cascade.Declaration)] = []
cascaded.each(fn(prop, decl) {
if !starts_with(prop, "--") && !is_preseeded_font_property(prop) {
declarations.push((prop, decl))
}
})
declarations.sort_by(fn(a, b) { a.1.source_order - b.1.source_order })
declarations
}
///|
/// Seed font-size/line-height ahead of general property application so
/// em/ch/ex units in width/height and gaps resolve against the element's font.
fn seed_font_metrics(
builder : StyleBuilder,
cascaded : @cascade.CascadedValues,
ctx : ComputeContext,
) -> ComputeContext {
let mut seeded_font_size = ctx.font_size
let mut seeded_line_height = ctx.font_size * 1.2 // CSS initial: normal ≈ 1.2
let mut seeded_font_weight = match ctx.parent_style {
Some(parent_style) => parent_style.font_weight
None => 400.0
}
let mut seeded_font_family = match ctx.parent_style {
Some(parent_style) => parent_style.font_family
None => ""
}
let mut font_decl : @cascade.Declaration? = None
let mut font_size_decl : @cascade.Declaration? = None
let mut line_height_decl : @cascade.Declaration? = None
let mut font_weight_decl : @cascade.Declaration? = None
let mut font_family_decl : @cascade.Declaration? = None
match cascaded.get("font") {
Some(decl) => font_decl = Some(decl)
None => ()
}
match cascaded.get("font-size") {
Some(decl) => font_size_decl = Some(decl)
None => ()
}
match cascaded.get("line-height") {
Some(decl) => line_height_decl = Some(decl)
None => ()
}
match cascaded.get("font-weight") {
Some(decl) => font_weight_decl = Some(decl)
None => ()
}
match cascaded.get("font-family") {
Some(decl) => font_family_decl = Some(decl)
None => ()
}
for idx = 0; idx < 5; idx = idx + 1 {
let mut next_prop = ""
let mut next_decl : @cascade.Declaration? = None
let mut next_order = 2147483647
match font_decl {
Some(decl) =>
if decl.source_order < next_order {
next_prop = "font"
next_decl = Some(decl)
next_order = decl.source_order
}
None => ()
}
match font_size_decl {
Some(decl) =>
if decl.source_order < next_order {
next_prop = "font-size"
next_decl = Some(decl)
next_order = decl.source_order
}
None => ()
}
match line_height_decl {
Some(decl) =>
if decl.source_order < next_order {
next_prop = "line-height"
next_decl = Some(decl)
next_order = decl.source_order
}
None => ()
}
match font_weight_decl {
Some(decl) =>
if decl.source_order < next_order {
next_prop = "font-weight"
next_decl = Some(decl)
next_order = decl.source_order
}
None => ()
}
match font_family_decl {
Some(decl) =>
if decl.source_order < next_order {
next_prop = "font-family"
next_decl = Some(decl)
}
None => ()
}
if next_prop == "" {
break
}
let decl = match next_decl {
Some(decl) => decl
None => break
}
let value = resolve_keyword(next_prop, decl.value, ctx)
match next_prop {
"font" => {
let parsed = parse_font_shorthand(value, ctx)
seeded_font_size = parsed.font_size
seeded_line_height = parsed.line_height
seeded_font_weight = parsed.font_weight
seeded_font_family = parsed.font_family
font_decl = None
}
"font-size" => {
seeded_font_size = parse_font_size(value, ctx)
font_size_decl = None
}
"line-height" => {
seeded_line_height = parse_line_height(
value,
seeded_font_size,
root_font_size=ctx.root_font_size,
)
line_height_decl = None
}
"font-weight" => {
seeded_font_weight = parse_font_weight(value)
font_weight_decl = None
}
"font-family" => {
seeded_font_family = parse_font_family(value)
font_family_decl = None
}
_ => ()
}
}
builder.font_size = seeded_font_size
builder.line_height = seeded_line_height
builder.font_weight = seeded_font_weight
builder.font_family = seeded_font_family
{ ..ctx, font_size: seeded_font_size }
}
///|
/// Resolve a 1-or-2 value logical shorthand (margin-inline, padding-block, …)
/// into a (start, end) dimension pair. A single value applies to both ends.
fn resolve_logical_pair(
value : String,
ctx : ComputeContext,
) -> (@types.Dimension, @types.Dimension) {
let dims : Array[@types.Dimension] = []
for part in value.trim().to_owned().split(" ") {
let p = part.to_owned().trim().to_owned()
if !p.is_empty() {
dims.push(resolve_dimension(p, ctx))
}
}
match dims.length() {
0 => {
let d = resolve_dimension(value, ctx)
(d, d)
}
1 => (dims[0], dims[0])
_ => (dims[0], dims[1])
}
}
///|
/// Apply a resolved property value to a style builder
fn apply_property(
builder : StyleBuilder,
property : String,
value : String,
ctx : ComputeContext,
) -> Unit {
fn grid_template_value(value : String) -> String {
normalize_grid_template_math_functions(resolve_all_vars(value, ctx), ctx)
}
match property {
// Display and positioning
"display" => builder.display = parse_display(value)
"position" => builder.position = parse_position(value)
"float" =>
builder.float = parse_float_with_direction(value, builder.direction)
"clear" =>
builder.clear = parse_clear_with_direction(value, builder.direction)
"box-sizing" =>
builder.box_sizing = match value.trim().to_lower() {
"border-box" => @types.BoxSizing::BorderBox
"content-box" | "initial" | "inherit" => @types.BoxSizing::ContentBox
_ => @types.BoxSizing::ContentBox
}
// Sizing
"width" =>
builder.width = resolve_dimension_with_calc_percent_fallback(value, ctx)
"height" =>
builder.height = resolve_dimension_with_calc_percent_fallback(value, ctx)
"min-width" =>
builder.min_width = resolve_dimension_with_mixed_calc_fallback(
"min-width", value, ctx,
)
"min-height" =>
builder.min_height = resolve_dimension_with_mixed_calc_fallback(
"min-height", value, ctx,
)
"max-width" =>
builder.max_width = resolve_dimension_with_calc_percent_fallback(
value, ctx,
)
"max-height" =>
builder.max_height = resolve_dimension_with_calc_percent_fallback(
value, ctx,
)
// Logical sizing properties
// In vertical writing-modes:
// - inline axis is vertical -> inline-size maps to height
// - block axis is horizontal -> block-size maps to width
"inline-size" => {
let dim = resolve_dimension_with_calc_percent_fallback(value, ctx)
if builder.writing_mode.is_vertical() {
builder.height = dim
} else {
builder.width = dim
}
}
"block-size" => {
let dim = resolve_dimension_with_calc_percent_fallback(value, ctx)
if builder.writing_mode.is_vertical() {
builder.width = dim
} else {
builder.height = dim
}
}
"min-inline-size" => {
let dim = resolve_dimension(value, ctx)
if builder.writing_mode.is_vertical() {
builder.min_height = dim
} else {
builder.min_width = dim
}
}
"min-block-size" => {
let dim = resolve_dimension(value, ctx)
if builder.writing_mode.is_vertical() {
builder.min_width = dim
} else {
builder.min_height = dim
}
}
"max-inline-size" => {
let dim = resolve_dimension(value, ctx)
if builder.writing_mode.is_vertical() {
builder.max_height = dim
} else {
builder.max_width = dim
}
}
"max-block-size" => {
let dim = resolve_dimension(value, ctx)
if builder.writing_mode.is_vertical() {
builder.max_width = dim
} else {
builder.max_height = dim
}
}
// Box model
"margin" => {
// Parse margin shorthand: 1-4 values
let parts : Array[StringView] = value
.split(" ")
.filter(fn(s) { !s.is_empty() })
.collect()
match parts.length() {
1 => {
let dim = resolve_dimension(parts[0].to_owned(), ctx)
builder.margin_top = dim
builder.margin_right = dim
builder.margin_bottom = dim
builder.margin_left = dim
}
2 => {
let tb = resolve_dimension(parts[0].to_owned(), ctx)
let lr = resolve_dimension(parts[1].to_owned(), ctx)
builder.margin_top = tb
builder.margin_bottom = tb
builder.margin_left = lr
builder.margin_right = lr
}
3 => {
builder.margin_top = resolve_dimension(parts[0].to_owned(), ctx)
let lr = resolve_dimension(parts[1].to_owned(), ctx)
builder.margin_left = lr
builder.margin_right = lr
builder.margin_bottom = resolve_dimension(parts[2].to_owned(), ctx)
}
4 => {
builder.margin_top = resolve_dimension(parts[0].to_owned(), ctx)
builder.margin_right = resolve_dimension(parts[1].to_owned(), ctx)
builder.margin_bottom = resolve_dimension(parts[2].to_owned(), ctx)
builder.margin_left = resolve_dimension(parts[3].to_owned(), ctx)
}
_ => ()
}
}
"margin-top" => builder.margin_top = resolve_dimension(value, ctx)
"margin-right" => builder.margin_right = resolve_dimension(value, ctx)
"margin-bottom" => builder.margin_bottom = resolve_dimension(value, ctx)
"margin-left" => builder.margin_left = resolve_dimension(value, ctx)
// Logical margin properties (LTR horizontal-tb writing mode)
"margin-inline" => {
let (start, end) = resolve_logical_pair(value, ctx)
builder.margin_left = start
builder.margin_right = end
}
"margin-inline-start" => builder.margin_left = resolve_dimension(value, ctx)
"margin-inline-end" => builder.margin_right = resolve_dimension(value, ctx)
"margin-block" => {
let (start, end) = resolve_logical_pair(value, ctx)
builder.margin_top = start
builder.margin_bottom = end
}
"margin-block-start" => builder.margin_top = resolve_dimension(value, ctx)
"margin-block-end" => builder.margin_bottom = resolve_dimension(value, ctx)
"margin-trim" => builder.margin_trim = parse_margin_trim(value)
"padding-top" => builder.padding_top = resolve_dimension(value, ctx)
"padding-right" => builder.padding_right = resolve_dimension(value, ctx)
"padding-bottom" => builder.padding_bottom = resolve_dimension(value, ctx)
"padding-left" => builder.padding_left = resolve_dimension(value, ctx)
"padding" => {
// Shorthand: padding (1, 2, 3, or 4 values)
let parts = value.trim().to_owned().split(" ")
let values : Array[@types.Dimension] = []
for part in parts {
let p = part.to_owned().trim().to_owned()
if !p.is_empty() {
values.push(resolve_dimension(p, ctx))
}
}
match values.length() {
1 => {
builder.padding_top = values[0]
builder.padding_right = values[0]
builder.padding_bottom = values[0]
builder.padding_left = values[0]
}
2 => {
builder.padding_top = values[0]
builder.padding_right = values[1]
builder.padding_bottom = values[0]
builder.padding_left = values[1]
}
3 => {
builder.padding_top = values[0]
builder.padding_right = values[1]
builder.padding_bottom = values[2]
builder.padding_left = values[1]
}
_ =>
// 4 or more values
if values.length() >= 4 {
builder.padding_top = values[0]
builder.padding_right = values[1]
builder.padding_bottom = values[2]
builder.padding_left = values[3]
}
}
}
// Logical padding properties (block/inline)
"padding-block" => {
let (start, end) = resolve_logical_pair(value, ctx)
builder.padding_top = start
builder.padding_bottom = end
}
"padding-block-start" => builder.padding_top = resolve_dimension(value, ctx)
"padding-block-end" =>
builder.padding_bottom = resolve_dimension(value, ctx)
"padding-inline" => {
let (start, end) = resolve_logical_pair(value, ctx)
builder.padding_left = start
builder.padding_right = end
}
"padding-inline-start" =>
builder.padding_left = resolve_dimension(value, ctx)
"padding-inline-end" =>
builder.padding_right = resolve_dimension(value, ctx)
"border-width" => {
// Shorthand: border-width (1, 2, 3, or 4 values)
let parts = value.trim().to_owned().split(" ")
let values : Array[@types.Dimension] = []
for part in parts {
let p = part.to_owned().trim().to_owned()
if !p.is_empty() {
values.push(resolve_dimension(p, ctx))
}
}
if values.length() == 1 {
// All sides
builder.border_top = values[0]
builder.border_right = values[0]
builder.border_bottom = values[0]
builder.border_left = values[0]
} else if values.length() == 2 {
// top/bottom, left/right
builder.border_top = values[0]
builder.border_bottom = values[0]
builder.border_right = values[1]
builder.border_left = values[1]
} else if values.length() == 3 {
// top, left/right, bottom
builder.border_top = values[0]
builder.border_right = values[1]
builder.border_left = values[1]
builder.border_bottom = values[2]
} else if values.length() >= 4 {
// top, right, bottom, left
builder.border_top = values[0]
builder.border_right = values[1]
builder.border_bottom = values[2]
builder.border_left = values[3]
}
}
"border" => {
// Shorthand: border: [width] [style] [color]
let width = resolve_border_width(value, ctx)
builder.border_top = width
builder.border_right = width
builder.border_bottom = width
builder.border_left = width
let bs = extract_border_style_from_shorthand(value)
builder.border_style_top = bs
builder.border_style_right = bs
builder.border_style_bottom = bs
builder.border_style_left = bs
let bc = extract_border_color_from_shorthand(value, ctx)
builder.border_color_top = bc
builder.border_color_right = bc
builder.border_color_bottom = bc
builder.border_color_left = bc
}
"border-top" => {
let bs = extract_border_style_from_shorthand(value)
builder.border_style_top = bs
builder.border_top = if is_none_border_style_enum(bs) {
@types.Length(0.0)
} else {
resolve_border_width(value, ctx)
}
builder.border_color_top = extract_border_color_from_shorthand(value, ctx)
}
"border-right" => {
let bs = extract_border_style_from_shorthand(value)
builder.border_style_right = bs
builder.border_right = if is_none_border_style_enum(bs) {
@types.Length(0.0)
} else {
resolve_border_width(value, ctx)
}
builder.border_color_right = extract_border_color_from_shorthand(
value, ctx,
)
}
"border-bottom" => {
let bs = extract_border_style_from_shorthand(value)
builder.border_style_bottom = bs
builder.border_bottom = if is_none_border_style_enum(bs) {
@types.Length(0.0)
} else {
resolve_border_width(value, ctx)
}
builder.border_color_bottom = extract_border_color_from_shorthand(
value, ctx,
)
}
"border-left" => {
let bs = extract_border_style_from_shorthand(value)
builder.border_style_left = bs
builder.border_left = if is_none_border_style_enum(bs) {
@types.Length(0.0)
} else {
resolve_border_width(value, ctx)
}
builder.border_color_left = extract_border_color_from_shorthand(
value, ctx,
)
}
"border-top-width" => builder.border_top = resolve_dimension(value, ctx)
"border-right-width" => builder.border_right = resolve_dimension(value, ctx)
"border-bottom-width" =>
builder.border_bottom = resolve_dimension(value, ctx)
"border-left-width" => builder.border_left = resolve_dimension(value, ctx)
"border-style" => {
let raw_parts = value.trim().to_lower().split(" ")
let parts : Array[String] = []
for part in raw_parts {
let p = part.to_owned().trim().to_owned()
if !p.is_empty() {
parts.push(p)
}
}
if parts.length() == 1 {
let s = parse_border_style_value(parts[0])
builder.border_style_top = s
builder.border_style_right = s
builder.border_style_bottom = s
builder.border_style_left = s
if is_none_border_style(parts[0]) {
builder.border_top = @types.Length(0.0)
builder.border_right = @types.Length(0.0)
builder.border_bottom = @types.Length(0.0)
builder.border_left = @types.Length(0.0)
}
} else if parts.length() == 2 {
let v = parse_border_style_value(parts[0])
let h = parse_border_style_value(parts[1])
builder.border_style_top = v
builder.border_style_bottom = v
builder.border_style_right = h
builder.border_style_left = h
if is_none_border_style(parts[0]) {
builder.border_top = @types.Length(0.0)
builder.border_bottom = @types.Length(0.0)
}
if is_none_border_style(parts[1]) {
builder.border_right = @types.Length(0.0)
builder.border_left = @types.Length(0.0)
}
} else if parts.length() >= 4 {
builder.border_style_top = parse_border_style_value(parts[0])
builder.border_style_right = parse_border_style_value(parts[1])
builder.border_style_bottom = parse_border_style_value(parts[2])
builder.border_style_left = parse_border_style_value(parts[3])
if is_none_border_style(parts[0]) {
builder.border_top = @types.Length(0.0)
}
if is_none_border_style(parts[1]) {
builder.border_right = @types.Length(0.0)
}
if is_none_border_style(parts[2]) {
builder.border_bottom = @types.Length(0.0)
}
if is_none_border_style(parts[3]) {
builder.border_left = @types.Length(0.0)
}
}
}
"border-top-style" => {
builder.border_style_top = parse_border_style_value(value)
if is_none_border_style(value) {
builder.border_top = @types.Length(0.0)
}
}
"border-right-style" => {
builder.border_style_right = parse_border_style_value(value)
if is_none_border_style(value) {
builder.border_right = @types.Length(0.0)
}
}
"border-bottom-style" => {
builder.border_style_bottom = parse_border_style_value(value)
if is_none_border_style(value) {
builder.border_bottom = @types.Length(0.0)
}
}
"border-left-style" => {
builder.border_style_left = parse_border_style_value(value)
if is_none_border_style(value) {
builder.border_left = @types.Length(0.0)
}
}
"border-color" => {
let c = parse_color_with_ctx(value, ctx).or_default(@types.Color::black())
builder.border_color_top = c
builder.border_color_right = c
builder.border_color_bottom = c
builder.border_color_left = c
}
"border-top-color" =>
builder.border_color_top = parse_color_with_ctx(value, ctx).or_default(
@types.Color::black(),
)
"border-right-color" =>
builder.border_color_right = parse_color_with_ctx(value, ctx).or_default(
@types.Color::black(),
)
"border-bottom-color" =>
builder.border_color_bottom = parse_color_with_ctx(value, ctx).or_default(
@types.Color::black(),
)
"border-left-color" =>
builder.border_color_left = parse_color_with_ctx(value, ctx).or_default(
@types.Color::black(),
)
// Logical border properties (block/inline)
"border-block" | "border-block-width" => {
let width = resolve_border_width(value, ctx)
builder.border_top = width
builder.border_bottom = width
}
"border-block-style" => {
let raw_parts = value.trim().to_lower().split(" ")
let parts : Array[String] = []
for part in raw_parts {
let p = part.to_owned().trim().to_owned()
if !p.is_empty() {
parts.push(p)
}
}
if parts.length() > 0 && is_none_border_style(parts[0]) {
builder.border_top = @types.Length(0.0)
}
if parts.length() > 1 && is_none_border_style(parts[1]) {
builder.border_bottom = @types.Length(0.0)
} else if parts.length() == 1 && is_none_border_style(parts[0]) {
builder.border_bottom = @types.Length(0.0)
}
}
"border-block-start" | "border-block-start-width" =>
builder.border_top = resolve_border_width(value, ctx)
"border-block-end" | "border-block-end-width" =>
builder.border_bottom = resolve_border_width(value, ctx)
"border-inline" | "border-inline-width" => {
let width = resolve_border_width(value, ctx)
if builder.writing_mode.is_vertical() {
builder.border_top = width
builder.border_bottom = width
} else {
builder.border_left = width
builder.border_right = width
}
}
"border-inline-style" => {
let raw_parts = value.trim().to_lower().split(" ")
let parts : Array[String] = []
for part in raw_parts {
let p = part.to_owned().trim().to_owned()
if !p.is_empty() {
parts.push(p)
}
}
if builder.writing_mode.is_vertical() {
if parts.length() > 0 && is_none_border_style(parts[0]) {
builder.border_top = @types.Length(0.0)
}
if parts.length() > 1 && is_none_border_style(parts[1]) {
builder.border_bottom = @types.Length(0.0)
} else if parts.length() == 1 && is_none_border_style(parts[0]) {
builder.border_bottom = @types.Length(0.0)
}
} else {
if parts.length() > 0 && is_none_border_style(parts[0]) {
builder.border_left = @types.Length(0.0)
}
if parts.length() > 1 && is_none_border_style(parts[1]) {
builder.border_right = @types.Length(0.0)
} else if parts.length() == 1 && is_none_border_style(parts[0]) {
builder.border_right = @types.Length(0.0)
}
}
}
"border-inline-start" | "border-inline-start-width" =>
if builder.writing_mode.is_vertical() {
builder.border_top = resolve_border_width(value, ctx)
} else {
builder.border_left = resolve_border_width(value, ctx)
}
"border-inline-end" | "border-inline-end-width" =>
if builder.writing_mode.is_vertical() {
builder.border_bottom = resolve_border_width(value, ctx)
} else {
builder.border_right = resolve_border_width(value, ctx)
}
// Flexbox container
"flex-direction" => builder.flex_direction = parse_flex_direction(value)
"flex-wrap" => builder.flex_wrap = parse_flex_wrap(value)
"justify-content" => {
let (alignment, is_unsafe) = parse_alignment_with_overflow(value)
builder.justify_content = alignment
builder.justify_content_unsafe = is_unsafe
}
"align-items" => builder.align_items = parse_alignment(value)
"align-content" => {
let normalized = value.trim().to_lower().to_owned()
let (alignment, is_unsafe) = if normalized == "normal" {
(@types.Alignment::Stretch, false)
} else if normalized == "last baseline" {
(@types.Alignment::Baseline, false)
} else {
parse_alignment_with_overflow(value)
}
builder.align_content = alignment
builder.align_content_unsafe = is_unsafe
}
"place-content" =>
match parse_place_content_with_overflow(value) {
Some(((align_content, align_unsafe), (justify_content, justify_unsafe))) => {
builder.align_content = align_content
builder.align_content_unsafe = align_unsafe
builder.justify_content = justify_content
builder.justify_content_unsafe = justify_unsafe
}
None => ()
}
// Flexbox/Grid item alignment
"align-self" => builder.align_self = parse_align_self(value)
"justify-self" => builder.justify_self = parse_align_self(value)
"place-self" =>
match parse_place_self(value) {
Some((align_self, justify_self)) => {
builder.align_self = align_self
builder.justify_self = justify_self
}
None => ()
}
// Grid container alignment
"justify-items" => builder.justify_items = parse_alignment(value)
"place-items" =>
match parse_place_items(value) {
Some((align_items, justify_items)) => {
builder.align_items = align_items
builder.justify_items = justify_items
}
None => ()
}
"flex-grow" => {
let v = parse_number(value)
if v >= 0.0 {
builder.flex_grow = v
}
}
"flex-shrink" => {
let v = parse_number(value)
if v >= 0.0 {
builder.flex_shrink = v
}
}
"flex-basis" => {
// CSS spec: negative values are invalid, keep as auto
let dim = resolve_dimension(value, ctx)
match dim {
@types.Length(v) => if v >= 0.0 { builder.flex_basis = dim }
@types.Percent(v) => if v >= 0.0 { builder.flex_basis = dim }
@types.Calc(px, pct) =>
if px > 0.0 || pct > 0.0 {
builder.flex_basis = dim
}
@types.MathFn(_, _) => builder.flex_basis = dim
@types.Auto => builder.flex_basis = dim
@types.MinContent => builder.flex_basis = dim
@types.MaxContent => builder.flex_basis = dim
@types.FitContent(_) => builder.flex_basis = dim
}
}
"order" => builder.order = parse_integer(value)
"flex" => {
// Parse flex shorthand: none | auto | [] []
// Order must be: numbers first (grow, shrink), then optionally basis
// Invalid orders like "1 0% 1" (basis in middle) must be rejected
let v = value.trim()
if v == "none" {
builder.flex_grow = 0.0
builder.flex_shrink = 0.0
builder.flex_basis = @types.Dimension::Auto
} else if v == "auto" {
builder.flex_grow = 1.0
builder.flex_shrink = 1.0
builder.flex_basis = @types.Dimension::Auto
} else if v == "initial" {
builder.flex_grow = 0.0
builder.flex_shrink = 1.0
builder.flex_basis = @types.Dimension::Auto
} else {
// Try to parse as number(s) and optionally a basis
let parts : Array[StringView] = v
.split(" ")
.filter(fn(s) { !s.is_empty() })
.collect()
// Validate ordering: numbers first, then basis
// A value is a "number" if it's purely numeric (no % or units)
// A value is a "basis" if it has %, px, or is "auto"/"content"
let mut num_count = 0
let mut basis_found = false
let mut valid = true
for i = 0; i < parts.length(); i = i + 1 {
let p = parts[i].to_owned()
if is_flex_basis_value(p) {
if basis_found {
// Second basis value is invalid
valid = false
}
basis_found = true
} else if is_pure_number(p) {
if basis_found {
// Number after basis is invalid (e.g., "1 0% 1")
valid = false
}
num_count = num_count + 1
} else {
// Unknown token
valid = false
}
}
if valid {
match parts.length() {
1 =>
if num_count == 1 {
// flex: = 1 0%
builder.flex_grow = parse_number(parts[0].to_owned())
builder.flex_shrink = 1.0
builder.flex_basis = @types.Dimension::Percent(0.0)
} else if basis_found {
// flex: = 1 1
builder.flex_grow = 1.0
builder.flex_shrink = 1.0
builder.flex_basis = resolve_dimension(parts[0].to_owned(), ctx)
}
2 =>
if num_count == 2 {
// flex: = 0%
builder.flex_grow = parse_number(parts[0].to_owned())
builder.flex_shrink = parse_number(parts[1].to_owned())
builder.flex_basis = @types.Dimension::Percent(0.0)
} else if num_count == 1 && basis_found {
// flex:
builder.flex_grow = parse_number(parts[0].to_owned())
builder.flex_shrink = 1.0
builder.flex_basis = resolve_dimension(parts[1].to_owned(), ctx)
}
3 =>
if num_count == 2 && basis_found {
// flex:
builder.flex_grow = parse_number(parts[0].to_owned())
builder.flex_shrink = parse_number(parts[1].to_owned())
builder.flex_basis = resolve_dimension(parts[2].to_owned(), ctx)
}
// If num_count != 2 or basis not at end, it's invalid and we do nothing
_ => ()
}
}
// If not valid, we don't modify the builder (keep defaults)
}
}
// flex-flow shorthand: ||
"flex-flow" => {
let parts : Array[StringView] = value
.trim()
.split(" ")
.filter(fn(s) { !s.is_empty() })
.collect()
for part in parts {
let p = part.to_owned()
// Check flex-direction values
if p == "row" {
builder.flex_direction = @types.FlexDirection::Row
} else if p == "row-reverse" {
builder.flex_direction = @types.FlexDirection::RowReverse
} else if p == "column" {
builder.flex_direction = @types.FlexDirection::Column
} else if p == "column-reverse" {
builder.flex_direction = @types.FlexDirection::ColumnReverse
// Check flex-wrap values
} else if p == "nowrap" {
builder.flex_wrap = @types.FlexWrap::NoWrap
} else if p == "wrap" {
builder.flex_wrap = @types.FlexWrap::Wrap
} else if p == "wrap-reverse" {
builder.flex_wrap = @types.FlexWrap::WrapReverse
}
}
}
// Gap (grid-gap is legacy alias)
"gap" | "grid-gap" => {
let parts : Array[StringView] = value
.trim()
.split(" ")
.filter(fn(s) { !s.is_empty() })
.collect()
builder.column_gap_is_normal = false
match parts.length() {
1 => {
let dim = resolve_dimension_with_calc_percent_fallback(
parts[0].to_owned(),
ctx,
)
builder.row_gap = dim
builder.column_gap = dim
}
2 => {
builder.row_gap = resolve_dimension_with_calc_percent_fallback(
parts[0].to_owned(),
ctx,
)
builder.column_gap = resolve_dimension_with_calc_percent_fallback(
parts[1].to_owned(),
ctx,
)
}
_ => {
let dim = resolve_dimension_with_calc_percent_fallback(value, ctx)
builder.row_gap = dim
builder.column_gap = dim
}
}
}
"row-gap" | "grid-row-gap" =>
builder.row_gap = resolve_dimension_with_calc_percent_fallback(value, ctx)
"column-gap" | "grid-column-gap" => {
builder.column_gap = resolve_dimension_with_calc_percent_fallback(
value, ctx,
)
builder.column_gap_is_normal = false
}
// Multi-column
"column-count" => builder.column_count = parse_column_count(value)
"column-width" => builder.column_width = parse_column_width(value, ctx)
"column-fill" => builder.column_fill = parse_column_fill(value)
"columns" =>
match parse_columns_shorthand(value, ctx) {
Some((count, width)) => {
builder.column_count = count
builder.column_width = width
}
None => ()
}
"break-before" => builder.break_before = parse_break_before(value)
"break-after" => builder.break_after = parse_break_before(value)
"break-inside" | "page-break-inside" =>
builder.break_inside = parse_break_inside(value)
"column-span" => builder.column_span = parse_column_span(value)
// Inset
"top" => builder.inset_top = resolve_dimension(value, ctx)
"right" => builder.inset_right = resolve_dimension(value, ctx)
"bottom" => builder.inset_bottom = resolve_dimension(value, ctx)
"left" => builder.inset_left = resolve_dimension(value, ctx)
"inset" => {
let parts : Array[StringView] = value
.split(" ")
.filter(fn(s) { !s.is_empty() })
.collect()
match parts.length() {
1 => {
let dim = resolve_dimension(parts[0].to_owned(), ctx)
builder.inset_top = dim
builder.inset_right = dim
builder.inset_bottom = dim
builder.inset_left = dim
}
2 => {
let tb = resolve_dimension(parts[0].to_owned(), ctx)
let lr = resolve_dimension(parts[1].to_owned(), ctx)
builder.inset_top = tb
builder.inset_bottom = tb
builder.inset_left = lr
builder.inset_right = lr
}
3 => {
builder.inset_top = resolve_dimension(parts[0].to_owned(), ctx)
let lr = resolve_dimension(parts[1].to_owned(), ctx)
builder.inset_left = lr
builder.inset_right = lr
builder.inset_bottom = resolve_dimension(parts[2].to_owned(), ctx)
}
4 => {
builder.inset_top = resolve_dimension(parts[0].to_owned(), ctx)
builder.inset_right = resolve_dimension(parts[1].to_owned(), ctx)
builder.inset_bottom = resolve_dimension(parts[2].to_owned(), ctx)
builder.inset_left = resolve_dimension(parts[3].to_owned(), ctx)
}
_ => ()
}
}
// Logical inset properties (LTR horizontal-tb writing mode)
"inset-inline" => {
let (start, end) = resolve_logical_pair(value, ctx)
builder.inset_left = start
builder.inset_right = end
}
"inset-inline-start" => builder.inset_left = resolve_dimension(value, ctx)
"inset-inline-end" => builder.inset_right = resolve_dimension(value, ctx)
"inset-block" => {
let (start, end) = resolve_logical_pair(value, ctx)
builder.inset_top = start
builder.inset_bottom = end
}
"inset-block-start" => builder.inset_top = resolve_dimension(value, ctx)
"inset-block-end" => builder.inset_bottom = resolve_dimension(value, ctx)
// Aspect ratio
"aspect-ratio" => builder.aspect_ratio = parse_aspect_ratio(value)
// Overflow
"overflow" => {
// `overflow: []` — one value applies to both axes.
let parts : Array[String] = []
for part in value.trim().to_owned().split(" ") {
let p = part.to_owned().trim().to_owned()
if !p.is_empty() {
parts.push(p)
}
}
match parts.length() {
0 | 1 => {
let ov = parse_overflow(value)
builder.overflow_x = ov
builder.overflow_y = ov
}
_ => {
builder.overflow_x = parse_overflow(parts[0])
builder.overflow_y = parse_overflow(parts[1])
}
}
}
"overflow-x" => builder.overflow_x = parse_overflow(value)
"overflow-y" => builder.overflow_y = parse_overflow(value)
"scroll-snap-type" =>
builder.scroll_snap_type = parse_scroll_snap_type(value)
"scroll-snap-align" => {
let (inline_align, block_align) = parse_scroll_snap_align(value)
builder.scroll_snap_align_x = inline_align
builder.scroll_snap_align_y = block_align
}
// Grid container
"grid-auto-flow" => builder.grid_auto_flow = parse_grid_auto_flow(value)
"grid-auto-rows" =>
builder.grid_auto_rows = parse_grid_template_tracks(
grid_template_value(value),
)
"grid-auto-columns" =>
builder.grid_auto_columns = parse_grid_template_tracks(
grid_template_value(value),
)
"grid-template-columns" => {
let (kind, tracks, line_names) = parse_grid_template_axis(
grid_template_value(value),
)
builder.grid_template_columns = tracks
builder.grid_template_column_line_names = line_names
builder.grid_template_columns_kind = kind
}
"grid-template-rows" => {
let (kind, tracks, line_names) = parse_grid_template_axis(
grid_template_value(value),
)
builder.grid_template_rows = tracks
builder.grid_template_row_line_names = line_names
builder.grid_template_rows_kind = kind
}
"grid-template-areas" =>
builder.grid_template_areas = parse_grid_template_areas_value(value)
"grid-template" | "grid" => {
let tv = grid_template_value(value)
// Try the "areas" form first (it carries rows); the `grid`
// shorthand additionally supports the auto-flow form; finally fall back to
// the ` / ` form.
match parse_grid_template_areas_form(tv) {
Some((areas, rows, row_names, cols, col_names)) => {
builder.grid_template_areas = areas
builder.grid_template_rows = rows
builder.grid_template_row_line_names = row_names
builder.grid_template_columns = cols
builder.grid_template_column_line_names = col_names
}
None => {
let autoflow = if property == "grid" {
parse_grid_autoflow_form(tv)
} else {
None
}
match autoflow {
Some((flow, rows, row_names, cols, col_names, auto_rows, auto_cols)) => {
builder.grid_auto_flow = flow
builder.grid_template_rows = rows
builder.grid_template_row_line_names = row_names
builder.grid_template_columns = cols
builder.grid_template_column_line_names = col_names
builder.grid_auto_rows = auto_rows
builder.grid_auto_columns = auto_cols
}
None =>
match parse_grid_template_shorthand_with_names(tv) {
Some((rows, row_names, cols, col_names)) => {
builder.grid_template_rows = rows
builder.grid_template_row_line_names = row_names
builder.grid_template_columns = cols
builder.grid_template_column_line_names = col_names
}
None => ()
}
}
}
}
}
// Grid item placement
"grid-column-start" =>
builder.grid_column_start = parse_grid_placement(value)
"grid-column-end" => builder.grid_column_end = parse_grid_placement(value)
"grid-row-start" => builder.grid_row_start = parse_grid_placement(value)
"grid-row-end" => builder.grid_row_end = parse_grid_placement(value)
"grid-column" => {
let (start, end) = parse_grid_line_shorthand(value)
builder.grid_column_start = start
match end {
@types.GridPlacement::Auto =>
match builder.grid_column_end {
@types.GridPlacement::Auto => builder.grid_column_end = end
_ => ()
}
_ => builder.grid_column_end = end
}
}
"grid-row" => {
let (start, end) = parse_grid_line_shorthand(value)
builder.grid_row_start = start
match end {
@types.GridPlacement::Auto =>
match builder.grid_row_end {
@types.GridPlacement::Auto => builder.grid_row_end = end
_ => ()
}
_ => builder.grid_row_end = end
}
}
"grid-area" => {
let v = value.trim().to_owned()
// Check if this is a single named area identifier (no slashes, not a number, not auto/span)
if !v.contains("/") && is_grid_area_name(v) {
builder.grid_area = Some(v)
} else {
let (row_start, column_start, row_end, column_end) = parse_grid_area_shorthand(
value,
)
builder.grid_row_start = row_start
builder.grid_column_start = column_start
builder.grid_row_end = row_end
builder.grid_column_end = column_end
}
}
// Animation longhands
"animation-name" =>
builder.animation_name = @parser.parse_animation_name_from_string(value)
"animation-duration" =>
builder.animation_duration = @parser.parse_time_list_from_string(value)
"animation-timing-function" =>
builder.animation_timing_function = @parser.parse_easing_list_from_string(
value,
)
"animation-delay" =>
builder.animation_delay = @parser.parse_time_list_from_string(value)
"animation-iteration-count" =>
builder.animation_iteration_count = @parser.parse_iteration_count_list_from_string(
value,
)
"animation-direction" =>
builder.animation_direction = @parser.parse_direction_list_from_string(
value,
)
"animation-fill-mode" =>
builder.animation_fill_mode = @parser.parse_fill_mode_list_from_string(
value,
)
"animation-play-state" =>
builder.animation_play_state = @parser.parse_play_state_list_from_string(
value,
)
"animation" => {
let a = @parser.parse_animation_shorthand_from_string(value)
builder.animation_name = a.names
builder.animation_duration = a.durations
builder.animation_timing_function = a.timing_functions
builder.animation_delay = a.delays
builder.animation_iteration_count = a.iteration_counts
builder.animation_direction = a.directions
builder.animation_fill_mode = a.fill_modes
builder.animation_play_state = a.play_states
}
// Transition longhands
"transition-property" =>
builder.transition_property = @parser.parse_animation_name_from_string(
value,
)
"transition-duration" =>
builder.transition_duration = @parser.parse_time_list_from_string(value)
"transition-timing-function" =>
builder.transition_timing_function = @parser.parse_easing_list_from_string(
value,
)
"transition-delay" =>
builder.transition_delay = @parser.parse_time_list_from_string(value)
"transition" => {
let t = @parser.parse_transition_shorthand_from_string(value)
builder.transition_property = t.properties
builder.transition_duration = t.durations
builder.transition_timing_function = t.timing_functions
builder.transition_delay = t.delays
}
// Font properties
"font-size" => builder.font_size = parse_font_size(value, ctx)
"line-height" =>
builder.line_height = parse_line_height(
value,
builder.font_size,
root_font_size=ctx.root_font_size,
)
"text-align" => builder.text_align = parse_text_align(value)
"white-space" => builder.white_space = parse_white_space(value)
"writing-mode" => builder.writing_mode = parse_writing_mode(value)
"direction" => builder.direction = parse_direction(value)
"vertical-align" => builder.vertical_align = parse_vertical_align(value)
"font" => {
// Parse font shorthand: [style] [variant] [weight] size[/line-height] family
let parsed = parse_font_shorthand(value, ctx)
builder.font_size = parsed.font_size
builder.line_height = parsed.line_height
builder.font_weight = parsed.font_weight
builder.font_family = parsed.font_family
}
// CSS Containment
"contain" => builder.contain = parse_contain(value)
"container-type" =>
match parse_container_type(value) {
Some(implied) =>
if implied.has_containment() {
builder.contain = merge_contain(builder.contain, implied)
}
None => ()
}
"contain-intrinsic-inline-size" =>
builder.contain_intrinsic_inline_size = parse_contain_intrinsic_axis(
value, ctx,
)
"contain-intrinsic-block-size" =>
builder.contain_intrinsic_block_size = parse_contain_intrinsic_axis(
value, ctx,
)
"contain-intrinsic-size" =>
match parse_contain_intrinsic_size(value, ctx) {
Some((inline_size, block_size)) => {
builder.contain_intrinsic_inline_size = inline_size
builder.contain_intrinsic_block_size = block_size
}
None => ()
}
// Clip (legacy, deprecated but widely used for a11y)
"clip" => builder.clip = parse_clip(value)
"clip-path" => builder.clip_path = parse_clip_path(value)
// Paint properties (visual, not layout)
"visibility" => builder.visibility = parse_visibility(value)
"pointer-events" => builder.pointer_events = parse_pointer_events(value)
"z-index" => builder.z_index = parse_z_index(value)
"opacity" => builder.opacity = parse_opacity(value)
// Color properties
"color" => {
let parsed = parse_color_with_ctx(value, ctx)
match parsed {
Resolved(c) => builder.color = c
CurrentColor =>
// currentColor for 'color' property inherits from parent
match ctx.parent_style {
Some(parent) => builder.color = parent.color
None => () // Keep default (black)
}
Inherit =>
match ctx.parent_style {
Some(parent) => builder.color = parent.color
None => ()
}
}
}
"background-color" | "background" => {
let resolved_value = resolve_all_vars(value, ctx)
// Check for linear-gradient first
let v_lower = resolved_value.trim().to_lower()
if v_lower.contains("linear-gradient") {
let grad = parse_linear_gradient(resolved_value)
builder.background_image = grad
} else {
let parsed = parse_color_with_ctx(resolved_value, ctx)
match parsed {
Resolved(c) => builder.background_color = c
CurrentColor =>
// currentColor uses the element's computed color
builder.background_color = builder.color
Inherit =>
match ctx.parent_style {
Some(parent) => builder.background_color = parent.background_color
None => ()
}
}
}
}
"background-image" => {
let resolved_value = resolve_all_vars(value, ctx)
let v_lower = resolved_value.trim().to_lower()
if v_lower.contains("linear-gradient") {
builder.background_image = parse_linear_gradient(resolved_value)
}
}
"box-shadow" =>
builder.box_shadows = parse_box_shadows(value, ctx, builder.color)
// Font weight
"font-weight" => builder.font_weight = parse_font_weight(value)
// Font family (store first family name for font matching)
"font-family" => builder.font_family = parse_font_family(value)
// Transform (visual position offset)
"transform" => builder.transform = parse_transform(value)
// Individual transform properties compose into one Transform.
"translate" => {
let t = @parser.parse_translate_property_from_string(value)
builder.individual_transform = {
..builder.individual_transform,
translate_x: t.translate_x,
translate_y: t.translate_y,
}
}
"scale" => {
let t = @parser.parse_scale_property_from_string(value)
builder.individual_transform = {
..builder.individual_transform,
scale_x: t.scale_x,
scale_y: t.scale_y,
}
}
"rotate" => {
let t = @parser.parse_rotate_property_from_string(value)
builder.individual_transform = {
..builder.individual_transform,
rotate_degrees: t.rotate_degrees,
}
}
// filter / backdrop-filter only need to be tracked as "present" for the
// containing-block rule in absolute positioning. The actual filter
// functions are paint-side and not modelled here.
"filter" | "backdrop-filter" => {
let v = value.trim().to_lower()
if v != "" && v != "none" && v != "initial" && v != "unset" {
builder.has_filter = true
}
}
// Zoom (layout scaling, inherited)
"zoom" => builder.zoom = parse_zoom(value)
// Table properties
"border-spacing" => {
let (horizontal, vertical) = parse_border_spacing(value, ctx)
builder.border_spacing = horizontal
builder.border_spacing_vertical = vertical
}
"border-collapse" => builder.border_collapse = parse_border_collapse(value)
"table-layout" => builder.table_layout = parse_table_layout(value)
"caption-side" => builder.caption_side = parse_caption_side(value)
// Border radius
"border-radius" => {
let r = resolve_border_radius(value, ctx)
builder.border_top_left_radius = r.0
builder.border_top_right_radius = r.1
builder.border_bottom_right_radius = r.2
builder.border_bottom_left_radius = r.3
}
"border-top-left-radius" =>
builder.border_top_left_radius = resolve_dimension_to_px(
resolve_dimension(value, ctx),
)
"border-top-right-radius" =>
builder.border_top_right_radius = resolve_dimension_to_px(
resolve_dimension(value, ctx),
)
"border-bottom-right-radius" =>
builder.border_bottom_right_radius = resolve_dimension_to_px(
resolve_dimension(value, ctx),
)
"border-bottom-left-radius" =>
builder.border_bottom_left_radius = resolve_dimension_to_px(
resolve_dimension(value, ctx),
)
"text-decoration" | "text-decoration-line" => {
let v = value.trim().to_lower()
builder.text_decoration_underline = v.contains("underline")
builder.text_decoration_line_through = v.contains("line-through")
builder.text_decoration_overline = v.contains("overline")
}
"text-overflow" =>
builder.text_overflow = match value.trim().to_lower() {
"ellipsis" => @style.TextOverflow::Ellipsis
_ => @style.TextOverflow::Clip
}
"letter-spacing" => {
let v = value.trim().to_lower()
if v == "normal" {
builder.letter_spacing = 0.0
} else {
builder.letter_spacing = resolve_dimension_to_px(
resolve_dimension(value, ctx),
)
}
}
"word-spacing" => {
let v = value.trim().to_lower()
if v == "normal" {
builder.word_spacing = 0.0
} else {
builder.word_spacing = resolve_dimension_to_px(
resolve_dimension(value, ctx),
)
}
}
_ => ()
}
}
///|
fn parse_font_weight(value : String) -> Double {
match value.trim().to_lower() {
"normal" => 400.0
"bold" => 700.0
"lighter" => 100.0
"bolder" => 700.0
_ => {
let n = @string.parse_double(value.trim()) catch { _ => return 400.0 }
if n >= 1.0 && n <= 1000.0 {
n
} else {
400.0
}
}
}
}
///|
/// Parse font-family, returning the first family name (lowercase, unquoted).
fn parse_font_family(value : String) -> String {
let v = value.trim()
// Split by comma, take first
let first = match v.split(",").collect() {
[] => v
parts => parts[0]
}
// Remove quotes and normalize
let trimmed = first.trim()
let len = trimmed.length()
if len >= 2 && trimmed[0] == '"' && trimmed[len - 1] == '"' {
trimmed[1:len - 1].to_owned().to_lower()
} else if len >= 2 && trimmed[0] == '\'' && trimmed[len - 1] == '\'' {
trimmed[1:len - 1].to_owned().to_lower()
} else {
trimmed.to_owned().to_lower()
}
}
///|
fn split_css_top_level(input : String, delimiter : Char) -> Array[String] {
let parts : Array[String] = []
let current = StringBuilder::new()
let mut depth = 0
for ch in input {
if ch == '(' {
depth += 1
current.write_char(ch)
continue
}
if ch == ')' {
if depth > 0 {
depth -= 1
}
current.write_char(ch)
continue
}
if ch == delimiter && depth == 0 {
let part = current.to_string().trim().to_owned()
if !part.is_empty() {
parts.push(part)
}
current.reset()
continue
}
current.write_char(ch)
}
let last = current.to_string().trim().to_owned()
if !last.is_empty() {
parts.push(last)
}
parts
}
///|
fn split_css_whitespace_preserving_functions(input : String) -> Array[String] {
let parts : Array[String] = []
let current = StringBuilder::new()
let mut depth = 0
for ch in input {
if ch == '(' {
depth += 1
current.write_char(ch)
continue
}
if ch == ')' {
if depth > 0 {
depth -= 1
}
current.write_char(ch)
continue
}
let is_space = ch == ' ' || ch == '\t' || ch == '\n'
if is_space && depth == 0 {
let part = current.to_string().trim().to_owned()
if !part.is_empty() {
parts.push(part)
}
current.reset()
continue
}
current.write_char(ch)
}
let last = current.to_string().trim().to_owned()
if !last.is_empty() {
parts.push(last)
}
parts
}
///|
fn parse_box_shadows(
value : String,
ctx : ComputeContext,
default_color : @types.Color,
) -> Array[@style.BoxShadow] {
let trimmed = value.trim().to_lower()
if trimmed == "none" {
return []
}
let shadows : Array[@style.BoxShadow] = []
for entry in split_css_top_level(value, ',') {
match parse_box_shadow(entry, ctx, default_color) {
Some(shadow) => shadows.push(shadow)
None => ()
}
}
shadows
}
///|
fn parse_box_shadow(
value : String,
ctx : ComputeContext,
default_color : @types.Color,
) -> @style.BoxShadow? {
let tokens = split_css_whitespace_preserving_functions(value)
if tokens.length() == 0 {
return None
}
let mut inset = false
let mut color = default_color
let length_tokens : Array[String] = []
for token in tokens {
let lower = token.to_lower()
if lower == "inset" {
inset = true
continue
}
match parse_css_value_to_px(token, ctx) {
Some(_) => {
length_tokens.push(token)
continue
}
None => ()
}
if is_box_shadow_color_token(lower) {
let parsed_color = parse_color_with_ctx(token, ctx)
match parsed_color {
Resolved(resolved) => {
color = resolved
continue
}
CurrentColor => {
color = default_color
continue
}
Inherit => return None
}
}
return None
}
if length_tokens.length() < 2 || length_tokens.length() > 4 {
return None
}
let lengths : Array[Double] = []
for token in length_tokens {
match parse_css_value_to_px(token, ctx) {
Some(px) => lengths.push(px)
None => return None
}
}
Some(
@style.BoxShadow::new(
inset,
lengths[0],
lengths[1],
if lengths.length() >= 3 {
@cmp.maximum(lengths[2], 0.0)
} else {
0.0
},
if lengths.length() >= 4 {
lengths[3]
} else {
0.0
},
color,
),
)
}
///|
fn is_box_shadow_color_token(token : String) -> Bool {
token.has_prefix("#") ||
token.has_prefix("rgb(") ||
token.has_prefix("rgba(") ||
token == "transparent" ||
token == "currentcolor" ||
get_named_color(token) is Some(_)
}
///|
/// Parse border-collapse value
fn parse_border_collapse(value : String) -> @style.BorderCollapse {
match value.trim() {
"collapse" => @style.Collapse
"separate" => @style.Separate
_ => @style.Separate
}
}
///|
/// Border style keywords that suppress used border width.
fn is_none_border_style_enum(style : @style.BorderStyle) -> Bool {
style == @style.BorderStyle::None || style == @style.BorderStyle::Hidden
}
///|
fn is_none_border_style(value : String) -> Bool {
let v = value.trim().to_lower()
v == "none" || v == "hidden"
}
///|
fn extract_border_color_from_shorthand(
value : String,
ctx : ComputeContext,
) -> @types.Color {
let parts = split_whitespace(value)
for part in parts {
let lower = part.to_lower()
// Skip border-style keywords and width keywords/values
match lower {
"none"
| "hidden"
| "dotted"
| "dashed"
| "solid"
| "double"
| "groove"
| "ridge"
| "inset"
| "outset"
| "thin"
| "medium"
| "thick" => continue
_ => ()
}
// Skip numeric values (border width)
if lower.length() > 0 && lower[0] >= '0' && lower[0] <= '9' {
continue
}
if lower == "0" {
continue
}
// Try to parse as color
let color_val = parse_color_with_ctx(part, ctx)
match color_val.get_color() {
Some(c) => return c
None => ()
}
}
@types.Color::black() // CSS default: currentColor → approximate as black
}
///|
fn extract_border_style_from_shorthand(value : String) -> @style.BorderStyle {
let parts = split_whitespace(value)
let mut found_style = false
let mut result = @style.BorderStyle::None
for part in parts {
let lower = part.to_lower()
match lower {
"none"
| "hidden"
| "dotted"
| "dashed"
| "solid"
| "double"
| "groove"
| "ridge"
| "inset"
| "outset" => {
found_style = true
result = parse_border_style_value(part)
}
_ => ()
}
}
if found_style {
result
} else {
@style.BorderStyle::Solid // Default when only width/color specified
}
}
///|
fn parse_border_style_value(value : String) -> @style.BorderStyle {
match value.trim().to_lower() {
"none" => @style.BorderStyle::None
"hidden" => @style.BorderStyle::Hidden
"dotted" => @style.BorderStyle::Dotted
"dashed" => @style.BorderStyle::Dashed
"solid" => @style.BorderStyle::Solid
"double" => @style.BorderStyle::Double
"groove" => @style.BorderStyle::Groove
"ridge" => @style.BorderStyle::Ridge
"inset" => @style.BorderStyle::Inset
"outset" => @style.BorderStyle::Outset
_ => @style.BorderStyle::None
}
}
///|
/// Resolve border width from width/style/color shorthand tokens.
/// Falls back to the existing default 1.5px for style-only shorthands.
fn resolve_border_width(
value : String,
ctx : ComputeContext,
) -> @types.Dimension {
let v = value.trim().to_lower()
if v == "none" || v == "hidden" || v == "0" {
return @types.Length(0.0)
}
let mut has_visible_style = false
let mut has_none_style = false
let parts = v
.split(" ")
.map(fn(part) { part.to_owned().trim().to_owned() })
.filter(fn(part) { !part.is_empty() })
.collect()
for part in parts {
match part {
"none" | "hidden" => {
has_none_style = true
continue
}
"thin" => return @types.Length(1.0)
"medium" => return @types.Length(3.0)
"thick" => return @types.Length(5.0)
"solid"
| "dotted"
| "dashed"
| "double"
| "groove"
| "ridge"
| "inset"
| "outset" => {
has_visible_style = true
continue
}
_ => ()
}
// Length-like token found in shorthand.
match resolve_dimension(part, ctx) {
@types.Length(px) => return @types.Length(px)
_ => ()
}
}
if has_none_style {
@types.Length(0.0)
} else if has_visible_style {
@types.Length(1.5)
} else {
@types.Length(0.0)
}
}
///|
/// Parse table-layout value
fn parse_table_layout(value : String) -> @style.TableLayout {
match value.trim() {
"fixed" => @style.TableLayout::Fixed
"auto" => @style.Auto
_ => @style.Auto
}
}
///|
/// Parse caption-side value
fn parse_caption_side(value : String) -> @style.CaptionSide {
match value.trim() {
"top" => @style.CaptionSide::Top
"bottom" => @style.CaptionSide::Bottom
_ => @style.CaptionSide::Top
}
}
///|
/// Parse margin-trim value
fn parse_margin_trim(value : String) -> @style.MarginTrim {
match value.trim().to_lower() {
"none" => @style.MarginTrim::None
"block-start" => @style.MarginTrim::BlockStart
"block-end" => @style.MarginTrim::BlockEnd
"block" => @style.MarginTrim::Block
"inline-start" => @style.MarginTrim::InlineStart
"inline-end" => @style.MarginTrim::InlineEnd
"inline" => @style.MarginTrim::Inline
_ => @style.MarginTrim::None
}
}
///|
/// Parse border-spacing value (single value or two values)
fn parse_border_spacing(
value : String,
ctx : ComputeContext,
) -> (Double, Double) {
let v = value.trim()
// Split by whitespace
let parts : Array[StringView] = v
.split(" ")
.filter(fn(s) { !s.is_empty() })
.collect()
if parts.length() == 0 {
return (0.0, 0.0)
}
let horizontal = match resolve_dimension(parts[0].to_owned(), ctx) {
@types.Length(px) => px
_ => 0.0
}
let vertical = if parts.length() >= 2 {
match resolve_dimension(parts[1].to_owned(), ctx) {
@types.Length(px) => px
_ => horizontal
}
} else {
horizontal
}
(horizontal, vertical)
}
///|
/// Parse border-radius shorthand (1-4 values)
/// Returns (top-left, top-right, bottom-right, bottom-left) in px
fn resolve_border_radius(
value : String,
ctx : ComputeContext,
) -> (Double, Double, Double, Double) {
let v = value.trim()
// Handle slash syntax (horizontal / vertical) - take only horizontal for now
let effective = if v.contains("/") {
let slash_parts : Array[StringView] = v.split("/").collect()
slash_parts[0].to_owned().trim()
} else {
v
}
let parts : Array[StringView] = effective
.split(" ")
.filter(fn(s) { !s.is_empty() })
.collect()
if parts.length() == 0 {
return (0.0, 0.0, 0.0, 0.0)
}
let values : Array[Double] = parts.map(fn(p) {
let dim = resolve_dimension(p.to_owned(), ctx)
match dim {
@types.Percent(pct) => -pct // Negative encodes percentage (fraction) for render-time resolution
_ => resolve_dimension_to_px(dim)
}
})
match values.length() {
1 => (values[0], values[0], values[0], values[0])
2 => (values[0], values[1], values[0], values[1])
3 => (values[0], values[1], values[2], values[1])
_ => (values[0], values[1], values[2], values[3])
}
}
///|
/// Parse a value inside CSS math functions and resolve it to a dimension
/// Handles recursively nested calc(), min(), max(), clamp()
fn parse_css_value_to_px(value : String, ctx : ComputeContext) -> Double? {
let v = value.trim()
if v.is_empty() {
return None
}
// Handle nested calc()
if v.has_prefix("calc(") && v.has_suffix(")") {
let inner = view_to_string(
v.view(start_offset=5, end_offset=v.length() - 1),
)
return parse_simple_calc_expr(inner, ctx)
}
// Handle nested min()
if v.has_prefix("min(") && v.has_suffix(")") {
let inner = view_to_string(
v.view(start_offset=4, end_offset=v.length() - 1),
)
let args = split_css_args(inner)
let mut min_val : Double? = None
for arg in args {
match parse_css_value_to_px(arg, ctx) {
Some(val) =>
match min_val {
None => min_val = Some(val)
Some(current) => if val < current { min_val = Some(val) }
}
None => return None
}
}
return min_val
}
// Handle nested max()
if v.has_prefix("max(") && v.has_suffix(")") {
let inner = view_to_string(
v.view(start_offset=4, end_offset=v.length() - 1),
)
let args = split_css_args(inner)
let mut max_val : Double? = None
for arg in args {
match parse_css_value_to_px(arg, ctx) {
Some(val) =>
match max_val {
None => max_val = Some(val)
Some(current) => if val > current { max_val = Some(val) }
}
None => return None
}
}
return max_val
}
// Handle nested clamp()
if v.has_prefix("clamp(") && v.has_suffix(")") {
let inner = view_to_string(
v.view(start_offset=6, end_offset=v.length() - 1),
)
let args = split_css_args(inner)
if args.length() != 3 {
return None
}
let min_px = parse_css_value_to_px(args[0], ctx)
let val_px = parse_css_value_to_px(args[1], ctx)
let max_px = parse_css_value_to_px(args[2], ctx)
match (min_px, val_px, max_px) {
(Some(min_v), Some(val_v), Some(max_v)) =>
// clamp(min, val, max) == max(min, min(val, max))
return Some(@types.apply_math_op(@types.MathOp::Clamp, [min_v, val_v, max_v]))
_ => return None
}
}
// Parse simple dimension value
let dim = resolve_dimension(v.to_owned(), ctx)
match dim {
@types.Length(px) => Some(px)
@types.Percent(_) => None // Cannot simplify percentages
@types.Calc(_, _) => None // Mixed calc needs a layout basis
@types.MathFn(_, _) => None // min/max/clamp need a layout basis
@types.Auto => None
@types.MinContent => None
@types.MaxContent => None
@types.FitContent(_) => None
}
}
///|
/// Split CSS function arguments by comma (handling nested parentheses)
fn split_css_args(input : String) -> Array[String] {
let result : Array[String] = []
let mut current = StringBuilder::new()
let mut paren_depth = 0
for i = 0; i < input.length(); i = i + 1 {
let c = input[i].to_int().unsafe_to_char()
if c == '(' {
paren_depth += 1
current.write_char(c)
} else if c == ')' {
paren_depth -= 1
current.write_char(c)
} else if c == ',' && paren_depth == 0 {
let s = current.to_string().trim().to_owned()
if !s.is_empty() {
result.push(s)
}
current = StringBuilder::new()
} else {
current.write_char(c)
}
}
let s = current.to_string().trim().to_owned()
if !s.is_empty() {
result.push(s)
}
result
}
///|
/// Parse a simple calc expression (handles + - * / with px values)
fn is_calc_signed_number_start(
expr : String,
index : Int,
token_start : Int,
) -> Bool {
if index != token_start || index + 1 >= expr.length() {
return false
}
let sign = expr[index].to_int().unsafe_to_char()
if sign != '+' && sign != '-' {
return false
}
let next = expr[index + 1].to_int().unsafe_to_char()
(next >= '0' && next <= '9') || next == '.'
}
///|
fn parse_simple_calc_expr(expr : String, ctx : ComputeContext) -> Double? {
let mut result : Double = 0.0
let mut current_op : Char = '+'
let mut i = 0
let mut token_start = 0
while i <= expr.length() {
let c = if i < expr.length() {
expr[i].to_int().unsafe_to_char()
} else {
' '
}
let signed_number_start = if i < expr.length() {
is_calc_signed_number_start(expr, i, token_start)
} else {
false
}
if (c == '+' || c == '-' || c == '*' || c == '/' || i == expr.length()) &&
!signed_number_start {
if i > token_start {
let token = expr.unsafe_substring(start=token_start, end=i).trim()
if !token.is_empty() {
match parse_css_value_to_px(token.to_owned(), ctx) {
Some(val) =>
match current_op {
'+' => result = result + val
'-' => result = result - val
'*' => result = result * val
'/' => result = result / val
_ => ()
}
None => {
// Try parsing as unitless number for * and /
let n = @string.parse_double(token.to_owned()) catch {
_ => return None
}
match current_op {
'*' => result = result * n
'/' => result = result / n
'+' | '-' =>
// Unitless number for + or - is invalid unless 0
if n != 0.0 {
return None
}
_ => ()
}
}
}
}
}
if i < expr.length() {
current_op = c
}
token_start = i + 1
}
i = i + 1
}
Some(result)
}
///|
/// Reduce a single min()/max()/clamp() argument to a linear (px, percent_ratio)
/// form. Returns None for intrinsic/auto values that cannot be reduced.
fn math_arg_lp(arg : String, ctx : ComputeContext) -> (Double, Double)? {
// A bare unitless number is only a valid length argument when it is 0; reject
// non-zero unitless numbers so the result matches the parser layer.
let trimmed = arg.trim().to_owned()
if is_pure_number(trimmed) {
let n = @string.parse_double(trimmed) catch { _ => return None }
return if n == 0.0 { Some((0.0, 0.0)) } else { None }
}
match resolve_dimension(arg, ctx) {
@types.Length(px) => Some((px, 0.0))
@types.Percent(p) => Some((0.0, p))
@types.Calc(px, pct) => Some((px, pct))
_ => None
}
}
///|
/// Collapse a linear (px, percent_ratio) form back to the most specific
/// Dimension. Mirrors parse_calc_string's pure-vs-mixed classification.
fn lp_to_dimension(px : Double, pct : Double) -> @types.Dimension {
if pct.abs() < 0.0001 {
@types.Length(px)
} else if px.abs() < 0.0001 {
@types.Percent(pct)
} else {
@types.Calc(px, pct)
}
}
///|
/// Parse min(), max(), clamp() CSS functions.
///
/// Each argument is reduced to a linear (px, percent_ratio) form so that
/// percentage arguments are preserved instead of collapsing to Auto
/// (crater#282, WPT css/css-values/calc-in-max.html).
/// - single argument: returned as-is (Length/Percent/Calc).
/// - all pure-px arguments: compared by px -> Length.
/// - all pure-percent arguments: the containing-block basis is positive, so
/// ordering by ratio matches ordering by resolved length -> Percent.
/// - mixed px + percent arguments cannot be ordered without a layout basis and
/// are carried as a MathFn dimension, resolved at layout time.
fn parse_css_math_function(
expr : String,
ctx : ComputeContext,
) -> @types.Dimension? {
let v = expr.trim()
let (op, prefix_len) = if v.has_prefix("min(") {
(@types.MathOp::Min, 4)
} else if v.has_prefix("max(") {
(@types.MathOp::Max, 4)
} else if v.has_prefix("clamp(") {
(@types.MathOp::Clamp, 6)
} else {
return None
}
if !v.has_suffix(")") {
return None
}
let inner = view_to_string(
v.view(start_offset=prefix_len, end_offset=v.length() - 1),
)
let raw_args = split_css_args(inner)
if raw_args.is_empty() {
return None
}
if op == @types.MathOp::Clamp && raw_args.length() != 3 {
return None
}
let lps : Array[(Double, Double)] = []
for arg in raw_args {
match math_arg_lp(arg, ctx) {
Some(lp) => lps.push(lp)
None => return None
}
}
// Single min()/max() argument resolves to that argument verbatim.
if lps.length() == 1 {
let (px, pct) = lps[0]
return Some(lp_to_dimension(px, pct))
}
let mut all_px = true
let mut all_pct = true
for lp in lps {
if lp.1 != 0.0 {
all_px = false
}
if lp.0 != 0.0 {
all_pct = false
}
}
if all_px {
Some(@types.Length(@types.apply_math_op(op, lps.map(fn(lp) { lp.0 }))))
} else if all_pct {
Some(@types.Percent(@types.apply_math_op(op, lps.map(fn(lp) { lp.1 }))))
} else {
// Mixed length + percentage: defer to layout via MathFn.
Some(@types.MathFn(op, lps))
}
}
///|
/// Internal calc() value: either a dimensionless number or a length/percentage
/// linear form (`px` plus `pct` in percentage points, e.g. 50.0 for 50%).
priv struct CalcVal {
px : Double
pct : Double
num : Double
is_num : Bool
}
///|
fn calc_num(n : Double) -> CalcVal {
{ px: 0.0, pct: 0.0, num: n, is_num: true }
}
///|
fn calc_lp(px : Double, pct : Double) -> CalcVal {
{ px, pct, num: 0.0, is_num: false }
}
///|
/// Tokens for the calc() expression grammar.
priv enum CalcTok {
CNum(Double, String)
COp(Char)
CLP
CRP
CFun(String)
CComma
}
///|
fn calc_char_at(s : String, i : Int) -> Char {
s[i].to_int().unsafe_to_char()
}
///|
/// Tokenize a calc() inner expression. `+`/`-` are treated as binary operators
/// unless an operand is expected (start, after another operator, after `(` or
/// `,`), in which case they are the sign of the following number.
fn calc_tokenize(s : String) -> Array[CalcTok]? {
let toks : Array[CalcTok] = []
let n = s.length()
let mut i = 0
let mut expect_operand = true
while i < n {
let c = calc_char_at(s, i)
if c == ' ' || c == '\t' || c == '\n' || c == '\r' {
i = i + 1
continue
}
if c == '(' {
toks.push(CLP)
expect_operand = true
i = i + 1
continue
}
if c == ')' {
toks.push(CRP)
expect_operand = false
i = i + 1
continue
}
if c == ',' {
toks.push(CComma)
expect_operand = true
i = i + 1
continue
}
if c == '*' || c == '/' {
toks.push(COp(c))
expect_operand = true
i = i + 1
continue
}
let is_sign = c == '+' || c == '-'
if is_sign && !expect_operand {
toks.push(COp(c))
expect_operand = true
i = i + 1
continue
}
let is_digit = c >= '0' && c <= '9'
if is_digit || c == '.' || (is_sign && expect_operand) {
let start = i
if is_sign {
i = i + 1
}
while i < n {
let d = calc_char_at(s, i)
if (d >= '0' && d <= '9') || d == '.' {
i = i + 1
} else {
break
}
}
if i < n {
let e = calc_char_at(s, i)
if e == 'e' || e == 'E' {
// Only an exponent if `e` is followed by [+-]?digit; otherwise the
// `e` begins a unit (e.g. the "em"/"ex" in "20em", "2ex").
let mut j = i + 1
if j < n {
let sgn = calc_char_at(s, j)
if sgn == '+' || sgn == '-' {
j = j + 1
}
}
if j < n && calc_char_at(s, j) >= '0' && calc_char_at(s, j) <= '9' {
i = j + 1
while i < n {
let d = calc_char_at(s, i)
if d >= '0' && d <= '9' {
i = i + 1
} else {
break
}
}
}
}
}
let num_str = s.unsafe_substring(start~, end=i)
let value = @string.parse_double(num_str) catch { _ => return None }
let ustart = i
while i < n {
let d = calc_char_at(s, i)
if (d >= 'a' && d <= 'z') || (d >= 'A' && d <= 'Z') || d == '%' {
i = i + 1
} else {
break
}
}
let unit = s.unsafe_substring(start=ustart, end=i).to_lower()
toks.push(CNum(value, unit))
expect_operand = false
continue
}
if (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') {
let start = i
while i < n {
let d = calc_char_at(s, i)
if (d >= 'a' && d <= 'z') || (d >= 'A' && d <= 'Z') {
i = i + 1
} else {
break
}
}
let name = s.unsafe_substring(start~, end=i).to_lower()
toks.push(CFun(name))
expect_operand = true
continue
}
return None
}
Some(toks)
}
///|
/// Resolve a numeric token with its unit into a CalcVal.
fn calc_unit_value(
value : Double,
unit : String,
ctx : ComputeContext,
) -> CalcVal? {
if unit == "" {
return Some(calc_num(value))
}
if unit == "%" {
return Some(calc_lp(0.0, value))
}
match resolve_dimension("\{value}\{unit}", ctx) {
@types.Length(px) => Some(calc_lp(px, 0.0))
@types.Percent(p) => Some(calc_lp(0.0, p * 100.0))
_ => None
}
}
///|
/// Resolve a CalcVal to pixels for min()/max()/clamp() comparisons, resolving
/// percentages against a width reference.
fn calc_to_px(cv : CalcVal, ctx : ComputeContext) -> Double {
if cv.is_num {
cv.num
} else {
cv.px + cv.pct / 100.0 * mixed_calc_width_reference(ctx)
}
}
///|
fn calc_add(a : CalcVal, b : CalcVal, sign : Double) -> CalcVal? {
if a.is_num && b.is_num {
Some(calc_num(a.num + sign * b.num))
} else if !a.is_num && !b.is_num {
Some(calc_lp(a.px + sign * b.px, a.pct + sign * b.pct))
} else {
None
}
}
///|
fn calc_mul(a : CalcVal, b : CalcVal) -> CalcVal? {
if a.is_num && b.is_num {
Some(calc_num(a.num * b.num))
} else if a.is_num {
Some(calc_lp(b.px * a.num, b.pct * a.num))
} else if b.is_num {
Some(calc_lp(a.px * b.num, a.pct * b.num))
} else {
None
}
}
///|
fn calc_div(a : CalcVal, b : CalcVal) -> CalcVal? {
if !b.is_num || b.num == 0.0 {
return None
}
if a.is_num {
Some(calc_num(a.num / b.num))
} else {
Some(calc_lp(a.px / b.num, a.pct / b.num))
}
}
///|
/// primary := number | '(' sum ')' | fn '(' sum (',' sum)* ')'
fn calc_parse_primary(
toks : Array[CalcTok],
pos : Int,
ctx : ComputeContext,
) -> (CalcVal, Int)? {
if pos >= toks.length() {
return None
}
match toks[pos] {
CNum(v, unit) =>
match calc_unit_value(v, unit, ctx) {
Some(cv) => Some((cv, pos + 1))
None => None
}
CLP =>
match calc_parse_sum(toks, pos + 1, ctx) {
Some((v, p2)) =>
if p2 < toks.length() &&
(match toks[p2] {
CRP => true
_ => false
}) {
Some((v, p2 + 1))
} else {
None
}
None => None
}
CFun(name) => {
if pos + 1 >= toks.length() {
return None
}
match toks[pos + 1] {
CLP => ()
_ => return None
}
let args : Array[CalcVal] = []
let mut p = pos + 2
let mut closed = false
while !closed {
match calc_parse_sum(toks, p, ctx) {
Some((a, np)) => {
args.push(a)
if np >= toks.length() {
return None
}
match toks[np] {
CComma => p = np + 1
CRP => {
p = np + 1
closed = true
}
_ => return None
}
}
None => return None
}
}
match name {
"calc" => if args.length() == 1 { Some((args[0], p)) } else { None }
"min" => {
if args.is_empty() {
return None
}
let mut m = calc_to_px(args[0], ctx)
for k = 1; k < args.length(); k = k + 1 {
let x = calc_to_px(args[k], ctx)
if x < m {
m = x
}
}
Some((calc_lp(m, 0.0), p))
}
"max" => {
if args.is_empty() {
return None
}
let mut m = calc_to_px(args[0], ctx)
for k = 1; k < args.length(); k = k + 1 {
let x = calc_to_px(args[k], ctx)
if x > m {
m = x
}
}
Some((calc_lp(m, 0.0), p))
}
"clamp" => {
if args.length() != 3 {
return None
}
let lo = calc_to_px(args[0], ctx)
let mid = calc_to_px(args[1], ctx)
let hi = calc_to_px(args[2], ctx)
let mut r = mid
if r < lo {
r = lo
}
if r > hi {
r = hi
}
Some((calc_lp(r, 0.0), p))
}
_ => None
}
}
_ => None
}
}
///|
/// product := primary (('*' | '/') primary)*
fn calc_parse_product(
toks : Array[CalcTok],
pos : Int,
ctx : ComputeContext,
) -> (CalcVal, Int)? {
match calc_parse_primary(toks, pos, ctx) {
Some((first, p1)) => {
let mut acc = first
let mut p = p1
let mut stop = false
while !stop && p < toks.length() {
let op = match toks[p] {
COp('*') => '*'
COp('/') => '/'
_ => ' '
}
if op == ' ' {
stop = true
} else {
match calc_parse_primary(toks, p + 1, ctx) {
Some((rhs, p2)) => {
let combined = if op == '*' {
calc_mul(acc, rhs)
} else {
calc_div(acc, rhs)
}
match combined {
Some(v) => {
acc = v
p = p2
}
None => return None
}
}
None => return None
}
}
}
Some((acc, p))
}
None => None
}
}
///|
/// sum := product (('+' | '-') product)*
fn calc_parse_sum(
toks : Array[CalcTok],
pos : Int,
ctx : ComputeContext,
) -> (CalcVal, Int)? {
match calc_parse_product(toks, pos, ctx) {
Some((first, p1)) => {
let mut acc = first
let mut p = p1
let mut stop = false
while !stop && p < toks.length() {
let sign = match toks[p] {
COp('+') => 1.0
COp('-') => -1.0
_ => 0.0
}
if sign == 0.0 {
stop = true
} else {
match calc_parse_product(toks, p + 1, ctx) {
Some((rhs, p2)) =>
match calc_add(acc, rhs, sign) {
Some(v) => {
acc = v
p = p2
}
None => return None
}
None => return None
}
}
}
Some((acc, p))
}
None => None
}
}
///|
/// Precedence-aware calc() evaluator. Returns (px, percent_ratio) or None if
/// the expression is malformed or not fully understood (callers then fall back
/// to the legacy left-to-right evaluator).
fn eval_calc(expr : String, ctx : ComputeContext) -> (Double, Double)? {
let v = expr.trim()
if !v.has_prefix("calc(") || !v.has_suffix(")") {
return None
}
let inner = view_to_string(v.view(start_offset=5, end_offset=v.length() - 1))
let toks = match calc_tokenize(inner) {
Some(t) => t
None => return None
}
if toks.is_empty() {
return None
}
match calc_parse_sum(toks, 0, ctx) {
Some((result, p)) =>
// Trailing tokens or a bare (dimensionless) result is a parse failure.
if p != toks.length() || result.is_num {
None
} else {
Some((result.px, result.pct / 100.0))
}
None => None
}
}
///|
/// Parse calc() terms and return (length_px, percent_ratio); percent_ratio is
/// 0.5 for 50%. Delegates to the precedence-aware recursive-descent evaluator.
/// Returns None for malformed or semantically invalid calc() (e.g. a bare
/// number, length × length, or division by zero), so the property falls back to
/// its initial value rather than a silently wrong length.
fn parse_calc_terms(expr : String, ctx : ComputeContext) -> (Double, Double)? {
eval_calc(expr, ctx)
}
///|
/// Extract first percentage token from calc() expression as ratio.
/// Used as a fallback when mixed calc contains multiplicative terms.
fn extract_first_percent_ratio(expr : String) -> Double? {
let mut i = 0
while i < expr.length() {
if expr[i].to_int().unsafe_to_char() == '%' {
let mut start = i
while start > 0 {
let c = expr[start - 1].to_int().unsafe_to_char()
if (c >= '0' && c <= '9') || c == '.' {
start = start - 1
continue
}
if c == '+' || c == '-' {
start = start - 1
}
break
}
let num = expr.unsafe_substring(start~, end=i).trim()
if !num.is_empty() {
let n = @string.parse_double(num.to_owned()) catch { _ => return None }
return Some(n / 100.0)
}
}
i = i + 1
}
None
}
///|
/// Parse a calc() expression from a string and try to simplify it
/// Returns Some(dimension) if simplification is possible, None otherwise
fn parse_calc_string(expr : String, ctx : ComputeContext) -> @types.Dimension? {
match parse_calc_terms(expr, ctx) {
Some((result_px, result_percent)) =>
if result_percent.abs() < 0.0001 {
Some(@types.Length(result_px))
} else if result_px.abs() < 0.0001 {
Some(@types.Percent(result_percent))
} else {
Some(@types.Calc(result_px, result_percent))
}
None => None
}
}
///|
/// Choose a width reference for mixed calc() fallback.
/// Prefer parent's definite width when available; otherwise use viewport width.
fn mixed_calc_width_reference(ctx : ComputeContext) -> Double {
match ctx.parent_style {
Some(parent) =>
match parent.width {
@types.Length(w) => w
@types.Percent(p) => ctx.viewport_width * p
_ => ctx.viewport_width
}
None => ctx.viewport_width
}
}
///|
/// Fallback for mixed calc() on properties that need concrete dimensions.
/// For expressions like calc(50% - 10px), resolve using a width reference.
fn resolve_dimension_with_calc_percent_fallback(
value : String,
ctx : ComputeContext,
) -> @types.Dimension {
let dim = resolve_dimension(value, ctx)
match dim {
@types.Auto =>
if value.trim().has_prefix("calc(") {
match parse_calc_terms(value, ctx) {
Some((length_px, percent_ratio)) =>
if percent_ratio.abs() >= 0.0001 {
let percent_for_resolution = if value.contains("*") ||
value.contains("/") {
match extract_first_percent_ratio(value) {
Some(p) => p
None => percent_ratio
}
} else {
percent_ratio
}
if length_px.abs() >= 0.0001 {
// Preserve mixed calc() losslessly (px + percent) so layout
// can resolve it against the correct containing-block basis,
// e.g. table column widths. See mizchi/crater#67.
@types.Calc(length_px, percent_for_resolution)
} else {
let basis = mixed_calc_width_reference(ctx)
@types.Length(basis * percent_for_resolution + length_px)
}
} else if length_px.abs() >= 0.0001 {
@types.Length(length_px)
} else {
@types.Length(0.0)
}
None => dim
}
} else {
dim
}
_ => dim
}
}
///|
/// Compatibility fallback for mixed calc() in min-size properties.
/// keep current Dimension model and pick axis-specific term when mixed.
fn resolve_dimension_with_mixed_calc_fallback(
property : String,
value : String,
ctx : ComputeContext,
) -> @types.Dimension {
let dim = resolve_dimension(value, ctx)
match dim {
@types.Auto =>
if value.trim().has_prefix("calc(") {
match parse_calc_terms(value, ctx) {
Some((length_px, percent_ratio)) =>
if length_px.abs() >= 0.0001 && percent_ratio.abs() >= 0.0001 {
// Preserve mixed calc() losslessly; resolved at layout time
// against the property's containing-block basis. crater#67.
ignore(property)
@types.Calc(length_px, percent_ratio)
} else {
dim
}
None => dim
}
} else {
dim
}
_ => dim
}
}
///|
/// Resolve CSS var() function with context lookup
/// Looks up variable in context, falls back to provided fallback value if not found
fn resolve_var(value : String, ctx : ComputeContext) -> String {
let v = value.trim().to_owned()
if !v.has_prefix("var(") || !v.has_suffix(")") {
return v
}
// Extract content inside var(...) - skip "var(" (4 chars) and ")" (1 char)
let inner : String = view_to_string(
v.view(start_offset=4, end_offset=v.length() - 1),
)
// Find the comma separating variable name from fallback
// Need to handle nested var() and other functions
let mut paren_depth = 0
let mut comma_pos = -1
for i = 0; i < inner.length(); i = i + 1 {
let c = inner[i].to_int().unsafe_to_char()
if c == '(' {
paren_depth += 1
} else if c == ')' {
paren_depth -= 1
} else if c == ',' && paren_depth == 0 {
comma_pos = i
break
}
}
// Extract variable name as String
let var_name : String = if comma_pos > 0 {
let slice = inner.unsafe_substring(start=0, end=comma_pos)
slice.trim().to_owned()
} else {
inner.trim().to_owned()
}
// Look up variable in context
match ctx.custom_properties.get(var_name) {
Some(var_value) =>
// Variable found - recursively resolve if it contains var()
if var_value.contains("var(") {
resolve_all_vars(var_value, ctx)
} else {
var_value
}
None =>
// Variable not found - use fallback if available
if comma_pos > 0 {
let fallback_slice = inner.unsafe_substring(
start=comma_pos + 1,
end=inner.length(),
)
let fallback = fallback_slice.trim().to_owned()
// Recursively resolve fallback (it may also contain var())
resolve_all_vars(fallback, ctx)
} else {
// No fallback and no variable - return empty (invalid value)
""
}
}
}
///|
/// Resolve all var() references in a value string
fn resolve_all_vars(value : String, ctx : ComputeContext) -> String {
let v = value.trim()
// If no var() in value, return as-is
if !v.contains("var(") {
return v.to_owned()
}
// Simple case: entire value is a var()
if v.has_prefix("var(") && v.has_suffix(")") {
// Check if there's only one var() (no content after)
let mut depth = 0
let mut end_pos = 0
for i = 0; i < v.length(); i = i + 1 {
let c = v[i].to_int().unsafe_to_char()
if c == '(' {
depth += 1
} else if c == ')' {
depth -= 1
if depth == 0 {
end_pos = i
break
}
}
}
if end_pos == v.length() - 1 {
// Entire value is a single var()
return resolve_var(v.to_owned(), ctx)
}
}
// Complex case: var() embedded in value like "10px var(--gap) 20px"
// Need to replace each var() with its resolved value
let result = StringBuilder::new()
let mut i = 0
while i < v.length() {
// Look for "var("
if i + 4 <= v.length() {
let substr = view_to_string(v.view(start_offset=i, end_offset=i + 4))
if substr == "var(" {
// Find matching closing paren
let mut depth = 1
let mut j = i + 4
while j < v.length() && depth > 0 {
let c = v[j].to_int().unsafe_to_char()
if c == '(' {
depth += 1
} else if c == ')' {
depth -= 1
}
j += 1
}
// Extract and resolve the var()
let var_expr = view_to_string(v.view(start_offset=i, end_offset=j))
let resolved = resolve_var(var_expr, ctx)
result.write_string(resolved)
i = j
continue
}
}
result.write_char(v[i].to_int().unsafe_to_char())
i += 1
}
result.to_string()
}
///|
fn strip_css_ascii_whitespace(value : String) -> String {
let sb = StringBuilder::new()
for c in value.iter() {
if !is_css_ascii_whitespace(c) {
sb.write_char(c)
}
}
sb.to_string()
}
///|
fn is_css_ascii_whitespace(c : Char) -> Bool {
c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '\u000C'
}
///|
fn is_css_ident_char(c : Char) -> Bool {
(c >= 'a' && c <= 'z') ||
(c >= 'A' && c <= 'Z') ||
(c >= '0' && c <= '9') ||
c == '-' ||
c == '_'
}
///|
fn css_math_function_prefix_len(value : String, index : Int) -> Int {
if index > 0 {
let prev = value[index - 1].to_int().unsafe_to_char()
if is_css_ident_char(prev) {
return 0
}
}
let tail = view_to_string(value.view(start_offset=index))
if tail.has_prefix("calc(") {
5
} else if tail.has_prefix("min(") {
4
} else if tail.has_prefix("max(") {
4
} else if tail.has_prefix("clamp(") {
6
} else {
0
}
}
///|
fn find_matching_function_end(value : String, index : Int) -> Int {
let mut depth = 0
let mut i = index
while i < value.length() {
let c = value[i].to_int().unsafe_to_char()
if c == '(' {
depth = depth + 1
} else if c == ')' {
depth = depth - 1
if depth == 0 {
return i
}
}
i = i + 1
}
-1
}
///|
fn dimension_to_grid_token(dim : @types.Dimension) -> String? {
match dim {
@types.Length(v) => Some(v.to_string() + "px")
@types.Percent(p) => Some((p * 100.0).to_string() + "%")
_ => None
}
}
///|
fn normalize_grid_template_math_functions(
value : String,
ctx : ComputeContext,
) -> String {
let out = StringBuilder::new()
let mut i = 0
while i < value.length() {
let prefix_len = css_math_function_prefix_len(value, i)
if prefix_len > 0 {
let end = find_matching_function_end(value, i + prefix_len - 1)
if end > i {
let expr = view_to_string(
value.view(start_offset=i, end_offset=end + 1),
)
match dimension_to_grid_token(resolve_dimension(expr, ctx)) {
Some(token) => {
out.write_string(token)
i = end + 1
continue
}
None => ()
}
}
}
out.write_char(value[i].to_int().unsafe_to_char())
i = i + 1
}
out.to_string()
}
///|
/// Resolve dimension with relative unit conversion
///|
/// Split a "" string into its numeric value and unit. Returns
/// None when there is no leading number. `e`/`E` only begins an exponent when
/// followed by [+-]?digit, so unit strings like "em"/"ex" are not mis-scanned.
fn split_dimension(v : String) -> (Double, String)? {
let n = v.length()
if n == 0 {
return None
}
let mut i = 0
let c0 = v[0].to_int().unsafe_to_char()
if c0 == '+' || c0 == '-' {
i = i + 1
}
let digits_start = i
while i < n {
let c = v[i].to_int().unsafe_to_char()
if (c >= '0' && c <= '9') || c == '.' {
i = i + 1
} else {
break
}
}
if i < n {
let e = v[i].to_int().unsafe_to_char()
if e == 'e' || e == 'E' {
let mut j = i + 1
if j < n {
let s = v[j].to_int().unsafe_to_char()
if s == '+' || s == '-' {
j = j + 1
}
}
if j < n &&
v[j].to_int().unsafe_to_char() >= '0' &&
v[j].to_int().unsafe_to_char() <= '9' {
i = j + 1
while i < n {
let d = v[i].to_int().unsafe_to_char()
if d >= '0' && d <= '9' {
i = i + 1
} else {
break
}
}
}
}
}
if i == digits_start {
return None
}
let num = @string.parse_double(v.unsafe_substring(start=0, end=i)) catch {
_ => return None
}
Some((num, v.unsafe_substring(start=i, end=n)))
}
///|
fn resolve_dimension(value : String, ctx : ComputeContext) -> @types.Dimension {
// First resolve any var() functions
let raw_v = resolve_all_vars(value, ctx).trim()
// CSS units and keywords are ASCII case-insensitive. var() names are already
// resolved above, so lowercasing what remains (a number+unit or a keyword) is
// safe for the matching below.
let v = strip_css_ascii_whitespace(raw_v.to_owned()).to_lower()
if v == "auto" || v == "none" {
return @types.Dimension::Auto
}
// Handle intrinsic sizing keywords
if v == "min-content" {
return @types.Dimension::MinContent
}
if v == "max-content" {
return @types.Dimension::MaxContent
}
if v == "fit-content" {
// fit-content without argument is equivalent to fit-content(max-content)
return @types.Dimension::FitContent(1.0e10)
}
// Handle calc() expressions
if view_starts_with(raw_v, "calc(") {
match parse_calc_string(raw_v.to_owned(), ctx) {
Some(dim) => return dim
None => return @types.Dimension::Auto // Fallback for complex calc
}
}
// Handle min(), max(), clamp() CSS math functions
if view_starts_with(raw_v, "min(") ||
view_starts_with(raw_v, "max(") ||
view_starts_with(raw_v, "clamp(") {
match parse_css_math_function(raw_v.to_owned(), ctx) {
Some(dim) => return dim
None => return @types.Dimension::Auto // Fallback for complex expressions
}
}
// Extract the unit once and dispatch by equality. The string suffix tests
// this replaces cost ~190ns each; for the common px/% values the old cascade
// ran 20+ of them before falling through. ch/ex keep the historical 0.5em
// approximation for generic fonts.
match split_dimension(v) {
Some((n, unit)) => {
let resolved : @types.Dimension? = match unit {
"px" => Some(@types.Length(n))
"%" => Some(@types.Percent(n / 100.0))
"rem" => Some(@types.Length(n * ctx.root_font_size))
"em" => Some(@types.Length(n * ctx.font_size))
"vw" | "dvw" | "svw" | "lvw" | "vi" =>
Some(@types.Length(n * ctx.viewport_width / 100.0))
"vh" | "dvh" | "svh" | "lvh" | "vb" =>
Some(@types.Length(n * ctx.viewport_height / 100.0))
"vmin" => {
let base = if ctx.viewport_width < ctx.viewport_height {
ctx.viewport_width
} else {
ctx.viewport_height
}
Some(@types.Length(n * base / 100.0))
}
"vmax" => {
let base = if ctx.viewport_width > ctx.viewport_height {
ctx.viewport_width
} else {
ctx.viewport_height
}
Some(@types.Length(n * base / 100.0))
}
"ch" => Some(@types.Length(n * ctx.font_size * ch_unit_ratio(ctx)))
"ex" => Some(@types.Length(n * ctx.font_size * 0.5))
"pt" => Some(@types.Length(n * 96.0 / 72.0))
"pc" => Some(@types.Length(n * 16.0))
"in" => Some(@types.Length(n * 96.0))
"cm" => Some(@types.Length(n * 96.0 / 2.54))
"mm" => Some(@types.Length(n * 96.0 / 25.4))
"q" => Some(@types.Length(n * 96.0 / 101.6))
_ => None
}
match resolved {
Some(d) => return d
None => ()
}
}
None => ()
}
// Fall back for unitless, fit-content(...) and unknown units.
parse_dimension(v.to_string())
}
///|
/// Check if a string is a pure number (no units)
fn is_pure_number(s : String) -> Bool {
let v = s.trim()
if v.is_empty() {
return false
}
// Try to parse as double - will fail if it has units
let _ = @string.parse_double(v.to_owned()) catch { _ => return false }
// Make sure it doesn't have any unit suffixes that parse_double might accept
for i = 0; i < v.length(); i = i + 1 {
let c = v[i]
if !(c == '0' ||
c == '1' ||
c == '2' ||
c == '3' ||
c == '4' ||
c == '5' ||
c == '6' ||
c == '7' ||
c == '8' ||
c == '9' ||
c == '.' ||
c == '-' ||
c == '+') {
return false
}
}
true
}
///|
/// Parse CSS transform property
/// Supports translate, scale, rotate, and skew in the 2D hit-test subset.
fn parse_transform(value : String) -> @style.Transform {
let v = value.trim().to_lower()
// Handle none
if v == "none" || v.is_empty() {
return @style.Transform::none()
}
let mut translate_x = @style.TranslateValue::Length(0.0)
let mut translate_y = @style.TranslateValue::Length(0.0)
let mut scale_x = 1.0
let mut scale_y = 1.0
let mut rotate_degrees = 0.0
let mut skew_x_degrees = 0.0
let mut skew_y_degrees = 0.0
let mut has_matrix = false
let mut matrix_a = 1.0
let mut matrix_b = 0.0
let mut matrix_c = 0.0
let mut matrix_d = 1.0
let mut matrix_e = 0.0
let mut matrix_f = 0.0
let mut matrix3d : Array[Double] = []
// Parse transform functions
// We handle each function in sequence
let chars = v.to_array()
let len = chars.length()
let mut i = 0
while i < len {
// Skip whitespace
while i < len && (chars[i] == ' ' || chars[i] == '\t') {
i = i + 1
}
if i >= len {
break
}
// Find function name
let func_start = i
while i < len && chars[i] != '(' {
i = i + 1
}
if i >= len {
break
}
let func_name = chars_to_string(chars[func_start:i])
i = i + 1 // Skip '('
// Find function arguments (handle nested parens)
let args_start = i
let mut paren_depth = 1
while i < len && paren_depth > 0 {
if chars[i] == '(' {
paren_depth = paren_depth + 1
} else if chars[i] == ')' {
paren_depth = paren_depth - 1
}
i = i + 1
}
let args_str = chars_to_string(chars[args_start:i - 1])
// Parse transform function
match func_name.trim() {
"translate" => {
// translate(x) or translate(x, y)
let args = split_by_comma(args_str)
if args.length() >= 1 {
translate_x = parse_translate_value(args[0])
}
if args.length() >= 2 {
translate_y = parse_translate_value(args[1])
}
}
"translatex" => translate_x = parse_translate_value(args_str)
"translatey" => translate_y = parse_translate_value(args_str)
"translate3d" => {
// translate3d(x, y, z) - we only use x and y
let args = split_by_comma(args_str)
if args.length() >= 1 {
translate_x = parse_translate_value(args[0])
}
if args.length() >= 2 {
translate_y = parse_translate_value(args[1])
}
}
"scale" => {
// scale(x) or scale(x, y)
let args = split_by_comma(args_str)
if args.length() >= 1 {
let sx = parse_scale_value(args[0])
scale_x = scale_x * sx
scale_y = scale_y * sx // If only one arg, use same scale for both
}
if args.length() >= 2 {
scale_y = scale_y /
scale_x *
parse_scale_value(args[1]) *
scale_x /
scale_x
// Actually: if second arg provided, override scale_y
scale_y = parse_scale_value(args[1])
}
}
"scalex" => scale_x = scale_x * parse_scale_value(args_str)
"scaley" => scale_y = scale_y * parse_scale_value(args_str)
"scale3d" => {
// scale3d(x, y, z) - we only use x and y
let args = split_by_comma(args_str)
if args.length() >= 1 {
scale_x = scale_x * parse_scale_value(args[0])
}
if args.length() >= 2 {
scale_y = scale_y * parse_scale_value(args[1])
}
}
"rotate" =>
rotate_degrees = rotate_degrees + parse_rotate_degrees(args_str)
"skewx" => skew_x_degrees += parse_rotate_degrees(args_str)
"skewy" => skew_y_degrees += parse_rotate_degrees(args_str)
"skew" => {
let args = split_by_comma(args_str)
if args.length() >= 1 {
skew_x_degrees += parse_rotate_degrees(args[0])
}
if args.length() >= 2 {
skew_y_degrees += parse_rotate_degrees(args[1])
}
}
"matrix" => {
let args = split_by_comma(args_str)
if args.length() >= 6 {
has_matrix = true
matrix_a = parse_scale_value(args[0])
matrix_b = parse_scale_value(args[1])
matrix_c = parse_scale_value(args[2])
matrix_d = parse_scale_value(args[3])
matrix_e = parse_scale_value(args[4])
matrix_f = parse_scale_value(args[5])
}
}
"matrix3d" => {
let args = split_by_comma(args_str)
if args.length() >= 16 {
has_matrix = true
matrix_a = parse_scale_value(args[0])
matrix_b = parse_scale_value(args[1])
matrix_c = parse_scale_value(args[4])
matrix_d = parse_scale_value(args[5])
matrix_e = parse_scale_value(args[12])
matrix_f = parse_scale_value(args[13])
matrix3d = []
for k = 0; k < 16; k = k + 1 {
matrix3d.push(parse_scale_value(args[k]))
}
}
}
_ => () // Ignore unsupported functions like perspective.
}
}
{
translate_x,
translate_y,
scale_x,
scale_y,
rotate_degrees,
skew_x_degrees,
skew_y_degrees,
has_matrix,
matrix_a,
matrix_b,
matrix_c,
matrix_d,
matrix_e,
matrix_f,
matrix3d,
}
}
///|
fn parse_rotate_degrees(value : String) -> Double {
let v = value.trim()
if v.is_empty() {
return 0.0
}
if v.has_suffix("deg") {
let num_str = view_to_string(v.view(end_offset=v.length() - 3))
return @string.parse_double(num_str.trim()) catch { _ => 0.0 }
}
if v.has_suffix("turn") {
let num_str = view_to_string(v.view(end_offset=v.length() - 4))
let turns = @string.parse_double(num_str.trim()) catch { _ => return 0.0 }
return turns * 360.0
}
// `grad` must be checked before `rad`: it also ends with "rad".
// 400grad = 360deg, so 1grad = 0.9deg.
if v.has_suffix("grad") {
let num_str = view_to_string(v.view(end_offset=v.length() - 4))
let gradians = @string.parse_double(num_str.trim()) catch {
_ => return 0.0
}
return gradians * 0.9
}
if v.has_suffix("rad") {
let num_str = view_to_string(v.view(end_offset=v.length() - 3))
let radians = @string.parse_double(num_str.trim()) catch { _ => return 0.0 }
return radians * 57.29577951308232
}
@string.parse_double(v) catch {
_ => 0.0
}
}
///|
/// Parse a scale value (number, no unit)
fn parse_scale_value(value : String) -> Double {
let v = value.trim()
if v.is_empty() {
return 1.0
}
@string.parse_double(v) catch {
_ => 1.0
}
}
///|
/// Parse CSS zoom property
/// Supports: normal, number, percentage
fn parse_zoom(value : String) -> Double {
let v = value.trim().to_lower()
if v.is_empty() || v == "normal" {
return 1.0
}
// Check for percentage (e.g., "150%")
if v.has_suffix("%") {
let num_str = view_to_string(v.view(end_offset=v.length() - 1))
let n = @string.parse_double(num_str.trim()) catch { _ => return 1.0 }
return n / 100.0
}
// Parse as number (e.g., "1.5" or "2")
@string.parse_double(v) catch {
_ => 1.0
}
}
///|
/// Parse a single translate value (can be length or percentage)
fn parse_translate_value(value : String) -> @style.TranslateValue {
let v = value.trim()
if v.is_empty() {
return @style.TranslateValue::Length(0.0)
}
// Check for percentage
if v.has_suffix("%") {
let num_str = view_to_string(v.view(end_offset=v.length() - 1))
let n = @string.parse_double(num_str.trim()) catch {
_ => return @style.TranslateValue::Length(0.0)
}
@style.TranslateValue::Percent(n / 100.0)
} else if v.has_suffix("px") {
let num_str = view_to_string(v.view(end_offset=v.length() - 2))
let n = @string.parse_double(num_str.trim()) catch {
_ => return @style.TranslateValue::Length(0.0)
}
@style.TranslateValue::Length(n)
} else if v.has_suffix("em") || v.has_suffix("rem") {
// em/rem - convert to pixels assuming 16px base
let suffix_len = if v.has_suffix("rem") { 3 } else { 2 }
let num_str = view_to_string(v.view(end_offset=v.length() - suffix_len))
let n = @string.parse_double(num_str.trim()) catch {
_ => return @style.TranslateValue::Length(0.0)
}
@style.TranslateValue::Length(n * 16.0)
} else {
// Try parsing as plain number (assumed px)
let n = @string.parse_double(v) catch {
_ => return @style.TranslateValue::Length(0.0)
}
@style.TranslateValue::Length(n)
}
}
///|
/// Split string by comma (for transform arguments)
fn split_by_comma(s : String) -> Array[String] {
let result : Array[String] = []
let current = StringBuilder::new()
for c in s {
if c == ',' {
result.push(current.to_string())
current.reset()
} else {
current.write_char(c)
}
}
let last = current.to_string()
if !last.is_empty() {
result.push(last)
}
result
}
///|
/// Convert char array slice to string
fn chars_to_string(chars : ArrayView[Char]) -> String {
let sb = StringBuilder::new()
for c in chars {
sb.write_char(c)
}
sb.to_string()
}
///|
/// Check if a string is a flex-basis value (has units, % or is auto/content)
fn is_flex_basis_value(s : String) -> Bool {
let v = s.trim().to_lower()
if v == "auto" || v == "content" {
return true
}
// Check for common dimension suffixes
v.has_suffix("%") ||
v.has_suffix("px") ||
v.has_suffix("em") ||
v.has_suffix("rem") ||
v.has_suffix("dvw") ||
v.has_suffix("svw") ||
v.has_suffix("lvw") ||
v.has_suffix("vw") ||
v.has_suffix("dvh") ||
v.has_suffix("svh") ||
v.has_suffix("lvh") ||
v.has_suffix("vh")
}
///|
fn strip_inline_important_marker(
value : String,
) -> (String, @cascade.Importance) {
let trimmed = value.trim().to_owned()
let compact = strip_css_ascii_whitespace(trimmed.to_lower())
if !compact.has_suffix("!important") {
return (trimmed, @cascade.Importance::Normal)
}
let important_len = 9
if trimmed.length() <= important_len {
return (trimmed, @cascade.Importance::Normal)
}
let mut bang_pos = trimmed.length() - important_len
while bang_pos > 0 {
let c = trimmed[bang_pos - 1].to_int().unsafe_to_char()
if is_css_ascii_whitespace(c) {
bang_pos -= 1
} else {
break
}
}
if bang_pos <= 0 {
return (trimmed, @cascade.Importance::Normal)
}
if trimmed[bang_pos - 1].to_int().unsafe_to_char() != '!' {
return (trimmed, @cascade.Importance::Normal)
}
let stripped = view_to_string(trimmed.view(end_offset=bang_pos - 1))
.trim()
.to_owned()
if stripped.is_empty() {
return (trimmed, @cascade.Importance::Normal)
}
(stripped, @cascade.Importance::Important)
}
///|
/// Apply inheritance for inherited properties not in cascaded values
fn apply_inheritance(
builder : StyleBuilder,
cascaded : @cascade.CascadedValues,
ctx : ComputeContext,
) -> Unit {
// List of inherited properties we support
let inherited_props = [
"direction", "writing-mode", "text-align", "line-height", "font-size", "font-family",
"font-weight", "font-style", "color", "visibility", "white-space", "pointer-events",
]
for prop in inherited_props {
let covered_by_font_shorthand = cascaded.has("font") &&
(
prop == "font-size" ||
prop == "line-height" ||
prop == "font-family" ||
prop == "font-weight"
)
// If property not in cascaded values, inherit from parent
if !cascaded.has(prop) && !covered_by_font_shorthand {
match ctx.parent_style {
Some(parent) => {
let value = get_style_value_as_string(prop, parent)
apply_property(builder, prop, value, ctx)
}
None => ()
}
}
}
}
///|
/// Compute style from inline style string (raw, without box-sizing adjustment)
/// Use this when merging individual properties into an existing style.
/// The caller is responsible for calling adjust_for_box_sizing on the final result.
pub fn compute_inline_raw(
inline_css : String,
ctx : ComputeContext,
) -> @style.Style {
// Parse inline style to declarations
let decls : Array[@cascade.Declaration] = []
// Simple parsing: split by semicolon
let pairs = inline_css.split(";")
let mut order = 0
for pair in pairs {
let pair_str = pair.to_owned().trim()
if pair_str.is_empty() {
continue
}
// Find colon
let mut colon_pos = -1
for i = 0; i < pair_str.length(); i = i + 1 {
if pair_str[i].to_int().unsafe_to_char() == ':' {
colon_pos = i
break
}
}
if colon_pos > 0 {
let prop = view_to_string(pair_str.view(end_offset=colon_pos)).trim()
let val = view_to_string(pair_str.view(start_offset=colon_pos + 1)).trim()
if !prop.is_empty() && !val.is_empty() {
let (value, importance) = strip_inline_important_marker(val.to_owned())
decls.push(
@cascade.Declaration::with_metadata(
prop.to_owned(),
@cascade.PropertyValue::Value(value),
@cascade.Origin::Author,
importance,
{ a: 1000, b: 0, c: 0 },
order,
),
)
order += 1
}
}
}
// Cascade
let cascaded = @cascade.cascade(decls)
// Compute (without box-sizing adjustment)
compute(cascaded, ctx)
}
///|
/// Compute style from inline style string
/// This is the standard function that applies box-sizing adjustment.
pub fn compute_inline(
inline_css : String,
ctx : ComputeContext,
) -> @style.Style {
let style = compute_inline_raw(inline_css, ctx)
// Convert content-box dimensions to border-box dimensions
// The layout engine always works with outer (border-box) dimensions
adjust_for_box_sizing(style)
}
///|
/// Apply a single CSS property to an existing style directly
/// This skips the CSS string parsing step for better performance
pub fn apply_property_direct(
style : @style.Style,
property : String,
value : String,
ctx : ComputeContext,
) -> @style.Style {
// Special handling for font-size: when font-size changes,
// update line-height proportionally to maintain the ratio
if property == "font-size" {
let builder = StyleBuilder::from_style(style)
let old_fs = style.font_size
apply_property(builder, property, value, ctx)
let new_fs = builder.font_size
// Maintain line-height / font-size ratio
let lh_ratio = if old_fs > 0.0 { style.line_height / old_fs } else { 1.0 }
builder.line_height = new_fs * lh_ratio
return builder.build()
}
let builder = StyleBuilder::from_style(style)
apply_property(builder, property, value, ctx)
builder.build()
}
///|
/// Adjust dimensions for box-sizing: content-box
/// When box-sizing is content-box (default), specified width/height are content dimensions.
/// The layout engine expects outer dimensions (border-box), so we adjust here.
fn adjust_for_box_sizing(style : @style.Style) -> @style.Style {
match style.box_sizing {
@types.BorderBox => style // No adjustment needed
@types.ContentBox => {
// Calculate padding and border sums
let padding_h = resolve_dimension_to_px(style.padding.left) +
resolve_dimension_to_px(style.padding.right)
let padding_v = resolve_dimension_to_px(style.padding.top) +
resolve_dimension_to_px(style.padding.bottom)
let border_h = resolve_dimension_to_px(style.border.left) +
resolve_dimension_to_px(style.border.right)
let border_v = resolve_dimension_to_px(style.border.top) +
resolve_dimension_to_px(style.border.bottom)
// Adjust width and height to include padding+border
let adjusted_width = match style.width {
@types.Dimension::Length(w) =>
@types.Dimension::Length(w + padding_h + border_h)
other => other
}
let adjusted_height = match style.height {
@types.Dimension::Length(h) =>
@types.Dimension::Length(h + padding_v + border_v)
other => other
}
// Adjust min/max constraints too
let adjusted_min_width = match style.min_width {
@types.Dimension::Length(w) =>
@types.Dimension::Length(w + padding_h + border_h)
other => other
}
let adjusted_min_height = match style.min_height {
@types.Dimension::Length(h) =>
@types.Dimension::Length(h + padding_v + border_v)
other => other
}
let adjusted_max_width = match style.max_width {
@types.Dimension::Length(w) =>
@types.Dimension::Length(w + padding_h + border_h)
other => other
}
let adjusted_max_height = match style.max_height {
@types.Dimension::Length(h) =>
@types.Dimension::Length(h + padding_v + border_v)
other => other
}
{
..style,
width: adjusted_width,
height: adjusted_height,
min_width: adjusted_min_width,
min_height: adjusted_min_height,
max_width: adjusted_max_width,
max_height: adjusted_max_height,
// Mark as adjusted (now using border-box semantics internally)
box_sizing: @types.BorderBox,
}
}
}
}
///|
/// Helper to resolve a dimension to pixels (for padding/border calculation)
fn resolve_dimension_to_px(dim : @types.Dimension) -> Double {
match dim {
@types.Length(v) => v
@types.Percent(_) => 0.0 // Percentages are resolved later
@types.Calc(px, _) => px // Percent component is resolved later
@types.MathFn(op, args) =>
// Percent components resolve later; reduce using px parts only.
if args.is_empty() {
0.0
} else {
@types.apply_math_op(op, args.map(fn(a) { a.0 }))
}
@types.Auto => 0.0
@types.MinContent => 0.0 // Intrinsic sizing resolved during layout
@types.MaxContent => 0.0
@types.FitContent(_) => 0.0
}
}
///|
/// Parse font-size value and return computed value in pixels
fn parse_font_size(value : String, ctx : ComputeContext) -> Double {
let v = value.trim()
// Handle rem units (relative to root font-size) first.
// "1rem" also ends with "em", so ordering matters.
match v.strip_suffix("rem") {
Some(num_str) => {
let n = @string.parse_double(num_str.to_owned()) catch { _ => 1.0 }
return n * ctx.root_font_size
}
None => ()
}
// Handle em units (relative to parent font-size)
match v.strip_suffix("em") {
Some(num_str) => {
let n = @string.parse_double(num_str.to_owned()) catch { _ => 1.0 }
return n * ctx.font_size
}
None => ()
}
// Handle px units
match v.strip_suffix("px") {
Some(num_str) => {
let n = @string.parse_double(num_str.to_owned()) catch {
_ => return ctx.font_size
}
return n
}
None => ()
}
// Handle pt units (1pt = 1.333px)
match v.strip_suffix("pt") {
Some(num_str) => {
let n = @string.parse_double(num_str.to_owned()) catch {
_ => return ctx.font_size
}
return n * 1.333
}
None => ()
}
// Handle percent (relative to parent font-size)
match v.strip_suffix("%") {
Some(num_str) => {
let n = @string.parse_double(num_str.to_owned()) catch {
_ => return ctx.font_size
}
return n / 100.0 * ctx.font_size
}
None => ()
}
let plain_number = @string.parse_double(v) catch { _ => -1.0 }
if plain_number == 0.0 {
return 0.0
}
// Handle keyword values
match v {
"xx-small" => 9.0
"x-small" => 10.0
"small" => 13.0
"medium" => 16.0
"large" => 18.0
"x-large" => 24.0
"xx-large" => 32.0
"smaller" => ctx.font_size * 0.833
"larger" => ctx.font_size * 1.2
_ => ctx.font_size // Default to inherited font-size
}
}
///|
/// Parse line-height value and return computed value in pixels
fn parse_line_height(
value : String,
font_size : Double,
root_font_size? : Double = 16.0,
) -> Double {
let v = value.trim()
// Handle unitless number (multiplier of font-size)
// Check if it's a pure number
let is_pure_number = {
let mut pure = true
for i = 0; i < v.length(); i = i + 1 {
let c = v[i].to_int().unsafe_to_char()
if c != '.' && !(c >= '0' && c <= '9') {
pure = false
break
}
}
pure
}
if is_pure_number && !v.is_empty() {
let n = @string.parse_double(v.to_owned()) catch { _ => 1.0 }
return n * font_size
}
// Handle px units
match v.strip_suffix("px") {
Some(num_str) => {
let n = @string.parse_double(num_str.to_owned()) catch {
_ => return font_size
}
return n
}
None => ()
}
// Handle rem units
match v.strip_suffix("rem") {
Some(num_str) => {
let n = @string.parse_double(num_str.to_owned()) catch {
_ => return font_size
}
return n * root_font_size
}
None => ()
}
// Handle em units
match v.strip_suffix("em") {
Some(num_str) => {
let n = @string.parse_double(num_str.to_owned()) catch {
_ => return font_size
}
return n * font_size
}
None => ()
}
// Handle percent (relative to font-size)
match v.strip_suffix("%") {
Some(num_str) => {
let n = @string.parse_double(num_str.to_owned()) catch {
_ => return font_size
}
return n / 100.0 * font_size
}
None => ()
}
// "normal" = 1.2 * font-size typically
if v == "normal" {
return font_size * 1.2
}
// Default to font-size
font_size
}
///|
priv struct ParsedFontShorthand {
font_size : Double
line_height : Double
font_weight : Double
font_family : String
}
///|
/// Parse font shorthand and extract font-size and line-height
/// Supports: [style] [variant] [weight] size[/line-height] family
fn parse_font_shorthand(
value : String,
ctx : ComputeContext,
) -> ParsedFontShorthand {
let v = value.trim()
// Split by whitespace
let parts : Array[String] = []
let mut current = StringBuilder::new()
let mut in_quotes = false
for c in v.iter() {
if c == '"' || c == '\'' {
in_quotes = !in_quotes
current.write_char(c)
} else if (c == ' ' || c == '\t') && !in_quotes {
let s = current.to_string()
if s.length() > 0 {
parts.push(s)
}
current = StringBuilder::new()
} else {
current.write_char(c)
}
}
let s = current.to_string()
if s.length() > 0 {
parts.push(s)
}
// Find the part with font-size (and optional /line-height)
// Font-size is required and comes before font-family
// Look for a part that starts with a digit or contains 'px', 'em', 'rem', '%'
let mut font_size = ctx.font_size
let mut line_height = ctx.font_size
let mut font_weight = 400.0
let mut size_index = -1
for idx = 0; idx < parts.length(); idx = idx + 1 {
let part = parts[idx]
// Check if this part contains size/line-height
if part.contains("/") {
// Split by /
let mut slash_idx = -1
for i = 0; i < part.length(); i = i + 1 {
if part[i] == '/' {
slash_idx = i
break
}
}
if slash_idx > 0 {
let size_part = part.unsafe_substring(start=0, end=slash_idx)
let lh_part = part.unsafe_substring(
start=slash_idx + 1,
end=part.length(),
)
font_size = parse_font_size(size_part, ctx)
line_height = parse_line_height(
lh_part,
font_size,
root_font_size=ctx.root_font_size,
)
size_index = idx
break
}
} else {
// Check if it's a size value (starts with digit or contains unit)
let first_char = if part.length() > 0 { part[0] } else { ' ' }
if first_char >= '0' && first_char <= '9' {
font_size = parse_font_size(part, ctx)
line_height = font_size // Default line-height = font-size (ratio 1)
size_index = idx
break
}
}
}
if size_index > 0 {
for idx = 0; idx < size_index; idx = idx + 1 {
let part = parts[idx].trim().to_lower().to_owned()
if part == "normal" ||
part == "bold" ||
part == "bolder" ||
part == "lighter" {
font_weight = parse_font_weight(part)
continue
}
let parsed_weight = @string.parse_double(part) catch { _ => -1.0 }
if parsed_weight >= 1.0 && parsed_weight <= 1000.0 {
font_weight = parsed_weight
}
}
}
let font_family = if size_index >= 0 && size_index + 1 < parts.length() {
let family_parts : Array[String] = []
for idx = size_index + 1; idx < parts.length(); idx = idx + 1 {
family_parts.push(parts[idx])
}
parse_font_family(family_parts.join(" "))
} else {
""
}
{ font_size, line_height, font_weight, font_family }
}
// =============================================================================
// Multi-column / Fragmentation Parsing
// =============================================================================
///|
fn parse_column_count(value : String) -> Int? {
let trimmed = value.trim().to_owned()
if trimmed.is_empty() || trimmed == "auto" {
return None
}
try {
let parsed = @string.parse_int(trimmed)
if parsed >= 1 {
Some(parsed)
} else {
None
}
} catch {
_ => None
}
}
///|
fn parse_column_width(value : String, ctx : ComputeContext) -> @types.Dimension {
let trimmed = value.trim().to_owned()
if trimmed.is_empty() || trimmed == "auto" {
return @types.Dimension::Auto
}
let dim = resolve_dimension_with_calc_percent_fallback(trimmed, ctx)
match dim {
@types.Dimension::Length(v) =>
if v > 0.0 {
@types.Dimension::Length(v)
} else {
@types.Dimension::Auto
}
@types.Dimension::Percent(v) =>
if v > 0.0 {
@types.Dimension::Percent(v)
} else {
@types.Dimension::Auto
}
@types.Dimension::Calc(px, pct) =>
if px > 0.0 || pct > 0.0 {
@types.Dimension::Calc(px, pct)
} else {
@types.Dimension::Auto
}
@types.Dimension::MathFn(op, args) => @types.Dimension::MathFn(op, args)
@types.Dimension::Auto
| @types.Dimension::MinContent
| @types.Dimension::MaxContent
| @types.Dimension::FitContent(_) => @types.Dimension::Auto
}
}
///|
fn parse_column_fill(value : String) -> @style.ColumnFill {
match value.trim().to_owned() {
"auto" => @style.ColumnFill::Auto
_ => @style.ColumnFill::Balance
}
}
///|
fn parse_break_before(value : String) -> @style.BreakBefore {
match value.trim().to_owned() {
"column" => @style.BreakBefore::Column
"avoid" => @style.BreakBefore::Avoid
_ => @style.BreakBefore::Auto
}
}
///|
fn parse_break_inside(value : String) -> @style.BreakInside {
match value.trim().to_owned() {
"avoid" => @style.BreakInside::Avoid
_ => @style.BreakInside::Auto
}
}
///|
fn parse_column_span(value : String) -> @style.ColumnSpan {
match value.trim().to_owned() {
"all" => @style.ColumnSpan::All
_ => @style.ColumnSpan::None
}
}
///|
fn parse_columns_shorthand(
value : String,
ctx : ComputeContext,
) -> (Int?, @types.Dimension)? {
let parts : Array[StringView] = value
.trim()
.split(" ")
.filter(fn(s) { !s.is_empty() })
.collect()
if parts.length() == 0 {
return None
}
let mut count : Int? = None
let mut width = @types.Dimension::Auto
for part in parts {
let token = part.to_owned()
if token == "auto" {
continue
}
if count is None {
match parse_column_count(token) {
Some(v) => {
count = Some(v)
continue
}
None => ()
}
}
let parsed_width = parse_column_width(token, ctx)
match parsed_width {
@types.Dimension::Auto => ()
_ => width = parsed_width
}
}
Some((count, width))
}
// =============================================================================
// Containment Parsing
// =============================================================================
///|
/// Parse contain-intrinsic-inline-size / contain-intrinsic-block-size.
/// Returns a content-box fallback length in px, or None for none/auto.
fn parse_contain_intrinsic_axis(
value : String,
ctx : ComputeContext,
) -> Double? {
let v = resolve_all_vars(value, ctx).trim().to_owned()
if v.is_empty() || v == "none" || v == "auto" {
return None
}
match resolve_dimension(v, ctx) {
@types.Length(length) => if length > 0.0 { Some(length) } else { Some(0.0) }
_ => None
}
}
///|
/// Parse contain-intrinsic-size shorthand.
/// One value applies to both inline/block axes; two values map to inline then block.
fn parse_contain_intrinsic_size(
value : String,
ctx : ComputeContext,
) -> (Double?, Double?)? {
let v = resolve_all_vars(value, ctx).trim().to_owned()
if v.is_empty() {
return None
}
let axis_values : Array[Double?] = []
let parts = v.split(" ")
for part in parts {
let p = part.to_owned().trim().to_owned()
if p.is_empty() || p == "auto" {
continue
}
if p == "none" {
axis_values.push(None)
continue
}
match parse_contain_intrinsic_axis(p, ctx) {
Some(length) => axis_values.push(Some(length))
None => ()
}
}
if axis_values.length() == 0 {
None
} else if axis_values.length() == 1 {
Some((axis_values[0], axis_values[0]))
} else {
Some((axis_values[0], axis_values[1]))
}
}
///|
/// Parse contain property
fn parse_contain(value : String) -> @style.Contain {
let v = value.trim().to_owned()
// Handle keywords
if v == "none" {
return @style.Contain::none()
}
if v == "strict" {
return @style.Contain::strict()
}
if v == "content" {
return @style.Contain::content()
}
// Parse space-separated values
let mut result = @style.Contain::none()
let parts = v.split(" ")
for part in parts {
let p = part.to_owned().trim()
if p == "size" {
result = { ..result, size: true }
} else if p == "inline-size" {
result = { ..result, inline_size: true }
} else if p == "layout" {
result = { ..result, layout: true }
} else if p == "paint" {
result = { ..result, paint: true }
} else if p == "style" {
result = { ..result, style: true }
}
}
result
}
///|
fn merge_contain(
base : @style.Contain,
extra : @style.Contain,
) -> @style.Contain {
let merged : @style.Contain = {
size: base.size || extra.size,
inline_size: base.inline_size || extra.inline_size,
layout: base.layout || extra.layout,
paint: base.paint || extra.paint,
style: base.style || extra.style,
}
if merged.size {
{ ..merged, inline_size: false }
} else {
merged
}
}
///|
/// Parse container-type into implied contain bits.
/// Supported: normal | size | inline-size
fn parse_container_type(value : String) -> @style.Contain? {
match value.trim().to_owned() {
"normal" => Some(@style.Contain::none())
"size" => Some({ ..@style.Contain::none(), size: true, style: true })
"inline-size" =>
Some({ ..@style.Contain::none(), inline_size: true, style: true })
_ => None
}
}
// Paint Property Parsing
// =============================================================================
///|
/// Parse visibility property
fn parse_visibility(value : String) -> @style.Visibility {
match value.trim().to_owned() {
"visible" => @style.Visibility::Visible
"hidden" => @style.Visibility::Hidden
"collapse" => @style.Visibility::Collapse
_ => @style.Visibility::Visible
}
}
///|
fn parse_pointer_events(value : String) -> @style.PointerEvents {
match value.trim().to_owned() {
"none" => @style.PointerEvents::None
_ => @style.PointerEvents::Auto
}
}
///|
/// Parse vertical-align property
fn parse_vertical_align(value : String) -> @style.VerticalAlign {
match value.trim().to_owned() {
"baseline" => @style.VerticalAlign::Baseline
"top" => @style.VerticalAlign::Top
"middle" => @style.VerticalAlign::Middle
"bottom" => @style.VerticalAlign::Bottom
"text-top" => @style.VerticalAlign::TextTop
"text-bottom" => @style.VerticalAlign::TextBottom
"sub" => @style.VerticalAlign::Sub
"super" => @style.VerticalAlign::Super
_ => @style.VerticalAlign::Baseline
}
}
///|
/// Parse z-index property
fn parse_z_index(value : String) -> @style.ZIndex {
let trimmed = value.trim().to_owned()
if trimmed == "auto" {
@style.ZIndex::Auto
} else {
try {
let v = @string.parse_int(trimmed)
@style.ZIndex::Value(v)
} catch {
_ => @style.ZIndex::Auto
}
}
}
///|
/// Parse opacity property (0.0 to 1.0)
fn parse_opacity(value : String) -> Double {
let trimmed = value.trim().to_owned()
try {
let v = @string.parse_double(trimmed)
// Clamp to valid range
if v < 0.0 {
0.0
} else if v > 1.0 {
1.0
} else {
v
}
} catch {
_ => 1.0
}
}