slim v0.4.0

Slim templates for ris, ported from Slang

Slim

Lightweight, terse templates for ris, in the syntax of Slim.

This module continues Slang, a Slim-style template language for Crystal that started in 2016. The lexer, parser and code generation of Slang's last release (2.0.3) are ported to ris, along with its tests, and the port is named Slim after the syntax it follows. The Crystal code is kept unchanged in archive/crystal/ and is no longer maintained. The ris port is maintained by Priit Tark at domify.

Like the original, a template is turned into ris code at compile time, so a rendered page is ordinary compiled code by the time the program runs.

Installation

Add it to a consuming project's ris.mod; ris fetches the tag from GitHub:

require github.com/domify/slim v0.4.0

Usage

import github.com/domify/slim::{Slim}

def page(name : String, items : Array(String)) : String
  Slim.render("views/page.slim")
end

tape do
  body = page("Mari", ["one", "two"])
  # body lives until this block ends.
end

Slim.render(filename, capacity = 256) returns a String on the caller's tape. A def that ends with it builds the page in its caller's scope, and the page is freed when that scope ends: a tape do … end block, a def, a loop round, or the tape that main and every fiber have. There is one renderer and one tape buffer; Slim has no heap renderer. The compiler refuses returning or storing the page beyond the scope that owns it.

For a page that must outlive every scope, such as a cache entry, use render_string:

def kept_page(name : String, items : Array(String)) : String
  Slim.render_string("views/page.slim")
end

render_string(filename, capacity = 256) renders the same way and copies the page to Program.tape with copy_to; that copy stays until the program ends. Slim.render("views/page.slim").copy_to(owner) keeps a page for another owner.

Slim.embed(filename, io) writes into a writer with <<: Slim::TapeBuffer.new(capacity), a String::Builder or a sink of your own. A tape buffer's finish (and to_s) is a view of its bytes; do not append to the buffer after taking it.

Paths are relative to the source file calling the macro, or used as written when absolute. Templates see the calling method's local variables.

Rendering streams escaped bytes and class names into the sink instead of collecting fragments, escaped byte arrays or joined class strings. String values and 8–64-bit integers are written without a temporary String. Interpolations, non-integer to_s and HTML blocks passed to helpers build a String on the tape first.

TEMPLATES.md is a short guide to writing templates, for agents and people who know Slim; AGENTS.md is for working on Slim itself.

Syntax

doctype html
html lang="en"
  head
    meta charset="utf-8"
    title= title
    css:
      h1 {color: red;}
  body
    /! A visible comment
    / An invisible comment
    h1.title Hello, #{name}!
    p
      ' Text is <em>HTML</em>,
      | and #{name} in it is escaped.
    div!= "<hr>"
    ul#items
      - items.each do |item|
        li class=(item == current ? "current" : nil) = item
    input type="checkbox" checked=true
    input type="checkbox" checked=false
    a(href="/about" title="About") About
    span<> spaced
    javascript:
      console.log("8 * 3 =", 8 * 3);

With title = "Slim", name = "<world>", items = ["one", "two"] and current = "two", this renders as (one line, broken here for reading):

<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><title>Slim</title>
<style>h1 {color: red;}</style></head><body><!--A visible comment-->
<h1 class="title">Hello, &lt;world&gt;!</h1>
<p>Text is <em>HTML</em>, and &lt;world&gt; in it is escaped.</p><div><hr></div>
<ul id="items"><li>one</li><li class="current">two</li></ul>
<input type="checkbox" checked><input type="checkbox">
<a href="/about" title="About">About</a> <span>spaced</span>
<script>console.log("8 * 3 =", 8 * 3);</script></body></html>
Syntax Meaning
tag, .class, #id, tag.class#id An element; .class and #id alone are a div.
tag attr="text #{expr}", tag attr=expr Attributes. true writes the bare name, false and nil leave it out. Values are escaped.
tag(…), tag[…], tag{…} Attributes inside brackets.
tag *hash Every entry of a hash as an attribute; its class joins the others.
tag: tag An element nested on the same line.
tag text, | text Text, which is HTML; #{expr} in it is escaped.
' text Text followed by a space.
tag= expr, tag!= expr, = expr, != expr Output, escaped or raw, in the element or on its own line. =<, =>, tag< and tag> put a space before or after.
= expr do |x| A method that takes the nested HTML as a block's String, such as a form helper.
- code ris code: if/elsif/else, case/when/in, each do, assignments.
/ text, /! text, /[if IE] An invisible, a visible and a conditional comment.
javascript:, css: A script or style element with verbatim text.
ris:, iyi:, crystal: Lines of ris code.
doctype html <!DOCTYPE html>.

As in Slim, there is no whitespace between tags. An attribute value without quotes ends at the first space outside parentheses or brackets, so compute a long expression in a - name = … line first. For the same reason, output after such a value needs a space before its =, as in li class=css = item; after a tag or a quoted value, p= name and a href="/"= name need none.

Differences from Crystal Slang 2.0.3

  • Raw output is !=, as in Haml and Pug, so it does not read as a comparison; == stops the template with an error.
  • An attribute whose value is false or nil is left out and true writes the bare name, including id and the entries of a *splat hash.
  • Blank class names are left out, and an element without a class name has no class attribute.
  • ris: opens a code block; iyi: and crystal: remain compatible aliases.
  • begin/rescue lines are parsed, but ris has no exceptions, so they do not compile. Kilt and Kemal's content_for are not supported.
  • render returns a String on the caller's tape; render_string copies it to Program.tape. Both render through one tape buffer; render_tape, render_heap and the former Buffer class are removed.
  • Splat expressions are evaluated once per element, after explicit class expressions and before ordinary attributes.
  • Fixed bugs of the original:
    • =< and => put a space before and after the output; the original added it to the code.
    • Comparisons such as a == b and a != b inside an attribute value are kept; the original dropped every = in it.
    • An attribute value such as (f(x) == y) no longer ends at the space after the inner ).
    • Attributes after a(href="x") no longer stop the template with "Missing hash key: ')'".
  • A compile error in a template's code names the template's line.

Development

./check.sh

slim_test.ri holds the tests of the Crystal shard's spec/slang_spec.cr, with their templates under spec/, and the port's own additions. The module is slim.ri (what rendered code needs at run time, and the macros) and slim/process.ri (the lexer, parser and code generation the macros run at compile time).

Runtime benchmarks comparing iyi, ris, builder and tape output are in bench/. They do not change the public rendering API.

Thanks

Slim exists because of the people who wrote and kept it alive:

Their history is in this repository's git log.

License

MIT, see LICENSE.

Repository

slim

Owner
Statistic
  • 0
  • 0
  • 0
  • 0
  • 0
  • 7 days ago
  • October 16, 2026
License

MIT License

Links
Synced at

Wed, 04 Nov 2026 18:04:14 GMT

Languages