Papercraft

Introduction

Overview

Getting Started

Community

Basic Usage

Writing Templates

Rendering Templates

HTML Elements

Built-in Methods

Template Composition

Component Templates

Working with Layouts

Advanced Usage

Deferred Rendering

Extending Papercraft

Papercraft Internals

How Papercraft Works

How Papercraft Works

Papercraft employs some innovative techniques for achieving a rendering performance equal to compiled ERB templates. Just like in ERB/ERubi/Herb, Papercraft compiles the templates you write into highly-optimized code. Let’s look at a simple example:

->(name) {
  div {
    h1 {
      a "Hello, #{name}!", href: '/welcome'
    }
  }
}

From the point of view of the Ruby runtime, running the code in this template and generating HTML from it will necessitate some implicit buffer (because it doesn’t appear anywhere in the template code), and all those tag methods (div, h1 and a) to be defined somewhere, either in the local scope of the template, or in the Kernel module. Doing this is not only very intrusive, but also slow. So how does Papercraft do it?

Template Compilation

The first step in compiling a template is to read its source code and transform it into an AST (abstract syntax tree). This is done by reading the source location of the template lambda, reading the source file and parsing the lambda’s source code using Prism, the new Ruby source code parser shipped with Ruby.

The template’s AST is then transformed, and each receiver-less method call is mutated into a special TagNode that signifies that an HTML tag is to be emitted into an output buffer.

Finally, a special-purpose compiler converts the transformed AST back to source code, coalescing consecutive HTML snippets into optimized buffer concatenation calls, to finally produce the following code:

->(__buffer__, name) {
  __buffer__.<<("<div><h1><a href=\"/welcome\">").
            .<<(ERB::Escape.html_escape(
              ("Hello, #{name}!")
            ))
            .<<("</a></h1></div>")
  __buffer__
}

(The above code was edited to add whitespace for better readability.)

The compiled version of the template adds __buffer__ as its first argument, but otherwise keeps the same arguments as in the original template code. The static parts (the tag names and any literal values) are coalesced into a single push operation, while the dynamic parts are passed through html_escape in order to properly escape them and prevent HTML/Javascript injection. Finally the buffer is returned as the return value.

So, when you call #render on the template, Papercraft performs the compilation automatically (on the first invocation), calls the compiled code with a string buffer, and returns the buffer.