From 71f49a8fc267fb40562b21686e64a4f9e3cae00b Mon Sep 17 00:00:00 2001 From: Guillaume Gomez Date: Sun, 18 Aug 2024 15:21:59 +0200 Subject: [PATCH] Add support for prefixes for string literals --- rinja_derive/src/generator.rs | 36 ++++++++++++++---- rinja_parser/src/expr.rs | 4 +- rinja_parser/src/lib.rs | 69 ++++++++++++++++++++++++++++++++++- rinja_parser/src/node.rs | 10 ++--- rinja_parser/src/target.rs | 4 +- rinja_parser/src/tests.rs | 14 +++++-- testing/tests/literal.rs | 32 ++++++++++++++++ 7 files changed, 146 insertions(+), 23 deletions(-) diff --git a/rinja_derive/src/generator.rs b/rinja_derive/src/generator.rs index 9c6a4caf..000a134a 100644 --- a/rinja_derive/src/generator.rs +++ b/rinja_derive/src/generator.rs @@ -9,7 +9,7 @@ use std::{cmp, hash, mem, str}; use parser::node::{ Call, Comment, Cond, CondTest, FilterBlock, If, Include, Let, Lit, Loop, Match, Whitespace, Ws, }; -use parser::{CharLit, CharPrefix, Expr, Filter, Node, Target, WithSpan}; +use parser::{CharLit, CharPrefix, Expr, Filter, Node, StrLit, StrPrefix, Target, WithSpan}; use quote::quote; use rustc_hash::FxBuildHasher; @@ -1271,7 +1271,9 @@ impl<'a> Generator<'a> { // for now, we only escape strings and chars at compile time let (lit, escape_prefix) = match &**s { - Expr::StrLit(input) => (InputKind::StrLit(input), None), + Expr::StrLit(StrLit { prefix, content }) => { + (InputKind::StrLit(content), prefix.map(|p| p.to_char())) + } Expr::CharLit(CharLit { prefix, content }) => ( InputKind::CharLit(content), if *prefix == Some(CharPrefix::Binary) { @@ -1488,7 +1490,7 @@ impl<'a> Generator<'a> { Ok(match **expr { Expr::BoolLit(s) => self.visit_bool_lit(buf, s), Expr::NumLit(s) => self.visit_num_lit(buf, s), - Expr::StrLit(s) => self.visit_str_lit(buf, s), + Expr::StrLit(ref s) => self.visit_str_lit(buf, s), Expr::CharLit(ref s) => self.visit_char_lit(buf, s), Expr::Var(s) => self.visit_var(buf, s), Expr::Path(ref path) => self.visit_path(buf, path), @@ -1709,7 +1711,22 @@ impl<'a> Generator<'a> { return Err(ctx.generate_error("only two arguments allowed to escape filter", node)); } let opt_escaper = match args.get(1).map(|expr| &**expr) { - Some(Expr::StrLit(name)) => Some(*name), + Some(Expr::StrLit(StrLit { prefix, content })) => { + if let Some(prefix) = prefix { + let kind = if *prefix == StrPrefix::Binary { + "slice" + } else { + "CStr" + }; + return Err(ctx.generate_error( + &format!( + "invalid escaper `b{content:?}`. Expected a string, found a {kind}" + ), + &args[1], + )); + } + Some(content) + } Some(_) => { return Err(ctx.generate_error("invalid escaper type for escape filter", node)); } @@ -1751,7 +1768,7 @@ impl<'a> Generator<'a> { node: &WithSpan<'_, T>, ) -> Result { if !args.is_empty() { - if let Expr::StrLit(fmt) = *args[0] { + if let Expr::StrLit(ref fmt) = *args[0] { buf.write("::std::format!("); self.visit_str_lit(buf, fmt); if args.len() > 1 { @@ -1773,7 +1790,7 @@ impl<'a> Generator<'a> { node: &WithSpan<'_, T>, ) -> Result { if let [_, arg2] = args { - if let Expr::StrLit(fmt) = **arg2 { + if let Expr::StrLit(ref fmt) = **arg2 { buf.write("::std::format!("); self.visit_str_lit(buf, fmt); buf.write(','); @@ -2087,8 +2104,11 @@ impl<'a> Generator<'a> { DisplayWrap::Unwrapped } - fn visit_str_lit(&mut self, buf: &mut Buffer, s: &str) -> DisplayWrap { - buf.write(format_args!("\"{s}\"")); + fn visit_str_lit(&mut self, buf: &mut Buffer, s: &StrLit<'_>) -> DisplayWrap { + if let Some(prefix) = s.prefix { + buf.write(prefix.to_char()); + } + buf.write(format_args!("\"{}\"", s.content)); DisplayWrap::Unwrapped } diff --git a/rinja_parser/src/expr.rs b/rinja_parser/src/expr.rs index 5407c87a..da917d7f 100644 --- a/rinja_parser/src/expr.rs +++ b/rinja_parser/src/expr.rs @@ -12,7 +12,7 @@ use nom::sequence::{pair, preceded, terminated, tuple}; use crate::{ char_lit, filter, identifier, keyword, not_ws, num_lit, path_or_identifier, str_lit, ws, - CharLit, ErrorContext, Level, ParseResult, PathOrIdentifier, WithSpan, + CharLit, ErrorContext, Level, ParseResult, PathOrIdentifier, StrLit, WithSpan, }; macro_rules! expr_prec_layer { @@ -36,7 +36,7 @@ macro_rules! expr_prec_layer { pub enum Expr<'a> { BoolLit(bool), NumLit(&'a str), - StrLit(&'a str), + StrLit(StrLit<'a>), CharLit(CharLit<'a>), Var(&'a str), Path(Vec<&'a str>), diff --git a/rinja_parser/src/lib.rs b/rinja_parser/src/lib.rs index d9983eab..b55505c6 100644 --- a/rinja_parser/src/lib.rs +++ b/rinja_parser/src/lib.rs @@ -416,7 +416,36 @@ fn separated_digits(radix: u32, start: bool) -> impl Fn(&str) -> ParseResult<'_> } } -fn str_lit(i: &str) -> ParseResult<'_> { +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum StrPrefix { + Binary, + CLike, +} + +impl StrPrefix { + pub fn to_char(self) -> char { + match self { + Self::Binary => 'b', + Self::CLike => 'c', + } + } +} + +impl fmt::Display for StrPrefix { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + use std::fmt::Write; + + f.write_char(self.to_char()) + } +} + +#[derive(Clone, Debug, PartialEq)] +pub struct StrLit<'a> { + pub prefix: Option, + pub content: &'a str, +} + +fn str_lit_without_prefix(i: &str) -> ParseResult<'_> { let (i, s) = delimited( char('"'), opt(escaped(is_not("\\\""), '\\', anychar)), @@ -425,6 +454,17 @@ fn str_lit(i: &str) -> ParseResult<'_> { Ok((i, s.unwrap_or_default())) } +fn str_lit(i: &str) -> Result<(&str, StrLit<'_>), ParseErr<'_>> { + let (i, (prefix, content)) = + tuple((opt(alt((char('b'), char('c')))), str_lit_without_prefix))(i)?; + let prefix = match prefix { + Some('b') => Some(StrPrefix::Binary), + Some('c') => Some(StrPrefix::CLike), + _ => None, + }; + Ok((i, StrLit { prefix, content })) +} + #[derive(Clone, Copy, Debug, PartialEq)] pub enum CharPrefix { Binary, @@ -803,7 +843,7 @@ const PRIMITIVE_TYPES: &[&str] = &{ mod test { use std::path::Path; - use super::{char_lit, num_lit, strip_common}; + use super::{char_lit, num_lit, str_lit, strip_common, StrLit, StrPrefix}; #[test] fn test_strip_common() { @@ -896,4 +936,29 @@ mod test { assert!(char_lit("'\\u{}'").is_err()); assert!(char_lit("'\\u{110000}'").is_err()); } + + #[test] + fn test_str_lit() { + assert_eq!( + str_lit(r#"b"hello""#).unwrap(), + ( + "", + StrLit { + prefix: Some(StrPrefix::Binary), + content: "hello" + } + ) + ); + assert_eq!( + str_lit(r#"c"hello""#).unwrap(), + ( + "", + StrLit { + prefix: Some(StrPrefix::CLike), + content: "hello" + } + ) + ); + assert!(str_lit(r#"d"hello""#).is_err()); + } } diff --git a/rinja_parser/src/node.rs b/rinja_parser/src/node.rs index ff1dac43..1e8efa9e 100644 --- a/rinja_parser/src/node.rs +++ b/rinja_parser/src/node.rs @@ -9,8 +9,8 @@ use nom::sequence::{delimited, pair, preceded, tuple}; use crate::memchr_splitter::{Splitter1, Splitter2, Splitter3}; use crate::{ - filter, identifier, is_ws, keyword, not_ws, skip_till, str_lit, ws, ErrorContext, Expr, Filter, - ParseResult, State, Target, WithSpan, + filter, identifier, is_ws, keyword, not_ws, skip_till, str_lit_without_prefix, ws, + ErrorContext, Expr, Filter, ParseResult, State, Target, WithSpan, }; #[derive(Debug, PartialEq)] @@ -562,7 +562,7 @@ impl<'a> Import<'a> { opt(Whitespace::parse), ws(keyword("import")), cut(tuple(( - ws(str_lit), + ws(str_lit_without_prefix), ws(keyword("as")), cut(pair(ws(identifier), opt(Whitespace::parse))), ))), @@ -938,7 +938,7 @@ impl<'a> Include<'a> { let mut p = tuple(( opt(Whitespace::parse), ws(keyword("include")), - cut(pair(ws(str_lit), opt(Whitespace::parse))), + cut(pair(ws(str_lit_without_prefix), opt(Whitespace::parse))), )); let (i, (pws, _, (path, nws))) = p(i)?; Ok(( @@ -966,7 +966,7 @@ impl<'a> Extends<'a> { let (i, (pws, _, (path, nws))) = tuple(( opt(Whitespace::parse), ws(keyword("extends")), - cut(pair(ws(str_lit), opt(Whitespace::parse))), + cut(pair(ws(str_lit_without_prefix), opt(Whitespace::parse))), ))(i)?; match (pws, nws) { (None, None) => Ok((i, WithSpan::new(Self { path }, start))), diff --git a/rinja_parser/src/target.rs b/rinja_parser/src/target.rs index 37834235..0a02e00d 100644 --- a/rinja_parser/src/target.rs +++ b/rinja_parser/src/target.rs @@ -7,7 +7,7 @@ use nom::sequence::{pair, preceded, tuple}; use crate::{ bool_lit, char_lit, identifier, keyword, num_lit, path_or_identifier, str_lit, ws, CharLit, - ErrorContext, ParseErr, ParseResult, PathOrIdentifier, State, WithSpan, + ErrorContext, ParseErr, ParseResult, PathOrIdentifier, State, StrLit, WithSpan, }; #[derive(Clone, Debug, PartialEq)] @@ -17,7 +17,7 @@ pub enum Target<'a> { Array(Vec<&'a str>, Vec>), Struct(Vec<&'a str>, Vec<(&'a str, Target<'a>)>), NumLit(&'a str), - StrLit(&'a str), + StrLit(StrLit<'a>), CharLit(CharLit<'a>), BoolLit(&'a str), Path(Vec<&'a str>), diff --git a/rinja_parser/src/tests.rs b/rinja_parser/src/tests.rs index 737d8bcb..c30ade71 100644 --- a/rinja_parser/src/tests.rs +++ b/rinja_parser/src/tests.rs @@ -1,5 +1,5 @@ -use super::node::{Lit, Whitespace, Ws}; -use super::{Ast, Expr, Filter, Node, Syntax, WithSpan}; +use crate::node::{Lit, Whitespace, Ws}; +use crate::{Ast, Expr, Filter, Node, StrLit, Syntax, WithSpan}; impl WithSpan<'static, T> { fn no_span(inner: T) -> Self { @@ -218,7 +218,10 @@ fn test_parse_var_call() { WithSpan::no_span(Expr::Call( Box::new(WithSpan::no_span(Expr::Var("function"))), vec![ - WithSpan::no_span(Expr::StrLit("123")), + WithSpan::no_span(Expr::StrLit(StrLit { + content: "123", + prefix: None, + })), WithSpan::no_span(Expr::NumLit("3")) ] )), @@ -259,7 +262,10 @@ fn test_parse_path_call() { WithSpan::no_span(Expr::Call( Box::new(WithSpan::no_span(Expr::Path(vec!["self", "function"]))), vec![ - WithSpan::no_span(Expr::StrLit("123")), + WithSpan::no_span(Expr::StrLit(StrLit { + content: "123", + prefix: None, + })), WithSpan::no_span(Expr::NumLit("3")) ], ),) diff --git a/testing/tests/literal.rs b/testing/tests/literal.rs index eac02721..eb52816a 100644 --- a/testing/tests/literal.rs +++ b/testing/tests/literal.rs @@ -27,3 +27,35 @@ fn test_prefix_char_literal_in_target() { let t = Target { data: b"hi" }; assert_eq!(t.render().unwrap(), "bc hoy"); } + +#[derive(Template)] +#[template( + source = r#"{% if x == b"hi".as_slice() %}bc{% endif %} +{%- if c"a".to_bytes_with_nul() == b"a\0" %} hoy{% endif %}"#, + ext = "txt" +)] +struct ExprStr { + x: &'static [u8], +} + +#[test] +fn test_prefix_str_literal_in_expr() { + let t = ExprStr { x: b"hi" }; + assert_eq!(t.render().unwrap(), "bc hoy"); +} + +#[derive(Template)] +#[template( + source = r#"{% if let Some(b"hi") = Some(data) %}bc{% endif %} +{%- if let x = c"hi" %} hoy{% endif %}"#, + ext = "txt" +)] +struct TargetStr { + data: [u8; 2], +} + +#[test] +fn test_prefix_str_literal_in_target() { + let t = TargetStr { data: *b"hi" }; + assert_eq!(t.render().unwrap(), "bc hoy"); +}