commit fb1475c08abb8f8b19c586df166266bc69f9288b
parent bd1c1c4aa0dbcb7a01e9689b52c1761d80dcf60f
Author: Drew DeVault <sir@cmpwn.com>
Date: Sun, 17 Jul 2022 15:04:01 +0200
strings::template: new module
Diffstat:
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);
+};