edadma / squiggly   0.3.0

ISC License Website GitHub

Scala based template engine

Scala versions: 3.x
Scala.js versions: 1.x
Scala Native versions: 0.5

squiggly logo

squiggly

Maven Central GitHub last commit GitHub Scala Version Scala.js Version Scala Native Version

squiggly is a Scala 3 string templating engine, cross-built for the JVM, Scala.js, and Scala Native.

Overview

squiggly is a language, a Scala library, and a small command-line application for doing string templating. It can be compared to Mustache, Go templates, or Liquid — a string template composed of text and tags (instructions in squiggly) is applied to context data, producing textual output.

Unlike Mustache, squiggly is not logic-less; it allows basic logic in templates. The expression language is inspired by Hugo and Liquid, with infix operators, comparison chains, pipes, and method calls.

The name "squiggly" is a colloquialism for curly braces — the tag delimiters.

Documentation

Full reference, syntax guide, and built-in function index: https://edadma.github.io/squiggly/

Installation

Add the dependency to your build.sbt:

libraryDependencies += "io.github.edadma" %%% "squiggly" % "0.2.3"

%%% cross-builds against whatever target your project uses (JVM, Scala.js, or Scala Native).

Use the following import in your code:

import io.github.edadma.squiggly._

Basic use

Library

import io.github.edadma.squiggly._

@main def runDemo(): Unit =
  val data =
    Map(
      "user"  -> "ed",
      "tasks" -> List(
        Map("task" -> "Improve Parser and Renderer API", "done" -> true),
        Map("task" -> "Code template example",           "done" -> false),
        Map("task" -> "Update README",                   "done" -> false),
      ),
    )

  val template =
    """<!DOCTYPE html>
      |<html>
      |  <body>
      |    <p>To-do list for user '{{ .user }}'</p>
      |    <ul>
      |      {{ for .tasks -}}
      |      <li>{{ .task }} — {{ if .done }}done{{ else }}pending{{ end }}</li>
      |      {{- end }}
      |    </ul>
      |  </body>
      |</html>
      |""".stripMargin

  val ast = TemplateParser.default.parse(template)

  TemplateRenderer.default.render(data, ast)

TemplateRenderer.render takes any Scala value (Map, Seq, String, BigDecimal, Boolean, case class, …) — there is no special data-loading layer.

Command line

The CLI is invoked through sbt while developing. From the project root:

sbt 'squigglyJVM/run "Hello, {{ 1 + 2 }}!"'
Hello, 3!

Equivalently on Scala Native:

sbt 'squigglyNative/run "Hello, {{ 1 + 2 }}!"'

For Scala.js, link the program first and run with node:

sbt squigglyJS/fastLinkJS
node js/target/scala-3.8.3/squiggly-fastopt/main.js "Hello, {{ 1 + 2 }}!"

CLI flags

Squiggly Template Engine v0.2.3
Usage: squiggly [options] [[<template>]]

  -a, --ast              pretty print AST
  -d, --data <JSON>      JSON document (string)
  -f, --template <file>  template file
  -h, --help             prints this usage text
  -v, --version          prints the version
  -j, --json <file>      JSON data file
  [<template>]           template string

Example with inline JSON data (note shell quoting; for anything more complex than the simplest case prefer -j <file>):

echo '{"a": 3, "b": 4}' > /tmp/data.json
sbt 'squigglyJVM/run -j /tmp/data.json "{{ .a }} + {{ .b }} = {{ .a + .b }}"'
3 + 4 = 7

Templates

Squiggly templates are inspired by Hugo and Liquid. The goal is low boilerplate and readability. Hugo is compact but strictly prefix; Liquid has nicer infix syntax but more boilerplate. Squiggly takes the readable parts of each.

Values

Literals

  • null — represents no value; renders as an empty string. Maps to Scala null.
  • true, false — booleans.
  • numbers — internally BigDecimal (exact decimal arithmetic, arbitrary-precision integers, no floating-point surprises).
  • strings — between '…' or "…", whichever is convenient. Standard escapes: \n, \t, α, \\, etc.
  • lists[a, b, c].
  • maps{key: value, key: value}.

There is also an undefined value (Scala ()) that cannot be written literally — it represents a missing property and renders as an empty string. It is distinct from null because a present property may be explicitly null.

Expressions

Everything you'd expect: arithmetic (+ - * / mod ^ \ for integer division, ++ for string/list concat), comparison chains (a < b <= c), boolean (and, or, not), conditional (if x then a else b), index (.a[0]), method ('hello'.upper), function application (upper 'hello'), and pipes ('hello' | upper).

Tags

{{ _value_ }}

Renders a value into the output. {{ .title }} substitutes the title field of the current context.

{{ with _value_ }} … {{ else }} … {{ end }}

Binds the inner context to value; runs the falsy branch if the value is falsy.

{{ for [ _e_ [ , _i_ ] <- ] _value_ }} … [ {{ else }} … ] {{ end }}

Iterates over a Seq or Map. Optional element/index bindings.

{{ if _cond_ }} … [ {{ elsif _cond_ }} … ] [ {{ else }} … ] {{ end }}

Standard branching. else if is spelled elsif.

{{ match _expr_ }} {{ case _value_ }} … [ {{ else }} … ] {{ end }}

Switch on a value.

{{ define _name_ }} … {{ end }} and {{ block _name_ _expr_ }} … {{ end }}

Reusable named blocks with override-by-define.

{{ _name_ := _expr_ }}

Bind a variable in the current context.

{{ return [_expr_] }}

Halt rendering and yield a value back from TemplateRenderer.render.

{{ // _comment_ }} or {{ /* _comment_ */ }}

Template comments.

Built-in functions

~67 builtins: numeric (abs, ceil, floor, round, min, max, sum, number); string (upper, lower, capitalize, length, trim, ltrim, rtrim, htmlEscape, newline_to_br, urlize, urlEncode, urlDecode, split, startsWith, substring, truncate, remove, removeFirst); collection (head, last, tail, append, prepend, reverse, distinct, compact, drop, dropRight, take, takeRight, slice, join, contains, length, isEmpty, nonEmpty, toSeq, toString); set ops (intersect, union, symdiff, complement); higher-order (filter, filterNot, map, default); regex (findRE); date/time (now, time, unix, format with named formats :date_full / :date_long / :date_medium / :date_short and any java.time DateTimeFormatter pattern); misc (querify, partial, fileExists, random, shuffle, print, println, context).

Tests

sbt squigglyJVM/test
sbt squigglyJS/test
sbt squigglyNative/test

Or run the whole cross-build at once:

sbt test

Contributing

PRs welcome. New features and bug fixes should ship with tests; scalafmt is configured.

License

ISC