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