Add initial parsing

Signed-off-by: Marcel Müller <neikos@neikos.email>
This commit is contained in:
Marcel Müller 2026-03-05 08:22:10 +01:00
parent 31e102a4ee
commit f4e8137e17
6 changed files with 1098 additions and 6 deletions

View file

@ -1,14 +1,99 @@
pub fn add(left: u64, right: u64) -> u64 {
left + right
use std::collections::BTreeMap;
use std::collections::HashMap;
use displaydoc::Display;
use serde::Serialize;
use thiserror::Error;
pub mod parser;
#[derive(Debug, Error, Display)]
pub enum TempleError {
/// Could not parse the given template
ParseError {},
}
pub struct Temple {
templates: HashMap<String, Template>,
}
impl Default for Temple {
fn default() -> Self {
Self::new()
}
}
impl Temple {
pub fn new() -> Temple {
Temple {
templates: HashMap::new(),
}
}
pub fn add_template(
&mut self,
name: impl Into<String>,
value: impl AsRef<str>,
) -> Result<(), TempleError> {
Ok(())
}
fn render(&self, arg: &str, ctx: &Context) -> Result<String, TempleError> {
Ok(String::new())
}
}
struct Template {}
pub struct Context {
values: BTreeMap<String, serde_json::Value>,
}
impl Default for Context {
fn default() -> Self {
Context::new()
}
}
impl Context {
pub fn new() -> Context {
Context {
values: BTreeMap::new(),
}
}
pub fn try_insert(
&mut self,
key: impl Into<String>,
value: impl Serialize,
) -> Result<(), serde_json::Error> {
self.values.insert(key.into(), serde_json::to_value(value)?);
Ok(())
}
pub fn insert(&mut self, key: impl Into<String>, value: impl Serialize) {
self.try_insert(key, value)
.expect("inserted value should serialize without error");
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Context;
use crate::Temple;
#[test]
fn it_works() {
let result = add(2, 2);
assert_eq!(result, 4);
fn check_simple_template() {
let mut temp = Temple::new();
temp.add_template("base", "Hello {{= name }}").unwrap();
let mut ctx = Context::new();
ctx.insert("name", "World");
let rendered = temp.render("base", &ctx).unwrap();
insta::assert_snapshot!(rendered, @"")
}
}

367
src/parser/mod.rs Normal file
View file

@ -0,0 +1,367 @@
use std::ops::Range;
use std::sync::Arc;
use annotate_snippets::AnnotationKind;
use annotate_snippets::Level;
use annotate_snippets::Renderer;
use annotate_snippets::Snippet;
use winnow::LocatingSlice;
use winnow::Parser;
use winnow::RecoverableParser;
use winnow::ascii::alpha1;
use winnow::ascii::multispace0;
use winnow::ascii::multispace1;
use winnow::combinator::alt;
use winnow::combinator::cut_err;
use winnow::combinator::eof;
use winnow::combinator::opt;
use winnow::combinator::peek;
use winnow::combinator::repeat_till;
use winnow::combinator::terminated;
use winnow::combinator::trace;
use winnow::error::AddContext;
use winnow::error::FromRecoverableError;
use winnow::error::ModalError;
use winnow::error::ParserError;
use winnow::stream::Location;
use winnow::stream::Recoverable;
use winnow::stream::Stream;
use winnow::token::any;
use winnow::token::rest;
use winnow::token::take_until;
type Input<'input> = Recoverable<LocatingSlice<&'input str>, ParseError>;
type PResult<'input, T> = Result<T, ParseError>;
#[derive(Debug, Clone)]
pub struct SourceSpan {
pub range: Range<usize>,
}
#[derive(Debug)]
pub struct ParseFailure {
source: Arc<str>,
errors: Vec<ParseError>,
}
impl ParseFailure {
fn from_errors(errors: Vec<ParseError>, input: &str) -> ParseFailure {
ParseFailure {
source: Arc::from(input.to_string()),
errors,
}
}
pub fn to_report(&self) -> String {
let mut report = String::new();
for error in &self.errors {
let rep = &[Level::ERROR
.primary_title(
error
.message
.as_deref()
.unwrap_or("An error occurred while parsing"),
)
.element(
Snippet::source(self.source.as_ref()).annotation(
AnnotationKind::Primary
.span(error.span.clone().map(|s| s.range).unwrap_or_else(|| 0..0)),
),
)];
let renderer =
Renderer::styled().decor_style(annotate_snippets::renderer::DecorStyle::Unicode);
report.push_str(&renderer.render(rep));
}
report
}
}
#[derive(Debug, Clone)]
pub struct ParseError {
pub(crate) message: Option<String>,
pub(crate) span: Option<SourceSpan>,
is_fatal: bool,
}
impl ParseError {
fn ctx() -> Self {
ParseError {
message: None,
span: None,
is_fatal: false,
}
}
fn msg(mut self, message: &str) -> Self {
self.message = Some(message.to_string());
self
}
}
impl ModalError for ParseError {
fn cut(mut self) -> Self {
self.is_fatal = true;
self
}
fn backtrack(mut self) -> Self {
self.is_fatal = false;
self
}
}
impl<'input> FromRecoverableError<Input<'input>, ParseError> for ParseError {
fn from_recoverable_error(
token_start: &<Input<'input> as winnow::stream::Stream>::Checkpoint,
_err_start: &<Input<'input> as winnow::stream::Stream>::Checkpoint,
input: &Input<'input>,
mut e: ParseError,
) -> Self {
e.span = e
.span
.or_else(|| Some(span_from_checkpoint(input, token_start)));
e
}
}
impl<'input> AddContext<Input<'input>, ParseError> for ParseError {
fn add_context(
mut self,
_input: &Input<'input>,
_token_start: &<Input<'input> as Stream>::Checkpoint,
context: ParseError,
) -> Self {
self.message = context.message.or(self.message);
self
}
}
fn span_from_checkpoint<I: Stream + Location>(
input: &I,
token_start: &<I as Stream>::Checkpoint,
) -> SourceSpan {
let offset = input.offset_from(token_start);
SourceSpan {
range: (input.current_token_start() - offset)..input.current_token_start(),
}
}
impl<'input> ParserError<Input<'input>> for ParseError {
type Inner = ParseError;
fn from_input(_input: &Input<'input>) -> Self {
ParseError {
message: None,
span: None,
is_fatal: false,
}
}
fn into_inner(self) -> winnow::Result<Self::Inner, Self> {
Ok(self)
}
fn is_backtrack(&self) -> bool {
!self.is_fatal
}
}
#[derive(Debug)]
pub struct ParsedTemplate<'input> {
content: Vec<TemplateChunk<'input>>,
}
#[derive(Debug)]
pub enum TemplateChunk<'input> {
Content(&'input str),
Expression(InterpolateExpression<'input>),
}
#[derive(Debug)]
pub struct InterpolateExpression<'input> {
pub left_delim: &'input str,
pub wants_output: Option<&'input str>,
pub value: Box<TemplateExpression<'input>>,
pub right_delim: &'input str,
}
#[derive(Debug)]
pub struct TemplateExpression<'input> {
pub before_ws: &'input str,
pub expr: TemplateExpr<'input>,
pub after_ws: &'input str,
}
#[derive(Debug)]
pub enum TemplateExpr<'input> {
Variable(&'input str),
}
pub fn parse(input: &str) -> Result<ParsedTemplate<'_>, ParseFailure> {
let (_remaining, val, errors) = parse_chunks.recoverable_parse(LocatingSlice::new(input));
dbg!(&val);
if errors.is_empty()
&& let Some(val) = val
{
Ok(ParsedTemplate { content: val })
} else {
Err(ParseFailure::from_errors(errors, input))
}
}
fn parse_chunks<'input>(input: &mut Input<'input>) -> PResult<'input, Vec<TemplateChunk<'input>>> {
repeat_till(0.., alt((parse_interpolate, parse_content)), eof)
.map(|(v, _)| v)
.parse_next(input)
}
fn parse_content<'input>(input: &mut Input<'input>) -> PResult<'input, TemplateChunk<'input>> {
alt((take_until(1.., "{{"), rest))
.map(TemplateChunk::Content)
.parse_next(input)
}
fn parse_interpolate<'input>(input: &mut Input<'input>) -> PResult<'input, TemplateChunk<'input>> {
let left_delim = "{{".parse_next(input)?;
let (wants_output, value, right_delim) =
cut_err((opt("="), parse_value.map(Box::new), "}}")).parse_next(input)?;
Ok(TemplateChunk::Expression(InterpolateExpression {
left_delim,
wants_output,
value,
right_delim,
}))
}
fn parse_value<'input>(input: &mut Input<'input>) -> PResult<'input, TemplateExpression<'input>> {
let before_ws = multispace0(input)?;
let expr = trace("parse_value", alt((parse_variable,))).parse_next(input)?;
let after_ws = multispace0(input)?;
Ok(TemplateExpression {
before_ws,
expr,
after_ws,
})
}
fn parse_variable<'input>(input: &mut Input<'input>) -> PResult<'input, TemplateExpr<'input>> {
terminated(alpha1, ident_terminator_check)
.map(TemplateExpr::Variable)
.context(ParseError::ctx().msg("valid variables are alpha"))
.resume_after(bad_ident)
.map(|v| v.unwrap_or(TemplateExpr::Variable("BAD_VARIABLE")))
.parse_next(input)
}
fn ident_terminator<'input>(input: &mut Input<'input>) -> PResult<'input, ()> {
alt((eof.void(), "{".void(), "}".void(), multispace1.void())).parse_next(input)
}
fn ident_terminator_check<'input>(input: &mut Input<'input>) -> PResult<'input, ()> {
cut_err(peek(ident_terminator)).parse_next(input)
}
fn bad_ident<'input>(input: &mut Input<'input>) -> PResult<'input, ()> {
repeat_till(1.., any, peek(ident_terminator))
.map(|((), _)| ())
.parse_next(input)
}
#[cfg(test)]
mod tests {
use crate::parser::parse;
#[test]
fn parse_simple() {
let input = "Hello There";
let output = parse(input);
insta::assert_debug_snapshot!(output, @r#"
Ok(
ParsedTemplate {
content: [
Content(
"Hello There",
),
],
},
)
"#);
}
#[test]
fn parse_interpolate() {
let input = "Hello {{ there }}";
let output = parse(input);
insta::assert_debug_snapshot!(output, @r#"
Ok(
ParsedTemplate {
content: [
Content(
"Hello ",
),
Expression(
InterpolateExpression {
left_delim: "{{",
wants_output: None,
value: TemplateExpression {
before_ws: " ",
expr: Variable(
"there",
),
after_ws: " ",
},
right_delim: "}}",
},
),
],
},
)
"#);
}
#[test]
fn parse_interpolate_bad() {
let input = "Hello {{ the2re }}";
let output = parse(input);
insta::assert_debug_snapshot!(output, @r#"
Err(
ParseFailure {
source: "Hello {{ the2re }}",
errors: [
ParseError {
message: Some(
"valid variables are alpha",
),
span: Some(
SourceSpan {
range: 9..15,
},
),
is_fatal: true,
},
],
},
)
"#);
let error = output.unwrap_err();
insta::assert_snapshot!(error.to_report());
}
}

View file

@ -0,0 +1,8 @@
---
source: src/parser/mod.rs
expression: error.to_report()
---
error: valid variables are alpha
 ╭▸ 
1 │ Hello {{ the2re }}
╰╴ ━━━━━━