Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Loading…

Smart Templates #70

Merged
2 commits merged into from

2 participants

@rkh
Owner

Improved templates a bit:

  • Template engines now set default content type (i.e. using sass will default to css rather than html). Content types can still be set explicitly and all corner cases should be covered.
  • Nested templates don't use implicit layouts anymore. Eases the use of rendering methods for partials.

Those are two separate features, but applying the first without the latter to master results in a merge conflict that does not need to be resolved in case we decide to merge both.

Tests are, of course, included.

rkh added some commits
@rkh rkh Skip implicit layouts for nested templates.
That way the following will produce valid HTML:

@@ layout
!!!
= yield

@@ content
%html
  %head= haml :head
  %body= haml :body

That way using render methods for partials is a lot easier.
Tests included.
e5e0047
@rkh rkh Sets default content type according to template engine used instead o…
…f just text/html.

It does so by including a Mixin into the the returned string offering a content_type method. Therefore all of the following examples produce the expected results:

    # text/html
    get('/') do
      haml :index
    end

    # text/css
    get('/') do
      sass :index
    end

    # text/css
    get('/') do
      haml :index
      sass :index
    end

    # text/html
    get('/') do
      haml '= sass :index'
    end

It also allows setting the default content type for a template engine:

    set :builder, :content_type => :html

Tests and README adjustments (all languages) included.
1d676f4
@rkh
Owner
rkh commented

Since there is no feedback so far and these are just minor changes (no real API additions), I'll merge it into master. If there are any complains, I will revert the changes.

@ryansobol

Huh? Is this really the best way of adding a content_type accessor to a String instance (I presume) ? +1 clever points at least. :P

Owner

It avoids tons of edge cases (what if you nest different template engines, what if you call haml just for fun and return something different instead). Even though it doesn't seem so at first glance, this is rather noninvasive and OO. By extending output with a mixin we avoid monkey-patching plus get better documentation. But if you have another approach in mind, I'd love to wrap my head around it.

Your rationale seems sound, although I'm still wrestling how we get "better documentation" with this technique. I guess it makes sense if you're referring exclusively to Sinatra contributors. I might have gone with a different technique, encapsulating output and it's meta-data in a first-class object. But this surely would have lead to more code, more tests, more potential bugs, and more time. Your technique is quick and precise, which is essential for the next release.

BTW - Konstantin, you've done a f*cking amazing job with organizing this 1.1 release. I'm really excited about smart templates. When I upgrade Sinatra in my project, I'm going to delete so much code! And my users are going to love having more template language choices! I hope you don't mind a little peer-reviewing from me.

Owner

Thanks. No, it's fine, welcomed even. The reason for not creating a own class is that we had to cover every possibility where such a class could be handed to rack, including using it for streaming (wrapping it in another project) or with tools like async-sinatra, as Rack expects strings. Returning something that just behaves like a string would violate the Rack spec. With documentation I mean as opposed to monkey-patching the string directly. The mixin can actually show up in generated documentation (not that it matters much without comments, but it's a start). If you got more feedback/discussion, keep it coming.

It's no question that compatibility with the Rack spec is paramount (for all Ruby web frameworks). And I would never advocate violating that. Using the technique of a wrapper class, as I've hinted, would require more work to ensure compatibility with Rack (and with async-sinatra for that matter). Right now, I don't think it's worth the time investment.

As far as improving Sinatra's documentation, I wish I had more time to pitch in. :( It's a good thing the source is so readable already. :P I've actually added quite a few techniques into my own toolbox from periodically perusing the source.

This issue was closed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Sep 27, 2010
  1. @rkh

    Skip implicit layouts for nested templates.

    rkh authored
    That way the following will produce valid HTML:
    
    @@ layout
    !!!
    = yield
    
    @@ content
    %html
      %head= haml :head
      %body= haml :body
    
    That way using render methods for partials is a lot easier.
    Tests included.
  2. @rkh

    Sets default content type according to template engine used instead o…

    rkh authored
    …f just text/html.
    
    It does so by including a Mixin into the the returned string offering a content_type method. Therefore all of the following examples produce the expected results:
    
        # text/html
        get('/') do
          haml :index
        end
    
        # text/css
        get('/') do
          sass :index
        end
    
        # text/css
        get('/') do
          haml :index
          sass :index
        end
    
        # text/html
        get('/') do
          haml '= sass :index'
        end
    
    It also allows setting the default content type for a template engine:
    
        set :builder, :content_type => :html
    
    Tests and README adjustments (all languages) included.
