///|
/// 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 trimmed = part.trim().to_string()
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
}
///|
/// 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_string()
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_string()
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_string()
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_string()
if !s.is_empty() {
tokens.push(s)
}
current.reset()
}
} else {
current.write_char(c)
}
}
let final_str = current.to_string().trim().to_string()
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_string()
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_string(),
value.trim().to_string(),
)
}
// 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_string()) {
Some(value) => {
let range_op = match op {
">=" => Ge
"<=" => Le
">" => Gt
"<" => Lt
"=" => Eq
_ => return None
}
match name.trim().to_string().to_lower() {
"width" => return Some(WidthRange(range_op, value))
"height" => return Some(HeightRange(range_op, value))
_ => return Some(Custom(name.trim().to_string(), 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_string()
if s.is_empty() {
return 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_string()), parse_int(right.trim().to_string())) {
(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))
}
///|
/// 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
}