1#![forbid(unsafe_code)]
19#![deny(clippy::implicit_hasher)]
20#![warn(clippy::pedantic)]
21#![no_std]
22
23#[cfg(feature = "alloc")]
24extern crate alloc;
25#[cfg(feature = "std")]
26extern crate std;
27
28#[cfg(feature = "alloc")]
29use alloc::string::{String, ToString};
30#[cfg(feature = "std")]
31use std::collections::HashMap;
32#[cfg(feature = "std")]
33use core::borrow::Borrow;
34#[cfg(feature = "std")]
35use core::hash::Hash;
36use core::iter::Iterator;
37use core::fmt;
38use core::convert::Infallible;
39use core::ops::{Index, Range};
40
41#[derive(Debug, Clone)]
44struct ErrorContext{
45 absolute_offset: usize,
46 relative_offset: usize,
47 #[cfg(feature = "alloc")]
48 filename: Option<String>,
49 lineno: u32,
51 length: usize,
54 #[cfg(feature = "alloc")]
55 nearby: String,
56}
57
58impl ErrorContext {
59 #[cold]
60 fn new(src: &[u8], offset: usize, mut length: usize) -> Self {
61 let mut start = offset.saturating_sub(15);
62 let isctrl = |c: &u8| c.is_ascii_control();
63 if let Some(lidx) = src[start..offset].iter().rposition(isctrl) {
64 start += lidx;
65 }
66 let mut end = (offset.saturating_add(length.clamp(15, 100))).min(src.len());
67 if let Some(lidx) = src[offset..end].iter().position(isctrl) {
68 end = offset + lidx;
69 }
70 let lineno = src[..offset].split(|&b| b == b'\n').count().try_into().unwrap_or(u32::MAX);
71 length = length.min(end-offset);
72 ErrorContext{
74 absolute_offset: offset,
75 relative_offset: offset-start,
76 #[cfg(feature = "alloc")]
77 nearby: String::from_utf8_lossy(&src[start..end]).to_string(),
78 #[cfg(feature = "alloc")]
79 filename: None,
80 length,
81 lineno,
82 }
83 }
84
85 #[cfg(feature = "alloc")]
86 pub fn filename(&self) -> Option<&str> {
87 self.filename.as_ref().map(|x| x.as_str())
88 }
89
90 #[cfg(not(feature = "alloc"))]
91 #[allow(clippy::unused_self)]
92 pub fn filename(&self) -> Option<&str> {
93 None
94 }
95}
96
97impl fmt::Display for ErrorContext {
98 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
99 write!(f, "at ")?;
100 if let Some(name) = &self.filename() {
101 write!(f, "{name}:")?;
102 } else {
103 write!(f, "line ")?;
104 }
105
106 write!(f, "{}", self.lineno)?;
107 if f.alternate() {
108 writeln!(f, " (byte offset {})", self.absolute_offset)?;
109 } else {
110 writeln!(f)?;
111 }
112 #[cfg(feature = "alloc")]
113 {
114 writeln!(f, "{}", self.nearby)?;
115 }
116 for _ in 0..self.relative_offset {
117 write!(f, " ")?;
118 }
119 for _ in 0..self.length {
120 write!(f, "^")?;
121 }
122 writeln!(f)?;
123 Ok(())
124 }
125}
126
127#[non_exhaustive]
128#[derive(Debug, Clone, PartialEq)]
129pub enum ErrorKind<E = Infallible> {
130 UnmatchedPercent,
132 UnknownSubstitution,
135 Output(E),
137}
138
139impl<E: fmt::Display + fmt::Debug> fmt::Display for ErrorKind<E> {
140 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
141 write!(f, "{}", match self {
142 ErrorKind::UnmatchedPercent => "unmatched percent sign",
143 ErrorKind::UnknownSubstitution => "unknown substitution name",
144 ErrorKind::Output(err) => return write!(f, "error writing to output: {err:?}"),
145 })
146 }
147}
148
149#[derive(Debug, Clone)]
151pub struct Error<E = Infallible> {
152 kind: ErrorKind<E>,
153 cx: ErrorContext,
154}
155
156impl fmt::Display for Error {
163 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
164 write!(f, "error expanding template: {} ", self.kind)?;
165 self.cx.fmt(f)
168 }
169}
170
171impl Error {
172 #[must_use = "pure function with no side effects"]
173 pub fn kind(&self) -> &ErrorKind {
174 &self.kind
175 }
176
177 #[must_use = "pure function with no side effects"]
192 #[cfg(feature = "alloc")]
193 pub fn with_pos(self, filename: &str, lineno: u32) -> Self {
194 Self {
195 cx: ErrorContext{
196 filename: Some(filename.to_string()),
197 lineno: self.cx.lineno - 1 + lineno,
200 .. self.cx
201 },
202 .. self
203 }
204 }
205}
206
207
208pub fn substitute_into<'a, T, O, S>(template: &T, sub: &'a S, out: &mut O) -> Result<(), Error<O::Error>> where
212 T: AsRef<[u8]> + 'a + ?Sized,
213 T: Index<Range<usize>, Output = T>,
214 S: Substituter<'a, T> + ?Sized,
215 O: Output<T>,
216 str: AsRef<T>,
217{
218 let mut start_pos = 0;
220 let mut in_name = false;
221 for (i, &b) in template.as_ref().iter().enumerate() {
223 let err_cx = || ErrorContext::new(template.as_ref(), start_pos, i-start_pos);
224 let out_err = |err| Error{
225 kind: ErrorKind::Output(err),
226 cx: err_cx(),
227 };
228 if b == b'%' {
229 let slice = &template[start_pos..i];
230 if in_name {
231 if start_pos == i {
232 out.append_to_output("%".as_ref()).map_err(out_err)?;
233 } else if let Some(rep) = sub.lookup_replacement(slice) {
234 out.append_to_output(rep).map_err(out_err)?;
235 } else {
236 return Err(Error{
237 kind: ErrorKind::UnknownSubstitution,
238 cx: err_cx(),
239 });
240 }
241 } else {
242 out.append_to_output(slice).map_err(out_err)?;
243 }
244 start_pos = i + 1;
245 in_name = !in_name;
246 }
247 }
248 if in_name {
249 Err(Error{
250 kind: ErrorKind::UnmatchedPercent,
251 cx: ErrorContext::new(template.as_ref(), start_pos-1, 1),
252 })
253 } else {
254 let len = template.as_ref().len();
255 out.append_to_output(&template[start_pos..len]).map_err(|err| Error{
256 kind: ErrorKind::Output(err),
257 cx: ErrorContext::new(template.as_ref(), start_pos, len),
258 })?;
259 Ok(())
260 }
261}
262
263#[cfg(feature = "alloc")]
266pub fn substitute_string<'a, S: Substituter<'a> + ?Sized>(template: &str, sub: &'a S) -> Result<String, Error> {
267 let mut out = String::with_capacity(template.len());
268 substitute_into(template, sub, &mut out)?;
269 Ok(out)
270}
271
272
273#[cfg(feature = "alloc")]
274pub use crate::substitute_string as substitute;
275
276pub trait Substituter<'a, T: ?Sized = str> {
277 fn lookup_replacement(&'a self, name: &T) -> Option<&'a T>;
285}
286
287pub trait Output<T: ?Sized = str> {
288 type Error;
289
290 fn append_to_output(&mut self, section: &T) -> Result<(), Self::Error>;
291}
292
293#[cfg(feature = "std")]
294impl<'a, K, V, S> Substituter<'a> for HashMap<K, V, S> where
295 K: Borrow<str> + Eq + Hash + 'a,
296 V: AsRef<str> + 'a,
297 S: std::hash::BuildHasher,
298{
299 fn lookup_replacement(&'a self, name: &str) -> Option<&'a str> {
300 self.get(name).map(std::convert::AsRef::as_ref)
301 }
302}
303
304impl<'a, S: AsRef<str> + 'a> Substituter<'a> for [(S, S)] {
309 fn lookup_replacement(&'a self, name: &str) -> Option<&'a str> {
310 for (k, v) in self {
311 if k.as_ref() == name {
312 return Some(v.as_ref())
313 }
314 }
315 None
316 }
317}
318
319impl<'a, S: AsRef<str> + 'a, const N: usize> Substituter<'a> for [(S, S); N] {
320 fn lookup_replacement(&'a self, name: &str) -> Option<&'a str> {
321 self[..].lookup_replacement(name)
322 }
323}
324
325impl<'a, T: ?Sized> Substituter<'a, T> for () {
327 fn lookup_replacement(&'a self, _name: &T) -> Option<&'a T> {
328 None
329 }
330}
331
332#[cfg(feature = "alloc")]
333impl Output<str> for String {
334 type Error = Infallible;
335
336 fn append_to_output(&mut self, section: &str) -> Result<(), Infallible> {
337 *self += section;
338 Ok(())
339 }
340}
341
342#[cfg(feature = "alloc")]
343impl Output<[u8]> for alloc::vec::Vec<u8> {
344 type Error = Infallible;
345
346 fn append_to_output(&mut self, section: &[u8]) -> Result<(), Infallible> {
347 self.extend_from_slice(section);
348 Ok(())
349 }
350}
351
352
353#[cfg(all(test, feature = "alloc"))]
359mod tests {
360 use super::*;
361 use alloc::format;
362
363 #[test]
364 #[cfg(feature = "std")]
365 fn it_works() {
366 let templ1 = "Greetings, %name%, it is %weekday%, and I'm feeling 100%%";
367 let sub1 = HashMap::from([("name".to_string(), "Alice"), ("weekday".to_string(), "Monday")]);
368 assert_eq!(substitute(templ1, &sub1).unwrap(), "Greetings, Alice, it is Monday, and I'm feeling 100%");
369 }
370
371 #[test]
372 fn errors() {
373 let bad_templ1 = "Hi, %name";
374 let templ1 = "Hi, %name%";
375 let err1 = substitute(bad_templ1, &[("name", "Bob")][..]).unwrap_err();
376 assert_eq!(err1.kind(), &ErrorKind::UnmatchedPercent);
377 assert_eq!(err1.to_string(),
378 r"error expanding template: unmatched percent sign at line 1
379Hi, %name
380 ^
381");
382 let err2 = substitute(templ1, &[("lastname", "Smith")]).unwrap_err();
383 assert_eq!(err2.kind(), &ErrorKind::UnknownSubstitution);
384 assert_eq!(format!("{err2:#}"),
385 r"error expanding template: unknown substitution name at line 1 (byte offset 5)
386Hi, %name%
387 ^^^^
388");
389 }
390
391 }