diff --git a/Cargo.lock b/Cargo.lock index 72dd9f3..60269ce 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -240,6 +240,15 @@ dependencies = [ "syn", ] +[[package]] +name = "document-features" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" +dependencies = [ + "litrs", +] + [[package]] name = "either" version = "1.15.0" @@ -420,6 +429,12 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" +[[package]] +name = "litrs" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" + [[package]] name = "log" version = "0.4.29" @@ -439,7 +454,9 @@ dependencies = [ "annotate-snippets", "criterion", "displaydoc", + "document-features", "insta", + "nomo", "serde", "serde_json", "test_each_file", diff --git a/Cargo.toml b/Cargo.toml index 35f087d..6378b08 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,9 +16,14 @@ harness = false [profile.bench] debug = true +[lints.rust] +unsafe_code = "forbid" +missing_docs = "warn" + [dependencies] annotate-snippets = "0.12.13" displaydoc = "0.2.5" +document-features = { version = "0.2.12", optional = true } serde_json = { version = "1.0.149", optional = true } thiserror = "2.0.18" winnow = { version = "0.7.14", features = ["unstable-recover"] } @@ -30,11 +35,19 @@ insta = { version = "1.46.3", features = ["glob", "serde"] } serde = { version = "1.0.228", features = ["derive"] } serde_json = "1.0.149" test_each_file = "0.3.7" +nomo = { path = ".", features = ["unstable-pub"] } [profile.dev.package] insta.opt-level = 3 similar.opt-level = 3 +[package.metadata.docs.rs] +features = ["document-features"] + [features] default = ["serde_json"] +## Add support for inserting [`serde_json::Value`]s into [`Context`] objects serde_json = ["dep:serde_json"] +## Get access to the internals of the crate ⚠️ This is a perma-unstable feature ⚠️ +unstable-pub = [] +document-features = ["dep:document-features"] diff --git a/fuzz/Cargo.lock b/fuzz/Cargo.lock index ee307ed..2cf07f7 100644 --- a/fuzz/Cargo.lock +++ b/fuzz/Cargo.lock @@ -112,7 +112,7 @@ checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] name = "nomo" -version = "0.1.0" +version = "0.0.1" dependencies = [ "annotate-snippets", "displaydoc", diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index e6af9c6..fe1acb7 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -12,6 +12,7 @@ libfuzzer-sys = "0.4" [dependencies.nomo] path = ".." +features = ["unstable-pub"] [[bin]] name = "fuzz_target_1" diff --git a/src/compiler/mod.rs b/src/compiler/mod.rs index 4fc3644..7f8cddd 100644 --- a/src/compiler/mod.rs +++ b/src/compiler/mod.rs @@ -63,6 +63,7 @@ pub enum Instruction { slot: VariableSlot, }, PushScope { + #[expect(unused)] inherit_parent: bool, }, Abort, @@ -93,6 +94,7 @@ pub enum Instruction { value_slot: VariableSlot, }, LoadLiteralToSlot { + #[allow(unused)] source: TemplateToken, value: NomoValue, slot: VariableSlot, diff --git a/src/functions.rs b/src/functions.rs index c15031c..8eb33d2 100644 --- a/src/functions.rs +++ b/src/functions.rs @@ -7,42 +7,55 @@ use thiserror::Error; use crate::NomoValueError; use crate::value::NomoValue; +/// Possible errors while executing a function #[derive(Debug, Error, Display)] pub enum NomoFunctionError { /// Received {received} arguments, but this function only takes {expected} + #[expect(missing_docs)] WrongArgumentCount { received: usize, expected: usize }, /// The argument at this position is of the wrong type + #[expect(missing_docs)] InvalidArgumentType { index: usize }, /// A user-provided error #[error(transparent)] CustomError { + #[expect(missing_docs)] custom: Box, }, } +/// A function that can be used inside a template pub trait NomoFunction: 'static + Send + Sync { + /// Call the function with the given arguments fn call(&self, args: Vec) -> Result; } +#[cfg(feature = "unstable-pub")] #[derive(Default)] pub struct FunctionMap { funcs: HashMap, } +#[cfg(not(feature = "unstable-pub"))] +#[derive(Default)] +pub(crate) struct FunctionMap { + funcs: HashMap, +} + impl FunctionMap { pub fn register, T>(&mut self, name: impl Into, func: NF) { self.funcs .insert(name.into(), ErasedNomoFunction::erase(func)); } - pub fn get(&self, name: impl AsRef) -> Option<&ErasedNomoFunction> { + pub(crate) fn get(&self, name: impl AsRef) -> Option<&ErasedNomoFunction> { self.funcs.get(name.as_ref()) } } -pub struct ErasedNomoFunction { +pub(crate) struct ErasedNomoFunction { func: Box, call_fn: fn(&dyn Any, Vec) -> Result, } diff --git a/src/input.rs b/src/input.rs index ba404eb..70c01b2 100644 --- a/src/input.rs +++ b/src/input.rs @@ -8,6 +8,7 @@ use winnow::stream::Offset; use winnow::stream::Stream; use winnow::stream::StreamIsPartial; +/// The input for templates in [nomo](crate) #[derive(Clone, PartialEq, Eq)] pub struct NomoInput { backing: Arc, @@ -15,19 +16,26 @@ pub struct NomoInput { } impl NomoInput { + /// Manually create an input + /// + /// While it is not unsafe to pass in mismatched informations, the outcome will most likely not + /// work and possibly even panic later pub fn from_parts(backing: Arc, range: Range) -> NomoInput { NomoInput { backing, range } } + /// Turn the input into its parts pub fn into_parts(self) -> (Arc, Range) { (self.backing, self.range) } + /// Get the range this input covers pub fn get_range(&self) -> Range { self.range.clone() } } +#[doc(hidden)] #[derive(Debug, Clone)] pub struct NomoInputCheckpoint { range: Range, @@ -78,6 +86,7 @@ impl Deref for NomoInput { } impl NomoInput { + /// Get the input as a [`str`] pub fn as_str(&self) -> &str { self.deref() } @@ -101,6 +110,8 @@ impl Offset for NomoInput { } } + +#[doc(hidden)] pub struct NomoInputIter { idx: usize, input: NomoInput, diff --git a/src/lib.rs b/src/lib.rs index 7874d4b..9311ac0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,33 @@ +//! # Nomo, the templating library +//! +//! To get started, add the crate to your project: +//! +//! ```bash +//! cargo add nomo +//! ``` +//! +//! Then, load some templates: +//! +//! ```rust +//! # fn main() -> Result<(), Box> { +//! let mut templates = nomo::Nomo::new(); +//! templates.add_template("index.html", "

