tilt 1.2.2 → 1.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,131 @@
1
+ require 'tilt/template'
2
+
3
+ module Tilt
4
+ # Discount Markdown implementation. See:
5
+ # http://github.com/rtomayko/rdiscount
6
+ #
7
+ # RDiscount is a simple text filter. It does not support +scope+ or
8
+ # +locals+. The +:smart+ and +:filter_html+ options may be set true
9
+ # to enable those flags on the underlying RDiscount object.
10
+ class RDiscountTemplate < Template
11
+ self.default_mime_type = 'text/html'
12
+
13
+ ALIAS = {
14
+ :escape_html => :filter_html,
15
+ :smartypants => :smart
16
+ }
17
+
18
+ FLAGS = [:smart, :filter_html, :smartypants, :escape_html]
19
+
20
+ def flags
21
+ FLAGS.select { |flag| options[flag] }.map { |flag| ALIAS[flag] || flag }
22
+ end
23
+
24
+ def self.engine_initialized?
25
+ defined? ::RDiscount
26
+ end
27
+
28
+ def initialize_engine
29
+ require_template_library 'rdiscount'
30
+ end
31
+
32
+ def prepare
33
+ @engine = RDiscount.new(data, *flags)
34
+ @output = nil
35
+ end
36
+
37
+ def evaluate(scope, locals, &block)
38
+ @output ||= @engine.to_html
39
+ end
40
+ end
41
+
42
+ # Upskirt Markdown implementation. See:
43
+ # https://github.com/tanoku/redcarpet
44
+ #
45
+ # Compatible to RDiscount
46
+ class RedcarpetTemplate < RDiscountTemplate
47
+ self.default_mime_type = 'text/html'
48
+
49
+ def self.engine_initialized?
50
+ defined? ::RedcarpetCompat
51
+ end
52
+
53
+ def initialize_engine
54
+ require_template_library 'redcarpet'
55
+ end
56
+
57
+ def prepare
58
+ @engine = RedcarpetCompat.new(data, *flags)
59
+ @output = nil
60
+ end
61
+ end
62
+
63
+ # BlueCloth Markdown implementation. See:
64
+ # http://deveiate.org/projects/BlueCloth/
65
+ class BlueClothTemplate < Template
66
+ self.default_mime_type = 'text/html'
67
+
68
+ def self.engine_initialized?
69
+ defined? ::BlueCloth
70
+ end
71
+
72
+ def initialize_engine
73
+ require_template_library 'bluecloth'
74
+ end
75
+
76
+ def prepare
77
+ @engine = BlueCloth.new(data, options)
78
+ @output = nil
79
+ end
80
+
81
+ def evaluate(scope, locals, &block)
82
+ @output ||= @engine.to_html
83
+ end
84
+ end
85
+
86
+ # Maruku markdown implementation. See:
87
+ # http://maruku.rubyforge.org/
88
+ class MarukuTemplate < Template
89
+ def self.engine_initialized?
90
+ defined? ::Maruku
91
+ end
92
+
93
+ def initialize_engine
94
+ require_template_library 'maruku'
95
+ end
96
+
97
+ def prepare
98
+ @engine = Maruku.new(data, options)
99
+ @output = nil
100
+ end
101
+
102
+ def evaluate(scope, locals, &block)
103
+ @output ||= @engine.to_html
104
+ end
105
+ end
106
+
107
+ # Kramdown Markdown implementation. See:
108
+ # http://kramdown.rubyforge.org/
109
+ class KramdownTemplate < Template
110
+ DUMB_QUOTES = [39, 39, 34, 34]
111
+
112
+ def self.engine_initialized?
113
+ defined? ::Kramdown
114
+ end
115
+
116
+ def initialize_engine
117
+ require_template_library 'kramdown'
118
+ end
119
+
120
+ def prepare
121
+ options[:smart_quotes] = DUMB_QUOTES unless options[:smartypants]
122
+ @engine = Kramdown::Document.new(data, options)
123
+ @output = nil
124
+ end
125
+
126
+ def evaluate(scope, locals, &block)
127
+ @output ||= @engine.to_html
128
+ end
129
+ end
130
+ end
131
+
@@ -0,0 +1,43 @@
1
+ require 'tilt/template'
2
+
3
+ module Tilt
4
+ # Nokogiri template implementation. See:
5
+ # http://nokogiri.org/
6
+ class NokogiriTemplate < Template
7
+ self.default_mime_type = 'text/xml'
8
+
9
+ def self.engine_initialized?
10
+ defined? ::Nokogiri
11
+ end
12
+
13
+ def initialize_engine
14
+ require_template_library 'nokogiri'
15
+ end
16
+
17
+ def prepare; end
18
+
19
+ def evaluate(scope, locals, &block)
20
+ block &&= proc { yield.gsub(/^<\?xml version=\"1\.0\"\?>\n?/, "") }
21
+
22
+ if data.respond_to?(:to_str)
23
+ super(scope, locals, &block)
24
+ else
25
+ ::Nokogiri::XML::Builder.new.tap(&data).to_xml
26
+ end
27
+ end
28
+
29
+ def precompiled_preamble(locals)
30
+ return super if locals.include? :xml
31
+ "xml = ::Nokogiri::XML::Builder.new\n#{super}"
32
+ end
33
+
34
+ def precompiled_postamble(locals)
35
+ "xml.to_xml"
36
+ end
37
+
38
+ def precompiled_template(locals)
39
+ data.to_str
40
+ end
41
+ end
42
+ end
43
+
@@ -0,0 +1,51 @@
1
+ require 'tilt/template'
2
+
3
+ module Tilt
4
+ # Radius Template
5
+ # http://github.com/jlong/radius/
6
+ class RadiusTemplate < Template
7
+ def self.engine_initialized?
8
+ defined? ::Radius
9
+ end
10
+
11
+ def self.context_class
12
+ @context_class ||= Class.new(Radius::Context) do
13
+ attr_accessor :tilt_scope
14
+
15
+ def tag_missing(name, attributes)
16
+ tilt_scope.__send__(name)
17
+ end
18
+
19
+ def dup
20
+ i = super
21
+ i.tilt_scope = tilt_scope
22
+ i
23
+ end
24
+ end
25
+ end
26
+
27
+ def initialize_engine
28
+ require_template_library 'radius'
29
+ end
30
+
31
+ def prepare
32
+ end
33
+
34
+ def evaluate(scope, locals, &block)
35
+ context = self.class.context_class.new
36
+ context.tilt_scope = scope
37
+ context.define_tag("yield") do
38
+ block.call
39
+ end
40
+ locals.each do |tag, value|
41
+ context.define_tag(tag) do
42
+ value
43
+ end
44
+ end
45
+
46
+ options = {:tag_prefix => 'r'}.merge(@options)
47
+ parser = Radius::Parser.new(context, options)
48
+ parser.parse(data)
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,32 @@
1
+ require 'tilt/template'
2
+
3
+ module Tilt
4
+ # RDoc template. See:
5
+ # http://rdoc.rubyforge.org/
6
+ #
7
+ # It's suggested that your program require 'rdoc/markup' and
8
+ # 'rdoc/markup/to_html' at load time when using this template
9
+ # engine.
10
+ class RDocTemplate < Template
11
+ self.default_mime_type = 'text/html'
12
+
13
+ def self.engine_initialized?
14
+ defined? ::RDoc::Markup
15
+ end
16
+
17
+ def initialize_engine
18
+ require_template_library 'rdoc/markup'
19
+ require_template_library 'rdoc/markup/to_html'
20
+ end
21
+
22
+ def prepare
23
+ markup = RDoc::Markup::ToHtml.new
24
+ @engine = markup.convert(data)
25
+ @output = nil
26
+ end
27
+
28
+ def evaluate(scope, locals, &block)
29
+ @output ||= @engine.to_s
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,21 @@
1
+ require 'tilt/template'
2
+
3
+ module Tilt
4
+ # The template source is evaluated as a Ruby string. The #{} interpolation
5
+ # syntax can be used to generated dynamic output.
6
+ class StringTemplate < Template
7
+ def prepare
8
+ hash = "TILT#{data.hash.abs}"
9
+ @code = "<<#{hash}.chomp\n#{data}\n#{hash}"
10
+ end
11
+
12
+ def precompiled_template(locals)
13
+ @code
14
+ end
15
+
16
+ def precompiled(locals)
17
+ source, offset = super
18
+ [source, offset + 1]
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,285 @@
1
+ module Tilt
2
+ TOPOBJECT = defined?(BasicObject) ? BasicObject : Object
3
+
4
+ # Base class for template implementations. Subclasses must implement
5
+ # the #prepare method and one of the #evaluate or #precompiled_template
6
+ # methods.
7
+ class Template
8
+ # Template source; loaded from a file or given directly.
9
+ attr_reader :data
10
+
11
+ # The name of the file where the template data was loaded from.
12
+ attr_reader :file
13
+
14
+ # The line number in #file where template data was loaded from.
15
+ attr_reader :line
16
+
17
+ # A Hash of template engine specific options. This is passed directly
18
+ # to the underlying engine and is not used by the generic template
19
+ # interface.
20
+ attr_reader :options
21
+
22
+ # Used to determine if this class's initialize_engine method has
23
+ # been called yet.
24
+ @engine_initialized = false
25
+ class << self
26
+ attr_accessor :engine_initialized
27
+ alias engine_initialized? engine_initialized
28
+
29
+ attr_accessor :default_mime_type
30
+ end
31
+
32
+ # Create a new template with the file, line, and options specified. By
33
+ # default, template data is read from the file. When a block is given,
34
+ # it should read template data and return as a String. When file is nil,
35
+ # a block is required.
36
+ #
37
+ # All arguments are optional.
38
+ def initialize(file=nil, line=1, options={}, &block)
39
+ @file, @line, @options = nil, 1, {}
40
+
41
+ [options, line, file].compact.each do |arg|
42
+ case
43
+ when arg.respond_to?(:to_str) ; @file = arg.to_str
44
+ when arg.respond_to?(:to_int) ; @line = arg.to_int
45
+ when arg.respond_to?(:to_hash) ; @options = arg.to_hash.dup
46
+ else raise TypeError
47
+ end
48
+ end
49
+
50
+ raise ArgumentError, "file or block required" if (@file || block).nil?
51
+
52
+ # call the initialize_engine method if this is the very first time
53
+ # an instance of this class has been created.
54
+ if !self.class.engine_initialized?
55
+ initialize_engine
56
+ self.class.engine_initialized = true
57
+ end
58
+
59
+ # used to hold compiled template methods
60
+ @compiled_method = {}
61
+
62
+ # used on 1.9 to set the encoding if it is not set elsewhere (like a magic comment)
63
+ # currently only used if template compiles to ruby
64
+ @default_encoding = @options.delete :default_encoding
65
+
66
+ # load template data and prepare (uses binread to avoid encoding issues)
67
+ @reader = block || lambda { |t| File.respond_to?(:binread) ? File.binread(@file) : File.read(@file) }
68
+ @data = @reader.call(self)
69
+ prepare
70
+ end
71
+
72
+ # Render the template in the given scope with the locals specified. If a
73
+ # block is given, it is typically available within the template via
74
+ # +yield+.
75
+ def render(scope=Object.new, locals={}, &block)
76
+ evaluate scope, locals || {}, &block
77
+ end
78
+
79
+ # The basename of the template file.
80
+ def basename(suffix='')
81
+ File.basename(file, suffix) if file
82
+ end
83
+
84
+ # The template file's basename with all extensions chomped off.
85
+ def name
86
+ basename.split('.', 2).first if basename
87
+ end
88
+
89
+ # The filename used in backtraces to describe the template.
90
+ def eval_file
91
+ file || '(__TEMPLATE__)'
92
+ end
93
+
94
+ protected
95
+ # Called once and only once for each template subclass the first time
96
+ # the template class is initialized. This should be used to require the
97
+ # underlying template library and perform any initial setup.
98
+ def initialize_engine
99
+ end
100
+
101
+ # Like Kernel::require but issues a warning urging a manual require when
102
+ # running under a threaded environment.
103
+ def require_template_library(name)
104
+ if Thread.list.size > 1
105
+ warn "WARN: tilt autoloading '#{name}' in a non thread-safe way; " +
106
+ "explicit require '#{name}' suggested."
107
+ end
108
+ require name
109
+ end
110
+
111
+ # Do whatever preparation is necessary to setup the underlying template
112
+ # engine. Called immediately after template data is loaded. Instance
113
+ # variables set in this method are available when #evaluate is called.
114
+ #
115
+ # Subclasses must provide an implementation of this method.
116
+ def prepare
117
+ if respond_to?(:compile!)
118
+ # backward compat with tilt < 0.6; just in case
119
+ warn 'Tilt::Template#compile! is deprecated; implement #prepare instead.'
120
+ compile!
121
+ else
122
+ raise NotImplementedError
123
+ end
124
+ end
125
+
126
+ def evaluate(scope, locals, &block)
127
+ cached_evaluate(scope, locals, &block)
128
+ end
129
+
130
+ # Process the template and return the result. The first time this
131
+ # method is called, the template source is evaluated with instance_eval.
132
+ # On the sequential method calls it will compile the template to an
133
+ # unbound method which will lead to better performance. In any case,
134
+ # template executation is guaranteed to be performed in the scope object
135
+ # with the locals specified and with support for yielding to the block.
136
+ def cached_evaluate(scope, locals, &block)
137
+ # Redefine itself to use method compilation the next time:
138
+ def self.cached_evaluate(scope, locals, &block)
139
+ method = compiled_method(locals.keys)
140
+ method.bind(scope).call(locals, &block)
141
+ end
142
+
143
+ # Use instance_eval the first time:
144
+ evaluate_source(scope, locals, &block)
145
+ end
146
+
147
+ # Generates all template source by combining the preamble, template, and
148
+ # postamble and returns a two-tuple of the form: [source, offset], where
149
+ # source is the string containing (Ruby) source code for the template and
150
+ # offset is the integer line offset where line reporting should begin.
151
+ #
152
+ # Template subclasses may override this method when they need complete
153
+ # control over source generation or want to adjust the default line
154
+ # offset. In most cases, overriding the #precompiled_template method is
155
+ # easier and more appropriate.
156
+ def precompiled(locals)
157
+ preamble = precompiled_preamble(locals)
158
+ template = precompiled_template(locals)
159
+ magic_comment = extract_magic_comment(template)
160
+ if magic_comment
161
+ # Magic comment e.g. "# coding: utf-8" has to be in the first line.
162
+ # So we copy the magic comment to the first line.
163
+ preamble = magic_comment + "\n" + preamble
164
+ end
165
+ parts = [
166
+ preamble,
167
+ template,
168
+ precompiled_postamble(locals)
169
+ ]
170
+ [parts.join("\n"), preamble.count("\n") + 1]
171
+ end
172
+
173
+ # A string containing the (Ruby) source code for the template. The
174
+ # default Template#evaluate implementation requires either this method
175
+ # or the #precompiled method be overridden. When defined, the base
176
+ # Template guarantees correct file/line handling, locals support, custom
177
+ # scopes, and support for template compilation when the scope object
178
+ # allows it.
179
+ def precompiled_template(locals)
180
+ raise NotImplementedError
181
+ end
182
+
183
+ # Generates preamble code for initializing template state, and performing
184
+ # locals assignment. The default implementation performs locals
185
+ # assignment only. Lines included in the preamble are subtracted from the
186
+ # source line offset, so adding code to the preamble does not effect line
187
+ # reporting in Kernel::caller and backtraces.
188
+ def precompiled_preamble(locals)
189
+ locals.map { |k,v| "#{k} = locals[#{k.inspect}]" }.join("\n")
190
+ end
191
+
192
+ # Generates postamble code for the precompiled template source. The
193
+ # string returned from this method is appended to the precompiled
194
+ # template source.
195
+ def precompiled_postamble(locals)
196
+ ''
197
+ end
198
+
199
+ # The compiled method for the locals keys provided.
200
+ def compiled_method(locals_keys)
201
+ @compiled_method[locals_keys] ||=
202
+ compile_template_method(locals_keys)
203
+ end
204
+
205
+ private
206
+ # Evaluate the template source in the context of the scope object.
207
+ def evaluate_source(scope, locals, &block)
208
+ source, offset = precompiled(locals)
209
+ scope.instance_eval(source, eval_file, line - offset)
210
+ end
211
+
212
+ # JRuby doesn't allow Object#instance_eval to yield to the block it's
213
+ # closed over. This is by design and (ostensibly) something that will
214
+ # change in MRI, though no current MRI version tested (1.8.6 - 1.9.2)
215
+ # exhibits the behavior. More info here:
216
+ #
217
+ # http://jira.codehaus.org/browse/JRUBY-2599
218
+ #
219
+ # We redefine evaluate_source to work around this issues.
220
+ if defined?(RUBY_ENGINE) && RUBY_ENGINE == 'jruby'
221
+ undef evaluate_source
222
+ def evaluate_source(scope, locals, &block)
223
+ source, offset = precompiled(locals)
224
+ file, lineno = eval_file, (line - offset)
225
+ scope.instance_eval { Kernel::eval(source, binding, file, lineno) }
226
+ end
227
+ end
228
+
229
+ def compile_template_method(locals)
230
+ source, offset = precompiled(locals)
231
+ offset += 5
232
+ method_name = "__tilt_#{Thread.current.object_id.abs}"
233
+ Object.class_eval <<-RUBY, eval_file, line - offset
234
+ #{extract_magic_comment source}
235
+ TOPOBJECT.class_eval do
236
+ def #{method_name}(locals)
237
+ Thread.current[:tilt_vars] = [self, locals]
238
+ class << self
239
+ this, locals = Thread.current[:tilt_vars]
240
+ this.instance_eval do
241
+ #{source}
242
+ end
243
+ end
244
+ end
245
+ end
246
+ RUBY
247
+ unbind_compiled_method(method_name)
248
+ end
249
+
250
+ def unbind_compiled_method(method_name)
251
+ method = TOPOBJECT.instance_method(method_name)
252
+ TOPOBJECT.class_eval { remove_method(method_name) }
253
+ method
254
+ end
255
+
256
+ def extract_magic_comment(script)
257
+ comment = script.slice(/\A[ \t]*\#.*coding\s*[=:]\s*([[:alnum:]\-_]+).*$/)
258
+ return comment if comment and not %w[ascii-8bit binary].include?($1.downcase)
259
+ "# coding: #{@default_encoding}" if @default_encoding
260
+ end
261
+
262
+ # Special case Ruby 1.9.1's broken yield.
263
+ #
264
+ # http://github.com/rtomayko/tilt/commit/20c01a5
265
+ # http://redmine.ruby-lang.org/issues/show/3601
266
+ #
267
+ # Remove when 1.9.2 dominates 1.9.1 installs in the wild.
268
+ if RUBY_VERSION =~ /^1.9.1/
269
+ undef compile_template_method
270
+ def compile_template_method(locals)
271
+ source, offset = precompiled(locals)
272
+ offset += 1
273
+ method_name = "__tilt_#{Thread.current.object_id}"
274
+ Object.class_eval <<-RUBY, eval_file, line - offset
275
+ TOPOBJECT.class_eval do
276
+ def #{method_name}(locals)
277
+ #{source}
278
+ end
279
+ end
280
+ RUBY
281
+ unbind_compiled_method(method_name)
282
+ end
283
+ end
284
+ end
285
+ end