Skip to content
This repository

Enhancing Templates#render's abilities #531

Closed
wants to merge 12 commits into from

6 participants

Bernard Lambeau Don't Add Me To Your Organization a.k.a The Travis Bot Konstantin Haase sdalu Zachary Scott Jon Rowe
Bernard Lambeau

This pull request towards the following features (see #529 for original motivation):

  • ability to render a template from a path, as Tilt already allows, with the cache automatically enabled
  • ability to render a template without knowing the corresponding engine a priori. A logical consequence of the first point.

The pull request mostly extends the public API, in a backward compatible way, as follows:

def render(view, options={}, locals={}, &block)
    # ...
end

The render method becomes public. It also accepts a new signature allowing to omit the engine argument. The idea is to allow new calls such as:

render engine, path_to_a_file    # where path_to_a_file is a recognized path object
render path_to_a_file            # the engine is inferred using Tilt
render :view_name                # the engine is also inferred from the existing file (ambiguous if more than one)

The implementation mostly replaces compile_template by a new greedy algorithm called tilt_template. The latter recursively discovers the parameters needed by Tilt::Template.new. The stuff seems fully backward compatible and passes all the tests. compile_template is kept for backward compatibility.

Don't Add Me To Your Organization a.k.a The Travis Bot

This pull request passes (merged 26464bb into 8752085).

Don't Add Me To Your Organization a.k.a The Travis Bot

This pull request fails (merged 963c145 into 8752085).

Don't Add Me To Your Organization a.k.a The Travis Bot

This pull request passes (merged 38de911 into 8752085).

Don't Add Me To Your Organization a.k.a The Travis Bot

This pull request passes (merged e49e540 into 8752085).

Don't Add Me To Your Organization a.k.a The Travis Bot

This pull request passes (merged ec8a756 into 8752085).

added some commits June 14, 2012
Bernard Lambeau Factorize path-driven code in template_tests. e7cb8f9
Bernard Lambeau Factorize path extraction from a view argument. 53846ac
Bernard Lambeau Promote `render` in public API, with generalized signature.
A private method `_render(view, options, &block)` replaces the previous
render method. The engine to use can be specified under options[:engine],
either as a Tilt::Template subclass or a name. When not specified,
`_render` infers the engine to use from the `view` parameter when possible.

`render` is promoted in the public API to allow clients to request rendering
while relying on engine inference. The signature has been generalized
accordingly.
1df6fe6
Don't Add Me To Your Organization a.k.a The Travis Bot

This pull request fails (merged 1df6fe6 into 8752085).

Bernard Lambeau Make sure that options merging occurs in all possible cases.
There is an extra cost for infered engines, because we need two calls
to Tilt[] to guarantee that the global engine options will correctly be
found.
9e4f785
Don't Add Me To Your Organization a.k.a The Travis Bot

This pull request fails (merged 9e4f785 into 8752085).

Don't Add Me To Your Organization a.k.a The Travis Bot

This pull request fails (merged e445d00 into 8752085).

Bernard Lambeau

@rkh I'm almost done here. A few stuff could still be improved but I'm not sure where to attack exactly... I'll wait for your feedback before going any further. Thanks!

Konstantin Haase
Owner
rkh commented June 14, 2012

I'll go through this once I'm back from Nordic and RuLu. Thanks!

Bernard Lambeau

@rkh ping ;-) Any feedback on this?

I don't really care about the code itself. But I'm more and more convinced that, currently, knowing the engine is the responsibility of the caller, whereas IMHO, it should be the secret of the template itself. Through the file extension, as in Tilt. It should also be possible to specify the engine of inline templates at declaration time.

Any thoughts?

Konstantin Haase
Owner
rkh commented June 26, 2012

In general, I agree with you. I have yet to do a code review, though. At first glance, I don't like that there is a _render method.

Bernard Lambeau

Great, take your time. It was just to be sure that I can bet on the general idea in the future.

Konstantin Haase
Owner

I have been thinking about Sinatra 2.0 a lot and wanna redo the whole rendering thing there.

sdalu

It would be great if templates could also be chosen according to locales, for example:

erb :view, :locales     => I18n.fallbacks[:'fr-FR'],
           :locales_map => lambda {|relative_path, locale| locale+/+relative_path }

would render the first available :view by looking at files fr-FR/view.erb fr/view.erb en/view.erb

Konstantin Haase
Owner

While this would indeed be great for some apps, localisation is out of scope for the Sinatra gem itself.

sdalu

It's why in my example all the informations about localisation itself is provided as options (so no gem depency with i18n/l10n). All that is needed from sinatra is some very simple logic to iterate over a list of files and choose the first existing one.

