Arcana

Static-file templating engine.

View on Github


Installation

The arcana compiler can be downloaded with git and installed with cargo.

# download a copy of the repository
git clone "https://github.com/frankiebaffa/arcana"
cd arcana
# install the arcc compiler
cargo install --path compiler
arcc -h

See the help document for the usage of the compiler.

Glossary

alias: A reference to a value within the current context (i.e. value.is.here).

chain: A hyphen (-) character following the closure of tags such as if, for, and their respective else. Tells the parser to ignore whitespace until the next block opening.

content: Characters included in the output of the parser.

context: A map of values.

pathlike: A literal path (i.e. "path/to/file.txt") or an alias to a path in the current context (i.e. context.path.to.file).

sealed: A context scope whose modifications will not propagate to a higher level.

stringlike: A string, number, or boolean in the current context.

Whitespace Control

This is an example \
    of whitespace control.

Arcana has one mode of whitespace control, a single backslash ending a line. The parser will ignore the backslash and consume all following whitespace without dumping to content. The above example would compile to the following.

This is an example of whitespace control.

Tags

Expression tags can control the flow of the document, spawn other parsers, and can write content to other files.

Comment

#{This is a comment.}#

Comments are ignored by the parser. Comments are closed with a special endblock so they can span multiple lines and contain the templating syntax without prematurely closing.

#{ Uncomment in production
${alias}
}#

Extend-Template

+{pathlike}

A template to parse using the final context of the current file. The output content of the current file will be set to the special alias $content.

Source-File

.{pathlike}

A context file to include in the current context. Matching values will be overwritten.

Modifiers

As
.{pathlike|as obj}

The as modifier can be used to specify an alias at which to place the values sourced from the specified context file.

Consider the context file context.json:

{
    "name": "Jane Doe",
    "age": 42
}

And the template file template.arcana:

.{"context.json"|as person}
${person.name}: ${person.age}

When compiled, the template file would yield the result:

Jane Doe: 42

Include-File

&{pathlike}

A file to parse and include in the position of the tag. Any changes to the context while parsing the file are sealed.

An optional block can be included which will allow for modifications to the sealed context prior to parsing the given file. Any output of this block will also be included in the sealed context with the special $content alias.

Consider the following file link.arcana.

<a href="${href}"%{cls}( class="${cls}")">${$content}</a>

It can be included in another template like so:

<p>Click &{"./link.arcana"}(\
    ={}({
        "href": "https://github.com",
        "cls": "a-link"
    })\
    Here\
)</p>

Or without the JSON literal block:

<p>Click &{"./link.arcana"}(\
    ={href}("https://github.com")\
    ={cls}("a-link")\
    Here\
)</p>

Either of these templates would compile to the following:

<p>Click <a href="https://github.com" class="a-link">Here</a></p>

Modifiers

Raw
&{pathlike|raw}

Does not parse the content, only includes directly. Can be used in conjunction with the markdown modifier.

Markdown
&{pathlike|md}

Parses the output of the include as No-Flavor Markdown. Can be used in conjunction with the raw modifier.

If

%{alias}-
(${alias})-
(Alias is falsey.)

Evaluates the output of the condition as true or false. If true, the first trailing block is parsed. Else, the second trailing block is parsed. The default condition of the if tag is truthy. The initial if tag as well as the true condition tag can be optionally trailed by a chain as shown above. The condition can be preceeded by the not operator (!) to negate the evaluated condition.

Exists

%{alias exists}(${alias})(Alias does not exist.)

Evaluates to true if the given alias exists in the current context.

Empty

%{!alias empty}{${alias}}{Alias was empty.}

Evaluates to true if the given alias has an empty value in the current context.

Truthy

%{this.alias}(Alias was true.)(Alias was false.)

Evaluates to true if the given alias is truthy. A defined string will always be true. A number will be true when greater than 0. An array will always be true. An object will be evaluated based on whether or not it contains any keys. Null will always evaluate to false.

Comparisons

%{this.alias==that.alias}()
%{this.alias>that.alias}()
%{this.alias>=that.alias}()
%{this.alias<that.alias}()
%{this.alias<=that.alias}()

Evaluates the equality or comparison between two JSON objects. If the objects cannot be compared (string to number, etc), then an error will be thrown.

Multiple Conditions

%{this.alias&&this.alias>that.alias||$loop exists}()

Conditions can be chained together using && for and and || for or.

For-Each-Item

@{item in alias}(
    ${item.name}
)(
    No items.
)

Loop through contents of an array in context. The inner context of the loop is sealed. The first trailing block will be parsed for every item found within the array. If there are no items, the second block will be parsed. The for-each-item tag can be trailed by a chain.

Loop Context

For loops initialize a special alias into the sealed context named $loop. This object's values are mapped to the following aliases.

$loop.index: The 0-indexed position of the current iteration.

$loop.position: The 1-indexed position of the current iteration.

$loop.length: The length of the array being iterated over.

$loop.max: The maximum index of the array begin iterated over.

$loop.first: Set iff the current value of $loop.index is 0.

$loop.last: Set iff the current value of $loop.index is $loop.max.

Modifiers

Paths

Treat the values found within the array as paths to files.

@{dir in alias|paths}(
    *{file in dir|ext "json"}(
        .{file|as subobj}
        ${subobj.description}
    )(
        No files in directory.
    )
)(
    No directories in alias.
)
Reverse

Reverse the order of the array.

@{item in alias|reverse}-
(%{ !$loop.first }(
)${ item })-
(No items.)

For-Each-File

*{file in pathlike}(
    &{item}
)(
    No files in directory.
)

Loop through the files found within a given directory. The inner context of the loop is sealed. The trailing blocks function identically to the for-each-item tag.

Loop Context

The same loop context is set for this tag as for for-each-item.

Modifiers

With-Extension
*{file in "./this/dir"|ext "json"}(
    .{file|as sub}
    *{sub-file in sub.files}(
        \(${$loop.position}\) ${sub-file|filename}
    )
)

Only include files with the matching extension.

Reverse
*{file in pathlike|ext "arcana"|reverse}(
    &{file}
)(
    No files found in directory.
)

Reverse the order of the files.

Include-Content

${alias.to.stringlike}

Includes the stringlike value of the alias from the current context in the content.

Modifiers

Lower
${alias|lower}

Changes the content to lowercase.

Replace
${alias|replace "x" "y"}

Replaces instances of x with y.

Upper
${alias|upper}

Changes the content to uppercase.

Path
${ alias | path }

Handles the content as a path.

Filename
${ alias | path | filename }

Outputs only the filename of the path.

Trim
${alias|trim}

Removes whitespace from the start and end of the content.

Split
${alias|split 2 0}

Splits the content into 2 parts and uses the part at index 0.

Consider the context file context.json:

{
    "name": "Jane Doe"
}

And the template file template.arcana:

.{"context.json"}
${name|split 2 1}

The output of the parser would be:

 Doe
Json
={item}("This string here.")
={ctx.item}(${item|json})

Represents the content as raw json.

Set-Item

={alias}("Here is the value")

Sets the value at alias within the current context to the parsed JSON output of the block.

The block can contain arcana syntax as long as the parsed output is valid JSON.

={this-name}(${item.name|json})

Any JSON type can be set using the set-item block.

={this-object}({
    "key": "value",
    "items": [
        "first",
        "second",
        "third"
    ],
    "a-number": 54.2
})

The root context can also be written-to by avoiding the inclusion of an alias.

={}({
    "root-level-item": "Some value."
})
${root-level-item}

This comes in handy when an included file references an object from the root context and is accessed from within a loop or another context. Consider the following file artist.arcana:

# ${name}

${brief}

@{album in albums}(
    &{"./album.arcana"}{
        ={}(${album|json})
    }
)

The file considers the artist object to be at the root level. So if we wanted to include the file from another context, say artists.arcana, we could reference the artist as a root level object:

@{artist in artists}(
    &{"./artist.arcana"|md}(
        ={}(${artist|json})
    )
)

Unset Item

/{alias}

Unsets the value at alias.

File Operation Tags

The following tags perform file operations and are intended for deployment purposes. If you're feeling a bit nutty, you can use them in your standard templates however you wish.

Write-Content

^{pathlike}(\
    &{"this/file.arcana"}(={title}("A title here"))\
)

Writes to pathlike the content of the block.

Copy-Path

~{source-pathlike destination-pathlike}

Copies the file at the source pathlike to the file at the destination pathlike.

Delete-Path

-{pathlike}

Deletes the file at pathlike.

Example

Using the file operation tags, you can write your own logging deployment files like the following.

#{ ./deploy.arcana }#\
={font-file}("./in/font.ttf")\
={font-dest}("./out/fonts/font.ttf")\
Copying font "${font-file|path}" to "${font-dest|path}"\
~{font-file font-dest}
Compiling templates...\
*{template in "./in/templates"|files|ext "arcana"}(
    ={template-dest}("./out/${$loop.entry.stem}.html")\
    Compiling "${template|path}" to "${template-dest|path}"\
    ^{template-dest}(&{template})\
)\

Running the arcc ./deploy.arcana command would output the following while also properly deploying the files.

Compiling font "./in/font.ttf" to "./out/fonts/font.ttf"
Compiling templates...
    Compiling "./in/templates/first.arcana" to "./out/first.html"
    Compiling "./in/templates/second.arcana" to "./out/second.html"