Project

papercraft

0.06
The project is in a healthy, maintained state
Papercraft: component-based HTML templating for Ruby
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
 Dependencies

Development

~> 2.7.0
~> 5.15
~> 2.0.9

Runtime

~> 1.2.1
~> 2.3.1
~> 3.27.0
 Project Readme


Papercraft

Composable templating for Ruby

Ruby gem Tests MIT License

API reference

What is Papercraft?

Papercraft is a templating engine for dynamically producing HTML, XML or JSON. Papercraft templates are expressed in plain Ruby, leading to easier debugging, better protection against HTML/XML injection attacks, and better code reuse.

Papercraft templates can be composed in a variety of ways, facilitating the usage of layout templates, and enabling a component-oriented approach to building complex web interfaces.

In Papercraft, dynamic data is passed explicitly to the template as block arguments, making the data flow easy to follow and understand. Papercraft also lets developers create derivative templates using full or partial parameter application.

Papercraft includes built-in support for rendering Markdown (using Kramdown), as well as support for creating template extensions in order to allow the creation of component libraries.

Papercraft automatically escapes all text emitted in templates according to the template type. For more information see the section on escaping content.

require 'papercraft'

page = Papercraft.html { |*args|
  html {
    head { title 'Title' }
    body { emit_yield *args }
  }
}
page.render { p 'foo' }
#=> "<html><head><title>Title</title></head><body><p>foo</p></body></html>"

hello = page.apply { |name| h1 "Hello, #{name}!" }
hello.render('world')
#=> "<html><head><title>Title</title></head><body><h1>Hello, world!</h1></body></html>"

Table of Content

  • Installing Papercraft
  • Basic Usage
  • Adding Tags
  • Tag and Attribute Formatting
  • Escaping Content
  • Template Parameters
  • Template Logic
  • Template Blocks
  • Plain Procs as Templates
  • Template Composition
  • Parameter and Block Application
  • Higher-Order Templates
  • Layout Template Composition
  • Emitting Raw HTML
  • Emitting a String with HTML Encoding
  • Emitting Markdown
  • Working with MIME Types
  • Deferred Evaluation
  • XML Templates
  • JSON Templates
  • Papercraft Extensions
    • Bundled Extensions
  • API Reference

Installing Papercraft

Using bundler:

gem 'papercraft'

Or manually:

$ gem install papercraft

Basic Usage

To create an HTML template use Papercraft.html:

require 'papercraft'

html = Papercraft.html {
  div(id: 'greeter') { p 'Hello!' }
}

(You can also use Papercraft.xml and Papercraft.json to create XML and JSON templates, respectively.)

Rendering a template is done using #render:

html.render #=> "<div id="greeter"><p>Hello!</p></div>"

Adding Tags

Tags are added using unqualified method calls, and can be nested using blocks:

Papercraft.html {
  html {
    head {
      title 'page title'
    }
    body {
      article {
        h1 'article title'
      }
    }
  }
}

Tag methods accept a string argument, a block, or no argument at all:

Papercraft.html { p 'hello' }.render #=> "<p>hello</p>"

Papercraft.html { p { span '1'; span '2' } }.render #=> "<p><span>1</span><span>2</span></p>"

Papercraft.html { hr() }.render #=> "<hr/>"

Tag methods also accept tag attributes, given as a hash:

Papercraft.html { img src: '/my.gif' }.render #=> "<img src="/my.gif"/>

Papercraft.html { p "foobar", class: 'important' }.render #=> "<p class=\"important\">foobar</p>"

Tag and Attribute Formatting

Papercraft does not make any presumption about what tags and attributes you can use. You can mix upper and lower case letters, and you can include arbitrary characters in tag and attribute names. However, in order to best adhere to the HTML and XML specs and common practices, tag names and attributes will be formatted according to the following rules, depending on the template type:

  • HTML: underscores are converted to dashes:

    Papercraft.html {
      foo_bar { p 'Hello', data_name: 'world' }
    }.render #=> '<foo-bar><p data-name="world">Hello</p></foo-bar>'
  • XML: underscores are converted to dashes, double underscores are converted to colons:

    Papercraft.xml {
      soap__Envelope(
        xmlns__soap:  'http://schemas.xmlsoap.org/soap/envelope/',
      ) { }
    }.render #=> '<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"><soap:Envelope>'

