etch 0.4.2

Not just a text formatter, don't mark it down, etch it.
Documentation
use crate::plugins::Plugins;
use glue::prelude::*;
use std::collections::HashMap;
use std::fs::File;
use std::io::prelude::*;
use std::path::PathBuf;
use std::sync::{Arc, Mutex};

pub type LockableState<'a> = Arc<Mutex<&'a mut State>>;
pub type Attributes = Option<HashMap<String, String>>;
pub type Elements = Option<Vec<Token>>;
pub type Import = Result<Token, ImportError>;
pub type Imports = HashMap<PathBuf, Option<Import>>;
pub type TagDeclarations = HashMap<String, Attributes>;
pub type TagReferences = Option<Vec<String>>;

#[derive(Debug, Clone, PartialEq)]
pub enum ImportError {
    IO(String),
    Syntax(String),
}

fn read_file(filename: PathBuf) -> Result<String, String> {
    match File::open(filename) {
        Err(_) => Err("file not found".into()),
        Ok(mut file) => {
            let mut input = String::new();

            match file.read_to_string(&mut input) {
                Err(_) => Err("error reading file".into()),
                Ok(_) => Ok(input),
            }
        }
    }
}

pub struct State {
    pub tags: TagDeclarations,
    pub imports: Imports,
    pub paths: Vec<PathBuf>,
    pub plugins: Plugins,
}

impl Default for State {
    fn default() -> Self {
        State {
            tags: TagDeclarations::default(),
            imports: Imports::default(),
            paths: vec![std::env::current_dir().unwrap()],
            plugins: Plugins::default(),
        }
    }
}

impl State {
    pub fn lockable(&mut self) -> LockableState {
        Arc::new(Mutex::new(self))
    }

    pub fn import_file(&mut self, filename: &str) -> Import {
        let filename = PathBuf::from(filename);
        let filename = self.current_dir().join(filename);

        let is_etch = if let Some(ext) = filename.extension() {
            if ext == "etch" {
                true
            } else {
                false
            }
        } else {
            false
        };

        self.imports.entry(filename.clone()).or_insert(None);
        self.paths.push(
            filename
                .parent()
                .expect("No parent directory.")
                .to_path_buf(),
        );

        let input = match read_file(filename.clone()) {
            Ok(input) => input,
            Err(error) => {
                self.imports
                    .entry(filename.to_path_buf())
                    .and_modify(|item| *item = Some(Err(ImportError::IO(error))));

                return self.imports.get(&filename).unwrap().to_owned().unwrap();
            }
        };
        let input = input.as_str();

        if is_etch {
            let state = self.lockable();

            let result = Some(match crate::block::document(state.clone()).parse(input) {
                Ok((data, _)) => Ok(Token::Fragment {
                    children: Some(data),
                }),
                Err(error) => Err(ImportError::Syntax(format!(
                    "{}",
                    error.to_printable(filename.to_str().unwrap(), input)
                ))),
            });

            let mut state = state.lock().unwrap();

            state
                .imports
                .entry(filename.to_path_buf())
                .and_modify(|item| {
                    *item = result;
                });
            state.paths.pop();

            state.imports.get(&filename).unwrap().to_owned().unwrap()
        } else {
            Ok(Token::Element {
                name: "pre".into(),
                attributes: None,
                tags: None,
                children: Some(vec![Token::Text { text: input.into() }]),
            })
        }
    }

    pub fn current_dir(&self) -> &PathBuf {
        self.paths.last().unwrap()
    }
}

#[derive(Debug, Clone, PartialEq)]
pub enum Token {
    Fragment {
        children: Elements,
    },
    Element {
        name: String,
        attributes: Attributes,
        tags: TagReferences,
        children: Elements,
    },
    Span {
        attributes: Attributes,
        tags: TagReferences,
        children: Elements,
    },
    Html {
        html: String,
    },
    Text {
        text: String,
    },
    Placeholder {
        id: String,
    },
    Whitespace,
    Removed,
}

impl Token {
    pub fn element_name(&self) -> Option<&str> {
        match self {
            Token::Element { name, .. } => Some(name),
            _ => None,
        }
    }
}

pub trait WithTags {
    fn with_tags(self, tags: Vec<String>) -> Self;
}

impl WithTags for Token {
    fn with_tags(self, tags: Vec<String>) -> Self {
        match self {
            Token::Element {
                name,
                attributes,
                children,
                ..
            } => Token::Element {
                tags: Some(tags),
                name,
                attributes,
                children,
            },
            Token::Span {
                attributes,
                children,
                ..
            } => Token::Span {
                tags: Some(tags),
                attributes,
                children,
            },
            Token::Text { text } => Token::Span {
                tags: Some(tags),
                attributes: None,
                children: Some(vec![Token::Text { text }]),
            },
            Token::Placeholder { id } => Token::Span {
                tags: Some(tags),
                attributes: None,
                children: Some(vec![Token::Placeholder { id }]),
            },
            _ => self,
        }
    }
}