2020-04-23 21:29:50 +02:00
|
|
|
use std::collections::{HashMap, HashSet};
|
2020-04-23 14:26:43 +02:00
|
|
|
use std::fs::*;
|
|
|
|
use std::path::Path;
|
2020-04-23 21:29:50 +02:00
|
|
|
use std::sync::mpsc::channel;
|
|
|
|
use std::sync::{Arc, RwLock, RwLockWriteGuard};
|
|
|
|
use std::time::Duration;
|
2020-04-23 14:26:43 +02:00
|
|
|
use std::time::SystemTime;
|
|
|
|
|
2020-04-23 21:29:50 +02:00
|
|
|
use notify::{watcher, DebouncedEvent, RecursiveMode, Watcher};
|
|
|
|
|
2020-04-23 14:26:43 +02:00
|
|
|
const INPUT: &str = "./jirs-client/js/styles.css";
|
|
|
|
|
2020-04-23 21:29:50 +02:00
|
|
|
type Css = Arc<RwLock<CssFile>>;
|
|
|
|
|
|
|
|
#[derive(Debug)]
|
|
|
|
enum Partial {
|
|
|
|
String(String),
|
|
|
|
File(Css),
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Debug)]
|
|
|
|
enum FileState {
|
|
|
|
Clean,
|
|
|
|
Dirty,
|
|
|
|
Dead,
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Debug)]
|
|
|
|
struct CssFile {
|
|
|
|
pub path: String,
|
|
|
|
pub lines: Vec<Partial>,
|
|
|
|
pub last_changed: SystemTime,
|
|
|
|
pub state: FileState,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl CssFile {
|
|
|
|
pub fn new(path: String) -> Self {
|
|
|
|
Self {
|
|
|
|
path,
|
|
|
|
lines: vec![],
|
|
|
|
last_changed: SystemTime::UNIX_EPOCH,
|
|
|
|
state: FileState::Clean,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl std::fmt::Display for CssFile {
|
|
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
|
|
f.write_str(format!("\n/* -- {} --- */\n\n", self.path).as_str())?;
|
|
|
|
for line in self.lines.iter() {
|
|
|
|
match line {
|
|
|
|
Partial::String(line) => {
|
|
|
|
f.write_str(line.as_str())?;
|
|
|
|
f.write_str("\n")?;
|
|
|
|
}
|
|
|
|
Partial::File(file) => {
|
|
|
|
if let Ok(css) = file.read() {
|
|
|
|
f.write_str(format!("{}", css).as_str())?;
|
|
|
|
f.write_str("\n")?;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-04-23 14:26:43 +02:00
|
|
|
#[derive(Debug, Default)]
|
2020-04-23 21:29:50 +02:00
|
|
|
struct Application {
|
2020-04-23 14:26:43 +02:00
|
|
|
input: String,
|
|
|
|
output: Option<String>,
|
|
|
|
watch: bool,
|
|
|
|
prelude_selector: bool,
|
2020-04-23 21:29:50 +02:00
|
|
|
files_map: HashMap<String, HashSet<String>>,
|
|
|
|
fm: HashMap<String, Css>,
|
|
|
|
root_file: Option<Css>,
|
2020-04-23 14:26:43 +02:00
|
|
|
}
|
|
|
|
|
2020-04-23 21:29:50 +02:00
|
|
|
impl Application {
|
|
|
|
fn read_timestamp(input: &Path) -> Result<SystemTime, String> {
|
|
|
|
std::fs::File::open(input)
|
|
|
|
.and_then(|file| file.metadata())
|
|
|
|
.and_then(|meta| meta.modified())
|
|
|
|
.map_err(|e| format!("{}", e))
|
|
|
|
}
|
|
|
|
|
|
|
|
fn check_timestamps(
|
|
|
|
&mut self,
|
|
|
|
input: &Path,
|
|
|
|
output_timestamp: SystemTime,
|
|
|
|
) -> Result<bool, String> {
|
|
|
|
let input_dir = input
|
|
|
|
.clone()
|
2020-04-23 14:26:43 +02:00
|
|
|
.parent()
|
2020-04-23 21:29:50 +02:00
|
|
|
.ok_or_else(|| format!("Not a valid path {:?}", input))?;
|
2020-04-23 14:26:43 +02:00
|
|
|
|
|
|
|
let path = input_dir.to_str().unwrap();
|
|
|
|
let paths =
|
|
|
|
glob::glob(format!("{}/**/*.css", path).as_str()).map_err(|e| format!("{}", e))?;
|
|
|
|
for path in paths.filter_map(Result::ok) {
|
2020-04-23 21:29:50 +02:00
|
|
|
if Self::read_timestamp(path.as_path())? > output_timestamp {
|
|
|
|
return Ok(false);
|
|
|
|
}
|
2020-04-23 14:26:43 +02:00
|
|
|
}
|
2020-04-23 21:29:50 +02:00
|
|
|
Ok(true)
|
|
|
|
}
|
|
|
|
|
|
|
|
fn parse(&mut self) -> Result<(), String> {
|
|
|
|
let root_path = self.input.to_string();
|
|
|
|
let root = std::path::Path::new(&root_path);
|
|
|
|
let root_file = self.parse_file(root)?;
|
|
|
|
self.root_file = Some(root_file);
|
2020-04-23 14:26:43 +02:00
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
2020-04-23 21:29:50 +02:00
|
|
|
fn parse_file(&mut self, input: &Path) -> Result<Css, String> {
|
|
|
|
let file_path = input.display().to_string();
|
|
|
|
let input_dir = input
|
|
|
|
.clone()
|
|
|
|
.parent()
|
|
|
|
.ok_or_else(|| format!("Not a valid path {:?}", input))?;
|
|
|
|
let file = self
|
|
|
|
.fm
|
|
|
|
.entry(file_path.clone())
|
|
|
|
.or_insert_with(|| Arc::new(RwLock::new(CssFile::new(file_path.clone()))))
|
|
|
|
.clone();
|
|
|
|
|
|
|
|
if let Ok(mut css) = file.write() {
|
|
|
|
css.last_changed = Self::read_timestamp(input)?;
|
2020-04-23 14:26:43 +02:00
|
|
|
}
|
2020-04-23 21:29:50 +02:00
|
|
|
|
|
|
|
for line in read_to_string(file_path.as_str())
|
|
|
|
.map_err(|e| format!("{}", e))?
|
|
|
|
.lines()
|
|
|
|
{
|
|
|
|
let l = line.trim();
|
|
|
|
match l {
|
|
|
|
"" => continue,
|
|
|
|
_ if l.starts_with("@import ") => {
|
|
|
|
let imported = line
|
|
|
|
.replace("@import ", "")
|
|
|
|
.trim()
|
|
|
|
.replace("\"", "")
|
|
|
|
.replace(";", "")
|
|
|
|
.to_string();
|
|
|
|
let child = input_dir
|
|
|
|
.clone()
|
|
|
|
.join(imported.as_str())
|
|
|
|
.canonicalize()
|
|
|
|
.map_err(|e| format!("{}", e))?;
|
|
|
|
let child_file = self.parse_file(&child)?;
|
|
|
|
|
|
|
|
if let Ok(mut css) = file.write() {
|
|
|
|
css.lines.push(Partial::File(child_file));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
_ => {
|
|
|
|
if let Ok(mut css) = file.write() {
|
|
|
|
css.lines.push(Partial::String(l.to_string()));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2020-04-23 14:26:43 +02:00
|
|
|
}
|
2020-04-23 21:29:50 +02:00
|
|
|
Ok(file)
|
2020-04-23 14:26:43 +02:00
|
|
|
}
|
|
|
|
|
2020-04-23 21:29:50 +02:00
|
|
|
pub fn mark_dirty(&mut self, path: &Path) {
|
|
|
|
if let Ok(mut css) = self.css_at_path(path) {
|
|
|
|
css.state = FileState::Dirty;
|
|
|
|
}
|
|
|
|
}
|
2020-04-23 14:26:43 +02:00
|
|
|
|
2020-04-23 21:29:50 +02:00
|
|
|
pub fn mark_dead(&mut self, path: &Path) {
|
|
|
|
if let Ok(mut css) = self.css_at_path(path) {
|
|
|
|
css.state = FileState::Dead;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn css_at_path(&mut self, path: &Path) -> Result<RwLockWriteGuard<CssFile>, bool> {
|
|
|
|
self.fm
|
|
|
|
.get(path.display().to_string().as_str())
|
|
|
|
.ok_or(false)
|
|
|
|
.and_then(|css| css.write().or(Err(false)))
|
|
|
|
}
|
|
|
|
|
|
|
|
fn print(&self) {
|
|
|
|
let css = match self.root_file.as_ref().unwrap().read() {
|
|
|
|
Ok(css) => css,
|
|
|
|
_ => return,
|
|
|
|
};
|
|
|
|
match self.output.as_ref() {
|
|
|
|
Some(f) => {
|
|
|
|
std::fs::create_dir_all(Path::new(f).parent().unwrap()).unwrap();
|
|
|
|
std::fs::write(f, format!("{}", css)).unwrap();
|
|
|
|
println!("CSS merge done");
|
|
|
|
}
|
|
|
|
_ => println!("{}", css),
|
2020-04-23 14:26:43 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn main() -> Result<(), String> {
|
|
|
|
let matches = clap::App::new("jirs-css")
|
|
|
|
.arg(
|
|
|
|
clap::Arg::with_name("input")
|
|
|
|
.short("i")
|
|
|
|
.default_value(INPUT)
|
|
|
|
.takes_value(true),
|
|
|
|
)
|
|
|
|
.arg(clap::Arg::with_name("output").short("O").takes_value(true))
|
|
|
|
.arg(clap::Arg::with_name("watch").short("W"))
|
|
|
|
.arg(
|
|
|
|
clap::Arg::with_name("prelude")
|
|
|
|
.short("p")
|
|
|
|
.help("Prepend file name as class to each selector"),
|
|
|
|
)
|
|
|
|
.get_matches();
|
|
|
|
|
2020-04-23 21:29:50 +02:00
|
|
|
let mut app = Application {
|
2020-04-23 14:26:43 +02:00
|
|
|
input: matches.value_of("input").unwrap().to_string(),
|
|
|
|
output: matches.value_of("output").map(|s| s.to_string()),
|
|
|
|
watch: matches.is_present("watch"),
|
|
|
|
prelude_selector: matches.is_present("prelude"),
|
2020-04-23 21:29:50 +02:00
|
|
|
files_map: Default::default(),
|
|
|
|
fm: Default::default(),
|
|
|
|
root_file: None,
|
2020-04-23 14:26:43 +02:00
|
|
|
};
|
2020-04-23 21:29:50 +02:00
|
|
|
let root_path = app.input.to_string();
|
|
|
|
let root = std::path::Path::new(&root_path);
|
2020-04-23 14:26:43 +02:00
|
|
|
|
|
|
|
let output_timestamp = matches
|
|
|
|
.value_of("output")
|
|
|
|
.ok_or(std::io::Error::from_raw_os_error(0))
|
|
|
|
.and_then(|path| File::open(path))
|
|
|
|
.and_then(|file| file.metadata())
|
|
|
|
.and_then(|meta| meta.modified())
|
|
|
|
.unwrap_or_else(|_| SystemTime::UNIX_EPOCH.clone());
|
|
|
|
|
2020-04-23 21:29:50 +02:00
|
|
|
if app.check_timestamps(root, output_timestamp)? {
|
2020-04-23 14:26:43 +02:00
|
|
|
return Ok(());
|
|
|
|
}
|
|
|
|
|
2020-04-23 21:29:50 +02:00
|
|
|
app.parse()?;
|
|
|
|
app.print();
|
2020-04-23 14:26:43 +02:00
|
|
|
|
2020-04-23 21:29:50 +02:00
|
|
|
let (tx, rx) = channel();
|
|
|
|
let mut watcher = watcher(tx, Duration::from_secs(1)).unwrap();
|
2020-04-23 14:26:43 +02:00
|
|
|
|
2020-04-23 21:29:50 +02:00
|
|
|
for file in app.fm.keys() {
|
|
|
|
watcher
|
|
|
|
.watch(file.to_string(), RecursiveMode::NonRecursive)
|
|
|
|
.unwrap();
|
2020-04-23 14:26:43 +02:00
|
|
|
}
|
|
|
|
|
2020-04-23 21:29:50 +02:00
|
|
|
loop {
|
|
|
|
match rx.recv() {
|
|
|
|
Ok(DebouncedEvent::NoticeWrite(path)) => {
|
|
|
|
app.mark_dirty(path.as_path());
|
|
|
|
if let Err(s) = app.parse_file(&path) {
|
|
|
|
eprintln!("{}", s);
|
|
|
|
}
|
|
|
|
app.print();
|
|
|
|
}
|
|
|
|
Ok(DebouncedEvent::NoticeRemove(path)) => {
|
|
|
|
app.mark_dead(path.as_path());
|
|
|
|
watcher.unwatch(path).unwrap();
|
|
|
|
}
|
|
|
|
Ok(event) => println!("{:?}", event),
|
|
|
|
Err(e) => println!("watch error: {:?}", e),
|
|
|
|
}
|
|
|
|
}
|
2020-04-23 14:26:43 +02:00
|
|
|
}
|