use std::fmt; const SPACER: &str = "\u{200b}"; pub trait Key: Copy + 'static { const PLACEHOLDERS: &'static [(&'static str, Self)]; } #[derive(Debug, Clone, Copy)] pub enum NoKeys {} impl Key for NoKeys { const PLACEHOLDERS: &'static [(&'static str, Self)] = &[]; } pub trait Shape { type Repr; } #[derive(Debug)] pub struct Lines; impl Shape for Lines { type Repr = Vec>>; } #[derive(Debug)] pub struct Line; impl Shape for Line { type Repr = Vec>; } #[derive(Debug, thiserror::Error, PartialEq, Eq)] pub enum TemplateError { #[error("{field}: unknown placeholder {{{name}}}")] UnknownPlaceholder { field: &'static str, name: String }, #[error("{field}: unclosed {{ in template")] UnclosedBrace { field: &'static str }, #[error("{field}: stray }} in template")] StrayBrace { field: &'static str }, #[error("{field}: template mustn't be empty")] Empty { field: &'static str }, #[error("{field}: template must be a single line")] Multiline { field: &'static str }, } #[derive(Debug)] pub enum Segment { Literal(String), Placeholder(K), } pub struct Template { repr: S::Repr, } impl fmt::Debug for Template where S::Repr: fmt::Debug, { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("Template") .field("repr", &self.repr) .finish() } } impl Template { pub fn parse_lines(field: &'static str, texts: &[String]) -> Result { Ok(Self { repr: texts .iter() .map(|text| line_segments(field, text)) .collect::>()?, }) } pub fn lines(&self, resolve: impl Fn(K) -> String) -> Vec { self.repr .iter() .map(|segments| { let rendered = render_segments(segments, &resolve); match rendered.is_empty() { true => SPACER.to_string(), false => rendered, } }) .collect() } } impl Template { // A reject reason is a Line - `receive-pack` gives it as // "ng \n" inside one `pkt-line`. // `line_segments` won't accept newlines // so the reason stays on that one line. pub fn parse(field: &'static str, text: &str) -> Result { match text.is_empty() { true => Err(TemplateError::Empty { field }), false => Ok(Self { repr: line_segments(field, text)?, }), } } pub fn line(&self, resolve: impl Fn(K) -> String) -> String { render_segments(&self.repr, &resolve) } } impl Template { pub fn text_lines(&self) -> Vec { self.lines(|key| match key {}) } } impl Template { pub fn text(&self) -> String { self.line(|key| match key {}) } } fn line_segments( field: &'static str, text: &str, ) -> Result>, TemplateError> { match text.contains('\n') { true => Err(TemplateError::Multiline { field }), false => segments(field, text), } } fn render_segments(segments: &[Segment], resolve: &dyn Fn(K) -> String) -> String { segments .iter() .map(|segment| match segment { Segment::Literal(text) => text.clone(), Segment::Placeholder(key) => resolve(*key), }) .collect() } fn segments(field: &'static str, text: &str) -> Result>, TemplateError> { let Some(at) = text.find(['{', '}']) else { return Ok(literal(text)); }; let (before, rest) = text.split_at(at); let (parsed, remainder) = brace(field, rest)?; Ok(literal(before) .into_iter() .chain(parsed) .chain(segments(field, remainder)?) .collect()) } fn literal(text: &str) -> Vec> { match text.is_empty() { true => Vec::new(), false => vec![Segment::Literal(text.to_string())], } } type Braced<'a, K> = (Option>, &'a str); fn brace<'a, K: Key>(field: &'static str, rest: &'a str) -> Result, TemplateError> { if let Some(after) = rest.strip_prefix("{{") { return Ok((Some(Segment::Literal("{".to_string())), after)); } if let Some(after) = rest.strip_prefix("}}") { return Ok((Some(Segment::Literal("}".to_string())), after)); } if rest.starts_with('}') { return Err(TemplateError::StrayBrace { field }); } let close = rest .find('}') .ok_or(TemplateError::UnclosedBrace { field })?; let name = &rest[1..close]; K::PLACEHOLDERS .iter() .find(|(candidate, _)| *candidate == name) .map(|(_, key)| (Some(Segment::Placeholder(*key)), &rest[close + 1..])) .ok_or_else(|| TemplateError::UnknownPlaceholder { field, name: name.to_string(), }) }