use alloc::string::String;
use alloc::vec::Vec;
use core::fmt;
use crate::arg::FormatArg;
use crate::engine::render_arg;
use crate::error::FormatError;
use crate::param::{FormatParam, collect_args};
use crate::spec::{RawSpec, parse_placeholder};
#[derive(Clone, Debug, PartialEq)]
enum Segment<'t> {
Literal(&'t str),
Escaped(char),
Placeholder { index: usize, spec: RawSpec },
}
#[derive(Clone, Debug, PartialEq)]
pub struct Template<'t> {
source: &'t str,
segments: Vec<Segment<'t>>,
min_args: usize,
}
impl<'t> Template<'t> {
pub fn parse(template: &'t str) -> Result<Self, FormatError> {
let mut segments: Vec<Segment<'t>> = Vec::new();
let mut min_args = 0usize;
let mut implicit = 0usize;
let mut literal_start = 0usize;
let mut i = 0usize;
let bytes = template.as_bytes();
macro_rules! flush_literal {
($end:expr) => {
if literal_start < $end {
segments.push(Segment::Literal(&template[literal_start..$end]));
}
};
}
while i < bytes.len() {
match bytes[i] {
b'{' if bytes.get(i + 1) == Some(&b'{') => {
flush_literal!(i);
segments.push(Segment::Escaped('{'));
i += 2;
literal_start = i;
}
b'{' => {
flush_literal!(i);
let inner_start = i + 1;
let close = template[inner_start..]
.find('}')
.map(|p| inner_start + p)
.ok_or(FormatError::InvalidFormatString)?;
let (index, spec) = parse_placeholder(&template[inner_start..close])?;
let index = match index {
Some(n) => n,
None => {
let n = implicit;
implicit += 1;
n
}
};
min_args = min_args.max(index + 1);
if let Some(p) = spec.max_param_ref() {
min_args = min_args.max(p + 1);
}
segments.push(Segment::Placeholder { index, spec });
i = close + 1;
literal_start = i;
}
b'}' if bytes.get(i + 1) == Some(&b'}') => {
flush_literal!(i);
segments.push(Segment::Escaped('}'));
i += 2;
literal_start = i;
}
b'}' => return Err(FormatError::InvalidFormatString),
_ => i += 1,
}
}
flush_literal!(bytes.len());
Ok(Self {
source: template,
segments,
min_args,
})
}
pub fn arity(&self) -> usize {
self.min_args
}
pub fn format(&self, param: &dyn FormatParam) -> String {
self.try_format(param)
.unwrap_or_else(|error| panic!("format failed: {error}"))
}
pub fn try_format(&self, param: &dyn FormatParam) -> Result<String, FormatError> {
let mut out = String::with_capacity(self.source.len());
self.try_write_to(&mut out, param)?;
Ok(out)
}
pub fn write_to(&self, sink: &mut dyn fmt::Write, param: &dyn FormatParam) {
self.try_write_to(sink, param)
.unwrap_or_else(|error| panic!("format failed: {error}"))
}
pub fn try_write_to(
&self,
sink: &mut dyn fmt::Write,
param: &dyn FormatParam,
) -> Result<(), FormatError> {
let args = collect_args(param);
self.render(&args, sink)
}
pub(crate) fn render(
&self,
args: &[&dyn FormatArg],
w: &mut dyn fmt::Write,
) -> Result<(), FormatError> {
if args.len() < self.min_args {
return Err(FormatError::InsufficientParameters);
}
let mut scratch = String::new();
for segment in &self.segments {
match segment {
Segment::Literal(text) => note_sink(w.write_str(text))?,
Segment::Escaped(c) => note_sink(w.write_char(*c))?,
Segment::Placeholder { index, spec } => {
let arg = args
.get(*index)
.ok_or(FormatError::InsufficientParameters)?;
render_arg(arg, spec.resolve(args)?, w, &mut scratch)?;
}
}
}
Ok(())
}
}
fn note_sink(result: fmt::Result) -> Result<(), FormatError> {
result.map_err(|_| FormatError::WriteFailed)
}