This page is out of date. Refresh to see the latest.
View
7 README.de.rdoc
@@ -229,7 +229,6 @@ Das buidler gem wird benötigt um Builder-Templates rendern zu können:
require 'builder'
get '/' do
- content_type 'application/xml', :charset => 'utf-8'
builder :index
end
@@ -243,7 +242,6 @@ Das haml gem wird benötigt um SASS-Templates rendern zu können:
require 'sass'
get '/stylesheet.css' do
- content_type 'text/css', :charset => 'utf-8'
sass :stylesheet
end
@@ -257,7 +255,6 @@ und individuell überschrieben werden.
set :sass, :style => :compact # Standard Sass-Style ist :nested
get '/stylesheet.css' do
- content_type 'text/css', :charset => 'utf-8'
sass :stylesheet, :style => :expanded # überschrieben
end
@@ -269,7 +266,6 @@ Das haml gem wird benötigt um SCSS-Templates rendern zu können:
require 'sass'
get '/stylesheet.css' do
- content_type 'text/css', :charset => 'utf-8'
scss :stylesheet
end
@@ -283,7 +279,6 @@ und individuell überschrieben werden.
set :scss, :style => :compact # Standard Scss-Style ist :nested
get '/stylesheet.css' do
- content_type 'text/css', :charset => 'utf-8'
scss :stylesheet, :style => :expanded # überschrieben
end
@@ -295,7 +290,6 @@ Das less gem wird benötigt um Less-Templates rendern zu können:
require 'less'
get '/stylesheet.css' do
- content_type 'text/css', :charset => 'utf-8'
less :stylesheet
end
@@ -434,7 +428,6 @@ Das coffee-script gem und das `coffee`-Programm werden benötigt um CoffeScript-
require 'coffee-script'
get '/application.js' do
- content_type 'text/javascript', :charset => 'utf-8'
coffee :application
end
View
3  README.jp.rdoc
@@ -154,7 +154,6 @@ builderを使うにはbuilderライブラリが必要です:
require 'builder'
get '/' do
- content_type 'application/xml', :charset => 'utf-8'
builder :index
end
@@ -168,7 +167,6 @@ Sassテンプレートを使うにはsassライブラリが必要です:
require 'sass'
get '/stylesheet.css' do
- content_type 'text/css', :charset => 'utf-8'
sass :stylesheet
end
@@ -182,7 +180,6 @@ see {Options and Configurations}[http://www.sinatrarb.com/configuration.html],
set :sass, {:style => :compact } # デフォルトのSass styleは :nested
get '/stylesheet.css' do
- content_type 'text/css', :charset => 'utf-8'
sass :stylesheet, :sass_options => {:style => :expanded } # 上書き
end
View
7 README.rdoc
@@ -225,7 +225,6 @@ The builder gem/library is required to render builder templates:
require 'builder'
get '/' do
- content_type 'application/xml', :charset => 'utf-8'
builder :index
end
@@ -239,7 +238,6 @@ The sass gem/library is required to render Sass templates:
require 'sass'
get '/stylesheet.css' do
- content_type 'text/css', :charset => 'utf-8'
sass :stylesheet
end
@@ -253,7 +251,6 @@ and overridden on an individual basis.
set :sass, :style => :compact # default Sass style is :nested
get '/stylesheet.css' do
- content_type 'text/css', :charset => 'utf-8'
sass :stylesheet, :style => :expanded # overridden
end
@@ -265,7 +262,6 @@ The sass gem/library is required to render Scss templates:
require 'sass'
get '/stylesheet.css' do
- content_type 'text/css', :charset => 'utf-8'
scss :stylesheet
end
@@ -279,7 +275,6 @@ and overridden on an individual basis.
set :scss, :style => :compact # default Scss style is :nested
get '/stylesheet.css' do
- content_type 'text/css', :charset => 'utf-8'
scss :stylesheet, :style => :expanded # overridden
end
@@ -291,7 +286,6 @@ The less gem/library is required to render Less templates:
require 'less'
get '/stylesheet.css' do
- content_type 'text/css', :charset => 'utf-8'
less :stylesheet
end
@@ -422,7 +416,6 @@ CoffeeScript templates:
require 'coffee-script'
get '/application.js' do
- content_type 'text/javascript', :charset => 'utf-8'
coffee :application
end
View
46 lib/sinatra/base.rb
@@ -77,10 +77,12 @@ def status(value=nil)
# evaluation is deferred until the body is read with #each.
def body(value=nil, &block)
if block_given?
- def block.each ; yield call ; end
+ def block.each; yield(call) end
response.body = block
- else
+ elsif value
response.body = value
+ else
+ response.body
end
end
@@ -137,6 +139,7 @@ def mime_type(type)
def content_type(type, params={})
mime_type = mime_type(type)
fail "Unknown media type: %p" % type if mime_type.nil?
+ params[:charset] ||= defined?(Encoding) ? Encoding.default_external.to_s.downcase : 'utf-8'
if params.any?
params = params.collect { |kv| "%s=%s" % kv }.join(', ')
response['Content-Type'] = [mime_type, params].join(";")
@@ -300,6 +303,10 @@ def back ; request.referer ; end
# :locals A hash with local variables that should be available
# in the template
module Templates
+ module ContentTyped
+ attr_accessor :content_type
+ end
+
include Tilt::CompileSite
def erb(template, options={}, locals={})
@@ -315,21 +322,22 @@ def haml(template, options={}, locals={})
end
def sass(template, options={}, locals={})
- options[:layout] = false
+ options.merge! :layout => false, :default_content_type => :css
render :sass, template, options, locals
end
def scss(template, options={}, locals={})
- options[:layout] = false
+ options.merge! :layout => false, :default_content_type => :css
render :scss, template, options, locals
end
def less(template, options={}, locals={})
- options[:layout] = false
+ options.merge! :layout => false, :default_content_type => :css
render :less, template, options, locals
end
def builder(template=nil, options={}, locals={}, &block)
+ options[:default_content_type] = :xml
options, template = template, nil if template.is_a?(Hash)
template = Proc.new { block } if template.nil?
render :builder, template, options, locals
@@ -360,7 +368,7 @@ def markaby(template, options={}, locals={})
end
def coffee(template, options={}, locals={})
- options[:layout] = false
+ options.merge! :layout => false, :default_content_type => :js
render :coffee, template, options, locals
end
@@ -371,14 +379,19 @@ def render(engine, data, options={}, locals={}, &block)
options[:outvar] ||= '@_out_buf'
# extract generic options
- locals = options.delete(:locals) || locals || {}
- views = options.delete(:views) || settings.views || "./views"
- layout = options.delete(:layout)
- layout = :layout if layout.nil? || layout == true
+ locals = options.delete(:locals) || locals || {}
+ views = options.delete(:views) || settings.views || "./views"
+ @default_layout = :layout if @default_layout.nil?
+ layout = options.delete(:layout)
+ layout = @default_layout if layout.nil? or layout == true
+ content_type = options.delete(:content_type) || options.delete(:default_content_type)
# compile and render template
- template = compile_template(engine, data, options, views)
- output = template.render(self, locals, &block)
+ layout_was = @default_layout
+ @default_layout = false if layout
+ template = compile_template(engine, data, options, views)
+ output = template.render(self, locals, &block)
+ @default_layout = layout_was
# render layout
if layout
@@ -389,6 +402,7 @@ def render(engine, data, options={}, locals={}, &block)
end
end
+ output.extend(ContentTyped).content_type = content_type if content_type
output
end
@@ -453,8 +467,16 @@ def call!(env) # :nodoc:
template_cache.clear if settings.reload_templates
force_encoding(@params)
+ @response['Content-Type'] = nil
invoke { dispatch! }
invoke { error_block!(response.status) }
+ unless @response['Content-Type']
+ if body.respond_to?(:to_ary) and body.first.respond_to? :content_type
+ content_type body.first.content_type
+ else
+ content_type :html
+ end
+ end
status, header, body = @response.finish
View
26 test/builder_test.rb
@@ -2,9 +2,10 @@
require 'builder'
class BuilderTest < Test::Unit::TestCase
- def builder_app(&block)
+ def builder_app(options = {}, &block)
mock_app {
set :views, File.dirname(__FILE__) + '/views'
+ set options
get '/', &block
}
get '/'
@@ -16,6 +17,29 @@ def builder_app(&block)
assert_equal %{<?xml version="1.0" encoding="UTF-8"?>\n}, body
end
+ it 'defaults content type to xml' do
+ builder_app { builder 'xml.instruct!' }
+ assert ok?
+ assert_equal "application/xml;charset=utf-8", response['Content-Type']
+ end
+
+ it 'defaults allows setting content type per route' do
+ builder_app do
+ content_type :html
+ builder 'xml.instruct!'
+ end
+ assert ok?
+ assert_equal "text/html;charset=utf-8", response['Content-Type']
+ end
+
+ it 'defaults allows setting content type globally' do
+ builder_app(:builder => { :content_type => 'html' }) do
+ builder 'xml.instruct!'
+ end
+ assert ok?
+ assert_equal "text/html;charset=utf-8", response['Content-Type']
+ end
+
it 'renders inline blocks' do
builder_app {
@name = "Frank & Mary"
View
26 test/coffee_test.rb
@@ -4,9 +4,10 @@
require 'coffee-script'
class CoffeeTest < Test::Unit::TestCase
- def coffee_app(&block)
+ def coffee_app(options = {}, &block)
mock_app {
set :views, File.dirname(__FILE__) + '/views'
+ set(options)
get '/', &block
}
get '/'
@@ -18,6 +19,29 @@ def coffee_app(&block)
assert_equal "(function() {\n alert('Aye!');\n})();\n", body
end
+ it 'defaults content type to javascript' do
+ coffee_app { coffee "alert 'Aye!'\n" }
+ assert ok?
+ assert_equal "application/javascript;charset=utf-8", response['Content-Type']
+ end
+
+ it 'defaults allows setting content type per route' do
+ coffee_app do
+ content_type :html
+ coffee "alert 'Aye!'\n"
+ end
+ assert ok?
+ assert_equal "text/html;charset=utf-8", response['Content-Type']
+ end
+
+ it 'defaults allows setting content type globally' do
+ coffee_app(:coffee => { :content_type => 'html' }) do
+ coffee "alert 'Aye!'\n"
+ end
+ assert ok?
+ assert_equal "text/html;charset=utf-8", response['Content-Type']
+ end
+
it 'renders .coffee files in views path' do
coffee_app { coffee :hello }
assert ok?
View
1  test/helper.rb
@@ -1,4 +1,5 @@
ENV['RACK_ENV'] = 'test'
+Encoding.default_external = "UTF-8" if defined? Encoding
begin
require 'rack'
View
14 test/helpers_test.rb
@@ -291,21 +291,21 @@ def test_default
}
get '/'
- assert_equal 'text/plain', response['Content-Type']
+ assert_equal 'text/plain;charset=utf-8', response['Content-Type']
assert_equal 'Hello World', body
end
it 'takes media type parameters (like charset=)' do
mock_app {
get '/' do
- content_type 'text/html', :charset => 'utf-8'
+ content_type 'text/html', :charset => 'latin1'
"<h1>Hello, World</h1>"
end
}
get '/'
assert ok?
- assert_equal 'text/html;charset=utf-8', response['Content-Type']
+ assert_equal 'text/html;charset=latin1', response['Content-Type']
assert_equal "<h1>Hello, World</h1>", body
end
@@ -320,7 +320,7 @@ def test_default
get '/foo.xml'
assert ok?
- assert_equal 'application/foo', response['Content-Type']
+ assert_equal 'application/foo;charset=utf-8', response['Content-Type']
assert_equal 'I AM FOO', body
end
@@ -366,19 +366,19 @@ def send_file_app(opts={})
it 'sets the Content-Type response header if a mime-type can be located' do
send_file_app
get '/file.txt'
- assert_equal 'text/plain', response['Content-Type']
+ assert_equal 'text/plain;charset=utf-8', response['Content-Type']
end
it 'sets the Content-Type response header if type option is set to a file extesion' do
send_file_app :type => 'html'
get '/file.txt'
- assert_equal 'text/html', response['Content-Type']
+ assert_equal 'text/html;charset=utf-8', response['Content-Type']
end
it 'sets the Content-Type response header if type option is set to a mime type' do
send_file_app :type => 'application/octet-stream'
get '/file.txt'
- assert_equal 'application/octet-stream', response['Content-Type']
+ assert_equal 'application/octet-stream;charset=utf-8', response['Content-Type']
end
it 'sets the Content-Length response header' do
View
28 test/less_test.rb
@@ -2,20 +2,44 @@
require 'less'
class LessTest < Test::Unit::TestCase
- def less_app(&block)
+ def less_app(options = {}, &block)
mock_app {
set :views, File.dirname(__FILE__) + '/views'
+ set options
get '/', &block
}
get '/'
end
it 'renders inline Less strings' do
- less_app { less "@white_color: #fff; #main { background-color: @white_color }"}
+ less_app { less "@white_color: #fff; #main { background-color: @white_color }" }
assert ok?
assert_equal "#main { background-color: #ffffff; }\n", body
end
+ it 'defaults content type to css' do
+ less_app { less "@white_color: #fff; #main { background-color: @white_color }" }
+ assert ok?
+ assert_equal "text/css;charset=utf-8", response['Content-Type']
+ end
+
+ it 'defaults allows setting content type per route' do
+ less_app do
+ content_type :html
+ less "@white_color: #fff; #main { background-color: @white_color }"
+ end
+ assert ok?
+ assert_equal "text/html;charset=utf-8", response['Content-Type']
+ end
+
+ it 'defaults allows setting content type globally' do
+ less_app(:less => { :content_type => 'html' }) do
+ less "@white_color: #fff; #main { background-color: @white_color }"
+ end
+ assert ok?
+ assert_equal "text/html;charset=utf-8", response['Content-Type']
+ end
+
it 'renders .less files in views path' do
less_app { less :hello }
assert ok?
View
2  test/routing_test.rb
@@ -80,7 +80,7 @@ class RoutingTest < Test::Unit::TestCase
get '/foo'
assert_equal 404, status
- assert_equal 'text/html', response["Content-Type"]
+ assert_equal 'text/html;charset=utf-8', response["Content-Type"]
assert_equal "<h1>Not Found</h1>", response.body
end
View
26 test/sass_test.rb
@@ -4,9 +4,10 @@
require 'sass'
class SassTest < Test::Unit::TestCase
- def sass_app(&block)
+ def sass_app(options = {}, &block)
mock_app {
set :views, File.dirname(__FILE__) + '/views'
+ set options
get '/', &block
}
get '/'
@@ -18,6 +19,29 @@ def sass_app(&block)
assert_equal "#sass {\n background-color: white; }\n", body
end
+ it 'defaults content type to css' do
+ sass_app { sass "#sass\n :background-color white\n" }
+ assert ok?
+ assert_equal "text/css;charset=utf-8", response['Content-Type']
+ end
+
+ it 'defaults allows setting content type per route' do
+ sass_app do
+ content_type :html
+ sass "#sass\n :background-color white\n"
+ end
+ assert ok?
+ assert_equal "text/html;charset=utf-8", response['Content-Type']
+ end
+
+ it 'defaults allows setting content type globally' do
+ sass_app(:sass => { :content_type => 'html' }) do
+ sass "#sass\n :background-color white\n"
+ end
+ assert ok?
+ assert_equal "text/html;charset=utf-8", response['Content-Type']
+ end
+
it 'renders .sass files in views path' do
sass_app { sass :hello }
assert ok?
View
26 test/scss_test.rb
@@ -4,9 +4,10 @@
require 'sass'
class ScssTest < Test::Unit::TestCase
- def scss_app(&block)
+ def scss_app(options = {}, &block)
mock_app {
set :views, File.dirname(__FILE__) + '/views'
+ set options
get '/', &block
}
get '/'
@@ -18,6 +19,29 @@ def scss_app(&block)
assert_equal "#scss {\n background-color: white; }\n", body
end
+ it 'defaults content type to css' do
+ scss_app { scss "#scss {\n background-color: white; }\n" }
+ assert ok?
+ assert_equal "text/css;charset=utf-8", response['Content-Type']
+ end
+
+ it 'defaults allows setting content type per route' do
+ scss_app do
+ content_type :html
+ scss "#scss {\n background-color: white; }\n"
+ end
+ assert ok?
+ assert_equal "text/html;charset=utf-8", response['Content-Type']
+ end
+
+ it 'defaults allows setting content type globally' do
+ scss_app(:scss => { :content_type => 'html' }) do
+ scss "#scss {\n background-color: white; }\n"
+ end
+ assert ok?
+ assert_equal "text/html;charset=utf-8", response['Content-Type']
+ end
+
it 'renders .scss files in views path' do
scss_app { scss :hello }
assert ok?
View
43 test/templates_test.rb
@@ -15,9 +15,11 @@ def evaluate(scope, locals={}, &block)
end
class TemplatesTest < Test::Unit::TestCase
- def render_app(base=Sinatra::Base, &block)
+ def render_app(base=Sinatra::Base, options = {}, &block)
+ base, options = Sinatra::Base, base if base.is_a? Hash
mock_app(base) {
set :views, File.dirname(__FILE__) + '/views'
+ set options
get '/', &block
template(:layout3) { "Layout 3!\n" }
}
@@ -78,6 +80,27 @@ def with_default_layout
assert_equal "Layout 3!\nHello World!\n", body
end
+ it 'avoids wrapping layouts around nested templates' do
+ render_app { render :str, :nested, :layout => :layout2 }
+ assert ok?
+ assert_equal "<h1>String Layout!</h1>\n<content><h1>Hello From String</h1></content>", body
+ end
+
+ it 'allows explicitly wrapping layouts around nested templates' do
+ render_app { render :str, :explicitly_nested, :layout => :layout2 }
+ assert ok?
+ assert_equal "<h1>String Layout!</h1>\n<content><h1>String Layout!</h1>\n<h1>Hello From String</h1></content>", body
+ end
+
+ it 'two independent render calls do not disable layouts' do
+ render_app do
+ render :str, :explicitly_nested, :layout => :layout2
+ render :str, :nested, :layout => :layout2
+ end
+ assert ok?
+ assert_equal "<h1>String Layout!</h1>\n<content><h1>Hello From String</h1></content>", body
+ end
+
it 'loads templates from source file' do
mock_app { enable :inline_templates }
assert_equal "this is foo\n\n", @app.templates[:foo][0]
@@ -135,6 +158,24 @@ def with_default_layout
assert_equal 'bar', body
end
+ it 'allows setting default content type per template engine' do
+ render_app(:str => { :content_type => :txt }) { render :str, 'foo' }
+ assert_equal 'text/plain;charset=utf-8', response['Content-Type']
+ end
+
+ it 'setting default content type does not affect other template engines' do
+ render_app(:str => { :content_type => :txt }) { render :test, 'foo' }
+ assert_equal 'text/html;charset=utf-8', response['Content-Type']
+ end
+
+ it 'setting default content type per template engine does not override content_type' do
+ render_app :str => { :content_type => :txt } do
+ content_type :html
+ render :str, 'foo'
+ end
+ assert_equal 'text/html;charset=utf-8', response['Content-Type']
+ end
+
it 'uses templates in superclasses before subclasses' do
base = Class.new(Sinatra::Base)
base.template(:foo) { 'template in superclass' }
View
1  test/views/explicitly_nested.str
@@ -0,0 +1 @@
+<content>#{render :str, :hello, :layout => :layout2}</content>
View
1  test/views/hello.str
@@ -0,0 +1 @@
+<h1>Hello From String</h1>
View
2  test/views/layout2.str
@@ -0,0 +1,2 @@
+<h1>String Layout!</h1>
+#{yield}
View
1  test/views/nested.str
@@ -0,0 +1 @@
+<content>#{render :str, :hello}</content>
Something went wrong with that request. Please try again.