///|
/// CSS Property Metadata
/// Defines initial values and inheritance behavior for layout properties
///|
/// Whether a property is inherited by default
pub fn is_inherited(property : String) -> Bool {
match property {
// Text-related properties (inherited)
"direction"
| "writing-mode"
| "text-align"
| "line-height"
| "white-space"
| "font-size"
| "font-family"
| "font-weight"
| "font-style"
| "color"
| "visibility" => true
// Table properties that are inherited
"caption-side" | "border-collapse" | "border-spacing" => true
// Containment is not inherited
"contain" => false
"pointer-events" => true
// All layout properties are not inherited
_ => false
}
}
///|
/// Get the initial value for a property as a string
pub fn initial_value(property : String) -> String {
match property {
// Display
"display" => "block"
"position" => "relative"
"float" => "none"
"clear" => "none"
// Overflow
"overflow" | "overflow-x" | "overflow-y" => "visible"
"pointer-events" => "auto"
"scroll-snap-type" => "none"
"scroll-snap-align" => "none"
// Sizing
"width" | "height" | "min-width" | "min-height" => "auto"
"max-width" | "max-height" => "none"
// Box model
"margin"
| "margin-top"
| "margin-right"
| "margin-bottom"
| "margin-left" => "0"
"margin-trim" => "none"
"padding"
| "padding-top"
| "padding-right"
| "padding-bottom"
| "padding-left" => "0"
"border-width"
| "border-top-width"
| "border-right-width"
| "border-bottom-width"
| "border-left-width" => "0"
// Flexbox container
"flex-direction" => "row"
"flex-wrap" => "nowrap"
"justify-content" => "flex-start"
"align-items" => "stretch"
"align-content" => "stretch"
// Flexbox item
"align-self" => "auto"
"flex-grow" => "0"
"flex-shrink" => "1"
"flex-basis" => "auto"
// Gap (grid-gap is legacy alias)
"gap"
| "row-gap"
| "column-gap"
| "grid-gap"
| "grid-row-gap"
| "grid-column-gap" => "0"
"columns" | "column-count" | "column-width" => "auto"
"column-fill" => "balance"
"break-before" | "break-after" | "break-inside" | "page-break-inside" =>
"auto"
"column-span" => "none"
// Aspect ratio
"aspect-ratio" => "auto"
// Inset
"top" | "right" | "bottom" | "left" | "inset" => "auto"
// Grid container
"grid-template-rows" | "grid-template-columns" => "none"
"grid-auto-rows" | "grid-auto-columns" => "auto"
"grid-auto-flow" => "row"
// Grid item
"grid-row-start"
| "grid-row-end"
| "grid-column-start"
| "grid-column-end" => "auto"
// Text (inherited)
"direction" => "ltr"
"writing-mode" => "horizontal-tb"
"text-align" => "start"
"font-weight" => "400"
"white-space" => "normal"
"text-overflow" => "clip"
"text-decoration" | "text-decoration-line" => "none"
"box-shadow" => "none"
// Containment
"contain" => "none"
"contain-intrinsic-size"
| "contain-intrinsic-inline-size"
| "contain-intrinsic-block-size" => "none"
// Table properties
"caption-side" => "top"
"border-collapse" => "separate"
"border-spacing" => "0"
"table-layout" => "auto"
// Default
_ => "auto"
}
}
///|
/// Parse a dimension value from string
pub fn parse_dimension(value : String) -> @types.Dimension {
let v = value.trim()
if v == "auto" || v == "none" {
return @types.Dimension::Auto
}
// Intrinsic sizing keywords
if v == "min-content" {
return @types.Dimension::MinContent
}
if v == "max-content" {
return @types.Dimension::MaxContent
}
// fit-content()
if v.has_prefix("fit-content(") && v.has_suffix(")") {
// Extract content between "fit-content(" and ")"
let v_str = v.to_string()
let inner = v_str.unsafe_substring(start=12, end=v_str.length() - 1)
// Parse the inner value as a length
match inner.strip_suffix("px") {
Some(num_str) => {
let n = @string.parse_double(num_str.to_string()) catch {
_ => return @types.Dimension::Auto
}
return @types.Dimension::FitContent(n)
}
None => ()
}
// Try percentage
match inner.strip_suffix("%") {
Some(num_str) => {
let n = @string.parse_double(num_str.to_string()) catch {
_ => return @types.Dimension::Auto
}
// For fit-content, percentage is relative to available space
// Store as negative to differentiate from length
return @types.Dimension::FitContent(n)
}
None => ()
}
return @types.Dimension::Auto
}
// Try to parse as length (px)
match v.strip_suffix("px") {
Some(num_str) => {
let n = @string.parse_double(num_str.to_string()) catch {
_ => return @types.Dimension::Auto
}
return @types.Dimension::Length(n)
}
None => ()
}
// Try to parse as percentage
match v.strip_suffix("%") {
Some(num_str) => {
let n = @string.parse_double(num_str.to_string()) catch {
_ => return @types.Dimension::Auto
}
return @types.Dimension::Percent(n / 100.0)
}
None => ()
}
// Try to parse as plain number (treated as px)
let n = @string.parse_double(v.to_string()) catch {
_ => return @types.Dimension::Auto
}
@types.Dimension::Length(n)
}
///|
/// Extract border width from shorthand value
/// Handles formats like "3px", "solid 3px", "1px solid red", "none", etc.
pub fn extract_border_width(value : String) -> @types.Dimension {
let v = value.trim()
// Handle "none" case
if v == "none" || v == "0" {
return @types.Dimension::Length(0.0)
}
// Split by whitespace and look for a dimension value
let parts = split_whitespace(v.to_string())
let mut has_visible_style = false
for part in parts {
let lower = part.to_lower()
match lower {
"thin" => return @types.Dimension::Length(1.0)
"medium" => return @types.Dimension::Length(3.0)
"thick" => return @types.Dimension::Length(5.0)
"solid"
| "dotted"
| "dashed"
| "double"
| "groove"
| "ridge"
| "inset"
| "outset" => {
has_visible_style = true
continue
}
"none" | "hidden" => continue
_ => ()
}
// Check if this part looks like a dimension (ends with px or is a number)
if part.has_suffix("px") {
match part.strip_suffix("px") {
Some(num_str) => {
let n = @string.parse_double(num_str.to_string()) catch {
_ => continue
}
return @types.Dimension::Length(n)
}
None => continue
}
}
// Try plain number
let first_char = part[0].to_int()
if first_char >= '0'.to_int() && first_char <= '9'.to_int() {
let n = @string.parse_double(part) catch { _ => continue }
return @types.Dimension::Length(n)
}
}
// Compatible default for style-only shorthand like "border: solid".
if has_visible_style {
@types.Dimension::Length(1.5)
} else {
// Default: no border
@types.Dimension::Length(0.0)
}
}
///|
/// Split string by whitespace into parts
fn split_whitespace(s : String) -> Array[String] {
let result : Array[String] = []
let current = StringBuilder::new()
for c in s {
if c == ' ' || c == '\t' || c == '\n' {
if current.to_string().length() > 0 {
result.push(current.to_string())
current.reset()
}
} else {
current.write_char(c)
}
}
if current.to_string().length() > 0 {
result.push(current.to_string())
}
result
}
///|
/// Parse display value
pub fn parse_display(value : String) -> @types.Display {
let normalized = value.trim().to_lower().to_string()
let parts = split_whitespace(normalized)
if parts.length() == 0 {
return @types.Display::Block
}
if parts.length() == 2 {
let first = parts[0]
let second = parts[1]
if (first == "math" && second == "inline") ||
(first == "inline" && second == "math") {
return @types.Display::Inline
}
if (first == "math" && second == "block") ||
(first == "block" && second == "math") {
return @types.Display::Block
}
if (first == "flow-root" && second == "list-item") ||
(first == "list-item" && second == "flow-root") {
return @types.Display::FlowRoot
}
}
if parts.length() > 1 {
return @types.Display::Block
}
match parts[0] {
"block" => @types.Display::Block
"inline" => @types.Display::Inline
"inline-block" => @types.Display::InlineBlock
"flex" => @types.Display::Flex
"inline-flex" => @types.Display::InlineFlex
"grid" => @types.Display::Grid
"inline-grid" => @types.Display::InlineGrid
"none" => @types.Display::None
"contents" => @types.Display::Contents
"flow-root" => @types.Display::FlowRoot
"math" => @types.Display::Inline
// Table display types
"table" => @types.Display::Table
"inline-table" => @types.Display::InlineTable
"table-row" => @types.Display::TableRow
"table-cell" => @types.Display::TableCell
"table-caption" => @types.Display::TableCaption
"table-row-group" => @types.Display::TableRowGroup
"table-header-group" => @types.Display::TableHeaderGroup
"table-footer-group" => @types.Display::TableFooterGroup
"table-column" => @types.Display::TableColumn
"table-column-group" => @types.Display::TableColumnGroup
// Ruby internal display types are inline-level for layout-tree comparison.
// (Full ruby formatting context is not yet implemented.)
"ruby" => @types.Display::Inline
"ruby-base" => @types.Display::Inline
"ruby-base-container" => @types.Display::Inline
"ruby-text" => @types.Display::Inline
"ruby-text-container" => @types.Display::Inline
_ => @types.Display::Block
}
}
///|
/// Parse position value
pub fn parse_position(value : String) -> @types.Position {
match value.trim().to_lower() {
"static" => @types.Position::Static
"absolute" => @types.Position::Absolute
"fixed" => @types.Position::Fixed
"relative" | "sticky" => @types.Position::Relative
_ => @types.Position::Static // Default is static
}
}
///|
/// Parse float value
pub fn parse_float(value : String) -> @types.Float {
match value.trim().to_lower() {
"left" => @types.Float::Left
"right" => @types.Float::Right
"none" => @types.Float::None
_ => @types.Float::None
}
}
///|
pub fn parse_float_with_direction(
value : String,
direction : @style.Direction,
) -> @types.Float {
match value.trim().to_lower() {
"inline-start" =>
if direction.is_rtl() {
@types.Float::Right
} else {
@types.Float::Left
}
"inline-end" =>
if direction.is_rtl() {
@types.Float::Left
} else {
@types.Float::Right
}
_ => parse_float(value)
}
}
///|
/// Parse clear value
pub fn parse_clear(value : String) -> @types.Clear {
match value.trim().to_lower() {
"left" => @types.Clear::Left
"right" => @types.Clear::Right
"both" => @types.Clear::Both
"none" => @types.Clear::None
_ => @types.Clear::None
}
}
///|
pub fn parse_clear_with_direction(
value : String,
direction : @style.Direction,
) -> @types.Clear {
match value.trim().to_lower() {
"inline-start" =>
if direction.is_rtl() {
@types.Clear::Right
} else {
@types.Clear::Left
}
"inline-end" =>
if direction.is_rtl() {
@types.Clear::Left
} else {
@types.Clear::Right
}
_ => parse_clear(value)
}
}
///|
/// Parse flex-direction value
pub fn parse_flex_direction(value : String) -> @types.FlexDirection {
match value.trim().to_lower() {
"row" => @types.FlexDirection::Row
"row-reverse" => @types.FlexDirection::RowReverse
"column" => @types.FlexDirection::Column
"column-reverse" => @types.FlexDirection::ColumnReverse
_ => @types.FlexDirection::Row
}
}
///|
/// Parse flex-wrap value
pub fn parse_flex_wrap(value : String) -> @types.FlexWrap {
match value.trim().to_lower() {
"nowrap" => @types.FlexWrap::NoWrap
"wrap" => @types.FlexWrap::Wrap
"wrap-reverse" => @types.FlexWrap::WrapReverse
_ => @types.FlexWrap::NoWrap
}
}
///|
fn tokenize_alignment_value(value : String) -> Array[String] {
value
.trim()
.to_lower()
.split(" ")
.map(fn(part) { part.to_string().trim().to_string() })
.filter(fn(part) { !part.is_empty() })
.collect()
}
///|
fn parse_alignment_keyword(value : String) -> @types.Alignment {
match value {
"flex-start" => @types.Alignment::FlexStart
"flex-end" => @types.Alignment::FlexEnd
"start" => @types.Alignment::Start
"end" => @types.Alignment::End
"left" => @types.Alignment::Left
"right" => @types.Alignment::Right
"center" => @types.Alignment::Center
"space-between" => @types.Alignment::SpaceBetween
"space-around" => @types.Alignment::SpaceAround
"space-evenly" => @types.Alignment::SpaceEvenly
"stretch" => @types.Alignment::Stretch
"baseline" | "first baseline" => @types.Alignment::Baseline
// Fallback alignment for last-baseline in axis alignment.
"last baseline" => @types.Alignment::End
_ => @types.Alignment::FlexStart
}
}
///|
fn parse_alignment_segment(
tokens : Array[String],
start : Int,
) -> (String, Int)? {
if start >= tokens.length() {
return None
}
let head = tokens[start]
if start + 1 < tokens.length() {
let next = tokens[start + 1]
if head == "safe" ||
head == "unsafe" ||
((head == "first" || head == "last") && next == "baseline") {
return Some((head + " " + next, start + 2))
}
}
Some((head, start + 1))
}
///|
/// Parse alignment value (justify-content, align-items, align-content)
/// Note: flex-start/flex-end are flex-relative (affected by wrap-reverse)
/// start/end are physical/logical (not affected by wrap-reverse)
pub fn parse_alignment_with_overflow(
value : String,
) -> (@types.Alignment, Bool) {
let tokens = tokenize_alignment_value(value)
if tokens.is_empty() {
return (@types.Alignment::FlexStart, false)
}
let mut offset = 0
let mut is_safe = false
let mut is_unsafe = false
if tokens[0] == "safe" {
is_safe = true
offset = 1
} else if tokens[0] == "unsafe" {
is_unsafe = true
offset = 1
}
if offset >= tokens.length() {
return (@types.Alignment::FlexStart, false)
}
let keyword = if offset + 1 < tokens.length() &&
tokens[offset + 1] == "baseline" &&
(tokens[offset] == "first" || tokens[offset] == "last") {
tokens[offset] + " baseline"
} else {
tokens[offset]
}
let parsed = parse_alignment_keyword(keyword)
let parsed = if is_safe {
match parsed {
@types.Alignment::Center
| @types.Alignment::End
| @types.Alignment::FlexEnd => @types.Alignment::Start
_ => parsed
}
} else {
parsed
}
(parsed, is_unsafe)
}
///|
pub fn parse_alignment(value : String) -> @types.Alignment {
let (alignment, _) = parse_alignment_with_overflow(value)
alignment
}
///|
/// Parse place-content shorthand.
/// Returns (align_content, justify_content).
pub fn parse_place_content_with_overflow(
value : String,
) -> ((@types.Alignment, Bool), (@types.Alignment, Bool))? {
let tokens = tokenize_alignment_value(value)
if tokens.is_empty() {
return None
}
match parse_alignment_segment(tokens, 0) {
Some((first, next_index)) => {
let first_alignment = parse_alignment_with_overflow(first)
if next_index >= tokens.length() {
return Some((first_alignment, first_alignment))
}
match parse_alignment_segment(tokens, next_index) {
Some((second, _)) =>
Some((first_alignment, parse_alignment_with_overflow(second)))
None => Some((first_alignment, first_alignment))
}
}
None => None
}
}
///|
/// Parse place-content shorthand.
/// Returns (align_content, justify_content).
pub fn parse_place_content(
value : String,
) -> (@types.Alignment, @types.Alignment)? {
match parse_place_content_with_overflow(value) {
Some(((align_content, _), (justify_content, _))) =>
Some((align_content, justify_content))
None => None
}
}
///|
/// Parse align-self value
pub fn parse_align_self(value : String) -> @types.AlignSelf {
match value.trim().to_lower() {
"auto" => @types.AlignSelf::Auto
"flex-start" | "start" => @types.AlignSelf::Start
"flex-end" | "end" => @types.AlignSelf::End
"center" => @types.AlignSelf::Center
"stretch" => @types.AlignSelf::Stretch
"baseline" | "first baseline" => @types.AlignSelf::Baseline
// Fallback alignment for last-baseline in self alignment.
"last baseline" => @types.AlignSelf::End
_ => @types.AlignSelf::Auto
}
}
///|
/// Parse overflow value
pub fn parse_overflow(value : String) -> @types.Overflow {
match value.trim().to_lower() {
"visible" => @types.Overflow::Visible
"hidden" => @types.Overflow::Hidden
"clip" => @types.Overflow::Clip
"scroll" => @types.Overflow::Scroll
"auto" => @types.Overflow::Auto
_ => @types.Overflow::Visible
}
}
///|
/// Parse scroll-snap-type value
/// Supports a minimal subset: none | x mandatory | y mandatory | both mandatory.
pub fn parse_scroll_snap_type(value : String) -> @style.ScrollSnapType {
let tokens = value
.trim()
.to_lower()
.split(" ")
.map(fn(part) { part.to_string().trim().to_string() })
.filter(fn(part) { !part.is_empty() })
.collect()
if tokens.is_empty() {
return @style.ScrollSnapType::none()
}
let mut axis = @style.ScrollSnapAxis::None
let mut strictness = @style.ScrollSnapStrictness::None
for token in tokens {
match token {
"none" => return @style.ScrollSnapType::none()
"x" | "inline" => axis = @style.ScrollSnapAxis::X
"y" | "block" => axis = @style.ScrollSnapAxis::Y
"both" => axis = @style.ScrollSnapAxis::Both
"mandatory" => strictness = @style.ScrollSnapStrictness::Mandatory
"proximity" => strictness = @style.ScrollSnapStrictness::Proximity
_ => ()
}
}
if axis == @style.ScrollSnapAxis::None ||
strictness == @style.ScrollSnapStrictness::None {
@style.ScrollSnapType::none()
} else {
{ axis, strictness }
}
}
///|
/// Parse scroll-snap-align value as (inline, block).
/// Supports keywords: none | start | end | center.
pub fn parse_scroll_snap_align(
value : String,
) -> (@style.ScrollSnapAlign, @style.ScrollSnapAlign) {
let tokens = value
.trim()
.to_lower()
.split(" ")
.map(fn(part) { part.to_string().trim().to_string() })
.filter(fn(part) { !part.is_empty() })
.collect()
if tokens.is_empty() {
return (@style.ScrollSnapAlign::None, @style.ScrollSnapAlign::None)
}
if tokens.length() == 1 {
let keyword = parse_scroll_snap_align_keyword(tokens[0])
return (keyword, keyword)
}
let block = parse_scroll_snap_align_keyword(tokens[0])
let inline = parse_scroll_snap_align_keyword(tokens[1])
(inline, block)
}
///|
fn parse_scroll_snap_align_keyword(value : String) -> @style.ScrollSnapAlign {
match value.trim().to_lower() {
"none" => @style.ScrollSnapAlign::None
"start" => @style.ScrollSnapAlign::Start
"end" => @style.ScrollSnapAlign::End
"center" => @style.ScrollSnapAlign::Center
_ => @style.ScrollSnapAlign::None
}
}
///|
/// Parse white-space value
pub fn parse_white_space(value : String) -> @style.WhiteSpace {
match value.trim().to_lower() {
"normal" => @style.WhiteSpace::Normal
"nowrap" => @style.WhiteSpace::Nowrap
"pre" => @style.WhiteSpace::Pre
"pre-wrap" => @style.WhiteSpace::PreWrap
"pre-line" => @style.WhiteSpace::PreLine
_ => @style.WhiteSpace::Normal
}
}
///|
/// Parse writing-mode value
pub fn parse_writing_mode(value : String) -> @style.WritingMode {
match value.trim().to_lower() {
"horizontal-tb" => @style.WritingMode::HorizontalTb
"vertical-rl" => @style.WritingMode::VerticalRl
"vertical-lr" => @style.WritingMode::VerticalLr
// Legacy values
"lr" | "lr-tb" | "rl" | "rl-tb" => @style.WritingMode::HorizontalTb
"tb" | "tb-rl" => @style.WritingMode::VerticalRl
"tb-lr" => @style.WritingMode::VerticalLr
_ => @style.WritingMode::HorizontalTb
}
}
///|
/// Parse direction value (ltr/rtl)
pub fn parse_direction(value : String) -> @style.Direction {
match value.trim().to_lower() {
"rtl" => @style.Direction::Rtl
"ltr" | _ => @style.Direction::Ltr
}
}
///|
/// Parse text-align value
pub fn parse_text_align(value : String) -> @style.TextAlign {
match value.trim().to_lower() {
"center" => @style.TextAlign::Center
"justify" => @style.TextAlign::Justify
"left" => @style.TextAlign::Left
"right" => @style.TextAlign::Right
"end" => @style.TextAlign::End
"start" | _ => @style.TextAlign::Start
}
}
///|
/// Parse grid-auto-flow value
pub fn parse_grid_auto_flow(value : String) -> @types.GridAutoFlow {
match value.trim().to_lower() {
"row" => @types.GridAutoFlow::Row
"column" => @types.GridAutoFlow::Column
"row dense" | "dense row" => @types.GridAutoFlow::RowDense
"column dense" | "dense column" => @types.GridAutoFlow::ColumnDense
_ => @types.GridAutoFlow::Row
}
}
///|
/// Parse a grid placement value (for grid-column-start, grid-row-end, etc.)
/// Supports: auto, , span
pub fn parse_grid_placement(value : String) -> @types.GridPlacement {
let v = value.trim().to_lower()
if v == "auto" {
return @types.GridPlacement::Auto
}
// Check for "span N" pattern
if v.has_prefix("span") {
let num_str = view_to_string(v.view(start_offset=4))
let num = @string.parse_int(num_str.trim().to_string()) catch { _ => 1 }
return @types.GridPlacement::Span(num)
}
// Otherwise, parse as integer (line number)
let line = @string.parse_int(v.to_string()) catch { _ => 0 }
if line != 0 {
@types.GridPlacement::Line(line)
} else if is_basic_custom_ident(v) {
// Browser-compatible fallback: unresolved custom identifiers in placement
// create an implicit track before the item (equivalent to line 2 start).
@types.GridPlacement::Line(2)
} else {
@types.GridPlacement::Auto
}
}
///|
/// Parse grid-column or grid-row shorthand
/// Formats: , / , / span
pub fn parse_grid_line_shorthand(
value : String,
) -> (@types.GridPlacement, @types.GridPlacement) {
let v = value.trim()
// Check for "/" separator
let parts = v.split("/").map(fn(s) { s.trim().to_string() }).collect()
if parts.length() == 2 {
let start = parse_grid_placement(parts[0])
let end = parse_grid_placement(parts[1])
(start, end)
} else {
// Single value: applies to start, end is auto
let start = parse_grid_placement(v.to_string())
(start, @types.GridPlacement::Auto)
}
}
///|
/// Parse grid-area shorthand:
/// / / /
pub fn parse_grid_area_shorthand(
value : String,
) -> (
@types.GridPlacement,
@types.GridPlacement,
@types.GridPlacement,
@types.GridPlacement,
) {
let v = value.trim()
let parts = v
.split("/")
.map(fn(s) { s.trim().to_string() })
.filter(fn(s) { !s.is_empty() })
.collect()
if parts.length() >= 4 {
(
parse_grid_placement(parts[0]),
parse_grid_placement(parts[1]),
parse_grid_placement(parts[2]),
parse_grid_placement(parts[3]),
)
} else if parts.length() == 3 {
(
parse_grid_placement(parts[0]),
parse_grid_placement(parts[1]),
parse_grid_placement(parts[2]),
@types.GridPlacement::Auto,
)
} else if parts.length() == 2 {
(
parse_grid_placement(parts[0]),
parse_grid_placement(parts[1]),
@types.GridPlacement::Auto,
@types.GridPlacement::Auto,
)
} else if parts.length() == 1 {
let start = parse_grid_placement(parts[0])
(start, start, @types.GridPlacement::Auto, @types.GridPlacement::Auto)
} else {
(
@types.GridPlacement::Auto,
@types.GridPlacement::Auto,
@types.GridPlacement::Auto,
@types.GridPlacement::Auto,
)
}
}
///|
fn is_basic_custom_ident(value : StringView) -> Bool {
if value.length() == 0 {
return false
}
let first = value[0]
let first_ok = (first >= 'a' && first <= 'z') ||
(first >= 'A' && first <= 'Z') ||
first == '_' ||
first == '-'
if !first_ok {
return false
}
for i = 1; i < value.length(); i = i + 1 {
let c = value[i]
let ok = (c >= 'a' && c <= 'z') ||
(c >= 'A' && c <= 'Z') ||
(c >= '0' && c <= '9') ||
c == '_' ||
c == '-'
if !ok {
return false
}
}
true
}
///|
/// Parse a number value
pub fn parse_number(value : String) -> Double {
@string.parse_double(value.trim().to_string()) catch {
_ => 0.0
}
}
///|
/// Parse an integer value
pub fn parse_integer(value : String) -> Int {
@string.parse_int(value.trim().to_string()) catch {
_ => 0
}
}
///|
/// Parse grid-template-columns or grid-template-rows value
/// Delegate to the full parser to support minmax() and repeat().
pub fn parse_grid_template_tracks(
value : String,
) -> Array[@types.TrackSizingFunction] {
@parser.parse_grid_template_tracks_from_string(value)
}
///|
/// Parse grid-template shorthand: ` / `
pub fn parse_grid_template_shorthand(
value : String,
) -> (Array[@types.TrackSizingFunction], Array[@types.TrackSizingFunction])? {
let v = value.trim()
if !v.contains("/") {
return None
}
let parts = v.split("/").map(fn(s) { s.trim().to_string() }).collect()
if parts.length() != 2 {
return None
}
let rows = parse_grid_template_tracks(parts[0])
let cols = parse_grid_template_tracks(parts[1])
Some((rows, cols))
}
///|
/// Parse grid-template-areas CSS property value.
/// The value is a series of quoted strings like: "header header" "sidebar main" "footer footer"
pub fn parse_grid_template_areas_value(value : String) -> Array[String] {
let result : Array[String] = []
let v = value.trim()
// Parse quoted strings - each quoted string is one row
let mut i = 0
while i < v.length() {
let c = v[i].unsafe_to_char()
if c == '"' || c == '\'' {
let quote = c
let mut end = i + 1
while end < v.length() && v[end].unsafe_to_char() != quote {
end = end + 1
}
if end > i + 1 {
let row_str = view_to_string(v.view(start_offset=i + 1, end_offset=end))
result.push(row_str.to_string())
}
i = end + 1
} else {
i = i + 1
}
}
result
}
///|
/// Check if a value is a named grid area identifier (not a number, auto, or span keyword)
pub fn is_grid_area_name(value : String) -> Bool {
let v = value.trim().to_lower()
if v == "auto" ||
v == "none" ||
v == "inherit" ||
v == "initial" ||
v == "unset" {
return false
}
if v.has_prefix("span") {
return false
}
// Check if it's a number
let parsed = @string.parse_int(v.to_string()) catch { _ => 0 }
if parsed != 0 {
return false
}
// Must be a valid custom ident
is_basic_custom_ident(v)
}
///|
/// Parse aspect-ratio value
pub fn parse_aspect_ratio(value : String) -> Double? {
let v = value.trim()
if v == "auto" {
return None
}
// Check for ratio format: "16 / 9" or "16/9"
if v.contains("/") {
// Find the slash position
let mut slash_pos = -1
for i = 0; i < v.length(); i = i + 1 {
if v[i].to_int().unsafe_to_char() == '/' {
slash_pos = i
break
}
}
if slash_pos > 0 {
// Use view slicing
let width_str = view_to_string(v.view(end_offset=slash_pos)).trim()
let height_str = view_to_string(v.view(start_offset=slash_pos + 1)).trim()
let w = @string.parse_double(width_str.to_string()) catch {
_ => return None
}
let h = @string.parse_double(height_str.to_string()) catch {
_ => return None
}
if h != 0.0 {
return Some(w / h)
}
return None
}
}
// Try parsing as a single number
let n = @string.parse_double(v.to_string()) catch { _ => return None }
Some(n)
}
///|
/// Parse CSS clip property value
/// Supports: auto, rect(top, right, bottom, left)
/// Note: This is a deprecated property but widely used for accessibility
pub fn parse_clip(value : String) -> @types.ClipRect {
let v = value.trim().to_lower()
if v == "auto" || v == "initial" || v == "unset" {
return @types.ClipRect::Auto
}
// Parse rect(top, right, bottom, left)
if v.has_prefix("rect(") && v.has_suffix(")") {
// Extract the content inside rect()
let content = view_to_string(
v.view(start_offset=5, end_offset=v.length() - 1),
).trim()
// Split by comma or space (both are valid in CSS)
let parts : Array[String] = []
let mut current = StringBuilder::new()
for c in content.iter() {
if c == ',' || c == ' ' {
let s = current.to_string().trim().to_string()
if !s.is_empty() {
parts.push(s)
}
current = StringBuilder::new()
} else {
current.write_char(c)
}
}
let last = current.to_string().trim().to_string()
if !last.is_empty() {
parts.push(last)
}
if parts.length() == 4 {
let top = parse_clip_value(parts[0])
let right = parse_clip_value(parts[1])
let bottom = parse_clip_value(parts[2])
let left = parse_clip_value(parts[3])
return @types.ClipRect::Rect(top~, right~, bottom~, left~)
}
}
@types.ClipRect::Auto
}
///|
/// Parse the supported subset of CSS clip-path.
/// Supports: none, inset( ), circle( at ).
pub fn parse_clip_path(value : String) -> @style.ClipPath {
let raw_v = value.trim().to_string()
let v = raw_v.to_lower()
if v == "none" || v == "initial" || v == "unset" || v.is_empty() {
return @style.ClipPath::None
}
if v.has_prefix("inset(") && v.has_suffix(")") {
let raw_content = view_to_string(
v.view(start_offset=6, end_offset=v.length() - 1),
)
.trim()
.to_string()
let content = match raw_content.find("round") {
Some(idx) =>
raw_content.unsafe_substring(start=0, end=idx).trim().to_string()
None => raw_content
}
let parts = split_clip_path_values(content)
if parts.length() > 0 {
let values = parts.map(parse_clip_path_inset_value)
match values.length() {
1 => {
let v = values[0]
return @style.ClipPath::Inset(v, v, v, v)
}
2 => {
let vertical = values[0]
let horizontal = values[1]
return @style.ClipPath::Inset(
vertical, horizontal, vertical, horizontal,
)
}
3 =>
return @style.ClipPath::Inset(
values[0],
values[1],
values[2],
values[1],
)
_ =>
return @style.ClipPath::Inset(
values[0],
values[1],
values[2],
values[3],
)
}
}
}
if v.has_prefix("rect(") && v.has_suffix(")") {
let content = view_to_string(
v.view(start_offset=5, end_offset=v.length() - 1),
)
.trim()
.to_string()
return parse_clip_path_rect(content)
}
if v.has_prefix("xywh(") && v.has_suffix(")") {
let raw_content = view_to_string(
v.view(start_offset=5, end_offset=v.length() - 1),
)
.trim()
.to_string()
let content = match raw_content.find("round") {
Some(idx) =>
raw_content.unsafe_substring(start=0, end=idx).trim().to_string()
None => raw_content
}
return parse_clip_path_xywh(content)
}
if v.has_prefix("circle(") && v.has_suffix(")") {
let content = view_to_string(
v.view(start_offset=7, end_offset=v.length() - 1),
)
.trim()
.to_string()
return parse_clip_path_circle(content)
}
if v.has_prefix("ellipse(") && v.has_suffix(")") {
let content = view_to_string(
v.view(start_offset=8, end_offset=v.length() - 1),
)
.trim()
.to_string()
return parse_clip_path_ellipse(content)
}
if v.has_prefix("polygon(") && v.has_suffix(")") {
let content = view_to_string(
v.view(start_offset=8, end_offset=v.length() - 1),
)
.trim()
.to_string()
return parse_clip_path_polygon(content)
}
if v.has_prefix("path(") && v.has_suffix(")") {
let content = view_to_string(
raw_v.view(start_offset=5, end_offset=raw_v.length() - 1),
)
.trim()
.to_string()
return parse_clip_path_path(content)
}
@style.ClipPath::None
}
///|
fn parse_clip_path_rect(content : String) -> @style.ClipPath {
let parts = split_clip_path_values(content)
if parts.length() < 4 {
return @style.ClipPath::None
}
let top = parse_clip_path_shape_value(parts[0]) catch {
_ => return @style.ClipPath::None
}
let right = parse_clip_path_shape_value(parts[1]) catch {
_ => return @style.ClipPath::None
}
let bottom = parse_clip_path_shape_value(parts[2]) catch {
_ => return @style.ClipPath::None
}
let left = parse_clip_path_shape_value(parts[3]) catch {
_ => return @style.ClipPath::None
}
@style.ClipPath::Rect(top, right, bottom, left)
}
///|
fn parse_clip_path_xywh(content : String) -> @style.ClipPath {
let parts = split_clip_path_values(content)
if parts.length() < 4 {
return @style.ClipPath::None
}
let x = parse_clip_path_shape_value(parts[0]) catch {
_ => return @style.ClipPath::None
}
let y = parse_clip_path_shape_value(parts[1]) catch {
_ => return @style.ClipPath::None
}
let width = parse_clip_path_shape_value(parts[2]) catch {
_ => return @style.ClipPath::None
}
let height = parse_clip_path_shape_value(parts[3]) catch {
_ => return @style.ClipPath::None
}
@style.ClipPath::Xywh(x, y, width, height)
}
///|
fn parse_clip_path_circle(content : String) -> @style.ClipPath {
let parts = split_clip_path_values(content)
if parts.length() == 0 {
return @style.ClipPath::None
}
let mut at_index = -1
for i in 0.. return @style.ClipPath::None
}
center_y = parse_clip_path_shape_value(parts[2]) catch {
_ => return @style.ClipPath::None
}
} else if at_index > 0 {
radius = parse_clip_path_shape_value(parts[0]) catch {
_ => return @style.ClipPath::None
}
if parts.length() <= at_index + 2 {
return @style.ClipPath::None
}
center_x = parse_clip_path_shape_value(parts[at_index + 1]) catch {
_ => return @style.ClipPath::None
}
center_y = parse_clip_path_shape_value(parts[at_index + 2]) catch {
_ => return @style.ClipPath::None
}
} else {
radius = parse_clip_path_shape_value(parts[0]) catch {
_ => return @style.ClipPath::None
}
}
@style.ClipPath::Circle(radius, center_x, center_y)
}
///|
fn parse_clip_path_ellipse(content : String) -> @style.ClipPath {
let parts = split_clip_path_values(content)
if parts.length() == 0 {
return @style.ClipPath::None
}
let mut at_index = -1
for i in 0.. return @style.ClipPath::None
}
center_y = parse_clip_path_shape_value(parts[2]) catch {
_ => return @style.ClipPath::None
}
} else if at_index > 0 {
if at_index < 2 || parts.length() <= at_index + 2 {
return @style.ClipPath::None
}
radius_x = parse_clip_path_shape_value(parts[0]) catch {
_ => return @style.ClipPath::None
}
radius_y = parse_clip_path_shape_value(parts[1]) catch {
_ => return @style.ClipPath::None
}
center_x = parse_clip_path_shape_value(parts[at_index + 1]) catch {
_ => return @style.ClipPath::None
}
center_y = parse_clip_path_shape_value(parts[at_index + 2]) catch {
_ => return @style.ClipPath::None
}
} else {
if parts.length() < 2 {
return @style.ClipPath::None
}
radius_x = parse_clip_path_shape_value(parts[0]) catch {
_ => return @style.ClipPath::None
}
radius_y = parse_clip_path_shape_value(parts[1]) catch {
_ => return @style.ClipPath::None
}
}
@style.ClipPath::Ellipse(radius_x, radius_y, center_x, center_y)
}
///|
fn parse_clip_path_polygon(content : String) -> @style.ClipPath {
let parts = split_clip_path_values(content)
let values : Array[Double] = []
for part in parts {
if part == "evenodd" || part == "nonzero" {
continue
}
let v = parse_clip_path_shape_value(part) catch {
_ => return @style.ClipPath::None
}
values.push(v)
}
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(content : String) -> @style.ClipPath {
let path_data = match extract_clip_path_path_string(content) {
Some(v) => v
None => return @style.ClipPath::None
}
parse_clip_path_path_data(path_data)
}
///|
fn extract_clip_path_path_string(content : String) -> String? {
match extract_clip_path_path_string_with_quote(content, '"') {
Some(v) => Some(v)
None => extract_clip_path_path_string_with_quote(content, '\'')
}
}
///|
fn extract_clip_path_path_string_with_quote(
content : String,
quote : Char,
) -> String? {
let out = StringBuilder::new()
let mut in_quote = false
for c in content.iter() {
if in_quote {
if c == quote {
return Some(out.to_string())
}
out.write_char(c)
} else if c == quote {
in_quote = true
}
}
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_string()
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 split_clip_path_values(content : String) -> Array[String] {
let parts : Array[String] = []
let mut current = StringBuilder::new()
for c in content.iter() {
if c == ',' || c == ' ' || c == '\t' || c == '\n' {
let s = current.to_string().trim().to_string()
if !s.is_empty() {
parts.push(s)
}
current = StringBuilder::new()
} else {
current.write_char(c)
}
}
let last = current.to_string().trim().to_string()
if !last.is_empty() {
parts.push(last)
}
parts
}
///|
fn parse_clip_path_shape_value(value : String) -> Double raise Error {
let v = value.trim().to_lower()
match v.strip_suffix("%") {
Some(num_str) => {
let n = @string.parse_double(num_str.to_string().trim())
return -(n / 100.0)
}
None => ()
}
match v.strip_suffix("px") {
Some(num_str) => @string.parse_double(num_str.to_string().trim())
None => @string.parse_double(v)
}
}
///|
fn parse_clip_path_inset_value(value : String) -> Double {
let v = value.trim().to_lower()
match v.strip_suffix("%") {
Some(num_str) => {
let n = @string.parse_double(num_str.to_string().trim()) catch {
_ => return 0.0
}
return -(n / 100.0)
}
None => ()
}
match v.strip_suffix("px") {
Some(num_str) => {
let n = @string.parse_double(num_str.to_string().trim()) catch {
_ => return 0.0
}
return n
}
None => ()
}
@string.parse_double(v) catch {
_ => 0.0
}
}
///|
/// Parse a single clip rect value (auto or length)
fn parse_clip_value(value : String) -> Double {
let v = value.trim().to_lower()
if v == "auto" {
// auto in clip rect context means 0 (no offset)
return 0.0
}
// Parse px value
match v.strip_suffix("px") {
Some(num_str) => {
let n = @string.parse_double(num_str.to_string().trim()) catch {
_ => return 0.0
}
return n
}
None => ()
}
// Parse plain number
let n = @string.parse_double(v.to_string()) catch { _ => return 0.0 }
n
}