Skip to content

Instantly share code, notes, and snippets.

@decatur
Last active September 26, 2025 15:16
Show Gist options
  • Save decatur/0c5e27f1b64ce15219b6e791727e9ede to your computer and use it in GitHub Desktop.
Save decatur/0c5e27f1b64ce15219b6e791727e9ede to your computer and use it in GitHub Desktop.
Rust minimal template engine
/// Minimal template string substitution in 60 lines of code.
/// * No dependencies
/// * Single pass
///
/// Possible improvements: Write more tests.
#[cfg(test)]
mod tests {
use crate::fill_template;
use std::collections::HashMap;
#[test]
fn basic() {
let m = HashMap::from([("foo".to_owned(), "bar".to_owned())]);
let s = fill_template("Hello {{foo}} World", &m);
assert_eq!(s, "Hello bar World");
}
#[test]
fn trim_key() {
let m = HashMap::from([("foo".to_owned(), "bar".to_owned())]);
let s = fill_template("Hello {{ foo }} World", &m);
assert_eq!(s, "Hello bar World");
}
#[test]
fn key_in_curlies() {
let m = HashMap::from([("foo".to_owned(), "bar".to_owned())]);
let s = fill_template("Hello {{{foo}}} World", &m);
assert_eq!(s, "Hello {bar} World");
}
#[test]
fn partial_keys_a() {
let m = HashMap::from([("foo".to_owned(), "bar".to_owned())]);
let s = fill_template("Hello {{ foo} World {{foo}", &m);
assert_eq!(s, "Hello {{ foo} World {{foo}");
}
#[test]
fn partial_keys_b() {
let m = HashMap::from([("foo".to_owned(), "bar".to_owned())]);
let s = fill_template("Hello {{ foo World {{foo", &m);
assert_eq!(s, "Hello {{ foo World {{foo");
}
#[test]
fn partial_keys_c() {
let m = HashMap::from([("foo".to_owned(), "bar".to_owned())]);
let s = fill_template("Hello { {{foo}} World {{foo", &m);
assert_eq!(s, "Hello { bar World {{foo");
}
#[test]
fn empty_key() {
let m = HashMap::from([("".to_owned(), "empty key".to_owned())]);
let s = fill_template("Hello {{ }} World", &m);
assert_eq!(s, "Hello empty key World");
}
#[test]
fn fuzzy() {
let m = HashMap::new();
let s = fill_template("Hello, { foo { } {} {{FOO}} Rust!", &m);
assert_eq!(s, "Hello, { foo { } {} {{FOO}} Rust!");
}
}
pub fn fill_template(template: &str, m: &std::collections::HashMap<String, String>) -> String {
enum State {
Text,
SingleOpened,
DoubleOpened,
KeyStarted(usize),
SingleClosed(usize),
}
let mut state = State::Text;
let mut s = "".to_owned();
for (i, c) in template.chars().enumerate() {
state = match (state, c) {
(State::Text, '{') => State::SingleOpened,
(State::SingleOpened, '{') => State::DoubleOpened,
(State::DoubleOpened, '{') => {
s.push('{');
State::DoubleOpened
}
(State::KeyStarted(key_index), '{') => {
s.push_str(&template[key_index - 2..i]);
State::SingleOpened
}
(State::KeyStarted(key_index), '}') => State::SingleClosed(key_index),
(State::SingleClosed(key_index), '}') => {
let key = &template[key_index..i - 1];
if let Some(v) = m.get(key.trim()) {
s.push_str(&v);
} else {
s.push_str(&template[key_index - 2..i + 1]);
}
State::Text
}
(State::SingleClosed(key_index), _c) => {
s.push_str(&template[key_index - 2..i + 1]);
State::Text
}
(State::SingleOpened, c) => {
s.push('{');
s.push(c);
State::Text
}
(State::DoubleOpened, _) => State::KeyStarted(i),
(State::KeyStarted(key_index), _) => State::KeyStarted(key_index),
(_, c) => {
s.push(c);
State::Text
}
};
}
// Handle trailing {{foo and {{foo}
match state {
State::KeyStarted(key_index) | State::SingleClosed(key_index) => {
s.push_str(&template[key_index - 2..]);
}
_ => (),
};
s
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment