///|
/// CSS Media Query Parser
/// Parses media query strings like "@media screen and (min-width: 768px)"
///|
/// Parse a media query list from a string
/// e.g., "screen and (min-width: 768px), print"
pub fn parse_media_query_list(input : String) -> MediaQueryList {
let queries : Array[MediaQuery] = []
let parts = split_media_queries(input)
for part in parts {
let or_parts = split_media_query_or(part)
for or_part in or_parts {
let trimmed = or_part.trim().to_owned()
if !trimmed.is_empty() {
match parse_single_media_query(trimmed) {
Some(query) => queries.push(query)
None => ()
}
}
}
}
{ queries, }
}
///|
/// Split media queries by comma (top-level only, not inside parentheses)
fn split_media_queries(input : String) -> Array[String] {
let result : Array[String] = []
let current = StringBuilder::new()
let mut paren_depth = 0
for i = 0; i < input.length(); i = i + 1 {
let c = input[i].to_int().unsafe_to_char()
if c == '(' {
paren_depth += 1
current.write_char(c)
} else if c == ')' {
paren_depth -= 1
current.write_char(c)
} else if c == ',' && paren_depth == 0 {
result.push(current.to_string())
current.reset()
} else {
current.write_char(c)
}
}
let final_str = current.to_string()
if !final_str.is_empty() {
result.push(final_str)
}
result
}
///|
fn join_media_tokens(tokens : Array[String]) -> String {
let out = StringBuilder::new()
for i = 0; i < tokens.length(); i = i + 1 {
if i > 0 {
out.write_char(' ')
}
out.write_string(tokens[i])
}
out.to_string()
}
///|
fn split_media_query_or(input : String) -> Array[String] {
let result : Array[String] = []
let current : Array[String] = []
let tokens = tokenize_media_query(input)
for token in tokens {
if token.to_lower() == "or" {
if current.length() > 0 {
result.push(join_media_tokens(current))
current.clear()
}
} else {
current.push(token)
}
}
if current.length() > 0 {
result.push(join_media_tokens(current))
}
result
}
///|
/// Parse a single media query
/// e.g., "screen and (min-width: 768px)"
fn parse_single_media_query(input : String) -> MediaQuery? {
let input = input.trim().to_owned()
if input.is_empty() {
return None
}
let tokens = tokenize_media_query(input)
if tokens.is_empty() {
return None
}
let mut pos = 0
let mut negated = false
let mut only = false
let mut media_type : MediaType? = None
let conditions : Array[MediaFeature] = []
// Check for "not" or "only" prefix
if pos < tokens.length() {
let token = tokens[pos].to_lower()
if token == "not" {
negated = true
pos += 1
} else if token == "only" {
only = true
pos += 1
}
}
// Check for media type
if pos < tokens.length() {
let token = tokens[pos].to_lower()
match parse_media_type(token) {
Some(mt) => {
media_type = Some(mt)
pos += 1
}
None => ()
}
}
// Parse remaining conditions (and (...))
while pos < tokens.length() {
let token = tokens[pos].to_lower()
if token == "and" {
pos += 1
continue
}
if token.has_prefix("(") && token.has_suffix(")") {
// Extract content inside parentheses
let content = token.unsafe_substring(start=1, end=token.length() - 1)
match parse_media_feature(content) {
Some(feature) => conditions.push(feature)
None => ()
}
}
pos += 1
}
Some({ negated, only, media_type, conditions })
}
///|
/// Tokenize media query string into tokens
fn tokenize_media_query(input : String) -> Array[String] {
let tokens : Array[String] = []
let current = StringBuilder::new()
let mut in_paren = false
let mut paren_depth = 0
for i = 0; i < input.length(); i = i + 1 {
let c = input[i].to_int().unsafe_to_char()
if c == '(' {
if paren_depth == 0 {
// Push any preceding token
let s = current.to_string().trim().to_owned()
if !s.is_empty() {
tokens.push(s)
}
current.reset()
in_paren = true
}
paren_depth += 1
current.write_char(c)
} else if c == ')' {
paren_depth -= 1
current.write_char(c)
if paren_depth == 0 {
in_paren = false
let s = current.to_string().trim().to_owned()
if !s.is_empty() {
tokens.push(s)
}
current.reset()
}
} else if c == ' ' || c == '\t' || c == '\n' {
if in_paren {
current.write_char(c)
} else {
let s = current.to_string().trim().to_owned()
if !s.is_empty() {
tokens.push(s)
}
current.reset()
}
} else {
current.write_char(c)
}
}
let final_str = current.to_string().trim().to_owned()
if !final_str.is_empty() {
tokens.push(final_str)
}
tokens
}
///|
/// Parse media type from string
fn parse_media_type(s : String) -> MediaType? {
match s {
"all" => Some(All)
"screen" => Some(Screen)
"print" => Some(Print)
"speech" => Some(Speech)
_ => None
}
}
///|
/// Parse a media feature from the content inside parentheses
/// e.g., "min-width: 768px" or "color" or "width >= 768px"
fn parse_media_feature(content : String) -> MediaFeature? {
let content = content.trim().to_owned()
if content.is_empty() {
return None
}
// Check for range syntax (e.g., "width >= 768px")
match parse_range_feature(content) {
Some(feature) => return Some(feature)
None => ()
}
// Check for colon (property: value)
let colon_pos = find_char(content, ':')
if colon_pos >= 0 {
let name = content.unsafe_substring(start=0, end=colon_pos)
let value = content.unsafe_substring(
start=colon_pos + 1,
end=content.length(),
)
return parse_feature_with_value(
name.trim().to_owned(),
value.trim().to_owned(),
)
}
// Boolean feature (no value)
parse_boolean_feature(content)
}
///|
/// Parse range syntax feature (CSS Media Queries Level 4)
fn parse_range_feature(content : String) -> MediaFeature? {
// Check for comparison operators
let ops = [">=", "<=", ">", "<", "="]
for op in ops {
let op_pos = find_substring(content, op)
if op_pos >= 0 {
let name = content.unsafe_substring(start=0, end=op_pos)
let value_str = content.unsafe_substring(
start=op_pos + op.length(),
end=content.length(),
)
match parse_media_value(value_str.trim().to_owned()) {
Some(value) => {
let range_op = match op {
">=" => Ge
"<=" => Le
">" => Gt
"<" => Lt
"=" => Eq
_ => return None
}
match name.trim().to_owned().to_lower() {
"width" => return Some(WidthRange(range_op, value))
"height" => return Some(HeightRange(range_op, value))
_ => return Some(Custom(name.trim().to_owned(), Some(value)))
}
}
None => ()
}
}
}
None
}
///|
/// Parse a feature with a value (name: value)
fn parse_feature_with_value(name : String, value_str : String) -> MediaFeature? {
let name_lower = name.to_lower()
match parse_media_value(value_str) {
Some(value) =>
match name_lower {
"min-width" => Some(MinWidth(value))
"max-width" => Some(MaxWidth(value))
"width" => Some(Width(Some(value)))
"min-height" => Some(MinHeight(value))
"max-height" => Some(MaxHeight(value))
"height" => Some(Height(Some(value)))
"min-aspect-ratio" => Some(MinAspectRatio(value))
"max-aspect-ratio" => Some(MaxAspectRatio(value))
"aspect-ratio" => Some(AspectRatio(Some(value)))
"min-resolution" => Some(MinResolution(value))
"max-resolution" => Some(MaxResolution(value))
"resolution" => Some(Resolution(Some(value)))
"orientation" =>
match value {
Ident(s) => Some(Orientation(s))
_ => Some(Custom(name, Some(value)))
}
"display-mode" =>
match value {
Ident(s) => Some(DisplayMode(s))
_ => Some(Custom(name, Some(value)))
}
"prefers-color-scheme" =>
match value {
Ident(s) => Some(PrefersColorScheme(s))
_ => Some(Custom(name, Some(value)))
}
"prefers-reduced-motion" =>
match value {
Ident(s) => Some(PrefersReducedMotion(s))
_ => Some(Custom(name, Some(value)))
}
_ => Some(Custom(name, Some(value)))
}
None => Some(Custom(name, None))
}
}
///|
/// Parse a boolean feature (no value)
fn parse_boolean_feature(name : String) -> MediaFeature? {
let name_lower = name.to_lower()
match name_lower {
"color" => Some(Color)
"color-gamut" => Some(ColorGamut)
"color-index" => Some(ColorIndex)
"grid" => Some(Grid)
"hover" => Some(Hover)
"monochrome" => Some(Monochrome)
"pointer" => Some(Pointer)
"scripting" => Some(Scripting)
"update" => Some(Update)
"width" => Some(Width(None))
"height" => Some(Height(None))
"aspect-ratio" => Some(AspectRatio(None))
"resolution" => Some(Resolution(None))
_ => Some(Custom(name, None))
}
}
///|
/// Parse a media value
fn parse_media_value(s : String) -> MediaValue? {
let s = s.trim().to_owned()
if s.is_empty() {
return None
}
match parse_calc_media_value(s) {
Some(value) => return Some(value)
None => ()
}
// Check for ratio (e.g., "16/9")
let slash_pos = find_char(s, '/')
if slash_pos > 0 {
let left = s.unsafe_substring(start=0, end=slash_pos)
let right = s.unsafe_substring(start=slash_pos + 1, end=s.length())
match
(parse_int(left.trim().to_owned()), parse_int(right.trim().to_owned())) {
(Some(l), Some(r)) => return Some(Ratio(l, r))
_ => ()
}
}
// Check for dimension (e.g., "768px", "2dppx")
match parse_dimension_value(s) {
Some((value, unit)) => return Some(Dimension(value, unit))
None => ()
}
// Check for number
match parse_number_value(s) {
Some(n) => return Some(Number(n))
None => ()
}
// Otherwise, it's an identifier
Some(Ident(s))
}
///|
fn parse_calc_media_value(s : String) -> MediaValue? {
let lower = s.to_lower()
if !lower.has_prefix("calc(") || !s.has_suffix(")") {
return None
}
let inner = s.unsafe_substring(start=5, end=s.length() - 1)
match parse_calc_expr_at(inner, 0) {
Some((value, pos)) => {
let pos = skip_calc_ws(inner, pos)
if pos == inner.length() {
Some(Dimension(value, "px"))
} else {
None
}
}
None => None
}
}
///|
fn skip_calc_ws(s : String, pos : Int) -> Int {
let mut i = pos
while i < s.length() {
let c = s[i].to_int().unsafe_to_char()
if c == ' ' || c == '\t' || c == '\n' {
i = i + 1
} else {
break
}
}
i
}
///|
fn parse_calc_expr_at(s : String, pos : Int) -> (Double, Int)? {
match parse_calc_term_at(s, pos) {
Some((first, next)) => {
let mut value = first
let mut i = skip_calc_ws(s, next)
while i < s.length() {
let op = s[i].to_int().unsafe_to_char()
if op != '+' && op != '-' {
break
}
match parse_calc_term_at(s, i + 1) {
Some((rhs, after_rhs)) => {
value = if op == '+' { value + rhs } else { value - rhs }
i = skip_calc_ws(s, after_rhs)
}
None => return None
}
}
Some((value, i))
}
None => None
}
}
///|
fn parse_calc_term_at(s : String, pos : Int) -> (Double, Int)? {
match parse_calc_factor_at(s, pos) {
Some((first, next)) => {
let mut value = first
let mut i = skip_calc_ws(s, next)
while i < s.length() {
let op = s[i].to_int().unsafe_to_char()
if op != '*' && op != '/' {
break
}
match parse_calc_factor_at(s, i + 1) {
Some((rhs, after_rhs)) => {
if op == '*' {
value = value * rhs
} else if rhs != 0.0 {
value = value / rhs
} else {
return None
}
i = skip_calc_ws(s, after_rhs)
}
None => return None
}
}
Some((value, i))
}
None => None
}
}
///|
fn parse_calc_factor_at(s : String, pos : Int) -> (Double, Int)? {
let pos = skip_calc_ws(s, pos)
if pos >= s.length() {
return None
}
let c = s[pos].to_int().unsafe_to_char()
if c == '+' {
return parse_calc_factor_at(s, pos + 1)
}
if c == '-' {
match parse_calc_factor_at(s, pos + 1) {
Some((value, next)) => return Some((-value, next))
None => return None
}
}
if c == '(' {
match parse_calc_expr_at(s, pos + 1) {
Some((value, next)) => {
let next = skip_calc_ws(s, next)
if next < s.length() && s[next].to_int().unsafe_to_char() == ')' {
return Some((value, next + 1))
}
return None
}
None => return None
}
}
parse_calc_numeric_value_at(s, pos)
}
///|
fn parse_calc_numeric_value_at(s : String, pos : Int) -> (Double, Int)? {
let mut i = pos
let mut saw_digit = false
while i < s.length() {
let c = s[i].to_int().unsafe_to_char()
if c >= '0' && c <= '9' {
saw_digit = true
i = i + 1
} else if c == '.' {
i = i + 1
} else {
break
}
}
if !saw_digit {
return None
}
let num_str = s.unsafe_substring(start=pos, end=i)
let value = @string.parse_double(num_str) catch { _ => return None }
let unit_start = i
while i < s.length() {
let c = s[i].to_int().unsafe_to_char()
if (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '%' {
i = i + 1
} else {
break
}
}
let unit = if i > unit_start {
s.unsafe_substring(start=unit_start, end=i).to_lower()
} else {
""
}
let px_value = match unit {
"px" | "" => value
"em" | "rem" => value * 16.0
"pt" => value * 1.333333
"pc" => value * 16.0
"in" => value * 96.0
"cm" => value * 37.8
"mm" => value * 3.78
_ => value
}
Some((px_value, i))
}
///|
/// Parse a dimension value (number + unit)
fn parse_dimension_value(s : String) -> (Double, String)? {
let mut num_end = 0
for i = 0; i < s.length(); i = i + 1 {
let c = s[i].to_int().unsafe_to_char()
if (c >= '0' && c <= '9') || c == '.' || c == '-' || c == '+' {
num_end = i + 1
} else {
break
}
}
if num_end > 0 && num_end < s.length() {
let num_str = s.unsafe_substring(start=0, end=num_end)
let unit = s.unsafe_substring(start=num_end, end=s.length())
try {
let value = @string.parse_double(num_str)
Some((value, unit))
} catch {
_ => None
}
} else {
None
}
}
///|
/// Parse a number value
fn parse_number_value(s : String) -> Double? {
Some(@string.parse_double(s)) catch {
_ => None
}
}
///|
/// Parse an integer value
fn parse_int(s : String) -> Int? {
Some(@string.parse_int(s)) catch {
_ => None
}
}
///|
/// Find the position of a character in a string
fn find_char(s : String, c : Char) -> Int {
for i = 0; i < s.length(); i = i + 1 {
if s[i].to_int().unsafe_to_char() == c {
return i
}
}
-1
}
///|
/// Find the position of a substring
fn find_substring(s : String, sub : String) -> Int {
if sub.length() > s.length() {
return -1
}
for i = 0; i <= s.length() - sub.length(); i = i + 1 {
let mut found = true
for j = 0; j < sub.length(); j = j + 1 {
if s[i + j] != sub[j] {
found = false
break
}
}
if found {
return i
}
}
-1
}