asciidoctor 0.1.3 → 0.1.4
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of asciidoctor might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/CHANGELOG.adoc +387 -0
- data/README.adoc +358 -348
- data/asciidoctor.gemspec +30 -9
- data/bin/asciidoctor +3 -0
- data/bin/asciidoctor-safe +3 -0
- data/compat/asciidoc.conf +76 -4
- data/lib/asciidoctor.rb +174 -79
- data/lib/asciidoctor/abstract_block.rb +131 -101
- data/lib/asciidoctor/abstract_node.rb +108 -26
- data/lib/asciidoctor/attribute_list.rb +1 -1
- data/lib/asciidoctor/backends/_stylesheets.rb +204 -62
- data/lib/asciidoctor/backends/base_template.rb +11 -22
- data/lib/asciidoctor/backends/docbook45.rb +158 -163
- data/lib/asciidoctor/backends/docbook5.rb +103 -0
- data/lib/asciidoctor/backends/html5.rb +662 -445
- data/lib/asciidoctor/block.rb +54 -44
- data/lib/asciidoctor/cli/invoker.rb +41 -20
- data/lib/asciidoctor/cli/options.rb +66 -20
- data/lib/asciidoctor/debug.rb +1 -1
- data/lib/asciidoctor/document.rb +265 -100
- data/lib/asciidoctor/extensions.rb +443 -0
- data/lib/asciidoctor/helpers.rb +38 -6
- data/lib/asciidoctor/inline.rb +5 -5
- data/lib/asciidoctor/lexer.rb +532 -250
- data/lib/asciidoctor/{list_item.rb → list.rb} +33 -13
- data/lib/asciidoctor/path_resolver.rb +28 -2
- data/lib/asciidoctor/reader.rb +814 -455
- data/lib/asciidoctor/renderer.rb +128 -42
- data/lib/asciidoctor/section.rb +55 -41
- data/lib/asciidoctor/substituters.rb +380 -107
- data/lib/asciidoctor/table.rb +40 -30
- data/lib/asciidoctor/version.rb +1 -1
- data/man/asciidoctor.1 +32 -96
- data/man/{asciidoctor.ad → asciidoctor.adoc} +57 -48
- data/test/attributes_test.rb +200 -27
- data/test/blocks_test.rb +361 -22
- data/test/document_test.rb +496 -81
- data/test/extensions_test.rb +448 -0
- data/test/fixtures/basic-docinfo-footer.html +6 -0
- data/test/fixtures/basic-docinfo-footer.xml +8 -0
- data/test/fixtures/basic-docinfo.xml +3 -3
- data/test/fixtures/basic.asciidoc +1 -0
- data/test/fixtures/child-include.adoc +5 -0
- data/test/fixtures/custom-backends/haml/docbook45/block_paragraph.xml.haml +6 -0
- data/test/fixtures/custom-backends/haml/html5-tweaks/block_paragraph.html.haml +1 -0
- data/test/fixtures/custom-backends/haml/html5/block_paragraph.html.haml +3 -0
- data/test/fixtures/custom-backends/haml/html5/block_sidebar.html.haml +5 -0
- data/test/fixtures/custom-backends/slim/docbook45/block_paragraph.xml.slim +6 -0
- data/test/fixtures/custom-backends/slim/html5/block_paragraph.html.slim +3 -0
- data/test/fixtures/custom-backends/slim/html5/block_sidebar.html.slim +5 -0
- data/test/fixtures/docinfo-footer.html +1 -0
- data/test/fixtures/docinfo-footer.xml +9 -0
- data/test/fixtures/docinfo.xml +1 -0
- data/test/fixtures/grandchild-include.adoc +3 -0
- data/test/fixtures/parent-include-restricted.adoc +5 -0
- data/test/fixtures/parent-include.adoc +5 -0
- data/test/invoker_test.rb +82 -8
- data/test/lexer_test.rb +21 -3
- data/test/links_test.rb +34 -2
- data/test/lists_test.rb +304 -7
- data/test/options_test.rb +19 -3
- data/test/paragraphs_test.rb +13 -0
- data/test/paths_test.rb +22 -0
- data/test/preamble_test.rb +20 -0
- data/test/reader_test.rb +1096 -644
- data/test/renderer_test.rb +152 -12
- data/test/sections_test.rb +417 -76
- data/test/substitutions_test.rb +339 -138
- data/test/tables_test.rb +109 -4
- data/test/test_helper.rb +79 -13
- data/test/text_test.rb +111 -11
- metadata +54 -18
@@ -0,0 +1,443 @@
|
|
1
|
+
module Asciidoctor
|
2
|
+
module Extensions
|
3
|
+
class Extension
|
4
|
+
class << self
|
5
|
+
def register
|
6
|
+
::Asciidoctor::Extensions.register self
|
7
|
+
end
|
8
|
+
|
9
|
+
def activate registry, document
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
class << self
|
15
|
+
def registered?
|
16
|
+
!@registered.nil?
|
17
|
+
end
|
18
|
+
|
19
|
+
def registered
|
20
|
+
@registered ||= []
|
21
|
+
end
|
22
|
+
|
23
|
+
# QUESTION should we require extensions to have names?
|
24
|
+
# how about autogenerate name for class, assume extension
|
25
|
+
# is name of block if block is given
|
26
|
+
# having a name makes it easier to unregister an extension
|
27
|
+
def register extension = nil, &block
|
28
|
+
if block_given?
|
29
|
+
registered << block
|
30
|
+
elsif extension
|
31
|
+
registered << resolve_class(extension)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def resolve_class(object)
|
36
|
+
object.is_a?(Class) ? object : class_for_name(object.to_s)
|
37
|
+
end
|
38
|
+
|
39
|
+
def class_for_name(qualified_name)
|
40
|
+
qualified_name.split('::').inject(Object) do |module_, name|
|
41
|
+
if name.empty?
|
42
|
+
module_
|
43
|
+
elsif module_.const_defined? name
|
44
|
+
module_.const_get(name)
|
45
|
+
else
|
46
|
+
raise "Could not resolve class for name: #{qualified_name}"
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def unregister_all
|
52
|
+
@registered = []
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
class Registry
|
57
|
+
attr_accessor :preprocessors
|
58
|
+
attr_accessor :treeprocessors
|
59
|
+
attr_accessor :postprocessors
|
60
|
+
attr_accessor :include_processors
|
61
|
+
attr_accessor :blocks
|
62
|
+
attr_accessor :block_macros
|
63
|
+
attr_accessor :inline_macros
|
64
|
+
|
65
|
+
def initialize document = nil
|
66
|
+
@preprocessors = []
|
67
|
+
@treeprocessors = []
|
68
|
+
@postprocessors = []
|
69
|
+
@include_processors = []
|
70
|
+
@include_processor_cache = {}
|
71
|
+
@block_delimiters = {}
|
72
|
+
@blocks = {}
|
73
|
+
@block_processor_cache = {}
|
74
|
+
@block_macros = {}
|
75
|
+
@block_macro_processor_cache = {}
|
76
|
+
@inline_macros = {}
|
77
|
+
@inline_macro_processor_cache = {}
|
78
|
+
|
79
|
+
Extensions.registered.each do |extension|
|
80
|
+
if extension.is_a? Proc
|
81
|
+
register document, &extension
|
82
|
+
else
|
83
|
+
extension.activate self, document
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def preprocessor processor, position = :<<
|
89
|
+
processor = resolve_processor_class processor
|
90
|
+
if position == :<< || @preprocessors.empty?
|
91
|
+
@preprocessors.push processor
|
92
|
+
elsif position == :>>
|
93
|
+
@preprocessors.unshift processor
|
94
|
+
else
|
95
|
+
@preprocessors.push processor
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def preprocessors?
|
100
|
+
!@preprocessors.empty?
|
101
|
+
end
|
102
|
+
|
103
|
+
def load_preprocessors *args
|
104
|
+
@preprocessors.map do |processor|
|
105
|
+
processor.new(*args)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
def treeprocessor processor, position = :<<
|
110
|
+
processor = resolve_processor_class processor
|
111
|
+
if position == :<< || @treeprocessors.empty?
|
112
|
+
@treeprocessors.push processor
|
113
|
+
elsif position == :>>
|
114
|
+
@treeprocessors.unshift processor
|
115
|
+
else
|
116
|
+
@treeprocessors.push processor
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
def treeprocessors?
|
121
|
+
!@treeprocessors.empty?
|
122
|
+
end
|
123
|
+
|
124
|
+
def load_treeprocessors *args
|
125
|
+
@treeprocessors.map do |processor|
|
126
|
+
processor.new(*args)
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
def postprocessor processor, position = :<<
|
131
|
+
processor = resolve_processor_class processor
|
132
|
+
if position == :<< || @postprocessors.empty?
|
133
|
+
@postprocessors.push processor
|
134
|
+
elsif position == :>>
|
135
|
+
@postprocessors.unshift processor
|
136
|
+
else
|
137
|
+
@postprocessors.push processor
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
def postprocessors?
|
142
|
+
!@postprocessors.empty?
|
143
|
+
end
|
144
|
+
|
145
|
+
def load_postprocessors *args
|
146
|
+
@postprocessors.map do |processor|
|
147
|
+
processor.new(*args)
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
def include_processor processor, position = :<<
|
152
|
+
processor = resolve_processor_class processor
|
153
|
+
if position == :<< || @include_processors.empty?
|
154
|
+
@include_processors.push processor
|
155
|
+
elsif position == :>>
|
156
|
+
@include_processors.unshift processor
|
157
|
+
else
|
158
|
+
@include_processors.push processor
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
def include_processors?
|
163
|
+
!@include_processors.empty?
|
164
|
+
end
|
165
|
+
|
166
|
+
def load_include_processors *args
|
167
|
+
@include_processors.map do |processor|
|
168
|
+
processor.new(*args)
|
169
|
+
end
|
170
|
+
# QUESTION do we need/want the cache?
|
171
|
+
#@include_processors.map do |processor|
|
172
|
+
# @include_processor_cache[processor] ||= processor.new(*args)
|
173
|
+
#end
|
174
|
+
end
|
175
|
+
|
176
|
+
# TODO allow contexts to be specified here, perhaps as [:upper, [:paragraph, :sidebar]]
|
177
|
+
def block name, processor, delimiter = nil, &block
|
178
|
+
processor = resolve_processor_class processor
|
179
|
+
@blocks[name] = processor
|
180
|
+
if block_given?
|
181
|
+
@block_delimiters[block] = name
|
182
|
+
elsif delimiter && delimiter.is_a?(Regexp)
|
183
|
+
@block_delimiters[delimiter] = name
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
def blocks?
|
188
|
+
!@blocks.empty?
|
189
|
+
end
|
190
|
+
|
191
|
+
def block_delimiters?
|
192
|
+
!@block_delimiters.empty?
|
193
|
+
end
|
194
|
+
|
195
|
+
# NOTE block delimiters not yet implemented
|
196
|
+
def at_block_delimiter? line
|
197
|
+
@block_delimiters.each do |delimiter, name|
|
198
|
+
if delimiter.is_a? Proc
|
199
|
+
if delimiter.call(line)
|
200
|
+
return name
|
201
|
+
end
|
202
|
+
else
|
203
|
+
if line.match(delimiter)
|
204
|
+
return name
|
205
|
+
end
|
206
|
+
end
|
207
|
+
end
|
208
|
+
false
|
209
|
+
end
|
210
|
+
|
211
|
+
def load_block_processor name, *args
|
212
|
+
@block_processor_cache[name] ||= @blocks[name].new(name.to_sym, *args)
|
213
|
+
end
|
214
|
+
|
215
|
+
def processor_registered_for_block? name, context
|
216
|
+
if @blocks.has_key? name.to_sym
|
217
|
+
(@blocks[name.to_sym].config.fetch(:contexts, nil) || []).include?(context)
|
218
|
+
else
|
219
|
+
false
|
220
|
+
end
|
221
|
+
end
|
222
|
+
|
223
|
+
def block_macro name, processor
|
224
|
+
processor = resolve_processor_class processor
|
225
|
+
@block_macros[name.to_s] = processor
|
226
|
+
end
|
227
|
+
|
228
|
+
def block_macros?
|
229
|
+
!@block_macros.empty?
|
230
|
+
end
|
231
|
+
|
232
|
+
def load_block_macro_processor name, *args
|
233
|
+
@block_macro_processor_cache[name] ||= @block_macros[name].new(name, *args)
|
234
|
+
end
|
235
|
+
|
236
|
+
def processor_registered_for_block_macro? name
|
237
|
+
@block_macros.has_key? name
|
238
|
+
end
|
239
|
+
|
240
|
+
# TODO probably need ordering control before/after other inline macros
|
241
|
+
def inline_macro name, processor
|
242
|
+
processor = resolve_processor_class processor
|
243
|
+
@inline_macros[name.to_s] = processor
|
244
|
+
end
|
245
|
+
|
246
|
+
def inline_macros?
|
247
|
+
!@inline_macros.empty?
|
248
|
+
end
|
249
|
+
|
250
|
+
def load_inline_macro_processor name, *args
|
251
|
+
@inline_macro_processor_cache[name] ||= @inline_macros[name].new(name, *args)
|
252
|
+
end
|
253
|
+
|
254
|
+
def load_inline_macro_processors *args
|
255
|
+
@inline_macros.map do |name, processor|
|
256
|
+
load_inline_macro_processor name, *args
|
257
|
+
end
|
258
|
+
end
|
259
|
+
|
260
|
+
def processor_registered_for_inline_macro? name
|
261
|
+
@inline_macros.has_key? name
|
262
|
+
end
|
263
|
+
|
264
|
+
def register document, &block
|
265
|
+
instance_exec document, &block
|
266
|
+
end
|
267
|
+
|
268
|
+
def resolve_processor_class object
|
269
|
+
::Asciidoctor::Extensions.resolve_class object
|
270
|
+
end
|
271
|
+
|
272
|
+
def reset
|
273
|
+
@block_processor_cache = {}
|
274
|
+
@block_macro_processor_cache = {}
|
275
|
+
@inline_macro_processor_cache = {}
|
276
|
+
end
|
277
|
+
end
|
278
|
+
|
279
|
+
class Processor
|
280
|
+
def initialize(document)
|
281
|
+
@document = document
|
282
|
+
end
|
283
|
+
end
|
284
|
+
|
285
|
+
# Public: Preprocessors are run after the source text is split into lines and
|
286
|
+
# before parsing begins.
|
287
|
+
#
|
288
|
+
# Prior to invoking the preprocessor, Asciidoctor splits the source text into
|
289
|
+
# lines and normalizes them. The normalize process strips trailing whitespace
|
290
|
+
# from each line and leaves behind a line-feed character (i.e., "\n").
|
291
|
+
#
|
292
|
+
# Asciidoctor passes a reference to the Reader and a copy of the lines Array
|
293
|
+
# to the process method of an instance of each registered Preprocessor. The
|
294
|
+
# Preprocessor modifies the Array as necessary and either returns a reference
|
295
|
+
# to the same Reader or a reference to a new one.
|
296
|
+
#
|
297
|
+
# Preprocessors must extend Asciidoctor::Extensions::Preprocessor.
|
298
|
+
class Preprocessor < Processor
|
299
|
+
# Public: Accepts the Reader and an Array of lines, modifies them as
|
300
|
+
# needed, then returns the Reader or a reference to a new one.
|
301
|
+
#
|
302
|
+
# Each subclass of Preprocessor should override this method.
|
303
|
+
def process reader, lines
|
304
|
+
reader
|
305
|
+
end
|
306
|
+
end
|
307
|
+
|
308
|
+
# Public: Treeprocessors are run on the Document after the source has been
|
309
|
+
# parsed into an abstract syntax tree, as represented by the Document object
|
310
|
+
# and its child Node objects.
|
311
|
+
#
|
312
|
+
# Asciidoctor invokes the process method on an instance of each registered
|
313
|
+
# Treeprocessor.
|
314
|
+
#
|
315
|
+
# QUESTION should the treeprocessor get invoked after parse header too?
|
316
|
+
#
|
317
|
+
# Treeprocessors must extend Asciidoctor::Extensions::Treeprocessor.
|
318
|
+
class Treeprocessor < Processor
|
319
|
+
def process
|
320
|
+
end
|
321
|
+
end
|
322
|
+
|
323
|
+
# Public: Postprocessors are run after the document is rendered and before
|
324
|
+
# it's written to the output stream.
|
325
|
+
#
|
326
|
+
# Asciidoctor passes a reference to the output String to the process method
|
327
|
+
# of each registered Postprocessor. The Preprocessor modifies the String as
|
328
|
+
# necessary and returns the String replacement.
|
329
|
+
#
|
330
|
+
# The markup format in the String is determined from the backend used to
|
331
|
+
# render the Document. The backend and be looked up using the backend method
|
332
|
+
# on the Document object, as well as various backend-related document
|
333
|
+
# attributes.
|
334
|
+
#
|
335
|
+
# Postprocessors can also be used to relocate assets needed by the published
|
336
|
+
# document.
|
337
|
+
#
|
338
|
+
# Postprocessors must extend Asciidoctor::Extensions::Postprocessor.
|
339
|
+
class Postprocessor < Processor
|
340
|
+
def process output
|
341
|
+
output
|
342
|
+
end
|
343
|
+
end
|
344
|
+
|
345
|
+
# Public: IncludeProcessors are used to process include::[] macros in the
|
346
|
+
# source document.
|
347
|
+
#
|
348
|
+
# When Asciidoctor discovers an include::[] macro in the source document, it
|
349
|
+
# iterates through the IncludeProcessors and delegates the work of reading
|
350
|
+
# the content to the first processor that identifies itself as capable of
|
351
|
+
# handling that target.
|
352
|
+
#
|
353
|
+
# IncludeProcessors must extend Asciidoctor::Extensions::IncludeProcessor.
|
354
|
+
class IncludeProcessor < Processor
|
355
|
+
def process target, attributes
|
356
|
+
output
|
357
|
+
end
|
358
|
+
end
|
359
|
+
|
360
|
+
# Supported options:
|
361
|
+
# * :contexts - The blocks contexts (types) on which this style can be used (default: [:paragraph, :open]
|
362
|
+
# * :content_model - The structure of the content supported in this block (default: :compound)
|
363
|
+
# * :pos_attrs - A list of attribute names used to map positional attributes (default: nil)
|
364
|
+
# * :default_attrs - Set default values for attributes (default: nil)
|
365
|
+
# * ...
|
366
|
+
class BlockProcessor < Processor
|
367
|
+
class << self
|
368
|
+
def config
|
369
|
+
@config ||= {:contexts => [:paragraph, :open]}
|
370
|
+
end
|
371
|
+
|
372
|
+
def option(key, default_value)
|
373
|
+
config[key] = default_value
|
374
|
+
end
|
375
|
+
end
|
376
|
+
|
377
|
+
attr_reader :document
|
378
|
+
attr_reader :context
|
379
|
+
attr_reader :options
|
380
|
+
|
381
|
+
def initialize(context, document, opts = {})
|
382
|
+
super(document)
|
383
|
+
@context = context
|
384
|
+
@options = self.class.config.dup
|
385
|
+
opts.delete(:contexts) # contexts can't be overridden
|
386
|
+
@options.update(opts)
|
387
|
+
#@options[:contexts] ||= [:paragraph, :open]
|
388
|
+
@options[:content_model] ||= :compound
|
389
|
+
end
|
390
|
+
|
391
|
+
def process parent, reader, attributes
|
392
|
+
nil
|
393
|
+
end
|
394
|
+
end
|
395
|
+
|
396
|
+
class MacroProcessor < Processor
|
397
|
+
class << self
|
398
|
+
def config
|
399
|
+
@config ||= {}
|
400
|
+
end
|
401
|
+
|
402
|
+
def option(key, default_value)
|
403
|
+
config[key] = default_value
|
404
|
+
end
|
405
|
+
end
|
406
|
+
|
407
|
+
attr_reader :document
|
408
|
+
attr_reader :name
|
409
|
+
attr_reader :options
|
410
|
+
|
411
|
+
def initialize(name, document, opts = {})
|
412
|
+
super(document)
|
413
|
+
@name = name
|
414
|
+
@options = self.class.config.dup
|
415
|
+
@options.update(opts)
|
416
|
+
end
|
417
|
+
|
418
|
+
def process parent, target, attributes, source = nil
|
419
|
+
nil
|
420
|
+
end
|
421
|
+
end
|
422
|
+
|
423
|
+
class BlockMacroProcessor < MacroProcessor
|
424
|
+
end
|
425
|
+
|
426
|
+
# TODO break this out into different pattern types
|
427
|
+
# for example, FormalInlineMacro, ShortInlineMacro (no target) and other patterns
|
428
|
+
class InlineMacroProcessor < MacroProcessor
|
429
|
+
def initialize(name, document, opts = {})
|
430
|
+
super
|
431
|
+
@regexp = nil
|
432
|
+
end
|
433
|
+
|
434
|
+
def regexp
|
435
|
+
if @options[:short_form]
|
436
|
+
@regexp ||= %r(\\?#{@name}:\[((?:\\\]|[^\]])*?)\])
|
437
|
+
else
|
438
|
+
@regexp ||= %r(\\?#{@name}:(\S+?)\[((?:\\\]|[^\]])*?)\])
|
439
|
+
end
|
440
|
+
end
|
441
|
+
end
|
442
|
+
end
|
443
|
+
end
|
data/lib/asciidoctor/helpers.rb
CHANGED
@@ -7,7 +7,7 @@ module Helpers
|
|
7
7
|
#
|
8
8
|
# returns false if the library is detected on the load path or the return
|
9
9
|
# value of delegating to Kernel#require
|
10
|
-
def self.require_library(name)
|
10
|
+
def self.require_library(name, gem_name = nil)
|
11
11
|
if Thread.list.size > 1
|
12
12
|
main_script = "#{name}.rb"
|
13
13
|
main_script_path_segment = "/#{name}.rb"
|
@@ -18,7 +18,15 @@ module Helpers
|
|
18
18
|
"The use of an explicit require '#{name}' statement is recommended."
|
19
19
|
end
|
20
20
|
end
|
21
|
-
|
21
|
+
begin
|
22
|
+
require name
|
23
|
+
rescue LoadError => e
|
24
|
+
if gem_name
|
25
|
+
fail "asciidoctor: FAILED: required gem '#{gem_name === true ? name : gem_name}' is not installed. Processing aborted."
|
26
|
+
else
|
27
|
+
fail "asciidoctor: FAILED: #{e.chomp '.'}. Processing aborted."
|
28
|
+
end
|
29
|
+
end
|
22
30
|
end
|
23
31
|
|
24
32
|
# Public: Encode a string for inclusion in a URI
|
@@ -37,6 +45,25 @@ module Helpers
|
|
37
45
|
end
|
38
46
|
end
|
39
47
|
|
48
|
+
# Public: Removes the file extension from filename and returns the result
|
49
|
+
#
|
50
|
+
# file_name - The String file name to process
|
51
|
+
#
|
52
|
+
# Examples
|
53
|
+
#
|
54
|
+
# Helpers.rootname('part1/chapter1.adoc')
|
55
|
+
# # => "part1/chapter1"
|
56
|
+
#
|
57
|
+
# Returns the String filename with the file extension removed
|
58
|
+
def self.rootname(file_name)
|
59
|
+
ext = File.extname(file_name)
|
60
|
+
if ext.empty?
|
61
|
+
file_name
|
62
|
+
else
|
63
|
+
file_name[0...-ext.length]
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
40
67
|
def self.mkdir_p(dir)
|
41
68
|
unless File.directory? dir
|
42
69
|
parent_dir = File.dirname(dir)
|
@@ -47,9 +74,14 @@ module Helpers
|
|
47
74
|
end
|
48
75
|
end
|
49
76
|
|
50
|
-
# Public:
|
51
|
-
#
|
52
|
-
|
53
|
-
|
77
|
+
# Public: Create a copy of options such that no references are shared
|
78
|
+
# returns A deep clone of the options Hash
|
79
|
+
def self.clone_options(opts)
|
80
|
+
clone = opts.dup
|
81
|
+
if opts.has_key? :attributes
|
82
|
+
clone[:attributes] = opts[:attributes].dup
|
83
|
+
end
|
84
|
+
clone
|
85
|
+
end
|
54
86
|
end
|
55
87
|
end
|