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,613 @@
1
+ module Dryml
2
+
3
+ class TemplateEnvironment
4
+
5
+ class << self
6
+ def inherited(subclass)
7
+ subclass.compiled_local_names = []
8
+ end
9
+ attr_accessor :load_time, :compiled_local_names
10
+
11
+
12
+ def _register_tag_attrs(tag_name, attrs)
13
+ @tag_attrs ||= {}
14
+ @tag_attrs[tag_name] = attrs
15
+ end
16
+
17
+
18
+ def tag_attrs
19
+ @tag_attrs ||= {}
20
+ end
21
+
22
+ alias_method :delayed_alias_method_chain, :alias_method_chain
23
+
24
+ end
25
+
26
+ include ActionView::Helpers
27
+ include Helper ## FIXME remove
28
+
29
+ def initialize(view_name=nil, view=nil)
30
+ unless view_name.nil? && view.nil?
31
+ @view = view
32
+ @_view_name = view_name
33
+ @_erb_binding = binding
34
+ @_part_contexts = {}
35
+ @_scoped_variables = ScopedVariables.new
36
+ @_polymorphic_tag_cache = {}
37
+
38
+ # Make sure the "assigns" from the controller are available (instance variables)
39
+ if view
40
+ view.assigns.each do |key, value|
41
+ instance_variable_set("@#{key}", value)
42
+ end
43
+
44
+ # copy view instance variables over
45
+ view.instance_variables.each do |iv|
46
+ instance_variable_set(iv, view.instance_variable_get(iv))
47
+ end
48
+ end
49
+ end
50
+ end
51
+
52
+ for attr in [:erb_binding, :part_contexts, :view_name,
53
+ :this, :this_parent, :this_field, :this_key,
54
+ :form_this, :form_field_path, :form_field_names]
55
+ class_eval "def #{attr}; @_#{attr}; end"
56
+ end
57
+
58
+ def path_for_form_field
59
+ @_form_field_path.nil? and raise Dryml::DrymlException,
60
+ "DRYML cannot provide the correct form-field name here (this_field = #{this_field.inspect}, this = #{this.inspect})"
61
+ @_form_field_path
62
+ end
63
+
64
+
65
+ def this_key=(key)
66
+ @_this_key = key
67
+ end
68
+
69
+
70
+ # The type of this, or when this is nil, the type that would be expected in the current field
71
+ def this_type
72
+ @_this_type ||= if this == false || this == true
73
+ Hobo::Boolean
74
+ elsif this
75
+ this.class
76
+ elsif this_parent && this_field && (parent_class = this_parent.class).respond_to?(:attr_type)
77
+ type = parent_class.attr_type(this_field)
78
+ if type.is_a?(ActiveRecord::Reflection::AssociationReflection)
79
+ reflection = type
80
+ if reflection.macro == :has_many
81
+ Array
82
+ elsif reflection.options[:polymorphic]
83
+ # All we know is that it will be some active-record type
84
+ ActiveRecord::Base
85
+ else
86
+ reflection.klass
87
+ end
88
+ else
89
+ type
90
+ end
91
+ else
92
+ # Nothing to go on at all
93
+ Object
94
+ end
95
+ end
96
+
97
+
98
+ def this_field_reflection
99
+ this.try.proxy_reflection ||
100
+ (this_parent && this_field && this_parent.class.respond_to?(:reflections) && this_parent.class.reflections[this_field.to_sym])
101
+ end
102
+
103
+
104
+ def attrs_for(name)
105
+ self.class.tag_attrs[name.to_sym]
106
+ end
107
+
108
+
109
+ def add_classes!(attributes, *classes)
110
+ classes = classes.flatten.select{|x|x}
111
+ current = attributes[:class]
112
+ attributes[:class] = (current ? current.split + classes : classes).uniq.join(' ')
113
+ attributes
114
+ end
115
+
116
+
117
+ def add_classes(attributes, *classes)
118
+ add_classes!(HashWithIndifferentAccess.new(attributes), classes)
119
+ end
120
+
121
+
122
+ def merge_attrs(attrs, overriding_attrs)
123
+ return {}.update(attrs) if overriding_attrs.nil?
124
+ attrs = attrs.with_indifferent_access unless attrs.is_a?(HashWithIndifferentAccess)
125
+ classes = overriding_attrs[:class]
126
+ attrs = add_classes(attrs, *classes.split) if classes
127
+ attrs.update(overriding_attrs - [:class])
128
+ end
129
+
130
+
131
+ def scope
132
+ @_scoped_variables
133
+ end
134
+
135
+
136
+ def typed_id(object=nil, attribute=nil)
137
+ if object.nil?
138
+ # nothing passed -- use context
139
+ if this_parent && this_field && !this_parent.respond_to?(:member_class)
140
+ object, attribute = this_parent, this_field
141
+ else
142
+ object = this
143
+ end
144
+ end
145
+
146
+ id = if (typed_id = object.try.typed_id)
147
+ typed_id
148
+ elsif object == @this
149
+ "this"
150
+ end
151
+ attribute ? "#{id}:#{attribute}" : id
152
+ end
153
+
154
+
155
+ def call_part(part_node_id, part_name, *locals)
156
+ res = ''
157
+ new_context do
158
+ @_part_contexts[part_node_id] = PartContext.for_call(part_name, self, locals)
159
+ res = send("#{part_name}_part", *locals)
160
+ end
161
+ res
162
+ end
163
+
164
+
165
+ def refresh_part(encoded_context, session, dom_id)
166
+ context = Dryml::PartContext.for_refresh(encoded_context, @this, session)
167
+
168
+ with_part_context(context) do
169
+ send("#{context.part_name}_part", *context.locals)
170
+ end
171
+ end
172
+
173
+
174
+ def with_part_context(context, &block)
175
+ this, this_field = context.this, context.this_field
176
+
177
+ b = if context.form_field_path
178
+ proc { with_form_context(:unknown, context.form_field_path, &block) }
179
+ else
180
+ block
181
+ end
182
+
183
+ if this && this_field
184
+ new_object_context(this) { new_field_context(this_field, &b) }
185
+ elsif this
186
+ new_object_context(this, &b)
187
+ else
188
+ new_context(&b)
189
+ end
190
+ end
191
+
192
+
193
+ def call_polymorphic_tag(name, *args)
194
+ name = name.to_s.gsub('-', '_')
195
+ type = args.first.is_a?(Class) ? args.shift : nil
196
+ attributes, parameters = args
197
+
198
+ tag = find_polymorphic_tag(name, type)
199
+ if tag != name
200
+ send(tag, attributes || {}, parameters || {})
201
+ else
202
+ block_given? ? yield : nil
203
+ end
204
+ end
205
+
206
+
207
+ def find_polymorphic_tag(name, call_type=nil)
208
+ call_type ||= (this.respond_to?(:member_class) && this.member_class) || this_type
209
+
210
+ begin
211
+ found = nil
212
+ while true
213
+ if respond_to?(poly_name = "#{name}__for_#{call_type.name.to_s.underscore.gsub('/', '__')}")
214
+ found = poly_name
215
+ break
216
+ else
217
+ if call_type == ActiveRecord::Base || call_type == Object
218
+ found = name
219
+ break
220
+ else
221
+ call_type = call_type.superclass
222
+ end
223
+ end
224
+ end
225
+ found
226
+ end
227
+ end
228
+
229
+
230
+ def repeat_attribute(string_or_array)
231
+ res = nil
232
+ if string_or_array.instance_of?(String)
233
+ new_field_context(string_or_array) do
234
+ res = context_map { yield }
235
+ end
236
+ else
237
+ res = context_map(string_or_array) { yield }
238
+ end
239
+ res.join
240
+ end
241
+
242
+
243
+ def new_context
244
+ ctx = [ @_this, @_this_parent, @_this_field, @_this_type,
245
+ @_form_field_path, @_form_field_paths_by_object ]
246
+ @_this_type = nil
247
+ res = nil
248
+ @view.with_output_buffer { res = yield }
249
+ @_this, @_this_parent, @_this_field, @_this_type, @_form_field_path, @_form_field_paths_by_object = ctx
250
+ res.to_s
251
+ end
252
+
253
+
254
+ def new_object_context(new_this)
255
+ new_context do
256
+ if new_this.respond_to?(:origin)
257
+ @_this_parent, @_this_field = new_this.origin, new_this.origin_attribute
258
+ else
259
+ @_this_parent, @_this_field = [nil, nil]
260
+ end
261
+ @_this = new_this
262
+
263
+ # We might have lost track of where 'this' is relative to the form_this
264
+ # check if this or this_parent are objects we've seen before in this form
265
+ @_form_field_path = find_form_field_path(new_this) if @_form_field_path
266
+
267
+ yield
268
+ end
269
+ end
270
+
271
+
272
+ def new_field_context(field_path, new_this=nil)
273
+ new_context do
274
+ path = if field_path.is_a? String
275
+ field_path.split('.')
276
+ else
277
+ Array(field_path)
278
+ end
279
+ if new_this
280
+ raise ArgumentError, "invlaid context change" unless path.length == 1
281
+ @_this_parent, @_this_field, @_this = this, path.first, new_this
282
+ else
283
+ parent, field, obj = Dryml.get_field_path(this, path)
284
+ @_this, @_this_parent, @_this_field = obj, parent, field
285
+ end
286
+
287
+ if @_form_field_path
288
+ @_form_field_path += path
289
+ @_form_field_paths_by_object[@_this] = @_form_field_path
290
+ end
291
+
292
+ yield
293
+ end
294
+ end
295
+
296
+
297
+ def find_form_field_path(object)
298
+ back = []
299
+ while object
300
+ path = @_form_field_paths_by_object[object]
301
+ if path
302
+ path = path + back unless back.empty?
303
+ return path
304
+ end
305
+ if object.respond_to? :origin
306
+ back.unshift object.origin_attribute
307
+ object = object.origin
308
+ else
309
+ return nil
310
+ end
311
+ end
312
+ end
313
+
314
+
315
+
316
+
317
+ def _tag_context(attributes)
318
+ with = attributes[:with]
319
+ field = attributes[:field]
320
+
321
+ if with && field
322
+ new_object_context(with) { new_field_context(field) { yield } }
323
+ elsif field
324
+ new_field_context(field) { yield }
325
+ elsif attributes.has_key?(:with)
326
+ new_object_context(with) { yield }
327
+ else
328
+ new_context { yield }
329
+ end
330
+ end
331
+
332
+
333
+ def with_form_context(form_this=this, form_field_path=[form_this.class.name.underscore])
334
+ @_form_this = form_this
335
+ @_form_field_path = form_field_path
336
+ @_form_field_paths_by_object = { form_this => form_field_path }
337
+ res = scope.new_scope :in_form => true, :form_field_names => [] do
338
+ yield
339
+ end
340
+ @_form_this = @_form_field_path = @_form_field_paths_by_object = nil
341
+ res
342
+ end
343
+
344
+
345
+ def register_form_field(name)
346
+ scope.form_field_names << name
347
+ end
348
+
349
+
350
+ def _tag_locals(attributes, locals)
351
+ attributes.symbolize_keys!
352
+ #ensure with and field are not in attributes
353
+ attributes.delete(:with)
354
+ attributes.delete(:field)
355
+
356
+ # declared attributes don't appear in the attributes hash
357
+ stripped_attributes = HashWithIndifferentAccess.new.update(attributes)
358
+ locals.each {|a| stripped_attributes.delete(a.to_sym) }
359
+
360
+ # Return locals declared as local variables (attrs="...")
361
+ locals.map {|a| attributes[a.to_sym]} + [stripped_attributes]
362
+ end
363
+
364
+
365
+ def call_tag_parameter_with_default_content(the_tag, attributes, default_content, overriding_content_proc)
366
+ if the_tag.is_one_of?(String, Symbol) && the_tag.to_s.in?(Dryml.static_tags)
367
+ body = if overriding_content_proc
368
+ new_context { overriding_content_proc.call(proc { default_content._?.call(nil) }) }
369
+ elsif default_content
370
+ new_context { default_content.call(nil) }
371
+ else
372
+ nil
373
+ end
374
+ element(the_tag, attributes, body)
375
+ else
376
+ d = if overriding_content_proc
377
+ proc { |default| overriding_content_proc.call(proc { default_content._?.call(default) }) }
378
+ else
379
+ proc { |default| default_content._?.call(default) }
380
+ end
381
+ send(the_tag, attributes, { :default => d })
382
+ end
383
+ end
384
+
385
+
386
+ def call_tag_parameter(the_tag, attributes, parameters, caller_parameters, param_name)
387
+ overriding_proc = caller_parameters[param_name]
388
+ replacing_proc = caller_parameters[:"#{param_name}_replacement"]
389
+
390
+ unless param_name == the_tag || param_name == :default
391
+ classes = attributes[:class]
392
+ param_class = param_name.to_s.gsub('_', '-')
393
+ attributes[:class] = if classes
394
+ classes =~ /\b#{param_class}\b/ ? classes : "#{classes} #{param_class}"
395
+ else
396
+ param_class
397
+ end
398
+ end
399
+
400
+ if param_name == :default && overriding_proc && overriding_proc.arity>0
401
+ # :default content is handled specially
402
+
403
+ call_tag_parameter_with_default_content(the_tag, attributes, parameters[:default], overriding_proc)
404
+
405
+ elsif replacing_proc
406
+ # The caller is replacing this parameter. Don't call the tag
407
+ # at all, just the overriding proc, but pass the restorable
408
+ # tag as a parameter to the overriding proc
409
+
410
+ tag_restore = proc do |restore_attrs, restore_params|
411
+ # Call the replaced tag with the attributes and parameters
412
+ # as given in the original tag definition, and with the
413
+ # specialisation given on the 'restore' call
414
+
415
+ if overriding_proc
416
+ overriding_attributes, overriding_parameters = overriding_proc.call
417
+ restore_attrs = overriding_attributes.merge(restore_attrs)
418
+ restore_params = overriding_parameters.merge(restore_params)
419
+ end
420
+
421
+ override_and_call_tag(the_tag, attributes, parameters, restore_attrs, restore_params)
422
+ end
423
+ replacing_proc.call(tag_restore)
424
+
425
+ else
426
+ overriding_attributes, overriding_parameters = overriding_proc._?.call
427
+ override_and_call_tag(the_tag, attributes, parameters, overriding_attributes, overriding_parameters)
428
+ end
429
+ end
430
+
431
+
432
+ def override_and_call_tag(the_tag, general_attributes, general_parameters, overriding_attributes, overriding_parameters)
433
+ attributes = overriding_attributes ? merge_attrs(general_attributes, overriding_attributes) : general_attributes
434
+ if overriding_parameters
435
+ overriding_default_content = overriding_parameters.delete(:default)
436
+ parameters = general_parameters.merge(overriding_parameters)
437
+ else
438
+ parameters = general_parameters
439
+ end
440
+
441
+ default_content = parameters[:default]
442
+
443
+ if the_tag.is_one_of?(String, Symbol) && the_tag.to_s.in?(Dryml.static_tags)
444
+ body = if overriding_default_content
445
+ new_context { overriding_default_content.call(proc { default_content.call(nil) if default_content }) }
446
+ elsif default_content
447
+ new_context { default_content.call(nil) }
448
+ else
449
+ nil
450
+ end
451
+ element(the_tag, attributes, body)
452
+ else
453
+ if default_content || overriding_default_content
454
+ d = if overriding_default_content
455
+ proc { |default| overriding_default_content.call(proc { default_content.call(default) if default_content }) }
456
+ else
457
+ proc { |default| default_content.call(default) if default_content }
458
+ end
459
+ parameters = parameters.merge(:default => d)
460
+ end
461
+
462
+ if the_tag.is_one_of?(String, Symbol)
463
+ # It's a defined DRYML tag
464
+ send(the_tag, attributes, parameters)
465
+ else
466
+ # It's a proc - restoring a replaced parameter
467
+ the_tag.call(attributes, parameters)
468
+ end
469
+ end
470
+ end
471
+
472
+
473
+ # This method is used where 'param' is declared on a tag that is
474
+ # itself a parameter tag. Takes two procs that each return a pair
475
+ # of hashes (attributes and parameters). Returns a single proc
476
+ # that also returns a pair of hashes - the merged atributes and
477
+ # parameters.
478
+ def merge_tag_parameter(general_proc, overriding_proc)
479
+ if overriding_proc.nil?
480
+ general_proc
481
+ else
482
+ if overriding_proc.arity == 1
483
+ # The override is a replace parameter - just pass it on
484
+ overriding_proc
485
+ else
486
+ proc do
487
+ overriding_attrs, overriding_parameters = overriding_proc.call
488
+ general_attrs, general_parameters = general_proc.call
489
+
490
+ attrs = merge_attrs(general_attrs, overriding_attrs)
491
+ overriding_default = overriding_parameters.delete(:default)
492
+ params = merge_parameter_hashes(general_parameters, overriding_parameters)
493
+
494
+ # The overrider should provide its :default as the new
495
+ # 'default_content'
496
+ if overriding_default
497
+ params[:default] =
498
+ if general_parameters[:default]
499
+ proc do |default|
500
+ overriding_default.call(proc { new_context { concat(general_parameters[:default].call(default)) } } )
501
+ end
502
+ else
503
+ proc do |default|
504
+ overriding_default.call(default)
505
+ end
506
+ end
507
+ end
508
+
509
+ [attrs, params]
510
+ end
511
+ end
512
+ end
513
+ end
514
+
515
+
516
+ def merge_parameter_hashes(given_parameters, overriding_parameters)
517
+ to_merge = given_parameters.keys & overriding_parameters.keys
518
+ no_merge = overriding_parameters.keys - to_merge
519
+ result = given_parameters.dup
520
+
521
+ no_merge.each { |k| result[k] = overriding_parameters[k] }
522
+ to_merge.each { |k| result[k] = merge_tag_parameter(given_parameters[k], overriding_parameters[k])}
523
+
524
+ result
525
+ end
526
+
527
+
528
+
529
+
530
+ def part_contexts_javascripts
531
+ storage = part_contexts_storage
532
+ storage.blank? ? "" : "<script type=\"text/javascript\">\n#{storage}</script>\n"
533
+ end
534
+
535
+
536
+ def part_contexts_storage
537
+ PartContext.client_side_storage(@_part_contexts, session)
538
+ end
539
+
540
+
541
+ def render_tag(tag_name, attributes)
542
+ method_name = tag_name.to_s.gsub('-', '_')
543
+ if respond_to?(method_name)
544
+ res = send(method_name, attributes).strip
545
+
546
+ # TODO: Temporary hack to get the dryml metadata comments in the right place
547
+ if false && RAILS_ENV == "development"
548
+ res.gsub(/^(.*?)(<!DOCTYPE.*?>).*?(<html.*?>)/m, "\\2\\3\\1")
549
+ else
550
+ res
551
+ end
552
+ else
553
+ false
554
+ end
555
+ end
556
+
557
+
558
+ def element(name, attributes, content=nil, escape = true, empty = false, &block)
559
+ unless attributes.blank?
560
+ attrs = []
561
+ if escape
562
+ attributes.each do |key, value|
563
+ next unless value
564
+ key = key.to_s.gsub("_", "-")
565
+
566
+ value = if ActionView::Helpers::TagHelper::BOOLEAN_ATTRIBUTES.include?(key)
567
+ key
568
+ else
569
+ # escape once
570
+ value.to_s.gsub(/[\"><]|&(?!([a-zA-Z]+|(#\d+));)/) { |special| ERB::Util::HTML_ESCAPE[special] }
571
+ end
572
+ attrs << %(#{key}="#{value}")
573
+ end
574
+
575
+ else
576
+ attrs = options.map do |key, value|
577
+ key = key.to_s.gsub("_", "-")
578
+ %(#{key}="#{value}")
579
+ end
580
+ end
581
+ attr_string = " #{attrs.sort * ' '}" unless attrs.empty?
582
+ end
583
+
584
+ content = new_context { yield } if block_given?
585
+ res = if empty
586
+ "<#{name}#{attr_string}#{scope.xmldoctype ? ' /' : ''}>"
587
+ else
588
+ "<#{name}#{attr_string}>#{content}</#{name}>"
589
+ end
590
+ if block_called_from_erb? block
591
+ concat res
592
+ else
593
+ res
594
+ end
595
+ end
596
+
597
+
598
+ def session
599
+ @view ? @view.session : {}
600
+ end
601
+
602
+
603
+ def method_missing(name, *args, &b)
604
+ if @view
605
+ @view.send(name, *args, &b)
606
+ else
607
+ raise NoMethodError, name.to_s
608
+ end
609
+ end
610
+
611
+ end
612
+
613
+ end