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>>,
|
root: Vec<TemplateAstExpr<'input>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<'input> TemplateAst<'input> {
|
||||||
|
pub fn root(&self) -> &[TemplateAstExpr<'input>] {
|
||||||
|
&self.root
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct AstError {
|
pub struct AstError {
|
||||||
pub(crate) message: Option<String>,
|
pub(crate) message: Option<String>,
|
||||||
|
|
@ -145,6 +151,7 @@ pub enum TemplateAstExpr<'input> {
|
||||||
StaticContent(TemplateToken<'input>),
|
StaticContent(TemplateToken<'input>),
|
||||||
Interpolation {
|
Interpolation {
|
||||||
prev_whitespace: Option<TemplateToken<'input>>,
|
prev_whitespace: Option<TemplateToken<'input>>,
|
||||||
|
wants_output: Option<TemplateToken<'input>>,
|
||||||
expression: Box<TemplateAstExpr<'input>>,
|
expression: Box<TemplateAstExpr<'input>>,
|
||||||
post_whitespace: Option<TemplateToken<'input>>,
|
post_whitespace: Option<TemplateToken<'input>>,
|
||||||
},
|
},
|
||||||
|
|
@ -172,10 +179,11 @@ fn parse_interpolation<'input>(
|
||||||
)
|
)
|
||||||
.with_taken()
|
.with_taken()
|
||||||
.map(|(expr, taken)| expr.unwrap_or(TemplateAstExpr::Invalid(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),
|
opt(TokenKind::Whitespace),
|
||||||
TokenKind::LeftDelim,
|
TokenKind::LeftDelim,
|
||||||
cut_err((
|
cut_err((
|
||||||
|
opt(TokenKind::WantsOutput),
|
||||||
delimited(ignore_ws, expr_parser, ignore_ws).map(Box::new),
|
delimited(ignore_ws, expr_parser, ignore_ws).map(Box::new),
|
||||||
TokenKind::RightDelim,
|
TokenKind::RightDelim,
|
||||||
opt(TokenKind::Whitespace),
|
opt(TokenKind::Whitespace),
|
||||||
|
|
@ -185,6 +193,7 @@ fn parse_interpolation<'input>(
|
||||||
|
|
||||||
Ok(TemplateAstExpr::Interpolation {
|
Ok(TemplateAstExpr::Interpolation {
|
||||||
prev_whitespace,
|
prev_whitespace,
|
||||||
|
wants_output,
|
||||||
expression,
|
expression,
|
||||||
post_whitespace,
|
post_whitespace,
|
||||||
})
|
})
|
||||||
|
|
@ -230,7 +239,7 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn check_simple_variable_interpolation() {
|
fn check_simple_variable_interpolation() {
|
||||||
let input = "Hello {{ world }}";
|
let input = "Hello {{= world }}";
|
||||||
|
|
||||||
let parsed = crate::parser::parse(input).unwrap();
|
let parsed = crate::parser::parse(input).unwrap();
|
||||||
|
|
||||||
|
|
@ -252,6 +261,12 @@ mod tests {
|
||||||
source: " ",
|
source: " ",
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
wants_output: Some(
|
||||||
|
TemplateToken {
|
||||||
|
kind: WantsOutput,
|
||||||
|
source: "=",
|
||||||
|
},
|
||||||
|
),
|
||||||
expression: VariableAccess(
|
expression: VariableAccess(
|
||||||
TemplateToken {
|
TemplateToken {
|
||||||
kind: Ident,
|
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 {
|
use std::collections::HashMap;
|
||||||
stack: Vec<Evaluation>,
|
|
||||||
|
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 {
|
fn execute(instructions: &[Instruction], global_context: &Context) -> Result<String, EvalError> {
|
||||||
AppendContent { content: String },
|
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;
|
use thiserror::Error;
|
||||||
|
|
||||||
pub mod ast;
|
pub mod ast;
|
||||||
|
pub mod emit;
|
||||||
pub mod eval;
|
pub mod eval;
|
||||||
pub mod parser;
|
pub mod parser;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -285,6 +285,14 @@ impl<'input> TemplateToken<'input> {
|
||||||
source,
|
source,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn kind(&self) -> TokenKind {
|
||||||
|
self.kind
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn source(&self) -> &'input str {
|
||||||
|
self.source
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn parse(input: &str) -> Result<ParsedTemplate<'_>, ParseFailure> {
|
pub fn parse(input: &str) -> Result<ParsedTemplate<'_>, ParseFailure> {
|
||||||
|
|
@ -321,6 +329,7 @@ fn parse_interpolate<'input>(
|
||||||
) -> PResult<'input, Vec<TemplateToken<'input>>> {
|
) -> PResult<'input, Vec<TemplateToken<'input>>> {
|
||||||
let prev_whitespace = opt(parse_whitespace).parse_next(input)?;
|
let prev_whitespace = opt(parse_whitespace).parse_next(input)?;
|
||||||
let left_delim = "{{".map(TemplateToken::left_delim).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 get_tokens = repeat_till(1.., parse_interpolate_token, peek("}}"));
|
||||||
let recover = take_until(0.., "}}").void();
|
let recover = take_until(0.., "}}").void();
|
||||||
|
|
@ -337,6 +346,7 @@ fn parse_interpolate<'input>(
|
||||||
let mut tokens = vec![];
|
let mut tokens = vec![];
|
||||||
tokens.extend(prev_whitespace);
|
tokens.extend(prev_whitespace);
|
||||||
tokens.push(left_delim);
|
tokens.push(left_delim);
|
||||||
|
tokens.extend(wants_output);
|
||||||
tokens.extend(inside_tokens);
|
tokens.extend(inside_tokens);
|
||||||
tokens.push(right_delim);
|
tokens.push(right_delim);
|
||||||
tokens.extend(post_whitespace);
|
tokens.extend(post_whitespace);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue