~ntgg/template

b8d22eaded30ebea341eb71a58ed9d643b6d7a8a — Noah Graff 3 months ago main
initial commit
7 files changed, 230 insertions(+), 0 deletions(-)

A .gitignore
A .gitmodules
A build.zig
A deps/mecha
A deps/zig-clap
A src/main.zig
A src/template.zig
A  => .gitignore +4 -0
@@ 1,4 @@
.DS*
*~
zig-cache/
zig-out/

A  => .gitmodules +6 -0
@@ 1,6 @@
[submodule "deps/zig-clap"]
	path = deps/zig-clap
	url = https://github.com/Hejsil/zig-clap.git
[submodule "deps/mecha"]
	path = deps/mecha
	url = https://github.com/Hejsil/mecha.git

A  => build.zig +38 -0
@@ 1,38 @@
const std = @import("std");

pub fn build(b: *std.build.Builder) void {
    // Standard target options allows the person running `zig build` to choose
    // what target to build for. Here we do not override the defaults, which
    // means any target is allowed, and the default is native. Other options
    // for restricting supported target set are available.
    const target = b.standardTargetOptions(.{});

    // Standard release options allow the person running `zig build` to select
    // between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall.
    const mode = b.standardReleaseOptions();

    const exe = b.addExecutable("template", "src/main.zig");
    exe.setTarget(target);
    exe.setBuildMode(mode);
    exe.addPackagePath("clap", "deps/zig-clap/clap.zig");
    exe.addPackagePath("mecha", "deps/mecha/mecha.zig");
    exe.install();

    const run_cmd = exe.run();
    run_cmd.step.dependOn(b.getInstallStep());
    if (b.args) |args| {
        run_cmd.addArgs(args);
    }

    const run_step = b.step("run", "Run the app");
    run_step.dependOn(&run_cmd.step);

    const exe_tests = b.addTest("src/main.zig");
    exe_tests.setTarget(target);
    exe_tests.addPackagePath("clap", "deps/zig-clap/clap.zig");
    exe_tests.addPackagePath("mecha", "deps/mecha/mecha.zig");
    exe_tests.setBuildMode(mode);

    const test_step = b.step("test", "Run unit tests");
    test_step.dependOn(&exe_tests.step);
}

A  => deps/mecha +1 -0
@@ 1,1 @@
Subproject commit 55e751aa1c9dffe8a1ecc1bc2688eda97b00f216

A  => deps/zig-clap +1 -0
@@ 1,1 @@
Subproject commit 0970eb827fe53ad7a6c6744019707190d7b9bb32

A  => src/main.zig +77 -0
@@ 1,77 @@
const std = @import("std");
const clap = @import("clap");
const mecha = @import("mecha");
const template = @import("template.zig");

const max_bytes = std.math.pow(usize, std.mem.byte_size_in_bits, 10);

pub fn main() anyerror!void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();
    const allocator = gpa.allocator();

    const params = comptime clap.parseParamsComptime(
        \\-h, --help              Display usage and exit.
        \\-s, --substitute <STR>...  Replace key with value, must be in format key=value.
        \\
    );

    const parsers = comptime .{
        .STR = clap.parsers.string,
    };

    var diagnostics = clap.Diagnostic{};
    var result = clap.parse(clap.Help, &params, parsers, .{
        .diagnostic = &diagnostics,
    }) catch |err| {
        diagnostics.report(std.io.getStdErr().writer(), err) catch {};
        return err;
    };
    defer result.deinit();

    if (result.args.help) {
        return clap.help(std.io.getStdErr().writer(), clap.Help, &params);
    }

    var substitutions = std.StringHashMap([]const u8).init(allocator);
    defer substitutions.deinit();
    for (result.args.substitute) |kv| {
        const parsed = try template.identifier(allocator, kv);
        const identifier = parsed.value;
        const value = (try mecha.utf8.char('=')(allocator, parsed.rest)).rest;
        var get_or_put = try substitutions.getOrPut(identifier);
        if (get_or_put.found_existing) {
            try std.io.getStdErr().writer().print(
                "multiple substitutions with the same key ({s}) found!",
                .{identifier},
            );
            std.process.exit(1);
        } else {
            get_or_put.value_ptr.* = value;
        }
    }

    const content = init: {
        var in = std.io.getStdIn();
        break :init try in.readToEndAlloc(allocator, max_bytes);
    };
    defer allocator.free(content);
    const out = std.io.getStdOut();
    const substituted = template.substitute(
        allocator,
        content,
        substitutions,
    ) catch |err| {
        if (err == error.MissingSubstitution) {
            std.process.exit(1);
        } else {
            return err;
        }
    };
    defer allocator.free(substituted);
    try out.writeAll(substituted);
}

