Skip to content

Commit

Permalink
Initial version
Browse files Browse the repository at this point in the history
  • Loading branch information
sborrazas committed Oct 10, 2014
1 parent 8ac53e5 commit aadb066
Show file tree
Hide file tree
Showing 17 changed files with 409 additions and 0 deletions.
74 changes: 74 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
Armadillo
=========

A small library for [Django-like template inheritance](https://docs.djangoproject.com/en/dev/topics/templates/#template-inheritance)
adapted for ERB.

Usage
-----

To render an Armadillo template you need to call the `Armadillo.render` method.

This method accepts any of the following options:
* `:scope` - Any object you want to bound to the template scope.
* `:base_path` - The path of the directory for which the templates are going to
be searched on.

Note: A `.erb` extension is assumed for every file and should not be part of
the filename given as the template filename.


```ruby
Armadillo.render("myview.html", { :items => [1, 2, 3] }, {
:base_path => File.join(Dir.pwd, "views"),
:scope => self
})
```

```erb
<!-- views/myview.html.erb -->
<% extends("base.html") %>
<% vlock(:title) do %>
<%= current_user.name %>
<% end %>
<% vlock(:body) do %>
<ul>
<% items.each do |item| %>
<li><%= item %></li>
<% end %>
</ul>
<% end %>
<!-- views/base.html.erb -->
<!DOCTYPE>
<html>
<title><% vlock(:title) %> - MyApp</title>
<body>
<% vlock(:body) %>
</body>
</html>
```

### Usage example using Cuba

```ruby
module View
def render_view(template_name, locals = {})
content = Armadillo.render(template_name, locals, {
:base_path => File.join(APP_PATH, "views"),
:scope => self,
:escape_html => true
})
res.write(content)
halt(res.finish)
end
end

on get, root do
render_view("main/index.html", {
:items => [1, 2, 3]
})
end
```
7 changes: 7 additions & 0 deletions Rakefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
require "rake/testtask"

desc "Run all tests"
Rake::TestTask.new(:test) do |t|
t.pattern = "./spec/**/*_spec.rb"
t.verbose = false
end
24 changes: 24 additions & 0 deletions armadillo.gemspec
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
Gem::Specification.new do |s|
s.name = "armadillo"
s.version = "0.0.1"
s.summary = "Template inheritance with ERB templates"
s.description = "A small library for Django-like template inheritance adapted for ERB"
s.authors = ["Sebastian Borrazas"]
s.email = ["seba.borrazas@gmail.com"]
s.homepage = "http://github.com/sborrazas/armadillo"
s.license = "MIT"

s.files = Dir[
"LICENSE",
"README.md",
"Rakefile",
"lib/**/*.rb",
"*.gemspec",
"spec/*.*"
]

s.require_paths = ["lib"]

s.add_dependency "tilt"

end
171 changes: 171 additions & 0 deletions lib/armadillo.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
require "tilt"
require "delegate"

module Armadillo

VERSION = "0.0.1"

DEFAULT_OPTIONS = {
:default_encoding => Encoding.default_external,
:outvar => "@_output"
}

# @api private
class TemplateContext < SimpleDelegator

# Extend the specified template in which the inner view blocks will be
# rendered.
#
# @note
# This is a template instruction.
#
# @param template_path [String]
# @param locals [Hash]
def extends(template_path, locals = {})
@extends_data = [template_path, locals]
end

# Determine the contents or specify the place in which the view block will
# be rendered.
#
# @note
# This is a template instruction.
#
# @param block_name [Symbol]
# @param block [Block]
def vlock(block_name, &block)
raise "Invalid vlock usage" unless current_frame

if extends?
raise "No block given" unless block_given?

current_frame[:vlocks][block_name] = block
elsif (frame = get_frame(block_name, current_frame))
temporary_frame(frame[:parent_frame]) do
frame[:vlocks][block_name].call
end
elsif block_given?
block.call
end
end

# Create a new frame with previous frame as parent.
def create_frame
@current_frame = {
:vlocks => {},
:parent_frame => current_frame
}
end

# Determine if the current template should extend from a new template.
#
# @return [Boolean]
def extends?
!! @extends_data
end

# Return and delete the extract data.
#
# @return [Array<(String, Hash)>]
# The extended template name and the locals.
def extract_extends_data
@extends_data.tap { @extends_data = nil }
end

private

# Get the current frame. Each frame contains the blocks specified using
# #vlock and its parent frame.
#
# @return [Hash]
def current_frame
@current_frame
end

# Create a temporary current frame for the block to be executed.
#
# @param frame [Hash]
# @param block [Block]
def temporary_frame(frame, &block)
old = current_frame
@current_frame = frame
block.call
@current_frame = old
end

# Get the block from the frames stack by its name.
#
# @param block_name [Symbol]
def get_frame(block_name, frame)
if frame[:vlocks].has_key?(block_name)
frame
elsif frame[:parent_frame]
get_frame(block_name, frame[:parent_frame])
end
end
end

# Render the erb template.
#
# @param template_path [String]
# @param locals [Hash]
# @option options [Object] :scope (Object.new)
# Any object you want to bound to the template scope.
# @option options [String, nil] :base_path (nil)
# The path of the directory for which the templates are going to be
# searched on.
#
# @note
# options also accepts any options offered by the Erubis templating system.
#
# @return [String]
# @api public
def self.render(template_path, locals = {}, options = {})
scope = options.fetch(:scope) { Object.new }
context = TemplateContext.new(scope)
_render(template_path, locals, context, options)
end

# Render the erb template with the given context.
#
# @param template_path [String]
# @param context [Armadillo::TemplateContext]
# @param locals [Hash]
# @option options [String] :base_path (nil)
#
# @note
# options also accepts any options offered by the Erubis templating system.
#
# @api private
def self._render(template_path, locals, context, options)
context.create_frame
template_path = "#{template_path}.erb"
if (base_path = options.fetch(:base_path, nil))
template_path = File.join(base_path, template_path)
end
template = _templates_cache.fetch(template_path) do
Tilt.new(template_path, 1, DEFAULT_OPTIONS.merge(options))
end

content = template.render(context, locals)

if context.extends?
template_path, locals = context.extract_extends_data
content = _render(template_path, locals, context, options)
end

content
end
private_class_method :_render

# Get Tilt templates cache.
#
# @return [Tilt::Cache]
#
# @api private
def self._templates_cache
Thread.current[:tilt_cache] ||= Tilt::Cache.new
end
private_class_method :_templates_cache

end
3 changes: 3 additions & 0 deletions spec/spec_helper.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
require "minitest/spec"
require "minitest/autorun"
require "armadillo"
81 changes: 81 additions & 0 deletions spec/template_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
require_relative "spec_helper"

describe Armadillo do

TEMPLATES_PATH = File.join(File.dirname(__FILE__), "templates")

def assert_lines_match(content, lines)
content_lines = content.split("\n")
lines.each do |line|
assert_includes(content_lines, line)
end
end

describe ".render" do
it "renders a regular erb template" do
locals = { :items => ["a", "b", "c"] }
content = Armadillo.render("basic.text", locals, {
:base_path => TEMPLATES_PATH
})
assert_lines_match(content, ["Basic", "a", "b", "c"])
end

it "renders a one-step inheritance template" do
content = Armadillo.render("one_step_1.text", {}, {
:base_path => TEMPLATES_PATH
})
assert_lines_match(content, ["Base", "Title", "Subtitle"])
end

it "allows parent templates to access locals from #extends" do
locals = { :items => ["a", "b", "c"] }

content = Armadillo.render("parent_locals_2.text", locals, {
:base_path => TEMPLATES_PATH
})
assert_lines_match(content, ["Base", locals[:items].first])
end

it "renders a two-step inheritance template" do
content = Armadillo.render("two_step_2.text", {}, {
:base_path => TEMPLATES_PATH
})
assert_lines_match(content, ["Base", "Title", "Subtitle"])
end

describe "when reusing child vlocks" do
it "renders them according to the inheritance" do
content = Armadillo.render("nested_two_step_2.text", {}, {
:base_path => TEMPLATES_PATH
})
assert_lines_match(content, ["Base", "Title - Subtitle"])
end
end

describe "when sending a scope object" do
it "access the object methods as locals" do
obj = Object.new
def obj.some_text
"text!"
end

content = Armadillo.render("scope_object.text", {}, {
:base_path => TEMPLATES_PATH,
:scope => obj
})
assert_lines_match(content, ["Base", obj.some_text])
end
end

describe "when sending :escape_html option" do
it "sanitizes the HTML by default" do
content = Armadillo.render("sanitized.html", {}, {
:base_path => TEMPLATES_PATH,
:escape_html => true
})
assert_lines_match(content, ["Sanitized &amp;", "Not sanitized &"])
end
end
end

end
3 changes: 3 additions & 0 deletions spec/templates/base.text.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Base
<% vlock(:title) %>
<% vlock(:subtitle) %>
4 changes: 4 additions & 0 deletions spec/templates/basic.text.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Basic
<% items.each do |item| %>
<%= item %>
<% end %>
5 changes: 5 additions & 0 deletions spec/templates/nested_two_step_1.text.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<% extends("base.text") %>
<% vlock(:title) do %>
Title - <% vlock(:title) %>
<% end %>
5 changes: 5 additions & 0 deletions spec/templates/nested_two_step_2.text.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<% extends("nested_two_step_1.text") %>
<% vlock(:title) do %>
Subtitle
<% end %>
9 changes: 9 additions & 0 deletions spec/templates/one_step_1.text.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<% extends("base.text") %>
<% vlock(:title) do %>
Title
<% end %>
<% vlock(:subtitle) do %>
Subtitle
<% end %>
Loading

0 comments on commit aadb066

Please sign in to comment.