darkhelmet-sinatra 0.9.1.1 → 0.10.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (88) hide show
  1. data/AUTHORS +2 -0
  2. data/CHANGES +180 -0
  3. data/LICENSE +1 -1
  4. data/README.jp.rdoc +552 -0
  5. data/README.rdoc +177 -38
  6. data/Rakefile +18 -25
  7. data/lib/sinatra.rb +1 -2
  8. data/lib/sinatra/base.rb +405 -305
  9. data/lib/sinatra/main.rb +5 -24
  10. data/lib/sinatra/showexceptions.rb +303 -0
  11. data/lib/sinatra/tilt.rb +509 -0
  12. data/sinatra.gemspec +21 -51
  13. data/test/base_test.rb +123 -93
  14. data/test/builder_test.rb +2 -1
  15. data/test/contest.rb +64 -0
  16. data/test/erb_test.rb +1 -1
  17. data/test/erubis_test.rb +82 -0
  18. data/test/extensions_test.rb +24 -8
  19. data/test/filter_test.rb +99 -3
  20. data/test/haml_test.rb +25 -3
  21. data/test/helper.rb +43 -48
  22. data/test/helpers_test.rb +500 -424
  23. data/test/mapped_error_test.rb +163 -137
  24. data/test/middleware_test.rb +3 -3
  25. data/test/request_test.rb +16 -1
  26. data/test/response_test.rb +2 -2
  27. data/test/result_test.rb +1 -1
  28. data/test/route_added_hook_test.rb +59 -0
  29. data/test/routing_test.rb +170 -22
  30. data/test/sass_test.rb +44 -1
  31. data/test/server_test.rb +19 -13
  32. data/test/sinatra_test.rb +1 -1
  33. data/test/static_test.rb +9 -2
  34. data/test/templates_test.rb +78 -11
  35. data/test/views/error.builder +3 -0
  36. data/test/views/error.erb +3 -0
  37. data/test/views/error.erubis +3 -0
  38. data/test/views/error.haml +3 -0
  39. data/test/views/error.sass +2 -0
  40. data/test/views/foo/hello.test +1 -0
  41. data/test/views/hello.erubis +1 -0
  42. data/test/views/layout2.erubis +2 -0
  43. metadata +37 -55
  44. data/compat/app_test.rb +0 -282
  45. data/compat/application_test.rb +0 -262
  46. data/compat/builder_test.rb +0 -101
  47. data/compat/compat_test.rb +0 -12
  48. data/compat/custom_error_test.rb +0 -62
  49. data/compat/erb_test.rb +0 -136
  50. data/compat/events_test.rb +0 -78
  51. data/compat/filter_test.rb +0 -30
  52. data/compat/haml_test.rb +0 -233
  53. data/compat/helper.rb +0 -30
  54. data/compat/mapped_error_test.rb +0 -72
  55. data/compat/pipeline_test.rb +0 -45
  56. data/compat/public/foo.xml +0 -1
  57. data/compat/sass_test.rb +0 -57
  58. data/compat/sessions_test.rb +0 -42
  59. data/compat/streaming_test.rb +0 -133
  60. data/compat/sym_params_test.rb +0 -19
  61. data/compat/template_test.rb +0 -30
  62. data/compat/use_in_file_templates_test.rb +0 -47
  63. data/compat/views/foo.builder +0 -1
  64. data/compat/views/foo.erb +0 -1
  65. data/compat/views/foo.haml +0 -1
  66. data/compat/views/foo.sass +0 -2
  67. data/compat/views/foo_layout.erb +0 -2
  68. data/compat/views/foo_layout.haml +0 -2
  69. data/compat/views/layout_test/foo.builder +0 -1
  70. data/compat/views/layout_test/foo.erb +0 -1
  71. data/compat/views/layout_test/foo.haml +0 -1
  72. data/compat/views/layout_test/foo.sass +0 -2
  73. data/compat/views/layout_test/layout.builder +0 -3
  74. data/compat/views/layout_test/layout.erb +0 -1
  75. data/compat/views/layout_test/layout.haml +0 -1
  76. data/compat/views/layout_test/layout.sass +0 -2
  77. data/compat/views/no_layout/no_layout.builder +0 -1
  78. data/compat/views/no_layout/no_layout.haml +0 -1
  79. data/lib/sinatra/compat.rb +0 -250
  80. data/lib/sinatra/test.rb +0 -126
  81. data/lib/sinatra/test/bacon.rb +0 -19
  82. data/lib/sinatra/test/rspec.rb +0 -13
  83. data/lib/sinatra/test/spec.rb +0 -11
  84. data/lib/sinatra/test/unit.rb +0 -13
  85. data/test/data/reload_app_file.rb +0 -3
  86. data/test/options_test.rb +0 -374
  87. data/test/reload_test.rb +0 -68
  88. data/test/test_test.rb +0 -144
