///|
/// Inline style parser - parses CSS property declarations
/// e.g., "width: 100px; height: 50px; display: flex"
// Default viewport size for inline parser vh/vw resolution
let default_viewport_width : Double = 1000.0
///|
let default_viewport_height : Double = 1000.0
///|
fn is_viewport_width_unit(unit : String) -> Bool {
unit == "vw" || unit == "dvw" || unit == "svw" || unit == "lvw"
}
///|
fn is_viewport_height_unit(unit : String) -> Bool {
unit == "vh" || unit == "dvh" || unit == "svh" || unit == "lvh"
}
///|
/// Parse a calc() expression and try to simplify it to a Dimension
/// Returns Some(dimension) if simplification is possible, None otherwise.
/// Mixed length+percentage calc() returns None (the inline parser falls back to
/// Auto); min()/max()/clamp() use parse_calc_linear directly to keep the mixed
/// form.
fn parse_calc_expression(
tokens : ArrayView[@token.Token],
) -> (@types.Dimension, ArrayView[@token.Token])? {
match parse_calc_linear(tokens) {
Some(((px, pct), rest)) =>
if pct.abs() < 0.0001 {
Some((@types.Length(px), rest))
} else if px.abs() < 0.0001 {
Some((@types.Percent(pct), rest))
} else {
// Mixed - cannot simplify (fall back to Auto)
None
}
None => None
}
}
///|
/// Parse a calc() body into a linear (px, percent_ratio) form. Starts right
/// after the Function("calc") token and consumes through the matching
/// RightParen, returning the remaining tokens. Returns None if no RightParen.
fn parse_calc_linear(
tokens : ArrayView[@token.Token],
) -> ((Double, Double), ArrayView[@token.Token])? {
let mut remaining = tokens
let mut result_px : Double = 0.0
let mut result_pct : Double = 0.0
let mut current_op : Char = '+'
// A unitless number before the first dimensioned term (e.g. `calc(2 * 10px)`)
// multiplies that term; stash it here until the first value lands.
let mut have_value = false
let mut leading_factor : Double = 1.0
while remaining.length() > 0 {
remaining = skip_whitespace(remaining)
if remaining.length() == 0 {
break
}
match remaining[0] {
@token.Token::RightParen => {
// End of calc expression: return the linear form (pct as a ratio).
return Some(((result_px, result_pct / 100.0), remaining[1:]))
}
@token.Token::Dimension(value, raw_unit) => {
let unit = raw_unit.to_lower()
if unit == "%" {
if !have_value {
result_pct = value * leading_factor
have_value = true
leading_factor = 1.0
} else {
match current_op {
'+' => result_pct = result_pct + value
'-' => result_pct = result_pct - value
'*' => result_pct = result_pct * value
'/' => result_pct = result_pct / value
_ => ()
}
}
} else {
// px and other length units (rem/em/pt/cm/in/...) accumulate in px.
// Unknown units fall back to the legacy px interpretation.
let px = match dimension_unit_to_px(value, unit) {
Some(p) => p
None => value
}
if !have_value {
result_px = px * leading_factor
have_value = true
leading_factor = 1.0
} else {
match current_op {
'+' => result_px = result_px + px
'-' => result_px = result_px - px
'*' => result_px = result_px * px
'/' => result_px = result_px / px
_ => ()
}
}
}
remaining = remaining[1:]
}
@token.Token::Percentage(value) => {
if !have_value {
result_pct = value * leading_factor
have_value = true
leading_factor = 1.0
} else {
match current_op {
'+' => result_pct = result_pct + value
'-' => result_pct = result_pct - value
'*' => result_pct = result_pct * value
'/' => result_pct = result_pct / value
_ => ()
}
}
remaining = remaining[1:]
}
@token.Token::Number(value, _) => {
// Unitless number. After a dimensioned term it scales the running
// result (`10px * 2`); before the first term it is a leading factor for
// the next term (`2 * 10px`). Zero has no effect.
if value != 0.0 {
match current_op {
'*' =>
if have_value {
result_px = result_px * value
result_pct = result_pct * value
} else {
leading_factor = leading_factor * value
}
'/' =>
if have_value {
result_px = result_px / value
result_pct = result_pct / value
} else {
leading_factor = leading_factor / value
}
_ => if !have_value { leading_factor = leading_factor * value }
}
}
remaining = remaining[1:]
}
@token.Token::Delim(c) =>
if c == '+' || c == '-' || c == '*' || c == '/' {
current_op = c
remaining = remaining[1:]
} else {
remaining = remaining[1:]
}
_ => remaining = remaining[1:]
}
}
None
}
///|
/// Collapse a linear (px, percent_ratio) form to the most specific Dimension.
/// Mirrors the computed engine's lp_to_dimension.
fn lp_to_dim(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)
}
}
///|
/// Reduce a resolvable Dimension to a linear (px, percent_ratio) form. Returns
/// None for forms that cannot be expressed as a single linear term (MathFn,
/// intrinsic keywords, Auto) — matching the computed engine's math_arg_lp.
fn dimension_to_lp(dim : @types.Dimension) -> (Double, Double)? {
match dim {
@types.Length(px) => Some((px, 0.0))
@types.Percent(p) => Some((0.0, p))
@types.Calc(px, pct) => Some((px, pct))
_ => None
}
}
///|
/// Parse a single min()/max()/clamp() argument (one primary: a value token, a
/// calc(), or a nested math function) into a linear (px, percent_ratio) form.
/// Bare arithmetic without a calc() wrapper is not supported (parity with the
/// computed engine). Returns the remaining tokens positioned at the following
/// Comma or RightParen.
fn parse_math_arg_lp(
tokens : ArrayView[@token.Token],
) -> ((Double, Double), ArrayView[@token.Token])? {
let toks = skip_whitespace(tokens)
if toks.length() == 0 {
return None
}
match toks[0] {
@token.Token::Function(name) => {
let lname = name.to_lower()
if lname == "calc" {
parse_calc_linear(toks[1:])
} else if lname == "min" || lname == "max" || lname == "clamp" {
match parse_math_function(lname, toks[1:]) {
Some((dim, rest)) =>
match dimension_to_lp(dim) {
Some(lp) => Some((lp, rest))
None => None
}
None => None
}
} else {
None
}
}
@token.Token::Dimension(value, raw_unit) => {
let unit = raw_unit.to_lower()
if unit == "%" {
Some(((0.0, value / 100.0), toks[1:]))
} else {
match dimension_unit_to_px(value, unit) {
Some(px) => Some(((px, 0.0), toks[1:]))
// Unknown unit is an invalid length argument; reject it so the result
// matches the computed engine (rather than the legacy px fallback).
None => None
}
}
}
@token.Token::Percentage(value) => Some(((0.0, value / 100.0), toks[1:]))
@token.Token::Number(value, _) =>
if value == 0.0 {
Some(((0.0, 0.0), toks[1:]))
} else {
None
}
_ => None
}
}
///|
/// Parse min()/max()/clamp() arguments starting right after the Function token,
/// through the matching RightParen. Builds a collapsed Dimension when the
/// arguments share a unit (or there is a single argument) and a MathFn when
/// length and percentage are mixed and need a layout-time basis.
fn parse_math_function(
op_name : String,
tokens : ArrayView[@token.Token],
) -> (@types.Dimension, ArrayView[@token.Token])? {
let op = match op_name {
"min" => @types.MathOp::Min
"max" => @types.MathOp::Max
_ => @types.MathOp::Clamp
}
let lps : Array[(Double, Double)] = []
let mut remaining = tokens
let mut closed = false
while !closed {
remaining = skip_whitespace(remaining)
if remaining.length() == 0 {
return None // unterminated function
}
// Allow an empty argument list to terminate cleanly (e.g. malformed input).
match remaining[0] {
@token.Token::RightParen => {
remaining = remaining[1:]
closed = true
continue
}
_ => ()
}
match parse_math_arg_lp(remaining) {
Some((lp, rest)) => {
lps.push(lp)
remaining = skip_whitespace(rest)
if remaining.length() == 0 {
return None
}
match remaining[0] {
@token.Token::Comma => remaining = remaining[1:]
@token.Token::RightParen => {
remaining = remaining[1:]
closed = true
}
_ => return None // unexpected token
}
}
None => return None
}
}
if lps.is_empty() {
return None
}
if op == @types.MathOp::Clamp && lps.length() != 3 {
return None
}
// Single min()/max() argument resolves to that argument verbatim.
if lps.length() == 1 {
let (px, pct) = lps[0]
return Some((lp_to_dim(px, pct), remaining))
}
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
}
}
let dim = if all_px {
@types.Dimension::Length(@types.apply_math_op(op, lps.map(fn(lp) { lp.0 })))
} else if all_pct {
@types.Dimension::Percent(@types.apply_math_op(op, lps.map(fn(lp) { lp.1 })))
} else {
@types.Dimension::MathFn(op, lps)
}
Some((dim, remaining))
}
///|
/// Parse a dimension value (length, percent, auto)
/// viewport units are converted to Length using default viewport (1000x1000)
///|
/// Convert a length unit to pixels using the inline parser's context-free
/// defaults (16px font, 1000×1000 viewport). Mirrors the unit coverage of the
/// computed engine's resolve_dimension. Returns None for non-length units.
fn dimension_unit_to_px(value : Double, unit : String) -> Double? {
match unit {
"px" => Some(value)
"em" | "rem" => Some(value * 16.0)
// ex/ch approximated as 0.5em, matching the computed engine's default.
"ex" | "ch" => Some(value * 16.0 * 0.5)
"vw" | "dvw" | "svw" | "lvw" | "vi" =>
Some(value * default_viewport_width / 100.0)
"vh" | "dvh" | "svh" | "lvh" | "vb" =>
Some(value * default_viewport_height / 100.0)
"vmin" => {
let base = if default_viewport_width < default_viewport_height {
default_viewport_width
} else {
default_viewport_height
}
Some(value * base / 100.0)
}
"vmax" => {
let base = if default_viewport_width > default_viewport_height {
default_viewport_width
} else {
default_viewport_height
}
Some(value * base / 100.0)
}
"pt" => Some(value * 96.0 / 72.0)
"pc" => Some(value * 16.0)
"in" => Some(value * 96.0)
"cm" => Some(value * 96.0 / 2.54)
"mm" => Some(value * 96.0 / 25.4)
"q" => Some(value * 96.0 / 101.6)
_ => None
}
}
///|
fn parse_dimension(tokens : ArrayView[@token.Token]) -> @types.Dimension? {
if tokens.length() == 0 {
return None
}
match tokens[0] {
@token.Token::Function(name) => {
let lname = name.to_lower()
if lname == "calc" {
// Try to parse and simplify calc expression
match parse_calc_expression(tokens[1:]) {
Some((dim, _)) => return Some(dim)
None => return Some(@types.Auto) // Fallback for complex calc
}
} else if lname == "min" || lname == "max" || lname == "clamp" {
match parse_math_function(lname, tokens[1:]) {
Some((dim, _)) => return Some(dim)
None => return Some(@types.Auto) // Fallback for complex/mixed-calc args
}
} else {
None
}
}
@token.Token::Dimension(value, raw_unit) => {
// CSS units are case-insensitive.
let unit = raw_unit.to_lower()
if unit == "%" {
Some(@types.Percent(value / 100.0))
} else {
match dimension_unit_to_px(value, unit) {
Some(px) => Some(@types.Length(px))
// Unknown unit: fall back to treating the number as pixels (legacy).
None => Some(@types.Length(value))
}
}
}
@token.Token::Percentage(value) => Some(@types.Percent(value / 100.0))
@token.Token::Number(value, _) =>
// Unitless 0 is valid
if value == 0.0 {
Some(@types.Length(0.0))
} else {
None
}
@token.Token::Ident(s) =>
if s == "auto" {
Some(@types.Auto)
} else if s == "min-content" {
Some(@types.MinContent)
} else if s == "max-content" {
Some(@types.MaxContent)
} else if s == "fit-content" {
// fit-content without argument is equivalent to fit-content(max-content)
// Using infinity to represent unbounded
Some(@types.FitContent(1.0e10))
} else {
None
}
_ => None
}
}
///|
/// Parse multiple dimension values (for shorthand properties like margin, padding)
fn parse_multi_dimensions(
tokens : ArrayView[@token.Token],
max_count : Int,
) -> Array[@types.Dimension] {
let result : Array[@types.Dimension] = []
let mut rest = tokens
while result.length() < max_count && rest.length() > 0 {
// Skip whitespace
rest = skip_whitespace(rest)
if rest.length() == 0 {
break
}
// Try to parse a dimension
match parse_dimension(rest) {
Some(dim) => {
result.push(dim)
rest = rest[1:]
}
None => break
}
}
result
}
///|
fn parse_display_keyword(keyword : String) -> @types.Display? {
if keyword == "block" {
Some(@types.Block)
} else if keyword == "inline" {
Some(@types.Inline)
} else if keyword == "inline-block" {
Some(@types.InlineBlock)
} else if keyword == "flex" {
Some(@types.Flex)
} else if keyword == "inline-flex" {
Some(@types.InlineFlex)
} else if keyword == "grid" {
Some(@types.Grid)
} else if keyword == "inline-grid" {
Some(@types.InlineGrid)
} else if keyword == "table" {
Some(@types.Table)
} else if keyword == "inline-table" {
Some(@types.InlineTable)
} else if keyword == "table-row" {
Some(@types.TableRow)
} else if keyword == "table-cell" {
Some(@types.TableCell)
} else if keyword == "table-caption" {
Some(@types.TableCaption)
} else if keyword == "table-row-group" {
Some(@types.TableRowGroup)
} else if keyword == "table-header-group" {
Some(@types.TableHeaderGroup)
} else if keyword == "table-footer-group" {
Some(@types.TableFooterGroup)
} else if keyword == "table-column" {
Some(@types.TableColumn)
} else if keyword == "table-column-group" {
Some(@types.TableColumnGroup)
} else if keyword == "none" {
Some(@types.Display::None)
} else if keyword == "contents" {
Some(@types.Contents)
} else if keyword == "flow-root" {
Some(@types.FlowRoot)
} else if keyword == "math" {
Some(@types.Inline)
} else {
None
}
}
///|
/// Parse display property
fn parse_display(tokens : ArrayView[@token.Token]) -> @types.Display? {
if tokens.length() == 0 {
return None
}
let keywords : Array[String] = []
for token in tokens {
match token {
@token.Token::Ident(s) => keywords.push(s)
@token.Token::Whitespace => ()
_ => return None
}
}
if keywords.length() == 0 {
return None
}
if keywords.length() == 2 {
let first = keywords[0]
let second = keywords[1]
if (first == "math" && second == "inline") ||
(first == "inline" && second == "math") {
return Some(@types.Inline)
}
if (first == "math" && second == "block") ||
(first == "block" && second == "math") {
return Some(@types.Block)
}
if (first == "flow-root" && second == "list-item") ||
(first == "list-item" && second == "flow-root") {
return Some(@types.FlowRoot)
}
}
if keywords.length() != 1 {
return None
}
parse_display_keyword(keywords[0])
}
///|
/// Parse position property
fn parse_position(tokens : ArrayView[@token.Token]) -> @types.Position? {
if tokens.length() == 0 {
return None
}
match tokens[0] {
@token.Token::Ident(s) =>
if s == "relative" {
Some(@types.Relative)
} else if s == "absolute" {
Some(@types.Absolute)
} else if s == "fixed" {
Some(@types.Fixed)
} else {
None
}
_ => None
}
}
///|
/// Parse float property
fn parse_float(
tokens : ArrayView[@token.Token],
direction : @style.Direction,
) -> @types.CssFloat? {
if tokens.length() == 0 {
return None
}
match tokens[0] {
@token.Token::Ident(s) =>
if s == "left" {
Some(@types.CssFloat::Left)
} else if s == "right" {
Some(@types.CssFloat::Right)
} else if s == "inline-start" {
Some(
if direction.is_rtl() {
@types.CssFloat::Right
} else {
@types.CssFloat::Left
},
)
} else if s == "inline-end" {
Some(
if direction.is_rtl() {
@types.CssFloat::Left
} else {
@types.CssFloat::Right
},
)
} else if s == "none" {
Some(@types.CssFloat::None)
} else {
None
}
_ => None
}
}
///|
/// Parse clear property
fn parse_clear(
tokens : ArrayView[@token.Token],
direction : @style.Direction,
) -> @types.Clear? {
if tokens.length() == 0 {
return None
}
match tokens[0] {
@token.Token::Ident(s) =>
if s == "left" {
Some(@types.Clear::Left)
} else if s == "right" {
Some(@types.Clear::Right)
} else if s == "inline-start" {
Some(
if direction.is_rtl() {
@types.Clear::Right
} else {
@types.Clear::Left
},
)
} else if s == "inline-end" {
Some(
if direction.is_rtl() {
@types.Clear::Left
} else {
@types.Clear::Right
},
)
} else if s == "both" {
Some(@types.Clear::Both)
} else if s == "none" {
Some(@types.Clear::None)
} else {
None
}
_ => None
}
}
///|
/// Parse flex-direction property
fn parse_flex_direction(
tokens : ArrayView[@token.Token],
) -> @types.FlexDirection? {
if tokens.length() == 0 {
return None
}
match tokens[0] {
@token.Token::Ident(s) =>
if s == "row" {
Some(@types.Row)
} else if s == "row-reverse" {
Some(@types.RowReverse)
} else if s == "column" {
Some(@types.Column)
} else if s == "column-reverse" {
Some(@types.ColumnReverse)
} else {
None
}
_ => None
}
}
///|
/// Parse flex-wrap property
fn parse_flex_wrap(tokens : ArrayView[@token.Token]) -> @types.FlexWrap? {
if tokens.length() == 0 {
return None
}
match tokens[0] {
@token.Token::Ident(s) =>
if s == "nowrap" {
Some(@types.NoWrap)
} else if s == "wrap" {
Some(@types.FlexWrap::Wrap)
} else if s == "wrap-reverse" {
Some(@types.WrapReverse)
} else {
None
}
_ => None
}
}
///|
fn parse_alignment_keyword(value : String) -> @types.Alignment? {
if value == "flex-start" {
Some(@types.Alignment::FlexStart)
} else if value == "start" {
Some(@types.Alignment::Start)
} else if value == "flex-end" {
Some(@types.Alignment::FlexEnd)
} else if value == "end" {
Some(@types.Alignment::End)
} else if value == "left" {
Some(@types.Alignment::Left)
} else if value == "right" {
Some(@types.Alignment::Right)
} else if value == "center" {
Some(@types.Alignment::Center)
} else if value == "space-between" {
Some(@types.SpaceBetween)
} else if value == "space-around" {
Some(@types.SpaceAround)
} else if value == "space-evenly" {
Some(@types.SpaceEvenly)
} else if value == "stretch" {
Some(@types.Alignment::Stretch)
} else if value == "baseline" || value == "first baseline" {
Some(@types.Alignment::Baseline)
} else if value == "last baseline" {
Some(@types.Alignment::End)
} else {
None
}
}
///|
fn collect_alignment_parts(tokens : ArrayView[@token.Token]) -> Array[String] {
let parts : Array[String] = []
for i = 0; i < tokens.length(); i = i + 1 {
match tokens[i] {
@token.Token::Ident(s) => parts.push(s)
_ => ()
}
}
parts
}
///|
fn parse_alignment_segment_with_overflow(
parts : Array[String],
start : Int,
) -> ((@types.Alignment, Bool), Int)? {
if start >= parts.length() {
return None
}
let mut idx = start
let mut is_safe = false
let mut is_unsafe = false
if parts[idx] == "safe" {
is_safe = true
idx = idx + 1
} else if parts[idx] == "unsafe" {
is_unsafe = true
idx = idx + 1
}
if idx >= parts.length() {
return None
}
let keyword = if idx + 1 < parts.length() &&
parts[idx + 1] == "baseline" &&
(parts[idx] == "first" || parts[idx] == "last") {
parts[idx] + " baseline"
} else {
parts[idx]
}
let next_index = if keyword == parts[idx] { idx + 1 } else { idx + 2 }
match parse_alignment_keyword(keyword) {
Some(parsed) => {
let alignment = if is_safe {
match parsed {
@types.Alignment::Center
| @types.Alignment::End
| @types.Alignment::FlexEnd => @types.Alignment::Start
_ => parsed
}
} else {
parsed
}
Some(((alignment, is_unsafe), next_index))
}
None => None
}
}
///|
fn parse_alignment_segment(
parts : Array[String],
start : Int,
) -> (@types.Alignment, Int)? {
match parse_alignment_segment_with_overflow(parts, start) {
Some(((alignment, _), next_index)) => Some((alignment, next_index))
None => None
}
}
///|
fn parse_alignment_with_overflow(
tokens : ArrayView[@token.Token],
) -> (@types.Alignment, Bool)? {
let parts = collect_alignment_parts(tokens)
match parse_alignment_segment_with_overflow(parts, 0) {
Some(((alignment, is_unsafe), _)) => Some((alignment, is_unsafe))
None => None
}
}
///|
/// Parse alignment property (justify-content, align-items, etc.)
fn parse_alignment(tokens : ArrayView[@token.Token]) -> @types.Alignment? {
let parts = collect_alignment_parts(tokens)
match parse_alignment_segment(parts, 0) {
Some((alignment, _)) => Some(alignment)
None => None
}
}
///|
fn parse_place_content_with_overflow(
tokens : ArrayView[@token.Token],
) -> ((@types.Alignment, Bool), (@types.Alignment, Bool))? {
let parts = collect_alignment_parts(tokens)
match parse_alignment_segment_with_overflow(parts, 0) {
Some((align_content, next_index)) =>
if next_index >= parts.length() {
Some((align_content, align_content))
} else {
match parse_alignment_segment_with_overflow(parts, next_index) {
Some((justify_content, _)) => Some((align_content, justify_content))
None => Some((align_content, align_content))
}
}
None => None
}
}
///|
/// Parse self-alignment property (align-self, justify-self)
fn parse_self_alignment(tokens : ArrayView[@token.Token]) -> @types.AlignSelf? {
if tokens.length() == 0 {
return None
}
match tokens[0] {
@token.Token::Ident(s) =>
if s == "auto" {
Some(@types.AlignSelf::Auto)
} else if s == "flex-start" || s == "start" {
Some(@types.AlignSelf::Start)
} else if s == "flex-end" || s == "end" {
Some(@types.AlignSelf::End)
} else if s == "center" {
Some(@types.AlignSelf::Center)
} else if s == "stretch" {
Some(@types.AlignSelf::Stretch)
} else if s == "baseline" {
Some(@types.AlignSelf::Baseline)
} else {
None
}
_ => None
}
}
///|
/// Parse a numeric value
fn parse_number(tokens : ArrayView[@token.Token]) -> Double? {
if tokens.length() == 0 {
return None
}
match tokens[0] {
@token.Token::Number(value, _) => Some(value)
_ => None
}
}
///|
/// Parse an integer value
fn parse_integer(tokens : ArrayView[@token.Token]) -> Int? {
if tokens.length() == 0 {
return None
}
match tokens[0] {
@token.Token::Number(value, _) => Some(value.to_int())
_ => None
}
}
///|
/// Parse flex shorthand property
/// flex: none | auto | [] []
/// The values can appear in any order, with these rules:
/// - Numbers are flex-grow (first) and flex-shrink (second)
/// - Dimensions/percentages/keywords are flex-basis
fn parse_flex_shorthand(
tokens : ArrayView[@token.Token],
) -> (Double, Double, @types.Dimension)? {
let tokens = skip_whitespace(tokens)
if tokens.length() == 0 {
return None
}
// Check for keyword values
match tokens[0] {
@token.Token::Ident(s) => {
if s == "none" {
// flex: none = 0 0 auto
return Some((0.0, 0.0, @types.Auto))
}
if s == "auto" {
// flex: auto = 1 1 auto
return Some((1.0, 1.0, @types.Auto))
}
if s == "initial" {
// flex: initial = 0 1 auto
return Some((0.0, 1.0, @types.Auto))
}
}
_ => ()
}
// CSS flex shorthand order: [] []
// Numbers must come first, then optionally a basis value.
// Invalid orderings like "1 0% 1" (basis in middle) must return None.
let numbers : Array[Double] = []
let mut basis : @types.Dimension? = None
let mut tokens_view = tokens
let mut seen_basis = false
// First, parse all leading numbers (grow and optionally shrink)
while tokens_view.length() > 0 {
tokens_view = skip_whitespace(tokens_view)
if tokens_view.length() == 0 {
break
}
match tokens_view[0] {
@token.Token::Number(v, _) => {
if seen_basis {
// Number after basis is invalid (e.g., "1 0% 1")
return None
}
numbers.push(v)
tokens_view = tokens_view[1:]
}
@token.Token::Dimension(_, _) | @token.Token::Percentage(_) => {
// This is flex-basis
if seen_basis {
// Second basis value is invalid
return None
}
match parse_dimension(tokens_view) {
Some(dim) => {
basis = Some(dim)
seen_basis = true
tokens_view = tokens_view[1:]
}
None => break
}
}
@token.Token::Ident(s) =>
// Could be flex-basis keyword (auto, content, etc.)
if s == "auto" || s == "content" {
if seen_basis {
return None
}
basis = Some(@types.Auto)
seen_basis = true
tokens_view = tokens_view[1:]
} else {
break
}
_ => break
}
}
// Check for trailing tokens (anything after basis is invalid)
tokens_view = skip_whitespace(tokens_view)
if tokens_view.length() > 0 {
// Still have tokens left - could be invalid like "1 0% 1"
match tokens_view[0] {
@token.Token::Number(_, _) =>
// Number after basis is invalid
if seen_basis {
return None
}
_ => ()
}
}
// Determine values based on what we found
let grow = if numbers.length() >= 1 { numbers[0] } else { 1.0 }
let shrink = if numbers.length() >= 2 { numbers[1] } else { 1.0 }
let final_basis = match basis {
Some(b) => b
None =>
// CSS spec: when only numbers are specified, basis defaults to 0%.
if numbers.length() >= 1 {
@types.Percent(0.0)
} else {
@types.Auto
}
}
if numbers.length() == 0 && basis is None {
return None
}
Some((grow, shrink, final_basis))
}
///|
/// Parse flex-flow shorthand property
/// flex-flow: ||
fn parse_flex_flow_shorthand(
tokens : ArrayView[@token.Token],
) -> (@types.FlexDirection, @types.FlexWrap)? {
let tokens = skip_whitespace(tokens)
if tokens.length() == 0 {
return None
}
let mut direction : @types.FlexDirection? = None
let mut wrap : @types.FlexWrap? = None
// Parse up to 2 tokens
let mut i = 0
while i < tokens.length() {
match tokens[i] {
@token.Token::Ident(s) =>
// Check if it's a flex-direction value
if s == "row" {
direction = Some(@types.FlexDirection::Row)
} else if s == "row-reverse" {
direction = Some(@types.FlexDirection::RowReverse)
} else if s == "column" {
direction = Some(@types.FlexDirection::Column)
} else if s == "column-reverse" {
direction = Some(@types.FlexDirection::ColumnReverse)
// Check if it's a flex-wrap value
} else if s == "nowrap" {
wrap = Some(@types.FlexWrap::NoWrap)
} else if s == "wrap" {
wrap = Some(@types.FlexWrap::Wrap)
} else if s == "wrap-reverse" {
wrap = Some(@types.FlexWrap::WrapReverse)
}
@token.Token::Whitespace => ()
_ => ()
}
i += 1
}
// If we found at least one value, return with defaults for the other
if direction is Some(_) || wrap is Some(_) {
Some(
(
direction.unwrap_or(@types.FlexDirection::Row),
wrap.unwrap_or(@types.FlexWrap::NoWrap),
),
)
} else {
None
}
}
///|
/// Parse aspect-ratio property (e.g., "16 / 9" or "1.5")
fn parse_aspect_ratio(tokens : ArrayView[@token.Token]) -> Double? {
if tokens.length() == 0 {
return None
}
let tokens = skip_whitespace(tokens)
match tokens[0] {
@token.Token::Number(value, _) => {
// Check for "x / y" format
let rest = skip_whitespace(tokens[1:])
if rest.length() >= 2 {
match (rest[0], skip_whitespace(rest[1:])) {
(@token.Token::Delim('/'), rest2) =>
if rest2.length() > 0 {
match rest2[0] {
@token.Token::Number(divisor, _) =>
if divisor != 0.0 {
return Some(value / divisor)
} else {
return Some(value)
}
_ => return Some(value)
}
} else {
return Some(value)
}
_ => return Some(value)
}
}
Some(value)
}
_ => None
}
}
///|
/// Parse grid-auto-flow property
fn parse_grid_auto_flow(
tokens : ArrayView[@token.Token],
) -> @types.GridAutoFlow? {
if tokens.length() == 0 {
return None
}
match tokens[0] {
@token.Token::Ident(s) =>
if s == "row" {
Some(@types.GridAutoFlow::Row)
} else if s == "column" {
Some(@types.GridAutoFlow::Column)
} else if s == "row dense" {
Some(@types.RowDense)
} else if s == "column dense" {
Some(@types.ColumnDense)
} else {
None
}
_ => None
}
}
///|
/// Parse a single track value for MinTrackSizing
fn parse_min_track_sizing(
tokens : ArrayView[@token.Token],
) -> (@types.MinTrackSizing, ArrayView[@token.Token])? {
let tokens = skip_whitespace(tokens)
if tokens.length() == 0 {
return None
}
match tokens[0] {
@token.Token::Dimension(value, unit) =>
if unit == "px" {
Some((@types.MinTrackSizing::Length(value), tokens[1:]))
} else if unit == "%" {
Some((@types.MinTrackSizing::Percent(value / 100.0), tokens[1:]))
} else if is_viewport_width_unit(unit) {
Some(
(
@types.MinTrackSizing::Length(
value * default_viewport_width / 100.0,
),
tokens[1:],
),
)
} else if is_viewport_height_unit(unit) {
Some(
(
@types.MinTrackSizing::Length(
value * default_viewport_height / 100.0,
),
tokens[1:],
),
)
} else {
// rem/em and physical units (pt/cm/in/...) are converted to px.
let px = match dimension_unit_to_px(value, unit.to_lower()) {
Some(p) => p
None => value
}
Some((@types.MinTrackSizing::Length(px), tokens[1:]))
}
@token.Token::Percentage(value) =>
Some((@types.MinTrackSizing::Percent(value / 100.0), tokens[1:]))
@token.Token::Number(value, _) =>
if value == 0.0 {
Some((@types.MinTrackSizing::Length(0.0), tokens[1:]))
} else {
None
}
@token.Token::Ident(s) =>
if s == "auto" {
Some((@types.MinTrackSizing::Auto, tokens[1:]))
} else if s == "min-content" {
Some((@types.MinTrackSizing::MinContent, tokens[1:]))
} else if s == "max-content" {
Some((@types.MinTrackSizing::MaxContent, tokens[1:]))
} else {
None
}
_ => None
}
}
///|
/// Parse a single track value for MaxTrackSizing
fn parse_max_track_sizing(
tokens : ArrayView[@token.Token],
) -> (@types.MaxTrackSizing, ArrayView[@token.Token])? {
let tokens = skip_whitespace(tokens)
if tokens.length() == 0 {
return None
}
match tokens[0] {
@token.Token::Dimension(value, unit) =>
if unit == "px" {
Some((@types.MaxTrackSizing::Length(value), tokens[1:]))
} else if unit == "%" {
Some((@types.MaxTrackSizing::Percent(value / 100.0), tokens[1:]))
} else if unit == "fr" {
Some((@types.MaxTrackSizing::Fr(value), tokens[1:]))
} else if is_viewport_width_unit(unit) {
Some(
(
@types.MaxTrackSizing::Length(
value * default_viewport_width / 100.0,
),
tokens[1:],
),
)
} else if is_viewport_height_unit(unit) {
Some(
(
@types.MaxTrackSizing::Length(
value * default_viewport_height / 100.0,
),
tokens[1:],
),
)
} else {
// rem/em and physical units (pt/cm/in/...) are converted to px.
let px = match dimension_unit_to_px(value, unit.to_lower()) {
Some(p) => p
None => value
}
Some((@types.MaxTrackSizing::Length(px), tokens[1:]))
}
@token.Token::Percentage(value) =>
Some((@types.MaxTrackSizing::Percent(value / 100.0), tokens[1:]))
@token.Token::Number(value, _) =>
if value == 0.0 {
Some((@types.MaxTrackSizing::Length(0.0), tokens[1:]))
} else {
None
}
@token.Token::Ident(s) =>
if s == "auto" {
Some((@types.MaxTrackSizing::Auto, tokens[1:]))
} else if s == "min-content" {
Some((@types.MaxTrackSizing::MinContent, tokens[1:]))
} else if s == "max-content" {
Some((@types.MaxTrackSizing::MaxContent, tokens[1:]))
} else {
None
}
_ => None
}
}
///|
/// Parse the single `` argument of `fit-content( ... )`,
/// starting right after the `Function("fit-content")` token. Returns the value,
/// whether it is a percentage (ratio) rather than a px length, and the tokens
/// after the matching RightParen. Length units are converted to px.
fn parse_fit_content_arg(
tokens : ArrayView[@token.Token],
) -> (Double, Bool, ArrayView[@token.Token])? {
let rest = skip_whitespace(tokens)
if rest.length() == 0 {
return None
}
let (val, is_pct, rest2) : (Double, Bool, ArrayView[@token.Token]) = match
rest[0] {
@token.Token::Dimension(v, raw_unit) => {
let unit = raw_unit.to_lower()
if unit == "%" {
(v / 100.0, true, rest[1:])
} else {
let px = match dimension_unit_to_px(v, unit) {
Some(p) => p
None => v
}
(px, false, rest[1:])
}
}
@token.Token::Percentage(v) => (v / 100.0, true, rest[1:])
@token.Token::Number(v, _) =>
if v == 0.0 {
(0.0, false, rest[1:])
} else {
return None
}
_ => return None
}
let rest3 = skip_whitespace(rest2)
if rest3.length() == 0 {
return None
}
match rest3[0] {
@token.Token::RightParen => Some((val, is_pct, rest3[1:]))
_ => None
}
}
///|
/// Parse a single track sizing value (without repeat)
fn parse_single_track_sizing(
tokens : ArrayView[@token.Token],
) -> (@types.SingleTrackSizing, ArrayView[@token.Token])? {
let tokens = skip_whitespace(tokens)
if tokens.length() == 0 {
return None
}
match tokens[0] {
@token.Token::Dimension(value, unit) =>
if unit == "px" {
Some((@types.SingleTrackSizing::Length(value), tokens[1:]))
} else if unit == "%" {
Some((@types.SingleTrackSizing::Percent(value / 100.0), tokens[1:]))
} else if unit == "fr" {
Some((@types.SingleTrackSizing::Fr(value), tokens[1:]))
} else {
// rem/em and physical units (pt/cm/in/...) are converted to px.
let px = match dimension_unit_to_px(value, unit.to_lower()) {
Some(p) => p
None => value
}
Some((@types.SingleTrackSizing::Length(px), tokens[1:]))
}
@token.Token::Percentage(value) =>
Some((@types.SingleTrackSizing::Percent(value / 100.0), tokens[1:]))
@token.Token::Number(value, _) =>
if value == 0.0 {
Some((@types.SingleTrackSizing::Length(0.0), tokens[1:]))
} else {
None
}
@token.Token::Ident(s) =>
if s == "auto" {
Some((@types.SingleTrackSizing::Auto, tokens[1:]))
} else if s == "min-content" {
Some((@types.SingleTrackSizing::MinContent, tokens[1:]))
} else if s == "max-content" {
Some((@types.SingleTrackSizing::MaxContent, tokens[1:]))
} else {
None
}
@token.Token::Function(name) =>
if name == "minmax" {
// Parse minmax(min, max)
let rest = tokens[1:] // skip Function token (already consumed '(')
match parse_min_track_sizing(rest) {
Some((min_val, rest2)) => {
// Skip whitespace and comma
let rest3 = skip_whitespace(rest2)
if rest3.length() == 0 {
return None
}
let rest4 = match rest3[0] {
@token.Token::Comma => rest3[1:]
_ => rest3
}
match parse_max_track_sizing(rest4) {
Some((max_val, rest5)) => {
// Skip to closing paren
let rest6 = skip_whitespace(rest5)
if rest6.length() > 0 {
match rest6[0] {
@token.Token::RightParen => {
let next_tokens = rest6[1:]
Some(
(
@types.SingleTrackSizing::MinMax(min_val, max_val),
next_tokens,
),
)
}
_ => None
}
} else {
None
}
}
None => None
}
}
None => None
}
} else if name == "fit-content" {
match parse_fit_content_arg(tokens[1:]) {
Some((v, true, rest)) =>
Some((@types.SingleTrackSizing::FitContentPercent(v), rest))
Some((v, false, rest)) =>
Some((@types.SingleTrackSizing::FitContentLength(v), rest))
None => None
}
} else if name == "calc" {
match parse_calc_linear(tokens[1:]) {
Some(((px, pct), rest)) =>
if pct.abs() < 0.0001 {
Some((@types.SingleTrackSizing::Length(px), rest))
} else if px.abs() < 0.0001 {
Some((@types.SingleTrackSizing::Percent(pct), rest))
} else {
// Mixed length+percentage calc cannot be represented as a single
// track breadth; fall back to auto but keep parsing the rest.
Some((@types.SingleTrackSizing::Auto, rest))
}
None => None
}
} else if name == "min" || name == "max" || name == "clamp" {
match parse_math_function(name, tokens[1:]) {
Some((dim, rest)) => {
let tf = match dim {
@types.Length(v) => @types.SingleTrackSizing::Length(v)
@types.Percent(v) => @types.SingleTrackSizing::Percent(v)
_ => @types.SingleTrackSizing::Auto
}
Some((tf, rest))
}
None => None
}
} else {
None
}
_ => None
}
}
///|
/// Parse a track sizing function (including repeat)
fn parse_track_sizing_function(
tokens : ArrayView[@token.Token],
) -> (@types.TrackSizingFunction, ArrayView[@token.Token])? {
let tokens = skip_whitespace(tokens)
if tokens.length() == 0 {
return None
}
match tokens[0] {
@token.Token::Function(name) =>
if name == "repeat" {
// Parse repeat(count, tracks...)
let rest = tokens[1:]
let rest = skip_whitespace(rest)
if rest.length() == 0 {
return None
}
// Parse repeat count
let (count, rest2) : (@types.RepeatCount, ArrayView[@token.Token]) = match
rest[0] {
@token.Token::Number(value, _) =>
(@types.RepeatCount::Count(value.to_int()), rest[1:])
@token.Token::Ident(s) =>
if s == "auto-fill" {
(@types.RepeatCount::AutoFill, rest[1:])
} else if s == "auto-fit" {
(@types.RepeatCount::AutoFit, rest[1:])
} else {
return None
}
_ => return None
}
// Skip comma
let rest3 = skip_whitespace(rest2)
if rest3.length() == 0 {
return None
}
let rest4 = match rest3[0] {
@token.Token::Comma => rest3[1:]
_ => rest3
}
// Parse track list until closing paren, preserving the line names
// declared within one repetition (index = line, length = tracks + 1).
let tracks : Array[@types.SingleTrackSizing] = []
let line_names : Array[Array[String]] = []
let mut remaining = rest4
let mut done = false
while !done {
let (names, after_names) = capture_grid_line_names(remaining)
remaining = after_names
if remaining.length() == 0 {
line_names.push(names)
done = true
} else {
match remaining[0] {
@token.Token::RightParen => {
line_names.push(names)
remaining = remaining[1:]
done = true
}
_ =>
match parse_single_track_sizing(remaining) {
Some((track, rest)) => {
line_names.push(names)
tracks.push(track)
remaining = rest
}
None => {
line_names.push(names)
done = true
}
}
}
}
}
if tracks.length() > 0 {
Some(
(
@types.TrackSizingFunction::Repeat(count, tracks, line_names),
remaining,
),
)
} else {
None
}
} else if name == "minmax" {
// minmax is also valid as TrackSizingFunction
match parse_single_track_sizing(tokens) {
Some((@types.SingleTrackSizing::MinMax(min_val, max_val), rest)) =>
Some((@types.TrackSizingFunction::MinMax(min_val, max_val), rest))
_ => None
}
} else if name == "fit-content" {
match parse_fit_content_arg(tokens[1:]) {
Some((v, true, rest)) =>
Some((@types.TrackSizingFunction::FitContentPercent(v), rest))
Some((v, false, rest)) =>
Some((@types.TrackSizingFunction::FitContentLength(v), rest))
None => None
}
} else if name == "calc" {
match parse_calc_linear(tokens[1:]) {
Some(((px, pct), rest)) =>
if pct.abs() < 0.0001 {
Some((@types.TrackSizingFunction::Length(px), rest))
} else if px.abs() < 0.0001 {
Some((@types.TrackSizingFunction::Percent(pct), rest))
} else {
// Mixed length+percentage calc cannot be represented as a single
// track breadth; fall back to auto but keep parsing the rest.
Some((@types.TrackSizingFunction::Auto, rest))
}
None => None
}
} else if name == "min" || name == "max" || name == "clamp" {
match parse_math_function(name, tokens[1:]) {
Some((dim, rest)) => {
let tf = match dim {
@types.Length(v) => @types.TrackSizingFunction::Length(v)
@types.Percent(v) => @types.TrackSizingFunction::Percent(v)
_ => @types.TrackSizingFunction::Auto
}
Some((tf, rest))
}
None => None
}
} else {
None
}
@token.Token::Dimension(value, unit) =>
if unit == "px" {
Some((@types.TrackSizingFunction::Length(value), tokens[1:]))
} else if unit == "%" {
Some((@types.TrackSizingFunction::Percent(value / 100.0), tokens[1:]))
} else if unit == "fr" {
Some((@types.TrackSizingFunction::Fr(value), tokens[1:]))
} else {
// rem/em and physical units (pt/cm/in/...) are converted to px.
let px = match dimension_unit_to_px(value, unit.to_lower()) {
Some(p) => p
None => value
}
Some((@types.TrackSizingFunction::Length(px), tokens[1:]))
}
@token.Token::Percentage(value) =>
Some((@types.TrackSizingFunction::Percent(value / 100.0), tokens[1:]))
@token.Token::Number(value, _) =>
if value == 0.0 {
Some((@types.TrackSizingFunction::Length(0.0), tokens[1:]))
} else {
None
}
@token.Token::Ident(s) =>
if s == "auto" {
Some((@types.TrackSizingFunction::Auto, tokens[1:]))
} else if s == "min-content" {
Some((@types.TrackSizingFunction::MinContent, tokens[1:]))
} else if s == "max-content" {
Some((@types.TrackSizingFunction::MaxContent, tokens[1:]))
} else if s == "none" {
// none means empty template
None
} else {
None
}
_ => None
}
}
///|
/// Capture the grid line names declared in `[name1 name2 ...]` bracket groups at
/// the current position. Returns the collected names (lowercased, possibly
/// empty) and the remaining tokens after the brackets. Consecutive bracket
/// groups at the same line are merged.
fn capture_grid_line_names(
tokens : ArrayView[@token.Token],
) -> (Array[String], ArrayView[@token.Token]) {
let names : Array[String] = []
let mut remaining = skip_whitespace(tokens)
while remaining.length() > 0 {
match remaining[0] {
@token.Token::LeftBracket => {
let mut i = 1
while i < remaining.length() {
match remaining[i] {
@token.Token::RightBracket => {
i = i + 1
break
}
@token.Token::Ident(s) => {
names.push(s.to_lower())
i = i + 1
}
_ => i = i + 1
}
}
remaining = skip_whitespace(remaining[i:])
}
_ => return (names, remaining)
}
}
(names, remaining)
}
///|
/// Parse grid-template-columns or grid-template-rows value, preserving named
/// lines. Returns the track sizing functions and the line names (index = grid
/// line, length = tracks + 1).
fn parse_grid_template_track_list(
tokens : ArrayView[@token.Token],
) -> (Array[@types.TrackSizingFunction], Array[Array[String]]) {
let result : Array[@types.TrackSizingFunction] = []
let line_names : Array[Array[String]] = []
let mut remaining = tokens
let mut done = false
while !done {
let (names, rest) = capture_grid_line_names(remaining)
remaining = rest
let stop = if remaining.length() == 0 {
true
} else {
match remaining[0] {
@token.Token::Semicolon | @token.Token::EOF => true
@token.Token::Ident("none") =>
// `none` means empty template: no tracks, no line names.
return ([], [])
_ =>
match parse_track_sizing_function(remaining) {
Some((track, rest2)) => {
result.push(track)
remaining = rest2
false
}
None => true
}
}
}
if stop {
// `names` belongs to the final grid line.
line_names.push(names)
done = true
} else {
// `names` belongs to the line before the track we just pushed.
line_names.push(names)
}
}
(result, line_names)
}
///|
/// Parse grid-template-columns or grid-template-rows value (tracks only).
fn parse_grid_template_tracks(
tokens : ArrayView[@token.Token],
) -> Array[@types.TrackSizingFunction] {
parse_grid_template_track_list(tokens).0
}
///|
/// Parse the `` that follows `subgrid`. Unlike a track list,
/// each `[...]` bracket group is a SEPARATE grid line, so names are not merged.
/// `repeat(...)` inside the list is not yet expanded and is skipped.
fn parse_subgrid_line_names(
tokens : ArrayView[@token.Token],
) -> Array[Array[String]] {
let result : Array[Array[String]] = []
let mut remaining = skip_whitespace(tokens)
while remaining.length() > 0 {
match remaining[0] {
@token.Token::LeftBracket => {
let names : Array[String] = []
let mut i = 1
while i < remaining.length() {
match remaining[i] {
@token.Token::RightBracket => {
i = i + 1
break
}
@token.Token::Ident(s) => {
names.push(s.to_lower())
i = i + 1
}
_ => i = i + 1
}
}
result.push(names)
remaining = skip_whitespace(remaining[i:])
}
@token.Token::Semicolon | @token.Token::EOF => break
// Skip anything else (e.g. an unsupported repeat()) without aborting.
_ => remaining = skip_whitespace(remaining[1:])
}
}
result
}
///|
/// Parse one grid template axis (grid-template-columns / -rows), detecting the
/// `subgrid` and `masonry` keywords. Returns the axis kind, the explicit track
/// sizing functions, and the line names.
fn parse_grid_template_axis(
tokens : ArrayView[@token.Token],
) -> (
@types.GridTemplateKind,
Array[@types.TrackSizingFunction],
Array[Array[String]],
) {
let toks = skip_whitespace(tokens)
if toks.length() > 0 {
match toks[0] {
@token.Token::Ident("subgrid") =>
return (@types.Subgrid, [], parse_subgrid_line_names(toks[1:]))
@token.Token::Ident("masonry") => return (@types.Masonry, [], [])
_ => ()
}
}
let (tracks, names) = parse_grid_template_track_list(toks)
(@types.Explicit, tracks, names)
}
///|
/// Parse grid-template shorthand: ` / `, preserving named lines.
fn parse_grid_template_shorthand(
tokens : ArrayView[@token.Token],
) -> (
Array[@types.TrackSizingFunction],
Array[Array[String]],
Array[@types.TrackSizingFunction],
Array[Array[String]],
)? {
let mut slash_index = -1
for i = 0; i < tokens.length(); i = i + 1 {
match tokens[i] {
@token.Token::Delim('/') => {
slash_index = i
break
}
_ => ()
}
}
if slash_index < 0 {
return None
}
let (rows, row_names) = parse_grid_template_track_list(tokens[:slash_index])
let (cols, col_names) = parse_grid_template_track_list(tokens[slash_index + 1:])
Some((rows, row_names, cols, col_names))
}
///|
/// Parse the `grid-template` / `grid` "areas" form:
/// [ ? ? ? ]+ [ / ]?
/// Returns the areas (one string per row), the row track sizes and row line
/// names, and the column track sizes and column line names. Returns None when
/// the value is not the areas form (no tokens present).
fn parse_grid_template_areas_form(
tokens : ArrayView[@token.Token],
) -> (
Array[String],
Array[@types.TrackSizingFunction],
Array[Array[String]],
Array[@types.TrackSizingFunction],
Array[Array[String]],
)? {
// Split off the optional `/ ` part at the top-level slash.
let mut slash_index = -1
let mut has_string = false
for i = 0; i < tokens.length(); i = i + 1 {
match tokens[i] {
@token.Token::Delim('/') => if slash_index < 0 { slash_index = i }
@token.Token::String(_) => has_string = true
_ => ()
}
}
if !has_string {
return None
}
let left = if slash_index < 0 { tokens } else { tokens[:slash_index] }
let areas : Array[String] = []
let rows : Array[@types.TrackSizingFunction] = []
let row_names : Array[Array[String]] = []
let mut remaining = left
let mut done = false
while !done {
let (names, rest) = capture_grid_line_names(remaining)
remaining = rest
if remaining.length() == 0 {
row_names.push(names)
done = true
} else {
match remaining[0] {
@token.Token::String(s) => {
row_names.push(names)
areas.push(s)
remaining = remaining[1:]
// Optional row track size after the string.
let after = skip_whitespace(remaining)
match parse_track_sizing_function(after) {
Some((track, rest2)) => {
rows.push(track)
remaining = rest2
}
None => rows.push(@types.TrackSizingFunction::Auto)
}
}
_ => {
row_names.push(names)
done = true
}
}
}
}
let (cols, col_names) = if slash_index < 0 {
([], [])
} else {
parse_grid_template_track_list(tokens[slash_index + 1:])
}
Some((areas, rows, row_names, cols, col_names))
}
///|
/// Whether a side of the `grid` shorthand contains the `auto-flow` keyword.
fn grid_side_has_autoflow(tokens : ArrayView[@token.Token]) -> Bool {
for i = 0; i < tokens.length(); i = i + 1 {
match tokens[i] {
@token.Token::Ident("auto-flow") => return true
_ => ()
}
}
false
}
///|
/// Parse the `[ auto-flow && dense? ] ?` side of a `grid` shorthand.
/// Returns the resolved auto-flow direction and the implicit (auto) track list.
fn parse_grid_autoflow_side(
tokens : ArrayView[@token.Token],
is_column : Bool,
) -> (@types.GridAutoFlow, Array[@types.TrackSizingFunction]) {
let mut dense = false
let mut remaining = skip_whitespace(tokens)
let mut consuming = true
while consuming && remaining.length() > 0 {
match remaining[0] {
@token.Token::Ident("auto-flow") => remaining = skip_whitespace(remaining[1:])
@token.Token::Ident("dense") => {
dense = true
remaining = skip_whitespace(remaining[1:])
}
_ => consuming = false
}
}
let flow = if is_column {
if dense {
@types.GridAutoFlow::ColumnDense
} else {
@types.GridAutoFlow::Column
}
} else if dense {
@types.GridAutoFlow::RowDense
} else {
@types.GridAutoFlow::Row
}
let (tracks, _) = parse_grid_template_track_list(remaining)
(flow, tracks)
}
///|
/// Parse the `grid` shorthand auto-flow form:
/// / [ auto-flow && dense? ] ?
/// [ auto-flow && dense? ] ? /
/// Returns (auto-flow, template-rows, row-names, template-cols, col-names,
/// auto-rows, auto-cols). Returns None when neither side has `auto-flow`.
fn parse_grid_autoflow_form(
tokens : ArrayView[@token.Token],
) -> (
@types.GridAutoFlow,
Array[@types.TrackSizingFunction],
Array[Array[String]],
Array[@types.TrackSizingFunction],
Array[Array[String]],
Array[@types.TrackSizingFunction],
Array[@types.TrackSizingFunction],
)? {
let mut slash = -1
for i = 0; i < tokens.length(); i = i + 1 {
match tokens[i] {
@token.Token::Delim('/') => {
slash = i
break
}
_ => ()
}
}
if slash < 0 {
return None
}
let left = tokens[:slash]
let right = tokens[slash + 1:]
if grid_side_has_autoflow(right) {
// rows are explicit (left); columns flow automatically (right).
let (rows, row_names) = parse_grid_template_track_list(left)
let (flow, auto_cols) = parse_grid_autoflow_side(right, true)
Some((flow, rows, row_names, [], [], [], auto_cols))
} else if grid_side_has_autoflow(left) {
// rows flow automatically (left); columns are explicit (right).
let (cols, col_names) = parse_grid_template_track_list(right)
let (flow, auto_rows) = parse_grid_autoflow_side(left, false)
Some((flow, [], [], cols, col_names, auto_rows, []))
} else {
None
}
}
///|
/// Parse the `grid` shorthand auto-flow form from a raw string.
pub fn parse_grid_autoflow_form_from_string(
value : String,
) -> (
@types.GridAutoFlow,
Array[@types.TrackSizingFunction],
Array[Array[String]],
Array[@types.TrackSizingFunction],
Array[Array[String]],
Array[@types.TrackSizingFunction],
Array[@types.TrackSizingFunction],
)? {
let tokens = @token.tokenize(value)
parse_grid_autoflow_form(tokens[:])
}
///|
/// Classify one `` token slice into a GridPlacement. Handles
/// auto | | && ? |
/// span && ? && ?
/// (order-independent; CSS keywords are case-insensitive).
fn classify_grid_line(tokens : ArrayView[@token.Token]) -> @types.GridPlacement {
let mut is_span = false
let mut has_auto = false
let mut line = 0
let mut name : String? = None
for i = 0; i < tokens.length(); i = i + 1 {
match tokens[i] {
@token.Token::Ident(s) => {
let ls = s.to_lower()
if ls == "auto" {
has_auto = true
} else if ls == "span" {
is_span = true
} else {
name = Some(ls)
}
}
@token.Token::Number(v, _) => {
let n = v.to_int()
if n != 0 {
line = n
}
}
_ => ()
}
}
if is_span {
let count = if line != 0 { line } else { 1 }
match name {
Some(nm) => @types.GridPlacement::SpanNamed(count, nm)
None => @types.GridPlacement::Span(count)
}
} else if has_auto {
@types.GridPlacement::Auto
} else {
match name {
Some(nm) =>
if line != 0 {
@types.GridPlacement::NamedLine(line, nm)
} else {
@types.GridPlacement::Named(nm)
}
None => if line != 0 { @types.GridPlacement::Line(line) } else { @types.GridPlacement::Auto }
}
}
}
///|
/// Split a token view into segments separated by top-level `/` delimiters.
fn split_grid_segments(
tokens : ArrayView[@token.Token],
) -> Array[ArrayView[@token.Token]] {
let segments : Array[ArrayView[@token.Token]] = []
let mut start = 0
for i = 0; i < tokens.length(); i = i + 1 {
match tokens[i] {
@token.Token::Delim('/') => {
segments.push(tokens[start:i])
start = i + 1
}
_ => ()
}
}
segments.push(tokens[start:])
segments
}
///|
/// Whether a token segment carries any meaningful (non-whitespace/EOF) token.
fn grid_segment_is_empty(tokens : ArrayView[@token.Token]) -> Bool {
for i = 0; i < tokens.length(); i = i + 1 {
match tokens[i] {
@token.Token::Whitespace | @token.Token::EOF => ()
_ => return false
}
}
true
}
///|
/// Parse a grid-column / grid-row value (` [ / ]?`) from tokens.
/// A single bare `` is copied to both start and end (implicit
/// named area); any other single value leaves the end as auto.
fn parse_grid_line_shorthand_tokens(
tokens : ArrayView[@token.Token],
) -> (@types.GridPlacement, @types.GridPlacement) {
let segments = split_grid_segments(tokens)
if segments.length() >= 2 {
(classify_grid_line(segments[0]), classify_grid_line(segments[1]))
} else {
let start = classify_grid_line(tokens)
match start {
@types.GridPlacement::Named(_) => (start, start)
_ => (start, @types.GridPlacement::Auto)
}
}
}
///|
/// Parse a grid-area value (` [ / [ /
/// [ / ]?]?]?`) from tokens, filling omitted longhands with auto
/// (a single value applies to both start longhands).
fn parse_grid_area_shorthand_tokens(
tokens : ArrayView[@token.Token],
) -> (
@types.GridPlacement,
@types.GridPlacement,
@types.GridPlacement,
@types.GridPlacement,
) {
let parts = split_grid_segments(tokens).filter(fn(s) {
!grid_segment_is_empty(s)
})
let auto = @types.GridPlacement::Auto
if parts.length() >= 4 {
(
classify_grid_line(parts[0]),
classify_grid_line(parts[1]),
classify_grid_line(parts[2]),
classify_grid_line(parts[3]),
)
} else if parts.length() == 3 {
(
classify_grid_line(parts[0]),
classify_grid_line(parts[1]),
classify_grid_line(parts[2]),
auto,
)
} else if parts.length() == 2 {
(classify_grid_line(parts[0]), classify_grid_line(parts[1]), auto, auto)
} else if parts.length() == 1 {
let start = classify_grid_line(parts[0])
(start, start, auto, auto)
} else {
(auto, auto, auto, auto)
}
}
///|
/// If the value is a single `` (no slash, not a number / `auto` /
/// `span` / global keyword), return that lowercased ident as a grid-area name.
/// Otherwise return None (the value is a line-based placement).
fn grid_area_single_name(tokens : ArrayView[@token.Token]) -> String? {
let mut found : String? = None
for i = 0; i < tokens.length(); i = i + 1 {
match tokens[i] {
@token.Token::Whitespace | @token.Token::EOF => ()
@token.Token::Ident(s) => {
let ls = s.to_lower()
if ls == "auto" ||
ls == "span" ||
ls == "none" ||
ls == "inherit" ||
ls == "initial" ||
ls == "unset" {
return None
}
// More than one meaningful token => not a bare ident.
match found {
Some(_) => return None
None => found = Some(ls)
}
}
// Any number, slash, or other token means this is not a bare ident.
_ => return None
}
}
found
}
///|
/// Parse a single `` from a raw string.
pub fn parse_grid_placement_from_string(
value : String,
) -> @types.GridPlacement {
classify_grid_line(@token.tokenize(value)[:])
}
///|
/// Parse a grid-column / grid-row shorthand from a raw string.
pub fn parse_grid_line_shorthand_from_string(
value : String,
) -> (@types.GridPlacement, @types.GridPlacement) {
parse_grid_line_shorthand_tokens(@token.tokenize(value)[:])
}
///|
/// Parse a grid-area shorthand from a raw string.
pub fn parse_grid_area_shorthand_from_string(
value : String,
) -> (
@types.GridPlacement,
@types.GridPlacement,
@types.GridPlacement,
@types.GridPlacement,
) {
parse_grid_area_shorthand_tokens(@token.tokenize(value)[:])
}
///|
/// Parse grid-template-columns or grid-template-rows from a raw string
pub fn parse_grid_template_tracks_from_string(
value : String,
) -> Array[@types.TrackSizingFunction] {
let tokens = @token.tokenize(value)
parse_grid_template_tracks(tokens[:])
}
///|
/// Parse grid-template-columns or grid-template-rows from a raw string,
/// returning both the track sizing functions and the named lines (index = grid
/// line, length = tracks + 1).
pub fn parse_grid_template_track_list_from_string(
value : String,
) -> (Array[@types.TrackSizingFunction], Array[Array[String]]) {
let tokens = @token.tokenize(value)
parse_grid_template_track_list(tokens[:])
}
///|
/// Parse the `grid-template` / `grid` areas form from a raw string.
/// Returns (areas, rows, row_line_names, cols, col_line_names) or None when the
/// value is not the areas form.
pub fn parse_grid_template_areas_form_from_string(
value : String,
) -> (
Array[String],
Array[@types.TrackSizingFunction],
Array[Array[String]],
Array[@types.TrackSizingFunction],
Array[Array[String]],
)? {
let tokens = @token.tokenize(value)
parse_grid_template_areas_form(tokens[:])
}
///|
/// Parse one grid template axis from a raw string, detecting subgrid/masonry.
/// Returns the axis kind, the explicit tracks, and the line names.
pub fn parse_grid_template_axis_from_string(
value : String,
) -> (
@types.GridTemplateKind,
Array[@types.TrackSizingFunction],
Array[Array[String]],
) {
let tokens = @token.tokenize(value)
parse_grid_template_axis(tokens[:])
}
///|
/// Parse overflow property
fn parse_overflow(tokens : ArrayView[@token.Token]) -> @types.Overflow? {
if tokens.length() == 0 {
return None
}
match tokens[0] {
@token.Token::Ident(s) =>
if s == "visible" {
Some(@types.Visible)
} else if s == "hidden" {
Some(@types.Hidden)
} else if s == "clip" {
Some(@types.Clip)
} else if s == "scroll" {
Some(@types.Scroll)
} else if s == "auto" {
Some(@types.Overflow::Auto)
} else {
None
}
_ => None
}
}
///|
/// Parse clip property (legacy, for accessibility patterns)
fn parse_clip(tokens : ArrayView[@token.Token]) -> @types.ClipRect {
if tokens.length() == 0 {
return @types.ClipRect::Auto
}
match tokens[0] {
@token.Token::Ident(s) =>
if s == "auto" || s == "initial" || s == "unset" {
@types.ClipRect::Auto
} else {
@types.ClipRect::Auto
}
@token.Token::Function(name) =>
if name == "rect" {
// Parse rect(top, right, bottom, left)
// tokens[1:] contains the arguments
let args = tokens[1:]
let values : Array[Double] = []
for token in args {
match token {
@token.Token::Dimension(v, unit) => {
// rem/em and physical length units are converted to px.
let px = match dimension_unit_to_px(v, unit.to_lower()) {
Some(p) => p
None => v
}
values.push(px)
}
@token.Token::Number(v, _) => values.push(v)
@token.Token::Ident(s) =>
if s == "auto" {
values.push(0.0) // auto in rect is treated as 0
}
_ => ()
}
if values.length() >= 4 {
break
}
}
if values.length() >= 4 {
@types.ClipRect::Rect(
top=values[0],
right=values[1],
bottom=values[2],
left=values[3],
)
} else {
@types.ClipRect::Auto
}
} else {
@types.ClipRect::Auto
}
_ => @types.ClipRect::Auto
}
}
///|
/// Parse clip-path property subset for paint and hit testing.
fn parse_clip_path(tokens : ArrayView[@token.Token]) -> @style.ClipPath {
let remaining = skip_whitespace(tokens)
if remaining.length() == 0 {
return @style.ClipPath::None
}
match remaining[0] {
@token.Token::Ident(s) =>
if s == "none" || s == "initial" || s == "unset" {
@style.ClipPath::None
} else {
@style.ClipPath::None
}
@token.Token::Function(name) =>
if name == "inset" {
parse_clip_path_inset(remaining[1:])
} else if name == "rect" {
parse_clip_path_rect(remaining[1:])
} else if name == "xywh" {
parse_clip_path_xywh(remaining[1:])
} else if name == "circle" {
parse_clip_path_circle(remaining[1:])
} else if name == "ellipse" {
parse_clip_path_ellipse(remaining[1:])
} else if name == "polygon" {
parse_clip_path_polygon(remaining[1:])
} else if name == "path" {
parse_clip_path_path(remaining[1:])
} else {
@style.ClipPath::None
}
_ => @style.ClipPath::None
}
}
///|
fn parse_clip_path_inset(tokens : ArrayView[@token.Token]) -> @style.ClipPath {
let values : Array[Double] = []
for token in tokens {
match token {
@token.Token::Dimension(v, unit) =>
if unit == "%" {
values.push(-(v / 100.0))
} else {
// rem/em and physical length units are converted to px.
let px = match dimension_unit_to_px(v, unit.to_lower()) {
Some(p) => p
None => v
}
values.push(px)
}
@token.Token::Percentage(v) => values.push(-(v / 100.0))
@token.Token::Number(v, _) => values.push(v)
@token.Token::Ident(s) => if s == "round" { break }
@token.Token::RightParen | @token.Token::Semicolon | @token.Token::EOF =>
break
_ => ()
}
if values.length() >= 4 {
break
}
}
match values.length() {
0 => @style.ClipPath::None
1 => {
let v = values[0]
@style.ClipPath::Inset(v, v, v, v)
}
2 => {
let vertical = values[0]
let horizontal = values[1]
@style.ClipPath::Inset(vertical, horizontal, vertical, horizontal)
}
3 => @style.ClipPath::Inset(values[0], values[1], values[2], values[1])
_ => @style.ClipPath::Inset(values[0], values[1], values[2], values[3])
}
}
///|
fn parse_clip_path_rect(tokens : ArrayView[@token.Token]) -> @style.ClipPath {
let values : Array[Double] = []
for token in tokens {
match token {
@token.Token::RightParen | @token.Token::Semicolon | @token.Token::EOF =>
break
@token.Token::Whitespace => ()
_ =>
match parse_clip_path_shape_token(token) {
Some(v) => values.push(v)
None => ()
}
}
if values.length() >= 4 {
break
}
}
if values.length() < 4 {
@style.ClipPath::None
} else {
@style.ClipPath::Rect(values[0], values[1], values[2], values[3])
}
}
///|
fn parse_clip_path_xywh(tokens : ArrayView[@token.Token]) -> @style.ClipPath {
let values : Array[Double] = []
for token in tokens {
match token {
@token.Token::Ident(s) => if s == "round" { break }
@token.Token::RightParen | @token.Token::Semicolon | @token.Token::EOF =>
break
@token.Token::Whitespace => ()
_ =>
match parse_clip_path_shape_token(token) {
Some(v) => values.push(v)
None => ()
}
}
if values.length() >= 4 {
break
}
}
if values.length() < 4 {
@style.ClipPath::None
} else {
@style.ClipPath::Xywh(values[0], values[1], values[2], values[3])
}
}
///|
fn parse_clip_path_circle(tokens : ArrayView[@token.Token]) -> @style.ClipPath {
let mut radius = -0.5
let mut center_x = -0.5
let mut center_y = -0.5
let center_values : Array[Double] = []
let mut seen_at = false
let mut radius_set = false
for token in tokens {
match token {
@token.Token::Ident(s) =>
if s == "at" {
seen_at = true
} else {
return @style.ClipPath::None
}
@token.Token::RightParen | @token.Token::Semicolon | @token.Token::EOF =>
break
@token.Token::Whitespace => ()
_ =>
match parse_clip_path_shape_token(token) {
Some(v) =>
if seen_at {
center_values.push(v)
} else if !radius_set {
radius = v
radius_set = true
}
None => ()
}
}
}
if seen_at {
if center_values.length() < 2 {
return @style.ClipPath::None
}
center_x = center_values[0]
center_y = center_values[1]
}
@style.ClipPath::Circle(radius, center_x, center_y)
}
///|
fn parse_clip_path_ellipse(tokens : ArrayView[@token.Token]) -> @style.ClipPath {
let mut radius_x = -0.5
let mut radius_y = -0.5
let mut center_x = -0.5
let mut center_y = -0.5
let radii : Array[Double] = []
let center_values : Array[Double] = []
let mut seen_at = false
for token in tokens {
match token {
@token.Token::Ident(s) =>
if s == "at" {
seen_at = true
} else {
return @style.ClipPath::None
}
@token.Token::RightParen | @token.Token::Semicolon | @token.Token::EOF =>
break
@token.Token::Whitespace => ()
_ =>
match parse_clip_path_shape_token(token) {
Some(v) => if seen_at { center_values.push(v) } else { radii.push(v) }
None => ()
}
}
}
if radii.length() == 0 {
()
} else if radii.length() >= 2 {
radius_x = radii[0]
radius_y = radii[1]
} else {
return @style.ClipPath::None
}
if seen_at {
if center_values.length() < 2 {
return @style.ClipPath::None
}
center_x = center_values[0]
center_y = center_values[1]
}
@style.ClipPath::Ellipse(radius_x, radius_y, center_x, center_y)
}
///|
fn parse_clip_path_polygon(tokens : ArrayView[@token.Token]) -> @style.ClipPath {
let values : Array[Double] = []
for token in tokens {
match token {
@token.Token::Ident(s) =>
if s == "evenodd" || s == "nonzero" {
()
} else {
return @style.ClipPath::None
}
@token.Token::RightParen | @token.Token::Semicolon | @token.Token::EOF =>
break
@token.Token::Whitespace => ()
_ =>
match parse_clip_path_shape_token(token) {
Some(v) => values.push(v)
None => ()
}
}
}
if values.length() < 6 || values.length() % 2 != 0 {
return @style.ClipPath::None
}
let points : Array[(Double, Double)] = []
let mut i = 0
while i + 1 < values.length() {
points.push((values[i], values[i + 1]))
i = i + 2
}
@style.ClipPath::Polygon(points)
}
///|
fn parse_clip_path_path(tokens : ArrayView[@token.Token]) -> @style.ClipPath {
for token in tokens {
match token {
@token.Token::String(path_data) =>
return parse_clip_path_path_data(path_data)
@token.Token::RightParen | @token.Token::Semicolon | @token.Token::EOF =>
break
_ => ()
}
}
@style.ClipPath::None
}
///|
fn parse_clip_path_path_data(path_data : String) -> @style.ClipPath {
let tokens = tokenize_clip_path_path_data(path_data)
let points : Array[(Double, Double)] = []
let mut i = 0
let mut command = ""
let mut current_x = 0.0
let mut current_y = 0.0
let mut start_x = 0.0
let mut start_y = 0.0
let mut has_quadratic_control = false
let mut quadratic_control_x = 0.0
let mut quadratic_control_y = 0.0
let mut has_cubic_control = false
let mut cubic_control_x = 0.0
let mut cubic_control_y = 0.0
while i < tokens.length() {
let token = tokens[i]
if is_clip_path_path_command(token) {
if token == "Z" || token == "z" {
current_x = start_x
current_y = start_y
has_quadratic_control = false
has_cubic_control = false
command = ""
i += 1
continue
}
command = token
i += 1
continue
}
match command {
"M" | "m" => {
if i + 1 >= tokens.length() {
return @style.ClipPath::None
}
let x = match parse_clip_path_path_number(tokens[i]) {
Some(v) => v
None => return @style.ClipPath::None
}
let y = match parse_clip_path_path_number(tokens[i + 1]) {
Some(v) => v
None => return @style.ClipPath::None
}
if command == "m" {
current_x += x
current_y += y
} else {
current_x = x
current_y = y
}
start_x = current_x
start_y = current_y
points.push((current_x, current_y))
command = if command == "m" { "l" } else { "L" }
has_quadratic_control = false
has_cubic_control = false
i += 2
}
"L" | "l" => {
if i + 1 >= tokens.length() {
return @style.ClipPath::None
}
let x = match parse_clip_path_path_number(tokens[i]) {
Some(v) => v
None => return @style.ClipPath::None
}
let y = match parse_clip_path_path_number(tokens[i + 1]) {
Some(v) => v
None => return @style.ClipPath::None
}
if command == "l" {
current_x += x
current_y += y
} else {
current_x = x
current_y = y
}
points.push((current_x, current_y))
has_quadratic_control = false
has_cubic_control = false
i += 2
}
"H" | "h" => {
let x = match parse_clip_path_path_number(tokens[i]) {
Some(v) => v
None => return @style.ClipPath::None
}
if command == "h" {
current_x += x
} else {
current_x = x
}
points.push((current_x, current_y))
has_quadratic_control = false
has_cubic_control = false
i += 1
}
"V" | "v" => {
let y = match parse_clip_path_path_number(tokens[i]) {
Some(v) => v
None => return @style.ClipPath::None
}
if command == "v" {
current_y += y
} else {
current_y = y
}
points.push((current_x, current_y))
has_quadratic_control = false
has_cubic_control = false
i += 1
}
"Q" | "q" => {
if i + 3 >= tokens.length() {
return @style.ClipPath::None
}
let control_x = match parse_clip_path_path_number(tokens[i]) {
Some(v) => v
None => return @style.ClipPath::None
}
let control_y = match parse_clip_path_path_number(tokens[i + 1]) {
Some(v) => v
None => return @style.ClipPath::None
}
let end_x = match parse_clip_path_path_number(tokens[i + 2]) {
Some(v) => v
None => return @style.ClipPath::None
}
let end_y = match parse_clip_path_path_number(tokens[i + 3]) {
Some(v) => v
None => return @style.ClipPath::None
}
let absolute_control_x = if command == "q" {
current_x + control_x
} else {
control_x
}
let absolute_control_y = if command == "q" {
current_y + control_y
} else {
control_y
}
let absolute_end_x = if command == "q" {
current_x + end_x
} else {
end_x
}
let absolute_end_y = if command == "q" {
current_y + end_y
} else {
end_y
}
append_quadratic_path_points(
points, current_x, current_y, absolute_control_x, absolute_control_y, absolute_end_x,
absolute_end_y,
)
current_x = absolute_end_x
current_y = absolute_end_y
has_quadratic_control = true
quadratic_control_x = absolute_control_x
quadratic_control_y = absolute_control_y
has_cubic_control = false
i += 4
}
"T" | "t" => {
if i + 1 >= tokens.length() {
return @style.ClipPath::None
}
let end_x = match parse_clip_path_path_number(tokens[i]) {
Some(v) => v
None => return @style.ClipPath::None
}
let end_y = match parse_clip_path_path_number(tokens[i + 1]) {
Some(v) => v
None => return @style.ClipPath::None
}
let absolute_control_x = if has_quadratic_control {
2.0 * current_x - quadratic_control_x
} else {
current_x
}
let absolute_control_y = if has_quadratic_control {
2.0 * current_y - quadratic_control_y
} else {
current_y
}
let absolute_end_x = if command == "t" {
current_x + end_x
} else {
end_x
}
let absolute_end_y = if command == "t" {
current_y + end_y
} else {
end_y
}
append_quadratic_path_points(
points, current_x, current_y, absolute_control_x, absolute_control_y, absolute_end_x,
absolute_end_y,
)
current_x = absolute_end_x
current_y = absolute_end_y
has_quadratic_control = true
quadratic_control_x = absolute_control_x
quadratic_control_y = absolute_control_y
has_cubic_control = false
i += 2
}
"C" | "c" => {
if i + 5 >= tokens.length() {
return @style.ClipPath::None
}
let control1_x = match parse_clip_path_path_number(tokens[i]) {
Some(v) => v
None => return @style.ClipPath::None
}
let control1_y = match parse_clip_path_path_number(tokens[i + 1]) {
Some(v) => v
None => return @style.ClipPath::None
}
let control2_x = match parse_clip_path_path_number(tokens[i + 2]) {
Some(v) => v
None => return @style.ClipPath::None
}
let control2_y = match parse_clip_path_path_number(tokens[i + 3]) {
Some(v) => v
None => return @style.ClipPath::None
}
let end_x = match parse_clip_path_path_number(tokens[i + 4]) {
Some(v) => v
None => return @style.ClipPath::None
}
let end_y = match parse_clip_path_path_number(tokens[i + 5]) {
Some(v) => v
None => return @style.ClipPath::None
}
let absolute_control1_x = if command == "c" {
current_x + control1_x
} else {
control1_x
}
let absolute_control1_y = if command == "c" {
current_y + control1_y
} else {
control1_y
}
let absolute_control2_x = if command == "c" {
current_x + control2_x
} else {
control2_x
}
let absolute_control2_y = if command == "c" {
current_y + control2_y
} else {
control2_y
}
let absolute_end_x = if command == "c" {
current_x + end_x
} else {
end_x
}
let absolute_end_y = if command == "c" {
current_y + end_y
} else {
end_y
}
append_cubic_path_points(
points, current_x, current_y, absolute_control1_x, absolute_control1_y,
absolute_control2_x, absolute_control2_y, absolute_end_x, absolute_end_y,
)
current_x = absolute_end_x
current_y = absolute_end_y
has_cubic_control = true
cubic_control_x = absolute_control2_x
cubic_control_y = absolute_control2_y
has_quadratic_control = false
i += 6
}
"S" | "s" => {
if i + 3 >= tokens.length() {
return @style.ClipPath::None
}
let control2_x = match parse_clip_path_path_number(tokens[i]) {
Some(v) => v
None => return @style.ClipPath::None
}
let control2_y = match parse_clip_path_path_number(tokens[i + 1]) {
Some(v) => v
None => return @style.ClipPath::None
}
let end_x = match parse_clip_path_path_number(tokens[i + 2]) {
Some(v) => v
None => return @style.ClipPath::None
}
let end_y = match parse_clip_path_path_number(tokens[i + 3]) {
Some(v) => v
None => return @style.ClipPath::None
}
let absolute_control1_x = if has_cubic_control {
2.0 * current_x - cubic_control_x
} else {
current_x
}
let absolute_control1_y = if has_cubic_control {
2.0 * current_y - cubic_control_y
} else {
current_y
}
let absolute_control2_x = if command == "s" {
current_x + control2_x
} else {
control2_x
}
let absolute_control2_y = if command == "s" {
current_y + control2_y
} else {
control2_y
}
let absolute_end_x = if command == "s" {
current_x + end_x
} else {
end_x
}
let absolute_end_y = if command == "s" {
current_y + end_y
} else {
end_y
}
append_cubic_path_points(
points, current_x, current_y, absolute_control1_x, absolute_control1_y,
absolute_control2_x, absolute_control2_y, absolute_end_x, absolute_end_y,
)
current_x = absolute_end_x
current_y = absolute_end_y
has_cubic_control = true
cubic_control_x = absolute_control2_x
cubic_control_y = absolute_control2_y
has_quadratic_control = false
i += 4
}
"A" | "a" => {
if i + 6 >= tokens.length() {
return @style.ClipPath::None
}
let radius_x = match parse_clip_path_path_number(tokens[i]) {
Some(v) => v
None => return @style.ClipPath::None
}
let radius_y = match parse_clip_path_path_number(tokens[i + 1]) {
Some(v) => v
None => return @style.ClipPath::None
}
let rotation_degrees = match
parse_clip_path_path_number(tokens[i + 2]) {
Some(v) => v
None => return @style.ClipPath::None
}
let large_arc_flag = match parse_clip_path_path_number(tokens[i + 3]) {
Some(v) => v
None => return @style.ClipPath::None
}
let sweep_flag = match parse_clip_path_path_number(tokens[i + 4]) {
Some(v) => v
None => return @style.ClipPath::None
}
let end_x = match parse_clip_path_path_number(tokens[i + 5]) {
Some(v) => v
None => return @style.ClipPath::None
}
let end_y = match parse_clip_path_path_number(tokens[i + 6]) {
Some(v) => v
None => return @style.ClipPath::None
}
let absolute_end_x = if command == "a" {
current_x + end_x
} else {
end_x
}
let absolute_end_y = if command == "a" {
current_y + end_y
} else {
end_y
}
append_arc_path_points(
points,
current_x,
current_y,
radius_x,
radius_y,
rotation_degrees,
large_arc_flag != 0.0,
sweep_flag != 0.0,
absolute_end_x,
absolute_end_y,
)
current_x = absolute_end_x
current_y = absolute_end_y
has_quadratic_control = false
has_cubic_control = false
i += 7
}
_ => return @style.ClipPath::None
}
}
if points.length() < 3 {
return @style.ClipPath::None
}
@style.ClipPath::Polygon(points)
}
///|
fn tokenize_clip_path_path_data(path_data : String) -> Array[String] {
let tokens : Array[String] = []
let current = StringBuilder::new()
for c in path_data.iter() {
if is_clip_path_path_command_char(c) {
flush_clip_path_path_token(tokens, current)
tokens.push(clip_path_path_char_to_string(c))
} else if c == ',' || c == ' ' || c == '\t' || c == '\n' || c == '\r' {
flush_clip_path_path_token(tokens, current)
} else if c == '-' || c == '+' {
let current_text = current.to_string()
if !current_text.is_empty() &&
!current_text.has_suffix("e") &&
!current_text.has_suffix("E") {
flush_clip_path_path_token(tokens, current)
}
current.write_char(c)
} else {
current.write_char(c)
}
}
flush_clip_path_path_token(tokens, current)
tokens
}
///|
fn flush_clip_path_path_token(
tokens : Array[String],
current : StringBuilder,
) -> Unit {
let token = current.to_string().trim().to_owned()
if !token.is_empty() {
tokens.push(token)
current.reset()
}
}
///|
fn append_quadratic_path_points(
points : Array[(Double, Double)],
start_x : Double,
start_y : Double,
control_x : Double,
control_y : Double,
end_x : Double,
end_y : Double,
) -> Unit {
let segments = 8
for step in 1..<(segments + 1) {
let t = step.to_double() / segments.to_double()
let one_minus_t = 1.0 - t
let x = one_minus_t * one_minus_t * start_x +
2.0 * one_minus_t * t * control_x +
t * t * end_x
let y = one_minus_t * one_minus_t * start_y +
2.0 * one_minus_t * t * control_y +
t * t * end_y
points.push((x, y))
}
}
///|
fn append_cubic_path_points(
points : Array[(Double, Double)],
start_x : Double,
start_y : Double,
control1_x : Double,
control1_y : Double,
control2_x : Double,
control2_y : Double,
end_x : Double,
end_y : Double,
) -> Unit {
let segments = 8
for step in 1..<(segments + 1) {
let t = step.to_double() / segments.to_double()
let one_minus_t = 1.0 - t
let x = one_minus_t * one_minus_t * one_minus_t * start_x +
3.0 * one_minus_t * one_minus_t * t * control1_x +
3.0 * one_minus_t * t * t * control2_x +
t * t * t * end_x
let y = one_minus_t * one_minus_t * one_minus_t * start_y +
3.0 * one_minus_t * one_minus_t * t * control1_y +
3.0 * one_minus_t * t * t * control2_y +
t * t * t * end_y
points.push((x, y))
}
}
///|
fn append_arc_path_points(
points : Array[(Double, Double)],
start_x : Double,
start_y : Double,
radius_x : Double,
radius_y : Double,
rotation_degrees : Double,
large_arc : Bool,
sweep : Bool,
end_x : Double,
end_y : Double,
) -> Unit {
if (start_x == end_x && start_y == end_y) ||
radius_x == 0.0 ||
radius_y == 0.0 {
points.push((end_x, end_y))
return
}
let mut rx = if radius_x < 0.0 { -radius_x } else { radius_x }
let mut ry = if radius_y < 0.0 { -radius_y } else { radius_y }
let pi = 3.14159265358979323846
let phi = rotation_degrees * pi / 180.0
let cos_phi = @math.cos(phi)
let sin_phi = @math.sin(phi)
let dx = (start_x - end_x) / 2.0
let dy = (start_y - end_y) / 2.0
let x1p = cos_phi * dx + sin_phi * dy
let y1p = -sin_phi * dx + cos_phi * dy
let lambda = x1p * x1p / (rx * rx) + y1p * y1p / (ry * ry)
if lambda > 1.0 {
let scale = lambda.sqrt()
rx = rx * scale
ry = ry * scale
}
let rx2 = rx * rx
let ry2 = ry * ry
let x1p2 = x1p * x1p
let y1p2 = y1p * y1p
let denominator = rx2 * y1p2 + ry2 * x1p2
if denominator == 0.0 {
points.push((end_x, end_y))
return
}
let sq = (rx2 * ry2 - rx2 * y1p2 - ry2 * x1p2) / denominator
let sq_abs = if sq < 0.0 { 0.0 } else { sq }
let coef = sq_abs.sqrt() * (if large_arc == sweep { -1.0 } else { 1.0 })
let cxp = coef * rx * y1p / ry
let cyp = -coef * ry * x1p / rx
let center_x = cos_phi * cxp - sin_phi * cyp + (start_x + end_x) / 2.0
let center_y = sin_phi * cxp + cos_phi * cyp + (start_y + end_y) / 2.0
let theta1 = clip_path_angle_between(
1.0,
0.0,
(x1p - cxp) / rx,
(y1p - cyp) / ry,
)
let mut dtheta = clip_path_angle_between(
(x1p - cxp) / rx,
(y1p - cyp) / ry,
(-x1p - cxp) / rx,
(-y1p - cyp) / ry,
)
if !sweep && dtheta > 0.0 {
dtheta = dtheta - 2.0 * pi
} else if sweep && dtheta < 0.0 {
dtheta = dtheta + 2.0 * pi
}
let segments = clip_path_max_int((dtheta.abs() / pi * 8.0).to_int(), 2)
for step in 1..<(segments + 1) {
let t = step.to_double() / segments.to_double()
let theta = theta1 + t * dtheta
let cos_t = @math.cos(theta)
let sin_t = @math.sin(theta)
let x = cos_phi * rx * cos_t - sin_phi * ry * sin_t + center_x
let y = sin_phi * rx * cos_t + cos_phi * ry * sin_t + center_y
points.push((x, y))
}
}
///|
fn clip_path_angle_between(
ux : Double,
uy : Double,
vx : Double,
vy : Double,
) -> Double {
let dot = ux * vx + uy * vy
let len_product = (ux * ux + uy * uy).sqrt() * (vx * vx + vy * vy).sqrt()
if len_product == 0.0 {
return 0.0
}
let mut cos_angle = dot / len_product
if cos_angle > 1.0 {
cos_angle = 1.0
}
if cos_angle < -1.0 {
cos_angle = -1.0
}
let angle = @math.acos(cos_angle)
let cross = ux * vy - uy * vx
if cross < 0.0 {
-angle
} else {
angle
}
}
///|
fn clip_path_max_int(a : Int, b : Int) -> Int {
if a > b {
a
} else {
b
}
}
///|
fn is_clip_path_path_command(token : String) -> Bool {
token == "M" ||
token == "m" ||
token == "L" ||
token == "l" ||
token == "H" ||
token == "h" ||
token == "V" ||
token == "v" ||
token == "Q" ||
token == "q" ||
token == "T" ||
token == "t" ||
token == "C" ||
token == "c" ||
token == "S" ||
token == "s" ||
token == "A" ||
token == "a" ||
token == "Z" ||
token == "z"
}
///|
fn is_clip_path_path_command_char(c : Char) -> Bool {
c == 'M' ||
c == 'm' ||
c == 'L' ||
c == 'l' ||
c == 'H' ||
c == 'h' ||
c == 'V' ||
c == 'v' ||
c == 'Z' ||
c == 'z' ||
c == 'C' ||
c == 'c' ||
c == 'S' ||
c == 's' ||
c == 'Q' ||
c == 'q' ||
c == 'T' ||
c == 't' ||
c == 'A' ||
c == 'a'
}
///|
fn clip_path_path_char_to_string(c : Char) -> String {
let sb = StringBuilder::new()
sb.write_char(c)
sb.to_string()
}
///|
fn parse_clip_path_path_number(token : String) -> Double? {
let value = @string.parse_double(token) catch { _ => return None }
Some(value)
}
///|
fn parse_clip_path_shape_token(token : @token.Token) -> Double? {
match token {
@token.Token::Dimension(v, unit) =>
if unit == "%" {
Some(-(v / 100.0))
} else {
// rem/em and physical length units are converted to px.
match dimension_unit_to_px(v, unit.to_lower()) {
Some(p) => Some(p)
None => Some(v)
}
}
@token.Token::Percentage(v) => Some(-(v / 100.0))
@token.Token::Number(v, _) => Some(v)
_ => None
}
}
///|
/// Parse box-sizing property
fn parse_box_sizing(tokens : ArrayView[@token.Token]) -> @types.BoxSizing? {
if tokens.length() == 0 {
return None
}
match tokens[0] {
@token.Token::Ident(s) =>
if s == "content-box" {
Some(@types.ContentBox)
} else if s == "border-box" {
Some(@types.BorderBox)
} else {
None
}
_ => None
}
}
///|
/// Skip whitespace tokens and return remaining view
fn skip_whitespace(tokens : ArrayView[@token.Token]) -> ArrayView[@token.Token] {
let mut i = 0
while i < tokens.length() {
match tokens[i] {
@token.Token::Whitespace => i += 1
_ => break
}
}
tokens[i:]
}
///|
/// Parse CSS contain property
/// Supports: none, strict, content, size, inline-size, layout, paint, style
/// Multiple keywords can be combined (e.g., "layout paint")
fn parse_contain(tokens : ArrayView[@token.Token]) -> @style.Contain {
let mut result = @style.Contain::none()
let mut remaining = skip_whitespace(tokens)
while remaining.length() > 0 {
match remaining[0] {
@token.Token::Ident(s) => {
if s == "none" {
return @style.Contain::none()
} else if s == "strict" {
return @style.Contain::strict()
} else if s == "content" {
return @style.Contain::content()
} else if s == "size" {
result = { ..result, size: true }
} else if s == "inline-size" {
result = { ..result, inline_size: true }
} else if s == "layout" {
result = { ..result, layout: true }
} else if s == "paint" {
result = { ..result, paint: true }
} else if s == "style" {
result = { ..result, style: true }
}
remaining = skip_whitespace(remaining[1:])
}
@token.Token::Whitespace => remaining = skip_whitespace(remaining[1:])
_ => break
}
}
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 CSS container-type property into implied containment bits.
/// Supported: normal | size | inline-size
fn parse_container_type(tokens : ArrayView[@token.Token]) -> @style.Contain? {
let remaining = skip_whitespace(tokens)
if remaining.length() == 0 {
return None
}
match remaining[0] {
@token.Token::Ident(s) =>
if s == "normal" {
Some(@style.Contain::none())
} else if s == "size" {
Some({ ..@style.Contain::none(), size: true, style: true })
} else if s == "inline-size" {
Some({ ..@style.Contain::none(), inline_size: true, style: true })
} else {
None
}
_ => None
}
}
///|
/// 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(tokens : ArrayView[@token.Token]) -> Double? {
let remaining = skip_whitespace(tokens)
if remaining.length() == 0 {
return None
}
match remaining[0] {
@token.Token::Ident(s) =>
if s == "none" || s == "auto" {
None
} else {
match parse_dimension(remaining) {
Some(@types.Length(v)) => if v > 0.0 { Some(v) } else { Some(0.0) }
_ => None
}
}
_ =>
match parse_dimension(remaining) {
Some(@types.Length(v)) => if v > 0.0 { Some(v) } 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(
tokens : ArrayView[@token.Token],
) -> (Double?, Double?)? {
let mut remaining = skip_whitespace(tokens)
let axis_values : Array[Double?] = []
while remaining.length() > 0 {
match remaining[0] {
@token.Token::Whitespace => remaining = skip_whitespace(remaining[1:])
@token.Token::Ident(s) =>
if s == "auto" {
remaining = skip_whitespace(remaining[1:])
} else if s == "none" {
axis_values.push(None)
remaining = skip_whitespace(remaining[1:])
} else {
match parse_dimension(remaining) {
Some(@types.Length(v)) =>
axis_values.push(Some(if v > 0.0 { v } else { 0.0 }))
_ => ()
}
remaining = skip_whitespace(remaining[1:])
}
_ => {
match parse_dimension(remaining) {
Some(@types.Length(v)) =>
axis_values.push(Some(if v > 0.0 { v } else { 0.0 }))
_ => ()
}
remaining = skip_whitespace(remaining[1:])
}
}
}
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 CSS transform property
/// Supports: translate(x, y), translateX(x), translateY(y), translate3d(x, y, z), none
fn parse_transform(tokens : ArrayView[@token.Token]) -> @style.Transform {
let remaining = skip_whitespace(tokens)
if remaining.length() == 0 {
return @style.Transform::none()
}
// Check for "none"
match remaining[0] {
@token.Token::Ident(s) => if s == "none" { return @style.Transform::none() }
_ => ()
}
// Reconstruct the value as string for parsing with the full transform parser
// Since transform values like "translate(10px, 20px)" are complex,
// we build the string and reuse the computed transform parser
let sb = StringBuilder::new()
for i = 0; i < remaining.length(); i = i + 1 {
match remaining[i] {
@token.Token::Ident(s) => sb.write_string(s)
@token.Token::Function(name) => {
sb.write_string(name)
sb.write_char('(')
}
@token.Token::Dimension(value, unit) => {
sb.write_string(value.to_string())
sb.write_string(unit)
}
@token.Token::Percentage(value) => {
sb.write_string(value.to_string())
sb.write_char('%')
}
@token.Token::Number(value, _) => sb.write_string(value.to_string())
@token.Token::Comma => sb.write_char(',')
@token.Token::RightParen => sb.write_char(')')
@token.Token::LeftParen => sb.write_char('(')
@token.Token::Whitespace => sb.write_char(' ')
_ => ()
}
}
let value_str = sb.to_string()
// Parse the reconstructed string
parse_transform_string(value_str)
}
///|
fn parse_pointer_events(
tokens : ArrayView[@token.Token],
) -> @style.PointerEvents {
let remaining = skip_whitespace(tokens)
if remaining.length() == 0 {
return @style.PointerEvents::Auto
}
match remaining[0] {
@token.Token::Ident(s) =>
if s == "none" {
@style.PointerEvents::None
} else {
@style.PointerEvents::Auto
}
_ => @style.PointerEvents::Auto
}
}
///|
/// Parse CSS transform from a string value
fn parse_transform_string(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
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 = transform_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 = transform_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_transform_args(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_transform_args(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_transform_args(args_str)
if args.length() >= 1 {
let sx = parse_scale_value(args[0])
scale_x = scale_x * sx
scale_y = scale_y * sx
}
if args.length() >= 2 {
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_transform_args(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_transform_args(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_transform_args(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_transform_args(args_str)
if args.length() >= 16 {
// Keep a 2D affine approximation (a,b,c,d,e,f) for 2D consumers and
// the full 16-element column-major matrix for 3D interpolation.
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().to_owned()
if v.is_empty() {
return 0.0
}
if v.has_suffix("deg") {
let num_str = v.unsafe_substring(start=0, end=v.length() - 3)
return @string.parse_double(num_str.trim()) catch { _ => 0.0 }
}
if v.has_suffix("turn") {
let num_str = v.unsafe_substring(start=0, end=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 = v.unsafe_substring(start=0, end=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 = v.unsafe_substring(start=0, end=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 a single translate value (can be length or percentage).
/// Tokenizes the value so every length unit (px/rem/em/pt/cm/in/vw/...) is
/// converted to px via dimension_unit_to_px, instead of only px/em/rem.
fn parse_translate_value(value : String) -> @style.TranslateValue {
let v = value.trim()
if v.is_empty() {
return @style.TranslateValue::Length(0.0)
}
let tokens = skip_whitespace(@token.tokenize(v.to_owned())[:])
if tokens.length() == 0 {
return @style.TranslateValue::Length(0.0)
}
match tokens[0] {
@token.Token::Dimension(n, raw_unit) => {
let unit = raw_unit.to_lower()
if unit == "%" {
@style.TranslateValue::Percent(n / 100.0)
} else {
let px = match dimension_unit_to_px(n, unit) {
Some(p) => p
None => n
}
@style.TranslateValue::Length(px)
}
}
@token.Token::Percentage(n) => @style.TranslateValue::Percent(n / 100.0)
@token.Token::Number(n, _) => @style.TranslateValue::Length(n)
_ => @style.TranslateValue::Length(0.0)
}
}
///|
/// Split a value string into whitespace-separated, non-empty parts.
fn split_ws_parts(value : String) -> Array[String] {
let parts : Array[String] = []
for p in value.trim().split(" ") {
let t = p.trim()
if t.length() > 0 {
parts.push(t.to_owned())
}
}
parts
}
///|
/// Parse the `translate` property (`{1,3}`) into a Transform with the
/// translate components set (z is ignored; this engine is 2D).
pub fn parse_translate_property_from_string(value : String) -> @style.Transform {
let parts = split_ws_parts(value)
if parts.length() == 0 || value.trim().to_lower() == "none" {
return @style.Transform::none()
}
let x = parse_translate_value(parts[0])
let y = if parts.length() > 1 {
parse_translate_value(parts[1])
} else {
@style.TranslateValue::Length(0.0)
}
{ ..@style.Transform::none(), translate_x: x, translate_y: y }
}
///|
/// Parse the `scale` property (`{1,3}`) into a Transform with scale set.
/// A single value applies to both axes.
pub fn parse_scale_property_from_string(value : String) -> @style.Transform {
let parts = split_ws_parts(value)
if parts.length() == 0 || value.trim().to_lower() == "none" {
return @style.Transform::none()
}
let sx = parse_scale_value(parts[0])
let sy = if parts.length() > 1 { parse_scale_value(parts[1]) } else { sx }
{ ..@style.Transform::none(), scale_x: sx, scale_y: sy }
}
///|
/// Parse the `rotate` property (`` or `[x|y|z|] `) into a
/// Transform with rotate set. Only the 2D z-rotation angle is kept.
pub fn parse_rotate_property_from_string(value : String) -> @style.Transform {
let parts = split_ws_parts(value)
if parts.length() == 0 || value.trim().to_lower() == "none" {
return @style.Transform::none()
}
// The angle is the last part (an axis keyword / vector may precede it).
let angle = parse_rotate_degrees(parts[parts.length() - 1])
{ ..@style.Transform::none(), rotate_degrees: angle }
}
///|
/// Split string by comma (for transform arguments) - local version
fn split_transform_args(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 transform_chars_to_string(chars : ArrayView[Char]) -> String {
let sb = StringBuilder::new()
for c in chars {
sb.write_char(c)
}
sb.to_string()
}
///|
/// Extract property name and value tokens from a declaration
fn parse_declaration(
tokens : ArrayView[@token.Token],
) -> (String, ArrayView[@token.Token])? {
let tokens = skip_whitespace(tokens)
if tokens.length() == 0 {
return None
}
// Get property name
let property_name = match tokens[0] {
@token.Token::Ident(name) => name
_ => return None
}
// Skip to colon
let mut pos = 1
while pos < tokens.length() {
match tokens[pos] {
@token.Token::Whitespace => pos += 1
@token.Token::Colon => {
pos += 1
break
}
_ => return None
}
}
// Get value tokens (until semicolon or end)
let value_start = pos
while pos < tokens.length() {
match tokens[pos] {
@token.Token::Semicolon | @token.Token::EOF => break
_ => pos += 1
}
}
let value_tokens = skip_whitespace(tokens[value_start:pos])
Some((property_name, value_tokens))
}
///|
/// Apply a single property to a style
fn apply_property(
style : @style.Style,
property : String,
value_tokens : ArrayView[@token.Token],
) -> @style.Style {
// display
if property == "display" {
match parse_display(value_tokens) {
Some(v) => return { ..style, display: v }
None => ()
}
}
// position
if property == "position" {
match parse_position(value_tokens) {
Some(v) => return { ..style, position: v }
None => ()
}
}
// float
if property == "float" {
match parse_float(value_tokens, style.direction) {
Some(v) => return { ..style, float: v }
None => ()
}
}
// clear
if property == "clear" {
match parse_clear(value_tokens, style.direction) {
Some(v) => return { ..style, clear: v }
None => ()
}
}
// box-sizing
if property == "box-sizing" {
match parse_box_sizing(value_tokens) {
Some(v) => return { ..style, box_sizing: v }
None => ()
}
}
// width
if property == "width" {
match parse_dimension(value_tokens) {
Some(v) => return { ..style, width: v }
None => ()
}
}
// height
if property == "height" {
match parse_dimension(value_tokens) {
Some(v) => return { ..style, height: v }
None => ()
}
}
// min-width
if property == "min-width" {
match parse_dimension(value_tokens) {
Some(v) => return { ..style, min_width: v }
None => ()
}
}
// min-height
if property == "min-height" {
match parse_dimension(value_tokens) {
Some(v) => return { ..style, min_height: v }
None => ()
}
}
// max-width
if property == "max-width" {
match parse_dimension(value_tokens) {
Some(v) => return { ..style, max_width: v }
None => ()
}
}
// max-height
if property == "max-height" {
match parse_dimension(value_tokens) {
Some(v) => return { ..style, max_height: v }
None => ()
}
}
// flex-direction
if property == "flex-direction" {
match parse_flex_direction(value_tokens) {
Some(v) => return { ..style, flex_direction: v }
None => ()
}
}
// flex-wrap
if property == "flex-wrap" {
match parse_flex_wrap(value_tokens) {
Some(v) => return { ..style, flex_wrap: v }
None => ()
}
}
// justify-content
if property == "justify-content" {
match parse_alignment_with_overflow(value_tokens) {
Some((v, is_unsafe)) =>
return {
..style,
justify_content: v,
justify_content_unsafe: is_unsafe,
}
None => ()
}
}
// align-items
if property == "align-items" {
match parse_alignment(value_tokens) {
Some(v) => return { ..style, align_items: v }
None => ()
}
}
// align-content
if property == "align-content" {
match parse_alignment_with_overflow(value_tokens) {
Some((v, is_unsafe)) =>
return { ..style, align_content: v, align_content_unsafe: is_unsafe }
None => ()
}
}
// place-content
if property == "place-content" {
match parse_place_content_with_overflow(value_tokens) {
Some(
(
(align_content, align_content_unsafe),
(justify_content, justify_content_unsafe),
)
) =>
return {
..style,
align_content,
align_content_unsafe,
justify_content,
justify_content_unsafe,
}
None => ()
}
}
// align-self
if property == "align-self" {
match parse_self_alignment(value_tokens) {
Some(v) => return { ..style, align_self: v }
None => ()
}
}
// justify-self
if property == "justify-self" {
match parse_self_alignment(value_tokens) {
Some(v) => return { ..style, justify_self: v }
None => ()
}
}
// justify-items
if property == "justify-items" {
match parse_alignment(value_tokens) {
Some(v) => return { ..style, justify_items: v }
None => ()
}
}
// flex-grow (must be non-negative)
if property == "flex-grow" {
match parse_number(value_tokens) {
Some(v) if v >= 0.0 => return { ..style, flex_grow: v }
_ => ()
}
}
// flex-shrink (must be non-negative)
if property == "flex-shrink" {
match parse_number(value_tokens) {
Some(v) if v >= 0.0 => return { ..style, flex_shrink: v }
_ => ()
}
}
// flex-basis
if property == "flex-basis" {
match parse_dimension(value_tokens) {
Some(v) => return { ..style, flex_basis: v }
None => ()
}
}
// flex shorthand
if property == "flex" {
match parse_flex_shorthand(value_tokens) {
Some((grow, shrink, basis)) =>
return {
..style,
flex_grow: grow,
flex_shrink: shrink,
flex_basis: basis,
}
None => ()
}
}
// flex-flow shorthand
if property == "flex-flow" {
match parse_flex_flow_shorthand(value_tokens) {
Some((direction, wrap)) =>
return { ..style, flex_direction: direction, flex_wrap: wrap }
None => ()
}
}
// order
if property == "order" {
match parse_integer(value_tokens) {
Some(v) => return { ..style, order: v }
None => ()
}
}
// margin (supports 1, 2, 3, or 4 values)
if property == "margin" {
let dims = parse_multi_dimensions(value_tokens, 4)
match dims.length() {
1 => {
// All sides
let v = dims[0]
return { ..style, margin: { top: v, right: v, bottom: v, left: v } }
}
2 => {
// top/bottom, left/right
let tb = dims[0]
let lr = dims[1]
return { ..style, margin: { top: tb, right: lr, bottom: tb, left: lr } }
}
3 => {
// top, left/right, bottom
let t = dims[0]
let lr = dims[1]
let b = dims[2]
return { ..style, margin: { top: t, right: lr, bottom: b, left: lr } }
}
4 =>
// top, right, bottom, left
return {
..style,
margin: {
top: dims[0],
right: dims[1],
bottom: dims[2],
left: dims[3],
},
}
_ => ()
}
}
// margin-left
if property == "margin-left" {
match parse_dimension(value_tokens) {
Some(v) => return { ..style, margin: { ..style.margin, left: v } }
None => ()
}
}
// margin-right
if property == "margin-right" {
match parse_dimension(value_tokens) {
Some(v) => return { ..style, margin: { ..style.margin, right: v } }
None => ()
}
}
// margin-top
if property == "margin-top" {
match parse_dimension(value_tokens) {
Some(v) => return { ..style, margin: { ..style.margin, top: v } }
None => ()
}
}
// margin-bottom
if property == "margin-bottom" {
match parse_dimension(value_tokens) {
Some(v) => return { ..style, margin: { ..style.margin, bottom: v } }
None => ()
}
}
// padding (supports 1, 2, 3, or 4 values)
if property == "padding" {
let dims = parse_multi_dimensions(value_tokens, 4)
match dims.length() {
1 => {
let v = dims[0]
return { ..style, padding: { top: v, right: v, bottom: v, left: v } }
}
2 => {
let tb = dims[0]
let lr = dims[1]
return {
..style,
padding: { top: tb, right: lr, bottom: tb, left: lr },
}
}
3 => {
let t = dims[0]
let lr = dims[1]
let b = dims[2]
return { ..style, padding: { top: t, right: lr, bottom: b, left: lr } }
}
4 =>
return {
..style,
padding: {
top: dims[0],
right: dims[1],
bottom: dims[2],
left: dims[3],
},
}
_ => ()
}
}
// padding-left
if property == "padding-left" {
match parse_dimension(value_tokens) {
Some(v) => return { ..style, padding: { ..style.padding, left: v } }
None => ()
}
}
// padding-right
if property == "padding-right" {
match parse_dimension(value_tokens) {
Some(v) => return { ..style, padding: { ..style.padding, right: v } }
None => ()
}
}
// padding-top
if property == "padding-top" {
match parse_dimension(value_tokens) {
Some(v) => return { ..style, padding: { ..style.padding, top: v } }
None => ()
}
}
// padding-bottom
if property == "padding-bottom" {
match parse_dimension(value_tokens) {
Some(v) => return { ..style, padding: { ..style.padding, bottom: v } }
None => ()
}
}
// padding-block (logical property: sets top and bottom for horizontal writing mode)
if property == "padding-block" {
match parse_dimension(value_tokens) {
Some(v) =>
return { ..style, padding: { ..style.padding, top: v, bottom: v } }
None => ()
}
}
// padding-inline (logical property: sets left and right for horizontal writing mode)
if property == "padding-inline" {
match parse_dimension(value_tokens) {
Some(v) =>
return { ..style, padding: { ..style.padding, left: v, right: v } }
None => ()
}
}
// gap
if property == "gap" {
match parse_dimension(value_tokens) {
Some(v) => return { ..style, row_gap: v, column_gap: v }
None => ()
}
}
// row-gap
if property == "row-gap" {
match parse_dimension(value_tokens) {
Some(v) => return { ..style, row_gap: v }
None => ()
}
}
// column-gap
if property == "column-gap" {
match parse_dimension(value_tokens) {
Some(v) => return { ..style, column_gap: v }
None => ()
}
}
// overflow
if property == "overflow" {
match parse_overflow(value_tokens) {
Some(v) => return { ..style, overflow_x: v, overflow_y: v }
None => ()
}
}
// overflow-x
if property == "overflow-x" {
match parse_overflow(value_tokens) {
Some(v) => return { ..style, overflow_x: v }
None => ()
}
}
// overflow-y
if property == "overflow-y" {
match parse_overflow(value_tokens) {
Some(v) => return { ..style, overflow_y: v }
None => ()
}
}
// clip (legacy, for accessibility)
if property == "clip" {
return { ..style, clip: parse_clip(value_tokens) }
}
// clip-path (paint/hit-test subset)
if property == "clip-path" {
return { ..style, clip_path: parse_clip_path(value_tokens) }
}
// border (single value)
if property == "border-width" {
match parse_dimension(value_tokens) {
Some(v) =>
return { ..style, border: { left: v, right: v, top: v, bottom: v } }
None => ()
}
}
// border-left-width
if property == "border-left-width" {
match parse_dimension(value_tokens) {
Some(v) => return { ..style, border: { ..style.border, left: v } }
None => ()
}
}
// border-right-width
if property == "border-right-width" {
match parse_dimension(value_tokens) {
Some(v) => return { ..style, border: { ..style.border, right: v } }
None => ()
}
}
// border-top-width
if property == "border-top-width" {
match parse_dimension(value_tokens) {
Some(v) => return { ..style, border: { ..style.border, top: v } }
None => ()
}
}
// border-bottom-width
if property == "border-bottom-width" {
match parse_dimension(value_tokens) {
Some(v) => return { ..style, border: { ..style.border, bottom: v } }
None => ()
}
}
// border-block (logical property: sets top and bottom border width)
// Shorthand can be: border-block: 10px | border-block: 10px solid | border-block: 10px solid green
if property == "border-block" {
// Parse first token as dimension (width)
match parse_dimension(value_tokens) {
Some(v) =>
return { ..style, border: { ..style.border, top: v, bottom: v } }
None => ()
}
}
// border-block-width
if property == "border-block-width" {
match parse_dimension(value_tokens) {
Some(v) =>
return { ..style, border: { ..style.border, top: v, bottom: v } }
None => ()
}
}
// border-inline (logical property: sets left and right border width)
if property == "border-inline" {
match parse_dimension(value_tokens) {
Some(v) =>
return { ..style, border: { ..style.border, left: v, right: v } }
None => ()
}
}
// border-inline-width
if property == "border-inline-width" {
match parse_dimension(value_tokens) {
Some(v) =>
return { ..style, border: { ..style.border, left: v, right: v } }
None => ()
}
}
// inset (supports 1, 2, 3, or 4 values like margin/padding)
if property == "inset" {
let dims = parse_multi_dimensions(value_tokens, 4)
match dims.length() {
1 => {
let v = dims[0]
return { ..style, inset: { top: v, right: v, bottom: v, left: v } }
}
2 => {
let tb = dims[0]
let lr = dims[1]
return { ..style, inset: { top: tb, right: lr, bottom: tb, left: lr } }
}
3 => {
let t = dims[0]
let lr = dims[1]
let b = dims[2]
return { ..style, inset: { top: t, right: lr, bottom: b, left: lr } }
}
4 =>
return {
..style,
inset: {
top: dims[0],
right: dims[1],
bottom: dims[2],
left: dims[3],
},
}
_ => ()
}
}
// top
if property == "top" {
match parse_dimension(value_tokens) {
Some(v) => return { ..style, inset: { ..style.inset, top: v } }
None => ()
}
}
// right
if property == "right" {
match parse_dimension(value_tokens) {
Some(v) => return { ..style, inset: { ..style.inset, right: v } }
None => ()
}
}
// bottom
if property == "bottom" {
match parse_dimension(value_tokens) {
Some(v) => return { ..style, inset: { ..style.inset, bottom: v } }
None => ()
}
}
// left
if property == "left" {
match parse_dimension(value_tokens) {
Some(v) => return { ..style, inset: { ..style.inset, left: v } }
None => ()
}
}
// aspect-ratio
if property == "aspect-ratio" {
match parse_aspect_ratio(value_tokens) {
Some(v) => return { ..style, aspect_ratio: Some(v) }
None => ()
}
}
// grid-auto-flow
if property == "grid-auto-flow" {
match parse_grid_auto_flow(value_tokens) {
Some(v) => return { ..style, grid_auto_flow: v }
None => ()
}
}
// grid-template-columns
if property == "grid-template-columns" {
let (kind, tracks, line_names) = parse_grid_template_axis(value_tokens)
return {
..style,
grid_template_columns: tracks,
grid_template_column_line_names: line_names,
grid_template_columns_kind: kind,
}
}
// grid-template-rows
if property == "grid-template-rows" {
let (kind, tracks, line_names) = parse_grid_template_axis(value_tokens)
return {
..style,
grid_template_rows: tracks,
grid_template_row_line_names: line_names,
grid_template_rows_kind: kind,
}
}
// grid-template / grid shorthand. Try the "areas" form first (it carries
// rows). The `grid` shorthand additionally supports the auto-flow
// form. Finally fall back to the ` / ` form.
if property == "grid-template" || property == "grid" {
match parse_grid_template_areas_form(value_tokens) {
Some((areas, rows, row_names, cols, col_names)) =>
return {
..style,
grid_template_areas: areas,
grid_template_rows: rows,
grid_template_row_line_names: row_names,
grid_template_columns: cols,
grid_template_column_line_names: col_names,
}
None => ()
}
if property == "grid" {
match parse_grid_autoflow_form(value_tokens) {
Some((flow, rows, row_names, cols, col_names, auto_rows, auto_cols)) =>
return {
..style,
grid_auto_flow: flow,
grid_template_rows: rows,
grid_template_row_line_names: row_names,
grid_template_columns: cols,
grid_template_column_line_names: col_names,
grid_auto_rows: auto_rows,
grid_auto_columns: auto_cols,
}
None => ()
}
}
match parse_grid_template_shorthand(value_tokens) {
Some((rows, row_names, cols, col_names)) =>
return {
..style,
grid_template_rows: rows,
grid_template_row_line_names: row_names,
grid_template_columns: cols,
grid_template_column_line_names: col_names,
}
None => ()
}
}
// Grid item placement longhands
if property == "grid-column-start" {
return {
..style,
grid_column: { ..style.grid_column, start: classify_grid_line(value_tokens) },
}
}
if property == "grid-column-end" {
return {
..style,
grid_column: { ..style.grid_column, end: classify_grid_line(value_tokens) },
}
}
if property == "grid-row-start" {
return {
..style,
grid_row: { ..style.grid_row, start: classify_grid_line(value_tokens) },
}
}
if property == "grid-row-end" {
return {
..style,
grid_row: { ..style.grid_row, end: classify_grid_line(value_tokens) },
}
}
// grid-column / grid-row shorthand
if property == "grid-column" {
let (start, end) = parse_grid_line_shorthand_tokens(value_tokens)
return { ..style, grid_column: { start, end } }
}
if property == "grid-row" {
let (start, end) = parse_grid_line_shorthand_tokens(value_tokens)
return { ..style, grid_row: { start, end } }
}
// grid-area: either a single named area, or up to four grid lines.
if property == "grid-area" {
match grid_area_single_name(value_tokens) {
// A single names a grid area rather than placing lines.
Some(name) => return { ..style, grid_area: Some(name) }
None => {
let (rs, cs, re, ce) = parse_grid_area_shorthand_tokens(value_tokens)
return {
..style,
grid_row: { start: rs, end: re },
grid_column: { start: cs, end: ce },
}
}
}
}
// Individual transform properties (compose into individual_transform).
if property == "translate" {
let t = parse_translate_property_from_string(reconstruct_css(value_tokens))
return {
..style,
individual_transform: {
..style.individual_transform,
translate_x: t.translate_x,
translate_y: t.translate_y,
},
}
}
if property == "scale" {
let t = parse_scale_property_from_string(reconstruct_css(value_tokens))
return {
..style,
individual_transform: {
..style.individual_transform,
scale_x: t.scale_x,
scale_y: t.scale_y,
},
}
}
if property == "rotate" {
let t = parse_rotate_property_from_string(reconstruct_css(value_tokens))
return {
..style,
individual_transform: {
..style.individual_transform,
rotate_degrees: t.rotate_degrees,
},
}
}
// Animation longhands
if property == "animation-name" {
return { ..style, animation_name: parse_ident_list(value_tokens) }
}
if property == "animation-duration" {
return { ..style, animation_duration: parse_time_list(value_tokens) }
}
if property == "animation-timing-function" {
return {
..style,
animation_timing_function: parse_easing_list(value_tokens),
}
}
if property == "animation-delay" {
return { ..style, animation_delay: parse_time_list(value_tokens) }
}
if property == "animation-iteration-count" {
return {
..style,
animation_iteration_count: parse_iteration_count_list(value_tokens),
}
}
if property == "animation-direction" {
return { ..style, animation_direction: parse_direction_list(value_tokens) }
}
if property == "animation-fill-mode" {
return { ..style, animation_fill_mode: parse_fill_mode_list(value_tokens) }
}
if property == "animation-play-state" {
return {
..style,
animation_play_state: parse_play_state_list(value_tokens),
}
}
// animation shorthand
if property == "animation" {
let a = parse_animation_shorthand(value_tokens)
return {
..style,
animation_name: a.names,
animation_duration: a.durations,
animation_timing_function: a.timing_functions,
animation_delay: a.delays,
animation_iteration_count: a.iteration_counts,
animation_direction: a.directions,
animation_fill_mode: a.fill_modes,
animation_play_state: a.play_states,
}
}
// Transition longhands
if property == "transition-property" {
return { ..style, transition_property: parse_ident_list(value_tokens) }
}
if property == "transition-duration" {
return { ..style, transition_duration: parse_time_list(value_tokens) }
}
if property == "transition-timing-function" {
return {
..style,
transition_timing_function: parse_easing_list(value_tokens),
}
}
if property == "transition-delay" {
return { ..style, transition_delay: parse_time_list(value_tokens) }
}
// transition shorthand
if property == "transition" {
let t = parse_transition_shorthand(value_tokens)
return {
..style,
transition_property: t.properties,
transition_duration: t.durations,
transition_timing_function: t.timing_functions,
transition_delay: t.delays,
}
}
// contain
if property == "contain" {
let contain = parse_contain(value_tokens)
return { ..style, contain, }
}
// container-type
if property == "container-type" {
match parse_container_type(value_tokens) {
Some(implied) =>
if implied.has_containment() {
return { ..style, contain: merge_contain(style.contain, implied) }
} else {
return style
}
None => ()
}
}
// contain-intrinsic-inline-size
if property == "contain-intrinsic-inline-size" {
let contain_intrinsic_inline_size = parse_contain_intrinsic_axis(
value_tokens,
)
return { ..style, contain_intrinsic_inline_size, }
}
// contain-intrinsic-block-size
if property == "contain-intrinsic-block-size" {
let contain_intrinsic_block_size = parse_contain_intrinsic_axis(
value_tokens,
)
return { ..style, contain_intrinsic_block_size, }
}
// contain-intrinsic-size
if property == "contain-intrinsic-size" {
match parse_contain_intrinsic_size(value_tokens) {
Some((contain_intrinsic_inline_size, contain_intrinsic_block_size)) =>
return {
..style,
contain_intrinsic_inline_size,
contain_intrinsic_block_size,
}
None => ()
}
}
// transform
if property == "transform" {
let transform = parse_transform(value_tokens)
return { ..style, transform, }
}
// pointer-events
if property == "pointer-events" {
let pointer_events = parse_pointer_events(value_tokens)
return { ..style, pointer_events, }
}
// Unknown property - ignore
style
}
///|
fn seed_direction(tokens : Array[@token.Token]) -> @style.Direction {
let mut direction = @style.Direction::Ltr
let mut pos = 0
while pos < tokens.length() {
match tokens[pos] {
@token.Token::Whitespace | @token.Token::Semicolon => {
pos += 1
continue
}
@token.Token::EOF => break
_ => ()
}
match parse_declaration(tokens[pos:]) {
Some((property, value_tokens)) => {
if property == "direction" {
match value_tokens.length() {
0 => ()
_ =>
match value_tokens[0] {
@token.Token::Ident(s) =>
if s == "rtl" {
direction = @style.Direction::Rtl
} else if s == "ltr" {
direction = @style.Direction::Ltr
}
_ => ()
}
}
}
while pos < tokens.length() {
match tokens[pos] {
@token.Token::Semicolon => {
pos += 1
break
}
@token.Token::EOF => break
_ => pos += 1
}
}
}
None => pos += 1
}
}
direction
}
///|
/// Parse inline CSS style string and return a Style object
/// e.g., "width: 100px; height: 50px; display: flex"
pub fn parse_inline_style(css : String) -> @style.Style {
let tokens = @token.tokenize(css)
let mut style = {
..@style.Style::default(),
direction: seed_direction(tokens),
}
let mut pos = 0
while pos < tokens.length() {
// Skip leading whitespace and semicolons
match tokens[pos] {
@token.Token::Whitespace | @token.Token::Semicolon => {
pos += 1
continue
}
@token.Token::EOF => break
_ => ()
}
// Try to parse a declaration
match parse_declaration(tokens[pos:]) {
Some((property, value_tokens)) => {
style = apply_property(style, property, value_tokens)
// Skip to next semicolon or end
while pos < tokens.length() {
match tokens[pos] {
@token.Token::Semicolon => {
pos += 1
break
}
@token.Token::EOF => break
_ => pos += 1
}
}
}
None => pos += 1
}
}
// Convert content-box dimensions to border-box dimensions
// The layout engine always works with outer (border-box) dimensions
// For content-box (CSS default), width/height specify content area only,
// so we need to add padding and border to get the outer dimensions
adjust_for_box_sizing(style)
}
///|
/// 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
}
}