#![warn(clippy::pedantic)]
#![warn(missing_docs)]
#![doc = include_str!("../README.md")]
use ariadne::{Cache, Source};
use ast::{SourceFile, Template};
use builtins::{StandardContextWorker, StandardContextualizer, StandardEscaper};
use itertools::Itertools;
use maybe_sync::{dyn_maybe_send_sync, Rc};
#[cfg(feature = "serde")]
use rendertime::value::serde_values;
use std::fmt::{Debug, Display};
use std::hash::Hash;
use std::marker::PhantomData;
use std::{collections::HashMap, fmt};
#[doc(hidden)]
#[allow(non_snake_case)]
pub mod __internal__tests_util;
mod ast;
mod parse;
pub use parse::Error as ParseError;
pub(crate) mod rendertime;
pub use rendertime::Error as RendertimeError;
pub use rendertime::Value;
#[cfg(feature = "serde")]
pub use serde_values::Error as SerializationError;
mod type_checking;
pub use type_checking::{Error as TypeCheckingError, Indentation, Type};
use crate::ast::TypeDefinition;
pub mod builtins;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ContextualizationError {
UnknownTextType(String),
InvalidStatic,
IncompatibleTextType {
target_ty: String,
source_ty: String,
},
StartAndEndDifferentContext(String, String),
}
impl Display for ContextualizationError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::UnknownTextType(ty) => write!(f, "Text type {ty:?} unknown"),
Self::InvalidStatic => write!(f, "Static text invalid for given text type"),
Self::IncompatibleTextType {
target_ty,
source_ty: dynamic_ty,
} => write!(
f,
"The target type {target_ty:?} is incompatible with the source type {dynamic_ty:?}"
),
Self::StartAndEndDifferentContext(start, end) => write!(
f,
"The static text starts in type {start:?} but ends in {end:?}"
),
}
}
}
impl std::error::Error for ContextualizationError {}
pub trait Contextualizer<Esc, Wrk>: Debug
where
Esc: Escaper,
Wrk: ContextWorker<Esc>,
{
fn default_text_type(&self) -> String;
fn contextualize(&self, text_ty: &str) -> Result<Wrk, ContextualizationError>;
fn escaper(&self, target_ty: &str, source_ty: &str) -> Result<Esc, ContextualizationError> {
self.contextualize(target_ty)?.dynamic(source_ty)
}
}
pub trait ContextWorker<Esc: Escaper> {
fn push_static(&mut self, s: &str) -> Result<(), ContextualizationError>;
fn dynamic(&mut self, input_ty: &str) -> Result<Esc, ContextualizationError>;
}
pub trait Escaper: Debug + Clone {
fn escape(&self, inp: Value) -> Result<Value, String>;
}
pub trait Function {
fn accepts(&self, args: Vec<Type>) -> Option<Type>;
fn run(
&self,
args: Vec<Value>,
) -> Result<Value, Box<maybe_sync::dyn_maybe_send_sync!(std::error::Error)>>;
}
pub struct Templates<Esc = StandardEscaper> {
tmpl_names: HashMap<String, usize>,
tmpls: Vec<type_checking::Template<Esc>>,
}
impl<Esc> Debug for Templates<Esc> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Templates")
.field("templates", &self.tmpl_names.keys().collect_vec())
.finish_non_exhaustive()
}
}
#[derive(Clone, Copy, Hash, PartialEq, Eq)]
pub struct Span<'s> {
start: usize,
end: usize,
path_and_source: (&'s str, &'s str),
}
impl Debug for Span<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{start}..{end}", start = self.start, end = self.end)
}
}
impl<'s> chumsky::span::Span for Span<'s> {
type Context = (&'s str, &'s str);
type Offset = usize;
fn new(context: Self::Context, range: std::ops::Range<Self::Offset>) -> Self {
Self {
start: range.start,
end: range.end,
path_and_source: context,
}
}
fn context(&self) -> Self::Context {
self.path_and_source
}
fn start(&self) -> Self::Offset {
self.start
}
fn end(&self) -> Self::Offset {
self.end
}
}
impl<'s> ariadne::Span for Span<'s> {
type SourceId = (&'s str, &'s str);
fn source(&self) -> &Self::SourceId {
&self.path_and_source
}
fn start(&self) -> usize {
self.start
}
fn end(&self) -> usize {
self.end
}
}
pub enum Error<'s> {
Parse(ParseError<'s>),
TypeChecking(Box<type_checking::Error<'s>>),
}
struct AriadneCache<'s>(HashMap<&'s str, Source<&'s str>>);
impl<'s> Cache<(&'s str, &'s str)> for AriadneCache<'s> {
type Storage = &'s str;
fn fetch(
&mut self,
id: &(&'s str, &'s str),
) -> Result<&Source<Self::Storage>, impl fmt::Debug> {
Ok::<_, Box<dyn Debug>>(self.0.entry(id.0).or_insert_with(|| Source::from(id.1)))
}
fn display<'a>(&self, id: &'a (&'s str, &'s str)) -> Option<impl fmt::Display + 'a> {
Some(Box::new(id.0))
}
}
impl Debug for Error<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("\n")?;
<Self as Display>::fmt(self, f)
}
}
impl Display for Error<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Error::Parse(err) => {
if f.alternate() {
write!(f, "{err:#}")
} else {
write!(f, "{err}")
}
}
Error::TypeChecking(err) => {
if f.alternate() {
write!(f, "{err:#}")
} else {
write!(f, "{err}")
}
}
}
}
}
impl std::error::Error for Error<'_> {}
pub struct TemplatesBuilder<
's,
Ctx = StandardContextualizer,
Esc = StandardEscaper,
Wrk = StandardContextWorker,
> {
tmpls: Vec<Template<'s>>,
type_definitions: Vec<(TypeDefinition<'s>, Span<'s>)>,
funcs: HashMap<&'s str, Rc<dyn_maybe_send_sync!(Function)>>,
ctxer: Ctx,
_esc: PhantomData<Esc>,
_wrk: PhantomData<Wrk>,
}
impl Debug for TemplatesBuilder<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("TemplatesBuilder")
.field("tmpls", &self.tmpls)
.field("funcs", &self.funcs.keys())
.finish()
}
}
impl<'s, Ctx, Esc, Wrk> TemplatesBuilder<'s, Ctx, Esc, Wrk> {
pub fn with_source_file(
mut self,
path: &'s str,
source: &'s str,
) -> Result<TemplatesBuilder<'s, Ctx, Esc, Wrk>, ParseError<'s>> {
let (tmpls, ty_defs) = SourceFile::parse(path, source)?.into_templates_and_type_defs();
self.tmpls.extend(tmpls);
self.type_definitions.extend(ty_defs);
Ok(self)
}
pub fn with_source_files<I: IntoIterator<Item = (&'s str, &'s str)>>(
self,
sources: I,
) -> Result<TemplatesBuilder<'s, Ctx, Esc, Wrk>, ParseError<'s>> {
let mut res = Ok(self);
for (path, source) in sources {
let inter_res =
SourceFile::parse(path, source).map(SourceFile::into_templates_and_type_defs);
res = match (res, inter_res) {
(Ok(mut builder), Ok((templates, type_definitions))) => {
builder.tmpls.extend(templates);
builder.type_definitions.extend(type_definitions);
Ok(builder)
}
(Ok(_), Err(errors)) => Err(errors),
(res @ Err(_), Ok(_)) => res,
(Err(mut prev_error), Err(next_error)) => {
if let ParseError::Multiple(ref mut prev) = prev_error {
prev.push(next_error);
return Err(prev_error);
}
return Err(ParseError::Multiple(vec![prev_error, next_error]));
}
}
}
res
}
#[must_use]
pub fn with_function(
mut self,
name: &'s str,
func: impl Function + 'static + maybe_sync::MaybeSync + maybe_sync::MaybeSend,
) -> TemplatesBuilder<'s, Ctx, Esc, Wrk> {
self.funcs.insert(name, Rc::new(func));
self
}
#[must_use]
pub fn with_functions(
mut self,
funcs: impl IntoIterator<Item = (&'s str, Rc<dyn_maybe_send_sync!(Function)>)>,
) -> TemplatesBuilder<'s, Ctx, Esc, Wrk> {
self.funcs.extend(funcs);
self
}
pub fn build(self) -> Result<Templates<Esc>, Error<'s>>
where
Esc: Escaper,
Ctx: Contextualizer<Esc, Wrk>,
Wrk: ContextWorker<Esc>,
{
let (tmpls, tmpl_names) = type_checking::type_check_templates(
self.tmpls,
builtins::functions()
.into_iter()
.chain(self.funcs.into_iter())
.collect(),
self.type_definitions,
&self.ctxer,
)
.map_err(Box::new)
.map_err(Error::TypeChecking)?;
Ok(Templates { tmpl_names, tmpls })
}
#[must_use]
pub fn template_names<'a>(&'a self) -> impl ExactSizeIterator<Item = &'s str> + 'a {
self.tmpls.iter().map(|tmpl| tmpl.name.0)
}
}
impl<Ctx: Default, Esc, Wrk> TemplatesBuilder<'_, Ctx, Esc, Wrk> {
#[must_use]
pub fn new() -> Self {
TemplatesBuilder {
tmpls: Vec::new(),
type_definitions: Vec::new(),
funcs: builtins::functions().into_iter().collect(),
ctxer: Ctx::default(),
_esc: PhantomData,
_wrk: PhantomData,
}
}
}
impl<Ctx, Esc, Wrk> TemplatesBuilder<'_, Ctx, Esc, Wrk> {
#[must_use]
pub fn new_with_ctxer(ctxer: Ctx) -> Self {
TemplatesBuilder {
tmpls: Vec::new(),
type_definitions: Vec::new(),
funcs: builtins::functions().into_iter().collect(),
ctxer,
_esc: PhantomData,
_wrk: PhantomData,
}
}
}
impl<Ctx> Default for TemplatesBuilder<'_, Ctx>
where
Ctx: Default,
{
fn default() -> Self {
Self::new()
}
}
#[cfg(feature = "serde")]
#[derive(Debug)]
pub enum SerdeRenderError {
Serde(serde_values::Error),
Rendertime(rendertime::Error),
}
#[cfg(feature = "serde")]
impl Display for SerdeRenderError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Serde(err) => Display::fmt(&err, f),
Self::Rendertime(err) => Display::fmt(&err, f),
}
}
}
#[cfg(feature = "serde")]
impl std::error::Error for SerdeRenderError {}
impl<'s, Esc: Escaper> Templates<Esc> {
pub fn new<'f, Ctx, Wrk>(
source_files: impl IntoIterator<Item = (&'s str, &'s str)>,
functions: HashMap<&'s str, Rc<dyn_maybe_send_sync!(Function)>>,
) -> Result<Templates<Esc>, Error<'s>>
where
Ctx: Contextualizer<Esc, Wrk> + Default,
Esc: Escaper,
Wrk: ContextWorker<Esc>,
{
TemplatesBuilder::<Ctx, Esc, Wrk>::new()
.with_source_files(source_files)
.map_err(Error::Parse)?
.with_functions(functions)
.build()
}
#[must_use]
pub fn builder<Ctx, Wrk>() -> TemplatesBuilder<'s, Esc, Ctx, Wrk>
where
Esc: Default,
{
TemplatesBuilder::new()
}
pub fn new_with_ctxer<'f, Ctx, Wrk>(
source_files: impl IntoIterator<Item = (&'s str, &'s str)>,
functions: HashMap<&'s str, Rc<dyn_maybe_send_sync!(Function)>>,
ctxer: Ctx,
) -> Result<Templates<Esc>, Error<'s>>
where
Ctx: Contextualizer<Esc, Wrk> + Default,
Esc: Escaper,
Wrk: ContextWorker<Esc>,
{
TemplatesBuilder::<Ctx, Esc, Wrk>::new_with_ctxer(ctxer)
.with_source_files(source_files)
.map_err(Error::Parse)?
.with_functions(functions)
.build()
}
pub fn render<V: Into<Value>>(&self, name: &str, args: V) -> Result<String, RendertimeError> {
let Value::Struct(args) = args.into() else {
return Err(rendertime::Error::NonStructRootValue);
};
rendertime::tmpl_output(self, self.tmpl_names[name], args)
}
pub fn render_fmt<V: Into<Value>>(
&self,
name: &str,
args: V,
writer: impl fmt::Write,
) -> Result<(), rendertime::Error> {
let Value::Struct(args) = args.into() else {
return Err(rendertime::Error::NonStructRootValue);
};
rendertime::write_tmpl(writer, self, self.tmpl_names[name], args)
}
#[cfg(feature = "serde")]
pub fn render_serde(
&self,
name: &str,
args: impl serde::ser::Serialize,
) -> Result<String, SerdeRenderError> {
self.render(
name,
args.serialize(serde_values::ValueSerializer)
.map_err(SerdeRenderError::Serde)?,
)
.map_err(SerdeRenderError::Rendertime)
}
#[cfg(feature = "serde")]
pub fn render_serde_fmt(
&self,
name: &str,
args: impl serde::ser::Serialize,
writer: impl fmt::Write,
) -> Result<(), SerdeRenderError> {
self.render_fmt(
name,
args.serialize(serde_values::ValueSerializer)
.map_err(SerdeRenderError::Serde)?,
writer,
)
.map_err(SerdeRenderError::Rendertime)
}
}