From fc342147ea686dc3b4ca8badab5cae9021f2305e Mon Sep 17 00:00:00 2001 From: Joshua Thijssen Date: Mon, 12 Aug 2024 20:17:00 +0200 Subject: [PATCH] Initial implementation of the CSS3 engine Co-Authored-By: Shark --- Cargo.lock | 189 +- Cargo.toml | 4 +- crates/gosub_css3/Cargo.toml | 1 + .../src/colors.rs} | 110 +- .../gosub_css3/src/convert/ast_converter.rs | 110 +- crates/gosub_css3/src/lib.rs | 1 + crates/gosub_css3/src/stylesheet.rs | 335 +- crates/gosub_html5/src/lib.rs | 16 + crates/gosub_render_utils/Cargo.toml | 1 + crates/gosub_render_utils/src/style/parse.rs | 207 + crates/gosub_renderer/Cargo.toml | 1 + crates/gosub_renderer/src/draw.rs | 4 +- crates/gosub_renderer/src/render_tree.rs | 2 +- crates/gosub_styling/Cargo.toml | 11 +- crates/gosub_styling/README.md | 55 + .../resources/definitions/definitions.json | 8036 +++++++++++++++++ .../definitions/definitions_at-rules.json | 663 ++ .../definitions/definitions_properties.json | 5604 ++++++++++++ .../definitions/definitions_selectors.json | 329 + .../definitions/definitions_values.json | 1438 +++ .../gosub_styling/resources}/useragent.css | 0 crates/gosub_styling/src/errors.rs | 8 + crates/gosub_styling/src/lib.rs | 20 +- .../gosub_styling/src/property_definitions.rs | 843 ++ crates/gosub_styling/src/property_list.rs | 577 -- crates/gosub_styling/src/render_tree.rs | 103 +- crates/gosub_styling/src/shorthands.rs | 312 - .../src/{css_values.rs => styling.rs} | 220 +- crates/gosub_styling/src/syntax.rs | 2445 +++++ crates/gosub_styling/src/syntax_matcher.rs | 1461 +++ .../tools/generate_definitions/go.mod | 3 + .../tools/generate_definitions/main.go | 159 + .../tools/generate_definitions/mdn/mdn.go | 41 + .../patch/patch_darwin.go | 5 + .../generate_definitions/patch/patch_linux.go | 29 + .../patch/patch_windows.go | 5 + .../generate_definitions/patch/patcher.go | 376 + .../tools/generate_definitions/specs/specs.go | 116 + .../utils/stringmaybearray.go | 22 + .../tools/generate_definitions/utils/types.go | 44 + .../tools/generate_definitions/utils/utils.go | 54 + .../generate_definitions/webref/webref.go | 446 + docs/css_properties.md | 107 + 43 files changed, 23453 insertions(+), 1060 deletions(-) rename crates/{gosub_styling/src/css_colors.rs => gosub_css3/src/colors.rs} (90%) create mode 100644 crates/gosub_render_utils/src/style/parse.rs create mode 100644 crates/gosub_styling/README.md create mode 100644 crates/gosub_styling/resources/definitions/definitions.json create mode 100644 crates/gosub_styling/resources/definitions/definitions_at-rules.json create mode 100644 crates/gosub_styling/resources/definitions/definitions_properties.json create mode 100644 crates/gosub_styling/resources/definitions/definitions_selectors.json create mode 100644 crates/gosub_styling/resources/definitions/definitions_values.json rename {resources => crates/gosub_styling/resources}/useragent.css (100%) create mode 100644 crates/gosub_styling/src/errors.rs create mode 100644 crates/gosub_styling/src/property_definitions.rs delete mode 100644 crates/gosub_styling/src/property_list.rs delete mode 100644 crates/gosub_styling/src/shorthands.rs rename crates/gosub_styling/src/{css_values.rs => styling.rs} (76%) create mode 100644 crates/gosub_styling/src/syntax.rs create mode 100644 crates/gosub_styling/src/syntax_matcher.rs create mode 100644 crates/gosub_styling/tools/generate_definitions/go.mod create mode 100644 crates/gosub_styling/tools/generate_definitions/main.go create mode 100644 crates/gosub_styling/tools/generate_definitions/mdn/mdn.go create mode 100644 crates/gosub_styling/tools/generate_definitions/patch/patch_darwin.go create mode 100644 crates/gosub_styling/tools/generate_definitions/patch/patch_linux.go create mode 100644 crates/gosub_styling/tools/generate_definitions/patch/patch_windows.go create mode 100644 crates/gosub_styling/tools/generate_definitions/patch/patcher.go create mode 100644 crates/gosub_styling/tools/generate_definitions/specs/specs.go create mode 100644 crates/gosub_styling/tools/generate_definitions/utils/stringmaybearray.go create mode 100644 crates/gosub_styling/tools/generate_definitions/utils/types.go create mode 100644 crates/gosub_styling/tools/generate_definitions/utils/utils.go create mode 100644 crates/gosub_styling/tools/generate_definitions/webref/webref.go create mode 100644 docs/css_properties.md diff --git a/Cargo.lock b/Cargo.lock index 245b05ed8..5435d5f0c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -68,6 +68,17 @@ dependencies = [ "subtle", ] +[[package]] +name = "ahash" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" +dependencies = [ + "getrandom", + "once_cell", + "version_check", +] + [[package]] name = "ahash" version = "0.8.11" @@ -270,6 +281,12 @@ version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d151e35f61089500b617991b791fc8bfd237ae50cd5950803758a179b41e67a" +[[package]] +name = "arrayvec" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" + [[package]] name = "arrayvec" version = "0.7.4" @@ -321,9 +338,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6678909d8c5d46a42abcf571271e15fdbc0a225e3646cf23762cd415046c78bf" dependencies = [ "anyhow", - "arrayvec", + "arrayvec 0.7.4", "log", - "nom", + "nom 7.1.3", "num-rational", "v_frame", ] @@ -334,7 +351,7 @@ version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "876c75a42f6364451a033496a14c44bffe41f5f4a8236f697391f11024e596d2" dependencies = [ - "arrayvec", + "arrayvec 0.7.4", ] [[package]] @@ -573,7 +590,7 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" dependencies = [ - "nom", + "nom 7.1.3", ] [[package]] @@ -1500,6 +1517,7 @@ name = "gosub_css3" version = "0.1.0" dependencies = [ "anyhow", + "colors-transform", "gosub_shared", "lazy_static", "log", @@ -1537,6 +1555,8 @@ dependencies = [ "gosub_webexecutor", "js-sys", "lazy_static", + "nom 7.1.3", + "nom-trace", "regex", "serde", "serde_derive", @@ -1612,6 +1632,7 @@ name = "gosub_renderer" version = "0.1.0" dependencies = [ "anyhow", + "gosub_css3", "gosub_html5", "gosub_net", "gosub_render_backend", @@ -1629,6 +1650,7 @@ name = "gosub_rendering" version = "0.1.0" dependencies = [ "anyhow", + "gosub_css3", "gosub_html5", "gosub_render_backend", "gosub_styling", @@ -1658,15 +1680,24 @@ name = "gosub_styling" version = "0.1.0" dependencies = [ "anyhow", + "backtrace", "colors-transform", "gosub_css3", "gosub_html5", "gosub_render_backend", "gosub_shared", "gosub_typeface", + "itertools 0.10.5", "lazy_static", + "log", + "memoize", + "nom 7.1.3", + "rand 0.9.0-alpha.2", "regex", "rust-fontconfig", + "serde", + "serde_json", + "thiserror", ] [[package]] @@ -1701,7 +1732,7 @@ dependencies = [ "gosub_html5", "gosub_shared", "lazy_static", - "nom", + "nom 7.1.3", "nom_locate", "regex", "serde", @@ -1757,9 +1788,9 @@ dependencies = [ "image", "raw-window-handle", "smallvec", - "vello", + "vello", "vello_svg", - "wgpu", + "wgpu", ] [[package]] @@ -1829,8 +1860,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c08c1f623a8d0b722b8b99f821eb0ba672a1618f0d3b16ddbee1cedd2dd8557" dependencies = [ "bitflags 2.6.0", - "gpu-descriptor-types", - "hashbrown", + "gpu-descriptor-types", + "hashbrown 0.14.5", ] [[package]] @@ -1886,13 +1917,22 @@ dependencies = [ "byteorder", ] +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash 0.7.8", +] + [[package]] name = "hashbrown" version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" dependencies = [ - "ahash", + "ahash 0.8.11", "allocator-api2", ] @@ -2117,7 +2157,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.14.5", ] [[package]] @@ -2268,7 +2308,7 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e5aa9f0f96a938266bdb12928a67169e8d22c6a786fda8ed984b85e6ba93c3c" dependencies = [ - "arrayvec", + "arrayvec 0.7.4", "smallvec", ] @@ -2290,6 +2330,19 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8" +[[package]] +name = "lexical-core" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6607c62aa161d23d17a9072cc5da0be67cdfc89d3afb1e8d9c842bebc2525ffe" +dependencies = [ + "arrayvec 0.5.2", + "bitflags 1.3.2", + "cfg-if", + "ryu", + "static_assertions", +] + [[package]] name = "libc" version = "0.2.155" @@ -2387,6 +2440,15 @@ dependencies = [ "imgref", ] +[[package]] +name = "lru" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999beba7b6e8345721bd280141ed958096a2e4abdf74f67ff4ce49b4b54e47a" +dependencies = [ + "hashbrown 0.12.3", +] + [[package]] name = "lru-cache" version = "0.1.2" @@ -2435,6 +2497,29 @@ dependencies = [ "libc", ] +[[package]] +name = "memoize" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5df4051db13d0816cf23196d3baa216385ae099339f5d0645a8d9ff2305e82b8" +dependencies = [ + "lazy_static", + "lru", + "memoize-inner", +] + +[[package]] +name = "memoize-inner" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27bdece7e91f0d1e33df7b46ec187a93ea0d4e642113a1039ac8bfdd4a3273ac" +dependencies = [ + "lazy_static", + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "metal" version = "0.28.0" @@ -2494,7 +2579,7 @@ version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e536ae46fcab0876853bd4a632ede5df4b1c2527a58f6c5a4150fe86be858231" dependencies = [ - "arrayvec", + "arrayvec 0.7.4", "bit-set", "bitflags 2.6.0", "codespan-reporting", @@ -2554,6 +2639,17 @@ version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" +[[package]] +name = "nom" +version = "5.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08959a387a676302eebf4ddbcbc611da04285579f76f88ee0506c63b1a61dd4b" +dependencies = [ + "lexical-core", + "memchr", + "version_check", +] + [[package]] name = "nom" version = "7.1.3" @@ -2564,6 +2660,15 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "nom-trace" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5670812706767ed69c4a4b8f520e8743b3aeee70d9f22adf63d0194a386c12f2" +dependencies = [ + "nom 5.1.3", +] + [[package]] name = "nom_locate" version = "4.2.0" @@ -2572,7 +2677,7 @@ checksum = "1e3c83c053b0713da60c5b8de47fe8e494fe3ece5267b2f23090a07a053ba8f3" dependencies = [ "bytecount", "memchr", - "nom", + "nom 7.1.3", ] [[package]] @@ -3354,7 +3459,7 @@ checksum = "cd87ce80a7665b1cce111f8a16c1f3929f6547ce91ade6addf4ec86a8dda5ce9" dependencies = [ "arbitrary", "arg_enum_proc_macro", - "arrayvec", + "arrayvec 0.7.4", "av1-grain", "bitstream-io", "built", @@ -3428,7 +3533,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8b8af39d1f23869711ad4cea5e7835a20daa987f80232f7f2a2374d648ca64d" dependencies = [ "bytemuck", - "font-types", + "font-types", ] [[package]] @@ -3805,7 +3910,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ab45fb68b53576a43d4fc0e9ec8ea64e29a4d2cc7f44506964cb75f288222e9" dependencies = [ "bytemuck", - "read-fonts", + "read-fonts", ] [[package]] @@ -4012,7 +4117,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9cb893bff0f80ae17d3a57e030622a967b8dbc90e38284d9b4b1442e23873c94" dependencies = [ - "arrayvec", + "arrayvec 0.7.4", "grid", "num-traits", "serde", @@ -4147,7 +4252,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "83d13394d44dae3207b52a326c0c85a8bf87f1541f23b0d143811088497b09ab" dependencies = [ "arrayref", - "arrayvec", + "arrayvec 0.7.4", "bytemuck", "cfg-if", "log", @@ -4517,12 +4622,12 @@ dependencies = [ "log", "peniko", "raw-window-handle", - "skrifa", + "skrifa", "static_assertions", "thiserror", - "vello_encoding", + "vello_encoding", "vello_shaders", - "wgpu", + "wgpu", ] [[package]] @@ -4534,7 +4639,7 @@ dependencies = [ "bytemuck", "guillotiere", "peniko", - "skrifa", + "skrifa", "smallvec", ] @@ -4545,9 +4650,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13ab6bcb2b079c3cf57e964d1ba0b1f08901284be1c7f5cba34d3e0e08154bce" dependencies = [ "bytemuck", - "naga", + "naga", "thiserror", - "vello_encoding", + "vello_encoding", ] [[package]] @@ -4559,7 +4664,7 @@ dependencies = [ "image", "thiserror", "usvg", - "vello", + "vello", ] [[package]] @@ -4831,13 +4936,13 @@ version = "0.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90e37c7b9921b75dfd26dd973fdcbce36f13dfa6e2dc82aece584e0ed48c355c" dependencies = [ - "arrayvec", + "arrayvec 0.7.4", "cfg-if", "cfg_aliases 0.1.1", "document-features", "js-sys", "log", - "naga", + "naga", "parking_lot", "profiling", "raw-window-handle", @@ -4846,9 +4951,9 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "wgpu-core", - "wgpu-hal", - "wgpu-types", + "wgpu-core", + "wgpu-hal", + "wgpu-types", ] [[package]] @@ -4857,7 +4962,7 @@ version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d50819ab545b867d8a454d1d756b90cd5f15da1f2943334ca314af10583c9d39" dependencies = [ - "arrayvec", + "arrayvec 0.7.4", "bit-vec", "bitflags 2.6.0", "cfg_aliases 0.1.1", @@ -4865,7 +4970,7 @@ dependencies = [ "document-features", "indexmap", "log", - "naga", + "naga", "once_cell", "parking_lot", "profiling", @@ -4874,8 +4979,8 @@ dependencies = [ "smallvec", "thiserror", "web-sys", - "wgpu-hal", - "wgpu-types", + "wgpu-hal", + "wgpu-types", ] [[package]] @@ -4885,27 +4990,27 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "172e490a87295564f3fcc0f165798d87386f6231b04d4548bca458cbbfd63222" dependencies = [ "android_system_properties", - "arrayvec", + "arrayvec 0.7.4", "ash", "bit-set", "bitflags 2.6.0", "block", "cfg_aliases 0.1.1", "core-graphics-types", - "d3d12", + "d3d12", "glow", "glutin_wgl_sys", "gpu-alloc", "gpu-allocator", - "gpu-descriptor", + "gpu-descriptor", "hassle-rs", "js-sys", "khronos-egl", "libc", "libloading 0.8.5", "log", - "metal", - "naga", + "metal", + "naga", "ndk-sys 0.5.0+25.2.9519653", "objc", "once_cell", @@ -4919,7 +5024,7 @@ dependencies = [ "thiserror", "wasm-bindgen", "web-sys", - "wgpu-types", + "wgpu-types", "winapi", ] @@ -5231,7 +5336,7 @@ version = "0.30.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4225ddd8ab67b8b59a2fee4b34889ebf13c0460c1c3fa297c58e21eb87801b33" dependencies = [ - "ahash", + "ahash 0.8.11", "android-activity", "atomic-waker", "bitflags 2.6.0", 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..514364fb9 100644 --- a/crates/gosub_renderer/src/draw.rs +++ b/crates/gosub_renderer/src/draw.rs @@ -3,6 +3,8 @@ use std::sync::mpsc::Sender; use anyhow::anyhow; use url::Url; +use gosub_css3::colors::RgbColor; +use gosub_css3::stylesheet::CssValue; use gosub_html5::node::NodeId; use gosub_render_backend::geo::{Size, SizeU32, FP}; use gosub_render_backend::layout::{Layout, LayoutTree, Layouter}; @@ -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..3cf3f76a4 100644 --- a/crates/gosub_styling/Cargo.toml +++ b/crates/gosub_styling/Cargo.toml @@ -15,9 +15,18 @@ lazy_static = "1.5" anyhow = "1.0.86" regex = "1.10.6" colors-transform = "0.2.11" +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/README.md b/crates/gosub_styling/README.md new file mode 100644 index 000000000..a6b7cdcd0 --- /dev/null +++ b/crates/gosub_styling/README.md @@ -0,0 +1,55 @@ +# Gosub Styling crate + +This crate holds the CSS3 styling functionality. It allows the engine to query CSS3 properties and returns the correct values based +on the HTML and CSS3 documents parsed. + +## It consists of the following parts: + + - The CSS property definitions, as found in `resources/definitions`. This holds the syntax for each CSS property. + - Css Value Syntax parser, which is used to parse the CSS property definitions syntax. + - CSs Matcher that matches CSS values (ie: `1px solid red`) to the CSS property definitions. + + +## Still missing, but needs to be added: + + - API for querying CSS properties, and returning the correct values, minding things like inherited and initial values. + - API for querying CSS properties at different levels. So normally we want to use the `"actual"` value, but we also need to + be able to query the `"computed"` value, and the `"used"` value. This is the case in for instance developer toolbars and such. + + +> Note that this crate might be merged later on with the gosub_ccs3 crate. + +# Css Property Definitions + +The CSS property definitions are generated from the CSS3 specification and MDN, and are stored in the `resources/definitions` folder. +The tool to generate this data can be found in the `tools/generate_definitions` folder. The definitions are generated in a JSON format, +which is then parsed on each run for now. Each property holds a `CSS Syntax Tree` that is used to match against CSS values. + +An example definition looks like this: + +```json + { + "name": "border-clip-top", + "syntax": "normal | [ | ]+", + "computed": [], + "initial": "normal", + "inherited": false + } +``` + +This defines the `border-clip-top` property. It's syntax tells us that it can have the value `normal`, or a list of `` or `` +values. The bracketed values are actually other properties or values that are definined in the json as well. Some of these values are built-in properties, +like ``, ``, `` etc. + +There are no computed values, meaning normally that there are no special rules for computing the value, and that there are no shorthand elements. +For instance, the `"border"` property has three computed elements: `border-color`, `border-style` and `border-width`, and thus results in three different +properties to be filled as well. + +The initial value of `border-clip-top` is `normal`, and it is not inherited, so it does not inherit the value from the parent element. + + +# Css Value Syntax Parser + +In order to check if the CSS property value is correct, we first need to parse the value syntax for that property. Even though this syntax is fairly +simple in setup, we use a nom-parser system (`syntax.rs`) to parse the syntax. This results in a `CSS Syntax Tree` that can be used to match against +the CSS values. This syntax matching is done within `syntax_matcher.rs`. 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": "