diff --git a/crates/core/src/engine/candidate.rs b/crates/core/src/engine/candidate.rs new file mode 100644 index 000000000..a0527d2dc --- /dev/null +++ b/crates/core/src/engine/candidate.rs @@ -0,0 +1,329 @@ +use std::{iter::empty, rc::Rc, sync::Arc}; +use bstr::ByteSlice; + +use crate::util::segment; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Candidate { + raw: Vec, + important: bool, + variants: Vec, + utilities: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Variant { + /// Arbitrary variants are variants that take a selector and generate a variant + /// on the fly. + /// + /// E.g.: `[&_p]` + Arbitrary { + selector: Vec, + + /// If true, it can be applied as a child of a compound variant + compounds: bool, + + /// Whether or not the selector is a relative selector + /// @see https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_selectors/Selector_structure#relative_selector + relative: bool, + }, + + /// Static variants are variants that don't take any arguments. + /// + /// E.g.: `hover` + Static { + root: Vec, + compounds: bool, + }, + + /// Functional variants are variants that can take an argument. The argument is + /// either a named variant value or an arbitrary variant value. + /// + /// E.g.: + /// + /// - `aria-disabled` + /// - `aria-[disabled]` + /// - `@container-size` -> @container, with named value `size` + /// - `@container-[inline-size]` -> @container, with arbitrary variant value `inline-size` + /// - `@container` -> @container, with no value + Functional { + root: Vec, + value: Option, + modifier: Option, + + /// If true, it can be applied as a child of a compound variant + compounds: bool, + }, + + /// Compound variants are variants that take another variant as an argument. + /// + /// E.g.: + /// + /// - `has-[&_p]` + /// - `group-*` + /// - `peer-*` + Compound { + root: Vec, + variant: Box, + modifier: Option, + + /// If true, it can be applied as a child of a compound variant + compounds: bool, + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Utility { + /// Arbitrary candidates are candidates that register utilities on the fly with + /// a property and a value. + /// + /// Examples: + /// - `[color:red]` + /// - `[color:red]/50` + /// - `[color:red]/50!` + Arbitrary { + property: Vec, + value: Vec, + modifier: Option, + }, + + /// Static candidates are candidates that don't take any arguments. + /// + /// Examples: + /// - `underline` + /// - `flex` + Static { + root: Vec, + }, + + /// Static candidates are candidates that don't take any arguments. + /// + /// Examples: + /// - `underline` + /// - `flex` + Functional { + root: Vec, + value: Option, + modifier: Option, + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum UtilityValue { + Arbitrary { + /// bg-[color:--my-color] + /// ^^^^^ + data_type: Option>, + + /// bg-[#0088cc] + /// ^^^^^^^ + /// bg-[var(--my_variable)] + /// ^^^^^^^^^^^^^^^^^^ + value: Vec, + }, + + Named { + /// bg-red-500 + /// ^^^^^^^ + /// + /// w-1/2 + /// ^ + value: Vec, + + /// w-1/2 + /// ^^^ + fraction: Option> + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum VariantValue { + Arbitrary { + value: Vec, + }, + + Named { + value: Vec, + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum CandidateModifier { + Arbitrary { + /// bg-red-500/[50%] + /// ^^^ + value: Vec + }, + + Named { + /// bg-red-500/50 + /// ^^ + value: Vec, + } +} + +pub struct DesignSystem { + prefix: Option>, + utilities: Utilities, +} + +pub struct Utilities { + // +} + +impl Utilities { + pub fn has(&self, utility: &[u8]) -> bool { + false + } +} + +pub fn parse_candidate(input: &[u8], design: Rc) -> Option { + let raw = input.to_vec(); + + // hover:focus:underline + // ^^^^^ ^^^^^^ -> Variants + // ^^^^^^^^^ -> Base + let mut raw_variants = segment(input, b':'); + + if let Some(prefix) = &design.prefix { + let Some(new_variants) = raw_variants.strip_prefix(prefix) else { + return None; + }; + + if new_variants.is_empty() { + return None; + } + + raw_variants = new_variants.to_vec(); + } + + // Safety: At this point it is safe to use TypeScript's non-null assertion + // operator because even if the `input` was an empty string, splitting an + // empty string by `:` will always result in an array with at least one + // element. + let mut base = raw_variants.pop().unwrap(); + + let mut parsed_variants: Vec = Vec::with_capacity(raw_variants.len()); + + for i in (0..raw_variants.len()).rev() { + let parsed_variant = parse_variant(raw_variants[i]); + if parsed_variant.is_none() { + return None; + } + + parsed_variants.push(parsed_variant.unwrap()) + } + + let mut important = false; + let mut negative = false; + + // Candidates that end with an exclamation mark are the important version with + // higher specificity of the non-important candidate, e.g. `mx-4!`. + if let Some(new_base) = base.strip_suffix(b"!") { + important = true; + base = new_base; + } + + // Legacy syntax with leading `!`, e.g. `!mx-4`. + else if let Some(new_base) = base.strip_prefix(b"!") { + important = true; + base = new_base; + } + + // Candidates that start with a dash are the negative versions of another + // candidate, e.g. `-mx-4`. + if let Some(new_base) = base.strip_prefix(b"-") { + negative = true; + base = new_base; + } + + let mut utilities: Vec = vec![]; + + // Check for an exact match of a static utility first as long as it does not + // look like an arbitrary value. + if design.utilities.has(base) && !base.contains(&b'[') { + utilities.push(Utility::Static { + root: base.to_vec(), + }); + } + + // Figure out the new base and the modifier segment if present. + // + // E.g.: + // + // ``` + // bg-red-500/50 + // ^^^^^^^^^^ -> Base without modifier + // ^^ -> Modifier segment + // ``` + let parts = segment(base, b'/'); + + // If there's more than one modifier, the utility is invalid. + // + // E.g.: + // + // - `bg-red-500/50/50` + if parts.len() > 2 { + return None; + } + + // let [baseWithoutModifier, modifierSegment = null, additionalModifier] = segment(base, '/') + + + Some(Candidate { + raw: raw.to_vec(), + important, + variants: parsed_variants.to_vec(), + utilities, + }) +} + +fn parse_variant(input: &[u8]) -> Option { + return Some(Variant::Static { root: vec![], compounds: false }) +} + +fn parse_arbitrary_property(base: &[u8]) -> Option { + // Arbitrary properties must start and end with square brackets. + let Some(base) = base.strip_prefix(b"[") else { + return None; + }; + + let Some(base) = base.strip_suffix(b"]") else { + return None; + }; + + // The property part of the arbitrary property can only start with a-z + // lowercase or a dash `-` in case of vendor prefixes such as `-webkit-` + // or `-moz-`. + // + // Otherwise, it is an invalid candidate, and skip continue parsing. + if base[0] != b'-' && !(base[0] >= b'a' && base[0] <= b'z') { + return None + } + + // Arbitrary properties consist of a property and a value separated by a + // `:`. If the `:` cannot be found, then it is an invalid candidate, and we + // can skip continue parsing. + // + // Since the property and the value should be separated by a `:`, we can + // also verify that the colon is not the first or last character in the + // candidate, because that would make it invalid as well. + let Some(idx) = base.find(b":") else { + return None; + }; + + if idx == 0 || idx == base.len() - 1 { + return None; + } + + let property = base[..idx].to_vec(); + let value = base[idx+1..].to_vec(); + + // let value = decodeArbitraryValue(base.slice(idx + 1)) + + Some(Utility::Arbitrary { + property, + value, + modifier: None, + }) +} diff --git a/crates/core/src/engine/mod.rs b/crates/core/src/engine/mod.rs new file mode 100644 index 000000000..bccbf9095 --- /dev/null +++ b/crates/core/src/engine/mod.rs @@ -0,0 +1,10 @@ +// mod candidate; + +/// This represents the core logic of: +/// - Parsing a candidate +/// - Matching that against a list of known utilities and known variants +/// - Returning the appropriate "functions" to be called + +struct Engine { + // +} diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs index 03a336535..0e7cdbff0 100644 --- a/crates/core/src/lib.rs +++ b/crates/core/src/lib.rs @@ -1,6 +1,7 @@ mod compat; mod css; mod util; +mod engine; use css::optimize::optimize_ast; use css::parser::parse;