use serde::{Deserialize, Serialize};
use crate::{Timeline, Track};
pub const TEXT_STYLES_KEY: &str = "textStyles";
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct TextStyle {
pub name: String,
pub declarations: String,
}
impl TextStyle {
pub fn new(name: impl Into<String>, declarations: impl Into<String>) -> Self {
TextStyle {
name: name.into(),
declarations: declarations.into(),
}
}
pub fn normalized(&self) -> Option<TextStyle> {
let name = self.name.trim();
let declarations = self.declarations.trim();
if !is_valid_text_style_name(name) || !is_valid_text_style_declarations(declarations) {
return None;
}
Some(TextStyle {
name: name.to_string(),
declarations: declarations.to_string(),
})
}
pub fn is_valid(&self) -> bool {
self.normalized().is_some()
}
pub fn to_css_rule(&self) -> String {
format!(".{} {{ {} }}", self.name, self.declarations)
}
}
pub fn is_valid_text_style_name(name: &str) -> bool {
let mut chars = name.chars().peekable();
let first = match chars.next() {
Some(c) => c,
None => return false,
};
let valid_first = match first {
'_' => true,
'-' => matches!(chars.peek(), Some(c) if c.is_ascii_alphabetic() || *c == '_' || *c == '-'),
c => c.is_ascii_alphabetic(),
};
valid_first && chars.all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-')
}
pub fn is_valid_text_style_declarations(declarations: &str) -> bool {
!declarations.is_empty() && !declarations.contains('{') && !declarations.contains('}')
}
pub fn text_styles_css(styles: &[TextStyle]) -> String {
styles
.iter()
.map(TextStyle::to_css_rule)
.collect::<Vec<_>>()
.join("\n")
}
pub fn resolve_text_styles(metadata: &serde_json::Value) -> Vec<TextStyle> {
raw_text_styles(metadata)
.map(|entries| {
entries
.iter()
.filter_map(|(name, raw)| {
let declarations = raw.as_str()?;
TextStyle::new(name.as_str(), declarations).normalized()
})
.collect()
})
.unwrap_or_default()
}
pub fn set_text_style(metadata: &mut serde_json::Value, style: TextStyle) -> bool {
let Some(style) = style.normalized() else {
return false;
};
text_styles_object_mut(metadata)
.insert(style.name, serde_json::Value::String(style.declarations));
true
}
pub fn remove_text_style(metadata: &mut serde_json::Value, name: &str) -> bool {
let Some(ai) = metadata
.get_mut("tellers.ai")
.and_then(|value| value.as_object_mut())
else {
return false;
};
let Some(styles) = ai.get_mut(TEXT_STYLES_KEY).and_then(|v| v.as_object_mut()) else {
return false;
};
let removed = styles.remove(name.trim()).is_some();
if styles.is_empty() {
ai.remove(TEXT_STYLES_KEY);
}
removed
}
impl Timeline {
pub fn get_text_styles(&self) -> Vec<TextStyle> {
resolve_text_styles(&self.metadata)
}
pub fn get_text_style(&self, name: &str) -> Option<String> {
let wanted = name.trim();
self.get_text_styles()
.into_iter()
.find(|style| style.name == wanted)
.map(|style| style.declarations)
}
pub fn set_text_style(
&mut self,
name: impl Into<String>,
declarations: impl Into<String>,
) -> bool {
set_text_style(&mut self.metadata, TextStyle::new(name, declarations))
}
pub fn remove_text_style(&mut self, name: &str) -> bool {
remove_text_style(&mut self.metadata, name)
}
pub fn text_styles_css(&self) -> String {
text_styles_css(&self.get_text_styles())
}
}
pub fn merge_text_styles(base: &[TextStyle], overrides: &[TextStyle]) -> Vec<TextStyle> {
let mut merged: Vec<TextStyle> = base.to_vec();
for style in overrides {
match merged
.iter_mut()
.find(|existing| existing.name == style.name)
{
Some(existing) => existing.declarations = style.declarations.clone(),
None => merged.push(style.clone()),
}
}
merged
}
impl Track {
pub fn get_text_styles(&self) -> Vec<TextStyle> {
resolve_text_styles(&self.metadata)
}
pub fn get_text_style(&self, name: &str) -> Option<String> {
let wanted = name.trim();
self.get_text_styles()
.into_iter()
.find(|style| style.name == wanted)
.map(|style| style.declarations)
}
pub fn set_text_style(
&mut self,
name: impl Into<String>,
declarations: impl Into<String>,
) -> bool {
set_text_style(&mut self.metadata, TextStyle::new(name, declarations))
}
pub fn remove_text_style(&mut self, name: &str) -> bool {
remove_text_style(&mut self.metadata, name)
}
}
impl Timeline {
pub fn get_track_text_styles(&self, track_id: &str) -> Option<Vec<TextStyle>> {
let (_, track) = self.tracks.get_track_by_id(track_id)?;
Some(merge_text_styles(
&self.get_text_styles(),
&track.get_text_styles(),
))
}
pub fn track_text_styles_css(&self, track_id: &str) -> Option<String> {
self.get_track_text_styles(track_id)
.map(|styles| text_styles_css(&styles))
}
pub fn set_track_text_style(
&mut self,
track_id: &str,
name: impl Into<String>,
declarations: impl Into<String>,
) -> bool {
match self.track_mut_by_id(track_id) {
Some(track) => track.set_text_style(name, declarations),
None => false,
}
}
pub fn remove_track_text_style(&mut self, track_id: &str, name: &str) -> bool {
match self.track_mut_by_id(track_id) {
Some(track) => track.remove_text_style(name),
None => false,
}
}
fn track_mut_by_id(&mut self, track_id: &str) -> Option<&mut Track> {
let (index, _) = self.tracks.get_track_by_id(track_id)?;
self.tracks.children.get_mut(index)
}
}
fn raw_text_styles(
metadata: &serde_json::Value,
) -> Option<&serde_json::Map<String, serde_json::Value>> {
metadata
.get("tellers.ai")?
.get(TEXT_STYLES_KEY)?
.as_object()
}
fn text_styles_object_mut(
metadata: &mut serde_json::Value,
) -> &mut serde_json::Map<String, serde_json::Value> {
if metadata.as_object().is_none() {
*metadata = serde_json::Value::Object(serde_json::Map::new());
}
let map = metadata.as_object_mut().unwrap();
let ai = map
.entry("tellers.ai".to_string())
.or_insert_with(|| serde_json::Value::Object(serde_json::Map::new()));
if ai.as_object().is_none() {
*ai = serde_json::Value::Object(serde_json::Map::new());
}
let ai_map = ai.as_object_mut().unwrap();
let styles = ai_map
.entry(TEXT_STYLES_KEY.to_string())
.or_insert_with(|| serde_json::Value::Object(serde_json::Map::new()));
if styles.as_object().is_none() {
*styles = serde_json::Value::Object(serde_json::Map::new());
}
styles.as_object_mut().unwrap()
}