From 8a72219cdcf52a83c29627c311dc29ef73d22d22 Mon Sep 17 00:00:00 2001 From: Joshua Thijssen Date: Tue, 23 Jan 2024 17:16:49 +0100 Subject: [PATCH] initial style commit --- Cargo.lock | 59 +- Cargo.toml | 1 + Makefile | 5 +- benches/tree_iterator.rs | 4 +- crates/gosub_bindings/src/lib.rs | 2 +- crates/gosub_css3/Cargo.toml | 1 + crates/gosub_css3/src/convert.rs | 1 + .../gosub_css3/src/convert/ast_converter.rs | 185 +++ crates/gosub_css3/src/cssom/cssrule.rs | 198 +++ crates/gosub_css3/src/cssom/cssrule_list.rs | 11 + crates/gosub_css3/src/cssom/stylesheet.rs | 62 + .../gosub_css3/src/cssom/stylesheet_list.rs | 11 + crates/gosub_css3/src/lib.rs | 4 +- crates/gosub_css3/src/node.rs | 286 +++- crates/gosub_css3/src/parser/at_rule.rs | 10 +- .../gosub_css3/src/parser/at_rule/import.rs | 42 +- crates/gosub_css3/src/parser/selector.rs | 137 +- crates/gosub_css3/src/parser_config.rs | 5 +- crates/gosub_css3/src/stylesheet.rs | 206 +++ crates/gosub_css3/src/tokenizer.rs | 4 + crates/gosub_css3/src/walker.rs | 10 +- crates/gosub_html5/Cargo.toml | 4 + crates/gosub_html5/src/lib.rs | 1 + crates/gosub_html5/src/node.rs | 80 +- crates/gosub_html5/src/node/arena.rs | 30 +- crates/gosub_html5/src/node/data/comment.rs | 2 +- crates/gosub_html5/src/node/data/element.rs | 8 +- crates/gosub_html5/src/node/data/text.rs | 2 +- crates/gosub_html5/src/parser.rs | 240 ++- crates/gosub_html5/src/parser/document.rs | 150 +- crates/gosub_html5/src/visit.rs | 22 + crates/gosub_styling/Cargo.toml | 14 + crates/gosub_styling/src/calculator.rs | 549 +++++++ crates/gosub_styling/src/css_colors.rs | 800 ++++++++++ crates/gosub_styling/src/lib.rs | 8 + crates/gosub_styling/src/pipeline.rs | 62 + crates/gosub_styling/src/property_list.rs | 592 ++++++++ crates/gosub_styling/src/shorthands.rs | 312 ++++ .../src/testing/tree_construction.rs | 4 +- crates/gosub_webexecutor/src/test.rs | 9 +- docs/css_styles.md | 76 + examples/html5-parser.rs | 2 +- resources/useragent.css | 1304 +++++++++++++++++ src/bin/gosub-parser.rs | 40 +- src/bin/style-parser.rs | 154 ++ src/bin/test-user-agent.rs | 2 +- src/engine.rs | 4 +- test.html | 37 + 48 files changed, 5524 insertions(+), 228 deletions(-) create mode 100644 crates/gosub_css3/src/convert.rs create mode 100644 crates/gosub_css3/src/convert/ast_converter.rs create mode 100644 crates/gosub_css3/src/cssom/cssrule.rs create mode 100644 crates/gosub_css3/src/cssom/cssrule_list.rs create mode 100644 crates/gosub_css3/src/cssom/stylesheet.rs create mode 100644 crates/gosub_css3/src/cssom/stylesheet_list.rs create mode 100644 crates/gosub_css3/src/stylesheet.rs create mode 100644 crates/gosub_html5/src/visit.rs create mode 100644 crates/gosub_styling/Cargo.toml create mode 100644 crates/gosub_styling/src/calculator.rs create mode 100644 crates/gosub_styling/src/css_colors.rs create mode 100644 crates/gosub_styling/src/lib.rs create mode 100644 crates/gosub_styling/src/pipeline.rs create mode 100644 crates/gosub_styling/src/property_list.rs create mode 100644 crates/gosub_styling/src/shorthands.rs create mode 100644 docs/css_styles.md create mode 100644 resources/useragent.css create mode 100644 src/bin/style-parser.rs create mode 100644 test.html diff --git a/Cargo.lock b/Cargo.lock index dcf7d29d9..f6c9b7f2c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -129,7 +129,7 @@ checksum = "c980ee35e870bd1a4d2c8294d4c04d0499e67bca1e4b5cefcc693c2fa00caea9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.49", + "syn 2.0.50", ] [[package]] @@ -182,9 +182,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.15.0" +version = "3.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d32a994c2b3ca201d9b263612a374263f05e7adde37c4707f693dcd375076d1f" +checksum = "a3b1be7772ee4501dba05acbe66bb1e8760f6a6c474a36035631638e4415f130" [[package]] name = "bytecount" @@ -206,12 +206,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.0.83" +version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" -dependencies = [ - "libc", -] +checksum = "7f9fa1897e4325be0d68d48df6aa1a71ac2ed4d27723887e7754192705350730" [[package]] name = "cfg-if" @@ -293,7 +290,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.49", + "syn 2.0.50", ] [[package]] @@ -506,7 +503,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.49", + "syn 2.0.50", ] [[package]] @@ -669,6 +666,7 @@ dependencies = [ name = "gosub_css3" version = "0.1.0" dependencies = [ + "anyhow", "gosub_shared", "lazy_static", "log", @@ -690,6 +688,7 @@ dependencies = [ "gosub_jsapi", "gosub_net", "gosub_shared", + "gosub_styling", "gosub_testing", "gosub_webexecutor", "lazy_static", @@ -708,10 +707,14 @@ name = "gosub_html5" version = "0.1.0" dependencies = [ "derive_more", + "gosub_css3", "gosub_shared", "lazy_static", + "log", "phf", "thiserror", + "ureq", + "url", ] [[package]] @@ -759,6 +762,18 @@ dependencies = [ "url", ] +[[package]] +name = "gosub_styling" +version = "0.1.0" +dependencies = [ + "anyhow", + "gosub_css3", + "gosub_html5", + "gosub_shared", + "lazy_static", + "regex", +] + [[package]] name = "gosub_testing" version = "0.1.0" @@ -1229,7 +1244,7 @@ dependencies = [ "phf_shared", "proc-macro2", "quote", - "syn 2.0.49", + "syn 2.0.50", ] [[package]] @@ -1551,7 +1566,7 @@ checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.49", + "syn 2.0.50", ] [[package]] @@ -1681,9 +1696,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.49" +version = "2.0.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "915aea9e586f80826ee59f8453c1101f9d1c4b3964cd2460185ee8e299ada496" +checksum = "74f1bdc9872430ce9b75da68329d1c1746faf50ffac5f19e02b71e37ff881ffb" dependencies = [ "proc-macro2", "quote", @@ -1708,7 +1723,7 @@ dependencies = [ "cfg-if", "proc-macro2", "quote", - "syn 2.0.49", + "syn 2.0.50", ] [[package]] @@ -1719,7 +1734,7 @@ checksum = "5c89e72a01ed4c579669add59014b9a524d609c0c88c6a585ce37485879f6ffb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.49", + "syn 2.0.50", "test-case-core", ] @@ -1749,7 +1764,7 @@ checksum = "a953cb265bef375dae3de6663da4d3804eee9682ea80d8e2542529b73c531c81" dependencies = [ "proc-macro2", "quote", - "syn 2.0.49", + "syn 2.0.50", ] [[package]] @@ -1845,7 +1860,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.49", + "syn 2.0.50", ] [[package]] @@ -1877,9 +1892,9 @@ checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" [[package]] name = "unicode-normalization" -version = "0.1.22" +version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" dependencies = [ "tinyvec", ] @@ -1998,7 +2013,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.49", + "syn 2.0.50", "wasm-bindgen-shared", ] @@ -2020,7 +2035,7 @@ checksum = "642f325be6301eb8107a83d12a8ac6c1e1c54345a7ef1a9261962dfefda09e66" dependencies = [ "proc-macro2", "quote", - "syn 2.0.49", + "syn 2.0.50", "wasm-bindgen-backend", "wasm-bindgen-shared", ] diff --git a/Cargo.toml b/Cargo.toml index bfd1c3277..2eb9583f0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -44,6 +44,7 @@ gosub_net = { path = "./crates/gosub_net", features = [] } gosub_config = { path = "./crates/gosub_config", features = [] } gosub_html5 = { path = "./crates/gosub_html5", features = [] } gosub_css3 = { path = "./crates/gosub_css3", features = [] } +gosub_styling = { path = "./crates/gosub_styling", features = [] } gosub_webexecutor = { path = "./crates/gosub_webexecutor", features = [] } gosub_jsapi = { path = "./crates/gosub_jsapi", features = [] } gosub_testing = { path = "./crates/gosub_testing", features = [] } diff --git a/Makefile b/Makefile index 275f3b94d..ac6defd1d 100644 --- a/Makefile +++ b/Makefile @@ -14,9 +14,6 @@ build: ## Build the project section "Cargo build" ;\ cargo build --all -fix: ## Fix formatting and clippy errors (deprecated) - echo "Use 'make format' instead" - format: ## Fix formatting and clippy errors cargo fmt --all cargo clippy --all --fix --allow-dirty --allow-staged @@ -40,7 +37,7 @@ test_commands: cargo run --bin html5-parser-test >/dev/null cargo run --bin parser-test >/dev/null cargo run --bin config-store list >/dev/null - cargo run --bin gosub-parser ./tests/data/tree_iterator/stackoverflow.html >/dev/null + cargo run --bin gosub-parser file://tests/data/tree_iterator/stackoverflow.html >/dev/null cargo run --example html5-parser >/dev/null help: ## Display available commands diff --git a/benches/tree_iterator.rs b/benches/tree_iterator.rs index 664162c56..551363874 100644 --- a/benches/tree_iterator.rs +++ b/benches/tree_iterator.rs @@ -18,7 +18,7 @@ fn wikipedia_main_page(c: &mut Criterion) { let _ = char_iter.read_from_file(html_file, Some(gosub_shared::bytes::Encoding::UTF8)); char_iter.set_confidence(gosub_shared::bytes::Confidence::Certain); - let main_document = DocumentBuilder::new_document(); + let main_document = DocumentBuilder::new_document(None); let document = Document::clone(&main_document); let _ = Html5Parser::parse_document(&mut char_iter, document, None); @@ -45,7 +45,7 @@ fn stackoverflow_home(c: &mut Criterion) { let _ = char_iter.read_from_file(html_file, Some(gosub_shared::bytes::Encoding::UTF8)); char_iter.set_confidence(gosub_shared::bytes::Confidence::Certain); - let main_document = DocumentBuilder::new_document(); + let main_document = DocumentBuilder::new_document(None); let document = Document::clone(&main_document); let _ = Html5Parser::parse_document(&mut char_iter, document, None); diff --git a/crates/gosub_bindings/src/lib.rs b/crates/gosub_bindings/src/lib.rs index fe8fcb8c0..567d23bbc 100644 --- a/crates/gosub_bindings/src/lib.rs +++ b/crates/gosub_bindings/src/lib.rs @@ -34,7 +34,7 @@ pub unsafe extern "C" fn gosub_rendertree_init(html: *const c_char) -> *mut Rend chars.read_from_str(html_str, Some(Encoding::UTF8)); chars.set_confidence(Confidence::Certain); - let doc = DocumentBuilder::new_document(); + let doc = DocumentBuilder::new_document(None); let parse_result = Html5Parser::parse_document(&mut chars, Document::clone(&doc), None); if parse_result.is_ok() { diff --git a/crates/gosub_css3/Cargo.toml b/crates/gosub_css3/Cargo.toml index 4c75d7f19..c8b1968d4 100644 --- a/crates/gosub_css3/Cargo.toml +++ b/crates/gosub_css3/Cargo.toml @@ -10,4 +10,5 @@ gosub_shared = { path = "../gosub_shared", features = [] } lazy_static = "1.4" log = "0.4.20" simple_logger = "4.2.0" +anyhow = { version = "1.0.80", features = [] } diff --git a/crates/gosub_css3/src/convert.rs b/crates/gosub_css3/src/convert.rs new file mode 100644 index 000000000..4637be32d --- /dev/null +++ b/crates/gosub_css3/src/convert.rs @@ -0,0 +1 @@ +pub mod ast_converter; diff --git a/crates/gosub_css3/src/convert/ast_converter.rs b/crates/gosub_css3/src/convert/ast_converter.rs new file mode 100644 index 000000000..1116776b5 --- /dev/null +++ b/crates/gosub_css3/src/convert/ast_converter.rs @@ -0,0 +1,185 @@ +use crate::node::{Node as CssNode, NodeType}; +use crate::stylesheet::{ + CssDeclaration, CssOrigin, CssRule, CssSelector, CssSelectorPart, CssSelectorType, + CssStylesheet, MatcherType, +}; +use anyhow::anyhow; +use gosub_shared::types::Result; + +/* + +Given the following css: + + * { color: red; } + h1 { color: blue; } + h3, h4 { color: rebeccapurple; } + ul > li { color: green; } + +this will parse to an AST, which this function turns into the following structure: + +CssStylesheet + Rule + SelectorList + SelectorGroup + Selector: Universal * + Rule + SelectorList + SelectorGroup + part: Ident h1 + Rule + SelectorList + Selector + part: Ident h3 + Selector + part: Ident h4 + Rule + SelectorList + Selector + part: Ident ul + part: Combinator > + part: Ident li + +In case of h3, h4, the SelectorList contains two entries in the SelectorList, each with a single Selector. But having 2 rules with each one single +selector list entry would have been the same thing: + + Rule + SelectorList + Selector + part: Ident h3 + Rule + SelectorList + Selector + part: Ident h4 + +in css: + h3, h4 { color: rebeccapurple; } +vs + h3 { color: rebeccapurple; } + h4 { color: rebeccapurple; } + +*/ + +/// Converts a CSS AST to a CSS stylesheet structure +pub fn convert_ast_to_stylesheet( + css_ast: &CssNode, + origin: CssOrigin, + location: &str, +) -> Result { + if !css_ast.is_stylesheet() { + return Err(anyhow!("CSS AST must start with a stylesheet node")); + } + + let mut sheet = CssStylesheet { + rules: vec![], + origin, + location: location.to_string(), + }; + + for node in css_ast.as_stylesheet() { + if !node.is_rule() { + continue; + } + + let mut rule = CssRule { + selectors: vec![], + declarations: vec![], + }; + + let (prelude, declarations) = node.as_rule(); + for node in prelude.iter() { + if !node.is_selector_list() { + continue; + } + + let mut selector = CssSelector { parts: vec![] }; + + for node in node.as_selector_list().iter() { + if !node.is_selector() { + continue; + } + + for node in node.as_selector() { + let part = match &*node.node_type { + NodeType::Ident { value } => CssSelectorPart { + type_: CssSelectorType::Type, + value: value.clone(), + ..Default::default() + }, + NodeType::ClassSelector { value } => CssSelectorPart { + type_: CssSelectorType::Class, + value: value.clone(), + ..Default::default() + }, + NodeType::Combinator { value } => CssSelectorPart { + type_: CssSelectorType::Combinator, + value: value.clone(), + ..Default::default() + }, + NodeType::IdSelector { value } => CssSelectorPart { + type_: CssSelectorType::Id, + value: value.clone(), + ..Default::default() + }, + NodeType::TypeSelector { value, .. } if value == "*" => CssSelectorPart { + type_: CssSelectorType::Universal, + value: "*".to_string(), + ..Default::default() + }, + NodeType::PseudoClassSelector { value, .. } => CssSelectorPart { + type_: CssSelectorType::PseudoClass, + value: value.to_string(), + ..Default::default() + }, + NodeType::PseudoElementSelector { value, .. } => CssSelectorPart { + type_: CssSelectorType::PseudoElement, + value: value.clone(), + ..Default::default() + }, + NodeType::TypeSelector { value, .. } => CssSelectorPart { + type_: CssSelectorType::Type, + value: value.clone(), + ..Default::default() + }, + NodeType::AttributeSelector { + name, value, flags, .. + } => CssSelectorPart { + type_: CssSelectorType::Attribute, + name: name.clone(), + matcher: MatcherType::Equals, // @todo: this needs to be parsed + value: value.clone(), + flags: flags.clone(), + }, + _ => { + panic!("Unknown selector type: {:?}", node); + } + }; + selector.parts.push(part); + } + } + rule.selectors.push(selector); + } + + for declaration in declarations.iter() { + if !declaration.is_block() { + continue; + } + + let block = declaration.as_block(); + for declaration in block.iter() { + if !declaration.is_declaration() { + continue; + } + + let (property, value, important) = declaration.as_declaration(); + rule.declarations.push(CssDeclaration { + property: property.clone(), + value: value[0].to_string(), + important: *important, + }); + } + } + + sheet.rules.push(rule); + } + Ok(sheet) +} diff --git a/crates/gosub_css3/src/cssom/cssrule.rs b/crates/gosub_css3/src/cssom/cssrule.rs new file mode 100644 index 000000000..0c188cb08 --- /dev/null +++ b/crates/gosub_css3/src/cssom/cssrule.rs @@ -0,0 +1,198 @@ +pub enum CssRuleType { + UnknownRule = 0, + StyleRule = 1, + CharsetRule = 2, // Obsolete + ImportRule = 3, + MediaRule = 4, + FontFaceRule = 5, + PageRule = 6, + KeyframesRule = 7, + KeyframeRule = 8, + MarginRule = 9, // Obsolete + NamespaceRule = 10, + CounterStyleRule = 11, + SupportsRule = 12, + DocumentRule = 13, // Obsolete + FontFeatureValuesRule = 14, + ViewportRule = 15, // Obsolete + RegionStyleRule = 16, // Obsolete +} + +pub enum CssTypeRuleType { + StyleRule(CssStyleRule), + CharsetRule(CssCharsetRule), + ImportRule(CssImportRule), + MediaRule(CssMediaRule), + FontFaceRule(CssFontFaceRule), + PageRule(CssPageRule), + KeyframesRule(CssKeyframesRule), + KeyframeRule(CssKeyframeRule), + MarginRule(CssMarginRule), + NamespaceRule(CssNamespaceRule), + CounterStyleRule(CssCounterStyleRule), + SupportsRule(CssSupportsRule), + DocumentRule(CssDocumentRule), + FontFeatureValuesRule(CssFontFeatureValuesRule), + ViewportRule(CssViewportRule), + RegionStyleRule(CssRegionStyleRule), +} + +struct CssStyleDeclaration { + css_float: String, + css_text: String, + length: usize, + /// All the properties that are defined + property_list: HashMap, + parent_rule: Rc +} + +impl CssStyleDeclaration { + pub fn get_property_priority(&self, property: &str) -> Option<&str> { + None + } + + pub fn get_property_value(&self, property: &str) -> Option<&str> { + self.property_list.get(property).map(|s| s.as_str()) + } + + pub fn item(&self, idx: usize) -> Option<&str> { + None + } + + pub fn remove_property(&mut self, property: &str) { + self.property_list.remove(property); + } + + pub fn set_property(&mut self, property: &str, value: &str) { + self.property_list.insert(property.to_string(), value.to_string()); + } + + pub fn get_property_css_value(&self, property: &str) -> Option<&str> { + None + } +} + + +struct CssRule { + text: String, + parent_rule: Option>, + parent_stylesheet: Option>, + type_: CssRuleType, +} + +struct CssGroupingRule { + css_rules: CssRuleList, + parent: CssRule, +} + +impl CssGroupingRule { + pub fn delete_rule(&mut self, idx: usize) { + self.css_rules.remove(idx); + } + + pub fn insert_rule(&mut self, idx: usize, rule: CssRule) { + self.css_rules.insert(idx, rule); + } +} + +struct CssStyleRule { + selector_text: String, + style: CssStyleDeclaration, + // style_map: StylePropertyMap, // This is basically the same as the style, but in a different format I think + parent: CssGroupingRule, +} + +struct CssImportRule { + href: String, + layer_name: String, + media: String, + style_sheet: Rc, + supports_rule: Option, + parent: CssRule, +} + +struct CssMediaRule { + media: MediaList, + parent: CssGroupingRule, +} + +struct CssFontFaceRule { + style: CssStyleDeclaration, + parent: CssGroupingRule, +} + +struct CssPageRule { + selector_text: String, + style: CssStyleDeclaration, + parent: CssGroupingRule, +} + +struct CssNamespaceRule { + namespace: String, + prefix: String, + parent: CssRule, +} + +struct CssKeyframesRule { + name: String, + css_rules: CssRuleList, + parent: CssRule, +} + +struct CssKeyframeRule { + key_text: String, + style: CssStyleDeclaration, + parent: CssRule, +} + +struct CssCounterStyleRule { + name: String, + system: String, + symbols: String, + additive_symbols: String, + negative: String, + prefix: String, + suffix: String, + range: String, + pad: String, + speak_as: String, + fallback: String, + parent: CssRule, +} + +struct CssSupportsRule { + parent: CssConditionRule, +} + +struct CssFontFeatureValuesRule { + font_family: String, + parent: CssRule, +} + +struct CssFontPaletteValuesRule { + name: String, + font_family: String, + base_palette: String, + override_colors: String, + parent: CssRule, +} + +struct CssLayerBlockRule { + name: String, + parent: CssGroupingRule, +} + +struct CssLayerStatementRule { + name_list: NameList, + parent: CssRule, +} + +struct CssPropertyRule { + inherits: String, + initial_value: String, + name: String, + syntax: String, + parent: CssRule, +} + + diff --git a/crates/gosub_css3/src/cssom/cssrule_list.rs b/crates/gosub_css3/src/cssom/cssrule_list.rs new file mode 100644 index 000000000..43f5868fe --- /dev/null +++ b/crates/gosub_css3/src/cssom/cssrule_list.rs @@ -0,0 +1,11 @@ +type CssRuleList = vec; + +impl CssRuleList { + fn item(&self, idx: usize) -> Option<&CssRule> { + self.get(idx) + } + + fn length(&self) -> usize { + self.len() + } +} \ No newline at end of file diff --git a/crates/gosub_css3/src/cssom/stylesheet.rs b/crates/gosub_css3/src/cssom/stylesheet.rs new file mode 100644 index 000000000..ec936134d --- /dev/null +++ b/crates/gosub_css3/src/cssom/stylesheet.rs @@ -0,0 +1,62 @@ +type MediaList = Vec; + +impl MediaList { + pub fn append_medium(&mut self, medium: String) { + self.push(medium); + } + + pub fn delete_medium(&mut self, idx: usize) { + self.remove(idx); + } + + pub fn item(&self, idx: usize) -> Option<&String> { + self.get(idx) + } +} + +struct StyleSheet { + disabled: bool, + href: String, + media: MediaList, + owner_node: Option, + parent_style_sheet: Option, + title: String, + type_: String, +} + +struct CSSStyleSheet { + rules: CssRuleList, + owner_rule: Option, + stylesheet: StyleSheet, +} + +impl CSSStyleSheet { + pub fn new(stylesheet: StyleSheet) -> Self { + Self { + rules: vec![] + owner_rule: None, + stylesheet, + } + } + + pub fn delete_rule(&mut self, idx: usize) { + self.rules.remove(idx); + } + + pub fn insert_rule(&mut self, idx: usize, rule: CssRule) { + self.rules.insert(idx, rule); + } + + pub fn replace_async(&mut self, idx: usize, rule: CssRule) { + self.rules[idx] = rule; + } + + pub fn replace(&mut self, idx: usize, rule: CssRule) { + self.rules[idx] = rule; + } + + // CSSOM: + // property: rules obsolete + // method: addRule() obsolete + // method: removeRule() obsolete +} \ No newline at end of file diff --git a/crates/gosub_css3/src/cssom/stylesheet_list.rs b/crates/gosub_css3/src/cssom/stylesheet_list.rs new file mode 100644 index 000000000..067fe1c8a --- /dev/null +++ b/crates/gosub_css3/src/cssom/stylesheet_list.rs @@ -0,0 +1,11 @@ +type StyleSheetList = vec; + +impl StyleSheetList { + fn item(&self, idx: usize) -> Option<&StyleSheet> { + self.get(idx) + } + + fn length(&self) -> usize { + self.len() + } +} \ No newline at end of file diff --git a/crates/gosub_css3/src/lib.rs b/crates/gosub_css3/src/lib.rs index b17579662..119afa898 100644 --- a/crates/gosub_css3/src/lib.rs +++ b/crates/gosub_css3/src/lib.rs @@ -4,10 +4,12 @@ use crate::parser_config::{Context, ParserConfig}; use crate::tokenizer::Tokenizer; use gosub_shared::byte_stream::{ByteStream, Encoding, Stream}; +pub mod convert; pub mod location; mod node; -mod parser; +pub mod parser; pub mod parser_config; +pub mod stylesheet; pub mod tokenizer; mod unicode; pub mod walker; diff --git a/crates/gosub_css3/src/node.rs b/crates/gosub_css3/src/node.rs index 2b7ec2044..e4ed49227 100644 --- a/crates/gosub_css3/src/node.rs +++ b/crates/gosub_css3/src/node.rs @@ -1,4 +1,6 @@ use crate::location::Location; +use core::fmt::{Display, Formatter}; +use std::ops::Deref; pub type Number = f32; @@ -9,7 +11,7 @@ pub enum FeatureKind { Supports, } -#[derive(Debug, PartialEq)] +#[derive(Debug, PartialEq, Clone)] pub enum NodeType { StyleSheet { children: Vec, @@ -157,7 +159,7 @@ pub enum NodeType { } /// A node is a single element in the AST -#[derive(Debug, PartialEq)] +#[derive(Debug, PartialEq, Clone)] pub struct Node { pub node_type: Box, pub location: Location, @@ -170,4 +172,284 @@ impl Node { location, } } + + pub fn is_block(&self) -> bool { + matches!(&*self.node_type, NodeType::Block { .. }) + } + + pub fn as_block(&self) -> &Vec { + match &self.node_type.deref() { + &NodeType::Block { children } => children, + _ => panic!("Node is not a block"), + } + } + + pub fn is_stylesheet(&self) -> bool { + matches!(&*self.node_type, NodeType::StyleSheet { .. }) + } + + pub fn is_rule(&self) -> bool { + matches!(&*self.node_type, NodeType::Rule { .. }) + } + + pub fn as_stylesheet(&self) -> &Vec { + match &self.node_type.deref() { + &NodeType::StyleSheet { children } => children, + _ => panic!("Node is not a stylesheet"), + } + } + + pub fn as_rule(&self) -> (&Option, &Option) { + match &self.node_type.deref() { + &NodeType::Rule { prelude, block } => (prelude, block), + _ => panic!("Node is not a rule"), + } + } + + pub fn is_selector_list(&self) -> bool { + matches!(&*self.node_type, NodeType::SelectorList { .. }) + } + + pub fn as_selector_list(&self) -> &Vec { + match &self.node_type.deref() { + &NodeType::SelectorList { selectors } => selectors, + _ => panic!("Node is not a selector list"), + } + } + + pub fn is_selector(&self) -> bool { + matches!(&*self.node_type, NodeType::Selector { .. }) + } + + pub fn as_selector(&self) -> &Vec { + match &self.node_type.deref() { + &NodeType::Selector { children } => children, + _ => panic!("Node is not a selector"), + } + } + + pub fn is_ident(&self) -> bool { + matches!(&*self.node_type, NodeType::Ident { .. }) + } + + pub fn as_ident(&self) -> &String { + match &self.node_type.deref() { + &NodeType::Ident { value } => value, + _ => panic!("Node is not an ident"), + } + } + + pub fn is_number(&self) -> bool { + matches!(&*self.node_type, NodeType::Number { .. }) + } + + pub fn as_number(&self) -> &Number { + match &self.node_type.deref() { + &NodeType::Number { value } => value, + _ => panic!("Node is not a number"), + } + } + + pub fn is_hash(&self) -> bool { + matches!(&*self.node_type, NodeType::Hash { .. }) + } + + pub fn as_hash(&self) -> &String { + match &self.node_type.deref() { + &NodeType::Hash { value } => value, + _ => panic!("Node is not a hash"), + } + } + + pub fn as_class_selector(&self) -> &String { + match &self.node_type.deref() { + &NodeType::ClassSelector { value } => value, + _ => panic!("Node is not a class selector"), + } + } + + pub fn is_class_selector(&self) -> bool { + matches!(self.node_type.deref(), NodeType::ClassSelector { .. }) + } + + pub fn is_type_selector(&self) -> bool { + match &self.node_type.deref() { + &NodeType::TypeSelector { value, .. } => value != "*", + _ => false, + } + } + + pub fn as_type_selector(&self) -> &String { + match &self.node_type.deref() { + &NodeType::TypeSelector { value, .. } => value, + _ => panic!("Node is not a type selector"), + } + } + + pub fn is_universal_selector(&self) -> bool { + match &self.node_type.deref() { + &NodeType::TypeSelector { value, .. } => value == "*", + _ => false, + } + } + + pub fn is_attribute_selector(&self) -> bool { + matches!(&*self.node_type, NodeType::AttributeSelector { .. }) + } + + pub fn as_attribute_selector(&self) -> (&String, &Option, &String, &String) { + match &self.node_type.deref() { + &NodeType::AttributeSelector { + name, + matcher, + value, + flags, + } => (&name, matcher, &value, &flags), + _ => panic!("Node is not an attribute selector"), + } + } + + pub fn is_pseudo_class_selector(&self) -> bool { + matches!(&*self.node_type, NodeType::PseudoClassSelector { .. }) + } + + pub fn as_pseudo_class_selector(&self) -> String { + match &self.node_type.deref() { + &NodeType::PseudoClassSelector { value } => value.to_string(), + _ => panic!("Node is not a pseudo class selector"), + } + } + + pub fn is_pseudo_element_selector(&self) -> bool { + matches!(&*self.node_type, NodeType::PseudoElementSelector { .. }) + } + + pub fn as_pseudo_element_selector(&self) -> &String { + match &self.node_type.deref() { + &NodeType::PseudoElementSelector { value } => value, + _ => panic!("Node is not a pseudo element selector"), + } + } + + pub fn is_combinator(&self) -> bool { + matches!(&*self.node_type, NodeType::Combinator { .. }) + } + + pub fn as_combinator(&self) -> &String { + match &self.node_type.deref() { + &NodeType::Combinator { value } => value, + _ => panic!("Node is not a combinator"), + } + } + + pub fn is_dimension(&self) -> bool { + matches!(self.node_type.deref(), NodeType::Dimension { .. }) + } + + pub fn as_dimension(&self) -> (&Number, &String) { + match &self.node_type.deref() { + &NodeType::Dimension { value, unit } => (&value, &unit), + _ => panic!("Node is not a dimension"), + } + } + + pub fn is_id_selector(&self) -> bool { + matches!(&*self.node_type, NodeType::IdSelector { .. }) + } + + pub fn as_id_selector(&self) -> &String { + match &self.node_type.deref() { + &NodeType::IdSelector { value } => value, + _ => panic!("Node is not an id selector"), + } + } + + pub fn is_declaration(&self) -> bool { + matches!(&*self.node_type, NodeType::Declaration { .. }) + } + + pub fn as_declaration(&self) -> (&String, &Vec, &bool) { + match &self.node_type.deref() { + &NodeType::Declaration { + property, + value, + important, + } => (&property, &value, &important), + _ => panic!("Node is not a declaration"), + } + } +} + +impl Display for Node { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + let s = match self.node_type.deref() { + NodeType::SelectorList { selectors } => selectors + .iter() + .map(|s| s.to_string()) + .collect::>() + .join(", "), + NodeType::Selector { children } => children + .iter() + .map(|s| s.to_string()) + .collect::>() + .join(""), + NodeType::IdSelector { value } => value.clone(), + NodeType::Ident { value } => value.clone(), + NodeType::Number { value } => value.to_string(), + NodeType::Percentage { value } => format!("{}%", value), + NodeType::Dimension { value, unit } => format!("{}{}", value, unit), + NodeType::Hash { value } => format!("#{}", value.clone()), + NodeType::String { value } => value.clone(), + NodeType::Url { url } => url.clone(), + NodeType::Function { name, arguments } => { + let args = arguments + .iter() + .map(|a| a.to_string()) + .collect::>() + .join(", "); + format!("{}({})", name, args) + } + NodeType::AttributeSelector { + name, + matcher, + value, + flags, + } => { + let matcher = matcher + .as_ref() + .map(|m| m.to_string()) + .unwrap_or("".to_string()); + format!("[{}{}{}{}]", name, matcher, value, flags) + } + NodeType::PseudoClassSelector { value } => format!(":{}", value), + NodeType::PseudoElementSelector { value } => format!("::{}", value), + NodeType::Operator(value) => value.clone(), + NodeType::ClassSelector { value } => format!(".{}", value), + NodeType::TypeSelector { namespace, value } => { + let ns = namespace + .as_ref() + .map(|ns| format!("{}|", ns)) + .unwrap_or("".to_string()); + format!("{}{}", ns, value) + } + NodeType::Combinator { value } => value.clone(), + NodeType::Nth { nth, selector } => { + let sel = selector + .as_ref() + .map(|s| s.to_string()) + .unwrap_or("".to_string()); + format!("{}{}", nth, sel) + } + NodeType::AnPlusB { a, b } => format!("{}n+{}", a, b), + NodeType::Calc { expr } => format!("calc({})", expr), + NodeType::Raw { value } => value.clone(), + + _ => { + "".to_string() + // panic!("cannot convert to string: {:?}", self) + } + }; + + write!(f, "{}", s) + } } diff --git a/crates/gosub_css3/src/parser/at_rule.rs b/crates/gosub_css3/src/parser/at_rule.rs index ade6c3875..30377acfc 100644 --- a/crates/gosub_css3/src/parser/at_rule.rs +++ b/crates/gosub_css3/src/parser/at_rule.rs @@ -43,7 +43,15 @@ impl Css3<'_> { fn read_sequence_at_rule_prelude(&mut self) -> Result { log::trace!("read_sequence_at_rule_prelude"); - todo!() + + let loc = self.tokenizer.lookahead(0).location.clone(); + + Ok(Node::new( + NodeType::Container { + children: self.parse_value_sequence()?, + }, + loc, + )) } fn parse_at_rule_prelude(&mut self, name: String) -> Result, Error> { diff --git a/crates/gosub_css3/src/parser/at_rule/import.rs b/crates/gosub_css3/src/parser/at_rule/import.rs index 1c5770822..5030a7b15 100644 --- a/crates/gosub_css3/src/parser/at_rule/import.rs +++ b/crates/gosub_css3/src/parser/at_rule/import.rs @@ -19,14 +19,20 @@ impl Css3<'_> { children.push(Node::new(NodeType::Url { url }, loc.clone())); } TokenType::Function(name) if name.eq_ignore_ascii_case("url") => { - children.push(self.parse_function()?); + self.tokenizer.reconsume(); + children.push(self.parse_url()?); + } + _ => { + return Err(Error::new( + "Expected string or url()".to_string(), + t.location.clone(), + )); } - _ => {} } self.consume_whitespace_comments(); - let t = self.consume_any()?; + let t = self.tokenizer.lookahead_sc(0); match t.token_type { TokenType::Ident(value) if value.eq_ignore_ascii_case("layer") => { children.push(Node::new(NodeType::Ident { value }, t.location.clone())); @@ -39,7 +45,7 @@ impl Css3<'_> { self.consume_whitespace_comments(); - let t = self.consume_any()?; + let t = self.tokenizer.lookahead_sc(0); match t.token_type { TokenType::Function(name) if name.eq_ignore_ascii_case("supports") => { children.push(self.parse_function()?); @@ -48,20 +54,20 @@ impl Css3<'_> { } self.consume_whitespace_comments(); - let nt = self.tokenizer.lookahead_sc(0); - match nt.token_type { - TokenType::Ident(_) => { - self.tokenizer.reconsume(); - let list = self.parse_media_query_list()?; - children.push(list); - } - TokenType::LParen => { - self.tokenizer.reconsume(); - let list = self.parse_media_query_list()?; - children.push(list); - } - _ => {} - } + // let nt = self.tokenizer.lookahead_sc(0); + // match nt.token_type { + // TokenType::Ident(_) => { + // self.tokenizer.reconsume(); + // let list = self.parse_media_query_list()?; + // children.push(list); + // } + // TokenType::LParen => { + // self.tokenizer.reconsume(); + // let list = self.parse_media_query_list()?; + // children.push(list); + // } + // _ => {} + // } Ok(Node::new(NodeType::ImportList { children }, loc.clone())) } diff --git a/crates/gosub_css3/src/parser/selector.rs b/crates/gosub_css3/src/parser/selector.rs index 554f46baa..d8a8e687b 100644 --- a/crates/gosub_css3/src/parser/selector.rs +++ b/crates/gosub_css3/src/parser/selector.rs @@ -12,6 +12,9 @@ impl Css3<'_> { let c = self.consume_any_delim()?; match &c { '=' | '~' | '|' | '^' | '$' | '*' => { + value.push(c); + } + _ => { self.tokenizer.reconsume(); return Err(Error::new( @@ -19,9 +22,7 @@ impl Css3<'_> { loc, )); } - _ => {} } - value.push(c); if c != '=' { self.consume_delim('=')?; @@ -122,31 +123,33 @@ impl Css3<'_> { let name = self.consume_any_ident()?; self.consume_whitespace_comments(); - let t = self.consume_any()?; - match t.token_type { - TokenType::RBracket => { - self.tokenizer.reconsume(); - } - TokenType::Ident(value) => { - flags = value; - } - _ => { - self.tokenizer.reconsume(); + let t = self.tokenizer.lookahead(0); + if t.token_type != TokenType::RBracket { + if !t.is_ident() { let op = self.parse_attribute_operator()?; matcher = Some(op); + self.consume_whitespace_comments(); - let t = self.consume_any()?; - value = match t.token_type { - TokenType::QuotedString(value) => value, - TokenType::Ident(value) => value, - _ => { - return Err(Error::new( - format!("Unexpected token {:?}", t), - self.tokenizer.current_location().clone(), - )); - } - }; + let t = self.tokenizer.lookahead(0); + if t.is_string() { + value = self.consume_any_string()?; + } else if t.is_ident() { + value = self.consume_any_ident()?; + } else { + return Err(Error::new( + format!("Unexpected token {:?}", t), + self.tokenizer.current_location().clone(), + )); + } + } + + self.consume_whitespace_comments(); + + let t = self.tokenizer.lookahead(0); + if t.is_ident() { + flags = self.consume_any_ident()?; + self.consume_whitespace_comments(); } } @@ -247,91 +250,99 @@ impl Css3<'_> { let mut children = vec![]; - while !self.tokenizer.eof() { - self.consume_whitespace_comments(); + // When true, we have encountered a space which means we need to emit a descendant combinator + let mut space = false; + let mut whitespace_location = loc.clone(); + while !self.tokenizer.eof() { let t = self.consume_any()?; - match t.token_type { + if t.is_comment() { + continue; + } + if t.is_whitespace() { + // on whitespace for selector + whitespace_location = t.location.clone(); + space = true; + continue; + } + + // let t = self.consume_any()?; + let child = match t.token_type { TokenType::LBracket => { self.tokenizer.reconsume(); - let selector = self.parse_attribute_selector()?; - children.push(selector); - } - TokenType::IDHash(value) => { - let node = Node::new(NodeType::IdSelector { value }, t.location); - children.push(node); - } - TokenType::Hash(value) => { - let node = Node::new(NodeType::IdSelector { value }, t.location); - children.push(node); + self.parse_attribute_selector()? } + TokenType::IDHash(value) => Node::new(NodeType::IdSelector { value }, t.location), + TokenType::Hash(value) => Node::new(NodeType::IdSelector { value }, t.location), TokenType::Colon => { let nt = self.tokenizer.lookahead(0); if nt.token_type == TokenType::Colon { self.tokenizer.reconsume(); - let selector = self.parse_pseudo_element_selector()?; - children.push(selector); + self.parse_pseudo_element_selector()? } else { self.tokenizer.reconsume(); - let selector = self.parse_pseudo_selector()?; - children.push(selector); + self.parse_pseudo_selector()? } } - TokenType::Ident(value) => { - let node = Node::new(NodeType::Ident { value }, t.location); - children.push(node); - } + TokenType::Ident(value) => Node::new(NodeType::Ident { value }, t.location), - TokenType::Number(value) => { - let node = Node::new(NodeType::Number { value }, t.location); - children.push(node); - } + TokenType::Number(value) => Node::new(NodeType::Number { value }, t.location), TokenType::Percentage(value) => { - let node = Node::new(NodeType::Percentage { value }, t.location); - children.push(node); + Node::new(NodeType::Percentage { value }, t.location) } TokenType::Dimension { value, unit } => { - let node = Node::new(NodeType::Dimension { value, unit }, t.location); - children.push(node); + Node::new(NodeType::Dimension { value, unit }, t.location) } TokenType::Delim('+') | TokenType::Delim('>') | TokenType::Delim('~') | TokenType::Delim('/') => { + // Dont add descendant combinator since we are now adding another one + space = false; + self.tokenizer.reconsume(); - let node = self.parse_combinator()?; - children.push(node); + self.parse_combinator()? } TokenType::Delim('.') => { self.tokenizer.reconsume(); - let selector = self.parse_class_selector()?; - children.push(selector); + self.parse_class_selector()? } TokenType::Delim('|') | TokenType::Delim('*') => { self.tokenizer.reconsume(); - let selector = self.parse_type_selector()?; - children.push(selector); + self.parse_type_selector()? } TokenType::Delim('#') => { self.tokenizer.reconsume(); - let selector = self.parse_id_selector()?; - children.push(selector); + self.parse_id_selector()? } TokenType::Delim('&') => { self.tokenizer.reconsume(); - let selector = self.parse_nesting_selector()?; - children.push(selector); + self.parse_nesting_selector()? } - _ => { self.tokenizer.reconsume(); break; } + }; + + if space { + // Detected a space previously, so we need to emit a descendant combinator + let node = Node::new( + NodeType::Combinator { + value: " ".to_string(), + }, + whitespace_location.clone(), + ); + // insert before the last added node + children.push(node); + space = false; } + + children.push(child); } Ok(Node::new(NodeType::Selector { children }, loc)) diff --git a/crates/gosub_css3/src/parser_config.rs b/crates/gosub_css3/src/parser_config.rs index dd96bc328..8fe9617b1 100644 --- a/crates/gosub_css3/src/parser_config.rs +++ b/crates/gosub_css3/src/parser_config.rs @@ -8,7 +8,7 @@ pub enum Context { Declaration, } -/// ParserConfig holds the configuration for the parser +/// ParserConfig holds the configuration for the CSS3 parser pub struct ParserConfig { /// Context defines how the data needs to be parsed pub context: Context, @@ -16,7 +16,8 @@ pub struct ParserConfig { pub location: Location, /// Optional source filename or url pub source: Option, - /// Ignore errors and continue parsing + /// Ignore errors and continue parsing. Any errors will not be returned in the final AST + /// (this means if a selector is invalid, all rules will be ignored, even when they are valid) pub ignore_errors: bool, } diff --git a/crates/gosub_css3/src/stylesheet.rs b/crates/gosub_css3/src/stylesheet.rs new file mode 100644 index 000000000..81fd69d97 --- /dev/null +++ b/crates/gosub_css3/src/stylesheet.rs @@ -0,0 +1,206 @@ +use core::fmt::Debug; +use std::cmp::Ordering; +use std::fmt::Display; + +/// Defines a complete stylesheet with all its rules and the location where it was found +#[derive(Debug, PartialEq, Clone)] +pub struct CssStylesheet { + /// List of rules found in this stylesheet + pub rules: Vec, + /// Origin of the stylesheet (user agent, author, user) + pub origin: CssOrigin, + /// Url or file path where the stylesheet was found + pub location: String, +} + +/// Defines the origin of the stylesheet (or declaration) +#[derive(Debug, PartialEq, Clone)] +pub enum CssOrigin { + /// Browser/user agent defined stylesheets + UserAgent, + /// Author defined stylesheets that are linked or embedded in the HTML files + Author, + /// User defined stylesheets that will override the author and user agent stylesheets (for instance, custom user styles or extensions) + User, +} + +/// A CSS rule, which contains a list of selectors and a list of declarations +#[derive(Debug, PartialEq, Clone)] +pub struct CssRule { + /// Selectors that must match for the declarations to apply + pub selectors: Vec, + /// Actual declarations that will be applied if the selectors match + pub declarations: Vec, +} + +impl CssRule { + pub fn selectors(&self) -> &Vec { + &self.selectors + } + + pub fn declarations(&self) -> &Vec { + &self.declarations + } +} + +/// A CSS declaration, which contains a property, value and a flag for !important +#[derive(Debug, PartialEq, Clone)] +pub struct CssDeclaration { + // Css property color + pub property: String, + // Raw value of the declaration. It is not calculated or converted in any way (ie: "red", "50px" etc) + pub value: String, + // ie: !important + pub important: bool, +} + +#[derive(Debug, PartialEq, Clone)] +pub struct CssSelector { + // List of parts that make up this selector + pub parts: Vec, +} + +impl CssSelector { + /// Generate specificity for this selector + pub fn specificity(&self) -> Specificity { + let mut id_count = 0; + let mut class_count = 0; + let mut element_count = 0; + for part in &self.parts { + match part.type_ { + CssSelectorType::Id => { + id_count += 1; + } + CssSelectorType::Class => { + class_count += 1; + } + CssSelectorType::Type => { + element_count += 1; + } + _ => {} + } + } + Specificity::new(id_count, class_count, element_count) + } +} + +/// @todo: it would be nicer to have a struct for each type of selector part, but for now we'll keep it simple +/// Represents a CSS selector part, which has a type and value (e.g. type=Class, class="my-class") +#[derive(PartialEq, Clone, Default)] +pub struct CssSelectorPart { + pub type_: CssSelectorType, + pub value: String, + pub matcher: MatcherType, + pub name: String, + pub flags: String, +} + +impl Debug for CssSelectorPart { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self.type_ { + CssSelectorType::Universal => { + write!(f, "*") + } + CssSelectorType::Attribute => { + write!( + f, + "[{} {} {} {}]", + self.name, self.matcher, self.value, self.flags + ) + } + CssSelectorType::Class => { + write!(f, ".{}", self.value) + } + CssSelectorType::Id => { + write!(f, "#{}", self.value) + } + CssSelectorType::PseudoClass => { + write!(f, ":{}", self.value) + } + CssSelectorType::PseudoElement => { + write!(f, "::{}", self.value) + } + CssSelectorType::Combinator => { + write!(f, "'{}'", self.value) + } + CssSelectorType::Type => { + write!(f, "{}", self.value) + } + } + } +} + +/// Represents a CSS selector type for this part +#[derive(Debug, PartialEq, Clone, Default)] +pub enum CssSelectorType { + Universal, // '*' + #[default] + Type, // ul, a, h1, etc + Attribute, // [type ~= "text" i] (name, matcher, value, flags) + Class, // .myclass + Id, // #myid + PseudoClass, // :hover, :active + PseudoElement, // ::first-child + Combinator, +} + +/// Represents which type of matcher is used (in case of an attribute selector type) +#[derive(Default, PartialEq, Clone)] +pub enum MatcherType { + #[default] + None, // No matcher + Equals, // Equals + Includes, // Must include + DashMatch, // Must start with + PrefixMatch, // Must begin with + SuffixMatch, // Must ends with + SubstringMatch, // Must contain +} + +impl Display for MatcherType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + MatcherType::None => write!(f, ""), + MatcherType::Equals => write!(f, "="), + MatcherType::Includes => write!(f, "~="), + MatcherType::DashMatch => write!(f, "|="), + MatcherType::PrefixMatch => write!(f, "^="), + MatcherType::SuffixMatch => write!(f, "$="), + MatcherType::SubstringMatch => write!(f, "*="), + } + } +} + +/// Defines the specificity for a selector +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct Specificity(u32, u32, u32); + +impl Specificity { + pub fn new(a: u32, b: u32, c: u32) -> Self { + Self(a, b, c) + } +} + +impl PartialOrd for Specificity { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for Specificity { + fn cmp(&self, other: &Self) -> Ordering { + match self.0.cmp(&other.0) { + Ordering::Greater => Ordering::Greater, + Ordering::Less => Ordering::Less, + Ordering::Equal => match self.1.cmp(&other.1) { + Ordering::Greater => Ordering::Greater, + Ordering::Less => Ordering::Less, + Ordering::Equal => match self.2.cmp(&other.2) { + Ordering::Greater => Ordering::Greater, + Ordering::Less => Ordering::Less, + Ordering::Equal => Ordering::Equal, + }, + }, + } + } +} diff --git a/crates/gosub_css3/src/tokenizer.rs b/crates/gosub_css3/src/tokenizer.rs index 5f6806e65..eedb5587e 100644 --- a/crates/gosub_css3/src/tokenizer.rs +++ b/crates/gosub_css3/src/tokenizer.rs @@ -154,6 +154,10 @@ impl Token { matches!(self.token_type, TokenType::Comma) } + pub(crate) fn is_string(&self) -> bool { + matches!(self.token_type, TokenType::QuotedString(_)) + } + pub(crate) fn is_ident(&self) -> bool { matches!(self.token_type, TokenType::Ident(_)) } diff --git a/crates/gosub_css3/src/walker.rs b/crates/gosub_css3/src/walker.rs index a7475196c..4e76b1521 100644 --- a/crates/gosub_css3/src/walker.rs +++ b/crates/gosub_css3/src/walker.rs @@ -3,19 +3,23 @@ use std::io::Write; use std::ops::Deref; /// The walker is used to walk the AST and print it to stdout. +#[allow(dead_code)] pub struct Walker<'a> { root: &'a Node, } impl<'a> Walker<'a> { + #[allow(dead_code)] pub fn new(root: &'a Node) -> Self { Self { root } } + #[allow(dead_code)] pub fn walk_stdout(&self) { let _ = inner_walk(self.root, 0, &mut std::io::stdout()); } + #[allow(dead_code)] pub fn walk_to_string(&self) -> String { let mut output: Vec = Vec::new(); @@ -76,8 +80,8 @@ fn inner_walk(node: &Node, depth: usize, f: &mut dyn Write) -> Result<(), std::i } } NodeType::Comment { .. } => {} - NodeType::Cdo => {} - NodeType::Cdc => {} + // NodeType::Cdo => {} + // NodeType::Cdc => {} NodeType::IdSelector { .. } => {} NodeType::Ident { value } => { writeln!(f, "{}[Ident] {}", prefix, value)?; @@ -267,6 +271,8 @@ fn inner_walk(node: &Node, depth: usize, f: &mut dyn Write) -> Result<(), std::i inner_walk(child, depth + 1, f)?; } } + NodeType::Cdo => {} + NodeType::Cdc => {} } Ok(()) } diff --git a/crates/gosub_html5/Cargo.toml b/crates/gosub_html5/Cargo.toml index 16f8ca216..a53138acb 100644 --- a/crates/gosub_html5/Cargo.toml +++ b/crates/gosub_html5/Cargo.toml @@ -7,8 +7,12 @@ license = "MIT" [dependencies] gosub_shared = { path = "../gosub_shared", features = [] } +gosub_css3 = { path = "../gosub_css3", features = [] } derive_more = "0.99" phf = { version = "0.11.2", features = ["macros"] } lazy_static = "1.4" thiserror = "1.0.57" +url = { version = "2.5.0", features = [] } +log = { version = "0.4.20", features = [] } +ureq = "2.9.6" diff --git a/crates/gosub_html5/src/lib.rs b/crates/gosub_html5/src/lib.rs index b4bc11432..aeb10c95e 100644 --- a/crates/gosub_html5/src/lib.rs +++ b/crates/gosub_html5/src/lib.rs @@ -10,3 +10,4 @@ pub mod node; pub mod parser; pub mod tokenizer; pub mod util; +pub mod visit; diff --git a/crates/gosub_html5/src/node.rs b/crates/gosub_html5/src/node.rs index b6e597b21..69e39095e 100644 --- a/crates/gosub_html5/src/node.rs +++ b/crates/gosub_html5/src/node.rs @@ -189,6 +189,7 @@ impl Node { is_registered, } } + /// Create a new document node #[must_use] pub fn new_document(document: &DocumentHandle) -> Self { @@ -337,6 +338,65 @@ impl Node { namespace == MATHML_NAMESPACE && ["mi", "mo", "mn", "ms", "mtext"].contains(&self.name.as_str()) } + + /// Returns true if the node is an element node + pub fn is_element(&self) -> bool { + if let NodeData::Element(_) = &self.data { + return true; + } + + false + } + + pub fn is_text(&self) -> bool { + if let NodeData::Text(_) = &self.data { + return true; + } + + false + } + + pub fn as_text(&self) -> &TextData { + if let NodeData::Text(text) = &self.data { + return text; + } + + panic!("Node is not a text"); + } + + pub fn as_element(&self) -> &ElementData { + if let NodeData::Element(element) = &self.data { + return element; + } + + panic!("Node is not an element"); + } + + pub fn as_element_mut(&mut self) -> &mut ElementData { + if let NodeData::Element(ref mut element) = self.data { + element + } else { + panic!("Node is not an element"); + } + } + + /// Returns true when the given attribute has been set on the node + pub fn has_attribute(&self, name: &str) -> bool { + if let NodeData::Element(element) = &self.data { + return element.attributes.contains_key(name); + } + + false + } + + /// Returns the given attribute value or None when the attribute is not found + pub fn get_attribute(&self, name: &str) -> Option<&String> { + if let NodeData::Element(element) = &self.data { + return element.attributes.get(name); + } + + None + } } pub trait NodeTrait { @@ -463,7 +523,7 @@ mod tests { #[test] fn new_document() { - let document = Document::shared(); + let document = Document::shared(None); let node = Node::new_document(&document); assert_eq!(node.id, NodeId::default()); assert_eq!(node.parent, None); @@ -480,7 +540,7 @@ mod tests { fn new_element() { let mut attributes = HashMap::new(); attributes.insert("id".to_string(), "test".to_string()); - let document = Document::shared(); + let document = Document::shared(None); let node = Node::new_element(&document, "div", attributes.clone(), HTML_NAMESPACE); assert_eq!(node.id, NodeId::default()); assert_eq!(node.parent, None); @@ -497,7 +557,7 @@ mod tests { #[test] fn new_comment() { - let document = Document::shared(); + let document = Document::shared(None); let node = Node::new_comment(&document, "test"); assert_eq!(node.id, NodeId::default()); assert_eq!(node.parent, None); @@ -512,7 +572,7 @@ mod tests { #[test] fn new_text() { - let document = Document::shared(); + let document = Document::shared(None); let node = Node::new_text(&document, "test"); assert_eq!(node.id, NodeId::default()); assert_eq!(node.parent, None); @@ -529,14 +589,14 @@ mod tests { fn is_special() { let mut attributes = HashMap::new(); attributes.insert("id".to_string(), "test".to_string()); - let document = Document::shared(); + let document = Document::shared(None); let node = Node::new_element(&document, "div", attributes, HTML_NAMESPACE); assert!(node.is_special()); } #[test] fn type_of() { - let document = Document::shared(); + let document = Document::shared(None); let node = Node::new_document(&document); assert_eq!(node.type_of(), NodeType::Document); let node = Node::new_text(&document, "test"); @@ -551,7 +611,7 @@ mod tests { #[test] fn special_html_elements() { - let document = Document::shared(); + let document = Document::shared(None); for element in SPECIAL_HTML_ELEMENTS.iter() { let mut attributes = HashMap::new(); attributes.insert("id".to_string(), "test".to_string()); @@ -562,7 +622,7 @@ mod tests { #[test] fn special_mathml_elements() { - let document = Document::shared(); + let document = Document::shared(None); for element in SPECIAL_MATHML_ELEMENTS.iter() { let mut attributes = HashMap::new(); attributes.insert("id".to_string(), "test".to_string()); @@ -573,7 +633,7 @@ mod tests { #[test] fn special_svg_elements() { - let document = Document::shared(); + let document = Document::shared(None); for element in SPECIAL_SVG_ELEMENTS.iter() { let mut attributes = HashMap::new(); attributes.insert("id".to_string(), "test".to_string()); @@ -584,7 +644,7 @@ mod tests { #[test] fn type_of_node() { - let document = Document::shared(); + let document = Document::shared(None); let node = Node::new_document(&document); assert_eq!(node.type_of(), NodeType::Document); let node = Node::new_text(&document, "test"); diff --git a/crates/gosub_html5/src/node/arena.rs b/crates/gosub_html5/src/node/arena.rs index 095fdac4a..d50ae6b28 100644 --- a/crates/gosub_html5/src/node/arena.rs +++ b/crates/gosub_html5/src/node/arena.rs @@ -8,11 +8,6 @@ use super::NodeId; pub struct NodeArena { /// Current nodes stored as nodes: HashMap, - /// Order of nodes - /// - /// Note that the order of nodes isn't directly needed for functionality, but merely present - /// for debugging purposes. - order: Vec, /// Next node ID to use next_id: NodeId, } @@ -24,7 +19,6 @@ impl NodeArena { Self { nodes: HashMap::new(), next_id: NodeId::default(), - order: Vec::new(), } } @@ -50,6 +44,10 @@ impl NodeArena { self.nodes.get_mut(&node_id) } + pub fn delete_node(&mut self, node_id: NodeId) { + self.nodes.remove(&node_id); + } + /// Registered an unregistered node into the arena pub fn register_node(&mut self, mut node: Node) -> NodeId { assert!(!node.is_registered, "Node is already attached to an arena"); @@ -61,18 +59,8 @@ impl NodeArena { node.id = id; self.nodes.insert(id, node); - self.order.push(id); id } - - /// Prints the list of nodes in sequential order. This makes debugging a bit easier, but should - /// be removed. - #[allow(dead_code)] - pub(crate) fn print_nodes(&self) { - for id in &self.order { - println!("({}): {:?}", id, self.nodes.get(id).expect("node")); - } - } } impl Default for NodeArena { @@ -89,7 +77,7 @@ mod tests { #[test] fn register_node() { - let mut doc = Document::shared(); + let mut doc = Document::shared(None); let node = Node::new_element(&doc, "test", HashMap::new(), HTML_NAMESPACE); let mut document = doc.get_mut(); @@ -103,7 +91,7 @@ mod tests { #[test] #[should_panic] fn register_node_twice() { - let mut doc = Document::shared(); + let mut doc = Document::shared(None); let node = Node::new_element(&doc, "test", HashMap::new(), HTML_NAMESPACE); let mut document = doc.get_mut(); @@ -115,7 +103,7 @@ mod tests { #[test] fn get_node() { - let mut doc = Document::shared(); + let mut doc = Document::shared(None); let node = Node::new_element(&doc, "test", HashMap::new(), HTML_NAMESPACE); let mut document = doc.get_mut(); @@ -127,7 +115,7 @@ mod tests { #[test] fn get_node_mut() { - let mut doc = Document::shared(); + let mut doc = Document::shared(None); let node = Node::new_element(&doc, "test", HashMap::new(), HTML_NAMESPACE); let mut document = doc.get_mut(); @@ -140,7 +128,7 @@ mod tests { #[test] fn register_node_through_document() { - let mut doc = Document::shared(); + let mut doc = Document::shared(None); let parent = Node::new_element(&doc, "parent", HashMap::new(), HTML_NAMESPACE); let child = Node::new_element(&doc, "child", HashMap::new(), HTML_NAMESPACE); diff --git a/crates/gosub_html5/src/node/data/comment.rs b/crates/gosub_html5/src/node/data/comment.rs index d9aed8684..21287ecfb 100644 --- a/crates/gosub_html5/src/node/data/comment.rs +++ b/crates/gosub_html5/src/node/data/comment.rs @@ -2,7 +2,7 @@ /// Data structure for comment nodes pub struct CommentData { /// The actual comment value - pub(crate) value: String, + pub value: String, } impl Default for CommentData { diff --git a/crates/gosub_html5/src/node/data/element.rs b/crates/gosub_html5/src/node/data/element.rs index 1f7e303d2..cb1e3b22a 100644 --- a/crates/gosub_html5/src/node/data/element.rs +++ b/crates/gosub_html5/src/node/data/element.rs @@ -10,7 +10,7 @@ use std::fmt; /// Data structure for element nodes pub struct ElementData { /// Numerical ID of the node this data is attached to - pub(crate) node_id: NodeId, + pub node_id: NodeId, /// Name of the element (e.g., div) pub name: String, /// Element's attributes stored as key-value pairs. @@ -19,11 +19,11 @@ pub struct ElementData { /// to keep attributes in sync with the DOM. pub attributes: HashMap, /// CSS classes - pub(crate) classes: ElementClass, + pub classes: ElementClass, // Only used for + + [Default again] + + \ No newline at end of file