Let's say if you prefer without references to localisation:

erb :view, :file_opt      => [ 'a', 'b', 'c' ]
           :file_mangling => lambda {|relative_path, opt| opt+/+relative_path }

would render the first available :view by looking at files a/view.erb b/view.erb c/view.erb

Zachary Scott

@sdalu Please don't hijack someone else's thread and open a separate ticket to request your feature.

Thanks!

sdalu

Oops

Jon Rowe

Great, what does that mean for this pull request? Seems that it's gone a bit stale, can/should it be closed if you're planning on reworking it for Sinatra 2.0?

edit This is in reference to @rkh's original comment,

"I have been thinking about Sinatra 2.0 a lot and wanna redo the whole rendering thing there."
Bernard Lambeau

I think it can simply be closed, provided requested rendering support remains remembered: mostly to be able to ask for rendering based on a file (through its extension).

Jon Rowe

Doesn't tilt provide that functionality to sinatra already?

Konstantin Haase
Owner
rkh commented March 03, 2013

Yes, vanilla Sinatra just doesn't make use of it.

Konstantin Haase rkh closed this April 15, 2013
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Showing 12 unique commits by 1 author.

Jun 14, 2012
Bernard Lambeau Introduce Templates#tilt_compile. 26464bb
Bernard Lambeau Fix vocabulary, we compile a Sinatra view to a Tilt::Template 963c145
Bernard Lambeau Fix vocabulary, find_view_xxx instead of find_template_xxx 38de911
Bernard Lambeau Refactor and test rendering from Path-able objects. e49e540
Bernard Lambeau Remove unused greedy entry c4b65f5
Bernard Lambeau Let `render` accept an :engine option. ec8a756
Bernard Lambeau The engine is always known there. f8ebdd9
Bernard Lambeau Factorize path-driven code in template_tests. e7cb8f9
Bernard Lambeau Factorize path extraction from a view argument. 53846ac
Bernard Lambeau Promote `render` in public API, with generalized signature.
A private method `_render(view, options, &block)` replaces the previous
render method. The engine to use can be specified under options[:engine],
either as a Tilt::Template subclass or a name. When not specified,
`_render` infers the engine to use from the `view` parameter when possible.

`render` is promoted in the public API to allow clients to request rendering
while relying on engine inference. The signature has been generalized
accordingly.
1df6fe6
Bernard Lambeau Make sure that options merging occurs in all possible cases.
There is an extra cost for infered engines, because we need two calls
to Tilt[] to guarantee that the global engine options will correctly be
found.
9e4f785
Bernard Lambeau Avoid bloating the public API, keep find_view_xxx private. e445d00
This page is out of date. Refresh to see the latest.
142  lib/sinatra/base.rb
@@ -664,6 +664,13 @@ def find_template(views, name, engine)
664 664
       end
665 665
     end
666 666
 
  667
+    def render(engine, view=nil, options={}, locals={}, &block)
  668
+      engine, view, options, locals = nil, engine, view || {}, options if view.nil? or view.is_a?(Hash)
  669
+      options[:engine]   = engine
  670
+      options[:locals] ||= locals
  671
+      _render(view, options, &block)
  672
+    end
  673
+
667 674
   private
668 675
     # logic shared between builder and nokogiri
669 676
     def render_ruby(engine, template, options={}, locals={}, &block)
@@ -672,14 +679,20 @@ def render_ruby(engine, template, options={}, locals={}, &block)
672 679
       render engine, template, options, locals
673 680
     end
674 681
 
675  
-    def render(engine, data, options={}, locals={}, &block)
  682
+    def _render(view, options={}, &block)
  683
+      # get the engine to use
  684
+      engine = options[:engine] || find_view_engine(view, options)
  685
+
676 686
       # merge app-level options
677  
-      options = settings.send(engine).merge(options) if settings.respond_to?(engine)
  687
+      if engine.is_a?(Symbol) and settings.respond_to?(engine)
  688
+        options = settings.send(engine).merge(options)
  689
+      end
  690
+
678 691
       options[:outvar]           ||= '@_out_buf'
679 692
       options[:default_encoding] ||= settings.default_encoding
680 693
 
681 694
       # extract generic options
682  
-      locals          = options.delete(:locals) || locals         || {}
  695
+      locals          = options.delete(:locals) || {}
683 696
       views           = options.delete(:views)  || settings.views || "./views"
684 697
       layout          = options.delete(:layout)
685 698
       eat_errors      = layout.nil?
@@ -692,7 +705,7 @@ def render(engine, data, options={}, locals={}, &block)
692 705
       begin
