1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186
|
// SPDX-License-Identifier: MPL-2.0
// (c) Hare authors <https://harelang.org>
use ascii;
use fmt;
use io;
use memio;
use strings;
export type literal = str;
export type variable = str;
export type instruction = (literal | variable);
export type template = []instruction;
// Parameters to execute with a template, a tuple of a variable name and a
// formattable object.
export type param = (str, fmt::formattable);
// The template string has an invalid format.
export type invalid = !void;
// Returns a human-friendly string for [[invalid]].
export fn strerror(err: invalid) str = "Template string has invalid format";
// Compiles a template string. The return value must be freed with [[finish]]
// after use.
export fn compile(input: str) (template | invalid | nomem) = {
let buf = memio::dynamic();
defer io::close(&buf)!;
let instrs: []instruction = [];
let ok = false;
defer if (!ok) finish(&instrs);
const iter = strings::iter(input);
for (let rn => strings::next(&iter)) {
if (rn == '$') {
match (strings::next(&iter)) {
case let next_rn: rune =>
if (next_rn == '$') {
memio::appendrune(&buf, rn)!;
} else {
strings::prev(&iter);
const lit = memio::string(&buf)!;
append(instrs, strings::dup(lit)?: literal)?;
memio::reset(&buf);
parse_variable(&instrs, &iter, &buf)?;
};
case =>
return invalid;
};
} else {
memio::appendrune(&buf, rn)!;
};
};
if (len(memio::string(&buf)!) != 0) {
const lit = memio::string(&buf)!;
append(instrs, strings::dup(lit)?: literal)?;
};
ok = true;
return instrs;
};
// Frees resources associated with a [[template]].
export fn finish(tmpl: *template) void = {
for (let instr .. *tmpl) {
match (instr) {
case let lit: literal =>
free(lit);
case let var: variable =>
free(var);
};
};
free(*tmpl);
};
// Executes a template, writing the output to the given [[io::handle]]. If the
// template calls for a parameter which is not provided, an assertion will be
// fired.
export fn execute(
tmpl: *template,
out: io::handle,
params: param...
) (size | io::error) = {
let z = 0z;
for (let instr .. *tmpl) {
match (instr) {
case let lit: literal =>
z += fmt::fprint(out, lit)?;
case let var: variable =>
const value = get_param(var, params...);
z += fmt::fprint(out, value)?;
};
};
return z;
};
fn get_param(name: str, params: param...) fmt::formattable = {
// TODO: Consider preparing a parameter map or something
for (let (var_name, obj) .. params) {
if (var_name == name) {
return obj;
};
};
fmt::errorfln("strings::template: required parameter ${} was not provided", name)!;
abort();
};
fn parse_variable(
instrs: *[]instruction,
iter: *strings::iterator,
buf: *memio::stream,
) (void | invalid | nomem) = {
let brace = false;
match (strings::next(iter)) {
case let rn: rune =>
if (rn == '{') {
brace = true;
} else {
strings::prev(iter);
};
case =>
return invalid;
};
for (true) {
const rn = match (strings::next(iter)) {
case let rn: rune =>
yield rn;
case =>
return invalid;
};
if (brace) {
if (rn == '{') {
return invalid;
} else if (rn != '}') {
memio::appendrune(buf, rn)!;
} else {
break;
};
} else {
if (ascii::isalnum(rn) || rn == '_') {
memio::appendrune(buf, rn)!;
} else {
strings::prev(iter);
break;
};
};
};
const var = memio::string(buf)!;
append(instrs, strings::dup(var)?: variable)?;
memio::reset(buf);
};
def test_input: str = `Dear ${recipient},
I am the crown prince of $country. Your brother, $brother, has recently passed
away in my country. I am writing to you to facilitate the transfer of his
foreign bank account balance of $$1,000,000 to you.`;
def test_output: str = `Dear Mrs. Johnson,
I am the crown prince of South Africa. Your brother, Elon Musk, has recently passed
away in my country. I am writing to you to facilitate the transfer of his
foreign bank account balance of $1,000,000 to you.`;
@test fn template() void = {
const tmpl = compile(test_input)!;
defer finish(&tmpl);
let buf = memio::dynamic();
defer io::close(&buf)!;
execute(&tmpl, &buf,
("recipient", "Mrs. Johnson"),
("country", "South Africa"),
("brother", "Elon Musk"),
)!;
assert(memio::string(&buf)! == test_output);
};
|