use crate::CssProps;
use indexmap::{IndexMap, IndexSet};
use serde_json::json;
pub fn parse_var_references(input: &str) -> Vec<(usize, usize, String)> {
let mut results = Vec::new();
let bytes = input.as_bytes();
let mut i = 0;
while i < bytes.len() {
if i + 4 <= bytes.len() && &bytes[i..i + 4] == b"var(" {
let start = i;
i += 4;
while i < bytes.len()
&& (bytes[i] == b' ' || bytes[i] == b'\t' || bytes[i] == b'\n' || bytes[i] == b'\r')
{
i += 1;
}
let has_prefix = i + 2 <= bytes.len() && &bytes[i..i + 2] == b"--";
if has_prefix {
i += 2;
}
let name_start = i;
while i < bytes.len() {
let c = bytes[i];
if (c >= b'a' && c <= b'z')
|| (c >= b'A' && c <= b'Z')
|| (c >= b'0' && c <= b'9')
|| c == b'_'
|| c == b'.'
|| c == b'-'
{
i += 1;
} else {
break;
}
}
let name_end = i;
if name_start < name_end {
while i < bytes.len()
&& (bytes[i] == b' '
|| bytes[i] == b'\t'
|| bytes[i] == b'\n'
|| bytes[i] == b'\r')
{
i += 1;
}
if i < bytes.len() && bytes[i] == b')' {
let end = i + 1;
let var_name = std::str::from_utf8(&bytes[name_start..name_end])
.unwrap_or("")
.to_string();
results.push((start, end, var_name));
i = end;
continue;
}
}
}
i += 1;
}
results
}
pub fn resolve_vars(input: &str, vars: &IndexMap<String, String>) -> String {
let var_refs = parse_var_references(input);
if var_refs.is_empty() {
if input.starts_with('$') {
if let Some(val) = vars.get(&input[1..]) {
return val.clone();
}
}
return input.to_string();
}
let mut out = input.to_string();
for (start, end, var_name) in var_refs.iter().rev() {
if let Some(val) = vars.get(var_name) {
out.replace_range(*start..*end, val);
}
}
if out.starts_with('$') {
if let Some(val) = vars.get(&out[1..]) {
return val.clone();
}
}
out
}
pub fn camel_case(name: &str) -> String {
let mut out = String::new();
let mut upper = false;
for ch in name.chars() {
if ch == '-' {
upper = true;
continue;
}
if upper {
out.extend(ch.to_uppercase());
upper = false;
} else {
out.push(ch);
}
}
out
}
pub fn kebab_case(name: &str) -> String {
let mut out = String::new();
for (i, ch) in name.chars().enumerate() {
if ch.is_uppercase() {
if i > 0 {
out.push('-');
}
out.extend(ch.to_lowercase());
} else {
out.push(ch);
}
}
out
}
pub fn parse_prefixed_class(class: &str) -> (Option<String>, bool, String) {
let parts: Vec<&str> = class.split(':').collect();
if parts.len() == 1 {
return (None, false, class.to_string());
}
let mut bp: Option<String> = None;
let mut hover = false;
for &p in &parts[..parts.len() - 1] {
match p {
"hover" => hover = true,
"xs" | "sm" | "md" | "lg" | "xl" => bp = Some(p.to_string()),
_ => {}
}
}
let base = parts.last().unwrap().to_string();
(bp, hover, base)
}
pub fn css_escape_class(class: &str) -> String {
class.replace(':', "\\:")
}
pub fn class_to_selector(class: &str) -> String {
let (_bp, hover, base) = parse_prefixed_class(class);
if hover {
format!(".{}:hover", css_escape_class(&base))
} else {
format!(".{}", css_escape_class(&base))
}
}
pub fn css_value_to_android(
value: &serde_json::Value,
vars: &IndexMap<String, String>,
current_color: Option<&str>,
) -> serde_json::Value {
match value {
serde_json::Value::String(s) => {
let s2 = resolve_vars(s, vars);
if let Some(n) = s2.strip_suffix("px") {
if let Ok(parsed) = n.trim().parse::<f64>() {
return json!(parsed);
}
}
json!(s2)
}
_ => value.clone(),
}
}
pub fn merge_android_props(
into: &mut IndexMap<String, serde_json::Value>,
css_props: &CssProps,
vars: &IndexMap<String, String>,
) {
let current_color = css_props
.get("color")
.and_then(|v| v.as_str())
.map(|s| resolve_vars(s, vars))
.or_else(|| {
into.get("color")
.and_then(|v| v.as_str())
.map(|s| s.to_string())
});
for (k, v) in css_props.iter() {
let val = css_value_to_android(v, vars, current_color.as_deref());
match k.as_str() {
"padding" => {
into.insert("paddingTop".to_string(), val.clone());
into.insert("paddingBottom".to_string(), val.clone());
into.insert("paddingLeft".to_string(), val.clone());
into.insert("paddingRight".to_string(), val.clone());
into.insert("paddingHorizontal".to_string(), val.clone());
into.insert("paddingVertical".to_string(), val.clone());
into.insert("padding".to_string(), val);
}
"padding-horizontal" | "paddingHorizontal" => {
into.insert("paddingLeft".to_string(), val.clone());
into.insert("paddingRight".to_string(), val.clone());
into.insert("paddingHorizontal".to_string(), val);
}
"padding-vertical" | "paddingVertical" => {
into.insert("paddingTop".to_string(), val.clone());
into.insert("paddingBottom".to_string(), val.clone());
into.insert("paddingVertical".to_string(), val);
}
"margin" => {
into.insert("marginTop".to_string(), val.clone());
into.insert("marginBottom".to_string(), val.clone());
into.insert("marginLeft".to_string(), val.clone());
into.insert("marginRight".to_string(), val.clone());
into.insert("marginHorizontal".to_string(), val.clone());
into.insert("marginVertical".to_string(), val.clone());
into.insert("margin".to_string(), val);
}
"margin-horizontal" | "marginHorizontal" => {
into.insert("marginLeft".to_string(), val.clone());
into.insert("marginRight".to_string(), val.clone());
into.insert("marginHorizontal".to_string(), val);
}
"margin-vertical" | "marginVertical" => {
into.insert("marginTop".to_string(), val.clone());
into.insert("marginBottom".to_string(), val.clone());
into.insert("marginVertical".to_string(), val);
}
"border-radius" | "borderRadius" => {
into.insert("borderTopLeftRadius".to_string(), val.clone());
into.insert("borderTopRightRadius".to_string(), val.clone());
into.insert("borderBottomLeftRadius".to_string(), val.clone());
into.insert("borderBottomRightRadius".to_string(), val.clone());
into.insert("borderRadius".to_string(), val);
}
"background-color" => {
into.insert("backgroundColor".to_string(), val);
}
"text-align" => {
into.insert("textAlign".to_string(), val);
}
"flex-direction" | "flexDirection" => {
let orientation =
if val.as_str() == Some("column") || val.as_str() == Some("column-reverse") {
"vertical"
} else {
"horizontal"
};
into.insert(
"androidOrientation".to_string(),
serde_json::json!(orientation),
);
into.insert("flexDirection".to_string(), val);
}
_ => {
into.insert(camel_case(k), val);
}
}
}
}
pub fn merge_props(into: &mut CssProps, from: &CssProps) {
for (k, v) in from.iter() {
into.insert(k.clone(), v.clone());
}
}
pub fn split_tag_class_key(key: &str) -> Option<(String, String)> {
let mut it = key.splitn(2, '|');
let t = it.next()?.to_string();
let c = it.next()?.to_string();
if t.is_empty() || c.is_empty() {
return None;
}
Some((t, c))
}
pub fn strip_hover_suffix(selector: &str) -> (&str, bool) {
if let Some(stripped) = selector.strip_suffix(":hover") {
(stripped, true)
} else {
(selector, false)
}
}
pub fn should_emit_selector(
sel: &str,
used_tags: &IndexSet<String>,
used_classes: &IndexSet<String>,
used_tag_classes: &IndexSet<String>,
) -> bool {
let (base, _hover) = strip_hover_suffix(sel);
if is_simple_tag(base) {
return used_tags.contains(base)
|| used_tag_classes
.iter()
.any(|k: &String| k.split('|').next() == Some(base));
}
if let Some(class_name) = base.strip_prefix('.') {
return used_classes.contains(class_name)
|| used_tag_classes
.iter()
.any(|k: &String| k.ends_with(&format!("|{}", class_name)));
}
if let Some((tag, class_name)) = split_tag_class_selector(base) {
let key = format!("{}|{}", tag, class_name);
return used_tag_classes.contains(&key)
|| (used_tags.contains(&tag) && used_classes.contains(&class_name));
}
false
}
pub fn is_simple_tag(s: &str) -> bool {
let mut chars = s.chars();
match chars.next() {
Some(c) if c.is_ascii_alphabetic() => {}
_ => return false,
}
chars.all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_')
}
pub fn split_tag_class_selector(s: &str) -> Option<(String, String)> {
let mut parts = s.splitn(2, '.');
let tag = parts.next()?.to_string();
let class_name = parts.next()?.to_string();
if tag.is_empty() || class_name.is_empty() {
return None;
}
Some((tag, class_name))
}