Plet: The Complete Reference

1. Introduction

Plet is a flexible static site generator, web framework, and programming language. Static websites (or static web pages) are collections of HTML files and assets that are served exactly as they are stored on the web server. This provides better security, performance, and stability.

Warning: Plet is work in progress and may change at any time.

2. Getting started

2.1. Installation

2.1.1. Building from source

make clean all
2.1.1.1. Build options

The available build options are:

By default, the following options are enabled:

make UNICODE=1 GUMBO=1 IMAGEMAGICK=1 STATIC_MD4C=0 MUSL=0 all

2.2. Basic usage

In this section we will create a basic blog consisting of a sorted list of blog posts, and a page for each blog post.

First create a new empty directory, then open a terminal in that directory and type the following:

plet init

This will create an empty index.plet file in the directory. The directory containing index.plet will henceforth be referred to as the source root (SRC_ROOT in code).

index.plet is the entry script that Plet evaluates whenever you run plet build. Since the script is currently empty, the only thing that happens if you run plet build is that an empty dist directory is created in the source root.

2.2.1. Creating templates

We'll need to create three templates for our blog in a new directory called templates:

The templates/layout.plet.html template is defined as follows:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8"/>
    <title>My Blog</title>
  </head>
  <body>
    <header>
      <h1><a href="{'/' | link}">My Blog</a></h1>
    </header>
    <article>
      {CONTENT}
    </article>
  </body>
</html>

The template contains mostly HTML with two bits of Plet code surrounded by curly brackets:

The '/' | link expression makes use of the pipe operator (|) which is just another way of writing chained function calls. The expression a | b is equivalent to b(a), i.e. “apply the function b to argument a”. Thus we could have written link('/') instead, but the use of the pipe operator makes some expressions easier to read, especially in templates.

We'll use the layout template in templates/list.plet.html:

{LAYOUT = 'layout.plet.html'}
<p>Welcome to my blog.</p>
<ul>
  {for post in posts}
  <li>
    <a href="{post.link | link}">
      {post.title | h}
    </a>
    &ndash; Published {post.published | date('%Y-%m-%d')}
  </li>
  {end for}
</ul>

The first line selects a layout by assigning the relative path to the layout template to the special LAYOUT variable.

We then loop through an array of posts stored in the posts variable (we'll make sure that this variable has a value later) using the {for ITEM in ARRAY}....{end for} looping construct. We expect each post to be an object containing the properties link, title, and published (these are explained in the next section). The properties are accessed using dot notation, e.g. post.title. Aside from the previously used link-function, we also use two new functions:

The expression a | b(c) is equivalent to b(a, c), in fact the pipe operator can be used with any number of arguments, e.g. a | b(c, d, e) is the same as b(a, c, d, e).

The templates/post.plet.html template is a bit more simple:

{LAYOUT = 'layout.plet.html'}
<h1>{post.title | h}</h1>
{post.html | no_title | links | html}

This time we expect the variable post to contain an object with the properties title and html. The last line is an example of chaining multiple function calls using the pipe operator and is equivalent to {html(links(no_title(post.html)))}. post.html contains a syntax tree representing the content of the post that can be used for various transformations. no_title is one such transformation which simply removes the title (the first heading) from the document. links walks through the document and converts all internal links to absolute paths. Finally the html function converts the syntax tree to a raw HTML string that can be appended to the output.

2.2.2. Creating content

To get posts on our blog, we'll create a new directory called posts and add two Markdown files.

We'll call the first file first.md:

{
  published: '2021-04-10 12:00' | time,
}
# My first blog post
This is a blog post written using Markdown.

The first three lines (delimited by curly brackets) of the above Markdown file is the front matter. The front matter uses Plet object notation and is parsed and evaluated when we later load the file via the find_content function. Any property specified in the front matter will later be accessible in the file's content object. In this case we assign a timestamp (the time function converts an ISO 8601 formatted date string to a timestamp) to the published property so that we can display it in our post list template.

We'll create another file called second.md:

{
  published: '2021-04-11 12:00' | time,
}
# The second post
This is the second blog post.

We can easily link to the [first blog post](first.md).

In the second Markdown file we link to the first one via a relative path to first.md. This link will later be converted to an absolute path to the HTML document created from first.md.

2.2.3. Adding pages

At this point the directory structure of the source root should look like this:

We can now glue everything together in index.plet:

# We start by collecting all the Markdown files in the posts-directory
posts = list_content('posts', {suffix: '.md'}) | sort_by_desc(.published)
for post in posts
  # We give each post a link using its name (filename without extension):
  post.link = "posts/{post.name}/index.html"
  # Then we add a page to the site map using the post-template:
  add_page(post.link, 'templates/post.plet.html', {post: post})
  # And add a reverse path:
  add_reverse(post.path, post.link)
end for
# Finally we add the frontpage containing the list of blog posts:
add_page('index.html', 'templates/list.plet.html', {posts: posts})

The list_content function will look for files in the specified directory and return an array of content objects created from the matching files. We use the sort_by_desc function to sort the found posts in descending order by the their published-property (.prop is syntactic sugar for x => x.prop which is an anonymous function that accepts an object and returns the value of the prop property of that object).

The main purpose of index.plet is to add pages to the site map. One way of doing that is by using the add_page function. The add_page function accepts three arguments: 1) the destination file path (should end in .html for HTML files), 2) the source template, and 3) data for the template.

We also use add_reverse to connect the input Markdown file (post.path) to the output path (post.link). This is what makes it possible to have a link to first.md inside second.md.

2.2.4. Testing and deploying

To test our blog we can run:

plet serve

This starts a local web server on http://localhost:6500 which can be used to quickly check the output while developing templates or writing content. The page reloads automatically when changes are detected in the source files.

To build our blog we can run:

plet build

This creates the following structure in the dist directory:

We can copy the content of the dist directory to any web server in order to deploy the site.

3. Content management

3.1. Front matter

3.2. Finding content

list_content('pages', {suffix: '.md', recursive: true})

3.3. Content objects

{
  my_custom_field: 'foo',
}
# Title

Content
{
  path: '/home/user/mypletsite/pages/subdir/file.md',
  relative_path: 'subdir',
  name: 'file',
  type: 'md',
  modified: time('2021-04-28T18:06:13Z'),
  content: '<h1>Title</h1><p>Content</p>',
  html: {
    type: symbol('fragment'),
    children: [
      {
        type: symbol('element'),
        tag: symbol('h1'),
        attributes: {},
        children: ['Title'],
      },
      {
        type: symbol('element'),
        tag: symbol('p'),
        attributes: {},
        children: ['Content'],
      },
    ],
  },
  title: 'Title',
  read_more: false,
  my_custom_field: 'foo',
}

3.4. Relative paths

3.5. Handling images

3.6. Automatic table of contents

3.8. Pagination

3.9. Custom transformations

4. Templates and scripts

Templates are written using the Plet programming language. Plet is a high-level dynamically-typed imperative programming language. There are two types of Plet programs: Plet scripts and Plet templates. Plet scripts start out in command mode while Plet templates start out in text mode.

In text mode the only byte that has any special meaning is { (left curly bracket, U+007B) which enters command mode, or – if it's immediately followed by a # (U+0023) – enters comment mode. Comment mode can also be entered from within command mode with the same {# sequence. In comment mode the #} sequence exits to the most recent mode.

In command mode an unmatched } (right curly bracket, U+007D) will enter template mode. This means that any Plet template can be made into a Plet script by prepending a } and any Plet script can be made into a Plet template by prepending a {:

this is a valid Plet template
}this is a valid Plet script

In command mode a # that is not preceded by a { opens a single line comment:

# this is a single line comment
command()
{# this is a 
   multiline comment #}
command()

A Plet program is a sequence of text nodes and statements. A text node is a string of bytes consumed while in text mode, it may be empty. Two statements must be separated by at least one text node or newline (U+000A).

command()
command()
{command()}{command()}

Some statement can contain multiple nested statements and text nodes. The top-level text nodes and statements are not contained within another statement.

The return value of a Plet program – unless otherwise specified using the return keyword – is the byte string resulting from concatenating all top-level text nodes with the values resulting from evaluating all top-level statements.

4.1. Values

All expressions and statements in Plet evaluate to a value. A value has a type. Plet has 10 built-in types:

4.1.1. Nil

The nil type has only one value:

nil

It is used to represent the absence of data, e.g. as a return value from a function called for its side effects.

4.1.2. Booleans

The bool type has two values:

true
false

Plet has three boolean operators:

a or b   # returns a if a is truthy, otherwise returns b
a and b  # returns b if a is truthy, otherwise returns a
not a    # returns false if a is truthy, otherwise returns true

The boolean operators are not restricted to booleans and can be used on any Plet values. The following values are considered “falsy”, all other values are “truthy”:

nil
false
0
0.0
[]    # the empty array
{}    # the empty object
''    # the empty string

The or operator can thus be used to provide a fallback value if the left operand is nil or empty:

Your name is {name or 'unknown'}.

4.1.3. Numbers

Plet has two numeric types:

12345     # int
123.45    # float
123e4     # float
123.5e-4  # float

The following operators work on both ints and floats:

-25      # => -25   (minus)
12 + 56  # => 68    (addition)
14 - 5   # => 9     (subtraction)
10 * 3   # => 30    (multiplication)
7 / 3    # => 2     (integer division)
7 / 2.0  # => 2.5   (floating point division)
5 == 5.0 # => true  (equal)
5 != 2   # => true  (not equal)
7 < 3    # => false (less than)
7 > 3    # => true  (greater than)
4 <= 4   # => true  (less than or equal to)
4 >= 5   # => false (greater than or equal to)

The binary operators above accept both int and float operands. If one operand is a float and the other an int, then the int is automatically converted to a float before applying the operator. The remainder operator below only works on ints:

7 % 3    # => 1     (remainder)

4.1.4. Time

time('2021-04-11T21:10:17Z')

4.1.5. Symbols

Symbols are interned strings which means that they are faster to compare than regular strings.

symbol('foo')

4.1.6. Strings

Plet strings are arrays of bytes. There are three types of string literals:

# Single quote strings (no interpolation)
'Hello, World! \U0001F44D'
# Double quote string (interpolation)
"two plus two is {2 + 2}"
# Verbatim string (no interpolation or escape sequences)
"""Hello, "World"! \ and { and } are ignored."""

Single quote and double quote strings both support the following escape sequences:

Unicode code points (specified using \u or \U) higher than U+007F are encoded using UTF-8.

Double quote strings additionally support string interpolation with full Plet template support:

"foo {if x > 0}bar{else}baz{end if}"

The length of a string is always its byte length:

length('\U0001F44D') # => 4

4.1.7. Arrays

Plet arrays are dynamically typed mutable 0-indexed sequences of values:

a = [1, 'foo', false]
a[1]      # => 'foo'
length(a) # => 3

Trailing commas are allowed:

[
  'foo',
  'bar',
  'baz',
]

Items can be added to the end of an array using the push function:

a = []
a | push('foo')
a | push('bar')
a[0]      # => 'foo'
a[0] = 'baz'
a[0]      # => 'baz'
length(a) # => 2

4.1.8. Objects

Plet objects are dynamically typed mutable sets of key-value pairs:

obj = {
  foo: 25,
  bar: 'Test',
}
obj.foo     # => 25
length(obj) # => 2

The keys of an object can be of any type. Entries can be added and replaced usng the assignment operator:

obj = {}
obj.a = 'foo'
obj.b = 'bar'
obj.a = 'baz'
obj.a       # => 'baz'
length(obj) # => 2

The dot-operator can only be used to access entries when the keys are symbols. For other types, the array access operator can be used:

obj = {
  'a': 'foo', # string key
  15: 'bar',  # int key
  sym: 'baz'  # symbol key
}
obj['a']           # => 'foo'
obj[15]            # => 'bar'
obj[symbol('sym')] # => 'baz'
obj.sym            # => 'baz'

4.1.9. Functions

Functions in Plet are created using the => operator (“fat arrow”). The left side of the arrow is a tuple specifying the names of the parameters that the function accepts. The right side is the body of the function (a single expression).

# a function that accepts no parameters and always returns nil:
() => nil
# identity function that returns whatever value is passed to it:
(x) => x
# parentheses are optional when there's exactly one parameter:
x => x
# a function that accepts two parameters and returns the result of adding them together:
(x, y) => x + y

Functions can be assigned to variables with the assignment operator:

foo = () => 15
bar = x => x + 1
baz = (x, y, z) => x + y + z

There are two styles of function application. The first is prefix notation:

foo() # Parentheses are required
bar(5) 
baz(2, 4, 6)

The second style is infix notation using the pipe operator where the first parameter is written before the function name (the function must have at least one argument):

5 | bar()
5 | bar    # With only one parameter the parentheses are optional
2 | baz(4, 6)

The second style can be used to chain several function calls without nested pairs of parentheses. The following two lines are equivalent:

foo() | bar | baz(1, 2) | baz(3, 4)
baz(baz(bar(foo()), 1, 2), 3, 4)

Functions in Plet are first-class citizens meaning they can be passed as arguments to other functions. A higher-order function is a function that takes another function as an argument.

func1 = x => x + 1
func2 = (f, operand) => f(operand)
func2(func1, 5)      # => 6
func2(x => x + 2, 5) # => 7

Plet has several built-in higher-order functions. One example is map which applies a function to all elements of an array and returns the resulting array (withour modifying the existing array):

[1, 2, 3, 4] | map(x => x * 2)
# => [2, 4, 6, 8]
4.1.9.1. Blocks

Blocks are expressions that can contain multiple statements and text nodes.

do
  foo()
  bar()
end do

Because they are expressions they can be assigned to variables or used as function bodies:

{my_block = do}
Some text
{end do}

Content of block: {my_block}

{my_function = x => do}
The number is {x}
{end do}

Result of function: {my_function(42)}

The value of a block is the result of concatenating the value of each statement inside the block:

block = do
  'foo'
  42
  'bar'
end do

block # => 'foo42bar'

Inside function bodies this behaviour can be avoided by using the return keyword:

my_function = x => do
  return x + 5
end do

my_function(10) # => 15

The return keyword can alo be used to short-circuit out of a function:

factorial = n => do
  if n <= 1
    return 1
  end if
  return n * factorial(n - 1)
end do

factorial(8) # => 40320

4.2. Variables and scope

= is the assignment operator.

4.3. Control flow

4.3.1. Conditional

if x > 5
  info('x is greater than 5')
else if x < 5
  info('x is less than 5')
else
  info('x is equal to 5')
end if
{if x > 5}
x is greater than 5
{else if x < 5}
x is less than 5
{else}
x is equal to 5
{end if}
{if x > y then 'x' else 'y'}
is greater than
{if x <= y then 'x' else 'y'}

4.3.2. Switch

switch x
  case 5
    info('x is 5') 
  case 6
    info('x is 6') 
  default
    info('x is not 5 or 6') 
end switch 
{switch x}
  {case 5} x is 5
  {case 6} x is 6
  {default} x is not 5 or 6
{end switch}

4.3.3. Loops

for item in items
  info("item: {item}")
else
  info('array is empty')
end for
for i: item in items
  info("{i}: {item}")
end for
for key: value in obj
  info("{key}: {value}")
end for

4.4. Modules

5. CLI

5.1. init

plet init creates a new empty index.plet file in the current working directory. In the future this command may get more options.

5.2. build

plet build finds the nearest index.plet file and evaluates it.

5.3. watch

plet watch first builds the site like plet build, then watches all source files for changes. When changes are detected, the site is built again.

5.4. serve

plet serve [-p <port>] runs a built-in web server that builds pages on demand and automatically reloads when changes are detected.

5.5. clean

plet clean recursively deletes the dist directory.

5.6. eval

plet eval <file> evaluates a Plet script.

plet eval -t <file> evaluates a Plet template.

6. Module reference

The Plet standard library is split into several modules.

6.1. core

nil: nil
false: bool
true: bool
import(name: string): nil
copy(val: any): any
type(val: any): string
string(val: any): string
bool(val: any): bool
error(message: string): nil
warning(message: string): nil
info(message: string): nil

6.2. strings

lower(str: string): string
upper(str: string): string
title(str: string): string
starts_with(str: string, prefix: string): bool
ends_with(str: string, suffix: string): bool
symbol(str: string): symbol
json(var: any): string

6.3. collections

length(collection: array|object|string): int
keys(obj: object): array
values(obj: object): array
map(collection: array|object, f: func): array|object
map_keys(obj: object, f: func): object
flat_map(collection: array, f: func): array
filter(collection: array|object, predicate: func): array
exclude(collection: array|object, predicate: func): array
sort(array: array): array
sort_with(array: array, comparator: func): array
sort_by(array: array, f: func): array
sort_by_desc(array: array, f: func): array
group_by(array: array, f: func): array
take(array: array|string, n: int): array|string
drop(array: array|string, n: int): array|string
pop(array: array): any
push(array: array, element: any): array
push_all(array: array, elements: array): array
shift(array: array): any
unshift(array: array, element: any): array
contains(obj: array|object, key: any): bool
delete(obj: object, key: any): bool

6.4. datetime

now(): time
time(time: time|string|int): time
date(time: time|string|int, format: string): string
iso8601(time: time|string|int): string
rfc2822(time: time|string|int): string

6.5. template

embed(name: string, data: object?): string
link(link: string?): string
url(link: string?): string
is_current(link: string?): bool
read(file: string): string
page_list(n: int, page: int? = PAGE.page, pages: int? = PAGE.pages): array
page_link(page: int, path: string? = PAGE.path): string
filter_content(content: object, filters: array?): string

6.6. html

h(str: string): string
href(link: string?, class: string?): string
html(node: html_node): string
no_title(node: html_node): html_node
links(node: html_node): html_node
urls(node: html_node): html_node
read_more(node: html_node): html_node
text_content(node: html_node): string
parse_html(src: string): html_node

6.7. sitemap

add_static(path: string): nil
add_reverse(content_path: string, path: string): nil
add_page(path: string, template: string, data: object?): nil
add_task(path: string, src: string, handler: (dest: string, src: string) => any): nil
paginate(items: array, per_page: int, path: string, template: string, data: object?): nil

6.8. contentmap

list_content(path: string, options: {recursive: bool, suffix: string}?): array
read_content(path: string): object

6.9. exec

shell_escape(value: any): string
exec(command: string, ... args: any): string

7. Language reference

7.1. Lexical structure

tokenStream   ::= [bom] {text | comment | command}

bom           ::= "\xEF\xBB\xBF"     -- ignored

command       ::= commandStart {commandToken | lf | skip} commandEnd

commandToken  ::= token | paren | bracket | brace | comment | commentSingle

commandStart  ::= "{"  -- ignored
commandEnd    ::= "}"  -- ignored

comment       ::= "{#" {any - "#}"} "#}"   -- ignored

commentSingle ::= '#' {any - lf}   -- ignored

lf            ::= "\n"
skip          ::= " " | "\t" | "\r"    -- ignored

paren         ::= "(" {commandToken | lf | skip} ")"
bracket       ::= "[" {commandToken | lf | skip} "]"
brace         ::= "{" {commandToken | lf | skip} "}"

quote         ::= '"' {quoteText | command} '"'

text          ::= {any - (commandStart | commandEnd)}
quoteChar     ::= any - (commandStart | commandEnd | '"')
                | "\\" (commandStart | commandEnd | escape)
quoteText     ::= {quoteChar}

token         ::= keyword
                | operator
                | int
                | float
                | string
                | verbatim
                | name

keyword       ::= "if" | "then" | "else" | "end" | "for" | "in" | "switch" | "case" | "default"
                | "do" | "and" | "or" | "not" | "export" | "return" | "break" | "continue"

operator      ::= "." | "," | ":" | "=>"
                | "==" | "!=" | "<=" | ">=" | "<" | ">"
                | "=" | "+=" | "-=" | "*=" | "/="
                | "+" | "-" | "*" | "/" | "%"

name          ::= (nameStart {nameFollow}) - keyword

nameStart     ::= "a" | ... | "z"
                | "A" | ... | "z"
                | "_"
                | "\x80" | ... | "\xFF"

nameFollow    ::= nameStart | digit

digit         ::= "0" | ... | "9"

hex           ::= digit
                | "a" | ... | "f"
                | "A" | ... | "F"
hex2          ::= hex hex
hex4          ::= hex2 hex2
hex8          ::= hex4 hex4

int           ::= digit {digit}

exponent      ::= ("e" | "E") ["-" | "+"] int

float         ::= int ["." int] exponent

escape        ::= '"'
                | "'"
                | '\\'
                | '/'
                | 'b'
                | 'f'
                | 'n'
                | 'r'
                | 't'
                | 'x' hex2
                | 'u' hex4
                | 'U' hex8

string        ::= "'" {(any - ("\\" | "'")) | '\\' escape} "'"

verbatim      ::= '"""'  {any - '"""'} '"""'

7.2. Syntax

Template      ::= {Statements | text}

Block         ::= (lf | text) Template (lf | text)
                | text

Statements    ::= {lf} [Statement {lf {lf} Statement}] {lf}

Statement     ::= If
                | For
                | Switch
                | Export
                | "return" [Expression]
                | "break" [int]
                | "continue" [int]
                | Assignment

If            ::= "if" Expression Block
                  {"else" "if" Expression Block}
                  ["else" Block] "end" "if"
                | "if" Expression "then" Expression "else" Statement

For           ::= "for" name [":" name] "in" Expression Block
                 ["else" Block] "end" "for"

Switch        ::= "switch" Expression (lf | {lf} [text])
                  {"case" Expression Block}
                  ["default" Block] "end" "switch"

Assignment    ::= Expression [("=" | "+=" | "-=" | "*=" | "/=") Expression]

Export        ::= "export" name ["=" Expression]

Expression    ::= "." name {"." name}
                | FatArrow

FatArrow      ::= Tuple "=>" Statement
                | PipeLine

Tuple         ::= name
                | "(" [name {"," name} [","]] ")"

PipeLine      ::= PipeLine "|" name ["(" [Expression {"," Expression} [","]] ")"]
                | LogicalOr

LogicalOr     ::= LogicalOr "or" LogicalAnd
                | LogicalAnd

LogicalAnd    ::= Logical "and" LogicalNot
                | LogicalNot

LogicalNot    ::= "not" LogicalNot
                | Comparison

Comparison    ::= Comparison ("<" | ">" | "<=" | ">=" | "==" | "!=") MulDiv
                | MulDiv

MulDiv        ::= MulDiv ("*" | "/" | "%") AddSub
                | AddSub

AddSub        ::= AddSub ("+" | "-") Negate
                | Negate

Negate        ::= "-" Negate
                | ApplyDot

ApplyDot      ::= ApplyDot "(" [Expression {"," Expression} [","]] ")"
                | ApplyDot "." name ["?"] 
                | ApplyDot "[" Expression "]" ["?"] 
                | Atom

Key           ::= int
                | float
                | string
                | name

Atom          ::= "[" [Expression {"," Expression} [","]] "]"   -- ignore lf
                | "(" Expression ")"   -- ignore lf
                | "{" [Key ":" Expression {"," Key ":" Expression} [","]] "}"   -- ignore lf
                | '"' Template '"'
                | "do" Block "end" "do"   -- don't ignore lf
                | int
                | float
                | string
                | name ["?"]