693 706
         layout_was      = @default_layout
694 707
         @default_layout = false
695  
-        template        = compile_template(engine, data, options, views)
  708
+        template        = tilt_template(view, options, views)
696 709
         output          = template.render(scope, locals, &block)
697 710
       ensure
698 711
         @default_layout = layout_was
@@ -700,48 +713,107 @@ def render(engine, data, options={}, locals={}, &block)
700 713
 
701 714
       # render layout
702 715
       if layout
703  
-        options = options.merge(:views => views, :layout => false, :eat_errors => eat_errors, :scope => scope)
704  
-        catch(:layout_missing) { return render(layout_engine, layout, options, locals) { output } }
  716
+        options = options.merge(:engine => layout_engine, :views => views, :layout => false,
  717
+                                :eat_errors => eat_errors, :scope => scope, :locals => locals)
  718
+        catch(:layout_missing) { return _render(layout, options) { output } }
705 719
       end
706 720
 
707 721
       output.extend(ContentTyped).content_type = content_type if content_type
708 722
       output
709 723
     end
710 724
 
711  
-    def compile_template(engine, data, options, views)
712  
-      eat_errors = options.delete :eat_errors
713  
-      template_cache.fetch engine, data, options do
714  
-        template = Tilt[engine]
715  
-        raise "Template engine not found: #{engine}" if template.nil?
  725
+    def extract_path(view)
  726
+      path   = view.path    if view.respond_to?(:path)
  727
+      path ||= view.to_path if view.respond_to?(:to_path)
  728
+      path ||= view.to_s
  729
+      path
  730
+    end
716 731
 
717  
-        case data
718  
-        when Symbol
719  
-          body, path, line = settings.templates[data]
720  
-          if body
721  
-            body = body.call if body.respond_to?(:call)
722  
-            template.new(path, line.to_i, options) { body }
723  
-          else
724  
-            found = false
725  
-            @preferred_extension = engine.to_s
726  
-            find_template(views, data, template) do |file|
727  
-              path ||= file # keep the initial path rather than the last one
728  
-              if found = File.exists?(file)
729  
-                path = file
730  
-                break
731  
-              end
732  
-            end
733  
-            throw :layout_missing if eat_errors and not found
734  
-            template.new(path, 1, options)
735  
-          end
736  
-        when Proc, String
737  
-          body = data.is_a?(String) ? Proc.new { data } : data
738  
-          path, line = settings.caller_locations.first
739  
-          template.new(path, line.to_i, options, &body)
  732
+    def tilt_template(view, options, views)
  733
+      template_cache.fetch view, options do
  734
+        greedy = {}
  735
+        greedy[:views]        = views
  736
+        greedy[:engine]       = options.delete :engine
  737
+        greedy[:eat_errors]   = options.delete :eat_errors
  738
+        greedy[:options]      = options
  739
+        greedy[:location]     = settings.caller_locations.first
  740
+        tilt_compile(view, greedy)
  741
+      end
  742
+    end
  743
+
  744
+    def tilt_compile(view, greedy={})
  745
+      case view
  746
+      when Hash
  747
+        engine     = view[:engine]
  748
+        engine     = Tilt[engine.to_s] unless Class===engine
  749
+        path, line = view[:location]
  750
+        options    = view[:options]
  751
+        body       = view[:body]
  752
+        engine.new(path, line, options, &body)
  753
+      when Symbol
  754
+        greedy[:engine] ||= find_view_engine(view, greedy)
  755
+        body, path, line = settings.templates[view]
  756
+        if body
  757
+          body = body.call if body.respond_to?(:call)
  758
+          greedy[:body]     = Proc.new{ body }
  759
+          greedy[:location] = [path, line]
740 760
         else
741  
-          raise ArgumentError, "Sorry, don't know how to render #{data.inspect}."
  761
+          greedy[:location] = find_view_location(view, greedy)
742 762
         end
  763
+        tilt_compile(greedy)
  764
+      when Proc
  765
+        greedy[:body] = view
  766
+        tilt_compile(greedy)
  767
+      when String
  768
+        greedy[:body] = Proc.new{ view }
  769
+        tilt_compile(greedy)
  770
+      else
  771
+        path = extract_path(view)
  772
+        greedy[:engine] ||= find_view_engine(path, greedy)
  773
+        greedy[:location] = [path, 1]
  774
+        tilt_compile(greedy)
743 775
       end
744 776
     end
  777
+
  778
+    # Infers the Tilt engine to use for `view`, either a Symbol denoting a view name or a
  779
