hare

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

commit fb1475c08abb8f8b19c586df166266bc69f9288b
parent bd1c1c4aa0dbcb7a01e9689b52c1761d80dcf60f
Author: Drew DeVault <sir@cmpwn.com>
Date:   Sun, 17 Jul 2022 15:04:01 +0200

strings::template: new module

Diffstat:
Mscripts/gen-stdlib | 7+++++++
Mstdlib.mk | 32++++++++++++++++++++++++++++++++
Astrings/template/README | 15+++++++++++++++
Astrings/template/template.ha | 163+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
4 files changed, 217 insertions(+), 0 deletions(-)

diff --git a/scripts/gen-stdlib b/scripts/gen-stdlib @@ -1259,6 +1259,12 @@ strings() { gen_ssa strings bytes encoding::utf8 types } +strings_template() { + gen_srcs strings::template \ + template.ha + gen_ssa strings::template ascii errors fmt io strings strio +} + strio() { gen_srcs strio \ stream.ha \ @@ -1498,6 +1504,7 @@ slices sort strconv strings +strings::template strio temp linux freebsd time linux freebsd diff --git a/stdlib.mk b/stdlib.mk @@ -642,6 +642,12 @@ stdlib_deps_any += $(stdlib_strings_any) stdlib_strings_linux = $(stdlib_strings_any) stdlib_strings_freebsd = $(stdlib_strings_any) +# gen_lib strings::template (any) +stdlib_strings_template_any = $(HARECACHE)/strings/template/strings_template-any.o +stdlib_deps_any += $(stdlib_strings_template_any) +stdlib_strings_template_linux = $(stdlib_strings_template_any) +stdlib_strings_template_freebsd = $(stdlib_strings_template_any) + # gen_lib strio (any) stdlib_strio_any = $(HARECACHE)/strio/strio-any.o stdlib_deps_any += $(stdlib_strio_any) @@ -1893,6 +1899,16 @@ $(HARECACHE)/strings/strings-any.ssa: $(stdlib_strings_any_srcs) $(stdlib_rt) $( @HARECACHE=$(HARECACHE) $(HAREC) $(HAREFLAGS) -o $@ -Nstrings \ -t$(HARECACHE)/strings/strings.td $(stdlib_strings_any_srcs) +# strings::template (+any) +stdlib_strings_template_any_srcs = \ + $(STDLIB)/strings/template/template.ha + +$(HARECACHE)/strings/template/strings_template-any.ssa: $(stdlib_strings_template_any_srcs) $(stdlib_rt) $(stdlib_ascii_$(PLATFORM)) $(stdlib_errors_$(PLATFORM)) $(stdlib_fmt_$(PLATFORM)) $(stdlib_io_$(PLATFORM)) $(stdlib_strings_$(PLATFORM)) $(stdlib_strio_$(PLATFORM)) + @printf 'HAREC \t$@\n' + @mkdir -p $(HARECACHE)/strings/template + @HARECACHE=$(HARECACHE) $(HAREC) $(HAREFLAGS) -o $@ -Nstrings::template \ + -t$(HARECACHE)/strings/template/strings_template.td $(stdlib_strings_template_any_srcs) + # strio (+any) stdlib_strio_any_srcs = \ $(STDLIB)/strio/stream.ha \ @@ -2794,6 +2810,12 @@ testlib_deps_any += $(testlib_strings_any) testlib_strings_linux = $(testlib_strings_any) testlib_strings_freebsd = $(testlib_strings_any) +# gen_lib strings::template (any) +testlib_strings_template_any = $(TESTCACHE)/strings/template/strings_template-any.o +testlib_deps_any += $(testlib_strings_template_any) +testlib_strings_template_linux = $(testlib_strings_template_any) +testlib_strings_template_freebsd = $(testlib_strings_template_any) + # gen_lib strio (any) testlib_strio_any = $(TESTCACHE)/strio/strio-any.o testlib_deps_any += $(testlib_strio_any) @@ -4098,6 +4120,16 @@ $(TESTCACHE)/strings/strings-any.ssa: $(testlib_strings_any_srcs) $(testlib_rt) @HARECACHE=$(TESTCACHE) $(HAREC) $(TESTHAREFLAGS) -o $@ -Nstrings \ -t$(TESTCACHE)/strings/strings.td $(testlib_strings_any_srcs) +# strings::template (+any) +testlib_strings_template_any_srcs = \ + $(STDLIB)/strings/template/template.ha + +$(TESTCACHE)/strings/template/strings_template-any.ssa: $(testlib_strings_template_any_srcs) $(testlib_rt) $(testlib_ascii_$(PLATFORM)) $(testlib_errors_$(PLATFORM)) $(testlib_fmt_$(PLATFORM)) $(testlib_io_$(PLATFORM)) $(testlib_strings_$(PLATFORM)) $(testlib_strio_$(PLATFORM)) + @printf 'HAREC \t$@\n' + @mkdir -p $(TESTCACHE)/strings/template + @HARECACHE=$(TESTCACHE) $(HAREC) $(TESTHAREFLAGS) -o $@ -Nstrings::template \ + -t$(TESTCACHE)/strings/template/strings_template.td $(testlib_strings_template_any_srcs) + # strio (+any) testlib_strio_any_srcs = \ $(STDLIB)/strio/stream.ha \ diff --git a/strings/template/README b/strings/template/README @@ -0,0 +1,15 @@ +This module provides support for formatting of large or complex strings beyond +the scope of [[fmt]]. A template is compiled using [[compile]], then executed +with [[execute]] to print formatted text to an [[io::handle]]. + +The template format is a string with variable substituted using "$". Variable +names must be alphanumeric ASCII characters (i.e. for which [[ascii::isalnum]] +returns true). A literal "$" may be printed by using it twice: "$$". + + const src = "Hello, $user! Your balance is $$$balance\n"; + const template = template::compile(src)!; + defer template::finish(&template); + template::execute(&template, os::stdout, + ("user", "ddevault"), + ("balance", "1000"), + )!; // "Hello, ddevault! Your balance is $1000. diff --git a/strings/template/template.ha b/strings/template/template.ha @@ -0,0 +1,163 @@ +// License: MPL-2.0 +// (c) 2022 Drew DeVault <sir@cmpwn.com> +// TODO: Add ${whatever:x}-style formatting +use ascii; +use errors; +use fmt; +use io; +use strings; +use strio; + +export type literal = str; +export type variable = str; +export type instruction = (literal | variable); +export type template = []instruction; + +// Parameters to execute with a template, a tuple of a variable name and a +// formattable object. +export type param = (str, fmt::formattable); + +// Compiles a template string. The return value must be freed with [[finish]] +// after use. +export fn compile(input: str) (template | errors::invalid) = { + let buf = strio::dynamic(); + defer io::close(&buf)!; + + let instrs: []instruction = []; + const iter = strings::iter(input); + for (true) { + const rn = match (strings::next(&iter)) { + case void => + break; + case let rn: rune => + yield rn; + }; + + if (rn == '$') { + const lit = strio::string(&buf); + append(instrs, strings::dup(lit): literal); + strio::reset(&buf); + + parse_variable(&instrs, &iter, &buf)?; + } else { + strio::appendrune(&buf, rn)!; + }; + }; + + if (len(strio::string(&buf)) != 0) { + const lit = strio::string(&buf); + append(instrs, strings::dup(lit): literal); + }; + + return instrs; +}; + +// Frees resources associated with a [[template]]. +export fn finish(tmpl: *template) void = { + for (let i = 0z; i < len(tmpl); i += 1) { + match (tmpl[i]) { + case let lit: literal => + free(lit); + case let var: variable => + free(var); + }; + }; + free(*tmpl); +}; + +// Executes a template, writing the output to the given [[io::handle]]. If the +// template calls for a parameter which is not provided, an assertion will be +// fired. +export fn execute( + tmpl: *template, + out: io::handle, + params: param... +) (size | io::error) = { + let z = 0z; + for (let i = 0z; i < len(tmpl); i += 1) { + match (tmpl[i]) { + case let lit: literal => + z += fmt::fprint(out, lit)?; + case let var: variable => + const value = get_param(var, params...); + z += fmt::fprint(out, value)?; + }; + }; + return z; +}; + +fn get_param(name: str, params: param...) fmt::formattable = { + // TODO: Consider preparing a parameter map or something + for (let i = 0z; i < len(params); i += 1) { + if (params[i].0 == name) { + return params[i].1; + }; + }; + abort("strings::tmpl: required parameter was not provided"); +}; + +fn parse_variable( + instrs: *[]instruction, + iter: *strings::iterator, + buf: *strio::stream, +) (void | errors::invalid) = { + const rn = match (strings::next(iter)) { + case let rn: rune => + if (rn == '$') { + append(instrs, strings::dup("$"): literal); + return; + }; + yield rn; + case => + return errors::invalid; + }; + strio::appendrune(buf, rn)!; + + for (true) { + const rn = match (strings::next(iter)) { + case let rn: rune => + yield rn; + case => + return errors::invalid; + }; + + if (ascii::isalnum(rn)) { + strio::appendrune(buf, rn)!; + } else { + strings::prev(iter); + break; + }; + }; + + const var = strio::string(buf); + append(instrs, strings::dup(var): variable); + strio::reset(buf); +}; + +def test_input: str = `Dear $recipient, + +I am the crown prince of $country. Your brother, $brother, has recently passed +away in my country. I am writing to you to facilitate the transfer of his +foreign bank account balance of $$1,000,000 to you.`; + +def test_output: str = `Dear Mrs. Johnson, + +I am the crown prince of South Africa. Your brother, Elon Musk, has recently passed +away in my country. I am writing to you to facilitate the transfer of his +foreign bank account balance of $1,000,000 to you.`; + +@test fn template() void = { + const tmpl = compile(test_input)!; + defer finish(&tmpl); + + let buf = strio::dynamic(); + defer io::close(&buf)!; + + execute(&tmpl, &buf, + ("recipient", "Mrs. Johnson"), + ("country", "South Africa"), + ("brother", "Elon Musk"), + )!; + + assert(strio::string(&buf) == test_output); +};