/
template.rb
308 lines (288 loc) · 11.9 KB
/
template.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
# encoding: UTF-8
module Asciidoctor
# A {Converter} implementation that uses templates composed in template
# languages supported by {https://github.com/rtomayko/tilt Tilt} to convert
# {AbstractNode} objects from a parsed AsciiDoc document tree to the backend
# format.
#
# The converter scans the specified directories for template files that are
# supported by Tilt. If an engine name (e.g., "slim") is specified in the
# options Hash passed to the constructor, the scan is restricted to template
# files that have a matching extension (e.g., ".slim"). The scanner trims any
# extensions from the basename of the file and uses the resulting name as the
# key under which to store the template. When the {Converter#convert} method
# is invoked, the transform argument is used to select the template from this
# table and use it to convert the node.
#
# For example, the template file "path/to/templates/paragraph.html.slim" will
# be registered as the "paragraph" transform. The template is then used to
# convert a paragraph {Block} object from the parsed AsciiDoc tree to an HTML
# backend format (e.g., "html5").
#
# As an optimization, scan results and templates are cached for the lifetime
# of the Ruby process. If the {https://rubygems.org/gems/thread_safe
# thread_safe} gem is installed, these caches are guaranteed to be thread
# safe. If this gem is not present, there is no such guarantee and a warning
# will be issued.
class Converter::TemplateConverter < Converter::Base
DEFAULT_ENGINE_OPTIONS = {
:erb => { :trim => '<' },
# TODO line 466 of haml/compiler.rb sorts the attributes; file an issue to make this configurable
# NOTE AsciiDoc syntax expects HTML/XML output to use double quotes around attribute values
:haml => { :format => :xhtml, :attr_wrapper => '"', :escape_attrs => false, :ugly => true },
:slim => { :disable_escape => true, :sort_attrs => false, :pretty => false }
}
# QUESTION are we handling how we load the thread_safe support correctly?
begin
require 'thread_safe' unless defined? ::ThreadSafe
@caches = { :scans => ::ThreadSafe::Cache.new, :templates => ::ThreadSafe::Cache.new }
rescue ::LoadError
@caches = { :scans => {}, :templates => {} }
# FIXME perhaps only warn if the cache option is enabled (meaning not disabled)?
warn 'asciidoctor: WARNING: gem \'thread_safe\' is not installed. This gem is recommended when using custom backend templates.'
end
def self.caches
@caches
end
def self.clear_caches
@caches[:scans].clear if @caches[:scans]
@caches[:templates].clear if @caches[:templates]
end
def initialize backend, template_dirs, opts = {}
Helpers.require_library 'tilt' unless defined? ::Tilt
@backend = backend
@templates = {}
@template_dirs = template_dirs
@eruby = opts[:eruby]
@safe = opts[:safe]
@active_engines = {}
@engine = opts[:template_engine]
@engine_options = DEFAULT_ENGINE_OPTIONS.inject({}) do |accum, (engine, default_opts)|
accum[engine] = default_opts.dup
accum
end
if opts[:htmlsyntax] == 'html'
@engine_options[:haml][:format] = :html5
@engine_options[:slim][:format] = :html
end
@engine_options[:slim][:include_dirs] = template_dirs.reverse.map {|dir| ::File.expand_path dir }
if (overrides = opts[:template_engine_options])
overrides.each do |engine, override_opts|
(@engine_options[engine] ||= {}).update override_opts
end
end
case opts[:template_cache]
when true
@caches = self.class.caches
when ::Hash
@caches = opts[:template_cache]
else
@caches = {} # the empty Hash effectively disables caching
end
scan
#create_handlers
end
=begin
# Public: Called when this converter is added to a composite converter.
def composed parent
# TODO set the backend info determined during the scan
end
=end
# Internal: Scans the template directories specified in the constructor for Tilt-supported
# templates, loads the templates and stores the in a Hash that is accessible via the
# {TemplateConverter#templates} method.
#
# Returns nothing
def scan
path_resolver = PathResolver.new
backend = @backend
engine = @engine
@template_dirs.each do |template_dir|
# FIXME need to think about safe mode restrictions here
next unless ::File.directory?(template_dir = (path_resolver.system_path template_dir))
if engine
file_pattern = %(*.#{engine})
# example: templates/haml
if ::File.directory?(engine_dir = %(#{template_dir}/#{engine}))
template_dir = engine_dir
end
else
# NOTE last matching template wins for template name if no engine is given
file_pattern = '*'
end
# example: templates/html5 (engine not set) or templates/haml/html5 (engine set)
if ::File.directory?(backend_dir = %(#{template_dir}/#{backend}))
template_dir = backend_dir
end
pattern = %(#{template_dir}/#{file_pattern})
if (scan_cache = @caches[:scans])
template_cache = @caches[:templates]
unless (templates = scan_cache[pattern])
templates = (scan_cache[pattern] = (scan_dir template_dir, pattern, template_cache))
end
templates.each do |name, template|
@templates[name] = template_cache[template.file] = template
end
else
@templates.update scan_dir(template_dir, pattern, @caches[:templates])
end
nil
end
end
=begin
# Internal: Creates convert methods (e.g., inline_anchor) that delegate to the discovered templates.
#
# Returns nothing
def create_handlers
@templates.each do |name, template|
create_handler name, template
end
nil
end
# Internal: Creates a convert method for the specified name that delegates to the specified template.
#
# Returns nothing
def create_handler name, template
metaclass = class << self; self; end
if name == 'document'
metaclass.send :define_method, name do |node|
(template.render node).strip
end
else
metaclass.send :define_method, name do |node|
(template.render node).chomp
end
end
end
=end
# Public: Convert an {AbstractNode} to the backend format using the named template.
#
# Looks for a template that matches the value of the
# {AbstractNode#node_name} property if a template name is not specified.
#
# node - the AbstractNode to convert
# template_name - the String name of the template to use, or the value of
# the node_name property on the node if a template name is
# not specified. (optional, default: nil)
# opts - an optional Hash that is passed as local variables to the
# template. (optional, default: {})
#
# Returns the [String] result from rendering the template
def convert node, template_name = nil, opts = {}
template_name ||= node.node_name
unless (template = @templates[template_name])
raise %(Could not find a custom template to handle transform: #{template_name})
end
# Slim doesn't include helpers in the template's execution scope (like HAML), so do it ourselves
node.extend ::Slim::Helpers if (defined? ::Slim::Helpers) && (::Slim::Template === template)
# NOTE opts become locals in the template
if template_name == 'document'
(template.render node, opts).strip
else
(template.render node, opts).chomp
end
end
# Public: Checks whether there is a Tilt template registered with the specified name.
#
# name - the String template name
#
# Returns a [Boolean] that indicates whether a Tilt template is registered for the
# specified template name.
def handles? name
@templates.key? name
end
# Public: Retrieves the templates that this converter manages.
#
# Returns a [Hash] of Tilt template objects keyed by template name.
def templates
@templates.dup
end
# Public: Registers a Tilt template with this converter.
#
# name - the String template name
# template - the Tilt template object to register
#
# Returns the Tilt template object
def register name, template
@templates[name] = if (template_cache = @caches[:templates])
template_cache[template.file] = template
else
template
end
#create_handler name, template
end
# Internal: Scan the specified directory for template files matching pattern and instantiate
# a Tilt template for each matched file.
#
# Returns the scan result as a [Hash]
def scan_dir template_dir, pattern, template_cache = nil
result, helpers = {}, nil
# Grab the files in the top level of the directory (do not recurse)
::Dir.glob(pattern).select {|match| ::File.file? match }.each do |file|
if (basename = ::File.basename file) == 'helpers.rb'
helpers = file
next
elsif (path_segments = basename.split '.').size < 2
next
end
if (name = path_segments[0]) == 'block_ruler'
name = 'thematic_break'
elsif name.start_with? 'block_'
name = name.slice 6, name.length
end
unless template_cache && (template = template_cache[file])
template_class, extra_engine_options, extsym = ::Tilt, {}, path_segments[-1].to_sym
case extsym
when :slim
unless @active_engines[extsym]
# NOTE slim doesn't get automatically loaded by Tilt
Helpers.require_library 'slim' unless defined? ::Slim
# align safe mode of AsciiDoc embedded in Slim template with safe mode of current document
# NOTE safe mode won't get updated if using template cache and changing safe mode
(@engine_options[extsym][:asciidoc] ||= {})[:safe] ||= @safe if @safe && ::Slim::VERSION >= '3.0'
# load include plugin when using Slim >= 2.1
require 'slim/include' unless (defined? ::Slim::Include) || ::Slim::VERSION < '2.1'
@active_engines[extsym] = true
end
when :haml
unless @active_engines[extsym]
Helpers.require_library 'haml' unless defined? ::Haml
# NOTE Haml 5 dropped support for pretty printing
@engine_options[extsym].delete :ugly if defined? ::Haml::TempleEngine
@active_engines[extsym] = true
end
when :erb
template_class, extra_engine_options = (@active_engines[extsym] ||= (load_eruby @eruby))
when :rb
next
else
next unless ::Tilt.registered? extsym.to_s
end
template = template_class.new file, 1, (@engine_options[extsym] ||= {}).merge(extra_engine_options)
end
result[name] = template
end
if helpers || ::File.file?(helpers = %(#{template_dir}/helpers.rb))
require helpers
end
result
end
# Internal: Load the eRuby implementation
#
# name - the String name of the eRuby implementation
#
# Returns an [Array] containing the Tilt template Class for the eRuby implementation
# and a Hash of additional options to pass to the initializer
def load_eruby name
if !name || name == 'erb'
require 'erb' unless defined? ::ERB
[::Tilt::ERBTemplate, {}]
elsif name == 'erubis'
Helpers.require_library 'erubis' unless defined? ::Erubis::FastEruby
[::Tilt::ErubisTemplate, { :engine_class => ::Erubis::FastEruby }]
else
raise ::ArgumentError, %(Unknown ERB implementation: #{name})
end
end
end
end