@@ -6,42 +6,23 @@ module Sinatra
6
6
  # we assume that the first file that requires 'sinatra' is the
7
7
  # app_file. all other path related options are calculated based
8
8
  # on this path by default.
9
- set :app_file, lambda {
10
- ignore = [
11
- /lib\/sinatra.*\.rb$/, # all sinatra code
12
- /\(.*\)/, # generated code
13
- /custom_require\.rb$/ # rubygems require hacks
14
- ]
15
- path =
16
- caller.map{ |line| line.split(/:\d/, 2).first }.find do |file|
17
- next if ignore.any? { |pattern| file =~ pattern }
18
- file
19
- end
20
- path || $0
21
- }.call
9
+ set :app_file, caller_files.first || $0
22
10
 
23
11
  set :run, Proc.new { $0 == app_file }
24
12
 
25
13
  if run? && ARGV.any?
26
14
  require 'optparse'
27
15
  OptionParser.new { |op|
28
- op.on('-x') { set :mutex, true }
16
+ op.on('-x') { set :lock, true }
29
17
  op.on('-e env') { |val| set :environment, val.to_sym }
30
18
  op.on('-s server') { |val| set :server, val }
31
19
  op.on('-p port') { |val| set :port, val.to_i }
20
+ op.on('-h addr') { |val| set :host, val }
32
21
  }.parse!(ARGV.dup)
33
22
  end
34
23
  end
35
- end
36
-
37
- include Sinatra::Delegator
38
24
 
39
- def mime(ext, type)
40
- ext = ".#{ext}" unless ext.to_s[0] == ?.
41
- Rack::Mime::MIME_TYPES[ext.to_s] = type
25
+ at_exit { Application.run! if $!.nil? && Application.run? }
42
26
  end
43
27
 
