From 8dfbc70cee3dd7b82c24c1eeee2320fff6f891fe Mon Sep 17 00:00:00 2001 From: soup Date: Sun, 19 May 2024 00:26:08 -0400 Subject: [PATCH] Parse and execute print --- src/internal/execute.rs | 33 ++++++ src/internal/mod.rs | 1 + src/internal/parse.rs | 222 +++++++++++++++++++++++++++++++++----- src/lib.rs | 232 ++++++++++++++++++++++++++++++++++------ 4 files changed, 427 insertions(+), 61 deletions(-) create mode 100644 src/internal/execute.rs diff --git a/src/internal/execute.rs b/src/internal/execute.rs new file mode 100644 index 0000000..550865a --- /dev/null +++ b/src/internal/execute.rs @@ -0,0 +1,33 @@ +use core::fmt::Write; + +use crate::Value; + +use super::parse::{Ast, AstKind, Print}; + +pub fn execute(ast: &Ast, value: &dyn Value) -> String { + let mut out = String::new(); + for node in ast { + match node { + AstKind::Text(t) => out.push_str(&t), + AstKind::Print(p) => print(&mut out, p, value), + e => todo!("{e:?}"), + } + } + + out +} + +pub fn print(out: &mut String, p: &Print, value: &dyn Value) { + let path = &p.0 .0; + let mut value = value; + for name in path { + match value.lookup(name) { + Some(p) => value = p, + None => { + let _ = write!(out, "'{name}' missing in current value"); + }, + } + } + + value.print(out) +} diff --git a/src/internal/mod.rs b/src/internal/mod.rs index ba53dec..d62c0f9 100644 --- a/src/internal/mod.rs +++ b/src/internal/mod.rs @@ -2,4 +2,5 @@ //! considered stable when it comes to semver. However, they are exposed for //! advanced use cases. +pub mod execute; pub mod parse; diff --git a/src/internal/parse.rs b/src/internal/parse.rs index 0ef8e23..fe92ab7 100644 --- a/src/internal/parse.rs +++ b/src/internal/parse.rs @@ -1,26 +1,31 @@ -use crate::Result; +#[derive(Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub struct ValuePath(pub Vec); -pub struct ValuePath {} -pub enum Expr { - ValuePath(ValuePath), -} +pub type Expr = ValuePath; +#[derive(Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] pub struct If { - expr: Box, + expr: Expr, then: Ast, else_: Option, } + +#[derive(Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] pub struct For { - expr: Box, + expr: Expr, body: Ast, } + +#[derive(Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] pub struct Has { expr: Box, body: Ast, } -pub struct Print { - expr: Box, -} + +#[derive(Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub struct Print(pub Expr); + +#[derive(Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] pub enum AstKind { Text(String), Print(Print), @@ -30,29 +35,188 @@ pub enum AstKind { } pub type Ast = Vec; -pub fn parse(template: &str) -> Result { - let mut out = Ast::new(); - let mut remaining = template; - while !remaining.is_empty() { - let (ast, r) = parse_one(remaining)?; - remaining = r; - out.push(ast); - } - Ok(out) +pub struct ParseState<'a> { + template: &'a str, + last_byte_offset: usize, + speculative_text_starts_at: usize, + speculative_text_ends_at: usize, + current_byte_offset: usize, + ast: Ast, } +impl<'a> ParseState<'a> { + pub fn out_of_input(&self) -> bool { + self.current_byte_offset >= self.last_byte_offset + } -pub fn parse_one(template: &str) -> Result<(AstKind, &str)> { - if template.starts_with("{{") { - parse_text(template) - } else if let Some(r) = template.strip_prefix('{') { - parse_block(r) - } else { - parse_text(template) + pub fn has_input(&self) -> bool { + !self.out_of_input() + } + + pub fn remaining(&self) -> &str { + &self.template[self.current_byte_offset..] + } + + pub fn add_current_text_to_ast(&mut self) { + let text = &self.template + [self.speculative_text_starts_at..self.speculative_text_ends_at]; + if text.is_empty() { + return; + } + + self.ast.push(AstKind::Text( + self.template[self.speculative_text_starts_at + ..self.speculative_text_ends_at] + .to_string(), + )); + } + + pub fn text_might_end(&mut self) { + self.speculative_text_ends_at = self.current_byte_offset; + } + + pub fn parse(&mut self) { + while self.has_input() { + let started_at = self.current_byte_offset; + let block_start = match self.remaining().find('{') { + Some(bs) => bs, + None => { + self.current_byte_offset = self.last_byte_offset; + self.add_current_text_to_ast(); + break; + }, + }; + self.current_byte_offset += block_start; + self.text_might_end(); + self.maybe_parse_block(); + + if started_at == self.current_byte_offset { + panic!("Parser made no progress"); + } + } + } + + pub fn maybe_parse_block(&mut self) { + assert!(self.remaining().starts_with('{')); + self.current_byte_offset += 1; + self.skip_whitespace(); + + if self.remaining().starts_with('.') { + self.maybe_parse_print(); + } + } + + pub fn maybe_parse_print(&mut self) { + assert!(self.remaining().starts_with('.')); + let path = self.parse_path(); + + self.skip_whitespace(); + if !self.remaining().starts_with('}') { + return; + } + self.current_byte_offset += 1; + + self.push_node_and_text(AstKind::Print(Print(path))); + self.new_empty_text(); + } + + pub fn parse_path(&mut self) -> ValuePath { + assert!(self.remaining().starts_with('.')); + self.current_byte_offset += 1; + let end = self + .remaining() + .find(|ch: char| ch == '}' || ch.is_whitespace()) + .unwrap_or(self.last_byte_offset - self.current_byte_offset); + let inner = &self.remaining()[..end]; + let out = inner.split_terminator('.').map(|s| s.to_string()).collect(); + let out = ValuePath(out); + + self.current_byte_offset += end; + + out + } + + pub fn push_node_and_text(&mut self, node: AstKind) { + self.add_current_text_to_ast(); + self.ast.push(node); + } + + pub fn new_empty_text(&mut self) { + self.speculative_text_starts_at = self.current_byte_offset; + self.speculative_text_ends_at = self.speculative_text_starts_at + 1; + } + + pub fn skip_whitespace(&mut self) { + while self.has_input() { + if self.remaining().starts_with(char::is_whitespace) { + continue; + } + break; + } } } -pub fn parse_text(template: &str) -> Result<(AstKind, &str)> { - assert!(template.starts_with("{{") || !template.starts_with('{')); +pub fn parse(template: &str) -> Ast { + let mut state = ParseState { + template, + speculative_text_starts_at: 0, + speculative_text_ends_at: 1, + current_byte_offset: 0, + last_byte_offset: template.len(), + ast: Ast::new(), + }; + state.parse(); - let template.trim_start_matches(|c| c != '{') + state.ast +} + +#[cfg(test)] +mod test { + use crate::internal::parse::{AstKind, Print, ValuePath}; + + use super::{parse, Ast, For}; + + fn go(tmpl: &str, rhs: Ast) { + let ast = parse(tmpl); + assert_eq!(ast, rhs); + } + + #[test] + fn parse_print_only() { + go("{.}", vec![AstKind::Print(Print(ValuePath(vec![])))]); + } + + #[test] + fn parse_print() { + go( + "hello, {.}!", + vec![ + AstKind::Text("hello, ".to_string()), + AstKind::Print(Print(ValuePath(vec![]))), + AstKind::Text("!".to_string()), + ], + ); + } + + #[test] + fn parse_print_lookup() { + go( + "hello, {.name}!", + vec![ + AstKind::Text("hello, ".to_string()), + AstKind::Print(Print(ValuePath(vec!["name".to_string()]))), + AstKind::Text("!".to_string()), + ], + ); + } + + #[test] + fn parse_for() { + go( + "{for .}{.}{/for}", + vec![AstKind::For(For { + expr: ValuePath(vec![]), + body: vec![AstKind::Print(Print(ValuePath(vec![])))], + })], + ); + } } diff --git a/src/lib.rs b/src/lib.rs index 761647d..d0a8d95 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,27 +1,67 @@ +use core::fmt::{Display, Write}; + pub mod internal; -use std::fmt::Write; - -pub enum Error {} -pub type Result = core::result::Result; +use internal::{ + execute::execute, + parse::{parse, Ast}, +}; pub trait Value { - fn print(&self, w: &mut dyn Write) -> core::fmt::Result; - fn has(&self) -> Option<&dyn Value> - where - Self: Sized, - { - Some(self) + fn print(&self, out: &mut String); + fn lookup(&self, name: &str) -> Option<&dyn Value>; + fn has(&self) -> Option<&dyn Value>; + fn if_(&self) -> bool; + fn for_(&self, index: usize) -> Option<&dyn Value>; +} + +impl<'a, T> Value for &'a T +where + T: Value, +{ + fn print(&self, out: &mut String) { + (&**self).print(out) + } + + fn lookup(&self, name: &str) -> Option<&dyn Value> { + (&**self).lookup(name) + } + + fn has(&self) -> Option<&dyn Value> { + (&**self).has() } fn if_(&self) -> bool { - true + (&**self).if_() } - fn for_(&self, index: usize) -> Option<&dyn Value> - where - Self: Sized, - { + fn for_(&self, index: usize) -> Option<&dyn Value> { + (&**self).for_(index) + } +} + +impl Value for &str { + fn print(&self, out: &mut String) { + out.push_str(self); + } + + fn lookup(&self, _: &str) -> Option<&dyn Value> { + None + } + + fn has(&self) -> Option<&dyn Value> { + if self.is_empty() { + None + } else { + Some(self) + } + } + + fn if_(&self) -> bool { + self.is_empty() + } + + fn for_(&self, index: usize) -> Option<&dyn Value> { if index == 0 { Some(self) } else { @@ -30,38 +70,166 @@ pub trait Value { } } -impl<'a, T> Value for &'a T -where - T: Value, -{ - fn print(&self, w: &mut dyn Write) -> core::fmt::Result { - (&**self).print(w) +impl Value for (&str, &dyn Value) { + fn print(&self, out: &mut String) { + out.push('('); + self.0.print(out); + out.push_str(", "); + self.1.print(out); + } + + fn lookup(&self, name: &str) -> Option<&dyn Value> { + match name { + "0" => Some(&self.0), + "1" => Some(self.1), + _ => None, + } + } + + fn has(&self) -> Option<&dyn Value> { + Some(self) + } + + fn if_(&self) -> bool { + self.0.if_() && self.1.if_() + } + + fn for_(&self, index: usize) -> Option<&dyn Value> { + match index { + 0 => Some(&self.0), + 1 => Some(self.1), + _ => None, + } + } +} + +impl Value for (&dyn Value, &dyn Value) { + fn print(&self, out: &mut String) { + self.0.print(out); + self.1.print(out); + } + + fn lookup(&self, name: &str) -> Option<&dyn Value> { + match name { + "0" => Some(self.0), + "1" => Some(self.1), + _ => None, + } + } + + fn has(&self) -> Option<&dyn Value> { + Some(self) + } + + fn if_(&self) -> bool { + self.0.if_() && self.1.if_() + } + + fn for_(&self, index: usize) -> Option<&dyn Value> { + match index { + 0 => Some(self.0), + 1 => Some(self.1), + _ => None, + } + } +} + +impl Value for [(&str, &dyn Value); N] { + fn print(&self, w: &mut String) { + for (name, v) in self.iter() { + let _ = write!(w, "{name}"); + v.print(w); + } + } + + fn lookup(&self, name: &str) -> Option<&dyn Value> { + self.iter().find(|(n, _)| *n == name).map(|(_, v)| *v) + } + + fn has(&self) -> Option<&dyn Value> { + if self.is_empty() { + None + } else { + Some(self) + } + } + + fn if_(&self) -> bool { + self.is_empty() + } + + fn for_(&self, index: usize) -> Option<&dyn Value> { + self.get(index).map(|v| v as &dyn Value) + } +} + +impl Value for [&dyn Value; N] { + fn print(&self, w: &mut String) { + for v in self.iter() { + v.print(w) + } + } + + fn lookup(&self, name: &str) -> Option<&dyn Value> { + let idx: usize = name.parse().ok()?; + self.get(idx).copied() + } + + fn has(&self) -> Option<&dyn Value> { + if self.is_empty() { + None + } else { + Some(self) + } + } + + fn if_(&self) -> bool { + self.is_empty() + } + + fn for_(&self, index: usize) -> Option<&dyn Value> { + self.get(index).map(|v| *v) } } mod build {} -pub struct Newt {} +pub struct Newt { + ast: Ast, +} impl Newt { - pub fn build(template: &str) -> Result { + fn build(tmpl: &str) -> Self { + let ast = parse(tmpl); + Self { ast } } - pub fn build_and_execute( - template: &str, - value: &dyn Value, - ) -> Result { - let newt = Newt::build(template)?; - - newt.execute(value) + fn execute(&self, value: &dyn Value) -> String { + execute(&self.ast, value) } } #[cfg(test)] mod test { + use crate::{Newt, Value}; + #[test] - fn wishlist() { + fn simple_print() { let tmpl = r#"Hello, {.}"#; - let out = Newt::build_and_execute(tl, "World!").unwrap(); + let out = Newt::build(tmpl).execute(&"World!"); assert_eq!(out, "Hello, World!"); } + + #[test] + fn print_lookup() { + let tmpl = "Hello, {.name}!"; + let out = Newt::build(tmpl).execute(&[("name", &"Ted" as &dyn Value)]); + assert_eq!(out, "Hello, Ted!"); + } + + #[test] + fn print_for() { + let tmpl = "{for .}{.}, {/for}"; + let out = Newt::build(tmpl).execute(&[&"Bob" as &dyn Value, &"Larry"]); + assert_eq!(out, "Bob, Larry"); + } }