dryml 1.1.0.pre0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1021 @@
1
+ require 'rexml/document'
2
+ require 'digest/sha1'
3
+ require 'pathname'
4
+
5
+ module Dryml
6
+
7
+ class Template
8
+
9
+ DRYML_NAME = "[a-zA-Z\-][a-zA-Z0-9\-]*"
10
+ DRYML_NAME_RX = /^#{DRYML_NAME}$/
11
+
12
+ RUBY_NAME = "[a-zA-Z_][a-zA-Z0-9_]*"
13
+ RUBY_NAME_RX = /^#{RUBY_NAME}$/
14
+
15
+ CODE_ATTRIBUTE_CHAR = "&"
16
+
17
+ NO_METADATA_TAGS = %w(doctype if else unless repeat do with name type-name)
18
+
19
+ SPECIAL_ATTRIBUTES = %w(param merge merge-params merge-attrs
20
+ for-type
21
+ if unless repeat
22
+ part part-locals
23
+ restore)
24
+
25
+ VALID_PARAMETER_TAG_ATTRIBUTES = %w(param replace)
26
+
27
+ @build_cache = {}
28
+
29
+ class << self
30
+ attr_reader :build_cache
31
+
32
+ def clear_build_cache
33
+ @build_cache.clear()
34
+ end
35
+ end
36
+
37
+ def initialize(src, environment, template_path)
38
+ @src = src
39
+ @environment = environment # a class or a module
40
+ @template_path = template_path
41
+ @template_path = @template_path.sub(%r(^#{Regexp.escape(RAILS_ROOT)}/), "") if Object.const_defined? :RAILS_ROOT
42
+
43
+ @builder = Template.build_cache[@template_path] || DRYMLBuilder.new(self)
44
+ @builder.set_environment(environment)
45
+
46
+ @last_element = nil
47
+ end
48
+
49
+ attr_reader :tags, :template_path
50
+
51
+ def compile(local_names=[], auto_taglibs=[])
52
+ now = Time.now
53
+
54
+ unless @template_path.ends_with?(EMPTY_PAGE)
55
+ p = Pathname.new template_path
56
+ p = Pathname.new(RAILS_ROOT) + p unless p.absolute? || !Object.const_defined?(:RAILS_ROOT)
57
+ mtime = p.mtime rescue Time.now
58
+
59
+ if !@builder.ready?(mtime)
60
+ @builder.start
61
+ parsed = true
62
+ # parse the DRYML file creating a list of build instructions
63
+ if is_taglib?
64
+ process_src
65
+ else
66
+ create_render_page_method
67
+ end
68
+
69
+ # store build instructions in the cache
70
+ Template.build_cache[@template_path] = @builder
71
+ end
72
+ end
73
+
74
+ # compile the build instructions
75
+ @builder.build(local_names, auto_taglibs, mtime)
76
+
77
+ logger.try.info(" DRYML: Compiled #{template_path} in #{'%.2fs' % (Time.now - now)}") if parsed
78
+ end
79
+
80
+
81
+ def create_render_page_method
82
+ erb_src = process_src
83
+
84
+ @builder.add_build_instruction(:render_page, :src => erb_src, :line_num => 1)
85
+ end
86
+
87
+
88
+ def is_taglib?
89
+ @environment.class == Module
90
+ end
91
+
92
+
93
+ def process_src
94
+ @doc = Dryml::Parser::Document.new(@src, @template_path)
95
+ result = children_to_erb(@doc.root)
96
+ restore_erb_scriptlets(result)
97
+ end
98
+
99
+
100
+ def restore_erb_scriptlets(src)
101
+ @doc.restore_erb_scriptlets(src)
102
+ end
103
+
104
+
105
+ def children_to_erb(nodes)
106
+ nodes.map { |x| node_to_erb(x) }.join
107
+ end
108
+
109
+
110
+ def node_to_erb(node)
111
+ case node
112
+
113
+ # v important this comes before REXML::Text, as REXML::CData < REXML::Text
114
+ when REXML::CData
115
+ REXML::CData::START + node.to_s + REXML::CData::STOP
116
+
117
+ when REXML::Comment
118
+ REXML::Comment::START + node.to_s + REXML::Comment::STOP
119
+
120
+ when REXML::Text
121
+ strip_suppressed_whiteaspace(node.to_s)
122
+
123
+ when REXML::Element
124
+ element_to_erb(node)
125
+ end
126
+ end
127
+
128
+
129
+ def strip_suppressed_whiteaspace(s)
130
+ s # s.gsub(/ -(\s*\n\s*)/, '<% \1 %>')
131
+ end
132
+
133
+
134
+ def element_to_erb(el)
135
+ dryml_exception("old-style parameter tag (<#{el.name}>)", el) if el.name.starts_with?(":")
136
+
137
+ @last_element = el
138
+ case el.dryml_name
139
+
140
+ when "include"
141
+ include_element(el)
142
+ # return just the newlines to keep line-number matching - the
143
+ # include has no presence in the erb source
144
+ tag_newlines(el)
145
+
146
+ when "set-theme"
147
+ require_attribute(el, "name", /^#{DRYML_NAME}$/)
148
+ @builder.add_build_instruction(:set_theme, :name => el.attributes['name'])
149
+
150
+ # return nothing - set_theme has no presence in the erb source
151
+ tag_newlines(el)
152
+
153
+ when "def"
154
+ def_element(el)
155
+
156
+ when "extend"
157
+ extend_element(el)
158
+
159
+ when "set"
160
+ set_element(el)
161
+
162
+ when "set-scoped"
163
+ set_scoped_element(el)
164
+
165
+ when "param-content"
166
+ param_content_element(el)
167
+
168
+ else
169
+ if el.dryml_name.not_in?(Dryml.static_tags) || el.attributes['param'] || el.attributes['restore']
170
+ tag_call(el)
171
+ else
172
+ static_element_to_erb(el)
173
+ end
174
+ end
175
+ end
176
+
177
+
178
+ def include_element(el)
179
+ require_toplevel(el)
180
+ require_attribute(el, "as", /^#{DRYML_NAME}$/, true)
181
+ options = {}
182
+ %w(src module plugin as).each do |attr|
183
+ options[attr.to_sym] = el.attributes[attr] if el.attributes[attr]
184
+ end
185
+ @builder.add_build_instruction(:include, options)
186
+ end
187
+
188
+
189
+ def import_module(mod, as=nil)
190
+ @builder.import_module(mod, as)
191
+ end
192
+
193
+
194
+ def set_element(el)
195
+ assigns = el.attributes.map do |name, value|
196
+ next if name.in?(SPECIAL_ATTRIBUTES)
197
+ dryml_exception("invalid name in <set> (remember to use '-' rather than '_')", el) unless name =~ /^#{DRYML_NAME}(\.#{DRYML_NAME})*$/
198
+ "#{ruby_name name} = #{attribute_to_ruby(value)}; "
199
+ end.join
200
+ code = apply_control_attributes("begin; #{assigns}; end", el)
201
+ "<% #{code}#{tag_newlines(el)} %>"
202
+ end
203
+
204
+
205
+ def set_scoped_element(el)
206
+ variables = el.attributes.map do |name, value|
207
+ dryml_exception("invalid name in <set-scoped> (remember to use '-' rather than '_')", el) unless name =~ DRYML_NAME_RX
208
+ ":#{ruby_name name} => #{attribute_to_ruby(value)} "
209
+ end
210
+ "<% scope.new_scope(#{variables * ', '}) { #{tag_newlines(el)} %>#{children_to_erb(el)}<% } %>"
211
+ end
212
+
213
+
214
+ def declared_attributes(def_element)
215
+ attrspec = def_element.attributes["attrs"]
216
+ attr_names = attrspec ? attrspec.split(/\s*,\s*/).map{ |n| n.underscore.to_sym } : []
217
+ invalids = attr_names & ([:with, :field, :this] + SPECIAL_ATTRIBUTES.*.to_sym)
218
+ dryml_exception("invalid attrs in def: #{invalids * ', '}", def_element) unless invalids.empty?
219
+ attr_names
220
+ end
221
+
222
+
223
+ def ruby_name(dryml_name)
224
+ dryml_name.gsub('-', '_')
225
+ end
226
+
227
+
228
+ def with_containing_tag_name(el)
229
+ old = @containing_tag_name
230
+ @containing_tag_name = el.dryml_name
231
+ yield
232
+ @containing_tag_name = old
233
+ end
234
+
235
+
236
+ def define_polymorphic_dispatcher(el, name)
237
+ # FIXME: The new erb context ends up being set-up twice
238
+ src = %(
239
+ def #{name}(attributes={}, parameters={})
240
+ _tag_context(attributes) do
241
+ attributes.delete :with
242
+ attributes.delete :field
243
+ call_polymorphic_tag('#{name}', attributes, parameters) { #{name}__base(attributes.except, parameters) }
244
+ end
245
+ end
246
+ )
247
+ @builder.add_build_instruction(:eval, :src => src, :line_num => element_line_num(el))
248
+ end
249
+
250
+
251
+ def extend_element(el)
252
+ def_element(el, true)
253
+ end
254
+
255
+
256
+ def type_specific_suffix
257
+ el = @def_element
258
+ for_type = el.attributes['for']
259
+ if for_type
260
+ type_name = if defined?(HoboFields) && for_type =~ /^[a-z]/
261
+ # It's a symbolic type name - look up the Ruby type name
262
+ klass = HoboFields.to_class(for_type) or
263
+ dryml_exception("No such type in polymorphic tag definition: '#{for_type}'", el)
264
+ klass.name
265
+ else
266
+ for_type
267
+ end.underscore.gsub('/', '__')
268
+ "__for_#{type_name}"
269
+ end
270
+ end
271
+
272
+
273
+ def def_element(el, extend_tag=false)
274
+ require_toplevel(el)
275
+ require_attribute(el, "tag", DRYML_NAME_RX)
276
+ require_attribute(el, "attrs", /^\s*#{DRYML_NAME}(\s*,\s*#{DRYML_NAME})*\s*$/, true)
277
+ require_attribute(el, "alias-of", DRYML_NAME_RX, true)
278
+
279
+ @def_element = el
280
+
281
+ unsafe_name = el.attributes["tag"]
282
+ name = Dryml.unreserve(unsafe_name)
283
+ suffix = type_specific_suffix
284
+ if suffix
285
+ name += suffix
286
+ unsafe_name += suffix
287
+ end
288
+
289
+ if el.attributes['polymorphic']
290
+ %w(for alias-of).each do |attr|
291
+ dryml_exception("def cannot have both 'polymorphic' and '#{attr}' attributes") if el.attributes[attr]
292
+ end
293
+
294
+ define_polymorphic_dispatcher(el, ruby_name(name))
295
+ name += "__base"
296
+ unsafe_name += "__base"
297
+ end
298
+
299
+ alias_of = el.attributes['alias-of']
300
+ dryml_exception("def with alias-of must be empty", el) if alias_of and el.size > 0
301
+
302
+ alias_of and @builder.add_build_instruction(:alias_method,
303
+ :new => ruby_name(name).to_sym,
304
+ :old => ruby_name(Dryml.unreserve(alias_of)).to_sym)
305
+
306
+ res = if alias_of
307
+ "<% #{tag_newlines(el)} %>"
308
+ else
309
+ src = tag_method(name, el, extend_tag) +
310
+ "<% _register_tag_attrs(:#{ruby_name name}, #{declared_attributes(el).inspect.underscore}) %>"
311
+
312
+ logger.debug(restore_erb_scriptlets(src)) if el.attributes["debug-source"]
313
+
314
+ @builder.add_build_instruction(:def,
315
+ :src => restore_erb_scriptlets(src),
316
+ :line_num => element_line_num(el))
317
+ # keep line numbers matching up
318
+ "<% #{"\n" * src.count("\n")} %>"
319
+ end
320
+ @def_element = nil
321
+ res
322
+ end
323
+
324
+ def self.descendents(el,&block)
325
+ return if el.elements.empty?
326
+ el.elements.each do |child|
327
+ block.call(child)
328
+ descendents(child,&block)
329
+ end
330
+ end
331
+
332
+ # Using REXML::XPath is slow
333
+ def self.descendent_select(el)
334
+ result = []
335
+ descendents(el) { |desc|
336
+ result << desc if yield(desc)
337
+ }
338
+ result
339
+ end
340
+
341
+ def param_names_in_definition(el)
342
+ self.class.descendent_select(el) { |el| el.attribute 'param' }.map do |e|
343
+ name = get_param_name(e)
344
+ dryml_exception("invalid param name: #{name.inspect}", e) unless
345
+ is_code_attribute?(name) || name =~ RUBY_NAME_RX || name =~ /#\{/
346
+ name.to_sym unless is_code_attribute?(name)
347
+ end.compact
348
+ end
349
+
350
+
351
+ def tag_method(name, el, extend_tag=false)
352
+ name = ruby_name name
353
+ param_names = param_names_in_definition(el)
354
+
355
+ if extend_tag
356
+ @extend_key = 'a' + Digest::SHA1.hexdigest(el.to_s)[0..10]
357
+ alias_statement = "; alias_method_chain_on_include :#{name}, :#{@extend_key}"
358
+ name = "#{name}_with_#{@extend_key}"
359
+ end
360
+
361
+ src = "<% def #{name}(all_attributes={}, all_parameters={}); " +
362
+ "parameters = Dryml::TagParameters.new(all_parameters, #{param_names.inspect.underscore}); " +
363
+ "all_parameters = Dryml::TagParameters.new(all_parameters); " +
364
+ tag_method_body(el) +
365
+ "; end#{alias_statement} %>"
366
+ @extend_key = nil
367
+ src
368
+ end
369
+
370
+
371
+ def tag_method_body(el)
372
+ attrs = declared_attributes(el)
373
+
374
+ # A statement to assign values to local variables named after the tag's attrs
375
+ # The trailing comma on `attributes` is supposed to be there!
376
+ setup_locals = attrs.map{|a| "#{Dryml.unreserve(a).underscore}, "}.join + "attributes, = " +
377
+ "_tag_locals(all_attributes, #{attrs.inspect})"
378
+
379
+ start = "_tag_context(all_attributes) do #{setup_locals}"
380
+
381
+ "#{start} " +
382
+ # reproduce any line breaks in the start-tag so that line numbers are preserved
383
+ tag_newlines(el) + "%>" +
384
+ wrap_tag_method_body_with_metadata(children_to_erb(el)) +
385
+ "<% output_buffer; end"
386
+ end
387
+
388
+
389
+ def wrap_source_with_metadata(content, kind, name, *args)
390
+ if (!include_source_metadata) || name.in?(NO_METADATA_TAGS)
391
+ content
392
+ else
393
+ metadata = [kind, name] + args + [@template_path]
394
+ "<!--[DRYML|#{metadata * '|'}[-->" + content + "<!--]DRYML]-->"
395
+ end
396
+ end
397
+
398
+
399
+ def wrap_tag_method_body_with_metadata(content)
400
+ name = @def_element.attributes['tag']
401
+ for_ = @def_element.attributes['for']
402
+ name += " for #{for_}" if for_
403
+ wrap_source_with_metadata(content, "def", name, element_line_num(@def_element))
404
+ end
405
+
406
+
407
+ def wrap_tag_call_with_metadata(el, content)
408
+ name = el.expanded_name
409
+ param = el.attributes['param']
410
+
411
+ if param == "&true"
412
+ name += " param"
413
+ elsif param
414
+ name += " param='#{param}'"
415
+ end
416
+
417
+ wrap_source_with_metadata(content, "call", name, element_line_num(el))
418
+ end
419
+
420
+
421
+ def param_content_local_name(name)
422
+ "_#{ruby_name name}__default_content"
423
+ end
424
+
425
+
426
+ def param_content_element(name_or_el)
427
+ name = if name_or_el.is_a?(String)
428
+ name_or_el
429
+ else
430
+ el = name_or_el
431
+ el.attributes['for'] || @containing_tag_name
432
+ end
433
+ local_name = param_content_local_name(name)
434
+ "<%= #{local_name} && #{local_name}.call %>"
435
+ end
436
+
437
+
438
+ def part_element(el, content)
439
+ require_attribute(el, "part", DRYML_NAME_RX)
440
+
441
+ if contains_param?(el)
442
+ delegated_part_element(el, content)
443
+ else
444
+ simple_part_element(el, content)
445
+ end
446
+ end
447
+
448
+
449
+ def simple_part_element(el, content)
450
+ part_name = el.attributes['part']
451
+ dom_id = el.attributes['id'] || part_name
452
+ part_name = ruby_name(part_name)
453
+ part_locals = el.attributes["part-locals"]
454
+
455
+ part_src = "<% def #{part_name}_part(#{part_locals._?.gsub('@', '')}) #{tag_newlines(el)}; new_context do %>" +
456
+ content +
457
+ "<% end; end %>"
458
+ @builder.add_part(part_name, restore_erb_scriptlets(part_src), element_line_num(el))
459
+
460
+ newlines = "\n" * part_src.count("\n")
461
+ args = [attribute_to_ruby(dom_id), ":#{part_name}", part_locals].compact
462
+ "<%= call_part(#{args * ', '}) #{newlines} %>"
463
+ end
464
+
465
+
466
+ def delegated_part_element(el, content)
467
+ # TODO
468
+ end
469
+
470
+
471
+ def contains_param?(el)
472
+ # TODO
473
+ false
474
+ end
475
+
476
+
477
+ def part_delegate_tag_name(el)
478
+ "#{@def_name}_#{el.attributes['part']}__part_delegate"
479
+ end
480
+
481
+
482
+ def current_def_name
483
+ @def_element && @def_element.attributes['tag']
484
+ end
485
+
486
+
487
+ def get_param_name(el)
488
+ param_name = el.attributes["param"]
489
+
490
+ if param_name
491
+ def_tag = find_ancestor(el) {|e| e.name == "def" || e.name == "extend" }
492
+ dryml_exception("param is not allowed outside of tag definitions", el) if def_tag.nil?
493
+
494
+ ruby_name(param_name == "&true" ? el.dryml_name : param_name)
495
+ else
496
+ nil
497
+ end
498
+ end
499
+
500
+
501
+ def inside_def_for_type?
502
+ @def_element && @def_element.attributes['for']
503
+ end
504
+
505
+
506
+ def call_name(el)
507
+ dryml_exception("invalid tag name (remember to use '-' rather than '_')", el) unless el.dryml_name =~ /^#{DRYML_NAME}(\.#{DRYML_NAME})*$/
508
+
509
+ name = Dryml.unreserve(ruby_name(el.dryml_name))
510
+ if call_to_self_from_type_specific_def?(el)
511
+ "#{name}__base"
512
+ elsif old_tag_call?(el)
513
+ name = name[4..-1] # remove 'old-' prefix
514
+ name += type_specific_suffix if inside_def_for_type?
515
+ "#{name}_without_#{@extend_key}"
516
+ else
517
+ name
518
+ end
519
+ end
520
+
521
+
522
+ def old_tag_call?(el)
523
+ @def_element && el.dryml_name == "old-#{current_def_name}"
524
+ end
525
+
526
+
527
+ def call_to_self_from_type_specific_def?(el)
528
+ inside_def_for_type? && el.dryml_name == current_def_name &&!el.attributes['for-type']
529
+ end
530
+
531
+
532
+ def polymorphic_call_type(el)
533
+ t = el.attributes['for-type']
534
+ if t.nil?
535
+ nil
536
+ elsif t == "&true"
537
+ 'this_type'
538
+ elsif t =~ /^[A-Z]/
539
+ t
540
+ elsif t =~ /^[a-z]/ && defined? HoboFields.to_class
541
+ klass = HoboFields.to_class(t)
542
+ klass.name
543
+ elsif is_code_attribute?(t)
544
+ t[1..-1]
545
+ else
546
+ dryml_exception("invalid for-type attribute", el)
547
+ end
548
+ end
549
+
550
+
551
+ def tag_call(el)
552
+ name = call_name(el)
553
+ param_name = get_param_name(el)
554
+ attributes = tag_attributes(el)
555
+ newlines = tag_newlines(el)
556
+
557
+ parameters = tag_newlines(el) + parameter_tags_hash(el)
558
+
559
+ is_param_restore = el.attributes['restore']
560
+
561
+ call = if param_name
562
+ param_name = attribute_to_ruby(param_name, :symbolize => true)
563
+ args = "#{attributes}, #{parameters}, all_parameters, #{param_name}"
564
+ to_call = if is_param_restore
565
+ # The tag is available in a local variable
566
+ # holding a proc
567
+ param_restore_local_name(name)
568
+ elsif (call_type = polymorphic_call_type(el))
569
+ "find_polymorphic_tag(:#{ruby_name name}, #{call_type})"
570
+ else
571
+ ":#{ruby_name name}"
572
+ end
573
+ "call_tag_parameter(#{to_call}, #{args})"
574
+ else
575
+ if is_param_restore
576
+ # The tag is a proc available in a local variable
577
+ "#{param_restore_local_name(name)}.call(#{attributes}, #{parameters})"
578
+ elsif (call_type = polymorphic_call_type(el))
579
+ "send(find_polymorphic_tag(:#{ruby_name name}, #{call_type}), #{attributes}, #{parameters})"
580
+ elsif attributes == "{}" && parameters == "{}"
581
+ if name =~ /^[A-Z]/
582
+ # it's a tag with a cap name - not a local
583
+ "#{ruby_name name}()"
584
+ else
585
+ # could be a tag or a local variable
586
+ "#{ruby_name name}.to_s"
587
+ end
588
+ else
589
+ "#{ruby_name name}(#{attributes}, #{parameters})"
590
+ end
591
+ end
592
+
593
+ call = apply_control_attributes(call, el)
594
+ call = maybe_make_part_call(el, "<% concat(#{call}) %>")
595
+ wrap_tag_call_with_metadata(el, call)
596
+ end
597
+
598
+
599
+ def merge_attribute(el)
600
+ merge = el.attributes['merge']
601
+ dryml_exception("merge cannot have a RHS", el) if merge && merge != "&true"
602
+ merge
603
+ end
604
+
605
+
606
+ def parameter_tags_hash(el, containing_tag_name=nil)
607
+ call_type = nil
608
+
609
+ metadata_name = containing_tag_name || el.expanded_name
610
+
611
+ param_items = el.map do |node|
612
+ case node
613
+ when REXML::Text
614
+ text = node.to_s
615
+ unless text.blank?
616
+ dryml_exception("mixed content and parameter tags", el) if call_type == :named_params
617
+ call_type = :default_param_only
618
+ end
619
+ text
620
+ when REXML::Element
621
+ e = node
622
+ is_parameter_tag = e.parameter_tag?
623
+
624
+ # Make sure there isn't a mix of parameter tags and normal content
625
+ case call_type
626
+ when nil
627
+ call_type = is_parameter_tag ? :named_params : :default_param_only
628
+ when :named_params
629
+ dryml_exception("mixed parameter tags and non-parameter tags (did you forget a ':'?)", el) unless is_parameter_tag
630
+ when :default_param_only
631
+ dryml_exception("mixed parameter tags and non-parameter tags (did you forget a ':'?)", el) if is_parameter_tag
632
+ end
633
+
634
+ if is_parameter_tag
635
+ parameter_tag_hash_item(e, metadata_name) + ", "
636
+ end
637
+ end
638
+ end.join
639
+
640
+ if call_type == :default_param_only || (call_type.nil? && param_items.length > 0) || (el.children.empty? && el.has_end_tag?)
641
+ with_containing_tag_name(el) do
642
+ param_items = " :default => #{default_param_proc(el, containing_tag_name)}, "
643
+ end
644
+ end
645
+
646
+ param_items.concat without_parameters(el)
647
+
648
+ merge_params = el.attributes['merge-params'] || merge_attribute(el)
649
+ if merge_params
650
+ extra_params = if merge_params == "&true"
651
+ "parameters"
652
+ elsif is_code_attribute?(merge_params)
653
+ merge_params[1..-1]
654
+ else
655
+ merge_param_names = merge_params.split(/\s*,\s*/).*.gsub("-", "_")
656
+ "all_parameters & #{merge_param_names.inspect}"
657
+ end
658
+ "merge_parameter_hashes({#{param_items}}, (#{extra_params}) || {})"
659
+ else
660
+ "{#{param_items}}"
661
+ end
662
+ end
663
+
664
+
665
+ def without_parameters(el)
666
+ without_names = el.attributes.keys.map { |name| name =~ /^without-(.*)/ and $1 }.compact
667
+ without_names.map { |name| ":#{ruby_name name}_replacement => proc {|__discard__| '' }, " }.join
668
+ end
669
+
670
+
671
+ def parameter_tag_hash_item(el, metadata_name)
672
+ name = el.name.dup
673
+ if name.sub!(/^before-/, "")
674
+ before_parameter_tag_hash_item(name, el, metadata_name)
675
+ elsif name.sub!(/^after-/, "")
676
+ after_parameter_tag_hash_item(name, el, metadata_name)
677
+ elsif name.sub!(/^prepend-/, "")
678
+ prepend_parameter_tag_hash_item(name, el, metadata_name)
679
+ elsif name.sub!(/^append-/, "")
680
+ append_parameter_tag_hash_item(name, el, metadata_name)
681
+ else
682
+ hash_key = ruby_name name
683
+ hash_key += "_replacement" if el.attribute("replace")
684
+ if (param_name = get_param_name(el))
685
+ ":#{hash_key} => merge_tag_parameter(#{param_proc(el, metadata_name)}, all_parameters[:#{param_name}])"
686
+ else
687
+ ":#{hash_key} => #{param_proc(el, metadata_name)}"
688
+ end
689
+ end
690
+ end
691
+
692
+
693
+ def before_parameter_tag_hash_item(name, el, metadata_name)
694
+ param_name = get_param_name(el)
695
+ dryml_exception("param declaration not allowed on 'before' parameters", el) if param_name
696
+ content = children_to_erb(el) + "<% concat(#{param_restore_local_name(name)}.call({}, {})) %>"
697
+ ":#{ruby_name name}_replacement => #{replace_parameter_proc(el, metadata_name, content)}"
698
+ end
699
+
700
+
701
+ def after_parameter_tag_hash_item(name, el, metadata_name)
702
+ param_name = get_param_name(el)
703
+ dryml_exception("param declaration not allowed on 'after' parameters", el) if param_name
704
+ content = "<% concat(#{param_restore_local_name(name)}.call({}, {})) %>" + children_to_erb(el)
705
+ ":#{ruby_name name}_replacement => #{replace_parameter_proc(el, metadata_name, content)}"
706
+ end
707
+
708
+
709
+ def append_parameter_tag_hash_item(name, el, metadata_name)
710
+ ":#{ruby_name name} => proc { [{}, { :default => proc { |#{param_content_local_name(name)}| new_context { %>" +
711
+ param_content_element(name) + children_to_erb(el) +
712
+ "<% } } } ] }"
713
+ end
714
+
715
+
716
+ def prepend_parameter_tag_hash_item(name, el, metadata_name)
717
+ ":#{ruby_name name} => proc { [{}, { :default => proc { |#{param_content_local_name(name)}| new_context { %>" +
718
+ children_to_erb(el) + param_content_element(name) +
719
+ "<% } } } ] }"
720
+ end
721
+
722
+
723
+ def default_param_proc(el, containing_param_name=nil)
724
+ content = children_to_erb(el)
725
+ content = wrap_source_with_metadata(content, "param", containing_param_name,
726
+ element_line_num(el)) if containing_param_name
727
+ "proc { |#{param_content_local_name(el.dryml_name)}| new_context { %>#{content}<% } #{tag_newlines(el)}}"
728
+ end
729
+
730
+
731
+ def param_restore_local_name(name)
732
+ "_#{ruby_name name}_restore"
733
+ end
734
+
735
+
736
+ def wrap_replace_parameter(el, name)
737
+ wrap_source_with_metadata(children_to_erb(el), "replace", name, element_line_num(el))
738
+ end
739
+
740
+
741
+ def param_proc(el, metadata_name_prefix)
742
+ metadata_name = "#{metadata_name_prefix}><#{el.name}"
743
+
744
+ nl = tag_newlines(el)
745
+
746
+ if (repl = el.attribute("replace"))
747
+ dryml_exception("replace attribute must not have a value", el) if repl.has_rhs?
748
+ dryml_exception("replace parameters must not have attributes", el) if el.attributes.length > 1
749
+
750
+ replace_parameter_proc(el, metadata_name)
751
+ else
752
+ attributes = el.attributes.dup
753
+ # Providing one of 'with' or 'field' but not the other should cancel out the other
754
+ attributes[:with] = "&nil" if attributes.key?(:field) && !attributes.key?(:with)
755
+ attributes[:field] = "&nil" if !attributes.key?(:field) && attributes.key?(:with)
756
+ attribute_items = attributes.map do |name, value|
757
+ if name.in?(VALID_PARAMETER_TAG_ATTRIBUTES)
758
+ # just ignore
759
+ elsif name.in?(SPECIAL_ATTRIBUTES)
760
+ dryml_exception("attribute '#{name}' is not allowed on parameter tags (<#{el.name}:>)", el)
761
+ else
762
+ ":#{ruby_name name} => #{attribute_to_ruby(value, el)}"
763
+ end
764
+ end.compact
765
+
766
+ nested_parameters_hash = parameter_tags_hash(el, metadata_name)
767
+ "proc { [{#{attribute_items * ', '}}, #{nested_parameters_hash}] #{nl}}"
768
+ end
769
+ end
770
+
771
+
772
+ def replace_parameter_proc(el, metadata_name, content=nil)
773
+ content ||= wrap_replace_parameter(el, metadata_name)
774
+ param_name = el.dryml_name.sub(/^(before|after|append|prepend)-/, "")
775
+ "proc { |#{param_restore_local_name(param_name)}| new_context { %>#{content}<% } #{tag_newlines(el)}}"
776
+ end
777
+
778
+
779
+ def maybe_make_part_call(el, call)
780
+ part_name = el.attributes['part']
781
+ if part_name
782
+ part_id = el.attributes['id'] || part_name
783
+ "<div class='part-wrapper' id='<%= #{attribute_to_ruby part_id} %>'>#{part_element(el, call)}</div>"
784
+ else
785
+ call
786
+ end
787
+ end
788
+
789
+
790
+ def field_shorthand_element?(el)
791
+ el.expanded_name =~ /:./
792
+ end
793
+
794
+
795
+ def tag_attributes(el)
796
+ attributes = el.attributes
797
+ items = attributes.map do |n,v|
798
+ dryml_exception("invalid attribute name '#{n}' (remember to use '-' rather than '_')", el) unless n =~ DRYML_NAME_RX
799
+
800
+ next if n.in?(SPECIAL_ATTRIBUTES) || n =~ /^without-/
801
+ next if el.attributes['part'] && n == 'id' # The id is rendered on the <div class="part-wrapper"> instead
802
+
803
+ ":#{ruby_name n} => #{attribute_to_ruby(v)}"
804
+ end.compact
805
+
806
+ # if there's a ':' el.name is just the part after the ':'
807
+ items << ":field => \"#{ruby_name el.name}\"" if field_shorthand_element?(el)
808
+
809
+ hash = "{#{items.join(", ")}}"
810
+
811
+ if merge_attribute(el)
812
+ "merge_attrs(#{hash}, attributes)"
813
+ elsif el.attributes['merge-attrs']
814
+ merge_attrs = compile_merge_attrs(el)
815
+ "merge_attrs(#{hash}, #{merge_attrs} || {})"
816
+ else
817
+ hash
818
+ end
819
+ end
820
+
821
+
822
+ def compile_merge_attrs(el)
823
+ merge_attrs = el.attributes['merge-attrs']
824
+ if merge_attrs == "&true"
825
+ "attributes"
826
+ elsif is_code_attribute?(merge_attrs)
827
+ "(#{merge_attrs[1..-1]})"
828
+ else
829
+ merge_attr_names = merge_attrs.split(/\s*,\s*/).*.gsub("-", "_").*.to_sym
830
+ "(all_attributes & #{merge_attr_names.inspect})"
831
+ end
832
+ end
833
+
834
+
835
+ def static_tag_to_method_call(el)
836
+ part = el.attributes["part"]
837
+ attrs = el.attributes.map do |n, v|
838
+ next if n.in? SPECIAL_ATTRIBUTES
839
+ val = restore_erb_scriptlets(v).gsub('"', '\"').gsub(/<%=(.*?)%>/, '#{\1}')
840
+ %('#{n}' => "#{val}")
841
+ end.compact
842
+
843
+ # If there's a part but no id, the id defaults to the part name
844
+ if part && !el.attributes["id"]
845
+ attrs << ":id => '#{part}'"
846
+ end
847
+
848
+ # Convert the attributes hash to a call to merge_attrs if
849
+ # there's a merge-attrs attribute
850
+ attrs = if el.attributes['merge-attrs']
851
+ merge_attrs = compile_merge_attrs(el)
852
+ "merge_attrs({#{attrs * ', '}}, #{merge_attrs} || {})"
853
+ else
854
+ "{" + attrs.join(', ') + "}"
855
+ end
856
+
857
+ if el.children.empty?
858
+ dryml_exception("part attribute on empty static tag", el) if part
859
+
860
+ "<%= " + apply_control_attributes("element(:#{el.name}, #{attrs}, nil, true, #{!el.has_end_tag?} #{tag_newlines(el)})", el) + " %>"
861
+ else
862
+ if part
863
+ body = part_element(el, children_to_erb(el))
864
+ else
865
+ body = children_to_erb(el)
866
+ end
867
+
868
+ output_tag = "element(:#{el.name}, #{attrs}, new_context { %>#{body}<% })"
869
+ "<% concat(" + apply_control_attributes(output_tag, el) + ") %>"
870
+ end
871
+ end
872
+
873
+
874
+ def static_element_to_erb(el)
875
+ if promote_static_tag_to_method_call?(el)
876
+ static_tag_to_method_call(el)
877
+ else
878
+ start_tag_src = el.start_tag_source.gsub(REXML::CData::START, "").gsub(REXML::CData::STOP, "")
879
+
880
+ # Allow #{...} as an alternate to <%= ... %>
881
+ start_tag_src.gsub!(/=\s*('.*?'|".*?")/) do |s|
882
+ s.gsub(/#\{(.*?)\}/, '<%= \1 %>')
883
+ end
884
+
885
+ if el.has_end_tag?
886
+ start_tag_src + children_to_erb(el) + "</#{el.name}>"
887
+ else
888
+ start_tag_src
889
+ end
890
+ end
891
+ end
892
+
893
+
894
+ def promote_static_tag_to_method_call?(el)
895
+ %w(part merge-attrs if unless repeat).any? {|x| el.attributes[x]}
896
+ end
897
+
898
+
899
+ def apply_control_attributes(expression, el)
900
+ controls = %w(if unless repeat).map_hash { |x| el.attributes[x] }.compact
901
+
902
+ dryml_exception("You can't have multiple control attributes on the same element", el) if
903
+ controls.length > 1
904
+
905
+ attr = controls.keys.first
906
+ val = controls.values.first
907
+ if val.nil?
908
+ expression
909
+ else
910
+ control = if !el.attribute(attr).has_rhs?
911
+ "this"
912
+ elsif is_code_attribute?(val)
913
+ "#{val[1..-1]}"
914
+ else
915
+ val.gsub!('-', '_')
916
+ attr == "repeat" ? %("#{val}") : "this.#{val}"
917
+ end
918
+
919
+ x = gensym
920
+ case attr
921
+ when "if"
922
+ "(if !(#{control}).blank?; (#{x} = #{expression}; Dryml.last_if = true; #{x}) " +
923
+ "else (Dryml.last_if = false; ''); end)"
924
+ when "unless"
925
+ "(if (#{control}).blank?; (#{x} = #{expression}; Dryml.last_if = true; #{x}) " +
926
+ "else (Dryml.last_if = false; ''); end)"
927
+ when "repeat"
928
+ "repeat_attribute(#{control}) { #{expression} }"
929
+ end
930
+ end
931
+ end
932
+
933
+
934
+ def attribute_to_ruby(*args)
935
+ options = args.extract_options!
936
+ attr, el = args
937
+
938
+ dryml_exception('erb scriptlet not allowed in this attribute (use #{ ... } instead)', el) if
939
+ attr.is_a?(String) && attr.index("[![HOBO-ERB")
940
+
941
+ if options[:symbolize] && attr =~ /^[a-zA-Z_][^a-zA-Z0-9_]*[\?!]?/
942
+ ":#{attr}"
943
+ else
944
+ res = if attr.nil?
945
+ "nil"
946
+ elsif is_code_attribute?(attr)
947
+ "(#{attr[1..-1]})"
948
+ else
949
+ if attr !~ /"/
950
+ '"' + attr + '"'
951
+ elsif attr !~ /'/
952
+ "'#{attr}'"
953
+ else
954
+ dryml_exception("invalid quote(s) in attribute value")
955
+ end
956
+ end
957
+ options[:symbolize] ? (res + ".to_sym") : res
958
+ end
959
+ end
960
+
961
+ def find_ancestor(el)
962
+ e = el.parent
963
+ until e.is_a? REXML::Document
964
+ return e if yield(e)
965
+ e = e.parent
966
+ end
967
+ return nil
968
+ end
969
+
970
+ def require_toplevel(el, message=nil)
971
+ message ||= "can only be at the top level"
972
+ dryml_exception("<#{el.dryml_name}> #{message}", el) if el.parent != @doc.root
973
+ end
974
+
975
+ def require_attribute(el, name, rx=nil, optional=false)
976
+ val = el.attributes[name]
977
+ if val
978
+ dryml_exception("invalid #{name}=\"#{val}\" attribute on <#{el.dryml_name}>", el) unless rx && val =~ rx
979
+ else
980
+ dryml_exception("missing #{name} attribute on <#{el.dryml_name}>", el) unless optional
981
+ end
982
+ end
983
+
984
+ def dryml_exception(message, el=nil)
985
+ el ||= @last_element
986
+ raise DrymlException.new(message, template_path, element_line_num(el))
987
+ end
988
+
989
+ def element_line_num(el)
990
+ @doc.element_line_num(el)
991
+ end
992
+
993
+ def tag_newlines(el)
994
+ src = el.start_tag_source
995
+ "\n" * src.count("\n")
996
+ end
997
+
998
+ def is_code_attribute?(attr_value)
999
+ attr_value =~ /^\&/ && attr_value !~ /^\&\S+;/
1000
+ end
1001
+
1002
+ def logger
1003
+ ActionController::Base.logger rescue nil
1004
+ end
1005
+
1006
+ def gensym(name="__tmp")
1007
+ @gensym_counter ||= 0
1008
+ @gensym_counter += 1
1009
+ "#{name}_#{@gensym_counter}"
1010
+ end
1011
+
1012
+ def include_source_metadata
1013
+ # disabled for now -- we're still getting broken rendering with this feature on
1014
+ return false
1015
+ @include_source_metadata = RAILS_ENV == "development" && !ENV['DRYML_EDITOR'].blank? if @include_source_metadata.nil?
1016
+ @include_source_metadata
1017
+ end
1018
+
1019
+ end
1020
+
1021
+ end