Hello {{= name }}!

"); +//! +//! let mut context = nomo::Context::new(); +//! context.insert("name", "World"); +//! let result = templates.render("index.html", &context)?; +//! +//! assert_eq!(result, "

Hello World!

"); +//! # Ok(()) } +//! ``` +//! +//! The crate has the following feature flags: +#![cfg_attr( + feature = "document-features", + cfg_attr(doc, doc = ::document_features::document_features!()) +)] +//! + use std::collections::HashMap; use displaydoc::Display; @@ -5,34 +35,65 @@ use thiserror::Error; use crate::compiler::VMInstructions; use crate::functions::FunctionMap; +use crate::functions::NomoFunction; use crate::input::NomoInput; use crate::value::NomoValue; use crate::value::NomoValueError; -pub mod compiler; -pub mod eval; +macro_rules! unstable_pub { + ($(#[$m:meta])* mod $name:ident) => { + $(#[$m])* + #[cfg(feature = "unstable-pub")] + pub mod $name; + #[cfg(not(feature = "unstable-pub"))] + mod $name; + }; +} + +unstable_pub!( + /// The compiler internals + mod compiler +); +unstable_pub!( + /// Evaluation internals + mod eval +); +unstable_pub!( + /// Lexer internals + mod lexer +); +unstable_pub!( + /// Parser internals + mod parser +); + +/// Nomo Functions pub mod functions; +/// Input for nomo pub mod input; -pub mod lexer; -pub mod parser; +/// Values used in Nomo pub mod value; +/// Errors related to parsing and evaluating templates #[derive(Debug, Error, Display)] pub enum NomoError { /// Could not parse the given template ParseError { #[from] + #[expect(missing_docs)] source: lexer::ParseFailure, }, /// Invalid Template AstError { #[from] + #[expect(missing_docs)] source: parser::AstFailure, }, /// An error occurred while evaluating EvaluationError { #[from] + #[expect(missing_docs)] source: eval::EvaluationError, }, @@ -40,6 +101,7 @@ pub enum NomoError { UnknownTemplate(String), } +/// The main struct and entry point for the [`nomo`](crate) pub struct Nomo { templates: HashMap, function_map: FunctionMap, @@ -52,6 +114,7 @@ impl Default for Nomo { } impl Nomo { + /// Create a new Nomo Instance pub fn new() -> Nomo { Nomo { templates: HashMap::new(), @@ -59,6 +122,7 @@ impl Nomo { } } + /// Add a new template pub fn add_template( &mut self, name: impl Into, @@ -76,6 +140,17 @@ impl Nomo { Ok(()) } + /// Register a function to make it available when rendering + pub fn register_function(&mut self, name: impl Into, f: impl NomoFunction) { + self.function_map.register(name, f); + } + + /// List of currently available templates + pub fn templates(&self) -> Vec { + self.templates.keys().cloned().collect() + } + + /// Render a specific template pub fn render(&self, name: &str, ctx: &Context) -> Result { let template = self .templates @@ -92,6 +167,7 @@ struct Template { instructions: VMInstructions, } +/// The context for a given render call pub struct Context { values: HashMap, } @@ -103,12 +179,16 @@ impl Default for Context { } impl Context { + /// Create new Context pub fn new() -> Context { Context { values: HashMap::new(), } } + /// Add a value into the map + /// + /// If you enable the `serde_json` feature, you can insert [`serde_json::Value`]s pub fn try_insert( &mut self, key: impl Into, @@ -119,17 +199,19 @@ impl Context { Ok(()) } + /// Add a value into the map, panic if you can't pub fn insert(&mut self, key: impl Into, value: impl Into) { self.values.insert(key.into(), value.into()); } + /// Access the values inside this context pub fn values(&self) -> &HashMap { &self.values } } #[derive(Debug, Clone)] -pub struct SourceSpan { +struct SourceSpan { pub range: std::ops::Range, } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 43481a2..a357725 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -261,6 +261,7 @@ pub enum TemplateAstExpr<'input> { ElseConditional { expression: Option>>, }, + #[expect(unused)] Invalid(&'input [TemplateToken]), MathOperation { op: TokenOperator, diff --git a/src/value.rs b/src/value.rs index ab317bb..903fc73 100644 --- a/src/value.rs +++ b/src/value.rs @@ -1,11 +1,12 @@ use std::borrow::Cow; use std::collections::BTreeMap; -#[cfg(feature = "serde_json")] use displaydoc::Display; use thiserror::Error; +/// Values to be used inside templates #[derive(Clone)] +#[expect(missing_docs)] pub enum NomoValue { String { value: Cow<'static, str>, @@ -35,6 +36,7 @@ pub enum NomoValue { } impl NomoValue { + /// Return a str if there is one inside pub fn as_str(&self) -> Option<&str> { if let Self::String { value } = self { Some(value) @@ -43,6 +45,7 @@ impl NomoValue { } } + /// Return an arry if there is one inside pub fn as_array(&self) -> Option<&[NomoValue]> { if let Self::Array { value } = self { Some(value) @@ -51,6 +54,7 @@ impl NomoValue { } } + /// Return a bool if there is one inside pub fn as_bool(&self) -> Option { if let Self::Bool { value } = self { Some(*value) @@ -61,6 +65,7 @@ impl NomoValue { } } + /// Return an object if there is one inside pub fn as_object(&self) -> Option<&BTreeMap> { if let Self::Object { value } = self { Some(value) @@ -69,6 +74,7 @@ impl NomoValue { } } + /// Return an integer if there is one inside pub fn as_integer(&self) -> Option { if let Self::Integer { value } = self { Some(*value) @@ -77,6 +83,7 @@ impl NomoValue { } } + /// Return a float if there is one inside pub fn as_float(&self) -> Option { if let Self::Float { value } = self { Some(*value) @@ -85,6 +92,7 @@ impl NomoValue { } } + /// Return the iterator if there is one inside pub fn as_iterator(&self) -> Option<&dyn CloneIterator> { if let Self::Iterator { value } = self { Some(value) @@ -93,6 +101,7 @@ impl NomoValue { } } + /// Return the iterator mutably if there is one inside pub fn as_iterator_mut(&mut self) -> Option<&mut dyn CloneIterator> { if let Self::Iterator { value } = self { Some(value) @@ -249,7 +258,9 @@ impl NomoValue { } } +/// Marker trait for iterators that can be cloned pub trait CloneIterator: Iterator { + /// Create a new instance of the iterator inside a [Box] fn clone_box(&self) -> Box>; }