If you need more precise control over tag names, you can use the #tag method, which takes the tag name as its first parameter, then the rest of the parameters normally used for tags:

Papercraft.html {
  tag 'cra_zy__:!tag', 'foo'
}.render #=> '<cra_zy__:!tag>foo</cra_zy__:!tag>'

Escaping Content

Papercraft automatically escapes all text content emitted in a template. The specific escaping algorithm depends on the template type. For both HTML and XML templates, Papercraft uses escape_utils, specifically:

  • HTML: escape_utils.escape_html
  • XML: escape_utils.escape_xml

In order to emit raw HTML/XML, you can use the #emit method as described below.

JSON templates are rendered using the json gem bundled with Ruby, which takes care of escaping text values.

Template Parameters

In Papercraft, parameters are always passed explicitly. This means that template parameters are specified as block parameters, and are passed to the template on rendering:

greeting = Papercraft.html { |name| h1 "Hello, #{name}!" }
greeting.render('world') #=> "<h1>Hello, world!</h1>"

Templates can also accept named parameters:

greeting = Papercraft.html { |name:| h1 "Hello, #{name}!" }
greeting.render(name: 'world') #=> "<h1>Hello, world!</h1>"

Template Logic

Since Papercraft templates are just a bunch of Ruby, you can easily write your view logic right in the template:

Papercraft.html { |user = nil|
  if user
    span "Hello, #{user.name}!"
  else
    span "Hello, guest!"
  end
}

Template Blocks

Templates can also accept and render blocks by using emit_yield:

page = Papercraft.html {
  html {
    body { emit_yield }
  }
}

# we pass the inner HTML
page.render { h1 'hi' }

Plain Procs as Templates

With Papercraft you can write a template as a plain Ruby proc, and later render it by passing it as a block to Papercraft.html:

greeting = proc { |name| h1 "Hello, #{name}!" }
Papercraft.html(&greeting).render('world')

Components can also be expressed using lambda notation:

greeting = ->(name) { h1 "Hello, #{name}!" }
Papercraft.html(&greeting).render('world')

Template Composition

Papercraft makes it easy to compose multiple templates into a whole HTML document. A Papercraft template can contain other templates, as the following example shows.

Title = ->(title) { h1 title }

Item = ->(id:, text:, checked:) {
  li {
    input name: id, type: 'checkbox', checked: checked
    label text, for: id
  }
}

ItemList = ->(items) {
  ul {
    items.each { |i|
      Item(**i)
    }
  }
}

page = Papercraft.html { |title, items|
  html5 {
    head { Title(title) }
    body { ItemList(items) }
  }
}

page.render('Hello from composed templates', [
  { id: 1, text: 'foo', checked: false },
  { id: 2, text: 'bar', checked: true }
])

In addition to using templates defined as constants, you can also use non-constant templates by invoking the #emit method:

greeting = -> { span "Hello, world" }

Papercraft.html {
  div {
    emit greeting
  }
}

Parameter and Block Application

Parameters and blocks can be applied to a template without it being rendered, by using #apply. This mechanism is what allows template composition and the creation of higher-order templates.

The #apply method returns a new template which applies the given parameters and or block to the original template:

# parameter application
hello = Papercraft.html { |name| h1 "Hello, #{name}!" }
hello_world = hello.apply('world')
hello_world.render #=> "<h1>Hello, world!</h1>"

# block application
div_wrap = Papercraft.html { div { emit_yield } }
wrapped_h1 = div_wrap.apply { h1 'hi' }
wrapped_h1.render #=> "<div><h1>hi</h1></div>"

# wrap a template
wrapped_hello_world = div_wrap.apply(&hello_world)
wrapped_hello_world.render #=> "<div><h1>Hello, world!</h1></div>"

Higher-Order Templates

Papercraft also lets you create higher-order templates, that is, templates that take other templates as parameters, or as blocks. Higher-order templates are handy for creating layouts, wrapping templates in arbitrary markup, enhancing templates or injecting template parameters.

Here is a higher-order template that takes a template as parameter:

div_wrap = Papercraft.html { |inner| div { emit inner } }
greeter = Papercraft.html { h1 'hi' }
wrapped_greeter = div_wrap.apply(greeter)
wrapped_greeter.render #=> "<div><h1>hi</h1></div>"

