This commit is contained in:
annieversary 2022-05-13 11:35:22 +01:00
commit 4c799091b4
5 changed files with 324 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
/target
/Cargo.lock
.DS_Store

10
Cargo.toml Normal file
View File

@ -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"

107
src/lib.rs Normal file
View File

@ -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<Path>) -> 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::<Vec<_>>()
.join("")
}
pub fn generate_class(class: &str) -> Option<String> {
let class = parse_class(class)?;
let rule = RULES.get(&class.name)?;
Some(rule.generate(&class))
}
static RULES: Lazy<HashMap<&str, &dyn Rule>> = 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; }"
);
}
}

74
src/modifiers.rs Normal file
View File

@ -0,0 +1,74 @@
#[derive(Default, PartialEq, Debug)]
pub(crate) struct Modifiers<'a>(Vec<Modifier<'a>>);
impl<'a> Modifiers<'a> {
pub(crate) fn get(&self) -> Option<String> {
if self.is_empty() {
None
} else {
Some(
self.0
.iter()
.map(Modifier::value)
.collect::<Vec<_>>()
.join(":"),
)
}
}
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
}
impl<'a> From<Vec<&'a str>> 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,
}
}
}

130
src/parse.rs Normal file
View File

@ -0,0 +1,130 @@
use crate::modifiers::Modifiers;
pub(crate) fn parse_class<'a>(original: &'a str) -> Option<Class<'a>> {
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<usize> {
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);
}
}