sajt

a tool and template system for constructing simple textual resources like personal websites or project documentation

sajt is single-binary zero-dependency static site generator taking .md, .gmi, .html, and plaintext files as input, passing each input file to a defined template file for transformations and outputs .html.

sajt's executable is around 990kB and the same file can be run on most modern operating systems (osx, linux, windows, *BSD) with either AMD64 or ARM64 architectures.

The template syntax is small, comprised of 8 commands. The commands are focused to enable transformation operations useful for documentation websites and other types of static websites.

sajt can be called in two different modalities: in standalone mode and in a config-based mode.

Standalone: As a standalone cli that applies a single template file to a set of input files, saving output in a specified directory.

./sajt.com --template ./simple-html.sajt.html --out index.html ./black-lentil-daal-daal-makhani.md

Config-based: A complete website is described by a config file (TOML). Different sets of input files can use different template files. It can also copy over any required extras (stylesheets, images, etc) to the different destination directories. See the configuration section for more information.

./sajt.com --config config.toml

Goals

Simple syntax

The intention is to separate content files from templates in as simple a manner as possible. This is enabled by providing a tight set of commands, for fast iteration & creation.

For simple sites

Striving to cover the needs of small projects, personal websites, and tool documentation, sajt tries to strike a balance between being featureful while being small enough to understand quickly. This is not a full-blown CMS, nor is it a complex pipeline for nesting together thousands of different files; it is a simple way to transform content files into websites (or other files) by way of user-authored templates.

Emphasizing portability & ease of use

Public documentation sites often have a development pipeline which must be maintained in order to transform fixes and improvements from drive-by volunteers into the final website.

Instead of choosing a route that forces volunteers to understand and install a complicated set of development tools, or permanently tie your project to a resource intensive continuous integration system, sajt provides an alternative. A small and portable binary that can be included directly in a project's repository, possible to run by contributors comfortable enough with the terminal to type:
sajt.com --config config.toml.

Inspirations

sajt takes inspiration from its predecessors—mustache, ctemplate, et—while striving to minimize added complexity. It goes further to combine its syntax ideas with a tool offering reminiscent of static site generator systems like Hugo but with radically fewer features.

Configuration

For the configuration settings and format, see config.html.

Malleability

This software can be changed after the fact without needing to be recompiled. The binary's structure allows its contents to be zipped out, edited, and zipped back in again.

Read more about sajt's malleability.

Example

The documentation for sajt is built using sajt. Here's what that looks like.

As input files we use the markdown files of sajt's repository: README, config-docs, and malleability.

Template

The template website.sajt.html has the following contents:

<!DOCTYPE html>
${set page ${OUTPUT_NAME}}
${set title sajt / ${replace page /%.html/ }}
${set if_index ${equal page index.html => index }}
<html>
    <head>
        <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1">
        <meta charset="utf8">
        <link rel="stylesheet" href="links/style.css">
        ${comment if index.html -> just display "sajt". otherwise display sajt / *page name*}
        <title>${pick if_index : sajt title}</title>
    </head>
    <body>
        <header>
            <div id="logo">sajt</div>
            <nav>
                <ul>
                    <li><a href="./">readme</a></li>
                    <li><a href="config.html">config</a></li>
                    <li><a href="malleability.html">malleability</a></li>
                </ul>
            </nav>
        </header>
        <main>
${CONTENT}
        </main>
    </body>
</html>

Config

The config below is saved as config.toml.

[general]
root = "./"
dirFiles = "/home/cblgh/code/sajt/"
dirOutput = "dist"
template = "website.sajt.html"
extras = ["links/"]
suffix = "html"

[website]
replace = { README%.md = "./", config%-docs%.md = "config.html", malleability%.md = "malleability.html" }
files = ["./", "config.html", "malleability.html"]
rename = { ./ = "index.html", config.html = "config.html"}

Running sajt.com --config config.toml turns the markdown files into sajt's documentation website.

Commands

The syntax is succinct and familiar to many, presenting itself as:

${COMMAND}

Where COMMAND starts with one of the 8 commands listed below.

The syntax ${} is used to delineate commands from text otherwise present in template or content files. Most commands have one or more arguments. A command may, as one of its arguments, nest additional ${COMMAND} invocations.

set get paste equal
pick replace map comment

