feat: new styles
This commit is contained in:
@@ -1,8 +1,465 @@
|
||||
use std::collections::HashMap;
|
||||
use std::borrow::Cow;
|
||||
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum AttributeSelector {
|
||||
Exists(String),
|
||||
Equals(String, String),
|
||||
}
|
||||
|
||||
fn parse_attribute(input: &str) -> AttributeSelector {
|
||||
let input = input.trim();
|
||||
if let Some((name, val)) = input.split_once('=') {
|
||||
let name = name.trim().to_string();
|
||||
let val = val.trim().trim_matches('"').trim_matches('\'').to_string();
|
||||
AttributeSelector::Equals(name, val)
|
||||
} else {
|
||||
AttributeSelector::Exists(input.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CompoundSelector {
|
||||
pub tag: Option<String>,
|
||||
pub id: Option<String>,
|
||||
pub classes: Vec<String>,
|
||||
pub pseudo_classes: Vec<String>,
|
||||
pub attributes: Vec<AttributeSelector>,
|
||||
}
|
||||
|
||||
impl CompoundSelector {
|
||||
fn parse(input: &str) -> Self {
|
||||
let input = input.trim();
|
||||
let mut tag = None;
|
||||
let mut id = None;
|
||||
let mut classes = Vec::new();
|
||||
let mut pseudo_classes = Vec::new();
|
||||
let mut attributes = Vec::new();
|
||||
|
||||
let mut current = String::new();
|
||||
let mut delim: Option<char> = None;
|
||||
|
||||
for ch in input.chars() {
|
||||
match ch {
|
||||
'#' | '.' | ':' | '[' => {
|
||||
if !current.is_empty() {
|
||||
match (delim, ch) {
|
||||
(Some('.'), _) => classes.push(std::mem::take(&mut current)),
|
||||
(Some(':'), _) => pseudo_classes.push(std::mem::take(&mut current)),
|
||||
(Some('#'), _) => id = Some(std::mem::take(&mut current)),
|
||||
(Some('['), _) => {
|
||||
attributes.push(parse_attribute(&std::mem::take(&mut current)));
|
||||
}
|
||||
_ if tag.is_none() && id.is_none()
|
||||
&& classes.is_empty() && pseudo_classes.is_empty()
|
||||
&& attributes.is_empty() =>
|
||||
{
|
||||
tag = Some(std::mem::take(&mut current));
|
||||
}
|
||||
_ => current.clear(),
|
||||
}
|
||||
}
|
||||
delim = Some(ch);
|
||||
}
|
||||
']' => {
|
||||
if delim == Some('[') && !current.is_empty() {
|
||||
attributes.push(parse_attribute(&std::mem::take(&mut current)));
|
||||
delim = None;
|
||||
}
|
||||
}
|
||||
_ => current.push(ch),
|
||||
}
|
||||
}
|
||||
|
||||
if !current.is_empty() {
|
||||
match delim {
|
||||
Some('#') => id = Some(current),
|
||||
Some('.') => classes.push(current),
|
||||
Some(':') => pseudo_classes.push(current),
|
||||
Some('[') => attributes.push(parse_attribute(¤t)),
|
||||
None => tag = Some(current),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
Self { tag, id, classes, pseudo_classes, attributes }
|
||||
}
|
||||
|
||||
fn specificity(&self) -> (u32, u32, u32) {
|
||||
(
|
||||
if self.id.is_some() { 1 } else { 0 },
|
||||
self.classes.len() as u32 + self.attributes.len() as u32 + self.pseudo_classes.len() as u32,
|
||||
if self.tag.is_some() { 1 } else { 0 },
|
||||
)
|
||||
}
|
||||
|
||||
pub fn matches_element(
|
||||
&self,
|
||||
type_name: &str,
|
||||
el_id: Option<&str>,
|
||||
el_classes: &[&str],
|
||||
active_pseudo: &[&str],
|
||||
structural: &StructuralContext,
|
||||
el_attributes: &HashMap<String, String>,
|
||||
) -> bool {
|
||||
if let Some(ref t) = self.tag {
|
||||
if t != type_name && t != "*" {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if let Some(ref i) = self.id {
|
||||
match el_id {
|
||||
Some(eid) if eid == i => {}
|
||||
_ => return false,
|
||||
}
|
||||
}
|
||||
for cls in &self.classes {
|
||||
if !el_classes.contains(&cls.as_str()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
for attr in &self.attributes {
|
||||
match attr {
|
||||
AttributeSelector::Exists(name) => {
|
||||
if !el_attributes.contains_key(name) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
AttributeSelector::Equals(name, val) => {
|
||||
match el_attributes.get(name) {
|
||||
Some(v) if v == val => {}
|
||||
_ => return false,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for pc in &self.pseudo_classes {
|
||||
match pc.as_str() {
|
||||
"first-child" => {
|
||||
if structural.sibling_index != 0 { return false; }
|
||||
}
|
||||
"last-child" => {
|
||||
if structural.sibling_index + 1 != structural.sibling_total { return false; }
|
||||
}
|
||||
"first-of-type" => {
|
||||
if structural.type_index != 0 { return false; }
|
||||
}
|
||||
"empty" => {
|
||||
if structural.has_children { return false; }
|
||||
}
|
||||
"root" => {
|
||||
if !structural.is_root { return false; }
|
||||
}
|
||||
s if s.starts_with("nth-child(") => {
|
||||
let inner = &s[10..s.len().saturating_sub(1)];
|
||||
if !nth_matches(inner, structural.sibling_index + 1) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
if !active_pseudo.contains(&pc.as_str()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Combinator {
|
||||
Descendant, // Space
|
||||
Child, // >
|
||||
NextSibling, // +
|
||||
Subsequent, // ~
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ComplexSelector {
|
||||
pub compounds: Vec<CompoundSelector>,
|
||||
pub combinators: Vec<Combinator>,
|
||||
}
|
||||
|
||||
impl ComplexSelector {
|
||||
pub fn parse(input: &str) -> Self {
|
||||
let input = input.trim();
|
||||
if input.is_empty() {
|
||||
return Self { compounds: vec![], combinators: vec![] };
|
||||
}
|
||||
|
||||
let mut parts: Vec<(String, Combinator)> = Vec::new();
|
||||
let mut buf = String::new();
|
||||
let mut chars = input.chars().peekable();
|
||||
|
||||
while let Some(ch) = chars.next() {
|
||||
match ch {
|
||||
'>' | '+' | '~' => {
|
||||
let right = buf.trim().to_string();
|
||||
let combinator = match ch {
|
||||
'>' => Combinator::Child,
|
||||
'+' => Combinator::NextSibling,
|
||||
'~' => Combinator::Subsequent,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
if !right.is_empty() {
|
||||
parts.push((right, combinator));
|
||||
}
|
||||
buf.clear();
|
||||
while let Some(&c) = chars.peek() {
|
||||
if c.is_ascii_whitespace() { chars.next(); } else { break; }
|
||||
}
|
||||
}
|
||||
'#' | '.' | ':' => {
|
||||
buf.push(ch);
|
||||
}
|
||||
c if c.is_ascii_whitespace() => {
|
||||
let candidate = buf.trim().to_string();
|
||||
if !candidate.is_empty() {
|
||||
let mut peek_pos = chars.clone();
|
||||
let next_nonws = peek_pos.find(|c| !c.is_ascii_whitespace());
|
||||
match next_nonws {
|
||||
Some('>') | Some('+') | Some('~') => {
|
||||
buf.push(' ');
|
||||
}
|
||||
_ => {
|
||||
parts.push((candidate, Combinator::Descendant));
|
||||
buf.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => buf.push(ch),
|
||||
}
|
||||
}
|
||||
|
||||
let last = buf.trim().to_string();
|
||||
if !last.is_empty() {
|
||||
if parts.is_empty() {
|
||||
parts.push((last, Combinator::Descendant));
|
||||
} else {
|
||||
parts.push((last, Combinator::Descendant));
|
||||
}
|
||||
}
|
||||
|
||||
let compounds: Vec<CompoundSelector> = parts.iter().map(|(s, _)| CompoundSelector::parse(s)).collect();
|
||||
let combinators: Vec<Combinator> = parts.iter().rev().skip(1).rev().map(|(_, c)| *c).collect();
|
||||
|
||||
Self { compounds, combinators }
|
||||
}
|
||||
|
||||
fn check_compound_against(&self, i: usize, info: &AncestorInfo) -> bool {
|
||||
let compound = &self.compounds[i];
|
||||
let tag_ok = compound.tag.as_ref().map_or(true, |t| {
|
||||
t == &info.type_name || t == "*"
|
||||
});
|
||||
let id_ok = compound.id.as_ref().map_or(true, |id| {
|
||||
info.id.as_ref().map_or(false, |sid| sid == id)
|
||||
});
|
||||
let classes_ok = compound.classes.iter()
|
||||
.all(|c| info.classes.contains(c));
|
||||
tag_ok && id_ok && classes_ok
|
||||
}
|
||||
|
||||
pub fn matches(
|
||||
&self,
|
||||
type_name: &str,
|
||||
el_id: Option<&str>,
|
||||
el_classes: &[&str],
|
||||
active_pseudo: &[&str],
|
||||
structural: &StructuralContext,
|
||||
ancestors: &[AncestorInfo],
|
||||
preceding_siblings: &[AncestorInfo],
|
||||
el_attributes: &HashMap<String, String>,
|
||||
) -> bool {
|
||||
if self.compounds.is_empty() {
|
||||
return false;
|
||||
}
|
||||
|
||||
let target = self.compounds.last().unwrap();
|
||||
if !target.matches_element(type_name, el_id, el_classes, active_pseudo, structural, el_attributes) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if self.compounds.len() == 1 {
|
||||
return true;
|
||||
}
|
||||
|
||||
let mut ai = 0;
|
||||
let mut si = preceding_siblings.len();
|
||||
for i in (0..self.compounds.len() - 1).rev() {
|
||||
let combinator = self.combinators[i];
|
||||
|
||||
match combinator {
|
||||
Combinator::Descendant => {
|
||||
let mut found = false;
|
||||
for a in &ancestors[ai..] {
|
||||
if self.check_compound_against(i, a) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if !found { return false; }
|
||||
}
|
||||
Combinator::Child => {
|
||||
if ai >= ancestors.len() { return false; }
|
||||
let a = &ancestors[ai];
|
||||
if !self.check_compound_against(i, a) { return false; }
|
||||
ai += 1;
|
||||
}
|
||||
Combinator::NextSibling => {
|
||||
if si == 0 { return false; }
|
||||
let sib = &preceding_siblings[si - 1];
|
||||
if !self.check_compound_against(i, sib) { return false; }
|
||||
si -= 1;
|
||||
}
|
||||
Combinator::Subsequent => {
|
||||
let mut found = false;
|
||||
for sib in preceding_siblings[..si].iter().rev() {
|
||||
if self.check_compound_against(i, sib) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if !found { return false; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
pub fn specificity(&self) -> (u32, u32, u32) {
|
||||
let mut a = 0u32;
|
||||
let mut b = 0u32;
|
||||
let mut c = 0u32;
|
||||
for compound in &self.compounds {
|
||||
let (sa, sb, sc) = compound.specificity();
|
||||
a += sa;
|
||||
b += sb;
|
||||
c += sc;
|
||||
}
|
||||
(a, b, c)
|
||||
}
|
||||
|
||||
pub fn as_simple(&self) -> Option<&CompoundSelector> {
|
||||
if self.compounds.len() == 1 {
|
||||
self.compounds.first()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn has_pseudo_class(&self, pc: &str) -> bool {
|
||||
self.compounds.iter().any(|c| c.pseudo_classes.iter().any(|p| p == pc))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AncestorInfo {
|
||||
pub type_name: String,
|
||||
pub id: Option<String>,
|
||||
pub classes: Vec<String>,
|
||||
}
|
||||
|
||||
impl AncestorInfo {
|
||||
pub fn new(type_name: &str, classes: Vec<String>) -> Self {
|
||||
Self { type_name: type_name.to_string(), id: None, classes }
|
||||
}
|
||||
|
||||
pub fn new_with_id(type_name: &str, id: Option<String>, classes: Vec<String>) -> Self {
|
||||
Self { type_name: type_name.to_string(), id, classes }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct StructuralContext {
|
||||
pub sibling_index: usize,
|
||||
pub sibling_total: usize,
|
||||
pub type_index: usize,
|
||||
pub type_total: usize,
|
||||
pub has_children: bool,
|
||||
pub is_root: bool,
|
||||
}
|
||||
|
||||
fn nth_matches(expr: &str, n: usize) -> bool {
|
||||
let expr = expr.trim();
|
||||
|
||||
if expr.eq_ignore_ascii_case("odd") {
|
||||
return n % 2 == 1;
|
||||
}
|
||||
if expr.eq_ignore_ascii_case("even") {
|
||||
return n % 2 == 0;
|
||||
}
|
||||
|
||||
if let Ok(num) = expr.parse::<i32>() {
|
||||
return n == num as usize;
|
||||
}
|
||||
|
||||
let expr_lower = expr.to_lowercase();
|
||||
if let Some(n_pos) = expr_lower.find('n') {
|
||||
let a_str = expr_lower[..n_pos].trim();
|
||||
let b_str = expr_lower[n_pos + 1..].trim();
|
||||
|
||||
let a = if a_str.is_empty() || a_str == "+" {
|
||||
1
|
||||
} else if a_str == "-" {
|
||||
-1
|
||||
} else {
|
||||
a_str.parse::<i32>().unwrap_or(0)
|
||||
};
|
||||
|
||||
let b = if b_str.is_empty() {
|
||||
0
|
||||
} else if b_str.starts_with('+') {
|
||||
b_str[1..].trim().parse::<i32>().unwrap_or(0)
|
||||
} else if b_str.starts_with('-') {
|
||||
b_str.parse::<i32>().unwrap_or(0)
|
||||
} else {
|
||||
b_str.parse::<i32>().unwrap_or(0)
|
||||
};
|
||||
|
||||
if a == 0 {
|
||||
return n == b as usize;
|
||||
}
|
||||
|
||||
let n_i32 = n as i32;
|
||||
if a > 0 {
|
||||
let k = n_i32 - b;
|
||||
k >= 0 && k % a == 0
|
||||
} else {
|
||||
let diff = b - n_i32;
|
||||
diff >= 0 && diff % a.abs() == 0
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct StyleRule {
|
||||
pub selector: ComplexSelector,
|
||||
pub properties: HashMap<String, String>,
|
||||
}
|
||||
|
||||
impl StyleRule {
|
||||
pub fn build(selector_str: String, properties: HashMap<String, String>) -> Self {
|
||||
Self {
|
||||
selector: ComplexSelector::parse(&selector_str),
|
||||
properties,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct StyleSheet {
|
||||
index: HashMap<String, HashMap<String, String>>,
|
||||
rules: Vec<StyleRule>,
|
||||
}
|
||||
|
||||
impl StyleSheet {
|
||||
@@ -11,80 +468,374 @@ impl StyleSheet {
|
||||
}
|
||||
|
||||
pub fn add_rule(&mut self, selector: String, properties: HashMap<String, String>) {
|
||||
self.index
|
||||
.entry(selector.trim().to_string())
|
||||
.or_default()
|
||||
.extend(properties);
|
||||
for part in split_selectors(&selector) {
|
||||
self.rules.push(StyleRule::build(part, properties.clone()));
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn resolve(&self, element_type: &str) -> Option<&HashMap<String, String>> {
|
||||
self.index.get(element_type.trim())
|
||||
pub fn matching_rules<'a>(
|
||||
&'a self,
|
||||
type_name: &str,
|
||||
el_id: Option<&str>,
|
||||
el_classes: &[&str],
|
||||
active_pseudo: &[&str],
|
||||
structural: &StructuralContext,
|
||||
ancestors: &[AncestorInfo],
|
||||
preceding_siblings: &[AncestorInfo],
|
||||
el_attributes: &HashMap<String, String>,
|
||||
) -> Vec<&'a HashMap<String, String>> {
|
||||
let mut matched: Vec<(usize, &StyleRule)> = self
|
||||
.rules
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, rule)| {
|
||||
rule.selector
|
||||
.matches(type_name, el_id, el_classes, active_pseudo, structural, ancestors, preceding_siblings, el_attributes)
|
||||
})
|
||||
.collect();
|
||||
|
||||
matched.sort_by(|(i, a), (j, b)| {
|
||||
a.selector
|
||||
.specificity()
|
||||
.cmp(&b.selector.specificity())
|
||||
.then_with(|| i.cmp(j))
|
||||
});
|
||||
|
||||
matched.into_iter().map(|(_, rule)| &rule.properties).collect()
|
||||
}
|
||||
|
||||
pub fn matching_pseudo_rules(
|
||||
&self,
|
||||
pseudo: &str,
|
||||
type_name: &str,
|
||||
el_id: Option<&str>,
|
||||
el_classes: &[&str],
|
||||
structural: &StructuralContext,
|
||||
ancestors: &[AncestorInfo],
|
||||
preceding_siblings: &[AncestorInfo],
|
||||
el_attributes: &HashMap<String, String>,
|
||||
) -> HashMap<String, String> {
|
||||
let mut props = HashMap::new();
|
||||
for rule in &self.rules {
|
||||
if !rule.selector.has_pseudo_class(pseudo) {
|
||||
continue;
|
||||
}
|
||||
if !rule.selector.matches(type_name, el_id, el_classes, &[pseudo], structural, ancestors, preceding_siblings, el_attributes) {
|
||||
continue;
|
||||
}
|
||||
for (k, v) in &rule.properties {
|
||||
props.insert(k.clone(), v.clone());
|
||||
}
|
||||
}
|
||||
props
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.index.is_empty()
|
||||
self.rules.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
fn split_selectors(input: &str) -> Vec<String> {
|
||||
let mut parts = Vec::new();
|
||||
let mut depth = 0u32;
|
||||
let mut start = 0usize;
|
||||
|
||||
for (i, ch) in input.char_indices() {
|
||||
match ch {
|
||||
'(' | '[' => depth += 1,
|
||||
')' | ']' => depth = depth.saturating_sub(1),
|
||||
',' if depth == 0 => {
|
||||
let part = input[start..i].trim();
|
||||
if !part.is_empty() {
|
||||
parts.push(part.to_string());
|
||||
}
|
||||
start = i + 1;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
let last = input[start..].trim();
|
||||
if !last.is_empty() {
|
||||
parts.push(last.to_string());
|
||||
}
|
||||
|
||||
parts
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
pub enum Overflow {
|
||||
#[default]
|
||||
Visible,
|
||||
Hidden,
|
||||
Scroll,
|
||||
Auto,
|
||||
}
|
||||
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
pub enum Position {
|
||||
#[default]
|
||||
Static,
|
||||
Relative,
|
||||
Absolute,
|
||||
Sticky,
|
||||
Fixed,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
pub enum Display {
|
||||
#[default]
|
||||
Block,
|
||||
Flex,
|
||||
Grid,
|
||||
Inline,
|
||||
None,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Default)]
|
||||
pub struct ComputedStyle {
|
||||
pub font_size: Option<f32>,
|
||||
pub color: Option<iced::Color>,
|
||||
pub padding: Option<f32>,
|
||||
pub padding_top: Option<f32>,
|
||||
pub padding_right: Option<f32>,
|
||||
pub padding_bottom:Option<f32>,
|
||||
pub padding_left: Option<f32>,
|
||||
|
||||
pub margin: Option<f32>,
|
||||
pub margin_top: Option<f32>,
|
||||
pub margin_right: Option<f32>,
|
||||
pub margin_bottom: Option<f32>,
|
||||
pub margin_left: Option<f32>,
|
||||
|
||||
pub background: Option<iced::Color>,
|
||||
pub spacing: Option<f32>,
|
||||
pub border_radius: Option<f32>,
|
||||
pub border_width: Option<f32>,
|
||||
pub border_color: Option<iced::Color>,
|
||||
pub width: Option<f32>,
|
||||
|
||||
pub width: Option<iced::Length>,
|
||||
pub height: Option<iced::Length>,
|
||||
|
||||
pub min_width: Option<f32>,
|
||||
pub max_width: Option<f32>,
|
||||
pub min_height: Option<f32>,
|
||||
pub max_height: Option<f32>,
|
||||
pub direction: Option<LayoutDirection>,
|
||||
pub align_items: Option<iced::Alignment>,
|
||||
pub content_align: Option<ContentAlign>,
|
||||
|
||||
pub flex_grow: Option<u16>,
|
||||
|
||||
pub position: Option<Position>,
|
||||
pub top: Option<f32>,
|
||||
pub right: Option<f32>,
|
||||
pub bottom: Option<f32>,
|
||||
pub left: Option<f32>,
|
||||
|
||||
pub overflow_x: Option<Overflow>,
|
||||
pub overflow_y: Option<Overflow>,
|
||||
pub display: Option<Display>,
|
||||
|
||||
pub opacity: Option<f32>,
|
||||
pub font_weight: Option<u16>,
|
||||
pub line_height: Option<f32>,
|
||||
pub text_align: Option<TextAlign>,
|
||||
}
|
||||
|
||||
impl ComputedStyle {
|
||||
pub fn compute(
|
||||
inline: &HashMap<String, String>,
|
||||
sheet: Option<&HashMap<String, String>>,
|
||||
pub fn compute(
|
||||
inline: &[(Cow<'_, str>, Cow<'_, str>)],
|
||||
matched_sheets: &[&HashMap<String, String>],
|
||||
) -> Self {
|
||||
let overflow = lookup("overflow", inline, matched_sheets).and_then(parse_overflow);
|
||||
Self {
|
||||
font_size: lookup("font-size", inline, sheet).and_then(parse_size),
|
||||
color: lookup("color", inline, sheet).and_then(parse_color),
|
||||
padding: lookup("padding", inline, sheet).and_then(parse_size),
|
||||
background: lookup("background", inline, sheet)
|
||||
.or_else(|| lookup("background-color", inline, sheet))
|
||||
.and_then(parse_color),
|
||||
spacing: lookup("spacing", inline, sheet)
|
||||
.or_else(|| lookup("gap", inline, sheet))
|
||||
.and_then(parse_size),
|
||||
border_radius: lookup("border-radius", inline, sheet).and_then(parse_size),
|
||||
border_width: lookup("border-width", inline, sheet).and_then(parse_size),
|
||||
border_color: lookup("border-color", inline, sheet).and_then(parse_color),
|
||||
width: lookup("width", inline, sheet).and_then(parse_size),
|
||||
font_size: lookup("font-size", inline, matched_sheets).and_then(parse_size),
|
||||
color: lookup("color", inline, matched_sheets).and_then(parse_color),
|
||||
padding: lookup("padding", inline, matched_sheets).and_then(parse_size),
|
||||
padding_top: lookup("padding-top", inline, matched_sheets).and_then(parse_size),
|
||||
padding_right: lookup("padding-right", inline, matched_sheets).and_then(parse_size),
|
||||
padding_bottom: lookup("padding-bottom", inline, matched_sheets).and_then(parse_size),
|
||||
padding_left: lookup("padding-left", inline, matched_sheets).and_then(parse_size),
|
||||
|
||||
direction: lookup("direction", inline, sheet).and_then(parse_direction),
|
||||
align_items: lookup("align-items", inline, sheet).and_then(parse_alignment),
|
||||
content_align: lookup("content-align", inline, sheet).and_then(parse_content_align),
|
||||
margin: lookup("margin", inline, matched_sheets).and_then(parse_size),
|
||||
margin_top: lookup("margin-top", inline, matched_sheets).and_then(parse_size),
|
||||
margin_right: lookup("margin-right", inline, matched_sheets).and_then(parse_size),
|
||||
margin_bottom: lookup("margin-bottom", inline, matched_sheets).and_then(parse_size),
|
||||
margin_left: lookup("margin-left", inline, matched_sheets).and_then(parse_size),
|
||||
|
||||
background: lookup("background", inline, matched_sheets)
|
||||
.or_else(|| lookup("background-color", inline, matched_sheets))
|
||||
.and_then(parse_color),
|
||||
spacing: lookup("spacing", inline, matched_sheets)
|
||||
.or_else(|| lookup("gap", inline, matched_sheets))
|
||||
.and_then(parse_size),
|
||||
border_radius: lookup("border-radius", inline, matched_sheets).and_then(parse_size),
|
||||
border_width: lookup("border-width", inline, matched_sheets).and_then(parse_size),
|
||||
border_color: lookup("border-color", inline, matched_sheets).and_then(parse_color),
|
||||
|
||||
width: lookup("width", inline, matched_sheets).and_then(parse_length),
|
||||
height: lookup("height", inline, matched_sheets).and_then(parse_length),
|
||||
|
||||
min_width: lookup("min-width", inline, matched_sheets).and_then(parse_size),
|
||||
max_width: lookup("max-width", inline, matched_sheets).and_then(parse_size),
|
||||
min_height: lookup("min-height", inline, matched_sheets).and_then(parse_size),
|
||||
max_height: lookup("max-height", inline, matched_sheets).and_then(parse_size),
|
||||
|
||||
direction: lookup("direction", inline, matched_sheets).and_then(parse_direction),
|
||||
align_items: lookup("align-items", inline, matched_sheets).and_then(parse_alignment),
|
||||
content_align: lookup("content-align", inline, matched_sheets).and_then(parse_content_align),
|
||||
|
||||
flex_grow: lookup("flex-grow", inline, matched_sheets)
|
||||
.and_then(|s| s.trim().parse::<f32>().ok())
|
||||
.map(|v| v as u16),
|
||||
|
||||
position: lookup("position", inline, matched_sheets).and_then(parse_position),
|
||||
top: lookup("top", inline, matched_sheets).and_then(parse_size),
|
||||
right: lookup("right", inline, matched_sheets).and_then(parse_size),
|
||||
bottom: lookup("bottom", inline, matched_sheets).and_then(parse_size),
|
||||
left: lookup("left", inline, matched_sheets).and_then(parse_size),
|
||||
|
||||
overflow_x: lookup("overflow-x", inline, matched_sheets)
|
||||
.and_then(parse_overflow)
|
||||
.or(overflow),
|
||||
|
||||
overflow_y: lookup("overflow-y", inline, matched_sheets)
|
||||
.and_then(parse_overflow)
|
||||
.or(overflow),
|
||||
|
||||
display: lookup("display", inline, matched_sheets).and_then(parse_display),
|
||||
|
||||
opacity: lookup("opacity", inline, matched_sheets).and_then(parse_opacity),
|
||||
font_weight: lookup("font-weight", inline, matched_sheets).and_then(parse_font_weight),
|
||||
line_height: lookup("line-height", inline, matched_sheets).and_then(parse_size),
|
||||
text_align: lookup("text-align", inline, matched_sheets).and_then(parse_text_align),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn apply_overrides(&mut self, sheet: &HashMap<String, String>) {
|
||||
struct Override<'a>(&'a HashMap<String, String>);
|
||||
impl<'a> Override<'a> {
|
||||
fn get(&self, key: &str) -> Option<&'a str> { self.0.get(key).map(|s| s.as_str()) }
|
||||
}
|
||||
let ov = Override(sheet);
|
||||
|
||||
if let Some(v) = ov.get("font-size") { self.font_size = parse_size(v); }
|
||||
if let Some(v) = ov.get("color") { self.color = parse_color(v); }
|
||||
if let Some(v) = ov.get("padding") { self.padding = parse_size(v); }
|
||||
if let Some(v) = ov.get("padding-top") { self.padding_top = parse_size(v); }
|
||||
if let Some(v) = ov.get("padding-right") { self.padding_right = parse_size(v); }
|
||||
if let Some(v) = ov.get("padding-bottom") { self.padding_bottom = parse_size(v); }
|
||||
if let Some(v) = ov.get("padding-left") { self.padding_left = parse_size(v); }
|
||||
if let Some(v) = ov.get("margin") { self.margin = parse_size(v); }
|
||||
if let Some(v) = ov.get("margin-top") { self.margin_top = parse_size(v); }
|
||||
if let Some(v) = ov.get("margin-right") { self.margin_right = parse_size(v); }
|
||||
if let Some(v) = ov.get("margin-bottom") { self.margin_bottom = parse_size(v); }
|
||||
if let Some(v) = ov.get("margin-left") { self.margin_left = parse_size(v); }
|
||||
if let Some(v) = ov.get("background").or_else(|| ov.get("background-color")) {
|
||||
self.background = parse_color(v);
|
||||
}
|
||||
if let Some(v) = ov.get("spacing").or_else(|| ov.get("gap")) {
|
||||
self.spacing = parse_size(v);
|
||||
}
|
||||
if let Some(v) = ov.get("border-radius") { self.border_radius = parse_size(v); }
|
||||
if let Some(v) = ov.get("border-width") { self.border_width = parse_size(v); }
|
||||
if let Some(v) = ov.get("border-color") { self.border_color = parse_color(v); }
|
||||
if let Some(v) = ov.get("width") { self.width = parse_length(v); }
|
||||
if let Some(v) = ov.get("height") { self.height = parse_length(v); }
|
||||
if let Some(v) = ov.get("direction") { self.direction = parse_direction(v); }
|
||||
if let Some(v) = ov.get("align-items") { self.align_items = parse_alignment(v); }
|
||||
if let Some(v) = ov.get("content-align") { self.content_align = parse_content_align(v); }
|
||||
if let Some(v) = ov.get("flex-grow") { self.flex_grow = v.trim().parse::<f32>().ok().map(|x| x as u16); }
|
||||
if let Some(v) = ov.get("position") { self.position = parse_position(v); }
|
||||
if let Some(v) = ov.get("top") { self.top = parse_size(v); }
|
||||
if let Some(v) = ov.get("right") { self.right = parse_size(v); }
|
||||
if let Some(v) = ov.get("bottom") { self.bottom = parse_size(v); }
|
||||
if let Some(v) = ov.get("left") { self.left = parse_size(v); }
|
||||
if let Some(v) = ov.get("overflow") { let o = parse_overflow(v);
|
||||
if o.is_some() { self.overflow_x = o; self.overflow_y = o; } }
|
||||
if let Some(v) = ov.get("overflow-x") { self.overflow_x = parse_overflow(v); }
|
||||
if let Some(v) = ov.get("overflow-y") { self.overflow_y = parse_overflow(v); }
|
||||
if let Some(v) = ov.get("display") { self.display = parse_display(v); }
|
||||
if let Some(v) = ov.get("opacity") { self.opacity = parse_opacity(v); }
|
||||
if let Some(v) = ov.get("font-weight") { self.font_weight = parse_font_weight(v); }
|
||||
if let Some(v) = ov.get("line-height") { self.line_height = parse_size(v); }
|
||||
if let Some(v) = ov.get("text-align") { self.text_align = parse_text_align(v); }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[inline]
|
||||
fn lookup<'a>(
|
||||
key: &str,
|
||||
inline: &'a HashMap<String, String>,
|
||||
sheet: Option<&'a HashMap<String, String>>,
|
||||
inline: &'a [(Cow<'_, str>, Cow<'_, str>)],
|
||||
matched_sheets: &[&'a HashMap<String, String>],
|
||||
) -> Option<&'a str> {
|
||||
if let Some(v) = inline.get(key) {
|
||||
return Some(v.as_str());
|
||||
for (k, v) in inline {
|
||||
if **k == *key { return Some(v.as_ref()); }
|
||||
if k.len() == key.len() + 6
|
||||
&& k.starts_with("style:")
|
||||
&& &k[6..] == key
|
||||
{
|
||||
return Some(v.as_ref());
|
||||
}
|
||||
}
|
||||
if let Some(v) = inline.get(&format!("style:{key}")) {
|
||||
return Some(v.as_str());
|
||||
for sheet in matched_sheets.iter().rev() {
|
||||
if let Some(v) = sheet.get(key) {
|
||||
return Some(v.as_str());
|
||||
}
|
||||
}
|
||||
sheet.and_then(|s| s.get(key)).map(String::as_str)
|
||||
None
|
||||
}
|
||||
|
||||
|
||||
pub fn parse_overflow(s: &str) -> Option<Overflow> {
|
||||
let s = s.trim();
|
||||
if s.eq_ignore_ascii_case("visible") { Some(Overflow::Visible) }
|
||||
else if s.eq_ignore_ascii_case("hidden") { Some(Overflow::Hidden) }
|
||||
else if s.eq_ignore_ascii_case("scroll") { Some(Overflow::Scroll) }
|
||||
else if s.eq_ignore_ascii_case("auto") { Some(Overflow::Auto) }
|
||||
else { None }
|
||||
}
|
||||
|
||||
pub fn parse_position(s: &str) -> Option<Position> {
|
||||
let s = s.trim();
|
||||
if s.eq_ignore_ascii_case("fixed") { Some(Position::Fixed) }
|
||||
else if s.eq_ignore_ascii_case("sticky") { Some(Position::Sticky) }
|
||||
else if s.eq_ignore_ascii_case("absolute") { Some(Position::Absolute) }
|
||||
else if s.eq_ignore_ascii_case("relative") { Some(Position::Relative) }
|
||||
else if s.eq_ignore_ascii_case("static") { Some(Position::Static) }
|
||||
else { None }
|
||||
}
|
||||
|
||||
|
||||
pub fn parse_length(s: &str) -> Option<iced::Length> {
|
||||
let s = s.trim();
|
||||
|
||||
if s.eq_ignore_ascii_case("fill") || s.eq_ignore_ascii_case("100%") ||
|
||||
s.eq_ignore_ascii_case("100vw") || s.eq_ignore_ascii_case("100vh") ||
|
||||
s.eq_ignore_ascii_case("stretch") {
|
||||
return Some(iced::Length::Fill);
|
||||
}
|
||||
if s.eq_ignore_ascii_case("shrink") || s.eq_ignore_ascii_case("fit-content") || s.eq_ignore_ascii_case("auto") {
|
||||
return Some(iced::Length::Shrink);
|
||||
}
|
||||
|
||||
let val = s.trim_end_matches(|c: char| c.is_alphabetic() || c == '%').parse::<f32>().ok()?;
|
||||
|
||||
if s.ends_with('%') {
|
||||
if val >= 100.0 {
|
||||
Some(iced::Length::Fill)
|
||||
} else {
|
||||
Some(iced::Length::FillPortion(val as u16))
|
||||
}
|
||||
} else {
|
||||
Some(iced::Length::Fixed(val))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub fn parse_color(s: &str) -> Option<iced::Color> {
|
||||
let s = s.trim();
|
||||
|
||||
@@ -148,36 +899,91 @@ pub enum ContentAlign {
|
||||
End,
|
||||
}
|
||||
|
||||
pub fn parse_direction(s: &str) -> Option<LayoutDirection> {
|
||||
match s.trim().to_lowercase().as_str() {
|
||||
"horizontal" | "row" => Some(LayoutDirection::Row),
|
||||
"vertical" | "column" => Some(LayoutDirection::Column),
|
||||
"grid" => Some(LayoutDirection::Grid),
|
||||
_ => None,
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum TextAlign {
|
||||
Left,
|
||||
Center,
|
||||
Right,
|
||||
}
|
||||
|
||||
impl From<TextAlign> for iced::alignment::Horizontal {
|
||||
fn from(ta: TextAlign) -> Self {
|
||||
match ta {
|
||||
TextAlign::Left => iced::alignment::Horizontal::Left,
|
||||
TextAlign::Center => iced::alignment::Horizontal::Center,
|
||||
TextAlign::Right => iced::alignment::Horizontal::Right,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_direction(s: &str) -> Option<LayoutDirection> {
|
||||
let s = s.trim();
|
||||
if s.eq_ignore_ascii_case("horizontal") || s.eq_ignore_ascii_case("row") { Some(LayoutDirection::Row) }
|
||||
else if s.eq_ignore_ascii_case("vertical") || s.eq_ignore_ascii_case("column") { Some(LayoutDirection::Column) }
|
||||
else if s.eq_ignore_ascii_case("grid") { Some(LayoutDirection::Grid) }
|
||||
else { None }
|
||||
}
|
||||
|
||||
pub fn parse_alignment(s: &str) -> Option<iced::Alignment> {
|
||||
match s.trim().to_lowercase().as_str() {
|
||||
"start" => Some(iced::Alignment::Start),
|
||||
"center" => Some(iced::Alignment::Center),
|
||||
"end" => Some(iced::Alignment::End),
|
||||
_ => None,
|
||||
}
|
||||
let s = s.trim();
|
||||
if s.eq_ignore_ascii_case("start") { Some(iced::Alignment::Start) }
|
||||
else if s.eq_ignore_ascii_case("center") { Some(iced::Alignment::Center) }
|
||||
else if s.eq_ignore_ascii_case("end") { Some(iced::Alignment::End) }
|
||||
else { None }
|
||||
}
|
||||
|
||||
pub fn parse_content_align(s: &str) -> Option<ContentAlign> {
|
||||
match s.trim().to_lowercase().as_str() {
|
||||
"start" | "left" | "top" => Some(ContentAlign::Start),
|
||||
"center" => Some(ContentAlign::Center),
|
||||
"end" | "right" | "bottom" => Some(ContentAlign::End),
|
||||
_ => None,
|
||||
let s = s.trim();
|
||||
if s.eq_ignore_ascii_case("start") || s.eq_ignore_ascii_case("left") || s.eq_ignore_ascii_case("top") { Some(ContentAlign::Start) }
|
||||
else if s.eq_ignore_ascii_case("center") { Some(ContentAlign::Center) }
|
||||
else if s.eq_ignore_ascii_case("end") || s.eq_ignore_ascii_case("right") || s.eq_ignore_ascii_case("bottom") { Some(ContentAlign::End) }
|
||||
else { None }
|
||||
}
|
||||
|
||||
pub fn parse_display(s: &str) -> Option<Display> {
|
||||
let s = s.trim();
|
||||
if s.eq_ignore_ascii_case("none") { Some(Display::None) }
|
||||
else if s.eq_ignore_ascii_case("block") { Some(Display::Block) }
|
||||
else if s.eq_ignore_ascii_case("flex") { Some(Display::Flex) }
|
||||
else if s.eq_ignore_ascii_case("grid") { Some(Display::Grid) }
|
||||
else if s.eq_ignore_ascii_case("inline") { Some(Display::Inline) }
|
||||
else { None }
|
||||
}
|
||||
|
||||
pub fn parse_opacity(s: &str) -> Option<f32> {
|
||||
let v = s.trim().parse::<f32>().ok()?;
|
||||
Some(v.clamp(0.0, 1.0))
|
||||
}
|
||||
|
||||
pub fn parse_font_weight(s: &str) -> Option<u16> {
|
||||
let s = s.trim();
|
||||
match s {
|
||||
"normal" => Some(400),
|
||||
"bold" => Some(700),
|
||||
"lighter" => Some(300),
|
||||
"bolder" => Some(900),
|
||||
_ => s.parse::<f32>().ok().map(|v| v as u16),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_text_align(s: &str) -> Option<TextAlign> {
|
||||
let s = s.trim();
|
||||
if s.eq_ignore_ascii_case("left") { Some(TextAlign::Left) }
|
||||
else if s.eq_ignore_ascii_case("center") { Some(TextAlign::Center) }
|
||||
else if s.eq_ignore_ascii_case("right") { Some(TextAlign::Right) }
|
||||
else { None }
|
||||
}
|
||||
|
||||
pub fn parse_size(s: &str) -> Option<f32> {
|
||||
s.trim()
|
||||
.trim_end_matches(|c: char| c.is_alphabetic() || c == '%')
|
||||
let s = s.trim();
|
||||
if s.ends_with('%') || s.eq_ignore_ascii_case("auto") ||
|
||||
s.eq_ignore_ascii_case("fill") || s.eq_ignore_ascii_case("stretch") {
|
||||
return None;
|
||||
}
|
||||
if s.eq_ignore_ascii_case("vw") || s.eq_ignore_ascii_case("vh") {
|
||||
return None;
|
||||
}
|
||||
s.trim_end_matches(|c: char| c.is_alphabetic())
|
||||
.parse::<f32>()
|
||||
.ok()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user