noteflakes

Papercraft - Composable Templating for Ruby

04·02·2022

Papercraft is a new Ruby gem I’ve been working on, that provides a new way to render HTML, XML and JSON using plain Ruby. Here’s what it looks like:

require 'papercraft'

template = Papercraft.html {
  html5 {
    head {
      title 'Some title'
    }
    body {
      emit_yield
    }
  }
}

template.render('Page title') { h1 'Hello, world!' }
#=> "<html><head><title>Some title</title></head><body><h1>Hello, world!</h1></body></html>"

Of course, a Ruby DSL for constructing HTML, XML or JSON is nothing new. Here’s a list of previous projects that already do that:

All of the above offer some variation on the same theme: you construct HTML using nested Ruby blocks, e.g.:

html {
  body {
    div {
      p 'foo'
      p 'bar'
    }
  }
}

I’ve been a long time admirer of Ruby procs. To me, procs are perhaps the single most unique feature of Ruby, especially in their block form:

items.sort_by { |item| item.path }.each { |item| do_something_with(item) }

When you call a method with a block, you are in injecting a piece of your own code into some other code, and this lets you compose different pieces of code in a variety of ways. The way Ruby blocks are integrated into the language is perhaps the defining feature of Ruby, and to me it’s one of the main reasons Ruby makes developers happy.

I’ve been playing for a while now with all kinds of ideas for constructing DSLs using blocks. One project I created a while ago was Rubyoshka, a Ruby gem that lets you write HTML using plain Ruby syntax. The name was a nod to the Matryoshka, the Russian nesting doll.

I’ve recently came up with a way to make it even better, and have decided to give this library a new name, henceforth Papercraft. Papercraft is unique in how it embraces Ruby procs, and how it enables developers to express the different parts of an HTML page (or XML and JSON documents) using procs on one hand, and to compose those different parts in a variety of ways on the other hand.

Ruby procs as templates

The idea behind Papercraft is simple: a template is a Proc that can be rendered by executing it in the context of a special-purpose rendering object, using #instance_exec.

Ruby procs can take positional and/or named parameters, which lets us explicitly pass dynamic values to templates:

foobar = ->(foo, bar) {
  h1 foo
  p bar ? 'hi' : 'bye'
}

The above proc (strictly speaking it’s a lambda, which is a special kind of proc) takes two parameters, foo and bar, which affect its rendered output.

Ruby procs, being first-class objects, can also be composed and derived. For example, we can take the above foobar proc and use it in the context of a list template:

foobar_list = ->(list) {
  list.each { |i|
    emit foobar, i.foo, i.bar
  }
}

We could also create a derivative of foobar where foo is applied:

applied_foobar = ->(bar) { emit foobar, 'hi', bar }

