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:
UNICODE
– Enable Unicode support (requires ICU).GUMBO
– Enable support for HTML manipulation (requires Gumbo).IMAGEMAGICK
– Enable support for automatic resizing and conversion of images (requires ImageMagick 7).MUSL
– Enable compatibility with musl libc.STATIC_MD4C
– Build with md4c source in lib instead of dynamically linking with md4c.
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
:
templates/list.plet.html
– This will be the index page displaying a sorted list of blog posts.templates/post.plet.html
– This template will be used to display a single blog post.templates/layout.plet.html
– The layout shared by the two other 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:
{'/' | link}
evaluates to a link to the index page.{CONTENT}
evaluates to the value of the specialCONTENT
variable, which contains the output of whatever template is usingtemplates/layout.plet.html
as a layout.
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>
– 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:
post.title | h
encodes special characters for use in HTML documents, e.g.<
to<
and&
to&
.post.published | date('%Y-%m-%d')
formats a date/time.
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:
- index.plet
- posts/
- first.md
- second.md
- templates/
- layout.plet.html
- list.plet.html
- post.plet.html
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:
- index.html
- posts/
- first/
- index.html
- second/
- index.html
- first/
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.7. Reverse links
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:
- nil
- bool
- int
- float
- time
- symbol
- string
- array
- object
- function
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:
int
: 64-bit signed integersfloat
: 64-bit double-precision floating point numbers
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:
\'
– single quotation mark\"
– double quotation mark\\
– backslash\/
– forward slash\{
– left curly bracket (only in double quote strings)\}
– right curly bracket (only in double quote strings)\b
– backspace\f
– formfeed\n
– newline\r
– carriage return\t
– horizontal tab\xhh
– byte value given by hexadecimal numberhh
\uhhhh
– Unicode code point given by hexadecimal numberhhhh
\Uhhhhhhhh
– Unicode code point given by hexadecimal numberhhhhhhhh
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
asset_link(path: 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 ["?"]