hare

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

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