use crate::parser::{self, Error, ParserResult, Rule};
use pest::iterators::{Pair, Pairs};
use std::collections::HashMap;
use std::fmt;
use std::fs::File;
use std::io::prelude::*;
use std::path::PathBuf;
pub type Identifiers = HashMap<String, String>;
pub type Children = Vec<Child>;
#[derive(Debug, PartialEq)]
pub enum Child {
AtRule {
name: Option<String>,
rule: Option<String>,
children: Children,
},
Comment {
value: Option<String>,
},
Property {
name: Option<String>,
value: Option<String>,
},
SelectRule {
rule: Option<String>,
children: Children,
},
}
impl fmt::Display for Child {
fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
match self {
Child::AtRule {
name,
rule,
children,
} => {
if let (Some(name), Some(rule)) = (name, rule) {
if children.is_empty() {
write!(formatter, "@{} {}; ", name, rule.trim())?;
} else {
write!(formatter, "@{} {} {{ ", name, rule.trim())?;
for child in children {
write!(formatter, "{}", child)?;
}
write!(formatter, "}}\n")?;
}
} else if let Some(name) = name {
if children.is_empty() {
write!(formatter, "@{};", name)?;
} else {
write!(formatter, "@{} {{ ", name)?;
for child in children {
write!(formatter, "{}", child)?;
}
write!(formatter, "}}\n")?;
}
}
}
Child::SelectRule { rule, children } => {
if children.is_empty() {
write!(formatter, "")?;
} else if let Some(rule) = rule {
write!(formatter, "{} {{ ", rule)?;
for child in children {
write!(formatter, "{}", child)?;
}
write!(formatter, "}}\n")?;
}
}
Child::Property { name, value } => {
if let (Some(name), Some(value)) = (name, value) {
write!(formatter, "{}: {}; ", name, value)?;
} else if let Some(name) = name {
write!(formatter, "{}:; ", name)?;
}
}
Child::Comment { value } => {
if let Some(value) = value {
write!(formatter, "{}", value)?;
}
}
};
Ok(())
}
}
#[derive(Debug, PartialEq)]
pub struct Context<'c> {
pub module: &'c mut Module,
pub name: &'c str,
pub path: &'c PathBuf,
pub stylesheet: &'c mut Stylesheet,
}
impl<'c> Context<'c> {
fn add_identifier(&mut self, identifier: String) -> String {
self.module
.identifiers
.entry(identifier.clone())
.or_insert(format!(
"{}__{}__{}",
&self.name, &identifier, &self.stylesheet.identifiers
));
self.stylesheet.identifiers += 1;
self.module.identifiers.get(&identifier).unwrap().to_owned()
}
}
#[derive(Debug, PartialEq)]
pub struct Module {
pub children: Children,
pub identifiers: Identifiers,
pub input_path: PathBuf,
pub output_path: PathBuf,
}
#[cfg(test)]
impl Default for Module {
fn default() -> Self {
use std::str::FromStr;
let path = PathBuf::from_str(file!()).unwrap();
Self {
children: Vec::new(),
identifiers: HashMap::new(),
input_path: path.clone(),
output_path: path.clone().with_extension("css.rs"),
}
}
}
impl fmt::Display for Module {
fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
for child in &self.children {
write!(formatter, "{}", child)?;
}
Ok(())
}
}
impl<'m> Module {
pub fn new(
mut stylesheet: &mut Stylesheet,
path: PathBuf,
input: &'m str,
) -> ParserResult<'m, Self> {
let pairs = parser::stylesheet(&input)?;
let mut module = Module {
children: Children::new(),
identifiers: Identifiers::new(),
input_path: path.clone(),
output_path: path.clone().with_extension("css.rs"),
};
let mut context = Context {
module: &mut module,
name: &path.file_stem().unwrap().to_str().unwrap(),
path: &path.parent().unwrap().to_path_buf(),
stylesheet: &mut stylesheet,
};
for pair in pairs {
let child = match pair.as_rule() {
Rule::comment => comment(pair),
Rule::atrule => atrule(&mut context, pair)?,
Rule::selectrule => selectrule(&mut context, pair)?,
Rule::EOI => None,
_ => return Err(Error::from(pair)),
};
if let Some(child) = child {
context.module.children.push(child);
}
}
Ok(module)
}
}
pub type Modules = HashMap<PathBuf, Module>;
#[derive(Debug, PartialEq)]
pub struct Stylesheet {
pub identifiers: u64,
pub modules: Modules,
}
impl Default for Stylesheet {
fn default() -> Self {
Self {
identifiers: 0,
modules: HashMap::new(),
}
}
}
impl Stylesheet {
pub fn add_module<'m>(&mut self, path: PathBuf) -> ParserResult<'m, &Module> {
let mut file = File::open(path.clone()).expect("file not found");
let mut input = String::new();
file.read_to_string(&mut input).unwrap();
let module = Module::new(self, path.clone(), &input)?;
Ok(self.modules.entry(path).or_insert(module))
}
#[cfg(test)]
fn add_test_module<'m>(&mut self, input: &str) -> ParserResult<'m, &Module> {
use std::str::FromStr;
let path = PathBuf::from_str(file!()).unwrap();
let module = Module::new(self, path.clone(), &input)?;
Ok(self.modules.entry(path).or_insert(module))
}
pub fn has_module(self, path: PathBuf) -> bool {
self.modules.contains_key(&path)
}
pub fn remove_module(mut self, path: PathBuf) {
self.modules.remove_entry(&path);
}
}
pub fn atrule<'t>(
mut context: &mut Context,
pair: Pair<'t, Rule>,
) -> ParserResult<'t, Option<Child>> {
let mut name: Option<String> = None;
let mut rule: Option<String> = None;
let mut children = Vec::new();
for pair in pair.into_inner() {
let child = match pair.as_rule() {
Rule::identifier => {
name = Some(pair.as_str().into());
None
}
Rule::atrule_rule => {
if Some("keyframes".into()) == name {
rule = Some(format!(
"{}",
&context.add_identifier(pair.as_str().trim().into())
));
} else if Some("import".into()) == name {
let quotes: &[_] = &['"', '\''];
let path = context
.path
.clone()
.join(pair.as_str().trim_matches(quotes));
let import = context.stylesheet.add_module(path)?;
for (old, new) in import.identifiers.iter() {
context
.module
.identifiers
.entry(old.clone())
.or_insert(new.clone());
}
return Ok(None);
} else {
rule = Some(pair.as_str().into());
}
None
}
Rule::comment | Rule::line_comment => comment(pair),
Rule::property => property(&mut context, pair)?,
Rule::atrule => atrule(&mut context, pair)?,
Rule::selectrule => selectrule(&mut context, pair)?,
_ => return Err(Error::from(pair)),
};
if let Some(child) = child {
children.push(child);
}
}
Ok(Some(Child::AtRule {
name,
rule,
children,
}))
}
pub fn comment<'t>(pair: Pair<'t, Rule>) -> Option<Child> {
Some(Child::Comment {
value: Some(pair.as_str().into()),
})
}
pub fn property<'t>(
mut context: &mut Context,
pair: Pair<'t, Rule>,
) -> ParserResult<'t, Option<Child>> {
let mut name: Option<String> = None;
let mut value: Option<String> = None;
for pair in pair.into_inner() {
match pair.as_rule() {
Rule::identifier => {
name = Some(pair.as_str().into());
}
Rule::property_value => {
if Some("animation".into()) == name || Some("animation-name".into()) == name {
value = replace_identifiers(&mut context, parser::animation(pair.as_str())?)?;
} else {
value = Some(pair.as_str().trim().into());
}
}
_ => return Err(Error::from(pair)),
}
}
Ok(Some(Child::Property { name, value }))
}
pub fn selectrule<'t>(
mut context: &mut Context,
pair: Pair<'t, Rule>,
) -> ParserResult<'t, Option<Child>> {
let mut rule: Option<String> = None;
let mut children = Vec::new();
for pair in pair.into_inner() {
let child = match pair.as_rule() {
Rule::selectrule_rule => {
rule = replace_identifiers(&mut context, parser::selector(pair.as_str())?)?;
None
}
Rule::comment | Rule::line_comment => comment(pair),
Rule::property => property(&mut context, pair)?,
Rule::atrule => atrule(&mut context, pair)?,
Rule::selectrule => selectrule(&mut context, pair)?,
_ => return Err(Error::from(pair)),
};
if let Some(child) = child {
children.push(child);
}
}
Ok(Some(Child::SelectRule { rule, children }))
}
pub fn replace_identifiers<'t>(
context: &mut Context,
pairs: Pairs<Rule>,
) -> ParserResult<'t, Option<String>> {
let mut result = String::new();
for pair in pairs {
match pair.as_rule() {
Rule::identifier => {
result.push_str(&format!(
"{}",
&context.add_identifier(pair.as_str().trim().into())
));
}
Rule::selector_class => {
result.push_str(&format!(
".{}",
&context.add_identifier(pair.as_str()[1..].trim().into())
));
}
_ => {
result.push_str(pair.as_str());
}
}
}
result = result.trim().into();
if result.is_empty() {
Ok(None)
} else {
Ok(Some(result))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_stylesheet_parses() {
assert_eq!(
Stylesheet::default(),
Stylesheet {
identifiers: 0,
modules: HashMap::new(),
}
)
}
#[test]
fn empty_select_rule_parses() {
assert_eq!(
Stylesheet::default().add_test_module(".foobar {}").unwrap(),
&Module {
children: vec![Child::SelectRule {
children: Vec::new(),
rule: Some(".ast__foobar__0".into()),
}],
identifiers: vec![("foobar".into(), "ast__foobar__0".into())]
.into_iter()
.collect(),
..Module::default()
}
)
}
#[test]
fn select_rule_with_property_parses() {
assert_eq!(
Stylesheet::default()
.add_test_module(".foobar { color: red; }")
.unwrap(),
&Module {
children: vec![Child::SelectRule {
rule: Some(".ast__foobar__0".into()),
children: vec![Child::Property {
name: Some("color".into()),
value: Some("red".into())
}],
}],
identifiers: vec![("foobar".into(), "ast__foobar__0".into())]
.into_iter()
.collect(),
..Module::default()
}
)
}
#[test]
fn empty_at_rule_parses() {
assert_eq!(
Stylesheet::default()
.add_test_module("@keyframes foobar;")
.unwrap(),
&Module {
children: vec![Child::AtRule {
name: Some("keyframes".into()),
children: Vec::new(),
rule: Some("ast__foobar__0".into()),
}],
identifiers: vec![("foobar".into(), "ast__foobar__0".into())]
.into_iter()
.collect(),
..Module::default()
}
)
}
#[test]
fn unclosed_block_is_an_error() {
assert!(Stylesheet::default().add_test_module("p {").is_err());
}
#[test]
fn format_empty_module() {
let mut stylesheet = Stylesheet::default();
let module = stylesheet.add_test_module("").unwrap();
assert_eq!(format!("{}", module), String::new());
}
#[test]
fn format_selectrule_with_property() {
let mut stylesheet = Stylesheet::default();
let module = stylesheet
.add_test_module("p.foobar { color : #fff ; }")
.unwrap();
assert_eq!(
format!("{}", module),
String::from("p.ast__foobar__0 { color: #fff; }\n")
);
}
}