From c048b3df8c851748eb6dcb1f093fa0d26e10fa28 Mon Sep 17 00:00:00 2001 From: Joshua Thijssen Date: Tue, 12 Mar 2024 10:56:55 +0100 Subject: [PATCH] Initial style setup --- crates/gosub_html5/src/node.rs | 6 + crates/gosub_html5/src/parser/document.rs | 8 + crates/gosub_styling/src/calculator.rs | 549 ------------------- crates/gosub_styling/src/css_colors.rs | 44 ++ crates/gosub_styling/src/css_node_tree.rs | 630 ++++++++++++++++++++++ crates/gosub_styling/src/lib.rs | 27 +- crates/gosub_styling/src/pipeline.rs | 62 --- crates/gosub_styling/src/property_list.rs | 252 ++++----- src/bin/style-parser.rs | 89 +-- 9 files changed, 889 insertions(+), 778 deletions(-) delete mode 100644 crates/gosub_styling/src/calculator.rs create mode 100644 crates/gosub_styling/src/css_node_tree.rs delete mode 100644 crates/gosub_styling/src/pipeline.rs diff --git a/crates/gosub_html5/src/node.rs b/crates/gosub_html5/src/node.rs index 69e39095e..0a4f08bff 100644 --- a/crates/gosub_html5/src/node.rs +++ b/crates/gosub_html5/src/node.rs @@ -130,6 +130,12 @@ pub struct Node { pub is_registered: bool, } +impl Node { + pub fn is_root(&self) -> bool { + self.id.is_root() + } +} + impl PartialEq for Node { fn eq(&self, other: &Node) -> bool { self.id == other.id diff --git a/crates/gosub_html5/src/parser/document.rs b/crates/gosub_html5/src/parser/document.rs index 1ccb802c5..518327993 100755 --- a/crates/gosub_html5/src/parser/document.rs +++ b/crates/gosub_html5/src/parser/document.rs @@ -339,6 +339,14 @@ impl Document { None } + /// Returns the parent node of the given node, or None when no parent is found + pub fn parent_node(&self, node: &Node) -> Option<&Node> { + match node.parent { + Some(parent_node_id) => self.get_node_by_id(parent_node_id), + None => None, + } + } + pub fn add_new_node(&mut self, node: Node) -> NodeId { // if a node contains attributes when adding to the tree, // be sure to handle the special attributes "id" and "class" diff --git a/crates/gosub_styling/src/calculator.rs b/crates/gosub_styling/src/calculator.rs deleted file mode 100644 index b30e04ed1..000000000 --- a/crates/gosub_styling/src/calculator.rs +++ /dev/null @@ -1,549 +0,0 @@ -use crate::css_colors::RgbColor; -use core::fmt::Debug; -use gosub_css3::convert::ast_converter::convert_ast_to_stylesheet; -use gosub_css3::parser_config::ParserConfig; -use gosub_css3::stylesheet::{ - CssOrigin, CssSelector, CssSelectorPart, CssSelectorType, CssStylesheet, MatcherType, - Specificity, -}; -use gosub_css3::Css3; -use gosub_html5::node::{Node, NodeId}; -use gosub_html5::parser::document::{Document, DocumentHandle, TreeIterator}; -use std::cmp::Ordering; -use std::collections::HashMap; -use std::fs; - -/// Style calculator will generate a declared values map for all nodes in the document based on the stylesheets given -pub struct StyleCalculator { - /// List of stylesheets to use for the calculation - stylesheets: Vec, - /// Document to calculate the styles for - document: DocumentHandle, - /// Map of all declared values for all nodes in the document. Will be generated by the calculator - css_map: CssMap, -} - -impl StyleCalculator { - /// Creates a new style calculator for the given document - pub fn new(document: DocumentHandle) -> Self { - let mut sheets = vec![]; - for css in document.get().stylesheets.iter() { - sheets.push(css.clone()); - } - - let mut this = Self { - stylesheets: Vec::new(), - document, - css_map: CssMap::new(), - }; - - for sheet in sheets { - this.add_stylesheet(sheet); - } - - this - } - - /// Adds another stylesheet to the calculator. Order is not important (@todo: is it?) - pub fn add_stylesheet(&mut self, stylesheet: CssStylesheet) { - self.stylesheets.push(stylesheet); - } - - /// Extracts all declared values from the stylesheets and stores them in the calculator - pub fn find_declared_values(&mut self) { - // Restart css map - self.css_map = CssMap::new(); - - // Iterate the complete document tree - let tree_iterator = TreeIterator::new(&self.document); - for current_node_id in tree_iterator { - let mut css_map_entry = CssMapEntry::new(); - - let doc = self.document.get(); - let node = doc.get_node_by_id(current_node_id).expect("node not found"); - if !node.is_element() { - continue; - } - - for sheet in self.stylesheets.iter() { - for rule in sheet.rules.iter() { - for selector in rule.selectors().iter() { - if !self.match_selector(current_node_id, selector) { - continue; - } - - // Selector matched, so we add all declared values to the map - for declaration in rule.declarations().iter() { - let property = declaration.property.clone(); - - let declaration = DeclarationProperty { - value: declaration.value.clone(), - origin: sheet.origin.clone(), - important: declaration.important, - location: "".into(), - specificity: selector.specificity(), - }; - - if let std::collections::hash_map::Entry::Vacant(e) = - css_map_entry.properties.entry(property.clone()) - { - let mut entry = ValueEntry::new(); - entry.declared.push(declaration); - e.insert(entry); - } else { - let entry = css_map_entry.properties.get_mut(&property).unwrap(); - entry.declared.push(declaration); - } - } - } - } - } - - self.css_map.nodes.insert(current_node_id, css_map_entry); - } - } - - /// Orders all declared values and finds the cascaded values - pub fn find_cascaded_values(&mut self) { - for (_, css_map_entry) in self.css_map.nodes.iter_mut() { - for (_, entry) in css_map_entry.properties.iter_mut() { - // Sort on origin and importance - entry.declared.sort(); - - // sort on specificity - entry.declared.sort_by(|a, b| { - if a.priority() != b.priority() { - return Ordering::Equal; - } - a.specificity.cmp(&b.specificity) - }); - - // @todo: sort on scoping proximity - - // order of appearance in the stylesheet. We use the last entry as the cascaded value - entry.cascaded = entry.declared.last().map(|d| d.value.clone()); - } - } - } - - pub fn find_specified_values(&mut self) { - for (_, css_map_entry) in self.css_map.nodes.iter_mut() { - for (_, entry) in css_map_entry.properties.iter_mut() { - match entry.cascaded { - Some(ref cascaded) => { - entry.specified = cascaded.clone(); - } - None => { - // @todo: find default value for this property - entry.specified = "".into(); - } - } - } - } - } - - /// Returns the list of all properties (cascaded values for now) for the given node - pub fn get_css_properties_for_node(&self, node_id: NodeId) -> Option<&CssMapEntry> { - self.css_map.nodes.get(&node_id) - } - - // Matches a complete selector (all parts) against the given node(id) - fn match_selector(&self, node_id: NodeId, selector: &CssSelector) -> bool { - let mut parts = selector.parts.clone(); - parts.reverse(); - self.match_selector_part(node_id, &mut parts) - } - - /// Returns true when the given node matches the part(s) - fn match_selector_part( - &self, - node_id: NodeId, - selector_parts: &mut Vec, - ) -> bool { - let binding = self.document.get(); - let mut next_current_node = Some(binding.get_node_by_id(node_id).expect("node not found")); - - while !selector_parts.is_empty() { - if next_current_node.is_none() { - return false; - } - let current_node = next_current_node.expect("current_node not found"); - let part = selector_parts.remove(0); - - match part.type_ { - CssSelectorType::Universal => { - // '*' always matches any selector - } - CssSelectorType::Type => { - if part.value != current_node.as_element().name { - return false; - } - } - CssSelectorType::Class => { - if !current_node.as_element().classes.contains(&part.value) { - return false; - } - } - CssSelectorType::Id => { - if current_node - .as_element() - .attributes - .get("id") - .unwrap_or(&"".to_string()) - != &part.value - { - return false; - } - } - CssSelectorType::Attribute => { - let wanted_attr_name = part.name.clone(); - - if !current_node.has_attribute(&wanted_attr_name) { - return false; - } - - let mut wanted_attr_value = part.value.clone(); - let mut got_attr_value = current_node - .get_attribute(&wanted_attr_name) - .unwrap_or(&"".to_string()) - .to_string(); - - // If we need to match case-insensitive, just convert everything to lowercase for comparison - if part.flags.eq_ignore_ascii_case("i") { - wanted_attr_value = wanted_attr_value.to_lowercase(); - got_attr_value = got_attr_value.to_lowercase(); - }; - - match part.matcher { - MatcherType::None => { - // Just the presence of the attribute is enough - return true; - } - MatcherType::Equals => { - // Exact match - return wanted_attr_value == got_attr_value; - } - MatcherType::Includes => { - // Contains word - return wanted_attr_value - .split_whitespace() - .any(|s| s == got_attr_value); - } - MatcherType::DashMatch => { - // Exact value or value followed by a hyphen - return got_attr_value == wanted_attr_value - || got_attr_value.starts_with(&format!("{}-", wanted_attr_value)); - } - MatcherType::PrefixMatch => { - // Starts with - return got_attr_value.starts_with(&wanted_attr_value); - } - MatcherType::SuffixMatch => { - // Ends with - return got_attr_value.ends_with(&wanted_attr_value); - } - MatcherType::SubstringMatch => { - // Contains - return got_attr_value.contains(&wanted_attr_value); - } - } - } - CssSelectorType::PseudoClass => { - // @Todo: implement pseudo classes - if part.value == "link" { - return false; - } - return false; - } - CssSelectorType::PseudoElement => { - // @Todo: implement pseudo elements - if part.value == "first-child" { - return false; - } - return false; - } - CssSelectorType::Combinator => { - // We don't have the descendant combinator (space), as this is the default behaviour - match part.value.as_str() { - // @todo: We also should do: column combinator ('||' experimental) - // @todo: Namespace combinator ('|') - " " => { - // Descendant combinator, any parent that matches the previous selector will do - if !self.match_selector_part(current_node.id, selector_parts) { - // we insert the combinator back so we the next loop will match against the parent node - selector_parts.insert(0, part); - } - } - ">" => { - // Child combinator. Only matches the direct child - if !self.match_selector_part(current_node.id, selector_parts) { - return false; - } - } - "+" => { - // We need to match the previous sibling of the current node - } - "~" => { - // We need to match the previous siblings of the current node - } - _ => { - panic!("Unknown combinator: {}", part.value); - } - } - } - } - - // We have matched this part, so we move up the chain - next_current_node = parent_node(&binding, current_node); - } - - // All parts of the selector have matched - true - } -} - -/// Returns the parent node of the given node, or None when no parent is found -fn parent_node<'b>(doc: &'b Document, node: &'b Node) -> Option<&'b Node> { - // Find the next element node in the parent chain. Will return None if we hit the root of the chain - let mut cur_node = node; - - loop { - let node_id = cur_node.parent; - node_id?; - - cur_node = doc - .get_node_by_id(node_id.expect("node_id")) - .expect("node not found"); - if cur_node.is_element() { - return Some(cur_node); - } - } -} - -/// Loads the default user agent stylesheet -pub fn load_default_useragent_stylesheet() -> anyhow::Result { - // @todo: we should be able to browse to gosub://useragent.css and see the actual useragent css file - let location = "gosub://useragent.css"; - let config = ParserConfig { - source: Some(String::from(location)), - ignore_errors: true, - ..Default::default() - }; - - let css = - fs::read_to_string("resources/useragent.css").expect("Could not load useragent stylesheet"); - let css_ast = Css3::parse(css.as_str(), config).expect("Could not parse useragent stylesheet"); - - convert_ast_to_stylesheet(&css_ast, CssOrigin::UserAgent, location) -} - -/// A declarationProperty defines a single value for a property (color: red;). It consists of the value, -/// origin, importance, location and specificity of the declaration. -#[derive(Debug)] -pub struct DeclarationProperty { - /// The actual value of the property - pub value: String, - /// Origin of the declaration (user stylesheet, author stylesheet etc) - pub origin: CssOrigin, - /// Whether the declaration is !important - pub important: bool, - /// The location of the declaration in the stylesheet (name.css:123) or empty - pub location: String, - /// The specificity of the selector that declared this property - pub specificity: Specificity, -} - -impl DeclarationProperty { - /// Priority of the declaration based on the origin and importance as defined in https://developer.mozilla.org/en-US/docs/Web/CSS/Cascade - fn priority(&self) -> u8 { - match self.origin { - CssOrigin::UserAgent => { - if self.important { - 7 - } else { - 1 - } - } - CssOrigin::User => { - if self.important { - 6 - } else { - 2 - } - } - CssOrigin::Author => { - if self.important { - 5 - } else { - 3 - } - } - } - } -} - -impl PartialEq for DeclarationProperty { - fn eq(&self, other: &Self) -> bool { - self.priority() == other.priority() - } -} - -impl PartialOrd for DeclarationProperty { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - -impl Eq for DeclarationProperty {} - -impl Ord for DeclarationProperty { - fn cmp(&self, other: &Self) -> Ordering { - self.priority().cmp(&other.priority()) - } -} - -/// A value entry contains all values for a single property for a single node. It contains the declared values, and -/// all the computed values. -#[derive(Debug)] -pub struct ValueEntry { - /// List of all declared values for this property - pub declared: Vec, - /// Cascaded value - pub cascaded: Option, - pub specified: String, - pub computed: String, - pub used: String, - pub actual: String, -} - -impl Default for ValueEntry { - fn default() -> Self { - Self::new() - } -} - -impl ValueEntry { - pub fn new() -> Self { - Self { - declared: Vec::new(), - cascaded: None, - specified: "".into(), - computed: "".into(), - used: "".into(), - actual: "".into(), - } - } -} - -/// Map of all declared values for a single node -pub struct CssMapEntry { - properties: HashMap, -} - -impl CssMapEntry { - pub fn new() -> Self { - Self { - properties: HashMap::new(), - } - } - - // @todo: This should not be here. When we resolve a property, we should also resolve the value so we can - // use entry.actual or something - pub fn get_color_value(&self, prop_name: &str) -> Option { - let prop_name = prop_name.to_lowercase(); - - self.properties - .get(&prop_name) - .map(|entry| RgbColor::from(entry.specified.as_ref())) - } -} - -impl Default for CssMapEntry { - fn default() -> Self { - Self::new() - } -} - -/// Map of all declared values for all nodes in the document -pub struct CssMap { - nodes: HashMap, -} - -impl CssMap { - fn new() -> Self { - Self { - nodes: HashMap::new(), - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn compare_declared() { - let a = DeclarationProperty { - value: "red".into(), - origin: CssOrigin::Author, - important: false, - location: "".into(), - specificity: Specificity::new(1, 0, 0), - }; - let b = DeclarationProperty { - value: "blue".into(), - origin: CssOrigin::UserAgent, - important: false, - location: "".into(), - specificity: Specificity::new(1, 0, 0), - }; - let c = DeclarationProperty { - value: "green".into(), - origin: CssOrigin::User, - important: false, - location: "".into(), - specificity: Specificity::new(1, 0, 0), - }; - let d = DeclarationProperty { - value: "yellow".into(), - origin: CssOrigin::Author, - important: true, - location: "".into(), - specificity: Specificity::new(1, 0, 0), - }; - let e = DeclarationProperty { - value: "orange".into(), - origin: CssOrigin::UserAgent, - important: true, - location: "".into(), - specificity: Specificity::new(1, 0, 0), - }; - let f = DeclarationProperty { - value: "purple".into(), - origin: CssOrigin::User, - important: true, - location: "".into(), - specificity: Specificity::new(1, 0, 0), - }; - - assert_eq!(3, a.priority()); - assert_eq!(1, b.priority()); - assert_eq!(2, c.priority()); - assert_eq!(5, d.priority()); - assert_eq!(7, e.priority()); - assert_eq!(6, f.priority()); - - assert!(a > b); - assert!(b < c); - assert!(c < d); - assert!(d < e); - assert!(f < e); - assert!(a < e); - assert!(b < d); - assert!(a < d); - assert!(b < d); - assert!(c < d); - assert!(c == c); - assert!(d == d); - } -} diff --git a/crates/gosub_styling/src/css_colors.rs b/crates/gosub_styling/src/css_colors.rs index f490d751b..56c120df1 100644 --- a/crates/gosub_styling/src/css_colors.rs +++ b/crates/gosub_styling/src/css_colors.rs @@ -1,14 +1,18 @@ use lazy_static::lazy_static; use std::convert::From; +use std::fmt::Debug; // Values for this table is taken from https://www.w3.org/TR/CSS21/propidx.html // Probably not the complete list, but it will do for now +/// A list of CSS color names pub struct CssColorEntry { pub name: &'static str, pub value: &'static str, } +/// A RGB color with alpha channel +#[derive(Clone, Copy, Debug, PartialEq)] pub struct RgbColor { /// Red component pub r: u8, @@ -21,6 +25,7 @@ pub struct RgbColor { } impl RgbColor { + /// Create a new color with r,g,b and alpha values pub fn new(r: u8, g: u8, b: u8, a: u8) -> Self { RgbColor { r, g, b, a } } @@ -28,6 +33,7 @@ impl RgbColor { impl Default for RgbColor { fn default() -> Self { + // Default full alpha (solid) with black color RgbColor { r: 0, g: 0, @@ -47,12 +53,19 @@ impl From<&str> for RgbColor { } if value.starts_with("rgb(") { // Rgb function + todo!() } if value.starts_with("rgba(") { // Rgba function + todo!() } if value.starts_with("hsl(") { // HSL function + todo!() + } + if value.starts_with("hsla(") { + // HSLA function + todo!() } return get_hex_color_from_name(value).map_or(RgbColor::default(), parse_hex); @@ -726,6 +739,10 @@ lazy_static! { name: "yellowgreen", value: "#9acd32", }, + CssColorEntry { + name: "rebeccapurple", + value: "#663399", + }, ]; } @@ -829,4 +846,31 @@ mod tests { assert_eq!(color.b, 0); assert_eq!(color.a, 255); } + + #[test] + fn color_names() { + let color = super::RgbColor::from("red"); + assert_eq!(color.r, 255); + assert_eq!(color.g, 0); + assert_eq!(color.b, 0); + assert_eq!(color.a, 255); + + let color = super::RgbColor::from("green"); + assert_eq!(color.r, 0); + assert_eq!(color.g, 128); + assert_eq!(color.b, 0); + assert_eq!(color.a, 255); + + let color = super::RgbColor::from("blue"); + assert_eq!(color.r, 0); + assert_eq!(color.g, 0); + assert_eq!(color.b, 255); + assert_eq!(color.a, 255); + + let color = super::RgbColor::from("rebeccapurple"); + assert_eq!(color.r, 0x66); + assert_eq!(color.g, 0x33); + assert_eq!(color.b, 0x99); + assert_eq!(color.a, 255); + } } diff --git a/crates/gosub_styling/src/css_node_tree.rs b/crates/gosub_styling/src/css_node_tree.rs new file mode 100644 index 000000000..be31d0114 --- /dev/null +++ b/crates/gosub_styling/src/css_node_tree.rs @@ -0,0 +1,630 @@ +use crate::css_colors::RgbColor; +use core::fmt::Debug; +use gosub_css3::stylesheet::{ + CssOrigin, CssSelector, CssSelectorPart, CssSelectorType, MatcherType, Specificity, +}; +use gosub_html5::node::NodeId; +use gosub_html5::parser::document::{DocumentHandle, TreeIterator}; +use gosub_shared::types::Result; +use std::cmp::Ordering; +use std::collections::HashMap; + +/// Generates a css node tree for the given document based on its loaded stylesheets +pub fn generate_css_node_tree(document: DocumentHandle) -> Result { + // Restart css map + let mut css_node_tree = CssNodeTree::new(DocumentHandle::clone(&document)); + + // Iterate the complete document tree + let tree_iterator = TreeIterator::new(&document); + for current_node_id in tree_iterator { + let mut css_map_entry = CssProperties::new(); + + let binding = document.get(); + let node = binding + .get_node_by_id(current_node_id) + .expect("node not found"); + if !node.is_element() { + continue; + } + + for sheet in document.get().stylesheets.iter() { + for rule in sheet.rules.iter() { + for selector in rule.selectors().iter() { + if !match_selector(DocumentHandle::clone(&document), current_node_id, selector) + { + continue; + } + + // Selector matched, so we add all declared values to the map + for declaration in rule.declarations().iter() { + let prop_name = declaration.property.clone(); + + let declaration = DeclarationProperty { + value: CssValue::String(declaration.value.clone()), // @TODO: parse the value into the correct CSSValue + origin: sheet.origin.clone(), + important: declaration.important, + location: sheet.location.clone(), + specificity: selector.specificity(), + }; + + if let std::collections::hash_map::Entry::Vacant(e) = + css_map_entry.properties.entry(prop_name.clone()) + { + let mut entry = CssProperty::new(prop_name.as_str()); + entry.declared.push(declaration); + e.insert(entry); + } else { + let entry = css_map_entry.properties.get_mut(&prop_name).unwrap(); + entry.declared.push(declaration); + } + } + } + } + } + + css_node_tree.nodes.insert(current_node_id, css_map_entry); + } + + for (node_id, props) in css_node_tree.nodes.iter() { + println!("Node: {:?}", node_id); + for (prop, values) in props.properties.iter() { + println!(" {}", prop); + if prop == "color" { + for decl in values.declared.iter() { + println!( + " {:?} {:?} {:?} {:?}", + decl.origin, decl.location, decl.value, decl.specificity + ); + } + } + } + } + + Ok(css_node_tree) +} + +// Matches a complete selector (all parts) against the given node(id) +fn match_selector(document: DocumentHandle, node_id: NodeId, selector: &CssSelector) -> bool { + let mut parts = selector.parts.clone(); + parts.reverse(); + match_selector_part(document, node_id, &mut parts) +} + +/// Returns true when the given node matches the part(s) +fn match_selector_part( + document: DocumentHandle, + node_id: NodeId, + selector_parts: &mut Vec, +) -> bool { + let binding = document.get(); + let mut next_current_node = Some(binding.get_node_by_id(node_id).expect("node not found")); + + while !selector_parts.is_empty() { + if next_current_node.is_none() { + return false; + } + let current_node = next_current_node.expect("current_node not found"); + if current_node.is_root() { + return false; + } + + let part = selector_parts.remove(0); + + match part.type_ { + CssSelectorType::Universal => { + // '*' always matches any selector + } + CssSelectorType::Type => { + if part.value != current_node.as_element().name { + return false; + } + } + CssSelectorType::Class => { + if !current_node.as_element().classes.contains(&part.value) { + return false; + } + } + CssSelectorType::Id => { + if current_node + .as_element() + .attributes + .get("id") + .unwrap_or(&"".to_string()) + != &part.value + { + return false; + } + } + CssSelectorType::Attribute => { + let wanted_attr_name = part.name.clone(); + + if !current_node.has_attribute(&wanted_attr_name) { + return false; + } + + let mut wanted_attr_value = part.value.clone(); + let mut got_attr_value = current_node + .get_attribute(&wanted_attr_name) + .unwrap_or(&"".to_string()) + .to_string(); + + // If we need to match case-insensitive, just convert everything to lowercase for comparison + if part.flags.eq_ignore_ascii_case("i") { + wanted_attr_value = wanted_attr_value.to_lowercase(); + got_attr_value = got_attr_value.to_lowercase(); + }; + + return match part.matcher { + MatcherType::None => { + // Just the presence of the attribute is enough + true + } + MatcherType::Equals => { + // Exact match + wanted_attr_value == got_attr_value + } + MatcherType::Includes => { + // Contains word + wanted_attr_value + .split_whitespace() + .any(|s| s == got_attr_value) + } + MatcherType::DashMatch => { + // Exact value or value followed by a hyphen + got_attr_value == wanted_attr_value + || got_attr_value.starts_with(&format!("{}-", wanted_attr_value)) + } + MatcherType::PrefixMatch => { + // Starts with + got_attr_value.starts_with(&wanted_attr_value) + } + MatcherType::SuffixMatch => { + // Ends with + got_attr_value.ends_with(&wanted_attr_value) + } + MatcherType::SubstringMatch => { + // Contains + got_attr_value.contains(&wanted_attr_value) + } + }; + } + CssSelectorType::PseudoClass => { + // @Todo: implement pseudo classes + if part.value == "link" { + return false; + } + return false; + } + CssSelectorType::PseudoElement => { + // @Todo: implement pseudo elements + if part.value == "first-child" { + return false; + } + return false; + } + CssSelectorType::Combinator => { + // We don't have the descendant combinator (space), as this is the default behaviour + match part.value.as_str() { + // @todo: We also should do: column combinator ('||' experimental) + // @todo: Namespace combinator ('|') + " " => { + // Descendant combinator, any parent that matches the previous selector will do + if !match_selector_part( + DocumentHandle::clone(&document), + current_node.id, + selector_parts, + ) { + // we insert the combinator back so we the next loop will match against the parent node + selector_parts.insert(0, part); + } + } + ">" => { + // Child combinator. Only matches the direct child + if !match_selector_part( + DocumentHandle::clone(&document), + current_node.id, + selector_parts, + ) { + return false; + } + } + "+" => { + // We need to match the previous sibling of the current node + } + "~" => { + // We need to match the previous siblings of the current node + } + _ => { + panic!("Unknown combinator: {}", part.value); + } + } + } + } + + // We have matched this part, so we move up the chain + // let binding = document.get(); + next_current_node = binding.parent_node(current_node); + } + + // All parts of the selector have matched + true +} + +/// A declarationProperty defines a single value for a property (color: red;). It consists of the value, +/// origin, importance, location and specificity of the declaration. +#[derive(Debug, Clone)] +pub struct DeclarationProperty { + /// The actual value of the property + pub value: CssValue, + /// Origin of the declaration (user stylesheet, author stylesheet etc.) + pub origin: CssOrigin, + /// Whether the declaration is !important + pub important: bool, + /// The location of the declaration in the stylesheet (name.css:123) or empty + pub location: String, + /// The specificity of the selector that declared this property + pub specificity: Specificity, +} + +impl DeclarationProperty { + /// Priority of the declaration based on the origin and importance as defined in https://developer.mozilla.org/en-US/docs/Web/CSS/Cascade + fn priority(&self) -> u8 { + match self.origin { + CssOrigin::UserAgent => { + if self.important { + 7 + } else { + 1 + } + } + CssOrigin::User => { + if self.important { + 6 + } else { + 2 + } + } + CssOrigin::Author => { + if self.important { + 5 + } else { + 3 + } + } + } + } +} + +impl PartialEq for DeclarationProperty { + fn eq(&self, other: &Self) -> bool { + self.priority() == other.priority() + } +} + +impl PartialOrd for DeclarationProperty { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Eq for DeclarationProperty {} + +impl Ord for DeclarationProperty { + fn cmp(&self, other: &Self) -> Ordering { + self.priority().cmp(&other.priority()) + } +} + +/// A value entry contains all values for a single property for a single node. It contains the declared values, and +/// all the computed values. +#[derive(Debug, Clone)] +pub struct CssProperty { + /// The name of the property + pub name: String, + /// True when this property needs to be recalculated + pub dirty: bool, + /// List of all declared values for this property + pub declared: Vec, + /// Cascaded value from the declared values (if any) + pub cascaded: Option, + // Specified value from the cascaded value (if any), or inherited value, or initial value + pub specified: CssValue, + // Computed value from the specified value (needs viewport size etc.) + pub computed: CssValue, + pub used: CssValue, + // Actual value used in the rendering (after rounding, clipping etc.) + pub actual: CssValue, +} + +impl CssProperty { + pub fn new(prop_name: &str) -> Self { + Self { + name: prop_name.to_string(), + dirty: true, + declared: Vec::new(), + cascaded: None, + specified: CssValue::None, + computed: CssValue::None, + used: CssValue::None, + actual: CssValue::None, + } + } + + pub fn mark_dirty(&mut self) { + self.dirty = true; + } + + pub fn mark_clean(&mut self) { + self.dirty = false; + } + + /// Returns the actual value of the property. Will compute the value when needed + pub fn compute_value(&mut self) -> &CssValue { + if self.dirty { + self.calculate_value(); + self.dirty = false; + } + + &self.actual + } + + fn calculate_value(&mut self) { + self.cascaded = self.find_cascaded_value(); + self.specified = self.find_specified_value(); + self.computed = self.find_computed_value(); + self.used = self.find_used_value(); + self.actual = self.find_actual_value(); + } + + fn find_cascaded_value(&self) -> Option { + let mut declared = self.declared.clone(); + + declared.sort(); + declared.sort_by(|a, b| { + if a.priority() == b.priority() { + return Ordering::Equal; + } + + a.specificity.cmp(&b.specificity) + }); + + declared.last().map(|d| d.value.clone()) + } + + fn find_specified_value(&self) -> CssValue { + match self.declared.iter().max() { + Some(decl) => decl.value.clone(), + None => CssValue::None, + } + } + + fn find_computed_value(&self) -> CssValue { + if self.specified != CssValue::None { + return self.specified.clone(); + } + + if self.is_inheritable() { + todo!("inheritable properties") + // while let Some(parent) = self.get_parent() { + // if let Some(parent_value) = parent { + // return parent_value.find_computed_value(); + // } + // } + } + + self.get_initial_value().unwrap_or(CssValue::None) + } + + fn find_used_value(&self) -> CssValue { + self.computed.clone() + } + + fn find_actual_value(&self) -> CssValue { + // @TODO: stuff like clipping and such should occur as well + match &self.used { + CssValue::Number(len) => CssValue::Number(len.round()), + CssValue::Percentage(perc) => CssValue::Percentage(perc.round()), + CssValue::Unit(value, unit) => CssValue::Unit(value.round(), unit.clone()), + _ => self.used.clone(), + } + } + + // Returns true when the property is inheritable, false otherwise + fn is_inheritable(&self) -> bool { + crate::property_list::PROPERTY_TABLE + .iter() + .find(|entry| entry.name == self.name) + .map(|entry| entry.inheritable) + .unwrap_or(false) + } + + // Returns the initial value for the property, if any + fn get_initial_value(&self) -> Option { + crate::property_list::PROPERTY_TABLE + .iter() + .find(|entry| entry.name == self.name) + .map(|entry| entry.initial.clone()) + } +} + +/// Map of all declared values for a single node. Note that these are only the defined properties, not +/// the non-existing properties. +pub struct CssProperties { + properties: HashMap, +} + +impl Default for CssProperties { + fn default() -> Self { + Self::new() + } +} + +impl CssProperties { + pub fn new() -> Self { + Self { + properties: HashMap::new(), + } + } +} + +/// Actual CSS value, can be a color, length, percentage, string or unit. Some relative values will be computed +/// from other values (ie: Percent(50) will convert to Length(100) when the parent width is 200) +#[derive(Debug, Clone, PartialEq)] +pub enum CssValue { + None, + Color(RgbColor), + Number(f32), + Percentage(f32), + String(String), + Unit(f32, String), +} + +impl CssValue { + /// Converts the value to a string + pub fn to_string(&self) -> String { + match self { + CssValue::None => "".into(), + CssValue::Color(col) => { + format!("#{:02x}{:02x}{:02x}{:02x}", col.r, col.g, col.b, col.a) + } + CssValue::Number(num) => format!("{}", num), + CssValue::Percentage(p) => format!("{}%", p), + CssValue::String(s) => s.clone(), + CssValue::Unit(val, unit) => format!("{}{}", val, unit), + } + } +} + +/// Map of all declared values for all nodes in the document +pub struct CssNodeTree { + nodes: HashMap, + document: DocumentHandle, +} + +impl CssNodeTree { + /// Creates a new CssNodeTree for the given document + pub fn new(doc: DocumentHandle) -> Self { + Self { + document: doc, + nodes: HashMap::new(), + } + } + + /// Returns a clone of the document handle + pub fn get_document(&self) -> DocumentHandle { + DocumentHandle::clone(&self.document) + } + + /// Mark the given node as dirty, so it will be recalculated + pub fn mark_dirty(&mut self, node_id: NodeId) { + match self.nodes.get_mut(&node_id) { + Some(props) => { + for prop in props.properties.values_mut() { + prop.mark_dirty(); + } + } + None => {} + } + } + + /// Mark the given node as clean, so it will not be recalculated + pub fn mark_clean(&mut self, node_id: NodeId) { + match self.nodes.get_mut(&node_id) { + Some(props) => { + for prop in props.properties.values_mut() { + prop.mark_clean(); + } + } + None => {} + } + } + + /// Retrieves the property for the given node, or None when not found + pub fn get_property(&self, node_id: NodeId, prop_name: &str) -> Option { + let props = self.nodes.get(&node_id); + if props.is_none() { + return None; + } + + match props.expect("props").properties.get(prop_name) { + Some(entry) => Some(entry.clone()), + None => None, + } + } + + /// Retrieves the value for the given property for the given node, or None when not found + pub fn get_all_properties(&self, node_id: NodeId) -> Option<&CssProperties> { + self.nodes.get(&node_id) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn compare_declared() { + let a = DeclarationProperty { + value: CssValue::String("red".into()), + origin: CssOrigin::Author, + important: false, + location: "".into(), + specificity: Specificity::new(1, 0, 0), + }; + let b = DeclarationProperty { + value: CssValue::String("blue".into()), + origin: CssOrigin::UserAgent, + important: false, + location: "".into(), + specificity: Specificity::new(1, 0, 0), + }; + let c = DeclarationProperty { + value: CssValue::String("green".into()), + origin: CssOrigin::User, + important: false, + location: "".into(), + specificity: Specificity::new(1, 0, 0), + }; + let d = DeclarationProperty { + value: CssValue::String("yellow".into()), + origin: CssOrigin::Author, + important: true, + location: "".into(), + specificity: Specificity::new(1, 0, 0), + }; + let e = DeclarationProperty { + value: CssValue::String("orange".into()), + origin: CssOrigin::UserAgent, + important: true, + location: "".into(), + specificity: Specificity::new(1, 0, 0), + }; + let f = DeclarationProperty { + value: CssValue::String("purple".into()), + origin: CssOrigin::User, + important: true, + location: "".into(), + specificity: Specificity::new(1, 0, 0), + }; + + assert_eq!(3, a.priority()); + assert_eq!(1, b.priority()); + assert_eq!(2, c.priority()); + assert_eq!(5, d.priority()); + assert_eq!(7, e.priority()); + assert_eq!(6, f.priority()); + + assert!(a > b); + assert!(b < c); + assert!(c < d); + assert!(d < e); + assert!(f < e); + assert!(a < e); + assert!(b < d); + assert!(a < d); + assert!(b < d); + assert!(c < d); + assert_eq!(c, c); + assert_eq!(d, d); + } +} diff --git a/crates/gosub_styling/src/lib.rs b/crates/gosub_styling/src/lib.rs index 4e84ae4bb..756e4f4c2 100644 --- a/crates/gosub_styling/src/lib.rs +++ b/crates/gosub_styling/src/lib.rs @@ -3,6 +3,29 @@ //! This crate connects CSS3 and HTML5 into a styling pipeline //! -pub mod calculator; +use gosub_css3::convert::ast_converter::convert_ast_to_stylesheet; +use gosub_css3::parser_config::ParserConfig; +use gosub_css3::stylesheet::{CssOrigin, CssStylesheet}; +use gosub_css3::Css3; +use std::fs; + pub mod css_colors; -pub mod pipeline; +pub mod css_node_tree; +mod property_list; + +/// Loads the default user agent stylesheet +pub fn load_default_useragent_stylesheet() -> anyhow::Result { + // @todo: we should be able to browse to gosub://useragent.css and see the actual useragent css file + let location = "gosub://useragent.css"; + let config = ParserConfig { + source: Some(String::from(location)), + ignore_errors: true, + ..Default::default() + }; + + let css = + fs::read_to_string("resources/useragent.css").expect("Could not load useragent stylesheet"); + let css_ast = Css3::parse(css.as_str(), config).expect("Could not parse useragent stylesheet"); + + convert_ast_to_stylesheet(&css_ast, CssOrigin::UserAgent, location) +} diff --git a/crates/gosub_styling/src/pipeline.rs b/crates/gosub_styling/src/pipeline.rs deleted file mode 100644 index 209a825a8..000000000 --- a/crates/gosub_styling/src/pipeline.rs +++ /dev/null @@ -1,62 +0,0 @@ -use crate::calculator::StyleCalculator; -use gosub_html5::node::NodeId; -use gosub_html5::parser::document::DocumentHandle; - -/// The rendering pipeline to convert a document and stylesheets into a rendered page -/// It's a very simple pipeline with a single step (generate_render_tree). But more -/// will follow later. -pub struct Pipeline {} - -impl Default for Pipeline { - fn default() -> Self { - Self::new() - } -} - -impl Pipeline { - pub fn new() -> Self { - Self {} - } - - /// Generates a render tree by duplicating the DOM tree and removing all nodes that are not renderable or hidden. - pub fn generate_render_tree( - &self, - doc_handle: DocumentHandle, - _calculator: &StyleCalculator, - ) -> DocumentHandle { - // Create a complete copy of the document tree into the render tree - let mut rendertree_handle = doc_handle.deep_clone(); - - // Iterate tree and remove elements that are hidden or not renderable - remove_unrenderable_nodes(&mut rendertree_handle, NodeId::root(), _calculator); - - DocumentHandle::clone(&rendertree_handle) - } -} - -fn remove_unrenderable_nodes( - rendertree_handle: &mut DocumentHandle, - node_id: NodeId, - _calculator: &StyleCalculator, -) { - let node; - { - let binding = rendertree_handle.get(); - node = binding.get_node_by_id(node_id).unwrap().clone(); - } - - // There are more elements that are not renderable, but for now we only remove the most common ones - let removable_elements = ["head", "script", "style", "svg"]; - - if node.is_element() && removable_elements.contains(&node.as_element().name.as_str()) { - rendertree_handle.get_mut().delete_node(&node); - return; - } - - // Check CSS styles and remove if not renderable - - // Iterate all children from this node - for &child_id in &node.children { - remove_unrenderable_nodes(rendertree_handle, child_id, _calculator); - } -} diff --git a/crates/gosub_styling/src/property_list.rs b/crates/gosub_styling/src/property_list.rs index eb8ea1181..17e7203ff 100644 --- a/crates/gosub_styling/src/property_list.rs +++ b/crates/gosub_styling/src/property_list.rs @@ -1,592 +1,576 @@ +use crate::css_node_tree::CssValue; use lazy_static::lazy_static; // Values for this table is taken from https://www.w3.org/TR/CSS21/propidx.html // Probably not the complete list, but it will do for now -struct PropertyTableEntry { - name: &'static str, - initial: &'static str, - inheritable: bool, +pub struct PropertyTableEntry { + pub(crate) name: &'static str, + pub(crate) initial: CssValue, + pub(crate) inheritable: bool, } lazy_static! { - static ref PROPERTY_TABLE: &'static [PropertyTableEntry] = &[ + pub static ref PROPERTY_TABLE: &'static [PropertyTableEntry] = &[ PropertyTableEntry { name: "azimuth", - initial: "center", + initial: CssValue::String("center".into()), inheritable: true, }, PropertyTableEntry { name: "background-attachment", - initial: "scroll", + initial: CssValue::String("scroll".into()), inheritable: false, }, PropertyTableEntry { name: "background-color", - initial: "transparent", + initial: CssValue::String("transparent".into()), inheritable: false, }, PropertyTableEntry { name: "background-image", - initial: "none", + initial: CssValue::String("none".into()), inheritable: false, }, PropertyTableEntry { name: "background-position", - initial: "0% 0%", + initial: CssValue::String("0% 0%".into()), inheritable: false, }, PropertyTableEntry { name: "background-repeat", - initial: "repeat", + initial: CssValue::String("repeat".into()), inheritable: false, }, PropertyTableEntry { name: "border-collapse", - initial: "separate", + initial: CssValue::String("separate".into()), inheritable: false, }, PropertyTableEntry { name: "border-color", - initial: "initial", + initial: CssValue::String("initial".into()), inheritable: false, }, PropertyTableEntry { name: "border-spacing", - initial: "0", + initial: CssValue::Number(0_f32), inheritable: false, }, PropertyTableEntry { name: "border-style", - initial: "none", + initial: CssValue::String("none".into()), inheritable: false, }, PropertyTableEntry { name: "border-top", - initial: "initial", + initial: CssValue::String("initial".into()), inheritable: false, }, PropertyTableEntry { name: "border-right", - initial: "initial", + initial: CssValue::String("initial".into()), inheritable: false, }, PropertyTableEntry { name: "border-bottom", - initial: "initial", + initial: CssValue::String("initial".into()), inheritable: false, }, PropertyTableEntry { name: "border-left", - initial: "initial", + initial: CssValue::String("initial".into()), inheritable: false, }, PropertyTableEntry { name: "border-top-color", - initial: "initial", + initial: CssValue::String("initial".into()), inheritable: false, }, PropertyTableEntry { name: "border-right-color", - initial: "initial", + initial: CssValue::String("initial".into()), inheritable: false, }, PropertyTableEntry { name: "border-bottom-color", - initial: "initial", + initial: CssValue::String("initial".into()), inheritable: false, }, PropertyTableEntry { name: "border-left-color", - initial: "initial", + initial: CssValue::String("initial".into()), inheritable: false, }, PropertyTableEntry { name: "border-top-style", - initial: "none", + initial: CssValue::String("none".into()), inheritable: false, }, PropertyTableEntry { name: "border-right-style", - initial: "none", + initial: CssValue::String("none".into()), inheritable: false, }, PropertyTableEntry { name: "border-bottom-style", - initial: "none", + initial: CssValue::String("none".into()), inheritable: false, }, PropertyTableEntry { name: "border-left-style", - initial: "none", + initial: CssValue::String("none".into()), inheritable: false, }, PropertyTableEntry { name: "border-top-width", - initial: "medium", + initial: CssValue::String("medium".into()), inheritable: false, }, PropertyTableEntry { name: "border-right-width", - initial: "medium", + initial: CssValue::String("medium".into()), inheritable: false, }, PropertyTableEntry { name: "border-bottom-width", - initial: "medium", + initial: CssValue::String("medium".into()), inheritable: false, }, PropertyTableEntry { name: "border-left-width", - initial: "medium", + initial: CssValue::String("medium".into()), inheritable: false, }, PropertyTableEntry { name: "border-width", - initial: "initial", + initial: CssValue::String("initial".into()), inheritable: false, }, PropertyTableEntry { name: "bottom", - initial: "auto", + initial: CssValue::String("auto".into()), inheritable: false, }, PropertyTableEntry { name: "caption-side", - initial: "top", + initial: CssValue::String("top".into()), inheritable: false, }, PropertyTableEntry { name: "clear", - initial: "none", + initial: CssValue::String("none".into()), inheritable: false, }, PropertyTableEntry { name: "clip", - initial: "auto", + initial: CssValue::String("auto".into()), inheritable: false, }, PropertyTableEntry { name: "color", - initial: "initial", + initial: CssValue::String("initial".into()), inheritable: true, }, PropertyTableEntry { name: "content", - initial: "normal", + initial: CssValue::String("normal".into()), inheritable: false, }, PropertyTableEntry { name: "counter-increment", - initial: "none", + initial: CssValue::String("none".into()), inheritable: false, }, PropertyTableEntry { name: "counter-reset", - initial: "none", + initial: CssValue::String("none".into()), inheritable: false, }, PropertyTableEntry { name: "cue", - initial: "none", + initial: CssValue::String("none".into()), inheritable: false, }, PropertyTableEntry { name: "cue-after", - initial: "none", + initial: CssValue::String("none".into()), inheritable: false, }, PropertyTableEntry { name: "cue-before", - initial: "none", + initial: CssValue::String("none".into()), inheritable: false, }, PropertyTableEntry { name: "cursor", - initial: "auto", + initial: CssValue::String("auto".into()), inheritable: false, }, PropertyTableEntry { name: "direction", - initial: "ltr", + initial: CssValue::String("ltr".into()), inheritable: true, }, PropertyTableEntry { name: "display", - initial: "inline", + initial: CssValue::String("inline".into()), inheritable: false, }, PropertyTableEntry { name: "elevation", - initial: "level", + initial: CssValue::String("level".into()), inheritable: false, }, PropertyTableEntry { name: "empty-cells", - initial: "show", + initial: CssValue::String("show".into()), inheritable: false, }, PropertyTableEntry { name: "float", - initial: "none", + initial: CssValue::String("none".into()), inheritable: false, }, PropertyTableEntry { name: "font", - initial: "initial", + initial: CssValue::String("initial".into()), inheritable: false, }, PropertyTableEntry { name: "font-family", - initial: "initial", + initial: CssValue::String("initial".into()), inheritable: false, }, PropertyTableEntry { name: "font-size", - initial: "medium", + initial: CssValue::String("medium".into()), inheritable: true, }, PropertyTableEntry { name: "font-style", - initial: "normal", + initial: CssValue::String("normal".into()), inheritable: true, }, PropertyTableEntry { name: "font-variant", - initial: "normal", + initial: CssValue::String("normal".into()), inheritable: true, }, PropertyTableEntry { name: "font-weight", - initial: "normal", + initial: CssValue::String("normal".into()), inheritable: true, }, PropertyTableEntry { name: "height", - initial: "auto", + initial: CssValue::String("auto".into()), inheritable: false, }, PropertyTableEntry { name: "left", - initial: "auto", + initial: CssValue::String("auto".into()), inheritable: false, }, PropertyTableEntry { name: "letter-spacing", - initial: "normal", + initial: CssValue::String("normal".into()), inheritable: true, }, PropertyTableEntry { name: "line-height", - initial: "normal", + initial: CssValue::String("normal".into()), inheritable: true, }, PropertyTableEntry { name: "list-style", - initial: "initial", + initial: CssValue::String("initial".into()), inheritable: false, }, PropertyTableEntry { name: "list-style-image", - initial: "none", + initial: CssValue::String("none".into()), inheritable: false, }, PropertyTableEntry { name: "list-style-position", - initial: "outside", + initial: CssValue::String("outside".into()), inheritable: false, }, PropertyTableEntry { name: "list-style-type", - initial: "disc", + initial: CssValue::String("disc".into()), inheritable: false, }, PropertyTableEntry { name: "margin", - initial: "0", + initial: CssValue::Number(0_f32), inheritable: false, }, PropertyTableEntry { name: "margin-top", - initial: "0", + initial: CssValue::Number(0_f32), inheritable: false, }, PropertyTableEntry { name: "margin-right", - initial: "0", + initial: CssValue::Number(0_f32), inheritable: false, }, PropertyTableEntry { name: "margin-bottom", - initial: "0", + initial: CssValue::Number(0_f32), inheritable: false, }, PropertyTableEntry { name: "margin-left", - initial: "0", + initial: CssValue::Number(0_f32), inheritable: false, }, PropertyTableEntry { name: "max-height", - initial: "none", + initial: CssValue::String("none".into()), inheritable: false, }, PropertyTableEntry { name: "max-width", - initial: "none", + initial: CssValue::String("none".into()), inheritable: false, }, PropertyTableEntry { name: "min-height", - initial: "0", + initial: CssValue::Number(0_f32), inheritable: false, }, PropertyTableEntry { name: "min-width", - initial: "0", + initial: CssValue::Number(0_f32), inheritable: false, }, PropertyTableEntry { name: "orphans", - initial: "2", + initial: CssValue::Number(2_f32), inheritable: true, }, PropertyTableEntry { name: "outline", - initial: "initial", + initial: CssValue::String("initial".into()), inheritable: false, }, PropertyTableEntry { name: "outline-color", - initial: "invert", + initial: CssValue::String("invert".into()), inheritable: false, }, PropertyTableEntry { name: "outline-style", - initial: "none", + initial: CssValue::String("none".into()), inheritable: false, }, PropertyTableEntry { name: "outline-width", - initial: "medium", + initial: CssValue::String("medium".into()), inheritable: false, }, PropertyTableEntry { name: "overflow", - initial: "visible", + initial: CssValue::String("visible".into()), inheritable: false, }, PropertyTableEntry { name: "padding", - initial: "0", + initial: CssValue::Number(0_f32), inheritable: false, }, PropertyTableEntry { name: "padding-top", - initial: "0", + initial: CssValue::Number(0_f32), inheritable: false, }, PropertyTableEntry { name: "padding-right", - initial: "0", + initial: CssValue::Number(0_f32), inheritable: false, }, PropertyTableEntry { name: "padding-bottom", - initial: "0", + initial: CssValue::Number(0_f32), inheritable: false, }, PropertyTableEntry { name: "padding-left", - initial: "0", + initial: CssValue::Number(0_f32), inheritable: false, }, PropertyTableEntry { name: "page-break-after", - initial: "auto", + initial: CssValue::String("auto".into()), inheritable: false, }, PropertyTableEntry { name: "page-break-before", - initial: "auto", + initial: CssValue::String("auto".into()), inheritable: false, }, PropertyTableEntry { name: "page-break-inside", - initial: "auto", + initial: CssValue::String("auto".into()), inheritable: false, }, PropertyTableEntry { name: "pause-after", - initial: "0", + initial: CssValue::Number(0_f32), inheritable: false, }, PropertyTableEntry { name: "pause-before", - initial: "0", + initial: CssValue::Number(0_f32), inheritable: false, }, PropertyTableEntry { name: "pitch", - initial: "medium", + initial: CssValue::String("medium".into()), inheritable: false, }, PropertyTableEntry { name: "pitch-range", - initial: "50", + initial: CssValue::Number(50_f32), inheritable: false, }, PropertyTableEntry { name: "play-during", - initial: "auto", + initial: CssValue::String("auto".into()), inheritable: false, }, PropertyTableEntry { name: "position", - initial: "static", + initial: CssValue::String("static".into()), inheritable: false, }, PropertyTableEntry { name: "quotes", - initial: "initial", + initial: CssValue::String("initial".into()), inheritable: false, }, PropertyTableEntry { name: "richness", - initial: "50", + initial: CssValue::Number(50_f32), inheritable: false, }, PropertyTableEntry { name: "right", - initial: "auto", + initial: CssValue::String("auto".into()), inheritable: false, }, PropertyTableEntry { name: "speak", - initial: "normal", + initial: CssValue::String("normal".into()), inheritable: false, }, PropertyTableEntry { name: "speak-header", - initial: "once", + initial: CssValue::String("once".into()), inheritable: false, }, PropertyTableEntry { name: "speak-numeral", - initial: "continuous", + initial: CssValue::String("continuous".into()), inheritable: false, }, PropertyTableEntry { name: "speak-punctuation", - initial: "none", + initial: CssValue::String("none".into()), inheritable: false, }, PropertyTableEntry { name: "speech-rate", - initial: "medium", + initial: CssValue::String("medium".into()), inheritable: false, }, PropertyTableEntry { name: "stress", - initial: "50", + initial: CssValue::Number(50_f32), inheritable: false, }, PropertyTableEntry { name: "table-layout", - initial: "auto", + initial: CssValue::String("auto".into()), inheritable: false, }, PropertyTableEntry { name: "text-align", - initial: "initial", + initial: CssValue::String("initial".into()), inheritable: true, }, PropertyTableEntry { name: "text-decoration", - initial: "none", + initial: CssValue::String("none".into()), inheritable: true, }, PropertyTableEntry { name: "text-indent", - initial: "0", + initial: CssValue::Number(0_f32), inheritable: true, }, PropertyTableEntry { name: "text-transform", - initial: "none", + initial: CssValue::String("none".into()), inheritable: true, }, PropertyTableEntry { name: "top", - initial: "auto", + initial: CssValue::String("auto".into()), inheritable: false, }, PropertyTableEntry { name: "unicode-bidi", - initial: "normal", + initial: CssValue::String("normal".into()), inheritable: true, }, PropertyTableEntry { name: "vertical-align", - initial: "baseline", + initial: CssValue::String("baseline".into()), inheritable: true, }, PropertyTableEntry { name: "visibility", - initial: "visible", + initial: CssValue::String("visible".into()), inheritable: false, }, PropertyTableEntry { name: "voice-family", - initial: "initial", + initial: CssValue::String("initial".into()), inheritable: false, }, PropertyTableEntry { name: "volume", - initial: "medium", + initial: CssValue::String("medium".into()), inheritable: false, }, PropertyTableEntry { name: "white-space", - initial: "normal", + initial: CssValue::String("normal".into()), inheritable: true, }, PropertyTableEntry { name: "widows", - initial: "2", + initial: CssValue::Number(2_f32), inheritable: true, }, PropertyTableEntry { name: "width", - initial: "auto", + initial: CssValue::String("auto".into()), inheritable: false, }, PropertyTableEntry { name: "word-spacing", - initial: "normal", + initial: CssValue::String("normal".into()), inheritable: true, }, PropertyTableEntry { name: "z-index", - initial: "auto", + initial: CssValue::String("auto".into()), inheritable: false, }, ]; } - -#[allow(dead_code)] -fn get_initial_value(property: &str) -> Option<&'static str> { - PROPERTY_TABLE - .iter() - .find(|entry| entry.name == property) - .map(|entry| entry.initial) -} - -#[allow(dead_code)] -fn is_inheritable(property: &str) -> bool { - PROPERTY_TABLE - .iter() - .find(|entry| entry.name == property) - .map(|entry| entry.inheritable) - .unwrap_or(false) -} diff --git a/src/bin/style-parser.rs b/src/bin/style-parser.rs index 459894e40..432abe3a1 100644 --- a/src/bin/style-parser.rs +++ b/src/bin/style-parser.rs @@ -10,21 +10,20 @@ use gosub_html5::parser::document::{visit, Document}; use gosub_html5::parser::Html5Parser; use gosub_html5::visit::Visitor; use gosub_shared::bytes::{CharIterator, Confidence, Encoding}; -use gosub_styling::calculator::StyleCalculator; -use gosub_styling::pipeline::Pipeline; +use gosub_styling::css_node_tree::{generate_css_node_tree, CssNodeTree, CssValue}; use std::fs; use url::Url; struct TextVisitor { color: String, - calculator: StyleCalculator, + css_nodetree: CssNodeTree, } impl TextVisitor { - fn new(calculator: StyleCalculator) -> Self { + fn new(css_node_tree: CssNodeTree) -> Self { Self { color: String::from(""), - calculator, + css_nodetree: css_node_tree, } } } @@ -63,30 +62,60 @@ impl Visitor for TextVisitor { fn comment_leave(&mut self, _node: &Node, _data: &CommentData) {} fn element_enter(&mut self, node: &Node, data: &ElementData) { - let props = self.calculator.get_css_properties_for_node(node.id); - if props.is_some() { - let props = props.unwrap(); - if let Some(col) = props.get_color_value("color") { - print!("\x1b[38;2;{};{};{}m", col.r, col.g, col.b); - } - if let Some(col) = props.get_color_value("background-color") { - print!("\x1b[48;2;{};{};{}m", col.r, col.g, col.b); + match self.css_nodetree.get_property(node.id, "color") { + Some(mut prop) => { + match prop.compute_value() { + CssValue::Color(col) => { + println!("Color EL ENTER: {:?}", col); + // print!("\x1b[38;2;{};{};{}m", col.r, col.g, col.b) + self.color = format!("\x1b[38;2;{};{};{}m", col.r, col.g, col.b) + } + _ => {} + } } + _ => {} + } + + match self.css_nodetree.get_property(node.id, "background-color") { + Some(mut prop) => match prop.compute_value() { + CssValue::Color(col) => { + println!("BGCOL EL ENTER: {:?}", col); + + print!("\x1b[48;2;{};{};{}m", col.r, col.g, col.b) + } + _ => {} + }, + _ => {} } print!("<{}>", data.name); } fn element_leave(&mut self, node: &Node, data: &ElementData) { - let props = self.calculator.get_css_properties_for_node(node.id); - if props.is_some() { - let props = props.unwrap(); - if let Some(col) = props.get_color_value("color") { - print!("\x1b[38;2;{};{};{}m", col.r, col.g, col.b); - } - if let Some(col) = props.get_color_value("background-color") { - print!("\x1b[48;2;{};{};{}m", col.r, col.g, col.b); + match self.css_nodetree.get_property(node.id, "color") { + Some(mut prop) => { + match prop.compute_value() { + CssValue::Color(col) => { + println!("COL EL LEAVE: {:?}", col); + + self.color = format!("\x1b[38;2;{};{};{}m", col.r, col.g, col.b) + // print!("\x1b[38;2;{};{};{}m", col.r, col.g, col.b); + } + _ => {} + } } + _ => {} + } + + match self.css_nodetree.get_property(node.id, "background-color") { + Some(mut prop) => match prop.compute_value() { + CssValue::Color(col) => { + println!("BGCOL EL LEAVE: {:?}", col); + print!("\x1b[48;2;{};{};{}m", col.r, col.g, col.b) + } + _ => {} + }, + _ => {} } print!("", data.name); @@ -132,23 +161,21 @@ fn main() -> Result<()> { let _parse_errors = Html5Parser::parse_document(&mut chars, Document::clone(&doc_handle), None)?; - // Create stylesheet calculator and load default user agent stylesheets - let mut calculator = StyleCalculator::new(Document::clone(&doc_handle)); - // calculator.add_stylesheet(load_default_useragent_stylesheet()?); + let css_tree = generate_css_node_tree(Document::clone(&doc_handle))?; // pipeline - calculator.find_declared_values(); - calculator.find_cascaded_values(); - calculator.find_specified_values(); + // calculator.find_declared_values(); + // calculator.find_cascaded_values(); + // calculator.find_specified_values(); // calculator.find_computed_values(1024, 786); // Do we need more info? // calculator.find_used_values(/*layout*/); // we need to have a layout for calculating these values // calculator.find_actual_values(); // Makes sure we use 2px instead of a computed 2.25px - let pipeline = Pipeline::new(); - let render_tree = pipeline.generate_render_tree(Document::clone(&doc_handle), &calculator); + // let pipeline = Pipeline::new(); + // let render_tree = pipeline.generate_render_tree(css_tree); - let mut visitor = Box::new(TextVisitor::new(calculator)) as Box>; - visit(&Document::clone(&render_tree), &mut visitor); + let mut visitor = Box::new(TextVisitor::new(css_tree)) as Box>; + visit(&Document::clone(&doc_handle), &mut visitor); Ok(()) }