teacat_lib 0.5.0

Tools for working with TeaCat files
Documentation
use std::{borrow::Cow, collections::HashMap, ffi::OsStr, fmt::Display, path::Path};

use crate::{CatResult, Moo, prelude::*};

#[derive(Debug, Clone, Hash, PartialEq, Eq)]
pub struct ModuleName(pub Vec<String>);

#[derive(Debug, Clone, Default)]
pub struct Scope<'input>(Cow<'input, HashMap<Moo<'input>, LazyExpr<'input>>>);

#[derive(Debug, Clone)]
enum LazyModule<'input> {
	Awaiting(TeaCatAst<'input>),
	Finished(Scope<'input>),
}

#[derive(Debug, Clone)]
enum LazyExpr<'input> {
	Awaiting {
		captures: Scope<'input>,
		node: AstNode<'input>,
	},
	Finished(TeaCatDom<'input>),
}

#[derive(Default)]
pub struct Executor<'input> {
	modules: HashMap<ModuleName, LazyModule<'input>>,
}

impl ModuleName {
	pub fn new(path: impl AsRef<Path>) -> Self {
		Self::_new(path.as_ref())
	}
	fn _new(path: &Path) -> Self {
		let vec = path
			.with_extension("")
			.iter()
			.map(OsStr::to_string_lossy)
			.map(Cow::into_owned)
			.collect();

		Self(vec)
	}
}
impl Display for ModuleName {
	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
		f.write_str(&self.0.join("."))
	}
}

impl<'i> Scope<'i> {
	pub fn insert(&mut self, name: Moo<'i>, node: AstNode<'i>, captures: Scope<'i>) {
		self.0
			.to_mut()
			.insert(name, LazyExpr::Awaiting { captures, node });
	}

	fn insert_expr(&mut self, name: Moo<'i>, expr: LazyExpr<'i>) {
		self.0.to_mut().insert(name, expr);
	}
}

impl<'i> LazyModule<'i> {
	fn get_or_exec(&mut self, executor: &mut Executor<'i>) -> CatResult<&Scope<'i>> {
		match self {
			Self::Finished(scope) => Ok(scope),
			Self::Awaiting(ast) => {
				let ast = std::mem::replace(ast, vec![].into());
				let mut scope = Scope::default();
				let mut dom = TeaCatDom::default();

				for node in ast {
					executor.exec_node(node, &mut dom, &mut scope)?;
				}

				*self = Self::Finished(scope);
				self.get_or_exec(executor)
			}
		}
	}
}

impl<'i> LazyExpr<'i> {
	fn get_or_exec(&mut self, executor: &mut Executor<'i>) -> CatResult<&TeaCatDom<'i>> {
		match self {
			Self::Finished(dom) => Ok(dom),
			Self::Awaiting { captures, node } => {
				let node = std::mem::replace(node, AstNode::Space);
				let ast = vec![node].into();
				let dom = executor.exec_ast_with_scope(ast, captures.clone())?;

				*self = Self::Finished(dom);
				self.get_or_exec(executor)
			}
		}
	}
}

impl<'i> Executor<'i> {
	/// Gets a list of all modules contained in this [`Executor`].
	#[inline]
	#[must_use]
	pub fn list_modules(&self) -> Vec<ModuleName> {
		self.modules.keys().cloned().collect()
	}

	/// Removes all modules contained in this [`Executor`].
	#[inline]
	pub fn clear_modules(&mut self) {
		self.modules.clear();
	}

	/// If the given AST is a module, parse it and add it to the executor,
	/// overwriting any previous modules with the same [`ModuleName`].
	/// Otherwise, do nothing.
	#[inline]
	pub fn add_module(&mut self, name: ModuleName, ast: TeaCatAst<'i>) {
		if ast.module {
			self.modules.insert(name, LazyModule::Awaiting(ast));
		}
	}

	/// Executes an AST using the inserted modules.
	///
	/// Errors if the imported AST attempts to do something illegal, like using
	/// a non-existant module.
	///
	/// **Note**: unlike [`add_module`](Executor::add_module), this function does
	/// NOT check if the `AST` is a module.
	#[inline]
	pub fn exec_ast(&mut self, ast: TeaCatAst<'i>) -> CatResult<TeaCatDom<'i>> {
		self.exec_ast_with_scope(ast, Scope::default())
	}

	/// Executes an AST using the inserted modules and the given scope.
	///
	/// Errors if the imported AST attempts to do something illegal, like using
	/// a non-existant module.
	///
	/// **Note**: unlike [`add_module`](Executor::add_module), this function does
	/// NOT check if the `AST` is a module.
	#[inline]
	pub fn exec_ast_with_scope(
		&mut self,
		ast: TeaCatAst<'i>,
		mut scope: Scope<'i>,
	) -> CatResult<TeaCatDom<'i>> {
		let mut dom = TeaCatDom::default();

		for node in ast {
			self.exec_node(node, &mut dom, &mut scope)?;
		}

		Ok(dom)
	}

	fn exec_node(
		&mut self,
		node: AstNode<'i>,
		dom: &mut TeaCatDom<'i>,
		scope: &mut Scope<'i>,
	) -> CatResult<()> {
		match node {
			AstNode::Space => dom.push(DomNode::Space),
			AstNode::Word(moo) => dom.push(DomNode::Word(moo)),
			AstNode::Markup { of, inner } => dom.push(DomNode::Markup {
				of,
				inner: self.exec_ast_with_scope(inner, scope.clone())?,
			}),
			AstNode::Tag(tag) => dom.push(DomNode::Tag(DomTag {
				name: tag.name,
				attrs: tag.attrs,
				content: self.exec_ast_with_scope(tag.content, scope.clone())?,
			})),
			AstNode::With { path, imports } => {
				self.get_or_exec_module(path)?
					.0
					.iter()
					.filter(|(c, _)| imports.contains(c))
					.for_each(|(c, n)| scope.insert_expr(c.clone(), n.clone()));
			}
			AstNode::Define { name, content } => scope.insert(name, *content, scope.clone()),
			AstNode::ContentBlock(ast) => dom.push(DomNode::ContentBlock(
				self.exec_ast_with_scope(ast, scope.clone())?,
			)),
			AstNode::Variable(name) => {
				let Some(var) = scope.0.to_mut().get_mut(&name) else {
					return Err(TeaCatErr::NotInScope(name.to_string()));
				};

				dom.append(var.get_or_exec(self)?.clone());
			}
		}

		Ok(())
	}

	fn get_or_exec_module(&mut self, path: ModuleName) -> CatResult<Scope<'i>> {
		let Some(mut module) = self.modules.get(&path).cloned() else {
			return Err(TeaCatErr::ModuleNotFound(path));
		};

		let scope = module.get_or_exec(self)?.clone();
		self.modules.insert(path.clone(), module);

		Ok(scope)
	}
}