use std::cell::RefCell;
use std::path::PathBuf;
use std::str::SplitWhitespace;
use thiserror::Error;
use crate::consts::PREFIX_COMMENT;
use crate::pages::ParseError;
use crate::parser::ScriptContext;
use crate::Interaction;
mod include;
mod link;
pub use include::ScriptPath;
pub use link::{Link, LinkKVPair, Unlink};
pub type Result<T> = std::result::Result<T, ScriptError>;
#[derive(Error, Debug, PartialEq)]
pub enum ScriptError {
#[error("No such command")]
NoSuchCommand,
#[error("Incorrect usage of Link directive")]
InvalidLink,
#[error("Attempt to link 2 of the same property in associations")]
DoubleLink,
#[error("Attempt to link a target to be associated with itself")]
TargetToSelf,
#[error("Could not open file at path {0}")]
FileOpen(PathBuf),
#[error("Error while importing interactions from script at path {0}")]
Import(PathBuf, #[source] Box<ParseError>),
}
#[derive(Clone, Debug, Default)]
enum ComptimeState {
#[default]
Normal,
Quit,
Link(Link),
Unlink(Unlink),
}
pub struct Script {
content: String,
state: RefCell<ComptimeState>,
path: ScriptPath,
}
#[derive(Clone, Debug, PartialEq)]
pub enum ScriptOutput {
LogMessage(String),
Link(Link),
Interaction(String, Interaction),
}
impl Script {
pub fn new(content: String, path: ScriptPath) -> Self {
Self {
content,
state: RefCell::new(ComptimeState::default()),
path,
}
}
fn execute_normal(&self, line: &str, out: &mut ScriptContext) -> Result<Option<ComptimeState>> {
if line.starts_with(PREFIX_COMMENT) {
return Ok(None);
}
let mut split = line.split_whitespace();
let Some(command) = split.next() else {
return Ok(None);
};
fn script_path(script: &Script, split: SplitWhitespace) -> ScriptPath {
let args = split.collect::<Vec<_>>().join(" ");
let path = PathBuf::from(args);
script.path.make_append(path)
}
match command {
"Echo" => {
out.log(&split.collect::<Vec<_>>().join(" "));
}
"Link" => {
let pair = LinkKVPair::from_words(&mut split)?;
let link = Link::from_pair(pair);
return Ok(Some(ComptimeState::Link(link)));
}
"Unlink" => {
let pair = LinkKVPair::from_words(&mut split)?;
let unlink = Unlink::from_pair(pair);
return Ok(Some(ComptimeState::Unlink(unlink)));
}
"Import" => {
let path = script_path(self, split);
let interactions = path
.parse_import()
.map_err(|e| ScriptError::Import(path.0, Box::new(e)))?;
let mapped = interactions
.into_iter()
.map(|(id, v)| ScriptOutput::Interaction(id, v));
out.0.extend(mapped);
}
"Execute" => {
let path = script_path(self, split);
let content = path.read()?;
let mut script = Self::new(content, path);
script.execute(out)?;
}
"Quit" => return Ok(Some(ComptimeState::Quit)),
_ => {
return Err(ScriptError::NoSuchCommand);
}
};
Ok(None)
}
fn execute_unlink(
&self,
line: &str,
out: &mut ScriptContext,
unlink: &mut Unlink,
) -> Result<Option<ComptimeState>> {
let line = line.trim();
if line.starts_with(PREFIX_COMMENT) {
return Ok(None);
}
if !line.is_empty() {
unlink.add_association(line.to_owned());
return Ok(None);
}
out.unlink(&unlink);
Ok(Some(ComptimeState::Normal))
}
fn execute_link(
&self,
line: &str,
out: &mut ScriptContext,
link: &mut Link,
) -> Result<Option<ComptimeState>> {
if line.starts_with(PREFIX_COMMENT) {
return Ok(None);
}
if !line.is_empty() {
let mut split = line.split_whitespace();
let pair = LinkKVPair::from_words(&mut split)?;
link.add_association(pair);
return Ok(None);
}
let is_dupe = out.iter_links().any(|v| v.target == link.target);
if is_dupe {
return Err(ScriptError::DoubleLink);
}
out.link(link.clone());
Ok(Some(ComptimeState::Normal))
}
pub fn execute(&mut self, out: &mut ScriptContext) -> Result<()> {
use ComptimeState::*;
let lines = self.content.lines().chain(std::iter::once(""));
for line in lines {
let new_state = match *self.state.borrow_mut() {
Normal => self.execute_normal(line, out)?,
Link(ref mut link) => self.execute_link(line, out, link)?,
Unlink(ref mut unlink) => self.execute_unlink(line, out, unlink)?,
Quit => unreachable!(),
};
match new_state {
Some(Quit) => {
return Ok(());
}
Some(state) => {
self.state.replace(state);
}
_ => (),
};
}
Ok(())
}
}
#[cfg(test)]
mod tests;