Parse and execute print
This commit is contained in:
parent
453d954063
commit
8dfbc70cee
33
src/internal/execute.rs
Normal file
33
src/internal/execute.rs
Normal file
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -1,26 +1,31 @@
|
|||
use crate::Result;
|
||||
#[derive(Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
||||
pub struct ValuePath(pub Vec<String>);
|
||||
|
||||
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: Expr,
|
||||
then: Ast,
|
||||
else_: Option<Ast>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
||||
pub struct For {
|
||||
expr: Box<Expr>,
|
||||
expr: Expr,
|
||||
body: Ast,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
||||
pub struct Has {
|
||||
expr: Box<Expr>,
|
||||
body: Ast,
|
||||
}
|
||||
pub struct Print {
|
||||
expr: Box<Expr>,
|
||||
}
|
||||
|
||||
#[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<AstKind>;
|
||||
|
||||
pub fn parse(template: &str) -> Result<Ast> {
|
||||
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![])))],
|
||||
})],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
232
src/lib.rs
232
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<T> = core::result::Result<T, Error>;
|
||||
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<const N: usize> 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<const N: usize> 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<Self> {
|
||||
fn build(tmpl: &str) -> Self {
|
||||
let ast = parse(tmpl);
|
||||
Self { ast }
|
||||
}
|
||||
|
||||
pub fn build_and_execute(
|
||||
template: &str,
|
||||
value: &dyn Value,
|
||||
) -> Result<String> {
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue