λtext logo

λtext

General-Purpose Text-File Templating Utility

Reduce Duplication

λtext is a generic templating tool for any text file. For languages that don't support import-style code-reuse, it's common to just copy/paste. However, after a change is needed, that change must be replicated manually to every file needing it. With λtext, you parameterize the files themselves, and let λtext do the pasting.

Built for Precision

With Lambda Calculus as the underlying engine, λtext features both generality and precision - its Hindley-Milner type system gives us both polymorphism and correctness; generic templates that seamlessly fit.

Open Source

λtext is BSD licensed - you are free to do what you want with the project. It's a single, dependency-free binary written in Rust, with its code available on GitHub.

How It Works

1. Have a Text File

You will first need a text file to parameterize. This can be Html, JavaScript, Haskell, Bash, LaTeX, anything. In our example, we'll use a plaintext file foo:

Woo hoo! I'm a Text File!

2. Create a Parameter

Just like lambda calculus, we create arity with abstraction - the act of creating a parameter. In λtext, we need to declare our parameters on the first line, inside delimiters. Those parameters are later used within the same delimiters. We are free to choose whatever syntax we want - Mustache-style delimiters like {{ ... }}, Html-friendly delimiters via comments <!-- -->, etc. Here's an example parametric foo:

$$ x %%
Woo hoo! I'm a Text File!

$$ x %%

3. Use Another File as Input

Now that we have our function/file foo :: Text -> Text, we need some content to plug-in. Here's another file bar:


Here's some content that I /really/
want inside `foo`!

Note the empty first line - we don't want ltext to think some of our words are actually delimiters or parameters. Optionally, we could designate this file as "raw" with the --raw "bar" flag.

4. Apply the Files/Functions with λtext

To use our function/files, we must apply them to one another - application is the other counterpart to lambda calculus. To do this, invoke the ltext executable from a terminal:

$ ltext "foo bar"

Woo hoo! I'm a Text File!


Here's some content that I /really/
want inside `foo`!

The result expression / file text gets pumped out stdout.

Get λtext

Latest release: v0.2.2 · all releases and checksums

macOS

Package managers

Homebrew
brew install ltext/tap/ltext

Downloads

macOS may quarantine a downloaded binary; xattr -d com.apple.quarantine ltext clears that.

Windows

Downloads

A single standalone executable: rename it to ltext.exe and put it somewhere on your PATH.


or, on any platform

Nix

Run it straight from the flake:

nix run github:ltext/ltext -- --help

or install it into your profile:

nix profile add github:ltext/ltext

Container

A ~1 MB image (amd64, arm64) that renders files in the current directory:

docker run --rm -v "$PWD:/work" ghcr.io/ltext/ltext "foo bar"

or copy it into your own image:

COPY --from=ghcr.io/ltext/ltext /usr/local/bin/ltext /usr/local/bin/

Build From Source

λtext needs Rust 1.85 or newer.

git clone https://github.com/ltext/ltext
cd ltext
cargo install --path .

Advanced Usage

There are a few other things to consider when working with λtext - type checking, partial application, and... debugging! *gasp*

Type Checking

To inspect the type of a file or an expression, just use the -t option during invocation:

$ ltext --type "foo"

Text -> Text

You can read this as "foo takes in Text and returns Text", or "foo is a function from Text to Text". This is useful if you are getting type matching errors, or want to get a description of a file. We can also check the type signature of expressions directly:

$ ltext --type "\x -> x"

a0 -> a0

This is a little technical, but we are creating a polymorphic function \x -> x, which has a type from for all a0 to a0 (you may see these cryptic type variables instead of Text from time to time). This would happen if, for instance, you made an "apply"-like template:

%% f x %%

asdf

%% f x %%

If you check the type of apply, you can see it's...

$ ltext --type "apply"

(a0 -> Text) -> a0 -> Text

Meaning that x can be content of any type, so long as it's the same input as f.

Partial Application

Just like lambda calculus, we can partially apply templates to each other. Take our apply template from before - that's a good example to work though. If we apply our foo file from before to apply, we should get a template from Text -> Text - the parameter to foo being unified with the x parameter in apply:

$ ltext --type "apply foo"

Text -> Text

It turns out, we can actually render this partially applied template, but only if we explicitly supply new delimiters for the result's header and parameters - without them, λtext refuses:

$ ltext "apply foo"
[Print Error] Can't render a document with arity without explicit --left and --right delimiters

With delimiters, the result is itself a template, ready to be applied later:

$ ltext "apply foo" --left "{{" --right "}}"

{{ x }}

asdf

Woo hoo! I'm a Text File!

{{ x }}

Debugging

If you toyed around with the apply function from earlier, you may have seen a daunting error:

$ ltext "\f -> apply f bar"
[Print Error] Can't print textual data while inside an expression:

    f [text from "bar"]

...cannot be rendered to a file.

This is intended! This makes sure that the templates we render are actually renderable.

In layman's terms, a template can only be rendered if all of its text ends up concatenated at the "top level". Here, after reducing the expression, the text of bar is still waiting to be passed to f - a function we don't know yet - so there's no way to write it out as a file.

We can still check the type of such an expression with --type, but to render it, we have to apply whatever is still missing first.

Other mistakes are caught before anything is rendered, too. Applying a file to the wrong kind of argument is a type error, a misspelled file name is an unbound variable, and a malformed expression - in a file's header or on the command line - is a parse error that names the file, line and expression at fault. Add --verbose to see how λtext reads each file.