test {
    std.testing.refAllDecls(@This());
}

A  => src/template.zig +103 -0
@@ 1,103 @@
const std = @import("std");
const mecha = @import("mecha");

const whitespace_char = mecha.oneOf(.{
    // space
    mecha.utf8.char('\u{0020}'),
    // tab, line feed, line tab, form feed, carriage return
    mecha.discard(mecha.utf8.range('\u{0009}', '\u{000D}')),
    // next line
    mecha.utf8.char('\u{0085}'),
    // no-break space
    mecha.utf8.char('\u{00A0}'),
    // ogham space mark
    mecha.utf8.char('\u{1680}'),
    // en quad, em quad, en space, em space,
    // three-per-em space, four-per-em space,
    // six-per-em space, figure space,
    // punctuation space, thin space, hair
    // space
    mecha.discard(mecha.utf8.range('\u{2000}', '\u{200A}')),
    // line separator
    mecha.utf8.char('\u{2028}'),
    // paragraph separator
    mecha.utf8.char('\u{2029}'),
    // narrow no-break space
    mecha.utf8.char('\u{202F}'),
    // medium mathematical space
    mecha.utf8.char('\u{205F}'),
    // ideographic space
    mecha.utf8.char('\u{3000}'),
});
const whitespace = mecha.discard(mecha.many(whitespace_char, .{ .collect = false }));
const open = mecha.string("{%");
const close = mecha.string("%}");
const identifier_char = mecha.discard(mecha.utf8.not(mecha.oneOf(.{
    mecha.utf8.char('='),
    whitespace_char,
    close,
})));
pub const identifier = mecha.many(identifier_char, .{ .collect = false, .min = 1 });
pub const substitution = mecha.combine(.{
    open,
    whitespace,
    identifier,
    whitespace,
    close,
});

pub fn substitute(
    allocator: std.mem.Allocator,
    input: []const u8,
    substitutions: std.StringHashMap([]const u8),
) ![]const u8 {
    var output = std.ArrayListUnmanaged(u8){};
    errdefer output.deinit(allocator);
    var to_process = input;
    while (to_process.len > 0) {
        const maybe_substitute = try mecha.opt(substitution)(allocator, to_process);
        if (maybe_substitute.value) |key| {
            const replace_with = substitutions.get(key) orelse {
                std.io.getStdErr().writer().print("missing substitution for \"{s}\"\n", .{key}) catch {};
                return error.MissingSubstitution;
            };
            try output.appendSlice(allocator, replace_with);
            to_process = maybe_substitute.rest;
        } else {
            try output.append(allocator, to_process[0]);
            to_process = to_process[1..];
        }
    }
    return output.toOwnedSlice(allocator);
}

test "substitute" {
    const allocator = std.testing.allocator;

    var substitutions = std.StringHashMap([]const u8).init(allocator);
    defer substitutions.deinit();
    try substitutions.putNoClobber("just-a-substitution", "substituted");
    try substitutions.putNoClobber("just-b-substitution", "substituted too");

    const in1 = "";
    const out1 = "";
    const r1 = try substitute(allocator, in1, substitutions);
    defer allocator.free(r1);
    const in2 = "no substitutions in this one!";
    const out2 = "no substitutions in this one!";
    const r2 = try substitute(allocator, in2, substitutions);
    defer allocator.free(r2);
    const in3 = "{% just-a-substitution %}";
    const out3 = "substituted";
    const r3 = try substitute(allocator, in3, substitutions);
    defer allocator.free(r3);
    const in4 = "some stuff {% just-a-substitution %} and some more stuff {% just-b-substitution %} and a last little bit";
    const out4 = "some stuff substituted and some more stuff substituted too and a last little bit";
    const r4 = try substitute(allocator, in4, substitutions);
    defer allocator.free(r4);

    try std.testing.expectEqualStrings(out1, r1);
    try std.testing.expectEqualStrings(out2, r2);
    try std.testing.expectEqualStrings(out3, r3);
    try std.testing.expectEqualStrings(out4, r4);
}