use std::collections::HashMap;
use super::super::ThemeError;
pub( super ) fn extract_colors_map( value: &serde_json::Value )
-> Result<HashMap<String, String>, ThemeError>
{
let mut out = HashMap::new();
let raw = match value.get( "colors" )
{
Some( c ) => c,
None => return Ok( out ),
};
let map = raw.as_object().ok_or_else( ||
ThemeError::InvalidColor( "`colors` must be an object".to_string() )
)?;
for ( k, v ) in map
{
let s = v.as_str().ok_or_else( ||
ThemeError::InvalidColor( format!( "`colors.{}` must be a hex string", k ) )
)?;
let h = s.trim_start_matches( '#' );
let valid = ( h.len() == 6 || h.len() == 8 ) && h.chars().all( |c| c.is_ascii_hexdigit() );
if !valid
{
return Err( ThemeError::InvalidColor(
format!( "`colors.{}` = `{}` (expected #RRGGBB or #RRGGBBAA)", k, s )
));
}
out.insert( k.clone(), s.to_string() );
}
Ok( out )
}
pub( super ) fn extract_gradients_map( value: &serde_json::Value )
-> Result<HashMap<String, serde_json::Value>, ThemeError>
{
extract_token_map( value, "gradients", "paint object", serde_json::Value::is_object )
}
pub( super ) fn extract_inset_stacks_map( value: &serde_json::Value )
-> Result<HashMap<String, serde_json::Value>, ThemeError>
{
extract_token_map( value, "inset_stacks", "array of inset-shadow definitions", serde_json::Value::is_array )
}
fn extract_token_map(
value: &serde_json::Value,
section: &str,
expected_kind: &str,
valid: impl Fn( &serde_json::Value ) -> bool,
) -> Result<HashMap<String, serde_json::Value>, ThemeError>
{
let mut out = HashMap::new();
let raw = match value.get( section )
{
Some( c ) => c,
None => return Ok( out ),
};
let map = raw.as_object().ok_or_else( ||
ThemeError::InvalidColor( format!( "`{}` must be an object", section ) )
)?;
for ( k, v ) in map
{
if !valid( v )
{
return Err( ThemeError::InvalidColor(
format!( "`{}.{}` must be {}", section, k, expected_kind )
));
}
out.insert( k.clone(), v.clone() );
}
Ok( out )
}
pub( super ) fn resolve_refs(
value: &mut serde_json::Value,
colors: &HashMap<String, String>,
tokens: &HashMap<String, serde_json::Value>,
) -> Result<(), ThemeError>
{
let replacement = match value
{
serde_json::Value::String( s ) => match s.strip_prefix( '@' )
{
Some( rest ) => Some( resolve_one_ref( rest, colors, tokens )? ),
None => None,
},
_ => None,
};
if let Some( new_value ) = replacement
{
*value = new_value;
resolve_refs( value, colors, tokens )?;
return Ok( () );
}
match value
{
serde_json::Value::Object( map ) =>
{
for ( _, v ) in map.iter_mut()
{
resolve_refs( v, colors, tokens )?;
}
}
serde_json::Value::Array( arr ) =>
{
for v in arr.iter_mut()
{
resolve_refs( v, colors, tokens )?;
}
}
_ => {}
}
Ok( () )
}
fn resolve_one_ref(
rest: &str,
colors: &HashMap<String, String>,
tokens: &HashMap<String, serde_json::Value>,
) -> Result<serde_json::Value, ThemeError>
{
let ( name, alpha_hex ) = match rest.split_once( '/' )
{
Some( ( n, a ) ) => ( n, Some( a ) ),
None => ( rest, None ),
};
if let Some( tok ) = tokens.get( name )
{
if let Some( a ) = alpha_hex
{
return Err( ThemeError::InvalidColor( format!(
"@{} is a paint/inset token — alpha override `/{}` is not applicable", name, a
)));
}
return Ok( tok.clone() );
}
let base = colors.get( name )
.ok_or_else( || ThemeError::UnknownColorRef( name.to_string() ) )?;
let h = base.trim_start_matches( '#' );
let rgb = &h[0..6];
let s = match alpha_hex
{
Some( a ) =>
{
if a.len() != 2 || u8::from_str_radix( a, 16 ).is_err()
{
return Err( ThemeError::InvalidColor(
format!( "@{}/{} (alpha must be two hex digits)", name, a )
));
}
format!( "#{}{}", rgb.to_uppercase(), a.to_uppercase() )
}
None => format!( "#{}", h.to_uppercase() ),
};
Ok( serde_json::Value::String( s ) )
}