Add first working pipeline of parse -> ast -> instr -> render
Signed-off-by: Marcel Müller <neikos@neikos.email>
This commit is contained in:
parent
f5050e369e
commit
1ea15f0e49
5 changed files with 234 additions and 6 deletions
|
|
@ -24,6 +24,12 @@ pub struct TemplateAst<'input> {
|
|||
root: Vec<TemplateAstExpr<'input>>,
|
||||
}
|
||||
|
||||
impl<'input> TemplateAst<'input> {
|
||||
pub fn root(&self) -> &[TemplateAstExpr<'input>] {
|
||||
&self.root
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AstError {
|
||||
pub(crate) message: Option<String>,
|
||||
|
|
@ -145,6 +151,7 @@ pub enum TemplateAstExpr<'input> {
|
|||
StaticContent(TemplateToken<'input>),
|
||||
Interpolation {
|
||||
prev_whitespace: Option<TemplateToken<'input>>,
|
||||
wants_output: Option<TemplateToken<'input>>,
|
||||
expression: Box<TemplateAstExpr<'input>>,
|
||||
post_whitespace: Option<TemplateToken<'input>>,
|
||||
},
|
||||
|
|
@ -172,10 +179,11 @@ fn parse_interpolation<'input>(
|
|||
)
|
||||
.with_taken()
|
||||
.map(|(expr, taken)| expr.unwrap_or(TemplateAstExpr::Invalid(taken)));
|
||||
let (prev_whitespace, _left, (expression, _right, post_whitespace)) = (
|
||||
let (prev_whitespace, _left, (wants_output, expression, _right, post_whitespace)) = (
|
||||
opt(TokenKind::Whitespace),
|
||||
TokenKind::LeftDelim,
|
||||
cut_err((
|
||||
opt(TokenKind::WantsOutput),
|
||||
delimited(ignore_ws, expr_parser, ignore_ws).map(Box::new),
|
||||
TokenKind::RightDelim,
|
||||
opt(TokenKind::Whitespace),
|
||||
|
|
@ -185,6 +193,7 @@ fn parse_interpolation<'input>(
|
|||
|
||||
Ok(TemplateAstExpr::Interpolation {
|
||||
prev_whitespace,
|
||||
wants_output,
|
||||
expression,
|
||||
post_whitespace,
|
||||
})
|
||||
|
|
@ -230,7 +239,7 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn check_simple_variable_interpolation() {
|
||||
let input = "Hello {{ world }}";
|
||||
let input = "Hello {{= world }}";
|
||||
|
||||
let parsed = crate::parser::parse(input).unwrap();
|
||||
|
||||
|
|
@ -252,6 +261,12 @@ mod tests {
|
|||
source: " ",
|
||||
},
|
||||
),
|
||||
wants_output: Some(
|
||||
TemplateToken {
|
||||
kind: WantsOutput,
|
||||
source: "=",
|
||||
},
|
||||
),
|
||||
expression: VariableAccess(
|
||||
TemplateToken {
|
||||
kind: Ident,
|
||||
|
|
|
|||
139
src/emit/mod.rs
Normal file
139
src/emit/mod.rs
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
use crate::ast::TemplateAstExpr;
|
||||
|
||||
pub struct EmitMachine {
|
||||
current_index: usize,
|
||||
}
|
||||
|
||||
impl EmitMachine {
|
||||
fn reserve_slot(&mut self) -> VariableSlot {
|
||||
VariableSlot {
|
||||
index: {
|
||||
let val = self.current_index;
|
||||
self.current_index += 1;
|
||||
val
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub struct VariableSlot {
|
||||
index: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Instruction {
|
||||
AppendContent { content: String },
|
||||
LoadFromContextToSlot { name: String, slot: VariableSlot },
|
||||
EmitFromSlot { slot: VariableSlot },
|
||||
PushScope { inherit_parent: bool },
|
||||
Abort,
|
||||
}
|
||||
|
||||
pub fn emit_machine(input: crate::ast::TemplateAst<'_>) -> Vec<Instruction> {
|
||||
let mut eval = vec![];
|
||||
|
||||
let mut machine = EmitMachine { current_index: 0 };
|
||||
|
||||
for ast in input.root() {
|
||||
emit_ast_expr(&mut machine, &mut eval, ast);
|
||||
}
|
||||
|
||||
eval
|
||||
}
|
||||
|
||||
fn emit_ast_expr(machine: &mut EmitMachine, eval: &mut Vec<Instruction>, ast: &TemplateAstExpr<'_>) {
|
||||
match ast {
|
||||
TemplateAstExpr::StaticContent(template_token) => {
|
||||
eval.push(Instruction::AppendContent {
|
||||
content: template_token.source().to_string(),
|
||||
});
|
||||
}
|
||||
TemplateAstExpr::Interpolation {
|
||||
prev_whitespace,
|
||||
wants_output,
|
||||
expression,
|
||||
post_whitespace,
|
||||
} => {
|
||||
if let Some(ws) = prev_whitespace {
|
||||
eval.push(Instruction::AppendContent {
|
||||
content: ws.source().to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
let emit_slot = machine.reserve_slot();
|
||||
emit_expr(machine, eval, emit_slot, expression);
|
||||
|
||||
if wants_output.is_some() {
|
||||
eval.push(Instruction::EmitFromSlot { slot: emit_slot });
|
||||
}
|
||||
|
||||
if let Some(ws) = post_whitespace {
|
||||
eval.push(Instruction::AppendContent {
|
||||
content: ws.source().to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
TemplateAstExpr::Invalid { .. } | TemplateAstExpr::VariableAccess { .. } => {
|
||||
eval.push(Instruction::Abort)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn emit_expr(
|
||||
machine: &mut EmitMachine,
|
||||
eval: &mut Vec<Instruction>,
|
||||
emit_slot: VariableSlot,
|
||||
expression: &TemplateAstExpr<'_>,
|
||||
) {
|
||||
match expression {
|
||||
TemplateAstExpr::VariableAccess(template_token) => {
|
||||
eval.push(Instruction::LoadFromContextToSlot {
|
||||
name: template_token.source().to_string(),
|
||||
slot: emit_slot,
|
||||
});
|
||||
}
|
||||
TemplateAstExpr::Invalid { .. } => eval.push(Instruction::Abort),
|
||||
TemplateAstExpr::StaticContent { .. } | TemplateAstExpr::Interpolation { .. } => {
|
||||
unreachable!("Invalid AST here")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::emit::emit_machine;
|
||||
|
||||
#[test]
|
||||
fn check_simple_variable_interpolation() {
|
||||
let input = "Hello {{= world }}";
|
||||
|
||||
let parsed = crate::parser::parse(input).unwrap();
|
||||
|
||||
let ast = crate::ast::parse(parsed.tokens()).unwrap();
|
||||
|
||||
let emit = emit_machine(ast);
|
||||
|
||||
insta::assert_debug_snapshot!(emit, @r#"
|
||||
[
|
||||
AppendContent {
|
||||
content: "Hello",
|
||||
},
|
||||
AppendContent {
|
||||
content: " ",
|
||||
},
|
||||
LoadFromContextToSlot {
|
||||
name: "world",
|
||||
slot: VariableSlot {
|
||||
index: 0,
|
||||
},
|
||||
},
|
||||
EmitFromSlot {
|
||||
slot: VariableSlot {
|
||||
index: 0,
|
||||
},
|
||||
},
|
||||
]
|
||||
"#);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,70 @@
|
|||
pub struct EvalStack {
|
||||
stack: Vec<Evaluation>,
|
||||
use std::collections::HashMap;
|
||||
|
||||
use displaydoc::Display;
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::Context;
|
||||
use crate::emit::Instruction;
|
||||
|
||||
#[derive(Debug, Error, Display)]
|
||||
enum EvalError {
|
||||
/// An unknown variable was encountered: .0
|
||||
UnknownVariable(String),
|
||||
/// An explicit abort was requested
|
||||
ExplicitAbort,
|
||||
}
|
||||
|
||||
pub enum Evaluation {
|
||||
AppendContent { content: String },
|
||||
fn execute(instructions: &[Instruction], global_context: &Context) -> Result<String, EvalError> {
|
||||
let mut output = String::new();
|
||||
|
||||
let mut scopes: HashMap<crate::emit::VariableSlot, serde_json::Value> = HashMap::new();
|
||||
|
||||
for instr in instructions {
|
||||
match instr {
|
||||
Instruction::AppendContent { content } => output.push_str(content),
|
||||
Instruction::LoadFromContextToSlot { name, slot } => {
|
||||
let value = global_context
|
||||
.values
|
||||
.get(name)
|
||||
.ok_or(EvalError::UnknownVariable(name.clone()))?;
|
||||
|
||||
scopes.insert(*slot, value.clone());
|
||||
}
|
||||
Instruction::EmitFromSlot { slot } => {
|
||||
let value = scopes.get(slot).unwrap().as_str().unwrap();
|
||||
output.push_str(value);
|
||||
}
|
||||
Instruction::PushScope { inherit_parent: _ } => todo!(),
|
||||
Instruction::Abort => return Err(EvalError::ExplicitAbort),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(output)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::Context;
|
||||
use crate::eval::execute;
|
||||
|
||||
#[test]
|
||||
fn check_simple_variable_interpolation() {
|
||||
let input = "Hello {{= world }}";
|
||||
|
||||
let parsed = crate::parser::parse(input).unwrap();
|
||||
|
||||
let ast = crate::ast::parse(parsed.tokens()).unwrap();
|
||||
|
||||
let emit = crate::emit::emit_machine(ast);
|
||||
|
||||
let mut context = Context::new();
|
||||
context.insert("world", "World");
|
||||
let output = execute(&emit, &context);
|
||||
|
||||
insta::assert_debug_snapshot!(output, @r#"
|
||||
Ok(
|
||||
"Hello World",
|
||||
)
|
||||
"#);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ use serde::Serialize;
|
|||
use thiserror::Error;
|
||||
|
||||
pub mod ast;
|
||||
pub mod emit;
|
||||
pub mod eval;
|
||||
pub mod parser;
|
||||
|
||||
|
|
|
|||
|
|
@ -285,6 +285,14 @@ impl<'input> TemplateToken<'input> {
|
|||
source,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn kind(&self) -> TokenKind {
|
||||
self.kind
|
||||
}
|
||||
|
||||
pub fn source(&self) -> &'input str {
|
||||
self.source
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse(input: &str) -> Result<ParsedTemplate<'_>, ParseFailure> {
|
||||
|
|
@ -321,6 +329,7 @@ fn parse_interpolate<'input>(
|
|||
) -> PResult<'input, Vec<TemplateToken<'input>>> {
|
||||
let prev_whitespace = opt(parse_whitespace).parse_next(input)?;
|
||||
let left_delim = "{{".map(TemplateToken::left_delim).parse_next(input)?;
|
||||
let wants_output = opt("=".map(TemplateToken::wants_output)).parse_next(input)?;
|
||||
|
||||
let get_tokens = repeat_till(1.., parse_interpolate_token, peek("}}"));
|
||||
let recover = take_until(0.., "}}").void();
|
||||
|
|
@ -337,6 +346,7 @@ fn parse_interpolate<'input>(
|
|||
let mut tokens = vec![];
|
||||
tokens.extend(prev_whitespace);
|
||||
tokens.push(left_delim);
|
||||
tokens.extend(wants_output);
|
||||
tokens.extend(inside_tokens);
|
||||
tokens.push(right_delim);
|
||||
tokens.extend(post_whitespace);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue