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