///|
priv enum ComputedStyleProperty {
StyleFill
StyleFillRule
StyleClipRule
StyleFillOpacity
StyleStroke
StyleStrokeWidth
StyleStrokeLinecap
StyleStrokeLinejoin
StyleStrokeMiterlimit
StyleStrokeDasharray
StyleStrokeDashoffset
StyleStrokeOpacity
StyleVectorEffect
StyleImageRendering
StyleColor
StylePaintOrder
StyleOpacity
StyleMarkerStart
StyleMarkerMid
StyleMarkerEnd
StyleStopColor
StyleStopOpacity
StyleFontSize
}
///|
fn computed_style_property(name : String) -> ComputedStyleProperty? {
match name {
"fill" => Some(StyleFill)
"fill-rule" => Some(StyleFillRule)
"clip-rule" => Some(StyleClipRule)
"fill-opacity" => Some(StyleFillOpacity)
"stroke" => Some(StyleStroke)
"stroke-width" => Some(StyleStrokeWidth)
"stroke-linecap" => Some(StyleStrokeLinecap)
"stroke-linejoin" => Some(StyleStrokeLinejoin)
"stroke-miterlimit" => Some(StyleStrokeMiterlimit)
"stroke-dasharray" => Some(StyleStrokeDasharray)
"stroke-dashoffset" => Some(StyleStrokeDashoffset)
"stroke-opacity" => Some(StyleStrokeOpacity)
"vector-effect" => Some(StyleVectorEffect)
"image-rendering" => Some(StyleImageRendering)
"color" => Some(StyleColor)
"paint-order" => Some(StylePaintOrder)
"opacity" => Some(StyleOpacity)
"marker-start" => Some(StyleMarkerStart)
"marker-mid" => Some(StyleMarkerMid)
"marker-end" => Some(StyleMarkerEnd)
"stop-color" => Some(StyleStopColor)
"stop-opacity" => Some(StyleStopOpacity)
"font-size" => Some(StyleFontSize)
_ => None
}
}
///|
fn ComputedStyleProperty::name(self : ComputedStyleProperty) -> String {
match self {
StyleFill => "fill"
StyleFillRule => "fill-rule"
StyleClipRule => "clip-rule"
StyleFillOpacity => "fill-opacity"
StyleStroke => "stroke"
StyleStrokeWidth => "stroke-width"
StyleStrokeLinecap => "stroke-linecap"
StyleStrokeLinejoin => "stroke-linejoin"
StyleStrokeMiterlimit => "stroke-miterlimit"
StyleStrokeDasharray => "stroke-dasharray"
StyleStrokeDashoffset => "stroke-dashoffset"
StyleStrokeOpacity => "stroke-opacity"
StyleVectorEffect => "vector-effect"
StyleImageRendering => "image-rendering"
StyleColor => "color"
StylePaintOrder => "paint-order"
StyleOpacity => "opacity"
StyleMarkerStart => "marker-start"
StyleMarkerMid => "marker-mid"
StyleMarkerEnd => "marker-end"
StyleStopColor => "stop-color"
StyleStopOpacity => "stop-opacity"
StyleFontSize => "font-size"
}
}
///|
fn valid_svg_number(value : String, allow_length : Bool) -> Bool {
let trimmed = trim_string(value)
if allow_length {
svg_length_number_text(trimmed) is Some(_)
} else {
parse_number_strict(trimmed) is Some(_)
}
}
///|
fn svg_number_tokens(value : String) -> Array[String] {
let tokens : Array[String] = []
let mut token = StringBuilder::new()
for c in value {
if c == ',' || c == ' ' || c == '\t' || c == '\n' || c == '\r' {
let current = token.to_string()
if current.length() > 0 {
tokens.push(current)
token = StringBuilder::new()
}
} else {
token.write_char(c)
}
}
let current = token.to_string()
if current.length() > 0 {
tokens.push(current)
}
tokens
}
///|
fn valid_svg_paint(value : String) -> Bool {
let trimmed = trim_string(value)
let lower = trimmed.to_lower()
if lower == "none" || lower == "currentcolor" {
return true
}
if string_starts_with(lower, "url(") {
let close = find_first(trimmed, ')')
if close < 0 {
return false
}
let reference = build_substring(trimmed, 0, close + 1)
if parse_url_ref(reference) is None {
return false
}
let fallback = trim_string(
build_substring(trimmed, close + 1, trimmed.length()),
)
return fallback.length() == 0 || valid_svg_paint(fallback)
}
!(@css_computed.parse_color(trimmed) is Invalid)
}
///|
fn valid_svg_paint_order(value : String) -> Bool {
let tokens = split_tokens(trim_string(value).to_lower())
if tokens.length() == 0 {
return false
}
for token in tokens {
if token == "normal" {
return tokens.length() == 1
}
if token != "normal" &&
token != "fill" &&
token != "stroke" &&
token != "markers" {
return false
}
}
true
}
///|
fn valid_svg_dasharray(value : String) -> Bool {
let normalized = trim_string(value)
if normalized == "none" {
return true
}
let tokens = svg_number_tokens(normalized)
if tokens.length() == 0 {
return false
}
for token in tokens {
if !valid_svg_number(token, true) || parse_length(token) < 0.0 {
return false
}
}
true
}
///|
fn valid_svg_transform(value : String) -> Bool {
let input = trim_string(value)
if input == "none" {
return true
}
let mut i = 0
let mut functions = 0
while i < input.length() {
while i < input.length() {
let c = Int::unsafe_to_char(input[i].to_int())
if css_is_whitespace(c) || c == ',' {
i += 1
} else {
break
}
}
if i >= input.length() {
break
}
let name_start = i
while i < input.length() {
let c = Int::unsafe_to_char(input[i].to_int())
if (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') {
i += 1
} else {
break
}
}
let name = build_substring(input, name_start, i)
while i < input.length() &&
css_is_whitespace(Int::unsafe_to_char(input[i].to_int())) {
i += 1
}
if i >= input.length() || Int::unsafe_to_char(input[i].to_int()) != '(' {
return false
}
i += 1
let args_start = i
while i < input.length() && Int::unsafe_to_char(input[i].to_int()) != ')' {
if Int::unsafe_to_char(input[i].to_int()) == '(' {
return false
}
i += 1
}
if i >= input.length() {
return false
}
let args = svg_number_tokens(build_substring(input, args_start, i))
for arg in args {
if !valid_svg_number(arg, false) {
return false
}
}
let arity_ok = match name {
"translate" | "scale" => args.length() == 1 || args.length() == 2
"rotate" => args.length() == 1 || args.length() == 3
"skewX" | "skewY" => args.length() == 1
"matrix" => args.length() == 6
_ => false
}
if !arity_ok {
return false
}
functions += 1
i += 1
}
functions > 0
}
///|
fn svg_property_value_is_valid(property : String, value : String) -> Bool {
let trimmed = trim_string(value)
if trimmed.to_lower().contains("var(") {
return true
}
match property {
"fill" | "stroke" => valid_svg_paint(trimmed)
"color" | "stop-color" => !(@css_computed.parse_color(trimmed) is Invalid)
"fill-rule" | "clip-rule" => trimmed == "nonzero" || trimmed == "evenodd"
"fill-opacity" | "stroke-opacity" | "stop-opacity" | "opacity" =>
valid_svg_number(trimmed, false)
"stroke-width" =>
valid_svg_number(trimmed, true) && parse_length(trimmed) >= 0.0
"stroke-dashoffset" => valid_svg_number(trimmed, true)
"stroke-miterlimit" =>
valid_svg_number(trimmed, false) && parse_number(trimmed) >= 1.0
"stroke-linecap" =>
trimmed == "butt" || trimmed == "round" || trimmed == "square"
"stroke-linejoin" =>
trimmed == "miter" || trimmed == "round" || trimmed == "bevel"
"stroke-dasharray" => valid_svg_dasharray(trimmed)
"vector-effect" => trimmed == "none" || trimmed == "non-scaling-stroke"
"image-rendering" =>
match trimmed.to_lower() {
"auto"
| "optimizespeed"
| "optimizequality"
| "crisp-edges"
| "pixelated" => true
_ => false
}
"paint-order" => valid_svg_paint_order(trimmed)
"marker-start" | "marker-mid" | "marker-end" | "marker" =>
trimmed == "none" || parse_url_ref(trimmed) is Some(_)
"x" | "y" | "cx" | "cy" | "x1" | "y1" | "x2" | "y2" =>
valid_svg_number(trimmed, true)
"width" | "height" | "rx" | "ry" | "r" =>
valid_svg_number(trimmed, true) && parse_length(trimmed) >= 0.0
"d" | "points" => trimmed.length() > 0
"transform" => valid_svg_transform(trimmed)
_ => true
}
}
///|
fn svg_declaration_is_valid(
property : String,
value : @css_cascade.PropertyValue,
) -> Bool {
match value {
Value(value) => svg_property_value_is_valid(property, value)
_ => true
}
}
///|
fn ComputedStyleProperty::is_inherited(self : ComputedStyleProperty) -> Bool {
match self {
StyleOpacity | StyleVectorEffect => false
_ => true
}
}
///|
fn is_computed_style_property(name : String) -> Bool {
computed_style_property(name) is Some(_)
}