The inner template can also be passed as a block, as shown above:

div_wrap = Papercraft.html { div { emit_yield } }
wrapped_greeter = div_wrap.apply { h1 'hi' }
wrapped_greeter.render #=> "<div><h1>hi</h1></div>"

Layout Template Composition

One of the principal uses of higher-order templates is the creation of nested layouts. Suppose we have a website with a number of different layouts, and we'd like to avoid having to repeat the same code in the different layouts. We can do this by creating a default page template that takes a block, then use #apply to create the other templates:

default_layout = Papercraft.html { |**params|
  html5 {
    head {
      title: params[:title]
    }
    body {
      emit_yield(**params)
    }
  }
}

article_layout = default_layout.apply { |title:, body:|
  article {
    h1 title
    emit_markdown body
  }
}

article_layout.render(
  title: 'This is a title',
  body: 'Hello from *markdown body*'
)

Emitting Raw HTML

Raw HTML can be emitted using #emit:

wrapped = Papercraft.html { |html| div { emit html } }
wrapped.render("<h1>hi</h1>") #=> "<div><h1>hi</h1></div>"

Emitting a String with HTML Encoding

To emit a string with proper HTML encoding, without wrapping it in an HTML element, use #text:

Papercraft.html { text 'hi&lo' }.render #=> "hi&amp;lo"

Emitting Markdown

Markdown is rendered using the Kramdown gem. To emit Markdown, use #emit_markdown:

template = Papercraft.html { |md| div { emit_markdown md } }
template.render("Here's some *Markdown*") #=> "<div><p>Here's some <em>Markdown</em><p>\n</div>"

Kramdown options can be specified by adding them to the #emit_markdown call:

template = Papercraft.html { |md| div { emit_markdown md, auto_ids: false } }
template.render("# title") #=> "<div><h1>title</h1></div>"

The #emit_markdown method is available only to HTML templates. If you need to render markdown in XML or JSON templates (usually for implementing RSS or JSON feeds), you can use Papercraft.markdown directly:

Papercraft.markdown('# title') #=> "<h1>title</h1>"

The default Kramdown options are:

{
  entity_output: :numeric,
  syntax_highlighter: :rouge,
  input: 'GFM',
  hard_wrap: false  
}

The deafult options can be configured by accessing Papercraft.default_kramdown_options, e.g.:

Papercraft.default_kramdown_options[:auto_ids] = false

Working with MIME Types

Papercraft lets you set and interrogate a template's MIME type, in order to be able to dynamically set the Content-Type HTTP response header. A template's MIME type can be set when creating the template, e.g. Papercraft.xml(mime_type: 'application/rss+xml'). You can interrogate the template's MIME type using #mime_type:

# using Qeweney (https://github.com/digital-fabric/qeweney)
def serve_template(req, template)
  body = template.render
  respond(body, 'Content-Type' => template.mime_type)
end

Deferred Evaluation

Deferred evaluation allows deferring the rendering of parts of a template until the last moment, thus allowing an inner template to manipulate the state of the outer template. To in order to defer a part of a template, use #defer, and include any markup in the provided block. This technique, in in conjunction with holding state in instance variables, is an alternative to passing parameters, which can be limiting in some situations.

A few use cases for deferred evaulation come to mind:

  • Setting the page title.
  • Adding a flash message to a page.
  • Using templates that dynamically add static dependencies (JS and CSS) to the page.

The last use case is particularly interesting. Imagine a DependencyMananger class that can collect JS and CSS dependencies from the different templates integrated into the page, and adds them to the page's <head> element:

default_layout = Papercraft.html { |**args|
  @dependencies = DependencyMananger.new
  head {
    defer { emit @dependencies.head_markup }
  }
  body { emit_yield **args }
}

button = proc { |text, onclick|
  @dependencies.js '/static/js/button.js'
  @dependencies.css '/static/css/button.css'

  button text, onclick: onclick
}

heading = proc { |text|
  @dependencies.js '/static/js/heading.js'
  @dependencies.css '/static/css/heading.css'

  h1 text
}

page = default_layout.apply {
  emit heading, "What's your favorite cheese?"

  emit button, 'Beaufort', 'eat_beaufort()'
  emit button, 'Mont d''or', 'eat_montdor()'
  emit button, 'Époisses', 'eat_epoisses()'
}

XML Templates

