///|
/// 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" => BorderBox
"content-box" | "initial" | "inherit" => ContentBox
_ => 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) {
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) {
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) {
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) {
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 = Length(0.0)
builder.border_right = Length(0.0)
builder.border_bottom = Length(0.0)
builder.border_left = 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 = Length(0.0)
builder.border_bottom = Length(0.0)
}
if is_none_border_style(parts[1]) {
builder.border_right = Length(0.0)
builder.border_left = 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 = Length(0.0)
}
if is_none_border_style(parts[1]) {
builder.border_right = Length(0.0)
}
if is_none_border_style(parts[2]) {
builder.border_bottom = Length(0.0)
}
if is_none_border_style(parts[3]) {
builder.border_left = Length(0.0)
}
}
}
"border-top-style" => {
builder.border_style_top = parse_border_style_value(value)
if is_none_border_style(value) {
builder.border_top = Length(0.0)
}
}
"border-right-style" => {
builder.border_style_right = parse_border_style_value(value)
if is_none_border_style(value) {
builder.border_right = Length(0.0)
}
}
"border-bottom-style" => {
builder.border_style_bottom = parse_border_style_value(value)
if is_none_border_style(value) {
builder.border_bottom = Length(0.0)
}
}
"border-left-style" => {
builder.border_style_left = parse_border_style_value(value)
if is_none_border_style(value) {
builder.border_left = 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 = Length(0.0)
}
if parts.length() > 1 && is_none_border_style(parts[1]) {
builder.border_bottom = Length(0.0)
} else if parts.length() == 1 && is_none_border_style(parts[0]) {
builder.border_bottom = 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 = Length(0.0)
}
if parts.length() > 1 && is_none_border_style(parts[1]) {
builder.border_bottom = Length(0.0)
} else if parts.length() == 1 && is_none_border_style(parts[0]) {
builder.border_bottom = Length(0.0)
}
} else {
if parts.length() > 0 && is_none_border_style(parts[0]) {
builder.border_left = Length(0.0)
}
if parts.length() > 1 && is_none_border_style(parts[1]) {
builder.border_right = Length(0.0)
} else if parts.length() == 1 && is_none_border_style(parts[0]) {
builder.border_right = 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" {
(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 {
Length(v) => if v >= 0.0 { builder.flex_basis = dim }
Percent(v) => if v >= 0.0 { builder.flex_basis = dim }
Calc(px, pct) => if px > 0.0 || pct > 0.0 { builder.flex_basis = dim }
MathFn(_, _) => builder.flex_basis = dim
Auto => builder.flex_basis = dim
MinContent => builder.flex_basis = dim
MaxContent => builder.flex_basis = dim
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 = Auto
} else if v == "auto" {
builder.flex_grow = 1.0
builder.flex_shrink = 1.0
builder.flex_basis = Auto
} else if v == "initial" {
builder.flex_grow = 0.0
builder.flex_shrink = 1.0
builder.flex_basis = 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 = 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 = 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 = Row
} else if p == "row-reverse" {
builder.flex_direction = RowReverse
} else if p == "column" {
builder.flex_direction = Column
} else if p == "column-reverse" {
builder.flex_direction = ColumnReverse
// Check flex-wrap values
} else if p == "nowrap" {
builder.flex_wrap = NoWrap
} else if p == "wrap" {
builder.flex_wrap = Wrap
} else if p == "wrap-reverse" {
builder.flex_wrap = 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 {
Auto =>
match builder.grid_column_end {
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 {
Auto =>
match builder.grid_row_end {
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 => ()
}
Invalid => ()
}
}
"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 => ()
}
Invalid => ()
}
}
}
"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" => Ellipsis
_ => 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
Invalid => 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(_)
}