hare

[hare] The Hare programming language
git clone https://git.torresjrjr.com/hare.git
Log | Files | Refs | README | LICENSE

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 };