hare

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

template.ha (3834B)


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