diff --git a/Cargo.toml b/Cargo.toml index f80668293..016bd8d95 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,7 @@ name = "gosub_engine" version = "0.1.0" edition = "2021" -rust-version = "1.73" +rust-version = "1.79" authors = ["Gosub Community "] description = "An HTML5 browser engine written in Rust." license = "MIT" @@ -47,6 +47,8 @@ clap = { version = "4.5.13", features = ["derive"] } simple_logger = "5.0.0" cookie = { version = "0.18.1", features = ["secure", "private"] } url = "2.5.2" +nom = "7.1.3" +nom-trace = "0.2.1" [dev-dependencies] criterion = { version = "0.5", features = ["html_reports"] } diff --git a/crates/gosub_css3/Cargo.toml b/crates/gosub_css3/Cargo.toml index 6f6aa8907..3740e33db 100644 --- a/crates/gosub_css3/Cargo.toml +++ b/crates/gosub_css3/Cargo.toml @@ -11,3 +11,4 @@ lazy_static = "1.5" log = "0.4.22" simple_logger = "5.0.0" anyhow = { version = "1.0.86", features = [] } +colors-transform = "0.2.11" \ No newline at end of file diff --git a/crates/gosub_styling/src/css_colors.rs b/crates/gosub_css3/src/colors.rs similarity index 90% rename from crates/gosub_styling/src/css_colors.rs rename to crates/gosub_css3/src/colors.rs index 8f2a728bd..7575cb3bc 100644 --- a/crates/gosub_styling/src/css_colors.rs +++ b/crates/gosub_css3/src/colors.rs @@ -1,10 +1,11 @@ -use colors_transform::Color; -use colors_transform::{AlphaColor, Hsl, Rgb}; -use lazy_static::lazy_static; use std::convert::From; use std::fmt::Debug; use std::str::FromStr; +use colors_transform::Color; +use colors_transform::{AlphaColor, Hsl, Rgb}; +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 @@ -51,6 +52,11 @@ impl From<&str> for RgbColor { if value.is_empty() { return RgbColor::default(); } + if value == "currentcolor" { + // @todo: implement currentcolor + return RgbColor::default(); + } + if value.starts_with('#') { return parse_hex(value); } @@ -86,6 +92,7 @@ impl From<&str> for RgbColor { return RgbColor::new(rgb.get_red(), rgb.get_green(), rgb.get_blue(), 255.0); } if value.starts_with("hsla(") { + // @TODO: hsla() does not work properly // HSLA function let hsl = Hsl::from_str(value); if hsl.is_err() { @@ -105,7 +112,7 @@ impl From<&str> for RgbColor { } fn get_hex_color_from_name(color_name: &str) -> Option<&str> { - for entry in crate::css_colors::CSS_COLORNAMES.iter() { + for entry in crate::colors::CSS_COLORNAMES.iter() { if entry.name == color_name { return Some(entry.value); } @@ -778,6 +785,69 @@ lazy_static! { ]; } +pub fn is_system_color(name: &str) -> bool { + for entry in CSS_SYSTEM_COLOR_NAMES.iter() { + if entry == &name { + return true; + } + } + false +} + +pub fn is_named_color(name: &str) -> bool { + for entry in CSS_COLORNAMES.iter() { + if entry.name == name { + return true; + } + } + false +} + +pub const CSS_SYSTEM_COLOR_NAMES: [&str; 42] = [ + "AccentColor", + "AccentColorText", + "ActiveText", + "ButtonBorder", + "ButtonFace", + "ButtonText", + "Canvas", + "CanvasText", + "Field", + "FieldText", + "GrayText", + "Highlight", + "HighlightText", + "LinkText", + "Mark", + "MarkText", + "SelectedItem", + "SelectedItemText", + "VisitedText", + "ActiveBorder", + "ActiveCaption", + "AppWorkspace", + "Background", + "ButtonHighlight", + "ButtonShadow", + "CaptionText", + "InactiveBorder", + "InactiveCaption", + "InactiveCaptionText", + "InfoBackground", + "InfoText", + "Menu", + "MenuText", + "Scrollbar", + "ThreeDDarkShadow", + "ThreeDFace", + "ThreeDHighlight", + "ThreeDLightShadow", + "ThreeDShadow", + "Window", + "WindowFrame", + "WindowText", +]; + #[cfg(test)] mod tests { #[test] @@ -905,4 +975,36 @@ mod tests { assert_eq!(color.b, 0x99 as f32); assert_eq!(color.a, 255.0); } + + #[test] + fn rgb_func_colors() { + let color = super::RgbColor::from("rgb(10, 20, 30)"); + assert_eq!(color.r, 10.0); + assert_eq!(color.g, 20.0); + assert_eq!(color.b, 30.0); + assert_eq!(color.a, 255.0); + + // invalid color + let color = super::RgbColor::from("rgb(10)"); + assert_eq!(color.r, 0.0); + assert_eq!(color.g, 0.0); + assert_eq!(color.b, 0.0); + assert_eq!(color.a, 255.0); + } + + #[test] + fn hsl_func_colors() { + let color = super::RgbColor::from("hsl(10, 20%, 30%)"); + assert_eq!(color.r, 91.8); + assert_eq!(color.g, 66.3); + assert_eq!(color.b, 61.2); + assert_eq!(color.a, 255.0); + + // invalid color + let color = super::RgbColor::from("hsl(10)"); + assert_eq!(color.r, 0.0); + assert_eq!(color.g, 0.0); + assert_eq!(color.b, 0.0); + assert_eq!(color.a, 255.0); + } } diff --git a/crates/gosub_css3/src/convert/ast_converter.rs b/crates/gosub_css3/src/convert/ast_converter.rs index 1116776b5..16940fa02 100644 --- a/crates/gosub_css3/src/convert/ast_converter.rs +++ b/crates/gosub_css3/src/convert/ast_converter.rs @@ -1,7 +1,7 @@ use crate::node::{Node as CssNode, NodeType}; use crate::stylesheet::{ CssDeclaration, CssOrigin, CssRule, CssSelector, CssSelectorPart, CssSelectorType, - CssStylesheet, MatcherType, + CssStylesheet, CssValue, MatcherType, }; use anyhow::anyhow; use gosub_shared::types::Result; @@ -56,7 +56,6 @@ in css: vs h3 { color: rebeccapurple; } h4 { color: rebeccapurple; } - */ /// Converts a CSS AST to a CSS stylesheet structure @@ -92,7 +91,6 @@ pub fn convert_ast_to_stylesheet( } let mut selector = CssSelector { parts: vec![] }; - for node in node.as_selector_list().iter() { if !node.is_selector() { continue; @@ -170,10 +168,23 @@ pub fn convert_ast_to_stylesheet( continue; } - let (property, value, important) = declaration.as_declaration(); + let (property, nodes, important) = declaration.as_declaration(); + + // Convert the nodes into CSS Values + let mut css_values = vec![]; + for node in nodes.iter() { + if let Ok(value) = CssValue::parse_ast_node(node) { + css_values.push(value); + } + } + + if css_values.is_empty() { + continue; + } + rule.declarations.push(CssDeclaration { property: property.clone(), - value: value[0].to_string(), + value: css_values.to_vec(), important: *important, }); } @@ -183,3 +194,92 @@ pub fn convert_ast_to_stylesheet( } Ok(sheet) } + +#[cfg(test)] +mod tests { + use super::*; + use crate::parser_config::ParserConfig; + use crate::Css3; + + #[test] + fn convert_font_family() { + let ast = Css3::parse( + r#" + body { + border: 1px solid black; + color: #ffffff; + background-color: #121212; + font-family: "Arial", sans-serif; + margin: 0; + padding: 0; + } + "#, + ParserConfig::default(), + ) + .unwrap(); + + let tree = convert_ast_to_stylesheet(&ast, CssOrigin::UserAgent, "test.css").unwrap(); + + dbg!(&tree); + } + + #[test] + fn convert_test() { + let ast = Css3::parse( + r#" + h1 { color: red; } + h3, h4 { border: 1px solid black; } + "#, + ParserConfig::default(), + ) + .unwrap(); + + let tree = convert_ast_to_stylesheet(&ast, CssOrigin::UserAgent, "test.css").unwrap(); + + assert_eq!( + tree.rules + .first() + .unwrap() + .declarations + .first() + .unwrap() + .property, + "color" + ); + assert_eq!( + tree.rules + .first() + .unwrap() + .declarations + .first() + .unwrap() + .value, + vec![CssValue::String("red".into())] + ); + + assert_eq!( + tree.rules + .get(1) + .unwrap() + .declarations + .first() + .unwrap() + .property, + "border" + ); + assert_eq!( + tree.rules + .get(1) + .unwrap() + .declarations + .first() + .unwrap() + .value, + vec![ + CssValue::Unit(1.0, "px".into()), + CssValue::String("solid".into()), + CssValue::String("black".into()) + ] + ); + } +} diff --git a/crates/gosub_css3/src/lib.rs b/crates/gosub_css3/src/lib.rs index adfd4105a..b4880d8aa 100644 --- a/crates/gosub_css3/src/lib.rs +++ b/crates/gosub_css3/src/lib.rs @@ -4,6 +4,7 @@ use crate::tokenizer::Tokenizer; use gosub_shared::byte_stream::{ByteStream, Encoding, Location}; use gosub_shared::{timing_start, timing_stop}; +pub mod colors; pub mod convert; mod node; pub mod parser; diff --git a/crates/gosub_css3/src/stylesheet.rs b/crates/gosub_css3/src/stylesheet.rs index 81fd69d97..528eccfe4 100644 --- a/crates/gosub_css3/src/stylesheet.rs +++ b/crates/gosub_css3/src/stylesheet.rs @@ -1,4 +1,7 @@ +use crate::colors::RgbColor; +use anyhow::anyhow; use core::fmt::Debug; +use gosub_shared::types::Result; use std::cmp::Ordering; use std::fmt::Display; @@ -48,8 +51,9 @@ impl CssRule { 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, + // Raw values of the declaration. It is not calculated or converted in any way (ie: "red", "50px" etc) + // There can be multiple values (ie: "1px solid black" are split into 3 values) + pub value: Vec, // ie: !important pub important: bool, } @@ -204,3 +208,330 @@ impl Ord for Specificity { } } } + +/// 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), + Zero, + Number(f32), + Percentage(f32), + String(String), + Unit(f32, String), + Function(String, Vec), + Initial, + Inherit, + Comma, + List(Vec), +} + +impl Display for CssValue { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + CssValue::None => write!(f, "none"), + CssValue::Color(col) => { + write!( + f, + "#{:02x}{:02x}{:02x}{:02x}", + col.r as u8, col.g as u8, col.b as u8, col.a as u8 + ) + } + CssValue::Zero => write!(f, "0"), + CssValue::Number(num) => write!(f, "{}", num), + CssValue::Percentage(p) => write!(f, "{}%", p), + CssValue::String(s) => write!(f, "{}", s), + CssValue::Unit(val, unit) => write!(f, "{}{}", val, unit), + CssValue::Function(name, args) => { + write!(f, "{}(", name)?; + for (i, arg) in args.iter().enumerate() { + if i > 0 { + write!(f, ", ")?; + } + write!(f, "{}", arg)?; + } + write!(f, ")") + } + CssValue::Initial => write!(f, "initial"), + CssValue::Inherit => write!(f, "inherit"), + CssValue::Comma => write!(f, ","), + CssValue::List(v) => { + write!(f, "List(")?; + for (i, value) in v.iter().enumerate() { + if i > 0 { + write!(f, ", ")?; + } + write!(f, "{}", value)?; + } + write!(f, ")") + } + } + } +} + +impl CssValue { + pub fn to_color(&self) -> Option { + match self { + CssValue::Color(col) => Some(*col), + CssValue::String(s) => Some(RgbColor::from(s.as_str())), + _ => None, + } + } + + pub fn unit_to_px(&self) -> f32 { + //TODO: Implement the rest of the units + match self { + CssValue::Unit(val, unit) => match unit.as_str() { + "px" => *val, + "em" => *val * 16.0, + "rem" => *val * 16.0, + _ => *val, + }, + CssValue::String(value) => { + if value.ends_with("px") { + value.trim_end_matches("px").parse::().unwrap() + } else if value.ends_with("rem") { + value.trim_end_matches("rem").parse::().unwrap() * 16.0 + } else if value.ends_with("em") { + value.trim_end_matches("em").parse::().unwrap() * 16.0 + } else { + 0.0 + } + } + _ => 0.0, + } + } + + /// Converts a CSS AST node to a CSS value + pub fn parse_ast_node(node: &crate::node::Node) -> Result { + match *node.node_type.clone() { + crate::node::NodeType::Ident { value } => Ok(CssValue::String(value)), + crate::node::NodeType::Number { value } => { + if value == 0.0 { + // Zero is a special case since we need to do some pattern matching once in a while, and + // this is not possible (anymore) with floating point 0.0 it seems + Ok(CssValue::Zero) + } else { + Ok(CssValue::Number(value)) + } + } + crate::node::NodeType::Percentage { value } => Ok(CssValue::Percentage(value)), + crate::node::NodeType::Dimension { value, unit } => Ok(CssValue::Unit(value, unit)), + crate::node::NodeType::String { value } => Ok(CssValue::String(value)), + crate::node::NodeType::Hash { value } => Ok(CssValue::String(value)), + crate::node::NodeType::Operator(_) => Ok(CssValue::None), + crate::node::NodeType::Calc { .. } => { + Ok(CssValue::Function("calc".to_string(), vec![])) + } + crate::node::NodeType::Url { url } => Ok(CssValue::Function( + "url".to_string(), + vec![CssValue::String(url)], + )), + crate::node::NodeType::Function { name, arguments } => { + let mut list = vec![]; + for node in arguments.iter() { + match CssValue::parse_ast_node(node) { + Ok(value) => list.push(value), + Err(e) => return Err(e), + } + } + Ok(CssValue::Function(name, list)) + } + _ => Err(anyhow!(format!( + "Cannot convert node to CssValue: {:?}", + node + ))), + } + } + + /// Parses a string into a CSS value or list of css values + pub fn parse_str(value: &str) -> Result { + match value { + "initial" => return Ok(CssValue::Initial), + "inherit" => return Ok(CssValue::Inherit), + "none" => return Ok(CssValue::None), + "" => return Ok(CssValue::String("".into())), + _ => {} + } + + if let Ok(num) = value.parse::() { + return Ok(CssValue::Number(num)); + } + + // Color values + if value.starts_with("color(") && value.ends_with(')') { + return Ok(CssValue::Color(RgbColor::from( + value[6..value.len() - 1].to_string().as_str(), + ))); + } + + // Percentages + if value.ends_with('%') { + if let Ok(num) = value[0..value.len() - 1].parse::() { + return Ok(CssValue::Percentage(num)); + } + } + + // units. If the value starts with a number and ends with some non-numerical + let mut split_index = None; + for (index, char) in value.chars().enumerate() { + if char.is_alphabetic() { + split_index = Some(index); + break; + } + } + if let Some(index) = split_index { + let (number_part, unit_part) = value.split_at(index); + if let Ok(number) = number_part.parse::() { + return Ok(CssValue::Unit(number, unit_part.to_string())); + } + } + + Ok(CssValue::String(value.to_string())) + } +} + +#[cfg(test)] +mod test { + use super::*; + + // #[test] + // fn test_css_value_to_color() { + // assert_eq!(CssValue::from_str("color(#ff0000)").unwrap().to_color().unwrap(), RgbColor::from("#ff0000")); + // assert_eq!(CssValue::from_str("'Hello'").unwrap().to_color().unwrap(), RgbColor::from("#000000")); + // } + // + // #[test] + // fn test_css_value_unit_to_px() { + // assert_eq!(CssValue::from_str("10px").unwrap().unit_to_px(), 10.0); + // assert_eq!(CssValue::from_str("10em").unwrap().unit_to_px(), 160.0); + // assert_eq!(CssValue::from_str("10rem").unwrap().unit_to_px(), 160.0); + // assert_eq!(CssValue::from_str("10").unwrap().unit_to_px(), 0.0); + // } + + #[test] + fn test_css_rule() { + let rule = CssRule { + selectors: vec![CssSelector { + parts: vec![CssSelectorPart { + type_: CssSelectorType::Type, + value: "h1".to_string(), + ..Default::default() + }], + }], + declarations: vec![CssDeclaration { + property: "color".to_string(), + value: vec![CssValue::String("red".to_string())], + important: false, + }], + }; + + assert_eq!(rule.selectors().len(), 1); + assert_eq!( + rule.selectors() + .first() + .unwrap() + .parts + .first() + .unwrap() + .value, + "h1" + ); + assert_eq!(rule.declarations().len(), 1); + assert_eq!(rule.declarations().first().unwrap().property, "color"); + } + + #[test] + fn test_specificity() { + let selector = CssSelector { + parts: vec![ + CssSelectorPart { + type_: CssSelectorType::Type, + value: "h1".to_string(), + ..Default::default() + }, + CssSelectorPart { + type_: CssSelectorType::Class, + value: "myclass".to_string(), + ..Default::default() + }, + CssSelectorPart { + type_: CssSelectorType::Id, + value: "myid".to_string(), + ..Default::default() + }, + ], + }; + + let specificity = selector.specificity(); + assert_eq!(specificity, Specificity::new(1, 1, 1)); + + let selector = CssSelector { + parts: vec![ + CssSelectorPart { + type_: CssSelectorType::Type, + value: "h1".to_string(), + ..Default::default() + }, + CssSelectorPart { + type_: CssSelectorType::Class, + value: "myclass".to_string(), + ..Default::default() + }, + ], + }; + + let specificity = selector.specificity(); + assert_eq!(specificity, Specificity::new(0, 1, 1)); + + let selector = CssSelector { + parts: vec![CssSelectorPart { + type_: CssSelectorType::Type, + value: "h1".to_string(), + ..Default::default() + }], + }; + + let specificity = selector.specificity(); + assert_eq!(specificity, Specificity::new(0, 0, 1)); + + let selector = CssSelector { + parts: vec![ + CssSelectorPart { + type_: CssSelectorType::Class, + value: "myclass".to_string(), + ..Default::default() + }, + CssSelectorPart { + type_: CssSelectorType::Class, + value: "otherclass".to_string(), + ..Default::default() + }, + ], + }; + + let specificity = selector.specificity(); + assert_eq!(specificity, Specificity::new(0, 2, 0)); + } + + #[test] + fn test_specificity_ordering() { + let specificity1 = Specificity::new(1, 1, 1); + let specificity2 = Specificity::new(0, 1, 1); + let specificity3 = Specificity::new(0, 0, 1); + let specificity4 = Specificity::new(0, 2, 0); + let specificity5 = Specificity::new(1, 0, 0); + let specificity6 = Specificity::new(1, 2, 1); + let specificity7 = Specificity::new(1, 1, 2); + let specificity8 = Specificity::new(2, 1, 1); + + assert!(specificity1 > specificity2); + assert!(specificity2 > specificity3); + assert!(specificity3 < specificity4); + assert!(specificity4 < specificity5); + assert!(specificity5 < specificity6); + assert!(specificity6 > specificity7); + assert!(specificity7 < specificity8); + } +} diff --git a/crates/gosub_html5/src/lib.rs b/crates/gosub_html5/src/lib.rs index 6e6009630..0966d5c64 100644 --- a/crates/gosub_html5/src/lib.rs +++ b/crates/gosub_html5/src/lib.rs @@ -2,6 +2,10 @@ //! //! The parser's job is to take a stream of bytes and turn it into a DOM tree. The parser is //! implemented as a state machine and runs in the current thread. +use crate::parser::document::{Document, DocumentBuilder, DocumentHandle}; +use crate::parser::Html5Parser; +use gosub_shared::byte_stream::{ByteStream, Encoding}; + pub mod dom; pub mod element_class; pub mod error_logger; @@ -12,3 +16,15 @@ pub mod tokenizer; pub mod util; pub mod visit; pub mod writer; + +/// Parses the given HTML string and returns a handle to the resulting DOM tree. +pub fn html_compile(html: &str) -> DocumentHandle { + let mut stream = ByteStream::new(); + stream.read_from_str(html, Some(Encoding::UTF8)); + stream.close(); + + let document = DocumentBuilder::new_document(None); + let _ = Html5Parser::parse_document(&mut stream, Document::clone(&document), None); + + document +} diff --git a/crates/gosub_render_utils/Cargo.toml b/crates/gosub_render_utils/Cargo.toml index f735b4678..2cf2ef8eb 100644 --- a/crates/gosub_render_utils/Cargo.toml +++ b/crates/gosub_render_utils/Cargo.toml @@ -8,6 +8,7 @@ license = "MIT" [dependencies] gosub_html5 = { path = "../gosub_html5" } gosub_styling = { path = "../gosub_styling" } +gosub_css3 = { path = "../gosub_css3" } gosub_render_backend = { path = "../gosub_render_backend" } anyhow = "1.0.86" regex = "1.10.6" diff --git a/crates/gosub_render_utils/src/style/parse.rs b/crates/gosub_render_utils/src/style/parse.rs new file mode 100644 index 000000000..b8685081b --- /dev/null +++ b/crates/gosub_render_utils/src/style/parse.rs @@ -0,0 +1,207 @@ +use taffy::prelude::*; +use taffy::{ + AlignContent, AlignItems, Dimension, GridPlacement, LengthPercentage, LengthPercentageAuto, + TrackSizingFunction, +}; + +use gosub_render_backend::{PreRenderText, RenderBackend}; +// use gosub_styling::css_values::CssValue; +use gosub_css3::stylesheet::CssValue; +use gosub_styling::render_tree::{RenderTreeNode, TextData}; + +pub(crate) fn parse_len( + node: &mut RenderTreeNode, + name: &str, +) -> LengthPercentage { + let Some(property) = node.get_property(name) else { + return LengthPercentage::Length(0.0); + }; + + property.compute_value(); + + match &property.actual { + CssValue::Percentage(value) => LengthPercentage::Percent(*value), + CssValue::Unit(..) => LengthPercentage::Length(property.actual.unit_to_px()), + CssValue::String(_) => LengthPercentage::Length(property.actual.unit_to_px()), //HACK + _ => LengthPercentage::Length(0.0), + } +} + +pub(crate) fn parse_len_auto( + node: &mut RenderTreeNode, + name: &str, +) -> LengthPercentageAuto { + let Some(property) = node.get_property(name) else { + return LengthPercentageAuto::Auto; + }; + + property.compute_value(); + + match &property.actual { + CssValue::String(value) => match value.as_str() { + "auto" => LengthPercentageAuto::Auto, + _ => LengthPercentageAuto::Length(property.actual.unit_to_px()), //HACK + }, + CssValue::Percentage(value) => LengthPercentageAuto::Percent(*value), + CssValue::Unit(..) => LengthPercentageAuto::Length(property.actual.unit_to_px()), + _ => LengthPercentageAuto::Auto, + } +} + +pub(crate) fn parse_dimension( + node: &mut RenderTreeNode, + name: &str, +) -> Dimension { + let Some(property) = node.get_property(name) else { + return Dimension::Auto; + }; + + property.compute_value(); + + match &property.actual { + CssValue::String(value) => match value.as_str() { + "auto" => Dimension::Auto, + s if s.ends_with('%') => { + let value = s.trim_end_matches('%').parse::().unwrap_or(0.0); + Dimension::Percent(value) + } + _ => Dimension::Length(property.actual.unit_to_px()), //HACK + }, + CssValue::Percentage(value) => Dimension::Percent(*value), + CssValue::Unit(..) => Dimension::Length(property.actual.unit_to_px()), + _ => Dimension::Auto, + } +} + +pub(crate) fn parse_text_dim(text: &mut TextData, name: &str) -> Dimension { + let size = text.prerender.prerender(); + + if name == "width" || name == "max-width" || name == "min-width" { + Dimension::Length(size.width) + } else if name == "height" || name == "max-height" || name == "min-height" { + Dimension::Length(size.height) + } else { + Dimension::Auto + } +} + +pub(crate) fn parse_align_i( + node: &mut RenderTreeNode, + name: &str, +) -> Option { + let display = node.get_property(name)?; + display.compute_value(); + + let CssValue::String(ref value) = display.actual else { + return None; + }; + + match value.as_str() { + "start" => Some(AlignItems::Start), + "end" => Some(AlignItems::End), + "flex-start" => Some(AlignItems::FlexStart), + "flex-end" => Some(AlignItems::FlexEnd), + "center" => Some(AlignItems::Center), + "baseline" => Some(AlignItems::Baseline), + "stretch" => Some(AlignItems::Stretch), + _ => None, + } +} + +pub(crate) fn parse_align_c( + node: &mut RenderTreeNode, + name: &str, +) -> Option { + let display = node.get_property(name)?; + + display.compute_value(); + + let CssValue::String(ref value) = display.actual else { + return None; + }; + + match value.as_str() { + "start" => Some(AlignContent::Start), + "end" => Some(AlignContent::End), + "flex-start" => Some(AlignContent::FlexStart), + "flex-end" => Some(AlignContent::FlexEnd), + "center" => Some(AlignContent::Center), + "stretch" => Some(AlignContent::Stretch), + "space-between" => Some(AlignContent::SpaceBetween), + "space-around" => Some(AlignContent::SpaceAround), + _ => None, + } +} + +pub(crate) fn parse_tracking_sizing_function( + node: &mut RenderTreeNode, + name: &str, +) -> Vec { + let Some(display) = node.get_property(name) else { + return Vec::new(); + }; + + display.compute_value(); + + let CssValue::String(ref _value) = display.actual else { + return Vec::new(); + }; + + Vec::new() //TODO: Implement this +} + +#[allow(dead_code)] +pub(crate) fn parse_non_repeated_tracking_sizing_function( + _node: &mut RenderTreeNode, + _name: &str, +) -> NonRepeatedTrackSizingFunction { + todo!("implement parse_non_repeated_tracking_sizing_function") +} + +pub(crate) fn parse_grid_auto( + node: &mut RenderTreeNode, + name: &str, +) -> Vec { + let Some(display) = node.get_property(name) else { + return Vec::new(); + }; + + display.compute_value(); + + let CssValue::String(ref _value) = display.actual else { + return Vec::new(); + }; + + Vec::new() //TODO: Implement this +} + +pub(crate) fn parse_grid_placement( + node: &mut RenderTreeNode, + name: &str, +) -> GridPlacement { + let Some(display) = node.get_property(name) else { + return GridPlacement::Auto; + }; + + display.compute_value(); + + match &display.actual { + CssValue::String(value) => { + if value.starts_with("span") { + let value = value.trim_start_matches("span").trim(); + + if let Ok(value) = value.parse::() { + GridPlacement::from_span(value) + } else { + GridPlacement::Auto + } + } else if let Ok(value) = value.parse::() { + GridPlacement::from_line_index(value) + } else { + GridPlacement::Auto + } + } + CssValue::Number(value) => GridPlacement::from_line_index((*value) as i16), + _ => GridPlacement::Auto, + } +} diff --git a/crates/gosub_renderer/Cargo.toml b/crates/gosub_renderer/Cargo.toml index c567ac8b7..ee22a16f2 100644 --- a/crates/gosub_renderer/Cargo.toml +++ b/crates/gosub_renderer/Cargo.toml @@ -12,6 +12,7 @@ gosub_rendering = { path = "../gosub_render_utils" } gosub_html5 = { path = "../gosub_html5" } gosub_shared = { path = "../gosub_shared" } gosub_styling = { path = "../gosub_styling" } +gosub_css3 = { path = "../gosub_css3" } gosub_net = { path = "../gosub_net" } gosub_render_backend = { path = "../gosub_render_backend" } anyhow = "1.0.86" diff --git a/crates/gosub_renderer/src/draw.rs b/crates/gosub_renderer/src/draw.rs index 8aa9a039e..0f95ac042 100644 --- a/crates/gosub_renderer/src/draw.rs +++ b/crates/gosub_renderer/src/draw.rs @@ -6,6 +6,8 @@ use url::Url; use gosub_html5::node::NodeId; use gosub_render_backend::geo::{Size, SizeU32, FP}; use gosub_render_backend::layout::{Layout, LayoutTree, Layouter}; +use gosub_css3::colors::RgbColor; +use gosub_css3::stylesheet::CssValue; use gosub_render_backend::svg::SvgRenderer; use gosub_render_backend::{ Border, BorderSide, BorderStyle, Brush, Color, ImageBuffer, NodeDesc, Rect, RenderBackend, @@ -13,8 +15,6 @@ use gosub_render_backend::{ }; use gosub_rendering::position::PositionTree; use gosub_shared::types::Result; -use gosub_styling::css_colors::RgbColor; -use gosub_styling::css_values::CssValue; use gosub_styling::render_tree::{RenderNodeData, RenderTree, RenderTreeNode}; use crate::draw::img::request_img; diff --git a/crates/gosub_renderer/src/render_tree.rs b/crates/gosub_renderer/src/render_tree.rs index c945e3fb1..ca5e7c9c3 100644 --- a/crates/gosub_renderer/src/render_tree.rs +++ b/crates/gosub_renderer/src/render_tree.rs @@ -12,8 +12,8 @@ use gosub_render_backend::layout::Layouter; use gosub_render_backend::RenderBackend; use gosub_rendering::position::PositionTree; use gosub_shared::byte_stream::{ByteStream, Confidence, Encoding}; -use gosub_styling::css_values::CssProperties; use gosub_styling::render_tree::{generate_render_tree, RenderNodeData, RenderTree}; +use gosub_styling::styling::CssProperties; pub struct TreeDrawer { pub(crate) tree: RenderTree, diff --git a/crates/gosub_styling/Cargo.toml b/crates/gosub_styling/Cargo.toml index d5edc27cb..c86f8a3ba 100644 --- a/crates/gosub_styling/Cargo.toml +++ b/crates/gosub_styling/Cargo.toml @@ -15,9 +15,19 @@ lazy_static = "1.5" anyhow = "1.0.86" regex = "1.10.6" colors-transform = "0.2.11" +vello = "0.1.0" +backtrace = "0.3.71" +log = "0.4.21" +rand = "0.9.0-alpha.1" #[target.'cfg(target_arch = "wasm32")'.dependencies] #web-sys = { version = "0.3.69", features = ["fontface"] } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] -rust-fontconfig = "0.1.7" \ No newline at end of file +rust-fontconfig = "0.1.7" +itertools = "0.10.5" +serde = { version = "1.0.197", features = ["derive"] } +serde_json = "1.0.115" +memoize = "0.4.2" +thiserror = "1.0.58" +nom = "7.1.3" diff --git a/crates/gosub_styling/resources/definitions/definitions.json b/crates/gosub_styling/resources/definitions/definitions.json new file mode 100644 index 000000000..4af50bcdf --- /dev/null +++ b/crates/gosub_styling/resources/definitions/definitions.json @@ -0,0 +1,8036 @@ +{ + "properties": [ + { + "name": "border-clip-top", + "syntax": "normal | [ | ]+", + "computed": [], + "initial": "normal", + "inherited": false + }, + { + "name": "-webkit-mask-box-image-width", + "syntax": "", + "computed": [], + "initial": "", + "inherited": false + }, + { + "name": "-webkit-mask-image", + "syntax": "", + "computed": [ + "absoluteURIOrNone" + ], + "initial": "none", + "inherited": false + }, + { + "name": "object-fit", + "syntax": "fill | contain | cover | none | scale-down", + "computed": [ + "asSpecified" + ], + "initial": "fill", + "inherited": false + }, + { + "name": "outline", + "syntax": "[ <'outline-width'> || <'outline-style'> || <'outline-color'> ]", + "computed": [ + "outline-color", + "outline-width", + "outline-style" + ], + "initial": [ + "outline-color", + "outline-style", + "outline-width" + ], + "inherited": false + }, + { + "name": "white-space", + "syntax": "normal | pre | nowrap | pre-wrap | break-spaces | pre-line", + "computed": [ + "asSpecified" + ], + "initial": "normal", + "inherited": true + }, + { + "name": "text-indent", + "syntax": "[ ] && hanging? && each-line?", + "computed": [ + "percentageOrAbsoluteLengthPlusKeywords" + ], + "initial": "0", + "inherited": true + }, + { + "name": "column-span", + "syntax": "none | all", + "computed": [ + "asSpecified" + ], + "initial": "none", + "inherited": false + }, + { + "name": "shape-rendering", + "syntax": "auto | optimizeSpeed | crispEdges | geometricPrecision", + "computed": [], + "initial": "auto", + "inherited": true + }, + { + "name": "shape-outside", + "syntax": "none | [ || ] | ", + "computed": [ + "asDefinedForBasicShapeWithAbsoluteURIOtherwiseAsSpecified" + ], + "initial": "none", + "inherited": false + }, + { + "name": "min-height", + "syntax": "auto | | min-content | max-content | fit-content() | ", + "computed": [ + "percentageAsSpecifiedOrAbsoluteLength" + ], + "initial": "auto", + "inherited": false + }, + { + "name": "fill-rule", + "syntax": "nonzero | evenodd", + "computed": [], + "initial": "nonzero", + "inherited": true + }, + { + "name": "box-shadow", + "syntax": "#", + "computed": [ + "absoluteLengthsSpecifiedColorAsSpecified" + ], + "initial": "none", + "inherited": false + }, + { + "name": "dominant-baseline", + "syntax": "auto | text-bottom | alphabetic | ideographic | middle | central | mathematical | hanging | text-top", + "computed": [], + "initial": "auto", + "inherited": true + }, + { + "name": "stroke-dash-justify", + "syntax": "none | [ stretch | compress ] || [ dashes || gaps ]", + "computed": [], + "initial": "none", + "inherited": true + }, + { + "name": "stroke-opacity", + "syntax": "<'opacity'>", + "computed": [], + "initial": "1", + "inherited": true + }, + { + "name": "speak-as", + "syntax": "normal | spell-out || digits || [ literal-punctuation | no-punctuation ]", + "computed": [], + "initial": "normal", + "inherited": true + }, + { + "name": "column-rule-style", + "syntax": "", + "computed": [ + "asSpecified" + ], + "initial": "none", + "inherited": false + }, + { + "name": "float-defer", + "syntax": " | last | none", + "computed": [], + "initial": "none", + "inherited": false + }, + { + "name": "right", + "syntax": "auto | ", + "computed": [ + "lengthAbsolutePercentageAsSpecifiedOtherwiseAuto" + ], + "initial": "auto", + "inherited": false + }, + { + "name": "cursor", + "syntax": "[ [ | ] [ ]? ]#? [ auto | default | none | context-menu | help | pointer | progress | wait | cell | crosshair | text | vertical-text | alias | copy | move | no-drop | not-allowed | grab | grabbing | e-resize | n-resize | ne-resize | nw-resize | s-resize | se-resize | sw-resize | w-resize | ew-resize | ns-resize | nesw-resize | nwse-resize | col-resize | row-resize | all-scroll | zoom-in | zoom-out ]", + "computed": [ + "asSpecifiedURLsAbsolute" + ], + "initial": "auto", + "inherited": true + }, + { + "name": "grid-row-start", + "syntax": "", + "computed": [ + "asSpecified" + ], + "initial": "auto", + "inherited": false + }, + { + "name": "scroll-margin-top", + "syntax": "", + "computed": [ + "asSpecified" + ], + "initial": "0", + "inherited": false + }, + { + "name": "zoom", + "syntax": " || ", + "computed": [ + "asSpecified" + ], + "initial": "normal", + "inherited": false + }, + { + "name": "flex-direction", + "syntax": "row | row-reverse | column | column-reverse", + "computed": [ + "asSpecified" + ], + "initial": "row", + "inherited": false + }, + { + "name": "flex-flow", + "syntax": "<'flex-direction'> || <'flex-wrap'>", + "computed": [ + "flex-direction", + "flex-wrap" + ], + "initial": [ + "flex-direction", + "flex-wrap" + ], + "inherited": false + }, + { + "name": "offset-path", + "syntax": "none | || ", + "computed": [ + "asSpecified" + ], + "initial": "none", + "inherited": false + }, + { + "name": "rest", + "syntax": "<'rest-before'> <'rest-after'>?", + "computed": [], + "initial": "N/A (see individual properties)", + "inherited": false + }, + { + "name": "region-fragment", + "syntax": "auto | break", + "computed": [], + "initial": "auto", + "inherited": false + }, + { + "name": "column-rule", + "syntax": "<'column-rule-width'> || <'column-rule-style'> || <'column-rule-color'>", + "computed": [ + "column-rule-color", + "column-rule-style", + "column-rule-width" + ], + "initial": [ + "column-rule-width", + "column-rule-style", + "column-rule-color" + ], + "inherited": false + }, + { + "name": "pause-before", + "syntax": "