--- /dev/null
+# human-string-filler
+
+A tiny template language for human-friendly string substitutions.
+
+This crate is intended for situations where you need the user to be able to write simple
+templated strings, and conveniently evaluate them. It’s deliberately simple so that there are
+no surprises in its performance or functionality, and so that it’s not accidentally tied to
+Rust (e.g. you can readily implement it in a JavaScript-powered web app), which would happen
+if things like number formatting specifiers were included out of the box—instead, if you want
+that sort of thing, you’ll have to implement it yourself (don’t worry, it won’t be hard).
+
+No logic is provided in this template language, only simple string formatting: `{…}` template
+regions get replaced in whatever way you decide, curly braces get escaped by doubling them
+(`{{` and `}}`), and *that’s it*.
+
+## Sample usage
+
+The **lowest-level** handling looks like this:
+
+```rust
+use human_string_filler::{StrExt, SimpleFillerError};
+
+let mut output = String::new();
+"Hello, {name}!".fill_into(&mut output, |output: &mut String, key: &str| {
+ match key {
+ "name" => output.push_str("world"),
+ _ => return Err(SimpleFillerError::NoSuchKey),
+ }
+ Ok(())
+}).unwrap();
+
+assert_eq!(output, "Hello, world!");
+```
+
+`template.fill_into(output, filler)` (provided by `StrExt`) can also be spelled
+`fill(template, filler, output)` if you prefer a function to a method
+(I reckon the method syntax is clearer, but opinions will differ so I provided both).
+
+The filler function appends to the string directly for efficiency in case of computed values,
+and returns `Result<(), E>`; any error will become `Err(Error::BadReplacement { error, .. })`
+on the fill call. (In this example I’ve used `SimpleFillerError::NoSuchKey`, but `()` would
+work almost as well, or you can write your own error type altogether.)
+
+This example showed a closure that took `&mut String` and used `.push_str(…)`, but this crate
+is not tied to `String` in any way: for greater generality you would use a function generic
+over a type that implements `std::fmt::Write`, and use `.write_str(…)?` inside (`?` works there
+because `SimpleFillerError` implements `From<std::fmt::Error>`).
+
+At a **higher level**, you can use a string-string map as a filler, and you can also fill
+directly to a `String` with `.fill_to_string()` (also available as a standalone function
+`fill_to_string`):
+
+```rust
+use std::collections::HashMap;
+use human_string_filler::StrExt;
+
+let mut map = HashMap::new();
+map.insert("name", "world");
+
+let s = "Hello, {name}!".fill_to_string(&map);
+
+assert_eq!(s.unwrap(), "Hello, world!");
+```
+
+Or you can implement the `Filler` trait for some other type of your own if you like.
+
+## Cargo features
+
+- **std** (enabled by default): remove for `#![no_std]` operation. Implies *alloc*.
+ - Implementation of `std::error::Error` for `Error`;
+ - Implementation of `Filler` for `&HashMap`.
+
+- **alloc** (enabled by default via *std*):
+ - Implementation of `Filler` for `&BTreeMap`.
+ - `fill_to_string` and `StrExt::fill_to_string`.
+
+## The template language
+
+This is the grammar of the template language in [ABNF](https://tools.ietf.org/html/rfc5234):
+
+```abnf
+unescaped-normal-char = %x00-7A / %x7C / %x7E-D7FF / %xE000-10FFFF
+ ; any Unicode scalar value except for "{" and "}"
+
+normal-char = unescaped-normal-char / "{{" / "}}"
+
+template-region = "{" *unescaped-normal-char "}"
+
+template-string = *( normal-char / template-region )
+```
+
+This regular expression will validate a template string:
+
+```text
+^([^{}]|\{\{|\}\}|\{[^{}]*\})*$
+```
+
+Sample legal template strings:
+
+- The empty string
+- `Hello, {name}!`: one template region with key "name".
+- `Today is {date:short}`: one template region with key "date:short". (Although there’s no
+ format specification like with the `format!()` macro, a colon convention is one reasonable
+ option—see the next section.)
+- `Hello, {}!`: one template region with an empty key, not recommended but allowed.
+- `Escaped {{ braces {and replacements} for {fun}!`: string "Escaped { braces ", followed by a
+ template region with key "and replacements", followed by string " for ", followed by a
+ template region with key "fun", followed by string "!".
+
+Sample illegal template strings:
+
+- `hello, {world}foo}`: opening and closing curlies must match; any others (specifically, the
+ last character of the string) must be escaped by doubling.
+- `{{thing}`: the `{{` is an escaped opening curly, so the `}` is unmatched.
+- `{thi{{n}}g}`: no curlies of any form inside template region keys. (It’s possible that a
+ future version may make it possible to escape curlies inside template regions, if it proves
+ to be useful in something like format specifiers; but not at this time.)
+
+## Conventions on key semantics
+
+The key is an arbitrary string (except that it can’t contain `{` or `}`) with explicitly no
+defined semantics, but here are some suggestions, including helper functions:
+
+1. If it makes sense to have a format specifier (e.g. to specify a date format to use, or
+ whether to pad numbers with leading zeroes, *&c.*), split once on a character like `:`.
+ To do this most conveniently, a function `split_on` is provided.
+
+2. For more advanced formatting where you have multiple properties you could wish to set,
+ `split_propertied` offers some sound and similarly simple semantics for such strings as
+ `{key prop1 prop2=val2}` and `{key:prop1,prop2=val2}`.
+
+3. If it makes sense to have nested property access, split on `.` with the `key.split('.')`
+ iterator. (If you’re using `split_on` or `split_propertied` as mentioned above, you
+ probably want to apply them first to separate out the key part.)
+
+4. Only use [UAX #31 identifiers](https://www.unicode.org/reports/tr31/) for the key
+ (or keys, if supporting nested property access). Most of the time, empty strings and
+ numbers are probably not a good idea.
+
+With these suggestions, you might end up with the key `foo.bar:baz` being interpreted as
+retrieving the “bar” property from the “foo” object, and formatting it according to “baz”; or
+`aleph.beth.gimmel|alpha beta=5` as retrieving “gimmel” from “beth” of “aleph”, and formatting
+it with properties “alpha” set to true and “beta” set to 5. What those things actually *mean*
+is up to you to decide. *I* certainly haven’t a clue.
+
+## Author
+
+[Chris Morgan](https://chrismorgan.info/)
+([chris-morgan](https://gitlab.com/chris-morgan))
+is the author and maintainer of human-string-filler.
+
+## License
+
+Copyright © 2022 Chris Morgan
+
+This project is distributed under the terms of three different licenses,
+at your choice:
+
+- Blue Oak Model License 1.0.0: https://blueoakcouncil.org/license/1.0.0
+- MIT License: https://opensource.org/licenses/MIT
+- Apache License, Version 2.0: https://www.apache.org/licenses/LICENSE-2.0
+
+If you do not have particular cause to select the MIT or the Apache-2.0
+license, Chris Morgan recommends that you select BlueOak-1.0.0, which is
+better and simpler than both MIT and Apache-2.0, which are only offered
+due to their greater recognition and their conventional use in the Rust
+ecosystem. (BlueOak-1.0.0 was only published in March 2019.)
+
+When using this code, ensure you comply with the terms of at least one of
+these licenses.
--- /dev/null
+# A discussion of parts of the design and trade-offs in this library
+
+## Terminology and semantics
+
+- `Hello, {thing}!` is a template string.
+- In that, `{thing}` is a template region.
+- In that, `thing` is a key.
+- `{{` is an escaped opening curly brace.
+- `}}` is an escaped closing curly brace.
+
+Beyond that, these are *suggestions* for template region semantics:
+
+- In the `{post.date:short}` template region:
+ - `post.date` is a path (the `date` field within the `post` object);
+ - `short` is a format specifier, which we might here imagine to mean “use
+ localised date formatting, short form”.
+
+- In the `{post.date time hour=24}` and `{post.date:time,hour=24}`
+ template regions (I’m not suggesting particular characters):
+ - `post.date` is a path (the `date` field within the `post` object);
+ - `time` is a boolean property, and `hour` a property with value `24`,
+ which we might here imagine to mean “show the time, in 24-hour mode”.
+
+## Explanation of escaping
+
+It is necessary that literal `{` and `}` be able to be expressed, even if
+they’re rare; there are three common techniques for achieving this:
+
+1. **Reserve certain template region keys to signify the delimiters,** e.g.
+ `{open_curly}` and `{close_curly}` to expand to `{` and `}`. Downsides: may
+ clash with real template region keys when arbitrary user keys are supported;
+ and it’s generally less memorable.
+2. **Double the delimiter:** `{{` expands to `{` and `}}` to `}`. Seen in
+ string delimiters in a few languages, so `"foo""bar"` means “foo"bar”.
+ Downside: makes including the delimiters inside the key a difficult
+ proposition (see below).
+3. **Escape by prefixing with another selected character:** select an escape
+ character which is also rare, most commonly a backslash due to its
+ convention in programming languages. Problems with using backslash: if you
+ need to represent Windows paths, literal backslashes aren’t so rare after
+ all, and you need to escape them all; and if you need to encode the template
+ string in a string literal in another language that uses the same escape
+ character (e.g. a TOML config file), all backslashes must now be escaped
+ again, e.g. string literals `"C:\\\\dir"` or `"\\{ ← literal curly"`.
+
+(A fourth technique that can be employed is delimiting the replacement regions
+with values that will not appear in the desired text, as with the ␜, ␝, ␞ and ␟
+separator control characters in ASCII—and that more or less failed because it
+was roughly machine-writable only, and so file types like CSV prospered due to
+being human-writable, despite the acconpanying escaping woes. This language
+requires that the delimiters be easy for the user to type, so this fourth
+technique is deemed impossible—all values that are easy to type may occur in
+regular text.)
+
+One interesting consideration is whether you can express the delimiters inside
+template regions. For simplicity, I’ve currently banned curlies inside template
+regions, but there are things to consider if it becomes desirable to express it
+later (e.g. in a format specifier). Here’s how each of the three techniques
+previously mentioned handles it:
+
+1. Solvable in two ways, both of which require that curlies be paired inside
+ template regions:
+ (a) Replace only `{open_curly}` and `{close_curly}` literally inside
+ template regions; or
+ (b) Perform recursive template resolution!
+
+2. Well, should `{foo}}}` mean “template region with key ‘foo’ followed by
+ literal ‘}’”, or “template region with key ‘foo}’”? If you wish both to be
+ expressible, you need to provide some means of disambiguation. The most
+ straightforward is to ban a certain character at the start and end of
+ template regions, most probably HORIZONTAL SPACE (that is: declare “no
+ leading or trailing whitespace”), and then repurpose that to behave
+ essentially the same as `#` on Rust’s raw string literals, where you can
+ write `r"…"`, `r#"…"`, `r##"…"##`, *&c.* With this, `{foo}}}` would mean
+ “template region with key ‘foo’ followed by literal ‘}’”, and `{ foo} }`
+ would mean “template region with key ‘foo}’”. (Note how escaping curlies
+ inside template regions is not required. Decide for yourself whether that’s
+ good or bad.)
+
+3. Allow escapes inside the template region, e.g. `\{{\{\}}\}` would mean
+ “literal ‘{’ followed by template region with key ‘{}’ followed by literal
+ ‘}’”. Straightforward.
+
+This then leads to three possibilities:
+(a) full recursive template rendering;
+(b) arbitrary keys, or *almost* arbitrary if leading and trailing whitespace is
+ disallowed or ignored (depending on the approach); or
+(c) no curlies in keys please.
+
+I initially implemented the third technique (escape with backslash, and allow
+escapes in template regions), but the downsides of using backslashes became too
+severe for my liking, and I didn’t like any alternative characters on the
+keyboard as much as just doubling the delimiter, so I went with the second
+technique in the end. I don’t expect its downside to bite me, but we’ll see.
+
+## Other considered syntaxes
+
+I settled on using `{…}` for template regions. I also contemplated `$…`, but
+ruled it for the combination of these reasons:
+
+- it requires embedding UAX #31 identifier parsing which is heavier and less
+ *definitely obvious* for real users than I’d like;
+
+- for things like nested property access and format specifiers you’d need a
+ variant with a closing delimiter anyway, such as `${…}`, so now you’d have
+ *two* formats;
+
+- all this makes it harder to explain all the features of the language to
+ users.
+
+So `$…` was eliminated as too complex and insufficient.
+
+So it became more a toss-up between `$…$`, `%…%`, `${…}` and `{…}`.
+
+- `$…$` was eliminated because it’s not using any paired delimiting characters,
+ and because more programmery types would be more likely to expect `$…` than
+ `$…$`.
+
+- `%…%` has precedent in Windows’ environment variables, but again the lack of
+ *paired* delimiting characters makes it not so great. Also it just feels
+ uglier in the standard sort of case I have in mind. I’m picturing something
+ like `%ALBUM%/%TRACK% - %TITLE%.mp3`. (It doesn’t *have* to be uppercase, but
+ I find that using percent makes me think uppercase.)
+
+- Given my intended purpose (*small* strings rather than big multi-line
+ strings), the `$` in `${…}` didn’t feel necessary: consider for example
+ `{album}/{track} - {title}.mp3`, which feels about right, compared with
+ `${album}/${track} - ${title}.mp3`, which *works*, but feels… I dunno,
+ programmery rather than usery.
+
+So `{…}` won.
+
+Taken with the `{{` and `}}` escaping, this makes it match Rust’s format
+strings; this is purely coincidental.
+
+I’m pretty sure I considered `[…]` at some point too, but decided I didn’t like
+it as much as `{…}`.
--- /dev/null
+#![cfg_attr(not(feature = "std"), no_std)]
+#![deny(missing_docs)]
+#![cfg_attr(docsrs, feature(doc_cfg))]
+//! A tiny template language for human-friendly string substitutions.
+//!
+//! This crate is intended for situations where you need the user to be able to write simple
+//! templated strings, and conveniently evaluate them. It’s deliberately simple so that there are
+//! no surprises in its performance or functionality, and so that it’s not accidentally tied to
+//! Rust (e.g. you can readily implement it in a JavaScript-powered web app), which would happen
+//! if things like number formatting specifiers were included out of the box—instead, if you want
+//! that sort of thing, you’ll have to implement it yourself (don’t worry, it won’t be hard).
+//!
+//! No logic is provided in this template language, only simple string formatting: `{…}` template
+//! regions get replaced in whatever way you decide, curly braces get escaped by doubling them
+//! (`{{` and `}}`), and *that’s it*.
+//!
+//! ## Sample usage
+//!
+//! The **lowest-level** handling looks like this:
+//!
+//! ```rust
+//! use human_string_filler::{StrExt, SimpleFillerError};
+//!
+//! let mut output = String::new();
+//! "Hello, {name}!".fill_into(&mut output, |output: &mut String, key: &str| {
+//! match key {
+//! "name" => output.push_str("world"),
+//! _ => return Err(SimpleFillerError::NoSuchKey),
+//! }
+//! Ok(())
+//! }).unwrap();
+//!
+//! assert_eq!(output, "Hello, world!");
+//! ```
+//!
+//! `template.fill_into(output, filler)` (provided by `StrExt`) can also be spelled
+//! `fill(template, filler, output)` if you prefer a function to a method
+//! (I reckon the method syntax is clearer, but opinions will differ so I provided both).
+//!
+//! The filler function appends to the string directly for efficiency in case of computed values,
+//! and returns `Result<(), E>`; any error will become `Err(Error::BadReplacement { error, .. })`
+//! on the fill call. (In this example I’ve used `SimpleFillerError::NoSuchKey`, but `()` would
+//! work almost as well, or you can write your own error type altogether.)
+//!
+//! This example showed a closure that took `&mut String` and used `.push_str(…)`, but this crate
+//! is not tied to `String` in any way: for greater generality you would use a function generic
+//! over a type that implements `std::fmt::Write`, and use `.write_str(…)?` inside (`?` works there
+//! because `SimpleFillerError` implements `From<std::fmt::Error>`).
+//!
+//! At a **higher level**, you can use a string-string map as a filler, and you can also fill
+//! directly to a `String` with `.fill_to_string()` (also available as a standalone function
+//! `fill_to_string`):
+//!
+//! ```rust
+//! # #[cfg(feature = "std")] {
+//! use std::collections::HashMap;
+//! use human_string_filler::StrExt;
+//!
+//! let mut map = HashMap::new();
+//! map.insert("name", "world");
+//!
+//! let s = "Hello, {name}!".fill_to_string(&map);
+//!
+//! assert_eq!(s.unwrap(), "Hello, world!");
+//! # }
+//! ```
+//!
+//! Or you can implement the [`Filler`] trait for some other type of your own if you like.
+//!
+//! ## Cargo features
+//!
+#![cfg_attr(
+ feature = "std",
+ doc = " \
+ - **std** (enabled by default, enabled in this build): remove for `#![no_std]` operation. \
+ Implies *alloc*.\
+"
+)]
+#![cfg_attr(
+ not(feature = "std"),
+ doc = " \
+ - **std** (enabled by default, *disabled* in this build): remove for `#![no_std]` operation. \
+ Implies *alloc*.\
+"
+)]
+//! - Implementation of `std::error::Error` for `Error`;
+//! - Implementation of `Filler` for `&HashMap`.
+//!
+#![cfg_attr(
+ feature = "alloc",
+ doc = " \
+ - **alloc** (enabled by default via *std*, enabled in this build):\
+"
+)]
+#![cfg_attr(
+ not(feature = "alloc"),
+ doc = " \
+ - **alloc** (enabled by default via *std*, disabled in this build):\
+"
+)]
+//! - Implementation of `Filler` for `&BTreeMap`.
+//! - `fill_to_string` and `StrExt::fill_to_string`.
+//!
+//! ## The template language
+//!
+//! This is the grammar of the template language in [ABNF](https://tools.ietf.org/html/rfc5234):
+//!
+//! ```abnf
+//! unescaped-normal-char = %x00-7A / %x7C / %x7E-D7FF / %xE000-10FFFF
+//! ; any Unicode scalar value except for "{" and "}"
+//!
+//! normal-char = unescaped-normal-char / "{{" / "}}"
+//!
+//! template-region = "{" *unescaped-normal-char "}"
+//!
+//! template-string = *( normal-char / template-region )
+//! ```
+//!
+//! This regular expression will validate a template string:
+//!
+//! ```text
+//! ^([^{}]|\{\{|\}\}|\{[^{}]*\})*$
+//! ```
+//!
+//! Sample legal template strings:
+//!
+//! - The empty string
+//! - `Hello, {name}!`: one template region with key "name".
+//! - `Today is {date:short}`: one template region with key "date:short". (Although there’s no
+//! format specification like with the `format!()` macro, a colon convention is one reasonable
+//! option—see the next section.)
+//! - `Hello, {}!`: one template region with an empty key, not recommended but allowed.
+//! - `Escaped {{ braces {and replacements} for {fun}!`: string "Escaped { braces ", followed by a
+//! template region with key "and replacements", followed by string " for ", followed by a
+//! template region with key "fun", followed by string "!".
+//!
+//! Sample illegal template strings:
+//!
+//! - `hello, {world}foo}`: opening and closing curlies must match; any others (specifically, the
+//! last character of the string) must be escaped by doubling.
+//! - `{{thing}`: the `{{` is an escaped opening curly, so the `}` is unmatched.
+//! - `{thi{{n}}g}`: no curlies of any form inside template region keys. (It’s possible that a
+//! future version may make it possible to escape curlies inside template regions, if it proves
+//! to be useful in something like format specifiers; but not at this time.)
+//!
+//! ## Conventions on key semantics
+//!
+//! The key is an arbitrary string (except that it can’t contain `{` or `}`) with explicitly no
+//! defined semantics, but here are some suggestions, including helper functions:
+//!
+//! 1. If it makes sense to have a format specifier (e.g. to specify a date format to use, or
+//! whether to pad numbers with leading zeroes, *&c.*), split once on a character like `:`.
+//! To do this most conveniently, a function [`split_on`] is provided.
+//!
+//! 2. For more advanced formatting where you have multiple properties you could wish to set,
+//! [`split_propertied`] offers some sound and similarly simple semantics for such strings as
+//! `{key prop1 prop2=val2}` and `{key:prop1,prop2=val2}`.
+//!
+//! 3. If it makes sense to have nested property access, split on `.` with the `key.split('.')`
+//! iterator. (If you’re using `split_on` or `split_propertied` as mentioned above, you
+//! probably want to apply them first to separate out the key part.)
+//!
+//! 4. Only use [UAX #31 identifiers](https://www.unicode.org/reports/tr31/) for the key
+//! (or keys, if supporting nested property access). Most of the time, empty strings and
+//! numbers are probably not a good idea.
+//!
+//! With these suggestions, you might end up with the key `foo.bar:baz` being interpreted as
+//! retrieving the “bar” property from the “foo” object, and formatting it according to “baz”; or
+//! `aleph.beth.gimmel|alpha beta=5` as retrieving “gimmel” from “beth” of “aleph”, and formatting
+//! it with properties “alpha” set to true and “beta” set to 5. What those things actually *mean*
+//! is up to you to decide. *I* certainly haven’t a clue.
+
+use core::fmt;
+use core::iter::FusedIterator;
+use core::ops::Range;
+
+#[cfg(feature = "alloc")]
+extern crate alloc;
+
+#[cfg(feature = "alloc")]
+use alloc::string::String;
+
+#[cfg(feature = "alloc")]
+use alloc::collections::BTreeMap;
+#[cfg(feature = "alloc")]
+use core::borrow::Borrow;
+#[cfg(feature = "std")]
+use std::collections::HashMap;
+#[cfg(feature = "std")]
+use std::hash::Hash;
+
+/// Any error that occurs when filling a template string.
+///
+/// Template parsing and filling is all done in a single pass; so a failed replacement due to an
+/// unknown key will shadow a syntax error later in the string.
+#[derive(Debug, PartialEq, Eq)]
+pub enum Error<'a, E> {
+ /// A template region was not closed.
+ /// That is, an opening curly brace (`{`) with no matching closing curly brace (`}`).
+ ///
+ /// Example:
+ ///
+ /// ```rust
+ /// # #[cfg(feature = "alloc")] {
+ /// # use human_string_filler::{StrExt, Error};
+ /// # assert_eq!(
+ /// "Hello, {thing"
+ /// # .fill_to_string(|_: &mut String, _: &str| Result::<(), ()>::Ok(())),
+ /// # Err(Error::UnclosedRegion { source: "{thing", range: 7..13 }),
+ /// # );
+ /// # }
+ /// ```
+ UnclosedRegion {
+ /// The text of the unclosed region, which will start with `{` and contain no other curly
+ /// braces.
+ source: &'a str,
+ /// The indexes of `source` within the template string.
+ range: Range<usize>,
+ },
+
+ /// An unescaped closing curly brace (`}`) was found, outside a template region.
+ ///
+ /// Examples:
+ ///
+ /// ```rust
+ /// # #[cfg(feature = "alloc")] {
+ /// # use human_string_filler::{StrExt, Error};
+ /// # assert_eq!(
+ /// "Hello, thing}!"
+ /// # .fill_to_string(|_: &mut String, _: &str| Result::<(), ()>::Ok(())),
+ /// # Err(Error::UnexpectedClosingBrace { index: 12 }),
+ /// # );
+ /// # assert_eq!(
+ /// "Hello, {name}, look at my magnificent moustache: (}-:"
+ /// # .fill_to_string(|_: &mut String, _: &str| Result::<(), ()>::Ok(())),
+ /// # Err(Error::UnexpectedClosingBrace { index: 50 }),
+ /// # );
+ /// # assert_eq!(
+ /// "Hello, {name}}!"
+ /// # .fill_to_string(|_: &mut String, _: &str| Result::<(), ()>::Ok(())),
+ /// # Err(Error::UnexpectedClosingBrace { index: 13 }),
+ /// # );
+ /// # }
+ /// ```
+ UnexpectedClosingBrace {
+ /// The index of the closing brace within the template string.
+ index: usize,
+ },
+
+ /// An opening curly brace (`{`) was found within a template region.
+ ///
+ /// Examples:
+ ///
+ /// ```rust
+ /// # #[cfg(feature = "alloc")] {
+ /// # use human_string_filler::{StrExt, Error};
+ /// # assert_eq!(
+ /// "Hello, {thing{{sadness}}}"
+ /// # .fill_to_string(|_: &mut String, _: &str| Result::<(), ()>::Ok(())),
+ /// # Err(Error::UnexpectedOpeningBrace { index: 13 }),
+ /// # );
+ /// # }
+ /// ```
+ UnexpectedOpeningBrace {
+ /// The index of the opening brace within the template string.
+ index: usize,
+ },
+
+ /// The filler returned an error for the specified key.
+ BadReplacement {
+ /// The key on which the filler failed. Curly braces not included.
+ key: &'a str,
+ /// The indexes of `key` within the template string.
+ range: Range<usize>,
+ /// The error value returned by the filler.
+ error: E,
+ },
+
+ /// Writing to the output failed.
+ WriteFailed(fmt::Error),
+}
+
+impl<'a, E> From<fmt::Error> for Error<'a, E> {
+ fn from(e: fmt::Error) -> Self {
+ Error::WriteFailed(e)
+ }
+}
+
+impl<'a, E> fmt::Display for Error<'a, E>
+where
+ E: fmt::Display,
+{
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ match self {
+ Error::UnclosedRegion { source, .. } => {
+ write!(f, "Unclosed template region at \"{}\"", source)
+ }
+
+ Error::UnexpectedClosingBrace { index } => {
+ write!(f, "Unexpected closing brace at index {}", index)
+ }
+
+ Error::UnexpectedOpeningBrace { index } => {
+ write!(
+ f,
+ "Unexpected curly brace within template region at index {}",
+ index
+ )
+ }
+
+ Error::BadReplacement { key, error, .. } => {
+ write!(f, "Error in template string at \"{{{}}}\": {}", key, error)
+ }
+
+ Error::WriteFailed(fmt::Error) => f.write_str("Error in writing output"),
+ }
+ }
+}
+
+#[cfg(feature = "std")]
+#[cfg_attr(docsrs, doc(cfg(feature = "std")))]
+impl<'a, E> std::error::Error for Error<'a, E>
+where
+ E: std::error::Error + 'static,
+{
+ fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
+ match self {
+ Error::BadReplacement { error, .. } => Some(error),
+ Error::WriteFailed(error) => Some(error),
+ _ => None,
+ }
+ }
+}
+
+/// Implementers of this trait have the ability to fill template strings.
+///
+/// It is extremely strongly recommended that fillers only push to the output, and do not perform
+/// any other modifications of it.
+///
+/// I mean, if you implement `Filler<String, _>`, you get a `&mut String` and it’s *possible* to do
+/// other things with it, but that’s a terrible idea. I’m almost ashamed of ideas like making `{␡}`
+/// pop the last character, and `{←rot13}` ROT-13-encode what precedes it in the string.
+pub trait Filler<W, E>
+where
+ W: fmt::Write,
+{
+ /// Fill the value for the given key into the output string.
+ fn fill(&mut self, output: &mut W, key: &str) -> Result<(), E>;
+}
+
+impl<F, W, E> Filler<W, E> for F
+where
+ F: FnMut(&mut W, &str) -> Result<(), E>,
+ W: fmt::Write,
+{
+ fn fill(&mut self, output: &mut W, key: &str) -> Result<(), E> {
+ self(output, key)
+ }
+}
+
+#[cfg_attr(not(feature = "std"), allow(rustdoc::broken_intra_doc_links))]
+/// A convenient error type for fillers; you might even like to use it yourself.
+///
+/// You could also use `()`, but this gives you
+/// <code>[From](core::convert::From)<[core::fmt::Error]></code> so that you can use
+/// `write!(out, …)?`, and sane [`core::fmt::Display`] and [`std::error::Error`] implementations.
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub enum SimpleFillerError {
+ /// The map didn’t contain the requested key.
+ NoSuchKey,
+ /// Some fmt::Write operation returned an error.
+ WriteFailed(fmt::Error),
+}
+
+impl From<fmt::Error> for SimpleFillerError {
+ fn from(e: fmt::Error) -> Self {
+ SimpleFillerError::WriteFailed(e)
+ }
+}
+
+impl fmt::Display for SimpleFillerError {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ match self {
+ SimpleFillerError::NoSuchKey => f.write_str("no such key"),
+ SimpleFillerError::WriteFailed(fmt::Error) => f.write_str("write failed"),
+ }
+ }
+}
+
+#[cfg(feature = "std")]
+#[cfg_attr(docsrs, doc(cfg(feature = "std")))]
+impl std::error::Error for SimpleFillerError {
+ fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
+ match self {
+ SimpleFillerError::WriteFailed(error) => Some(error),
+ _ => None,
+ }
+ }
+}
+
+#[cfg(feature = "std")]
+#[cfg_attr(docsrs, doc(cfg(feature = "std")))]
+impl<K, V, W> Filler<W, SimpleFillerError> for &HashMap<K, V>
+where
+ K: Borrow<str> + Eq + Hash,
+ V: AsRef<str>,
+ W: fmt::Write,
+{
+ fn fill(&mut self, output: &mut W, key: &str) -> Result<(), SimpleFillerError> {
+ self.get(key)
+ .ok_or(SimpleFillerError::NoSuchKey)
+ .and_then(|value| output.write_str(value.as_ref()).map_err(Into::into))
+ }
+}
+
+#[cfg(feature = "alloc")]
+#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
+impl<K, V, W> Filler<W, SimpleFillerError> for &BTreeMap<K, V>
+where
+ K: Borrow<str> + Ord,
+ V: AsRef<str>,
+ W: fmt::Write,
+{
+ fn fill(&mut self, output: &mut W, key: &str) -> Result<(), SimpleFillerError> {
+ self.get(key)
+ .ok_or(SimpleFillerError::NoSuchKey)
+ .and_then(|value| output.write_str(value.as_ref()).map_err(Into::into))
+ }
+}
+
+/// String extension methods for the template string.
+///
+/// This is generally how I recommend using this library, because I find that the method receiver
+/// makes code clearer: that `template.fill_into(output, filler)` is easier to understand than
+/// `fill(template, filler, output)`.
+pub trait StrExt {
+ /// Fill this template, producing a new string.
+ ///
+ /// This is a convenience method for ergonomics in the case where you aren’t fussed about
+ /// allocations and are using the standard `String` type.
+ ///
+ #[cfg_attr(feature = "std", doc = " Example, using a hash map:")]
+ #[cfg_attr(
+ not(feature = "std"),
+ doc = " Example, using a hash map (requires the *std* feature):"
+ )]
+ ///
+ /// ```rust
+ /// # #[cfg(feature = "std")] {
+ /// # use human_string_filler::StrExt;
+ /// # use std::collections::HashMap;
+ /// let map = [("name", "world")].into_iter().collect::<HashMap<_, _>>();
+ /// assert_eq!(
+ /// "Hello, {name}!".fill_to_string(&map).unwrap(),
+ /// "Hello, world!",
+ /// );
+ /// # }
+ /// ```
+ #[cfg(feature = "alloc")]
+ #[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
+ fn fill_to_string<F, E>(&self, filler: F) -> Result<String, Error<E>>
+ where
+ F: Filler<String, E>,
+ {
+ let mut out = String::new();
+ self.fill_into(&mut out, filler).map(|()| out)
+ }
+
+ /// Fill this template string into the provided string, with the provided filler.
+ ///
+ /// Uses an existing string, which is more efficient if you want to push to an existing string
+ /// or can reuse a string allocation.
+ ///
+ /// Example, using a closure:
+ ///
+ /// ```rust
+ /// # use human_string_filler::StrExt;
+ /// let filler = |output: &mut String, key: &str| {
+ /// match key {
+ /// "name" => output.push_str("world"),
+ /// _ => return Err(()),
+ /// }
+ /// Ok(())
+ /// };
+ /// let mut string = String::new();
+ /// assert!("Hello, {name}!".fill_into(&mut string, filler).is_ok());
+ /// assert_eq!(string, "Hello, world!");
+ /// ```
+ fn fill_into<F, W, E>(&self, output: &mut W, filler: F) -> Result<(), Error<E>>
+ where
+ F: Filler<W, E>,
+ W: fmt::Write;
+}
+
+impl StrExt for str {
+ #[inline]
+ fn fill_into<F, W, E>(&self, output: &mut W, filler: F) -> Result<(), Error<E>>
+ where
+ F: Filler<W, E>,
+ W: fmt::Write,
+ {
+ fill(self, filler, output)
+ }
+}
+
+/// The lowest-level form, as a function: fill the template string, into a provided writer.
+///
+/// This is the most efficient form. It splits a string by `{…}` sections, adding anything outside
+/// them to the output string (with escaped curlies dedoubled) and passing template regions through
+/// the filler, which handles pushing to the output string itself.
+///
+/// See also [`StrExt::fill_into`] which respells `fill(template, filler, output)` as
+/// `template.fill_into(output, filler)`.
+pub fn fill<'a, F, W, E>(
+ mut template: &'a str,
+ mut filler: F,
+ output: &mut W,
+) -> Result<(), Error<'a, E>>
+where
+ F: Filler<W, E>,
+ W: fmt::Write,
+{
+ let mut index = 0;
+ loop {
+ if let Some(i) = template.find(|c| c == '{' || c == '}') {
+ #[allow(clippy::wildcard_in_or_patterns)]
+ match template.as_bytes()[i] {
+ c @ b'}' | c @ b'{' if template.as_bytes().get(i + 1) == Some(&c) => {
+ output.write_str(&template[0..i + 1])?;
+ template = &template[i + 2..];
+ index += i + 2;
+ }
+ b'}' => return Err(Error::UnexpectedClosingBrace { index: index + i }),
+ b'{' | _ => {
+ // (_ here just to lazily skip an unreachable!().)
+ output.write_str(&template[0..i])?;
+ template = &template[i..];
+ index += i;
+ if let Some(i) = template[1..].find(|c| c == '{' || c == '}') {
+ match template.as_bytes()[i + 1] {
+ b'}' => {
+ if let Err(e) = filler.fill(output, &template[1..i + 1]) {
+ return Err(Error::BadReplacement {
+ key: &template[1..i + 1],
+ range: (index + 1)..(index + i + 1),
+ error: e,
+ });
+ }
+ template = &template[i + 2..];
+ index += i + 2;
+ }
+ // (Again, _ is unreachable.)
+ b'{' | _ => {
+ return Err(Error::UnexpectedOpeningBrace {
+ index: index + i + 1,
+ })
+ }
+ }
+ } else {
+ return Err(Error::UnclosedRegion {
+ source: template,
+ range: index..(index + template.len()),
+ });
+ }
+ }
+ }
+ } else {
+ output.write_str(template)?;
+ break;
+ }
+ }
+
+ Ok(())
+}
+
+/// Fill a template, producing a new string.
+///
+/// This is a convenience function for ergonomics in the case where you aren’t fussed about
+/// allocations and are using the standard `String` type.
+///
+/// See also [`StrExt::fill_to_string`], which respells `fill_to_string(template, filler)` as
+/// `template.fill_to_string(filler)`.
+#[cfg(feature = "alloc")]
+#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
+pub fn fill_to_string<F, E>(template: &str, filler: F) -> Result<String, Error<E>>
+where
+ F: Filler<String, E>,
+{
+ let mut out = String::new();
+ fill(template, filler, &mut out).map(|()| out)
+}
+
+/// A convenience function to split a string on a character.
+///
+/// This is nicer than using `string.split(c, 2)` because it gives you the two values up-front.
+///
+/// # Returns
+///
+/// A two-tuple of:
+///
+/// 1. What comes before the split character, or the entire string if there was none; and
+/// 2. The remainder after the split character, if there was one (even if it’s empty).
+///
+/// ```
+/// # use human_string_filler::split_on;
+/// assert_eq!(split_on("The quick brown fox", ':'), ("The quick brown fox", None));
+/// assert_eq!(split_on("/", '/'), ("", Some("")));
+/// assert_eq!(split_on("harum = scarum", '='), ("harum ", Some(" scarum")));
+/// assert_eq!(split_on("diæresis:tréma:umlaut", ':'), ("diæresis", Some("tréma:umlaut")));
+/// ```
+pub fn split_on(string: &str, c: char) -> (&str, Option<&str>) {
+ match string.find(c) {
+ Some(i) => (&string[..i], Some(&string[i + c.len_utf8()..])),
+ None => (string, None),
+ }
+}
+
+/// The separators to use in [`split_propertied`].
+///
+/// A couple of sets of plausible-looking values (but if you want a concrete recommendation, like
+/// Gallio of old I refuse to be a judge of these things):
+///
+/// - `(' ', ' ', '=')` looks like `Hello, {name first formal=false case=lower}!`.
+/// - `('|', ',', ':')` looks like `Hello, {name|first,formal:false,case:lower}!`.
+#[derive(Clone, Copy, Debug)]
+pub struct Separators {
+ /// What character indicates the end of the key and the start of the properties.
+ pub between_key_and_properties: char,
+
+ /// What character indicates the end of one property’s name or value and the start of the next
+ /// property’s name.
+ pub between_properties: char,
+
+ /// What character indicates the end of a property’s name and the start of its value.
+ /// Remember that properties aren’t required to have values, but can be booleanyish.
+ // “booleanyish” sounded better than “booleanishy”. That’s my story and I’m sticking with it.
+ /// For that matter, if you want *all* properties to be boolean, set this to the same value as
+ /// `between_properties`, because `between_properties` is greedier.
+ pub between_property_name_and_value: char,
+}
+
+/// A convenience function to split a key that is followed by properties.
+///
+/// In keeping with this library in general, this is deliberately very simple and consequently not
+/// able to express all possible values; for example, if you use space as the separator between
+/// properties, you can’t use space in property values; and this doesn’t guard against empty keys
+/// or property names in any way.
+///
+/// ```
+/// use human_string_filler::{Separators, split_propertied};
+///
+/// let (key, properties) = split_propertied("key:prop1,prop2=value2,prop3=4+5=9", Separators {
+/// between_key_and_properties: ':',
+/// between_properties: ',',
+/// between_property_name_and_value: '=',
+/// });
+///
+/// assert_eq!(key, "key");
+/// assert_eq!(properties.collect::<Vec<_>>(),
+/// vec![("prop1", None), ("prop2", Some("value2")), ("prop3", Some("4+5=9"))]);
+/// ```
+///
+/// This method consumes exactly one character for the separators; if space is your
+/// between-properties separator, for example, multiple spaces will not be combined, but
+/// you’ll get `("", None)` properties instead. As I say, this is deliberately simple.
+pub fn split_propertied(
+ s: &str,
+ separators: Separators,
+) -> (
+ &str,
+ impl Iterator<Item = (&str, Option<&str>)>
+ + DoubleEndedIterator
+ + FusedIterator
+ + Clone
+ + fmt::Debug,
+) {
+ let (key, properties) = split_on(s, separators.between_key_and_properties);
+ let properties = properties
+ .map(|properties| properties.split(separators.between_properties))
+ .unwrap_or_else(|| {
+ // We need an iterator of the same type that will yield None, but Split yields an empty
+ // string first. Nice and easy: consume that, then continue on our way.
+ let mut dummy = "".split(' ');
+ dummy.next();
+ dummy
+ })
+ .map(move |word| split_on(word, separators.between_property_name_and_value));
+ (key, properties)
+}
+
+#[cfg(test)]
+mod tests {
+ #[allow(unused_imports)]
+ use super::*;
+
+ #[cfg(feature = "alloc")]
+ macro_rules! test {
+ ($name:ident, $filler:expr) => {
+ #[test]
+ fn $name() {
+ let filler = $filler;
+
+ assert_eq!(
+ "Hello, {}!".fill_to_string(&filler).as_ref().map(|s| &**s),
+ Ok("Hello, (this space intentionally left blank)!"),
+ );
+ assert_eq!(
+ "Hello, {name}!"
+ .fill_to_string(&filler)
+ .as_ref()
+ .map(|s| &**s),
+ Ok("Hello, world!"),
+ );
+ assert_eq!(
+ "Hello, {you}!".fill_to_string(&filler),
+ Err(Error::BadReplacement {
+ key: "you",
+ range: 8..11,
+ error: SimpleFillerError::NoSuchKey,
+ }),
+ );
+ assert_eq!(
+ "I like {keys with SPACES!? 😱}"
+ .fill_to_string(&filler)
+ .as_ref()
+ .map(|s| &**s),
+ Ok("I like identifier-only keys 👌"),
+ );
+ }
+ };
+ }
+
+ #[cfg(feature = "alloc")]
+ test!(closure_filler, |out: &mut String, key: &str| {
+ use core::fmt::Write;
+ out.write_str(match key {
+ "" => "(this space intentionally left blank)",
+ "name" => "world",
+ "keys with SPACES!? 😱" => "identifier-only keys 👌",
+ _ => return Err(SimpleFillerError::NoSuchKey),
+ })
+ .map_err(Into::into)
+ });
+
+ #[cfg(feature = "std")]
+ test!(hash_map_fillter, {
+ [
+ ("", "(this space intentionally left blank)"),
+ ("name", "world"),
+ ("keys with SPACES!? 😱", "identifier-only keys 👌"),
+ ]
+ .into_iter()
+ .collect::<HashMap<_, _>>()
+ });
+
+ #[cfg(feature = "alloc")]
+ test!(btree_map_fillter, {
+ [
+ ("", "(this space intentionally left blank)"),
+ ("name", "world"),
+ ("keys with SPACES!? 😱", "identifier-only keys 👌"),
+ ]
+ .into_iter()
+ .collect::<BTreeMap<_, _>>()
+ });
+
+ #[test]
+ #[cfg(feature = "alloc")]
+ fn fill_errors() {
+ let c = |_: &mut String, _: &str| -> Result<(), ()> { Ok(()) };
+
+ assert_eq!(
+ fill_to_string("Hello, {thing", c),
+ Err(Error::UnclosedRegion {
+ source: "{thing",
+ range: 7..13
+ })
+ );
+ assert_eq!(
+ fill_to_string("{}/{x}/{xx}/{xxx}/{{/}}/{thing", c),
+ Err(Error::UnclosedRegion {
+ source: "{thing",
+ range: 24..30
+ })
+ );
+
+ assert_eq!(
+ fill_to_string("Hello, }thing", c),
+ Err(Error::UnexpectedClosingBrace { index: 7 })
+ );
+ assert_eq!(
+ fill_to_string("{}/{x}/{xx}/{xxx}/{{/}}/}thing", c),
+ Err(Error::UnexpectedClosingBrace { index: 24 })
+ );
+
+ assert_eq!(
+ fill_to_string("Hello, {thi{{ng}", c),
+ Err(Error::UnexpectedOpeningBrace { index: 11 })
+ );
+ assert_eq!(
+ fill_to_string("{}/{x}/{xx}/{xxx}/{{/}}/{x{", c),
+ Err(Error::UnexpectedOpeningBrace { index: 26 })
+ );
+
+ assert_eq!(
+ fill_to_string("Hello, {thi}}ng}", c),
+ Err(Error::UnexpectedClosingBrace { index: 12 })
+ );
+ assert_eq!(
+ fill_to_string("{}/{x}/{xx}/{xxx}/{{/}}/}", c),
+ Err(Error::UnexpectedClosingBrace { index: 24 })
+ );
+ }
+
+ // This is almost enough to make me only expose a dyn fmt::Writer.
+ #[test]
+ #[cfg(feature = "alloc")]
+ fn do_not_do_this_at_home_kids() {
+ // Whatever possessed me!?
+ let s = "Don’t{␡}{\b}{^H} do this at home, {who}!".fill_to_string(
+ |output: &mut String, key: &str| {
+ match key {
+ "␡" | "\b" | "^H" => {
+ output.pop();
+ }
+ "who" => {
+ output.push_str("kids");
+ }
+ _ => return Err(()),
+ }
+ Ok(())
+ },
+ );
+ assert_eq!(s.unwrap(), "Do do this at home, kids!");
+
+ // I haven’t yet decided whether this is better or worse than the previous one.
+ let s = "Don’t yell at {who}!{←make ASCII uppercase} (Please.)".fill_to_string(
+ |output: &mut String, key: &str| {
+ match key {
+ "←make ASCII uppercase" => {
+ output.make_ascii_uppercase();
+ }
+ "who" => {
+ output.push_str("me");
+ }
+ _ => return Err(()),
+ }
+ Ok(())
+ },
+ );
+ assert_eq!(s.unwrap(), "DON’T YELL AT ME! (Please.)");
+ }
+}