init
This commit is contained in:
commit
4c799091b4
|
@ -0,0 +1,3 @@
|
|||
/target
|
||||
/Cargo.lock
|
||||
.DS_Store
|
|
@ -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"
|
|
@ -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; }"
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue