Static-file templating engine.
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"