#template #format

no-std human-string-filler

A tiny template language for human-friendly string substitutions

1 stable release

Uses new Rust 2021

1.0.0 Jan 6, 2022

#68 in Template engine

37 downloads per month
Used in sbrd-gen

BlueOak-1.0.0 OR MIT OR Apache-2.0

40KB
458 lines

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:

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):

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:

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:

^([^{}]|\{\{|\}\}|\{[^{}]*\})*$

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 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 (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:

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.

No runtime deps

Features

  • alloc
  • std