use crate::{
parse::{self, ParseInput},
re::{Sliceable, Writable},
se::TaggedCaptures,
};
use std::{
fmt,
io::{self, Write},
sync::Arc,
};
pub type Error = parse::Error<ErrorKind>;
#[non_exhaustive]
#[derive(Debug)]
pub enum ErrorKind {
MissingDelimiter(char),
UnexpectedEof,
UnknownEscape(char),
}
impl fmt::Display for ErrorKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::MissingDelimiter(ch) => write!(f, "missing delimiter '{ch}'"),
Self::UnknownEscape(ch) => write!(f, "unknown escape sequence '\\{ch}'"),
Self::UnexpectedEof => write!(f, "unexpected EOF"),
}
}
}
#[non_exhaustive]
#[derive(Debug)]
pub enum RenderError {
Io(io::Error),
UnknownVariable(String),
}
impl fmt::Display for RenderError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Io(e) => write!(f, "{e}"),
Self::UnknownVariable(s) => write!(f, "{s:?} is not a known variable"),
}
}
}
impl std::error::Error for RenderError {}
impl From<RenderError> for io::Error {
fn from(err: RenderError) -> Self {
match err {
RenderError::Io(e) => e,
RenderError::UnknownVariable(_) => io::Error::other(err),
}
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
enum Fragment {
Cap(usize),
Esc(char),
Lit(usize, usize),
Var(usize, usize),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Template {
raw: Arc<str>,
fragments: Vec<Fragment>,
}
impl Template {
pub fn parse(template: &str) -> Result<Self, Error> {
let input = ParseInput::new(template);
let mut fragments = Vec::new();
let mut offset = input.offset();
let error = |kind: ErrorKind| Error::new(kind, input.text(), input.span_char());
let push_lit = |offset, input: &ParseInput<'_>, fragments: &mut Vec<Fragment>| {
if offset < input.offset() {
fragments.push(Fragment::Lit(offset, input.offset()));
}
};
while !input.at_eof() {
match input.char() {
'\\' => {
push_lit(offset, &input, &mut fragments);
input.advance();
match input.try_char() {
Some('n') => fragments.push(Fragment::Esc('\n')),
Some('t') => fragments.push(Fragment::Esc('\t')),
Some('{') => fragments.push(Fragment::Esc('{')),
Some(ch) => return Err(error(ErrorKind::UnknownEscape(ch))),
None => return Err(error(ErrorKind::UnexpectedEof)),
}
input.advance();
offset = input.offset();
}
'{' => {
push_lit(offset, &input, &mut fragments);
input.advance();
offset = input.offset();
match input.read_until('}') {
Some(s) => {
let frag = match s.parse::<usize>() {
Ok(i) => Fragment::Cap(i),
Err(_) => Fragment::Var(offset, input.offset()),
};
fragments.push(frag);
}
None => return Err(error(ErrorKind::UnexpectedEof)),
}
input.advance();
offset = input.offset();
}
_ => {
input.advance();
}
}
}
push_lit(offset, &input, &mut fragments);
Ok(Template {
raw: Arc::from(template),
fragments,
})
}
pub fn variable_references(&self) -> impl Iterator<Item = &str> {
self.fragments.iter().flat_map(|frag| match frag {
Fragment::Var(from, to) => Some(&self.raw[*from..*to]),
_ => None,
})
}
pub fn render<H>(&self, caps: &TaggedCaptures<H>) -> Result<String, RenderError>
where
H: Sliceable + ?Sized,
{
let mut buf = Vec::with_capacity(self.raw.len() * 2);
self.render_to(&mut buf, caps)?;
Ok(String::from_utf8(buf).unwrap())
}
pub fn render_to<H, W>(&self, w: &mut W, caps: &TaggedCaptures<H>) -> Result<usize, RenderError>
where
H: Sliceable + ?Sized,
W: Write,
{
let mut n = 0;
for frag in self.fragments.iter() {
n += match frag {
Fragment::Lit(from, to) => {
w.write_all(&self.raw.as_bytes()[*from..*to])
.map_err(RenderError::Io)?;
to - from
}
Fragment::Esc(ch) => w
.write(ch.encode_utf8(&mut [0; 1]).as_bytes())
.map_err(RenderError::Io)?,
Fragment::Cap(n) => match caps.submatch_text(*n) {
Some(slice) => slice.write_to(w).map_err(RenderError::Io)?,
None => 0, },
Fragment::Var(from, to) => {
return Err(RenderError::UnknownVariable(
self.raw[*from..*to].to_string(),
));
}
};
}
Ok(n)
}
pub fn render_with_context<H, C>(
&self,
caps: &TaggedCaptures<H>,
ctx: &C,
) -> Result<String, RenderError>
where
H: Sliceable + ?Sized,
C: Context,
{
let mut buf = Vec::with_capacity(self.raw.len() * 2);
self.render_with_context_to(&mut buf, caps, ctx)?;
Ok(String::from_utf8(buf).unwrap())
}
pub fn render_with_context_to<H, W, C>(
&self,
w: &mut W,
caps: &TaggedCaptures<H>,
ctx: &C,
) -> Result<usize, RenderError>
where
H: Sliceable + ?Sized,
W: Write,
C: Context,
{
let mut n = 0;
for frag in self.fragments.iter() {
n += match frag {
Fragment::Lit(from, to) => {
w.write_all(&self.raw.as_bytes()[*from..*to])
.map_err(RenderError::Io)?;
to - from
}
Fragment::Esc(ch) => w
.write(ch.encode_utf8(&mut [0; 1]).as_bytes())
.map_err(RenderError::Io)?,
Fragment::Cap(n) => match caps.submatch_text(*n) {
Some(slice) => slice.write_to(w).map_err(RenderError::Io)?,
None => 0, },
Fragment::Var(from, to) => match ctx.render_var(&self.raw[*from..*to], w) {
Some(res) => res.map_err(RenderError::Io)?,
None => {
return Err(RenderError::UnknownVariable(
self.raw[*from..*to].to_string(),
));
}
},
};
}
Ok(n)
}
}
pub trait Context {
fn render_var<W>(&self, var: &str, w: &mut W) -> Option<io::Result<usize>>
where
W: Write;
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Captures;
use simple_test_case::test_case;
#[derive(Debug, PartialEq, Eq)]
enum Tag<'a> {
Cap(usize),
Lit(&'a str),
Esc(char),
Var(&'a str),
}
use Tag::*;
fn tags(t: &Template) -> Vec<Tag<'_>> {
t.fragments
.iter()
.map(|f| match f {
Fragment::Lit(from, to) => Tag::Lit(&t.raw[*from..*to]),
Fragment::Cap(n) => Tag::Cap(*n),
Fragment::Esc(ch) => Tag::Esc(*ch),
Fragment::Var(from, to) => Tag::Var(&t.raw[*from..*to]),
})
.collect()
}
#[test_case(
"just a raw string",
&[Lit("just a raw string")];
"raw string"
)]
#[test_case(
"foo\\nbar\\tbaz\\{",
&[Lit("foo"), Esc('\n'), Lit("bar"), Esc('\t'), Lit("baz"), Esc('{')];
"escape sequences"
)]
#[test_case(
"foo {1} {2} {bar}",
&[Lit("foo "), Cap(1), Lit(" "), Cap(2), Lit(" "), Var("bar")];
"variable references"
)]
#[test]
fn parse_works(s: &str, expected: &[Tag<'_>]) {
let t = Template::parse(s).unwrap();
let tagged_strs = tags(&t);
assert_eq!(tagged_strs, expected);
}
#[test_case("just a raw string", "just a raw string"; "raw string")]
#[test_case(">{0}<", ">foo bar<"; "full match")]
#[test_case("{1} {2}", "foo bar"; "both submatches")]
#[test_case("{2} {1}", "bar foo"; "flipped submatches")]
#[test_case("{1}\\n{2}", "foo\nbar"; "submatches and newline")]
#[test_case("{3}", ""; "unknown capture")]
#[test]
fn render_works(s: &str, expected: &str) {
let caps: TaggedCaptures<str> = TaggedCaptures {
captures: Captures::new("foo bar", vec![Some((0, 7)), Some((0, 3)), Some((4, 7))]),
action: None,
};
let t = Template::parse(s).unwrap();
let rendered = t.render(&caps).unwrap();
assert_eq!(rendered, expected);
}
#[test]
fn render_returns_error_for_variables() {
let caps: TaggedCaptures<str> = TaggedCaptures {
captures: Captures::new("foo bar", vec![Some((0, 7))]),
action: None,
};
let t = Template::parse("{unknown}").unwrap();
let err = t.render(&caps).unwrap_err();
assert!(matches!(err, RenderError::UnknownVariable(s) if s == "unknown"));
}
#[test_case("just a raw string", "just a raw string"; "raw string")]
#[test_case(">{0}<", ">foo bar<"; "full match")]
#[test_case("{1} {2}", "foo bar"; "both submatches")]
#[test_case("{2} {1}", "bar foo"; "flipped submatches")]
#[test_case("{1}\\n{2}", "foo\nbar"; "submatches and newline")]
#[test_case("{3}", ""; "unknown capture")]
#[test_case("{unknown}", "from context"; "variable without context")]
#[test]
fn render_with_context_works(s: &str, expected: &str) {
let caps: TaggedCaptures<str> = TaggedCaptures {
captures: Captures::new("foo bar", vec![Some((0, 7)), Some((0, 3)), Some((4, 7))]),
action: None,
};
struct Ctx;
impl Context for Ctx {
fn render_var<W>(&self, _: &str, w: &mut W) -> Option<io::Result<usize>>
where
W: Write,
{
Some(w.write(b"from context"))
}
}
let t = Template::parse(s).unwrap();
let rendered = t.render_with_context(&caps, &Ctx).unwrap();
assert_eq!(rendered, expected);
}
#[test]
fn render_with_context_returns_error_for_unknown_variables() {
let caps: TaggedCaptures<str> = TaggedCaptures {
captures: Captures::new("foo bar", vec![Some((0, 7))]),
action: None,
};
struct Ctx;
impl Context for Ctx {
fn render_var<W>(&self, _: &str, _: &mut W) -> Option<io::Result<usize>>
where
W: Write,
{
None
}
}
let t = Template::parse("{unknown}").unwrap();
let err = t.render_with_context(&caps, &Ctx).unwrap_err();
assert!(matches!(err, RenderError::UnknownVariable(s) if s == "unknown"));
}
}