commit 4c799091b4822d474b7f8cd21c0ec233248fe1c1 Author: annieversary Date: Fri May 13 11:35:22 2022 +0100 init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4c790d0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/target +/Cargo.lock +.DS_Store diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..3d8a947 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "zephyr" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +once_cell = "1.10.0" +thiserror = "1.0.31" diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..f32bdd8 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,107 @@ +use once_cell::sync::Lazy; +use std::{collections::HashMap, path::Path}; +use thiserror::Error; + +use crate::parse::*; + +mod modifiers; +mod parse; + +pub fn generate(classes: &[&str], path: impl AsRef) -> Result<(), Error> { + let out = generate_css(classes); + std::fs::write(path, out)?; + + Ok(()) +} + +pub fn generate_css(classes: &[&str]) -> String { + classes + .into_iter() + .flat_map(|c| generate_class(c)) + .collect::>() + .join("") +} + +pub fn generate_class(class: &str) -> Option { + let class = parse_class(class)?; + let rule = RULES.get(&class.name)?; + Some(rule.generate(&class)) +} + +static RULES: Lazy> = Lazy::new(|| { + let mut m = HashMap::new(); + m.insert("m", &Margin as &dyn Rule); + m +}); + +trait Rule: Sync { + fn generate<'a>(&self, class: &Class<'a>) -> String; +} + +impl Rule for Margin { + fn generate<'a>(&self, class: &Class<'a>) -> String { + format!( + "{selector} {{ margin: {value}; }}", + selector = class.selector(), + value = class.value + ) + } +} +struct Margin; + +#[derive(Error, Debug)] +pub enum Error { + #[error("io error")] + Disconnect(#[from] std::io::Error), +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn generate_margin_works() { + let class = Class { + name: "m", + value: "1rem", + modifiers: vec![].into(), + pseudo: None, + original: "m[1rem]", + }; + let css = Margin.generate(&class); + assert_eq!(css, ".m[1rem] { margin: 1rem; }"); + + let class = Class { + name: "m", + value: "1rem", + modifiers: vec!["focus"].into(), + pseudo: None, + original: "m[1rem]focus", + }; + let css = Margin.generate(&class); + assert_eq!(css, ".m[1rem]focus:focus { margin: 1rem; }"); + + let class = Class { + name: "m", + value: "1rem", + modifiers: vec!["focus", "hover", "odd"].into(), + pseudo: None, + original: "m[1rem]focus,hover,odd", + }; + let css = Margin.generate(&class); + assert_eq!( + css, + ".m[1rem]focus,hover,odd:focus:hover:nth-child(odd) { margin: 1rem; }" + ); + } + + #[test] + fn generate_classes_works() { + let classes = generate_css(&["m[3rem]hover,focus$placeholder"]); + + assert_eq!( + classes, + ".m[3rem]hover,focus$placeholder:hover:focus::placeholder { margin: 3rem; }" + ); + } +} diff --git a/src/modifiers.rs b/src/modifiers.rs new file mode 100644 index 0000000..45484b1 --- /dev/null +++ b/src/modifiers.rs @@ -0,0 +1,74 @@ +#[derive(Default, PartialEq, Debug)] +pub(crate) struct Modifiers<'a>(Vec>); + +impl<'a> Modifiers<'a> { + pub(crate) fn get(&self) -> Option { + if self.is_empty() { + None + } else { + Some( + self.0 + .iter() + .map(Modifier::value) + .collect::>() + .join(":"), + ) + } + } + + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } +} + +impl<'a> From> for Modifiers<'a> { + fn from(v: Vec<&'a str>) -> Self { + Modifiers(v.into_iter().map(Modifier::new).collect()) + } +} + +// TODO something like this +// i wanna be able to have both replaced variables for common modifiers +// eg: odd -> :nth-child(odd) +// but i also wanna be able to keep it relaxed so you can type whatever +#[derive(Debug, PartialEq)] +enum Modifier<'a> { + Converted { from: &'a str, to: &'static str }, + Unknown(&'a str), +} + +impl<'a> Modifier<'a> { + fn new(s: &'a str) -> Self { + match s { + "odd" => Self::Converted { + from: s, + to: "nth-child(odd)", + }, + "even" => Self::Converted { + from: s, + to: "nth-child(even)", + }, + "first" => Self::Converted { + from: s, + to: "first-child", + }, + "last" => Self::Converted { + from: s, + to: "last-child", + }, + "only" => Self::Converted { + from: s, + to: "only-child", + }, + // TODO add more + _ => Self::Unknown(s), + } + } + + fn value(&self) -> &str { + match self { + Modifier::Converted { from, to } => to, + Modifier::Unknown(v) => v, + } + } +} diff --git a/src/parse.rs b/src/parse.rs new file mode 100644 index 0000000..b231fde --- /dev/null +++ b/src/parse.rs @@ -0,0 +1,130 @@ +use crate::modifiers::Modifiers; + +pub(crate) fn parse_class<'a>(original: &'a str) -> Option> { + let (class, pseudo) = if let Some((class, pseudo)) = original.split_once('$') { + (class, Some(pseudo)) + } else { + (original, None) + }; + + let start = pos(class, '[')?; + let end = pos(class, ']')?; + + if start > end { + return None; + } + + let mods = if end + 1 == class.len() { + vec![] + } else { + class[end + 1..].split(',').collect() + }; + + Some(Class { + name: &class[0..start], + value: &class[start + 1..end], + modifiers: mods.into(), + pseudo, + original, + }) +} + +#[derive(PartialEq, Debug)] +pub(crate) struct Class<'a> { + pub name: &'a str, + pub value: &'a str, + pub modifiers: Modifiers<'a>, + pub pseudo: Option<&'a str>, + /// the original unparsed value + /// needed to generate the css selector + pub original: &'a str, +} + +impl<'a> Class<'a> { + pub(crate) fn selector(&self) -> String { + let Class { + modifiers, + pseudo, + original, + .. + } = self; + + let mut rest = if let Some(mods) = modifiers.get() { + format!(":{mods}") + } else { + "".to_string() + }; + if let Some(pseudo) = pseudo { + rest.push_str("::"); + rest.push_str(pseudo); + } + + format!(".{original}{rest}") + } +} + +fn pos(s: &str, c: char) -> Option { + s.find(|v| v == c) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn check(class: &str, (name, value, modifiers, pseudo): (&str, &str, Vec<&str>, Option<&str>)) { + assert_eq!( + parse_class(class), + Some(Class { + name, + value, + modifiers: modifiers.into(), + pseudo, + original: class + }) + ); + } + + #[test] + fn parse_works() { + check("m[1rem]", ("m", "1rem", vec![], None)); + check("text-align[center]", ("text-align", "center", vec![], None)); + check("something[one:two]", ("something", "one:two", vec![], None)); + // testing out weird unicode stuffs + check("hešŸ„°llo[one:two]", ("hešŸ„°llo", "one:two", vec![], None)); + } + + #[test] + fn parse_modifier() { + check("a[b]hover", ("a", "b", vec!["hover"], None)); + check( + "text-align[center]focus", + ("text-align", "center", vec!["focus"], None), + ); + } + + #[test] + fn parse_multiple_modifiers() { + check( + "a[b]hover,focus,odd", + ("a", "b", vec!["hover", "focus", "odd"], None), + ); + } + + #[test] + fn parse_pseudo() { + check( + "a[b]hover,focus,odd$before", + ("a", "b", vec!["hover", "focus", "odd"], Some("before")), + ); + check( + "a[b]hover$before$after", + ("a", "b", vec!["hover"], Some("before$after")), + ); + } + + #[test] + fn out_of_order_is_none() { + assert_eq!(parse_class("a]b["), None); + assert_eq!(parse_class("a]b[c]"), None); + } +}