+    # file path. Returns the engine either as a Tilt template class or an engine name.
  780
+    # Raises a runtime error if no engine can be found.
  781
+    def find_view_engine(view, greedy)
  782
+      if Symbol===view
  783
+        raise NotImplementedError
  784
+      else
  785
+        path   = extract_path(view)
  786
+        engine = Tilt[path]
  787
+        raise "Template engine not found: #{view}" if engine.nil?
  788
+        ext    = File.extname(path)[1..-1].to_sym
  789
+        engine = ext if Tilt[ext] == engine
  790
+      end
  791
+      engine
  792
+    end
  793
+
  794
+    # Infers the location of `view` location as a `[path, line]` pair. `view` is always a
  795
+    # Symbol denoting a view name. `greedy` contains what we already know about the view
  796
+    # and is guaranteed to contain the infered engine.
  797
+    def find_view_location(view, greedy)
  798
+      engine, views, eat_errors = greedy.values_at(:engine, :views, :eat_errors)
  799
+      engine = Tilt[engine] unless Class==engine
  800
+      found, path, @preferred_extension = false, nil, engine.to_s
  801
+      find_template(views, view, engine) do |file|
  802
+        path ||= file # keep the initial path rather than the last one
  803
+        if found = File.exists?(file)
  804
+          path = file
  805
+          break
  806
+        end
  807
+      end
  808
+      throw :layout_missing if eat_errors and not found
  809
+      [path, 1]
  810
+    end
  811
+
  812
+    # preserved for backward compatibility
  813
+    def compile_template(engine, view, options, views)
  814
+      tilt_template(view, options.merge(:engine => engine), views)
  815
+    end
  816
+
745 817
   end
746 818
 
747 819
   # Base class for all Sinatra applications and middleware.
51  test/templates_test.rb
@@ -7,8 +7,13 @@ def prepare
7 7
   end
8 8
 
9 9
   def evaluate(scope, locals={}, &block)
10  
-    inner = block ? block.call : ''
11  
-    data + inner
  10
+    inner  = block ? block.call : ''
  11
+    result = data + inner
  12
+    if p = options[:enclose]
  13
+      p = p.upcase if options[:upcase]
  14
+      result = "<#{p}>#{result}</#{p}>"
  15
+    end
  16
+    result
12 17
   end
13 18
 
14 19
   Tilt.register 'test', self
@@ -34,6 +39,14 @@ def with_default_layout
34 39
     File.unlink(layout) rescue nil
35 40
   end
36 41
 
  42
+  def with_hello_paths
  43
+    require 'pathname'
  44
+    path = File.dirname(__FILE__) + '/views/hello.test'
  45
+    yield Struct.new(:path).new(path)
  46
+    yield Struct.new(:to_path).new(path)
  47
+    yield Pathname.new(path)
  48
+  end
  49
+
37 50
   it 'renders String templates directly' do
38 51
     render_app { render(:test, 'Hello World') }
39 52
     assert ok?
@@ -52,6 +65,40 @@ def with_default_layout
52 65
     assert_equal "Hello World!\n", body
53 66
   end
54 67
 
  68
+  it 'renders path-able objects from their file' do
  69
+    with_hello_paths do |path|
  70
+      render_app{ render(:test, path) }
  71
+      assert ok?
  72
+      assert_equal "Hello World!\n", body
  73
+    end
  74
+  end
  75
+
  76
+  it 'infers the engine to use from path extension' do
  77
+    with_hello_paths do |path|
  78
+      render_app{ render(path) }
  79
+      assert ok?
  80
+      assert_equal "Hello World!\n", body
  81
+    end
  82
+  end
  83
+
  84
+  it 'merges global options with local engine options' do
  85
+    render_app(:test => {:enclose => "g"}) {
  86
+      render(:test, "Hello World!\n", :upcase => true)
  87
+    }
  88
+    assert ok?
  89
+    assert_equal "<G>Hello World!\n</G>", body
  90
+  end
  91
+
  92
+  it 'merges global options with local options when an infered engine as well' do
  93
+    with_hello_paths do |path|
  94
+      render_app(:test => {:enclose => "g"}) {
  95
+        render(path, :upcase => true)
  96
+      }
  97
+      assert ok?
  98
+      assert_equal "<G>Hello World!\n</G>", body
  99
+    end
  100
+  end
  101
+
55 102
   it 'uses the default layout template if not explicitly overridden' do
56 103
     with_default_layout do
57 104
       render_app { render(:test, :hello) }
Commit_comment_tip

Tip: You can add notes to lines in a file. Hover to the left of a line to make a note

Something went wrong with that request. Please try again.