dryml 1.1.0.pre0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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