The dictionary of internal machinations

Internally sajt maintains a dictionary of keys mapping to values as a means to store and reference template variables. For most commands, if any of the operands corresponds to a key in the dictionary, then the dictionary value is what is operated on. If there is no matching key, then the string content of the key is what the command operates on.

Note: commands interpret any operands evaluating to the empty string ('') as false.

Set

${set key value}

set associates a key with a value. The value operand may be a variable, in which case the variable's value is what the key will reference.

Get

${var}

The simplest command, get returns the associated value if var is a key in the dictionary. If var is not a key, then the key itself is returned as a string. Get is the only command without an explicit command name.

Paste

${paste path-to-file}

paste attempts to load a file from disk and include its entire text at the point of the paste command.

All paste commands are evaluated before processing any other commands. This applies to paste commands referenced by other paste commands as well, for a depth of up to three levels of indirection.

path-to-file is relative to the directory of the current template file as:

path.join(<template-file-dir>, <path-to-file>)

by default, paste doesn't interpret the file being pasted, instead opting to include it verbatim. if you want to interpret the file in any of sajt's supported file formats (currently .md, .gmi) then you can use the syntax:

${paste with-parse path-to-file.<ext>}

paste also has an optional base directory that can be set:

${paste path-to-filename, with-alt-base-dir}

This second alternative makes path-to-file relative to with-alt-base-dir instead of template-file-dir. In addition to strings, the base directory may be stored in template variables known at runtime—either set as a config dictionary key or the template variables listed in the configuration's section *Template variables). For example:

{paste myfile.txt, INPUT_DIRECTORY}

Equal

${equal arg1 arg2 => arg3}

equal evaluates the two first operands, and if they are equal to each other, returns the third operand. If the evaluation is false, the empty string is returned.

Pick

${pick arg1 : arg2 arg3}

pick evaluates the first argument and conditionally returns one of the other two arguments. If arg1 is a non-zero length string, then arg2 is returned. If arg1 is the empty string, then arg3 is returned. To those familiar, it is a conditional ternary operator.

Replace

${replace arg /pattern/ cmd}

replace operates on its input string arg and selects regions of it using pattern. The final operand, cmd, is a command mode that determines the output of the replacement operation on the selected region.

Current command modes are: upper, keep, <string>.

Note: <pattern> is a matching pattern from Lua). The following "magic characters" need to be escaped: ^$()%.[]*+-?. To escape a magic character, for example -, preface the character with %: %-

Note: If possible, <pattern> will be interpreted as the contents of a variable referenced by the name <pattern>. Failing to find a variable by that name, <pattern> is interpreted as a text string expressing a Lua matching pattern.

Note: split allows splitting an input file by separators and operating the files different parts -- we use a separator of ::: inside the content file.

Content:

# title 1

section text section text section text section text section text section text, lots of it.

:::

## title 2

after the separator: more text more text more text more text more text more text more text!

Template:

<html>
    <head>
    <link type="text/css" rel="stylesheet" href="/links/style.css">
    </head>
    <body>
    ${set parts ${replace CONTENT /:::/ split}}

    <article>${parts[1]}</article>
    <div class="cool-element">insert some visual flair to break up the text</div>
    <article>${parts[2]}</article>
    </body>
</html>

Map

${map <list> <line or lines of input, referencing other commands and/or ${ITEM}>}

map iterates over the contents of a stored variable whose value is a comma-separated list (${set list a, b, c, etc}). In each iteration, the ephemeral dictionary key ITEM is set to the currently iterated variable value and may be used in the block.

After map has finished evaluation, the result is the concatenation of all evaluated blocks.

Example:

${set mylist one, two, three}
${map mylist here is item ${ITEM}. the first letter is: ${replace ITEM /./ keep}.
}

Output:

here is item one. the first letter is: o.
here is item two. the first letter is: t.
here is item three. the first letter is: t.

Note: the closing brace sitting on its own line gives us a newline separating each evaluated block.

Comment

${comment setting a list like this, using comma-separated values, lets us use the map command on the key}
${set mylist one, two, three}

comment lets you comment your template files with reminders or instructions which will not appear in the output.

Future work

empty

Known issues

Markdown

Discovered issues can, and hopefully will, be mitigated by improving the markdown.lua parser.

Paths