44
- at_exit do
45
- raise $! if $!
46
- Sinatra::Application.run! if Sinatra::Application.run?
47
- end
28
+ include Sinatra::Delegator
@@ -0,0 +1,303 @@
1
+ require 'rack/showexceptions'
2
+
3
+ module Sinatra
4
+ class ShowExceptions < Rack::ShowExceptions
5
+ def initialize(app)
6
+ @app = app
7
+ @template = ERB.new(TEMPLATE)
8
+ end
9
+
10
+ def frame_class(frame)
11
+ if frame.filename =~ /lib\/sinatra.*\.rb/
12
+ "framework"
13
+ elsif (defined?(Gem) && frame.filename.include?(Gem.dir)) ||
14
+ frame.filename =~ /\/bin\/(\w+)$/
15
+ "system"
16
+ else
17
+ "app"
18
+ end
19
+ end
20
+
21
+ TEMPLATE = <<HTML
22
+ <!DOCTYPE html>
23
+ <html>
24
+ <head>
25
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
26
+ <title><%=h exception.class %> at <%=h path %></title>
27
+
28
+ <script type="text/javascript">
29
+ //<!--
30
+ function toggle(id) {
31
+ var pre = document.getElementById("pre-" + id);
32
+ var post = document.getElementById("post-" + id);
33
+ var context = document.getElementById("context-" + id);
34
+
35
+ if (pre.style.display == 'block') {
36
+ pre.style.display = 'none';
37
+ post.style.display = 'none';
38
+ context.style.background = "none";
39
+ } else {
40
+ pre.style.display = 'block';
41
+ post.style.display = 'block';
42
+ context.style.background = "#fffed9";
43
+ }
44
+ }
45
+
46
+ function toggleBacktrace(){
47
+ var bt = document.getElementById("backtrace");
48
+ var toggler = document.getElementById("expando");
49
+
50
+ if (bt.className == 'condensed') {
51
+ bt.className = 'expanded';
52
+ toggler.innerHTML = "(condense)";
53
+ } else {
54
+ bt.className = 'condensed';
55
+ toggler.innerHTML = "(expand)";
56
+ }
57
+ }
58
+ //-->
59
+ </script>
60
+
61
+ <style type="text/css" media="screen">
62
+ * {margin: 0; padding: 0; border: 0; outline: 0;}
63
+ div.clear {clear: both;}
64
+ body {background: #EEEEEE; margin: 0; padding: 0;
65
+ font-family: 'Lucida Grande', 'Lucida Sans Unicode',
66
+ 'Garuda';}
67
+ code {font-family: 'Lucida Console', monospace;
68
+ font-size: 12px;}
69
+ li {height: 18px;}
70
+ ul {list-style: none; margin: 0; padding: 0;}
71
+ ol:hover {cursor: pointer;}
72
+ ol li {white-space: pre;}
73
+ #explanation {font-size: 12px; color: #666666;
74
+ margin: 20px 0 0 100px;}
75
+ /* WRAP */
76
+ #wrap {width: 860px; background: #FFFFFF; margin: 0 auto;
77
+ padding: 30px 50px 20px 50px;
78
+ border-left: 1px solid #DDDDDD;
79
+ border-right: 1px solid #DDDDDD;}
80
+ /* HEADER */
81
+ #header {margin: 0 auto 25px auto;}
82
+ #header img {float: left;}
83
+ #header #summary {float: left; margin: 12px 0 0 20px; width:520px;
84
+ font-family: 'Lucida Grande', 'Lucida Sans Unicode';}
85
+ h1 {margin: 0; font-size: 36px; color: #981919;}
86
+ h2 {margin: 0; font-size: 22px; color: #333333;}
87
+ #header ul {margin: 0; font-size: 12px; color: #666666;}
88
+ #header ul li strong{color: #444444;}
89
+ #header ul li {display: inline; padding: 0 10px;}
90
+ #header ul li.first {padding-left: 0;}
91
+ #header ul li.last {border: 0; padding-right: 0;}
92
+ /* BODY */
93
+ #backtrace,
94
+ #get,
95
+ #post,
96
+ #cookies,
97
+ #rack {width: 860px; margin: 0 auto 10px auto;}
98
+ p#nav {float: right; font-size: 14px;}
99
+ /* BACKTRACE */
100
+ a#expando {float: left; padding-left: 5px; color: #666666;
101
+ font-size: 14px; text-decoration: none; cursor: pointer;}
102
+ a#expando:hover {text-decoration: underline;}
103
+ h3 {float: left; width: 100px; margin-bottom: 10px;
104
+ color: #981919; font-size: 14px; font-weight: bold;}
105
+ #nav a {color: #666666; text-decoration: none; padding: 0 5px;}
106
+ #backtrace li.frame-info {background: #f7f7f7; padding-left: 10px;
107
+ font-size: 12px; color: #333333;}
108
+ #backtrace ul {list-style-position: outside; border: 1px solid #E9E9E9;
109
+ border-bottom: 0;}
110
+ #backtrace ol {width: 808px; margin-left: 50px;
111
+ font: 10px 'Lucida Console', monospace; color: #666666;}
112
+ #backtrace ol li {border: 0; border-left: 1px solid #E9E9E9;
113
+ padding: 2px 0;}
114
+ #backtrace ol code {font-size: 10px; color: #555555; padding-left: 5px;}
115
+ #backtrace-ul li {border-bottom: 1px solid #E9E9E9; height: auto;
116
+ padding: 3px 0;}
117
+ #backtrace-ul .code {padding: 6px 0 4px 0;}
118
+ #backtrace.condensed .system,
119
+ #backtrace.condensed .framework {display:none;}
120
+ /* REQUEST DATA */
121
+ p.no-data {padding-top: 2px; font-size: 12px; color: #666666;}
122
+ table.req {width: 760px; text-align: left; font-size: 12px;
123
+ color: #666666; padding: 0; border-spacing: 0;
124
+ border: 1px solid #EEEEEE; border-bottom: 0;
125
+ border-left: 0;}
126
+ table.req tr th {padding: 2px 10px; font-weight: bold;
127
+ background: #F7F7F7; border-bottom: 1px solid #EEEEEE;
128
+ border-left: 1px solid #EEEEEE;}
129
+ table.req tr td {padding: 2px 20px 2px 10px;
130
+ border-bottom: 1px solid #EEEEEE;
131
+ border-left: 1px solid #EEEEEE;}
132
+ /* HIDE PRE/POST CODE AT START */
133
+ .pre-context,
134
+ .post-context {display: none;}
135
+ </style>
136
+ </head>
137
+ <body>
138
+ <div id="wrap">
139
+ <div id="header">
140
+ <img src="/__sinatra__/500.png" alt="application error" />
141
+ <div id="summary">
142
+ <h1><strong><%=h exception.class %></strong> at <strong><%=h path %>
143
+ </strong></h1>
144
+ <h2><%=h exception.message %></h2>
145
+ <ul>
146
+ <li class="first"><strong>file:</strong> <code>
147
+ <%=h frames.first.filename.split("/").last %></code></li>
148
+ <li><strong>location:</strong> <code><%=h frames.first.function %>
149
+ </code></li>
150
+ <li class="last"><strong>line:
151
+ </strong> <%=h frames.first.lineno %></li>
152
+ </ul>
153
+ </div>
154
+ <div class="clear"></div>
155
+ </div>
156
+
157
+ <div id="backtrace" class='condensed'>
158
+ <h3>BACKTRACE</h3>
159
+ <p><a href="#" id="expando"
160
+ onclick="toggleBacktrace(); return false">(expand)</a></p>
161
+ <p id="nav"><strong>JUMP TO:</strong>
162
+ <a href="#get-info">GET</a>
163
+ <a href="#post-info">POST</a>
164
+ <a href="#cookie-info">COOKIES</a>
165
+ <a href="#env-info">ENV</a>
166
+ </p>
167
+ <div class="clear"></div>
168
+
169
+ <ul id="backtrace-ul">
170
+
171
+ <% id = 1 %>
172
+ <% frames.each do |frame| %>
173
+ <% if frame.context_line && frame.context_line != "#" %>
174
+
175
+ <li class="frame-info <%= frame_class(frame) %>">
176
+ <code><%=h frame.filename %></code> in
177
+ <code><strong><%=h frame.function %></strong></code>
178
+ </li>
179
+
180
+ <li class="code <%= frame_class(frame) %>">
181
+ <% if frame.pre_context %>
182
+ <ol start="<%=h frame.pre_context_lineno + 1 %>"
183
+ class="pre-context" id="pre-<%= id %>"
184
+ onclick="toggle(<%= id %>);">
185
+ <% frame.pre_context.each do |line| %>
186
+ <li class="pre-context-line"><code><%=h line %></code></li>
187
+ <% end %>
188
+ </ol>
189
+ <% end %>
190
+
191
+ <ol start="<%= frame.lineno %>" class="context" id="<%= id %>"
192
+ onclick="toggle(<%= id %>);">
193
+ <li class="context-line" id="context-<%= id %>"><code><%=
194
+ h frame.context_line %></code></li>
195
+ </ol>
196
+
197
+ <% if frame.post_context %>
198
+ <ol start="<%=h frame.lineno + 1 %>" class="post-context"
199
+ id="post-<%= id %>" onclick="toggle(<%= id %>);">
200
+ <% frame.post_context.each do |line| %>
201
+ <li class="post-context-line"><code><%=h line %></code></li>
202
+ <% end %>
203
+ </ol>
204
+ <% end %>
205
+ <div class="clear"></div>
206
+ </li>
207
+
208
+ <% end %>
209
+
210
+ <% id += 1 %>
211
+ <% end %>
212
+
213
+ </ul>
214
+ </div> <!-- /BACKTRACE -->
215
+
216
+ <div id="get">
217
+ <h3 id="get-info">GET</h3>
218
+ <% unless req.GET.empty? %>
219
+ <table class="req">
220
+ <tr>
221
+ <th>Variable</th>
222
+ <th>Value</th>
223
+ </tr>
224
+ <% req.GET.sort_by { |k, v| k.to_s }.each { |key, val| %>
225
+ <tr>
226
+ <td><%=h key %></td>
227
+ <td class="code"><div><%=h val.inspect %></div></td>
228
+ </tr>
229
+ <% } %>
230
+ </table>
231
+ <% else %>
232
+ <p class="no-data">No GET data.</p>
233
+ <% end %>
234
+ <div class="clear"></div>
235
+ </div> <!-- /GET -->
236
+
237
+ <div id="post">
238
+ <h3 id="post-info">POST</h3>
239
+ <% unless req.POST.empty? %>
240
+ <table class="req">
241
+ <tr>
242
+ <th>Variable</th>
243
+ <th>Value</th>
244
+ </tr>
245
+ <% req.POST.sort_by { |k, v| k.to_s }.each { |key, val| %>
246
+ <tr>
247
+ <td><%=h key %></td>
248
+ <td class="code"><div><%=h val.inspect %></div></td>
249
+ </tr>
250
+ <% } %>
251
+ </table>
252
+ <% else %>
253
+ <p class="no-data">No POST data.</p>
254
+ <% end %>
255
+ <div class="clear"></div>
256
+ </div> <!-- /POST -->
257
+
258
+ <div id="cookies">
259
+ <h3 id="cookie-info">COOKIES</h3>
260
+ <% unless req.cookies.empty? %>
261
+ <table class="req">
262
+ <tr>
263
+ <th>Variable</th>
264
+ <th>Value</th>
265
+ </tr>
266
+ <% req.cookies.each { |key, val| %>
267
+ <tr>
268
+ <td><%=h key %></td>
269
+ <td class="code"><div><%=h val.inspect %></div></td>
270
+ </tr>
271
+ <% } %>
272
+ </table>
273
+ <% else %>
274
+ <p class="no-data">No cookie data.</p>
275
+ <% end %>
276
+ <div class="clear"></div>
277
+ </div> <!-- /COOKIES -->
278
+
279
+ <div id="rack">
280
+ <h3 id="env-info">Rack ENV</h3>
281
+ <table class="req">
282
+ <tr>
283
+ <th>Variable</th>
284
+ <th>Value</th>
285
+ </tr>
286
+ <% env.sort_by { |k, v| k.to_s }.each { |key, val| %>
287
+ <tr>
288
+ <td><%=h key %></td>
289
+ <td class="code"><div><%=h val %></div></td>
290
+ </tr>
291
+ <% } %>
292
+ </table>
293
+ <div class="clear"></div>
294
+ </div> <!-- /RACK ENV -->
295
+
296
+ <p id="explanation">You're seeing this error because you have
297
+ enabled the <code>show_exceptions</code> setting.</p>
298
+ </div> <!-- /WRAP -->
299
+ </body>
300
+ </html>
301
+ HTML
302
+ end
303
+ end
@@ -0,0 +1,509 @@
1
+ module Tilt
2
+ VERSION = '0.4'
3
+
4
+ @template_mappings = {}
5
+
6
+ # Hash of template path pattern => template implementation
7
+ # class mappings.
8
+ def self.mappings
9
+ @template_mappings
10
+ end
11
+
12
+ # Register a template implementation by file extension.
13
+ def self.register(ext, template_class)
14
+ ext = ext.to_s.sub(/^\./, '')
15
+ mappings[ext.downcase] = template_class
16
+ end
17
+
18
+ # Create a new template for the given file using the file's extension
19
+ # to determine the the template mapping.
20
+ def self.new(file, line=nil, options={}, &block)
21
+ if template_class = self[file]
22
+ template_class.new(file, line, options, &block)
23
+ else
24
+ fail "No template engine registered for #{File.basename(file)}"
25
+ end
26
+ end
27
+
28
+ # Lookup a template class given for the given filename or file
29
+ # extension. Return nil when no implementation is found.
30
+ def self.[](file)
31
+ if @template_mappings.key?(pattern = file.to_s.downcase)
32
+ @template_mappings[pattern]
33
+ elsif @template_mappings.key?(pattern = File.basename(pattern))
34
+ @template_mappings[pattern]
35
+ else
36
+ while !pattern.empty?
37
+ if @template_mappings.key?(pattern)
38
+ return @template_mappings[pattern]
39
+ else
40
+ pattern = pattern.sub(/^[^.]*\.?/, '')
41
+ end
42
+ end
43
+ nil
44
+ end
45
+ end
46
+
47
+
48
+ # Base class for template implementations. Subclasses must implement
49
+ # the #compile! method and one of the #evaluate or #template_source
50
+ # methods.
51
+ class Template
52
+ # Template source; loaded from a file or given directly.
53
+ attr_reader :data
54
+
55
+ # The name of the file where the template data was loaded from.
56
+ attr_reader :file
57
+
58
+ # The line number in #file where template data was loaded from.
59
+ attr_reader :line
60
+
61
+ # A Hash of template engine specific options. This is passed directly
62
+ # to the underlying engine and is not used by the generic template
63
+ # interface.
64
+ attr_reader :options
65
+
66
+ # Create a new template with the file, line, and options specified. By
67
+ # default, template data is read from the file specified. When a block
68
+ # is given, it should read template data and return as a String. When
69
+ # file is nil, a block is required.
70
+ #
71
+ # The #initialize_engine method is called if this is the very first
72
+ # time this template subclass has been initialized.
73
+ def initialize(file=nil, line=1, options={}, &block)
74
+ raise ArgumentError, "file or block required" if file.nil? && block.nil?
75
+ options, line = line, 1 if line.is_a?(Hash)
76
+ @file = file
77
+ @line = line || 1
78
+ @options = options || {}
79
+ @reader = block || lambda { |t| File.read(file) }
80
+
81
+ if !self.class.engine_initialized
82
+ initialize_engine
83
+ self.class.engine_initialized = true
84
+ end
85
+ end
86
+
87
+ # Called once and only once for each template subclass the first time
88
+ # the template class is initialized. This should be used to require the
89
+ # underlying template library and perform any initial setup.
90
+ def initialize_engine
91
+ end
92
+ @engine_initialized = false
93
+ class << self ; attr_accessor :engine_initialized ; end
94
+
95
+
96
+ # Load template source and compile the template. The template is
97
+ # loaded and compiled the first time this method is called; subsequent
98
+ # calls are no-ops.
99
+ def compile
100
+ if @data.nil?
101
+ @data = @reader.call(self)
102
+ compile!
103
+ end
104
+ end
105
+
106
+ # Render the template in the given scope with the locals specified. If a
107
+ # block is given, it is typically available within the template via
108
+ # +yield+.
109
+ def render(scope=Object.new, locals={}, &block)
110
+ compile
111
+ evaluate scope, locals || {}, &block
112
+ end
113
+
114
+ # The basename of the template file.
115
+ def basename(suffix='')
116
+ File.basename(file, suffix) if file
117
+ end
118
+
119
+ # The template file's basename with all extensions chomped off.
120
+ def name
121
+ basename.split('.', 2).first if basename
122
+ end
123
+
124
+ # The filename used in backtraces to describe the template.
125
+ def eval_file
126
+ file || '(__TEMPLATE__)'
127
+ end
128
+
129
+ protected
130
+ # Do whatever preparation is necessary to "compile" the template.
131
+ # Called immediately after template #data is loaded. Instance variables
132
+ # set in this method are available when #evaluate is called.
133
+ #
134
+ # Subclasses must provide an implementation of this method.
135
+ def compile!
136
+ raise NotImplementedError
137
+ end
138
+
139
+ # Process the template and return the result. Subclasses should override
140
+ # this method unless they implement the #template_source.
141
+ def evaluate(scope, locals, &block)
142
+ source, offset = local_assignment_code(locals)
143
+ source = [source, template_source].join("\n")
144
+ scope.instance_eval source, eval_file, line - offset
145
+ end
146
+
147
+ # Return a string containing the (Ruby) source code for the template. The
148
+ # default Template#evaluate implementation requires this method be
149
+ # defined.
150
+ def template_source
151
+ raise NotImplementedError
152
+ end
153
+
154
+ private
155
+ def local_assignment_code(locals)
156
+ return ['', 1] if locals.empty?
157
+ source = locals.collect { |k,v| "#{k} = locals[:#{k}]" }
158
+ [source.join("\n"), source.length]
159
+ end
160
+
161
+ def require_template_library(name)
162
+ if Thread.list.size > 1
163
+ warn "WARN: tilt autoloading '#{name}' in a non thread-safe way; " +
164
+ "explicit require '#{name}' suggested."
165
+ end
166
+ require name
167
+ end
168
+ end
169
+
170
+ # Extremely simple template cache implementation. Calling applications
171
+ # create a Tilt::Cache instance and use #fetch with any set of hashable
172
+ # arguments (such as those to Tilt.new):
173
+ # cache = Tilt::Cache.new
174
+ # cache.fetch(path, line, options) { Tilt.new(path, line, options) }
175
+ #
176
+ # Subsequent invocations return the already compiled template object.
177
+ class Cache
178
+ def initialize
179
+ @cache = {}
180
+ end
181
+
182
+ def fetch(*key)
183
+ @cache[key] ||= yield
184
+ end
185
+
186
+ def clear
187
+ @cache = {}
188
+ end
189
+ end
190
+
191
+
192
+ # Template Implementations ================================================
193
+
194
+
195
+ # The template source is evaluated as a Ruby string. The #{} interpolation
196
+ # syntax can be used to generated dynamic output.
197
+ class StringTemplate < Template
198
+ def compile!
199
+ @code = "%Q{#{data}}"
200
+ end
201
+
202
+ def template_source
203
+ @code
204
+ end
205
+ end
206
+ register 'str', StringTemplate
207
+
208
+
209
+ # ERB template implementation. See:
210
+ # http://www.ruby-doc.org/stdlib/libdoc/erb/rdoc/classes/ERB.html
211
+ class ERBTemplate < Template
212
+ def initialize_engine
213
+ require_template_library 'erb' unless defined? ::ERB
214
+ end
215
+
216
+ def compile!
217
+ @engine = ::ERB.new(data, options[:safe], options[:trim], '@_out_buf')
218
+ end
219
+
220
+ def template_source
221
+ @engine.src
222
+ end
223
+
224
+ def evaluate(scope, locals, &block)
225
+ source, offset = local_assignment_code(locals)
226
+ source = [source, template_source].join("\n")
227
+
228
+ original_out_buf =
229
+ scope.instance_variables.any? { |var| var.to_sym == :@_out_buf } &&
230
+ scope.instance_variable_get(:@_out_buf)
231
+
232
+ scope.instance_eval source, eval_file, line - offset
233
+
234
+ output = scope.instance_variable_get(:@_out_buf)
235
+ scope.instance_variable_set(:@_out_buf, original_out_buf)
236
+
237
+ output
238
+ end
239
+
240
+ private
241
+
242
+ # ERB generates a line to specify the character coding of the generated
243
+ # source in 1.9. Account for this in the line offset.
244
+ if RUBY_VERSION >= '1.9.0'
245
+ def local_assignment_code(locals)
246
+ source, offset = super
247
+ [source, offset + 1]
248
+ end
249
+ end
250
+ end
251
+ %w[erb rhtml].each { |ext| register ext, ERBTemplate }
252
+
253
+
254
+ # Erubis template implementation. See:
255
+ # http://www.kuwata-lab.com/erubis/
256
+ class ErubisTemplate < ERBTemplate
257
+ def initialize_engine
258
+ require_template_library 'erubis' unless defined? ::Erubis
259
+ end
260
+
261
+ def compile!
262
+ Erubis::Eruby.class_eval(%Q{def add_preamble(src) src << "@_out_buf = _buf = '';" end})
263
+ @engine = ::Erubis::Eruby.new(data, options)
264
+ end
265
+
266
+ private
267
+
268
+ # Erubis doesn't have ERB's line-off-by-one under 1.9 problem. Override
269
+ # and adjust back.
270
+ if RUBY_VERSION >= '1.9.0'
271
+ def local_assignment_code(locals)
272
+ source, offset = super
273
+ [source, offset - 1]
274
+ end
275
+ end
276
+ end
277
+ register 'erubis', ErubisTemplate
278
+
279
+
280
+ # Haml template implementation. See:
281
+ # http://haml.hamptoncatlin.com/
282
+ class HamlTemplate < Template
283
+ def initialize_engine
284
+ require_template_library 'haml' unless defined? ::Haml::Engine
285
+ end
286
+
287
+ def compile!
288
+ @engine = ::Haml::Engine.new(data, haml_options)
289
+ end
290
+
291
+ def evaluate(scope, locals, &block)
292
+ @engine.render(scope, locals, &block)
293
+ end
294
+
295
+ private
296
+ def haml_options
297
+ options.merge(:filename => eval_file, :line => line)
298
+ end
299
+ end
300
+ register 'haml', HamlTemplate
301
+
302
+
303
+ # Sass template implementation. See:
304
+ # http://haml.hamptoncatlin.com/
305
+ #
306
+ # Sass templates do not support object scopes, locals, or yield.
307
+ class SassTemplate < Template
308
+ def initialize_engine
309
+ require_template_library 'sass' unless defined? ::Sass::Engine
310
+ end
311
+
312
+ def compile!
313
+ @engine = ::Sass::Engine.new(data, sass_options)
314
+ end
315
+
316
+ def evaluate(scope, locals, &block)
317
+ @engine.render
318
+ end
319
+
320
+ private
321
+ def sass_options
322
+ options.merge(:filename => eval_file, :line => line)
323
+ end
324
+ end
325
+ register 'sass', SassTemplate
326
+
327
+
328
+ # Builder template implementation. See:
329
+ # http://builder.rubyforge.org/
330
+ class BuilderTemplate < Template
331
+ def initialize_engine
332
+ require_template_library 'builder' unless defined?(::Builder)
333
+ end
334
+
335
+ def compile!
336
+ end
337
+
338
+ def evaluate(scope, locals, &block)
339
+ xml = ::Builder::XmlMarkup.new(:indent => 2)
340
+ if data.respond_to?(:to_str)
341
+ locals[:xml] = xml
342
+ super(scope, locals, &block)
343
+ elsif data.kind_of?(Proc)
344
+ data.call(xml)
345
+ end
346
+ xml.target!
347
+ end
348
+
349
+ def template_source
350
+ data.to_str
351
+ end
352
+ end
353
+ register 'builder', BuilderTemplate
354
+
355
+
356
+ # Liquid template implementation. See:
357
+ # http://liquid.rubyforge.org/
358
+ #
359
+ # Liquid is designed to be a *safe* template system and threfore
360
+ # does not provide direct access to execuatable scopes. In order to
361
+ # support a +scope+, the +scope+ must be able to represent itself
362
+ # as a hash by responding to #to_h. If the +scope+ does not respond
363
+ # to #to_h it will be ignored.
364
+ #
365
+ # LiquidTemplate does not support yield blocks.
366
+ #
367
+ # It's suggested that your program require 'liquid' at load
368
+ # time when using this template engine.
369
+ class LiquidTemplate < Template
370
+ def initialize_engine
371
+ require_template_library 'liquid' unless defined? ::Liquid::Template
372
+ end
373
+
374
+ def compile!
375
+ @engine = ::Liquid::Template.parse(data)
376
+ end
377
+
378
+ def evaluate(scope, locals, &block)
379
+ locals = locals.inject({}){ |h,(k,v)| h[k.to_s] = v ; h }
380
+ if scope.respond_to?(:to_h)
381
+ scope = scope.to_h.inject({}){ |h,(k,v)| h[k.to_s] = v ; h }
382
+ locals = scope.merge(locals)
383
+ end
384
+ # TODO: Is it possible to lazy yield ?
385
+ locals['yield'] = block.nil? ? '' : yield
386
+ locals['content'] = block.nil? ? '' : yield
387
+ @engine.render(locals)
388
+ end
389
+ end
390
+ register 'liquid', LiquidTemplate
391
+
392
+
393
+ # Discount Markdown implementation. See:
394
+ # http://github.com/rtomayko/rdiscount
395
+ #
396
+ # RDiscount is a simple text filter. It does not support +scope+ or
397
+ # +locals+. The +:smart+ and +:filter_html+ options may be set true
398
+ # to enable those flags on the underlying RDiscount object.
399
+ class RDiscountTemplate < Template
400
+ def flags
401
+ [:smart, :filter_html].select { |flag| options[flag] }
402
+ end
403
+
404
+ def initialize_engine
405
+ require_template_library 'rdiscount' unless defined? ::RDiscount
406
+ end
407
+
408
+ def compile!
409
+ @engine = RDiscount.new(data, *flags)
410
+ end
411
+
412
+ def evaluate(scope, locals, &block)
413
+ @engine.to_html
414
+ end
415
+ end
416
+ register 'markdown', RDiscountTemplate
417
+ register 'mkd', RDiscountTemplate
418
+ register 'md', RDiscountTemplate
419
+
420
+
421
+ # RedCloth implementation. See:
422
+ # http://redcloth.org/
423
+ class RedClothTemplate < Template
424
+ def initialize_engine
425
+ require_template_library 'redcloth' unless defined? ::RedCloth
426
+ end
427
+
428
+ def compile!
429
+ @engine = RedCloth.new(data)
430
+ end
431
+
432
+ def evaluate(scope, locals, &block)
433
+ @engine.to_html
434
+ end
435
+ end
436
+ register 'textile', RedClothTemplate
437
+
438
+
439
+ # Mustache is written and maintained by Chris Wanstrath. See:
440
+ # http://github.com/defunkt/mustache
441
+ #
442
+ # When a scope argument is provided to MustacheTemplate#render, the
443
+ # instance variables are copied from the scope object to the Mustache
444
+ # view.
445
+ class MustacheTemplate < Template
446
+ attr_reader :engine
447
+
448
+ def initialize_engine
449
+ require_template_library 'mustache' unless defined? ::Mustache
450
+ end
451
+
452
+ def compile!
453
+ Mustache.view_namespace = options[:namespace]
454
+ @engine = options[:view] || Mustache.view_class(name)
455
+ options.each do |key, value|
456
+ next if %w[view namespace mustaches].include?(key.to_s)
457
+ @engine.send("#{key}=", value) if @engine.respond_to? "#{key}="
458
+ end
459
+ end
460
+
461
+ def evaluate(scope=nil, locals={}, &block)
462
+ instance = @engine.new
463
+
464
+ # copy instance variables from scope to the view
465
+ scope.instance_variables.each do |name|
466
+ instance.instance_variable_set(name, scope.instance_variable_get(name))
467
+ end
468
+
469
+ # locals get added to the view's context
470
+ locals.each do |local, value|
471
+ instance[local] = value
472
+ end
473
+
474
+ # if we're passed a block it's a subview. Sticking it in yield
475
+ # lets us use {{yield}} in layout.html to render the actual page.
476
+ instance[:yield] = block.call if block
477
+
478
+ instance.template = data unless instance.compiled?
479
+
480
+ instance.to_html
481
+ end
482
+ end
483
+ register 'mustache', MustacheTemplate
484
+
485
+ # RDoc template. See:
486
+ # http://rdoc.rubyforge.org/
487
+ #
488
+ # It's suggested that your program require 'rdoc/markup' and
489
+ # 'rdoc/markup/to_html' at load time when using this template
490
+ # engine.
491
+ class RDocTemplate < Template
492
+ def initialize_engine
493
+ unless defined?(::RDoc::Markup)
494
+ require_template_library 'rdoc/markup'
495
+ require_template_library 'rdoc/markup/to_html'
496
+ end
497
+ end
498
+
499
+ def compile!
500
+ markup = RDoc::Markup::ToHtml.new
501
+ @engine = markup.convert(data)
502
+ end
503
+
504
+ def evaluate(scope, locals, &block)
505
+ @engine.to_s
506
+ end
507
+ end
508
+ register 'rdoc', RDocTemplate
509
+ end