XML templates behave largely the same as HTML templates, with a few minor differences. XML templates employ a different encoding algorithm, and lack some specific HTML functionality, such as emitting Markdown.

Here's an example showing how to create an RSS feed:

rss = 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]
        }  
      }
    }
  }
}

JSON Templates

JSON templates behave largely the same as HTML and XML templates. The only major difference is that for adding array items you'll need to use the #item method:

Papercraft.json {
  item 1
  item 2
  item 3
}.render #=> "[1,2,3]"

Otherwise, you can create arbitrarily complex JSON structures by mixing hashes and arrays:

Papercraft.json {
  foo {
    bar {
      item nil
      item true
      item 123.456
    }
  }
}.render #=> "{\"foo\":{\"bar\":[null,true,123.456]}}"

Papercraft uses the JSON gem under the hood in order to generate actual JSON.

Papercraft Extensions

Papercraft extensions are modules that contain one or more methods that can be used to render complex HTML components. Extension modules can be used by installing them as a namespaced extension using Papercraft::extension. Extensions are particularly useful when you work with CSS frameworks such as Bootstrap, Tailwind or Primer.

For example, to create a Bootstrap card component, the following HTML markup is needed (example taken from the Bootstrap docs):

<div class="card" style="width: 18rem;">
  <div class="card-body">
    <h5 class="card-title">Card title</h5>
    <h6 class="card-subtitle mb-2 text-muted">Card subtitle</h6>
    <p class="card-text">Some quick example text to build on the card title and make up the bulk of the card's content.</p>
    <a href="#" class="card-link">Card link</a>
    <a href="#" class="card-link">Another link</a>
  </div>
</div>

With Papercraft, we could create a Bootstrap extension with a #card method and other associated methods:

module BootstrapComponents
  def card(**props, &block)
    div(class: 'card', **props) {
      div(class: 'card-body', &block)
    }
  end
  
  def card_title(title)
    h4(title, class: 'card-title')
  end

  def card_subtitle(subtitle)
    h5(subtitle, class: 'card-subtitle')
  end

  def card_text(text)
    p(text, class: 'card-text')
  end

  def card_link(text, **opts)
    a(text, class: 'card-link', **opts)
  end
end

Papercraft.extension(bootstrap: BootstrapComponents)

The call to Papercraft::extension lets us access the different methods of BootstrapComponents by calling #bootstrap inside a template. With this, we'll be able to express the above markup as follows:

Papercraft.html {
  bootstrap.card(style: 'width: 18rem') {
    bootstrap.card_title 'Card title'
    bootstrap.card_subtitle 'Card subtitle'
    bootstrap.card_text 'Some quick example text to build on the card title and make up the bulk of the card''s content.'
    bootstrap.card_link 'Card link', href: '#foo'
    bootstrap.card_link 'Another link', href: '#bar'
  }
}

Bundled Extensions

Papercraft comes bundled with a few extensions that address common use cases. All bundled extensions are namespaced under Papercraft::Extensions, and must be specifically required in order to be available to templates.

For all bundled Papercraft extensions, there's no need to call Papercraft.extension, requiring the extension is sufficient.

SOAP Extension

The SOAP extension was contributed by @aemadrid.

The SOAP extension provides common tags for building SOAP payloads. To load the SOAP extensions, require polyphony/extensions/soap. The extension provides the following methods:

  • soap.Body(...) - emits a soap:Body tag.
  • soap.Envelope(...) - emits a soap:Envelope tag.
  • soap.Fault(...) - emits a soap:Fault tag.
  • soap.Header(...) - emits a soap:Header tag.

As mentioned above, namespaced tags and attributes may be specified by using double underscores for colons. Other tags that contain special characters may be emitted using the #tag method:

require 'polyphony/extensions/soap'

xml = Papercraft.xml {
  soap.Envelope(
    xmlns__xsd: 'http://www.w3.org/2001/XMLSchema',
    xmlns__xsi: 'http://www.w3.org/2001/XMLSchema-instance'
  ) {
    soap.Body {
      PosRequest(xmlns: 'http://Some.Site') {
        tag('Ver1.0') {
          Header {
            SecretAPIKey 'some_secret_key'
          }
          Transaction {
            SomeData {}
          }
        }
      }
    }
  }
}

API Reference

The API reference for this library can be found here.