(Note the use of #emit in the last two example, more on that below.)

So, once templates are expressed as procs, they can be composed, combined and transformed in a variety of ways, including all the ways proc objects can be manipulated. In fact, the primary template class in Papercraft, Papercraft::Template, is a subclass of Proc, which means it can be exchanged for a proc at any place.

Emitting HTML

Let’s start with the basics: Papercraft templates produce HTML by emitting tags. This is done primarily by making method calls with the tagname, followed by the tag content and any tag attributes. Here are some examples:

p 'foo' #=> <p>foo</p>
a 'bar', href: 'https://example.com/' #=> <a href="https://example.com">foo</a>
h1 'Title', id: 'title' #=> <h1 id="title">Title</h1>

Tags are nested by using blocks:

div(id: 'foo') {
  div(id: 'bar') {
    p 'hi'
  }
} #=> <div id="foo"><div id="bar"><p>hi</p></div></div>

In order to render text and have it escaped correctly, you can use the #text method:

p {
  span 'pre'
  text ' foo & bar '
  span 'post'
} #=> <p><span>pre</span> foo &amp; bar <span>post</span></p>

Other content (such as raw text, nested templates, markdown etc.) can be rendered by using the #emit method:

emit '<h1>hi</h1>' #=> <h1>hi</h1>

foo = -> { p 'foo' }
emit foo #=> <p>foo</p>

emit markdown('## Hi') #=> <h2 id="hi">Hi</h2>

(Papercraft includes built-in support for rendering Markdown. More on that below.)

Explicit template parameters

The most important difference between Papercraft and all of its predecessors is the fact that in Papercraft, any variables referenced in the template logic should be passed explicitly to the template when it is rendered:

greeter = Papercraft.html { |name:| h1 "Hello, #{name}!" }

greeter.render(name: 'world') #=> "<h1>Hello, world!</h1>"

In the above example, we create a template that takes a single named argument, name:. When we want to render the template, we need to supply the a name: parameter, which is then injected by the template code into the resulting HTML.

This way of injecting data into templates offers multiple advantages: the data flow is much clearer - since you’re not implicitly reyling on variables that just happen to be in your template’s binding, and debugging is easier - if you forget to provide the name parameter, Ruby will tell you!

Parameter application

Papercraft takes this idea even further by letting you create a derivative template by applying parameters to the source template, using the #apply method. You can select to do a full or partial application:

greeter = Papercraft.html { |greeting:, name:| h1 "#{greeting}, #{name}!" }
goodbyer = greeter.apply(greeting: 'Goodbye')

goodbyer.render(name: 'world') #=> "<h1>Goodbye, world!</h1>"

In the above example, we take our greeter and make it a bit more general - it now also takes a greeting: argument. We then create a derivative template by partially applying greeter with just the greeting: parameter filled in. We then render the goodbyer template, passing in the missing name: parameter.

Block application

The idea of application can be taken further with the use of applied blocks. Here’s how you do derivative layouts in Papercraft:

layout = Papercraft.html { |**props|
  html {
    head {
      title props[:title]
    }
    body {
      emit_yield **props
    }
  }
}

article_layout = layout.apply { |title:, markdown_content:|
  article {
    h1 title
    emit_markdown markdown_content
  }
}

article_layout.render(title: 'Foo', markdown_content: '## Bar')

In the above example, we first create a layout. This layout creates a generic HTML structure, with the body section containing a call to emit_yield, which expects a block to be injected. This can be done either using #apply or #render and passing a block. We then create a derivative layout that applies a block to be emitted inside the body section.

Notice how parameters passed to the layout template are explicitly passed along to the applied block (in the call to #emit_yield), and how they are destructured in the block given to layout.apply. Finally, we can render the article_layout to HTML by calling #render with the needed parameters.

Template composition

Using #emit_yield is not the only way to pass in arbitrary blocks to a template. In fact, you can pass any number of template blocks as parameters into your template, and then use #emit to emit them. Here’s another way layout templates can be created with Papercraft:

layout = Papercraft.html { |header:, content:, footer:|
  html {
    body {
      emit header
      emit content
      emit footer
    }
  }
}

layout.render(
  header:   -> { header { p 'some header' } },
  content:  -> { content { h1 'Some content' } },
  footer:   -> { footer { p 'some footer' } }
)

Higher order templates

We can create higher order templates by writing methods (or procs) that take a template as input and return a composed template as output, normally :

div_wrap = ->(templ) {
  div {
    emit templ
  }
}

layout = Papercraft.html { |content|
  body {
    emit div_wrap.call(content)
  }
}

layout.render(-> { h1 'hi' })
#=> "<body><div><h1>hi</h1></div></body>"

In the above example, we create a higher order template called div_wrap. It takes as an input a given template, and returns as its output a template wrapping the original template with a div element.

Rendering Markdown

Papercraft has built-in support for rendering Markdown. Markdown can be converted to HTML by calling the #markdown method:

markdown '## Hi' #=> "<h2 id=\"id\">Hi</h2>"

In HTML templates you can also use the #emit_markdown convenience method:

emit_markdown '## Hi' #=> <h2 id="hi">Hi</h2>

Papercraft uses Kramdown and rouge(https://github.com/rouge-ruby/rouge) to convert Markdown to HTML with support for syntax-highlighted code blocks. The default Markdown options can be overriden by passing them to #markdown or #emit_markdown:

# render markdown without heading ids
emit_markdown '## Hi', auto_ids: false #=> <h2>Hi</h2>

XML and JSON templates

Papercraft XML templates work just like HTML templates:

Papercraft.xml { |movies|
  movies.each { |m|
    movie {
      title     m.title
      year      m.year
      director  m.director
    }
  }
}

And here’s how a JSON template can be generated:

Papercraft.json { |movies|
  movies.each { |m|
    item {
      title     m.title
      year      m.year
      director  m.director
    }
  }
}

Extending Papercraft

Since the main goal of Papercraft is to allow developers to produce dynamic HTML, XML or JSON with the least amount of code, it also includes the possibility of creating extensions that provide a convenient API for creating complex HTML components. This might be particularly useful when using design frameworks such as Bootstrap or Tailwind, where some components demand quite complex markup. Here’s an example of how a Bootstrap extension might look like:

module BootstrapComponents
  ...

  def card(**props)
    div(class: 'card', **props) {
      div(class: 'card-body') {
        emit_yield **props
      }
    }
  end

  def card_title(title)
    h5 title, class: 'card-title'
  end

  ...
end

Papercraft.extension(bootstrap: BootstrapComponents)

my_card = Papercraft.html {
  bootstrap.card(style: 'width: 18rem') {
    bootstrap.card_title 'Card title'
    bootstrap.card_subtitle 'Card subtitle'
    bootstrap.card_text 'Some quick example text.'
    bootstrap.card_link '#', 'Card link'
    bootstrap.card_link '#', 'Another link'
    ...
  }
}

How Papercraft is Used by this website

Papercraft is used by this website, which is, just so you know, not a static website. While the source layout of this website largely resembles a Jamstack website, it is rendered dynamically by Impression, a web framework I’m currently developing for my own use, which in turn relies heavily on Papercraft for dealing with layouts.

So let’s explore how this website is constructed. The file for the present article is just a markdown file with some YAML front matter (permalink):

---
title: Papercraft - Composable Templating for Ruby
layout: article
---

Papercraft is a new Ruby gem I've been working on, that provides a new way to
render HTML, XML and JSON using plain Ruby. Here's what it looks like:

...

Here’s the layout used for rendering articles (permalink):

default = import './default'

export_default default.apply { |title:, date:, **props|
  article {
    h1 title
    h3 date.strftime('%d·%m·%Y'), class: 'date'
    emit_yield
  }
}

The article layout above imports the default layout (using Modulation), and uses #apply to create a derivative layout that adds an article element and expects a nested block to be injected into it.

And here is the default layout (permalink):

require 'papercraft'

export_default Papercraft.html { |**props|
  html5 {
    head {
      title(props[:title] ? "Noteflakes - #{props[:title]}" : "Noteflakes")
      meta charset: 'utf-8'
      meta name: 'viewport', content: 'width=device-width, initial-scale=1.0'
      style 'body { display: none }' # prevent FUOC
      link rel: 'icon', type: 'image/png', href: '/assets/nf-icon-black.png'
      link rel: 'stylesheet', type: 'text/css', href: '/assets/style.css'
      link rel: 'alternate', type: 'application/rss+xml', href: '/feeds/rss'
    }
    body {
      header {
        h1 {
          a(href: '/') {
            img src: '/assets/nf-icon-black.png'
            span 'noteflakes'
          } 
        }
        ul {
          li 'by Sharon Rosner', class: 'byline'
          li { a 'archive', href: '/archive' }
          li { a 'about', href: '/about' }
          li { a 'RSS feed', href: '/feeds/rss' }
          li { a 'code', href: 'https://github.com/noteflakes', target: '_blank' }
        }
      }
      emit_yield **props
      footer {
        hr
        p {
          span 'Copyright © 2021 Sharon Rosner. This site runs on '
          a 'Impression', href: 'https://github.com/digital-fabric/impression'
          span ' and '
          a 'Tipi', href: 'https://github.com/digital-fabric/tipi'
          span '.'
        }
      }
    }
  }
}

Finally, Impression takes the layout referenced in the article’s front matter, and renders it by passing a block that renders the markdown:

def render_markdown_file(req, path_info)
  layout = get_layout(path_info[:layout])

  html = layout.render(request: req, resource: self, **path_info) {
    emit path_info[:html_content]
  }
  req.respond(html, 'Content-Type' => layout.mime_type)
end

Other templates on this website produce RSS and a JSON feed. Here’s the RSS template (permalink):

require 'papercraft'

export_default Papercraft.xml(mime_type: 'text/xml; charset=utf-8') { |resource:, **props|
  rss(version: '2.0', 'xmlns:atom' => 'http://www.w3.org/2005/Atom') {
    channel {
      title 'Noteflakes'
      link 'https://noteflakes.com/'
      description 'A website by Sharon Rosner'
      language 'en-us'
      pubDate Time.now.httpdate
      emit '<atom:link href="https://noteflakes.com/feeds/rss" rel="self" type="application/rss+xml" />'
      
      article_entries = resource.page_list('/articles').reverse

      article_entries.each { |e|
        item {
          title e[:title]
          link "https://noteflakes.com#{e[:url]}"
          guid "https://noteflakes.com#{e[:url]}"
          pubDate e[:date].to_time.httpdate
          description e[:html_content]
        }  
      }
    }
  }
}

A note on Papercraft’s design

You will have noticed that Papercraft’s DSL looks quite terse, and that is because all of its API consists basically of unqualified method calls that look like html { body { h1 'foo' } }. This might look confusing to the young Ruby padawan, as they might ask “where do all these calls go, and how are they intercepted?”

The answer is that all template procs are evaulated in the context of a Papercraft renderer instance, which intercepts all calls and turns them into chunks of HTML/XML that are added to an internal buffer (the JSON renderer works in a slightly different manner).

On one hand, this allows us to write templates with a minimum of boilerplate, and have templates that look very clean. On the other hand, it does prevents us notably from using instance variables, e.g. @foo in our templates, and doesn’t let us use any methods in the scope of the receiver where we create our template. For example, the following code will fail to produce the desired result:

class Foo
  def to_html
    Papercraft.html { h1 bar }
  end

  def bar
    'bar'
  end
end

Foo.new.to_html #=> not what you might expect...

So, when writing Papercraft templates we need to follow a few rules:

Conclusion

Papercraft is a new Ruby gem that lets you dynamically generate HTML, XML and JSON documents using plain Ruby. Papercraft templates use explicit parameter passing in order to “bind” template variables, and use application and composition to combine templates in a variety of ways. This website is the first to use Papercraft “in production”, and I hope other people will find it useful.

To learn more about Papercraft, checkout the API documentation. Contributions in the form of issues or pull requests will be gladly accepted on the Papercraft repository.