hobo 0.5.3 → 0.6

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.
Files changed (80) hide show
  1. data/bin/hobo +18 -4
  2. data/hobo_files/plugin/CHANGES.txt +511 -0
  3. data/hobo_files/plugin/README +8 -3
  4. data/hobo_files/plugin/Rakefile +81 -0
  5. data/hobo_files/plugin/generators/hobo/hobo_generator.rb +4 -4
  6. data/hobo_files/plugin/generators/hobo/templates/guest.rb +1 -1
  7. data/hobo_files/plugin/generators/hobo_front_controller/hobo_front_controller_generator.rb +1 -1
  8. data/hobo_files/plugin/generators/hobo_front_controller/templates/index.dryml +16 -22
  9. data/hobo_files/plugin/generators/hobo_front_controller/templates/login.dryml +4 -6
  10. data/hobo_files/plugin/generators/hobo_front_controller/templates/search.dryml +6 -5
  11. data/hobo_files/plugin/generators/hobo_front_controller/templates/signup.dryml +4 -6
  12. data/hobo_files/plugin/generators/hobo_migration/hobo_migration_generator.rb +237 -0
  13. data/hobo_files/plugin/generators/hobo_migration/templates/migration.rb +9 -0
  14. data/hobo_files/plugin/generators/hobo_model/USAGE +2 -3
  15. data/hobo_files/plugin/generators/hobo_model/hobo_model_generator.rb +1 -14
  16. data/hobo_files/plugin/generators/hobo_model/templates/fixtures.yml +1 -6
  17. data/hobo_files/plugin/generators/hobo_model/templates/model.rb +10 -4
  18. data/hobo_files/plugin/generators/hobo_rapid/hobo_rapid_generator.rb +7 -6
  19. data/hobo_files/plugin/generators/hobo_rapid/templates/hobo_base.css +68 -0
  20. data/hobo_files/plugin/generators/hobo_rapid/templates/hobo_rapid.css +93 -0
  21. data/hobo_files/plugin/generators/hobo_rapid/templates/hobo_rapid.js +11 -6
  22. data/hobo_files/plugin/generators/hobo_rapid/templates/themes/default/public/images/plus.png +0 -0
  23. data/hobo_files/plugin/generators/hobo_rapid/templates/themes/default/public/stylesheets/application.css +24 -14
  24. data/hobo_files/plugin/generators/hobo_rapid/templates/themes/default/views/application.dryml +28 -44
  25. data/hobo_files/plugin/generators/hobo_user_model/USAGE +2 -12
  26. data/hobo_files/plugin/generators/hobo_user_model/hobo_user_model_generator.rb +1 -14
  27. data/hobo_files/plugin/generators/hobo_user_model/templates/fixtures.yml +0 -6
  28. data/hobo_files/plugin/generators/hobo_user_model/templates/model.rb +8 -1
  29. data/hobo_files/plugin/init.rb +6 -2
  30. data/hobo_files/plugin/lib/active_record/has_many_association.rb +23 -12
  31. data/hobo_files/plugin/lib/extensions.rb +134 -40
  32. data/hobo_files/plugin/lib/extensions/test_case.rb +0 -1
  33. data/hobo_files/plugin/lib/hobo.rb +77 -46
  34. data/hobo_files/plugin/lib/hobo/authenticated_user.rb +24 -2
  35. data/hobo_files/plugin/lib/hobo/authentication_support.rb +2 -1
  36. data/hobo_files/plugin/lib/hobo/controller.rb +35 -12
  37. data/hobo_files/plugin/lib/hobo/define_tags.rb +4 -4
  38. data/hobo_files/plugin/lib/hobo/dryml.rb +33 -51
  39. data/hobo_files/plugin/lib/hobo/dryml/dryml_builder.rb +47 -34
  40. data/hobo_files/plugin/lib/hobo/dryml/scoped_variables.rb +37 -0
  41. data/hobo_files/plugin/lib/hobo/dryml/taglib.rb +27 -5
  42. data/hobo_files/plugin/lib/hobo/dryml/template.rb +545 -302
  43. data/hobo_files/plugin/lib/hobo/dryml/template_environment.rb +305 -135
  44. data/hobo_files/plugin/lib/hobo/email_address.rb +5 -0
  45. data/hobo_files/plugin/lib/hobo/field_spec.rb +66 -0
  46. data/hobo_files/plugin/lib/hobo/hobo_helper.rb +325 -0
  47. data/hobo_files/plugin/lib/hobo/html_string.rb +2 -0
  48. data/hobo_files/plugin/lib/hobo/lazy_hash.rb +13 -1
  49. data/hobo_files/plugin/lib/hobo/markdown_string.rb +3 -1
  50. data/hobo_files/plugin/lib/hobo/model.rb +185 -66
  51. data/hobo_files/plugin/lib/hobo/model_controller.rb +56 -49
  52. data/hobo_files/plugin/lib/hobo/password_string.rb +2 -0
  53. data/hobo_files/plugin/lib/hobo/plugins.rb +75 -0
  54. data/hobo_files/plugin/lib/hobo/rapid_helper.rb +98 -0
  55. data/hobo_files/plugin/lib/hobo/static_tags +0 -3
  56. data/hobo_files/plugin/lib/hobo/textile_string.rb +11 -1
  57. data/hobo_files/plugin/lib/hobo/undefined.rb +1 -1
  58. data/hobo_files/plugin/lib/rexml.rb +166 -75
  59. data/hobo_files/plugin/spec/fixtures/users.yml +9 -0
  60. data/hobo_files/plugin/spec/spec.opts +6 -0
  61. data/hobo_files/plugin/spec/spec_helper.rb +28 -0
  62. data/hobo_files/plugin/spec/unit/hobo/dryml/template_spec.rb +650 -0
  63. data/hobo_files/plugin/tags/core.dryml +58 -4
  64. data/hobo_files/plugin/tags/rapid.dryml +289 -135
  65. data/hobo_files/plugin/tags/rapid_document_tags.dryml +49 -0
  66. data/hobo_files/plugin/tags/rapid_editing.dryml +92 -69
  67. data/hobo_files/plugin/tags/rapid_forms.dryml +242 -0
  68. data/hobo_files/plugin/tags/rapid_navigation.dryml +65 -65
  69. data/hobo_files/plugin/tags/rapid_pages.dryml +197 -124
  70. data/hobo_files/plugin/tags/rapid_support.dryml +23 -0
  71. metadata +29 -22
  72. data/hobo_files/plugin/generators/hobo_model/templates/migration.rb +0 -13
  73. data/hobo_files/plugin/generators/hobo_rapid/templates/themes/default/default_mapping.rb +0 -11
  74. data/hobo_files/plugin/generators/hobo_user_model/templates/migration.rb +0 -15
  75. data/hobo_files/plugin/lib/hobo/HtmlString +0 -3
  76. data/hobo_files/plugin/lib/hobo/controller_helpers.rb +0 -135
  77. data/hobo_files/plugin/lib/hobo/core.rb +0 -475
  78. data/hobo_files/plugin/lib/hobo/rapid.rb +0 -447
  79. data/hobo_files/plugin/test/hobo_dryml_template_test.rb +0 -7
  80. data/hobo_files/plugin/test/hobo_test.rb +0 -7
@@ -0,0 +1,37 @@
1
+ module Hobo::Dryml
2
+
3
+ class ScopedVariables
4
+
5
+ def initialize
6
+ @scopes = [{}]
7
+ end
8
+
9
+ def [](key)
10
+ @scopes.reverse_each do |s|
11
+ return s[key] if s.has_key?(key)
12
+ end
13
+ nil
14
+ end
15
+
16
+ def []=(key, val)
17
+ @scopes.last[key] = val
18
+ end
19
+
20
+ def new_scope
21
+ @scopes << {}
22
+ res = yield
23
+ @scopes.pop
24
+ res
25
+ end
26
+
27
+ def method_missing(name, *args)
28
+ if name.to_s =~ /=$/
29
+ self[name.to_s[0..-2].to_sym] = args.first
30
+ else
31
+ self[name]
32
+ end
33
+ end
34
+
35
+ end
36
+
37
+ end
@@ -36,20 +36,42 @@ module Hobo::Dryml
36
36
  end
37
37
 
38
38
  def load
39
- @module = Module.new
39
+ @module = Module.new do
40
+ @tag_attrs = {}
41
+ def self._register_tag_attrs(tag_name, attrs)
42
+ @tag_attrs[tag_name] = attrs
43
+ end
44
+ class << self
45
+ attr_reader :tag_attrs
46
+ end
47
+ end
40
48
  @file.rewind
41
49
  template = Template.new(@file.read, @module, @file.path)
42
- template.compile([], false)
50
+ template.compile([], [])
43
51
  @last_load_time = @file.mtime
44
52
  end
45
53
 
46
54
  def import_into(class_or_module, as)
47
55
  if as
48
- raise NotImplementedError
49
- as_class = Class.new(TemplateEnvironment) { include @module }
50
- class_or_module.send(:define_method, as) { @module }
56
+ # Define a method on class_or_module named whatever 'as'
57
+ # is. The first time the method is called it creates and
58
+ # returns an object that provides the taglib's tags as
59
+ # methods. On subsequent calls the object is cached in an
60
+ # instance variable "@_#{as}_taglib"
61
+
62
+ taglib_module = @module
63
+ ivar = "@_#{as}_taglib"
64
+ class_or_module.send(:define_method, as) do
65
+ instance_variable_get(ivar) or begin
66
+ as_class = Class.new(TemplateEnvironment) { include taglib_module }
67
+ as_object = as_class.new
68
+ as_object.copy_instance_variables_from(self)
69
+ instance_variable_set(ivar, as_object)
70
+ end
71
+ end
51
72
  else
52
73
  class_or_module.send(:include, @module)
74
+ class_or_module.tag_attrs.update(@module.tag_attrs) if @module.respond_to?(:tag_attrs)
53
75
  end
54
76
  end
55
77
 
@@ -5,6 +5,11 @@ module Hobo::Dryml
5
5
  class Template
6
6
 
7
7
  DRYML_NAME = "[a-zA-Z_][a-zA-Z0-9_]*"
8
+ DRYML_NAME_RX = /^#{DRYML_NAME}$/
9
+
10
+ CODE_ATTRIBUTE_CHAR = "&"
11
+
12
+ SPECIAL_ATTRIBUTES = %w(param merge_attrs for_type if unless repeat part restore)
8
13
 
9
14
  @build_cache = {}
10
15
 
@@ -20,7 +25,6 @@ module Hobo::Dryml
20
25
  @src = src
21
26
 
22
27
  @environment = environment # a class or a module
23
- @environment.send(:include, Hobo::PredicateDispatch)
24
28
 
25
29
  @template_path = template_path.sub(/^#{Regexp.escape(RAILS_ROOT)}/, "")
26
30
 
@@ -32,12 +36,12 @@ module Hobo::Dryml
32
36
 
33
37
  attr_reader :tags, :template_path
34
38
 
35
- def compile(local_names=[], auto_taglibs=true)
39
+ def compile(local_names=[], auto_taglibs=[])
36
40
  now = Time.now
37
41
 
38
42
  unless @template_path == EMPTY_PAGE
39
43
  filename = RAILS_ROOT + (@template_path.starts_with?("/") ? @template_path : "/" + @template_path)
40
- mtime = File.stat(filename).mtime
44
+ mtime = File.stat(filename).mtime rescue nil
41
45
  end
42
46
 
43
47
  if mtime.nil? || !@builder.ready?(mtime)
@@ -61,12 +65,10 @@ module Hobo::Dryml
61
65
  logger.info(" DRYML: Compiled#{from_cache} #{template_path} in %.2fs" % (Time.now - now))
62
66
  end
63
67
 
68
+
64
69
  def create_render_page_method
65
70
  erb_src = process_src
66
-
67
- src = ERB.new(erb_src).src[("_erbout = '';").length..-1]
68
-
69
- @builder.add_build_instruction(:type => :render_page, :src => src, :line_num => 1)
71
+ @builder.add_build_instruction(:render_page, :src => erb_src, :line_num => 1)
70
72
  end
71
73
 
72
74
 
@@ -87,29 +89,26 @@ module Hobo::Dryml
87
89
  end
88
90
 
89
91
  @xmlsrc = "<dryml_page>" + src + "</dryml_page>"
90
-
91
- @doc = REXML::Document.new(RexSource.new(@xmlsrc))
92
-
93
- erb_src = restore_erb_scriptlets(children_to_erb(@doc.root))
92
+ begin
93
+ @doc = REXML::Document.new(RexSource.new(@xmlsrc), :dryml_mode => true)
94
+ rescue REXML::ParseException => e
95
+ raise DrymlSyntaxError, "File: #{@template_path}\n#{e}"
96
+ end
97
+ @doc.default_attribute_value = "&true"
94
98
 
95
- erb_src
99
+ restore_erb_scriptlets(children_to_erb(@doc.root))
96
100
  end
97
101
 
98
102
 
99
103
  def restore_erb_scriptlets(src)
100
- src.gsub(/\[!\[HOBO-ERB(\d+)\s*\]!\]/m) { "<%#{@scriptlets[Regexp.last_match[1].to_i]}%>" }
101
- end
102
-
103
-
104
- def erb_process(src)
105
- ERB.new(restore_erb_scriptlets(src)).src
104
+ src.gsub(/\[!\[HOBO-ERB(\d+)\s*\]!\]/m) {|s| "<%#{@scriptlets[$1.to_i]}%>" }
106
105
  end
107
106
 
108
-
107
+
109
108
  def children_to_erb(nodes)
110
109
  nodes.map{|x| node_to_erb(x)}.join
111
110
  end
112
-
111
+
113
112
 
114
113
  def node_to_erb(node)
115
114
  case node
@@ -131,50 +130,62 @@ module Hobo::Dryml
131
130
 
132
131
 
133
132
  def element_to_erb(el)
134
- dryml_exception("badly placed parameter tag <#{el.name}>", el) if
133
+ dryml_exception("parameter tags (<#{el.name}>) are no more, wake up and smell the coffee", el) if
135
134
  el.name.starts_with?(":")
136
135
 
137
136
  @last_element = el
138
- case el.name
137
+ case el.dryml_name
139
138
 
140
- when "taglib"
141
- taglib_element(el)
142
- # return nothing - the import has no presence in the erb source
139
+ when "include"
140
+ include_element(el)
141
+ # return nothing - the include has no presence in the erb source
143
142
  tag_newlines(el)
144
143
 
145
144
  when "set_theme"
146
145
  require_attribute(el, "name", /^#{DRYML_NAME}$/)
147
- @builder.add_build_instruction(:type => :set_theme, :name => el.attributes['name'])
146
+ @builder.add_build_instruction(:set_theme, :name => el.attributes['name'])
148
147
 
149
148
  # return nothing - set_theme has no presence in the erb source
150
149
  tag_newlines(el)
151
150
 
152
151
  when "def"
153
152
  def_element(el)
154
-
153
+
155
154
  when "tagbody"
156
155
  tagbody_element(el)
157
-
156
+
157
+ when "set"
158
+ set_element(el)
159
+
160
+ when "set_scoped"
161
+ set_scoped_element(el)
162
+
163
+ when "default_tagbody"
164
+ default_tagbody_element(el)
165
+
158
166
  else
159
- if el.name.not_in?(Hobo.static_tags) or
160
- el.attributes['replace_option'] or el.attributes['content_option']
161
- tag_call(el)
167
+ if el.dryml_name.not_in?(Hobo.static_tags) || el.attributes['param'] || el.attributes['restore']
168
+ if el.dryml_name =~ /^[A-Z]/
169
+ template_call(el)
170
+ else
171
+ tag_call(el)
172
+ end
162
173
  else
163
- html_element_to_erb(el)
174
+ static_element_to_erb(el)
164
175
  end
165
176
  end
166
177
  end
167
178
 
168
179
 
169
- def taglib_element(el)
180
+ def include_element(el)
170
181
  require_toplevel(el)
171
182
  require_attribute(el, "as", /^#{DRYML_NAME}$/, true)
172
183
  if el.attributes["src"]
173
- @builder.add_build_instruction(:type => :taglib,
184
+ @builder.add_build_instruction(:include,
174
185
  :name => el.attributes["src"],
175
186
  :as => el.attributes["as"])
176
187
  elsif el.attributes["module"]
177
- @builder.add_build_instruction(:type => :module,
188
+ @builder.add_build_instruction(:module,
178
189
  :name => el.attributes["module"],
179
190
  :as => el.attributes["as"])
180
191
  end
@@ -186,353 +197,578 @@ module Hobo::Dryml
186
197
  end
187
198
 
188
199
 
200
+ def set_element(el)
201
+ assigns = el.attributes.map do |name, value|
202
+ dryml_exception(el, "invalid name in set") unless name =~ DRYML_NAME_RX
203
+ "#{name} = #{attribute_to_ruby(value)}; "
204
+ end.join
205
+ "<% #{assigns}#{tag_newlines(el)} %>"
206
+ end
207
+
208
+
209
+ def set_scoped_element(el)
210
+ assigns = el.attributes.map do |name, value|
211
+ dryml_exception(el, "invalid name in set_scoped") unless name =~ DRYML_NAME_RX
212
+ "scope[:#{name}] = #{attribute_to_ruby(value)}; "
213
+ end.join
214
+ "<% scope.new_scope { #{assigns}#{tag_newlines(el)} %>#{children_to_erb(el)}<% } %>"
215
+ end
216
+
217
+
218
+ def declared_attributes(def_element)
219
+ attrspec = def_element.attributes["attrs"]
220
+ attr_names = attrspec ? attrspec.split(/\s*,\s*/).every(:to_sym) : []
221
+ invalids = attr_names & ([:with, :field, :this] + SPECIAL_ATTRIBUTES.every(:to_sym))
222
+ dryml_exception("invalid attrs in def: #{invalids * ', '}", def_element) unless invalids.empty?
223
+ attr_names
224
+ end
225
+
226
+
189
227
  def def_element(el)
190
228
  require_toplevel(el)
191
- require_attribute(el, "tag", /^#{DRYML_NAME}$/)
229
+ require_attribute(el, "tag", DRYML_NAME_RX)
192
230
  require_attribute(el, "attrs", /^\s*#{DRYML_NAME}(\s*,\s*#{DRYML_NAME})*\s*$/, true)
193
- require_attribute(el, "alias_of", /^#{DRYML_NAME}$/, true)
194
- require_attribute(el, "alias_current", /^#{DRYML_NAME}$/, true)
195
- require_attribute(el, "if", /^#{DRYML_NAME}$/, true)
196
-
197
- name = el.attributes["tag"]
231
+ require_attribute(el, "alias_of", DRYML_NAME_RX, true)
232
+ require_attribute(el, "alias_current_as", DRYML_NAME_RX, true)
233
+
234
+ unsafe_name = el.attributes["tag"]
235
+ name = Hobo::Dryml.unreserve(unsafe_name)
236
+ if (for_type = el.attributes['for'])
237
+ type_name = if for_type =~ /^[a-z]/
238
+ # It's a symbolic type name - look up the Ruby type name
239
+ Hobo.field_types[for_type].name
240
+ else
241
+ for_type
242
+ end.underscore.gsub('/', '__')
243
+ suffix = "__for_#{type_name}"
244
+ name += suffix
245
+ unsafe_name += suffix
246
+ end
247
+
248
+ # While processing this def, @def_name contains
249
+ # the names of all nested defs join with '_'. It's used to
250
+ # disambiguate local variables as a workaround for the broken
251
+ # scope semantics of Ruby 1.8.
252
+ old_def_name = @def_name
253
+ @def_name = @def_name ? "#{@def_name}_#{unsafe_name}" : unsafe_name
198
254
 
199
255
  alias_of = el.attributes['alias_of']
200
- alias_current = el.attributes['alias_current']
256
+ alias_current = el.attributes['alias_current_as']
201
257
 
202
- dryml_exception("def cannot have both alias_of and alias_current", el) if alias_of && alias_current
258
+ dryml_exception("def cannot have both alias_of and alias_current_as", el) if alias_of && alias_current
203
259
  dryml_exception("def with alias_of must be empty", el) if alias_of and el.size > 0
204
-
260
+
261
+ # If we're redefining, we need a statement in the method body
262
+ # that does the alias_method on the fly.
263
+ re_alias = ""
264
+
205
265
  if alias_of || alias_current
206
266
  old_name = alias_current ? name : alias_of
207
267
  new_name = alias_current ? alias_current : name
208
268
 
209
- @builder.add_build_instruction(:type => :alias_method, :new => new_name.to_sym, :old => old_name.to_sym)
269
+ @builder.add_build_instruction(:alias_method, :new => new_name.to_sym, :old => old_name.to_sym)
210
270
  end
211
-
212
- if alias_of
213
- "<% #{tag_newlines(el)} %>"
214
- else
215
- attrspec = el.attributes["attrs"]
216
- attr_names = attrspec ? attrspec.split(/\s*,\s*/) : []
217
-
218
- invalids = attr_names & %w{obj attr this}
219
- dryml_exception("invalid attrs in def: #{invalids * ', '}", el) unless invalids.empty?
220
-
221
- create_tag_method(el, name.to_sym, attr_names.omap{to_sym})
222
- end
223
- end
224
-
225
-
226
- def create_tag_method(el, name, attrs)
227
- name = Hobo::Dryml.unreserve(name)
228
271
 
229
- inner_tags = find_inner_tags(el)
230
-
231
- # A statement to assign values to local variables named after the tag's attrs.
232
- setup_locals = ( (attrs.map{|a| "#{Hobo::Dryml.unreserve(a)}, "} + ['options, inner_tag_options']).join +
233
- " = _tag_locals(__options__, #{attrs.inspect}, #{inner_tags.inspect})" )
234
-
235
- start = "_tag_context(__options__, __block__) do |tagbody| #{setup_locals}"
272
+ res = if alias_of
273
+ "#{re_alias}<% #{tag_newlines(el)} %>"
274
+ else
275
+ src = if template_name?(name)
276
+ template_method(name, re_alias, el)
277
+ else
278
+ tag_method(name, re_alias, el)
279
+ end
280
+ src << "<% _register_tag_attrs(:#{name}, #{declared_attributes(el).inspect}) %>"
281
+
282
+ logger.debug(restore_erb_scriptlets(src)) if el.attributes["debug_source"]
283
+
284
+ @builder.add_build_instruction(:def,
285
+ :src => restore_erb_scriptlets(src),
286
+ :line_num => element_line_num(el))
287
+ # keep line numbers matching up
288
+ "<% #{"\n" * src.count("\n")} %>"
289
+ end
290
+ @def_name = old_def_name
291
+ res
292
+ end
293
+
294
+
295
+ def template_call?(el)
296
+ template_name?(el.name)
297
+ end
298
+
299
+
300
+ def template_name?(name)
301
+ name =~ /^[A-Z]/
302
+ end
303
+
304
+
305
+ def param_names_in_template(el)
306
+ REXML::XPath.match(el, ".//*[@param]").map do |e|
307
+ name = get_param_name(e)
308
+ dryml_exception("invalid param name: #{name.inspect}", e) unless
309
+ is_code_attribute?(name) || name =~ DRYML_NAME_RX || name =~ /#\{/
310
+ name.to_sym unless is_code_attribute?(name)
311
+ end.compact
312
+ end
313
+
314
+
315
+ def template_method(name, re_alias, el)
316
+ param_names = param_names_in_template(el)
236
317
 
237
- pred = el.attributes["if"]
238
- pred = pred[1..-1] if pred && pred.starts_with?('#')
318
+ "<% def #{name}(all_attributes={}, all_parameters={}, &__block__); " +
319
+ "parameters = all_parameters - #{param_names.inspect}; " +
320
+ tag_method_body(el) +
321
+ "; end %>"
322
+ end
323
+
324
+
325
+ def tag_method(name, re_alias, el)
326
+ "<% def #{name}(all_attributes={}, &__block__); " +
327
+ "parameters = nil; " +
328
+ tag_method_body(el) +
329
+ "; end %>"
330
+ end
331
+
332
+
333
+ def tag_method_body(el, attributes_var="all_attributes", block_var="__block__")
334
+ attrs = declared_attributes(el)
239
335
 
240
- def_line = if pred
241
- "defp :#{name}, (proc {|options| #{pred}}) do |__options__, __block__|"
242
- elsif @environment.predicate_method?(name)
243
- # be sure not to overwrite the predicate dispatch method
244
- "defp :#{name} do |__options__, __block__|"
245
- else
246
- "def #{name}(__options__={}, &__block__)"
247
- end
336
+ # A statement to assign values to local variables named after the tag's attrs
337
+ # The trailing comma on `attributes` is supposed to be there!
338
+ setup_locals = attrs.map{|a| "#{Hobo::Dryml.unreserve(a)}, "}.join + "attributes, = " +
339
+ "_tag_locals(#{attributes_var}, #{attrs.inspect})"
248
340
 
249
- method_src = ( "<% #{def_line}; #{start} " +
250
- # reproduce any line breaks in the start-tag so that line numbers are preserved
251
- tag_newlines(el) + "%>" +
252
- children_to_erb(el) +
253
- "<% @output; end; end %>" )
341
+ start = "_tag_context(#{attributes_var}, #{block_var}) do |tagbody| #{setup_locals}"
254
342
 
255
- logger.debug(restore_erb_scriptlets(method_src)) if el.attributes["hobo_debug_source"]
256
-
257
- @builder.add_build_instruction(:type => :def, :src => erb_process(method_src), :line_num => element_line_num(el))
258
-
259
- # keep line numbers matching up
260
- "<% #{"\n" * method_src.count("\n")} %>"
343
+ "#{start} " +
344
+ # reproduce any line breaks in the start-tag so that line numbers are preserved
345
+ tag_newlines(el) + "%>" +
346
+ children_to_erb(el) +
347
+ "<% _erbout; end"
261
348
  end
262
349
 
263
350
 
264
- def find_inner_tags(el)
265
- el.map do |e|
266
- if e.is_a?(REXML::Element)
267
- name = e.attributes["content_option"] || e.attributes["replace_option"]
268
- [(name if name && !is_code_attribute?(name))] + find_inner_tags(e)
269
- else
270
- []
271
- end
272
- end.flatten.compact
273
- end
274
-
275
-
276
351
  def tagbody_element(el)
277
352
  dryml_exception("tagbody can only appear inside a <def>", el) unless
278
353
  find_ancestor(el) {|e| e.name == 'def'}
279
354
  dryml_exception("tagbody cannot appear inside a part", el) if
280
- find_ancestor(el) {|e| e.attributes['part_id']}
355
+ find_ancestor(el) {|e| e.attributes['part']}
281
356
  tagbody_call(el)
282
357
  end
283
-
358
+
284
359
 
285
360
  def tagbody_call(el)
286
- options = []
287
- obj = el.attributes['obj']
288
- attr = el.attributes['attr']
289
- options << ":obj => #{attribute_to_ruby(obj)}" if obj
290
- options << ":attr => #{attribute_to_ruby(attr)}" if attr
291
- else_ = attribute_to_ruby(el.attributes['else'])
292
- "<%= tagbody ? tagbody.call({ #{options * ', '} }) : #{else_} %>"
361
+ attributes = []
362
+ with = el.attributes['with']
363
+ field = el.attributes['field']
364
+ attributes << ":with => #{attribute_to_ruby(with)}" if with
365
+ attributes << ":field => #{attribute_to_ruby(field)}" if field
366
+
367
+ default_body = if el.children.empty?
368
+ "nil"
369
+ else
370
+ "proc { %>#{children_to_erb(el)}<% }"
371
+ end
372
+
373
+ call = apply_control_attributes("do_tagbody(tagbody, {#{attributes * ', '}}, #{default_body})", el)
374
+ "<% _output(#{call}) %>"
375
+ end
376
+
377
+
378
+ def default_tagbody_element(el)
379
+ name = el.attributes['for'] || @containing_tag_name
380
+ "<% #{name}_default_tagbody && #{name}_default_tagbody.call %>"
293
381
  end
294
382
 
295
383
 
296
384
  def part_element(el, content)
297
- require_attribute(el, "part_id", /^#{DRYML_NAME}$/)
298
- part_name = el.attributes['part_id']
385
+ require_attribute(el, "part", DRYML_NAME_RX)
386
+ part_name = el.attributes['part']
299
387
  dom_id = el.attributes['id'] || part_name
300
388
 
301
- dryml_exception("dupplicate part name: #{part_name}", el) if
302
- (part_name + "_part").in?(@environment.instance_methods)
303
-
304
389
  part_src = "<% def #{part_name}_part #{tag_newlines(el)}; new_context do %>" +
305
390
  content +
306
391
  "<% end; end %>"
307
- create_part(part_src, element_line_num(el))
392
+ @builder.add_part(part_name, restore_erb_scriptlets(part_src), element_line_num(el))
308
393
 
309
394
  newlines = "\n" * part_src.count("\n")
310
- res = "<%= call_part(#{attribute_to_ruby(dom_id)}, :#{part_name}) #{newlines} %>"
395
+ "<%= call_part(#{attribute_to_ruby(dom_id)}, :#{part_name}) #{newlines} %>"
396
+ end
397
+
398
+
399
+ def get_param_name(el)
400
+ param_name = el.attributes["param"]
401
+
402
+ if param_name
403
+ def_tag = find_ancestor(el) {|e| e.name == "def"}
404
+ dryml_exception("param is not allowed outside of template definitions", el) if
405
+ def_tag.nil? || !template_name?(def_tag.attributes["tag"])
406
+ end
407
+
408
+ res = param_name == "&true" ? el.dryml_name : param_name
409
+
410
+ dryml_exception("param name for a template call must be capitalised", el) if
411
+ res && template_call?(el) && !template_name?(res)
412
+ dryml_exception("param name for a block-tag call must not be capitalised", el) if
413
+ res && !template_call?(el) && template_name?(res)
414
+
311
415
  res
312
416
  end
417
+
418
+
419
+ def call_name(el)
420
+ Hobo::Dryml.unreserve(el.dryml_name)
421
+ end
422
+
423
+
424
+ def polymorphic_call_type(el)
425
+ t = el.attributes['for_type']
426
+ if t.nil?
427
+ nil
428
+ elsif t == "&true"
429
+ 'this_type'
430
+ elsif t =~ /^[A-Z]/
431
+ t
432
+ elsif t =~ /^[a-z]/
433
+ "Hobo.field_types[:#{t}]"
434
+ elsif is_code_attribute?(t)
435
+ t[1..-1]
436
+ else
437
+ dryml_exception("invalid for_type attribute", el)
438
+ end
439
+ end
440
+
441
+
442
+ def template_call(el)
443
+ name = call_name(el)
444
+ param_name = get_param_name(el)
445
+ attributes = tag_attributes(el)
446
+ newlines = tag_newlines(el)
447
+
448
+ parameters = tag_newlines(el) + tag_parameters(el)
449
+
450
+ is_param_default_call = el.attributes['restore']
451
+
452
+ call = if param_name
453
+ param_name = attribute_to_ruby(param_name, :symbolize => true)
454
+ args = "#{attributes}, #{parameters}, all_parameters[#{param_name}]"
455
+ to_call = if is_param_default_call
456
+ # The tag is available in a local variable
457
+ # holding a proc
458
+ el.dryml_name
459
+ elsif (call_type = polymorphic_call_type(el))
460
+ "find_polymorphic_template(:#{name}, #{call_type})"
461
+ else
462
+ ":#{name}"
463
+ end
464
+ "call_template_parameter(#{to_call}, #{args})"
465
+ else
466
+ if is_param_default_call
467
+ # The tag is a proc available in a local variable
468
+ "#{name}__default.call(#{attributes}, #{parameters})"
469
+ elsif (call_type = polymorphic_call_type(el))
470
+ "send(find_polymorphic_template(:#{name}, #{call_type}), #{attributes}, #{parameters})"
471
+ else
472
+ "#{name}(#{attributes}, #{parameters})"
473
+ end
474
+ end
313
475
 
476
+ call = apply_control_attributes(call, el)
477
+ maybe_make_part_call(el, "<% _output(#{call}) %>")
478
+ end
479
+
480
+
481
+ def merge_attribute(el)
482
+ merge = el.attributes['merge']
483
+ dryml_exception("merge cannot have a RHS", el) if merge && merge != "&true"
484
+ merge
485
+ end
486
+
314
487
 
315
- def create_part(erb_src, line_num)
316
- # Add a method to the part module for this template
488
+ # Tag parameters are parameters to templates.
489
+ def tag_parameters(el)
490
+ dryml_exception("content is not allowed directly inside template calls", el) if
491
+ el.children.find { |e| e.is_a?(REXML::Text) && !e.to_s.blank? }
317
492
 
318
- @builder.add_build_instruction(:type => :part, :src => erb_process(erb_src), :line_num => line_num)
493
+ param_items = el.map do |node|
494
+ case node
495
+ when REXML::Text
496
+ # Just whitespace
497
+ node.to_s
498
+ when REXML::Element
499
+ e = node
500
+ param_name = get_param_name(e)
501
+ if param_name
502
+ if template_call?(e)
503
+ ":#{e.name} => merge_template_parameter_procs(#{template_proc(e)}, all_parameters[:#{param_name}]), "
504
+ else
505
+ ":#{e.name} => merge_option_procs(#{template_proc(e)}, all_parameters[:#{param_name}]), "
506
+ end
507
+ else
508
+ ":#{e.name} => #{template_proc(e)}, "
509
+ end
510
+ end
511
+ end.join
512
+
513
+
514
+ merge_params = el.attributes['merge_params'] || merge_attribute(el)
515
+ if merge_params
516
+ extra_params = if merge_params == "&true"
517
+ "parameters"
518
+ elsif is_code_attribute?(merge_params)
519
+ merge_params[1..-1]
520
+ else
521
+ dryml_exception("invalid merge_params", el)
522
+ end
523
+ "{#{param_items}}.merge((#{extra_params}) || {})"
524
+ else
525
+ "{#{param_items}}"
526
+ end
319
527
  end
320
528
 
321
529
 
322
- def tag_call(el)
323
- require_attribute(el, "content_option", /#{DRYML_NAME}/, true)
324
- require_attribute(el, "replace_option", /#{DRYML_NAME}/, true)
325
-
326
- # find out if it's empty before removing any <:param_tags>
327
- empty_el = el.size == 0
328
-
329
- # gather <:param_tags>, and remove them from the dom
330
- compiled_param_tags = compile_parameter_tags(el)
530
+ def template_proc(el)
531
+ nl = tag_newlines(el)
532
+ if (repl = el.attribute("replace"))
533
+ dryml_exception("replace attribute must not have a value", el) if repl.has_rhs?
534
+ dryml_exception("replace parameters must not have attributes", el) if el.attributes.length > 1
331
535
 
332
- name = Hobo::Dryml.unreserve(el.name)
333
- options = tag_options(el, compiled_param_tags)
334
- newlines = tag_newlines(el)
335
- replace_option = el.attributes["replace_option"]
336
- content_option = el.attributes["content_option"]
337
- dryml_exception("both replace_option and content_option given") if replace_option && content_option
338
- call = if replace_option
339
- replace_option = attribute_to_ruby(replace_option)
340
- "call_replaceable_tag(:#{name}, #{options}, inner_tag_options[#{replace_option}.to_sym])"
341
- elsif content_option
342
- content_option = attribute_to_ruby(content_option)
343
- "call_replaceable_content_tag(:#{name}, #{options}, inner_tag_options[#{content_option}.to_sym])"
344
- else
345
- "#{name}(#{options})"
346
- end
536
+ default_tag_name = "#{el.dryml_name}__default"
347
537
 
348
- part_id = el.attributes['part_id']
349
- if empty_el
350
- if part_id
351
- "<span id='#{part_id}'>" + part_element(el, "<%= #{call} %>") + "</span>"
352
- else
353
- "<%= #{call} #{newlines}%>"
354
- end
538
+ "proc {|#{default_tag_name}| new_context { %>#{children_to_erb(el)}<% } #{nl}}"
355
539
  else
356
- children = children_to_erb(el)
357
- if part_id
358
- id = el.attributes['id'] || part_id
359
- "<span id='<%= #{attribute_to_ruby(id)} %>'>" +
360
- part_element(el, "<% _erbout.concat(#{call} do %>#{children}<% end) %>") +
361
- "</span>"
540
+ attributes = el.attributes.map do
541
+ |name, value| ":#{name} => #{attribute_to_ruby(value, el)}" unless name.in?(SPECIAL_ATTRIBUTES)
542
+ end.compact
543
+
544
+ if template_call?(el || modifiers.first)
545
+ parameters = el ? tag_parameters(el) : "{}"
546
+ "proc { [{#{attributes * ', '}}, #{parameters}] #{nl}}"
362
547
  else
363
- "<% _erbout.concat(#{call} do #{newlines}%>#{children}<% end) %>"
548
+ if el && el.has_end_tag?
549
+ old = @containing_tag_name
550
+ @containing_tag_name = el.dryml_name
551
+ body = children_to_erb(el)
552
+ @containing_tag_name = old
553
+
554
+ attributes << ":tagbody => proc {|#{el.dryml_name}_default_tagbody| new_context { %>#{body}<% } } "
555
+ end
556
+ "proc { {#{attributes * ', '}} #{nl}}"
364
557
  end
365
558
  end
366
559
  end
367
560
 
368
561
 
369
- def compile_parameter_tags(el)
370
- # The implementation of parameter tags is greatly complicated
371
- # by the need to maintain line-number parity between the dryml source
372
- # and generated erb source
373
-
374
- last = el.children.reverse.ofind{is_a?(REXML::Element) && name.starts_with?(":")}
375
- return "" if last.nil?
376
- param_section = el.children[0..el.index(last)]
377
-
378
- dryml_exception("invalid content before parameter tag", el) unless param_section.all? do |e|
379
- (e.is_a?(REXML::Element) && e.name.starts_with?(":")) ||
380
- (e.is_a?(REXML::Text) && e.to_s.blank?) ||
381
- e.is_a?(REXML::Comment)
562
+ def default_tag_name(el)
563
+ if template_call?(el)
564
+ "Default#{el.dryml_name}"
565
+ else
566
+ "default_#{el.dryml_name}"
382
567
  end
568
+ end
569
+
570
+
571
+ def tag_call(el)
572
+ name = call_name(el)
573
+ param_name = get_param_name(el)
574
+ attributes = tag_attributes(el)
575
+ newlines = tag_newlines(el)
383
576
 
384
- last = param_section.last
385
- compiled = param_section.map do |e|
386
- # REXML Bug - don't use el.remove (also removes other children that are == )
387
- el.delete_at(el.index(e))
388
- case e
389
- when REXML::Element
390
- array_index = begin
391
- # If there are other param-tags with this
392
- # name, the index of this one, else nil
393
- same_name_params = param_section.select {|x| x.is_a?(REXML::Element) and x.name == e.name}
394
- same_name_params.length > 1 && same_name_params.index(e)
395
- end
396
-
397
- param_name = attr_name_to_option_key(e.name[1..-1], array_index)
398
-
399
- dryml_exception("duplicate attribute/parameter-tag #{param}", el) if
400
- param_name.in?(el.attributes.keys)
401
-
402
- param_value =
403
- if e.has_attributes?
404
- pairs = e.attributes.map do |n,v|
405
- "#{attr_name_to_option_key(n)} => " + "#{attribute_to_ruby(v)}"
406
- end
407
- # If there is content to, that goes in the hash under the key :content
408
- if e.size > 0
409
- pairs << "#{tag_newlines(el)}:content => (capture do %>#{children_to_erb(e)}<%; end)"
410
- end
411
- "{" + pairs.join(",") + "}"
412
- elsif e.size > 0
413
- "#{tag_newlines(el)}(capture do %>#{children_to_erb(e)}<%; end)"
414
- else
415
- "''"
416
- end
417
- pair = "#{param_name} => #{param_value}"
418
- pair << ", " unless e == last
419
- pair
420
-
421
- when REXML::Text
422
- e.to_s
423
-
424
- when REXML::Comment
425
- REXML::Comment::START + node.to_s + REXML::Comment::STOP
426
-
427
- end
428
- end
577
+ is_param_default_call = el.attributes['restore']
578
+
579
+ call = if param_name
580
+ to_call = if is_param_default_call
581
+ # The tag is available in a local variable
582
+ # holding a proc
583
+ el.dryml_name
584
+ elsif (call_type = polymorphic_call_type(el))
585
+ "find_polymorphic_tag(:#{name}, #{call_type})"
586
+ else
587
+ ":#{name}"
588
+ end
589
+ param_name = attribute_to_ruby(param_name, :symbolize => true)
590
+ "call_block_tag_parameter(#{to_call}, #{attributes}, all_parameters[#{param_name}])"
591
+ else
592
+ if is_param_default_call
593
+ "#{name}__default.call_with_block(#{attributes})"
594
+ elsif (call_type = polymorphic_call_type(el))
595
+ "send(find_polymorphic_tag(:#{name}, #{call_type}), #{attributes})"
596
+ else
597
+ "#{name}(#{attributes unless attributes == '{}'})"
598
+ end
599
+ end
429
600
 
430
- compiled.join
601
+ if el.children.empty?
602
+ call = apply_control_attributes(call, el)
603
+ maybe_make_part_call(el, "<%= #{call} #{newlines}%>")
604
+ else
605
+ old = @containing_tag_name
606
+ @containing_tag_name = el.dryml_name
607
+ children = children_to_erb(el)
608
+ @containing_tag_name = old
609
+
610
+ call_statement = "#{call} do |#{el.dryml_name}_default_tagbody| #{newlines}%>#{children}<% end"
611
+ call = "<% _output(" + apply_control_attributes(call_statement, el) + ") %>"
612
+ maybe_make_part_call(el, call)
613
+ end
431
614
  end
432
-
433
615
 
434
- def attr_name_to_option_key(name, array_index=nil)
435
- parts = name.split(".").map { |p| ":#{p}" }
436
- parts << array_index if array_index
437
- if parts.length == 1
438
- parts.first
616
+ def maybe_make_part_call(el, call)
617
+ part_name = el.attributes['part']
618
+ if part_name
619
+ part_id = part_name && "<%= #{attribute_to_ruby(el.attributes['id'] || part_name)} %>"
620
+ "<span class='part_wrapper' id='#{part_id}'>" + part_element(el, call) + "</span>"
439
621
  else
440
- "[" + parts.join(', ') + "]"
622
+ call
441
623
  end
442
624
  end
443
625
 
444
626
 
445
- def tag_options(el, param_tags_compiled)
627
+ def tag_attributes(el)
446
628
  attributes = el.attributes
447
-
448
- options = attributes.map do |n,v|
449
- param_name = attr_name_to_option_key(n)
450
- param_value = attribute_to_ruby(v)
451
- "#{param_name} => #{param_value}"
452
- end
629
+ items = attributes.map do |n,v|
630
+ ":#{n} => #{attribute_to_ruby(v)}" unless n.in?(SPECIAL_ATTRIBUTES)
631
+ end.compact
453
632
 
454
- options << param_tags_compiled unless param_tags_compiled.blank?
455
- all = options.join(', ')
633
+ # if there's a ':' el.name is just the part after the ':'
634
+ items << ":field => \"#{el.name}\"" if el.name != el.expanded_name
456
635
 
457
- xattrs = attributes['xattrs']
458
- if xattrs
459
- extra_options = if xattrs.blank?
460
- "options"
461
- elsif xattrs.starts_with?("#")
462
- xattrs[1..-1]
636
+ items = items.join(", ")
637
+
638
+ merge_attrs = attributes['merge_attrs'] || merge_attribute(el)
639
+ if merge_attrs
640
+ extra_attributes = if merge_attrs == "&true"
641
+ "attributes"
642
+ elsif is_code_attribute?(merge_attrs)
643
+ merge_attrs[1..-1]
463
644
  else
464
- dryml_exception("invalid xattrs", el)
645
+ dryml_exception("invalid merge_attrs", el)
465
646
  end
466
- "{#{all}}.merge((#{extra_options}) || {})"
647
+ "{#{items}}.merge((#{extra_attributes}) || {})"
467
648
  else
468
- "{#{all}}"
649
+ "{#{items}}"
469
650
  end
470
651
  end
471
652
 
472
-
473
- def html_element_to_erb(el)
474
- start_tag_src = el.instance_variable_get("@start_tag_source").
475
- gsub(REXML::CData::START, "").gsub(REXML::CData::STOP, "")
476
-
477
- xattrs = el.attributes["xattrs"]
478
- if xattrs
479
- attr_args = if xattrs.starts_with?('#')
480
- xattrs[1..-1]
481
- elsif xattrs.blank?
482
- "options"
483
- else
484
- dryml_exception("invalid xattrs", el)
485
- end
486
- class_attr = el.attributes["class"]
487
- if class_attr
488
- raise HoboError.new("invalid class attribute with xattrs: '#{class_attr}'") if
489
- class_attr =~ /'|\[!\[HOBO-ERB/
490
-
491
- attr_args.concat(", '#{class_attr}'")
492
- start_tag_src.sub!(/\s*class\s*=\s*('[^']*?'|"[^"]*?")/, "")
493
- end
494
- start_tag_src.sub!(/\s*xattrs\s*=\s*('[^']*?'|"[^"]*?")/, " <%= xattrs(#{attr_args}) %>")
653
+ def static_tag_to_method_call(el)
654
+ part = el.attributes["part"]
655
+ attrs = el.attributes.map do |n, v|
656
+ next if n.in? SPECIAL_ATTRIBUTES
657
+ val = restore_erb_scriptlets(v).gsub('"', '\"').gsub(/<%=(.*?)%>/, '#{\1}')
658
+ %(:#{n} => "#{val}")
659
+ end.compact
660
+
661
+ # If there's a part but no id, the id defaults to the part name
662
+ if part && !el.attributes["id"]
663
+ attrs << ":id => '#{part}'"
495
664
  end
665
+
666
+ # Convert the attributes hash to a call to merge_attrs if
667
+ # there's a merge_attrs attribute
668
+ attrs = if (merge_attrs = el.attributes['merge_attrs'])
669
+ dryml_exception("merge_attrs was given a string", el) unless is_code_attribute?(merge_attrs)
670
+
671
+ "merge_attrs({#{attrs * ', '}}, " +
672
+ "((__merge_attrs__ = (#{merge_attrs[1..-1]})) == true ? attributes : __merge_attrs__))"
673
+ else
674
+ "{" + attrs.join(', ') + "}"
675
+ end
676
+
677
+ if el.children.empty?
678
+ dryml_exception("part attribute on empty static tag", el) if part
496
679
 
497
- if start_tag_src.ends_with?("/>")
498
- start_tag_src
680
+ "<%= " + apply_control_attributes("tag(:#{el.name}, #{attrs} #{tag_newlines(el)})", el) + " %>"
499
681
  else
500
- if el.attributes['part_id']
682
+ if part
501
683
  body = part_element(el, children_to_erb(el))
502
- if el.attributes["id"]
503
- # remove part_id, and eval the id attribute with an erb scriptlet
504
- start_tag_src.sub!(/\s*part_id\s*=\s*('[^']*?'|"[^"]*?")/, "")
505
- id_expr = attribute_to_ruby(el.attributes['id'])
506
- start_tag_src.sub!(/id\s*=\s*('[^']*?'|"[^"]*?")/, "id='<%= #{id_expr} %>'")
507
- else
508
- # rename part_id to id
509
- start_tag_src.sub!(/part_id\s*=\s*('[^']*?'|"[^"]*?")/, "id=\\1")
510
- end
511
- dryml_exception("multiple part ids", el) if start_tag_src.index("part_id=")
684
+ else
685
+ body = children_to_erb(el)
686
+ end
512
687
 
688
+ output_tag = "tag(:#{el.name}, #{attrs}, true) + new_context { %>#{body}</#{el.name}><% }"
689
+ "<% _output(" + apply_control_attributes(output_tag, el) + ") %>"
690
+ end
691
+ end
692
+
693
+
694
+ def static_element_to_erb(el)
695
+ if %w(part merge_attrs if unless repeat).any? {|x| el.attributes[x]}
696
+ static_tag_to_method_call(el)
697
+ else
698
+ start_tag_src = el.start_tag_source.gsub(REXML::CData::START, "").gsub(REXML::CData::STOP, "")
699
+
700
+ # Allow #{...} as an alternate to <%= ... %>
701
+ start_tag_src.gsub!(/=\s*('.*?'|".*?")/) do |s|
702
+ s.gsub(/#\{(.*?)\}/, '<%= \1 %>')
703
+ end
513
704
 
514
- start_tag_src + body + "</#{el.name}>"
515
- else
705
+ if el.has_end_tag?
516
706
  start_tag_src + children_to_erb(el) + "</#{el.name}>"
707
+ else
708
+ start_tag_src
517
709
  end
518
710
  end
519
711
  end
712
+
713
+
714
+ def apply_control_attributes(expression, el)
715
+ if_, unless_, repeat = controls = %w(if unless repeat).map {|x| el.attributes[x]}
716
+ controls.compact!
717
+
718
+ dryml_exception("You can't have multiple control attributes on the same element", el) if
719
+ controls.length > 1
720
+
721
+ val = controls.first
722
+ if val.nil?
723
+ expression
724
+ else
725
+ control = if repeat && val == "&true"
726
+ "this"
727
+ elsif is_code_attribute?(val)
728
+ "#{val[1..-1]}"
729
+ else
730
+ "this.#{val}"
731
+ end
732
+
733
+ x = gensym
734
+ if if_
735
+ "(if !(#{control}).blank?; (#{x} = #{expression}; Hobo::Dryml.last_if = true; #{x}) " +
736
+ "else (Hobo::Dryml.last_if = false; ''); end)"
737
+ elsif unless_
738
+ "(if (#{control}).blank?; (#{x} = #{expression}; Hobo::Dryml.last_if = true; #{x}) " +
739
+ "else (Hobo::Dryml.last_if = false; ''); end)"
740
+ elsif repeat
741
+ "repeat_attribute(#{control}) { #{expression} }"
742
+ end
743
+ end
744
+ end
745
+
746
+
747
+ def attribute_to_ruby(*args)
748
+ options = extract_options_from_args!(args)
749
+ attr, el = args
750
+
751
+ dryml_exception('erb scriptlet not allowed in this attribute (use #{ ... } instead)', el) if
752
+ attr.is_a?(String) && attr.index("[![HOBO-ERB")
520
753
 
521
- def attribute_to_ruby(attr)
522
- dryml_exception("erb scriptlet in attribute of defined tag") if attr && attr.index("[![HOBO-ERB")
523
- if attr.nil?
524
- "nil"
525
- elsif is_code_attribute?(attr)
526
- "(#{attr[1..-1]})"
754
+ if options[:symbolize] && attr =~ /^[a-zA-Z_][^a-zA-Z0-9_]*[\?!]?/
755
+ ":#{attr}"
527
756
  else
528
- str = if not attr =~ /"/
529
- '"' + attr + '"'
530
- elsif not attr =~ /'/
531
- "'#{attr}'"
757
+ res = if attr.nil?
758
+ "nil"
759
+ elsif is_code_attribute?(attr)
760
+ "(#{attr[1..-1]})"
532
761
  else
533
- dryml_exception("invalid quote(s) in attribute value")
534
- end
535
- attr.starts_with?("++") ? "attr_extension(#{str})" : str
762
+ if attr !~ /"/
763
+ '"' + attr + '"'
764
+ elsif attr !~ /'/
765
+ "'#{attr}'"
766
+ else
767
+ dryml_exception("invalid quote(s) in attribute value")
768
+ end
769
+ #attr.starts_with?("++") ? "attr_extension(#{str})" : str
770
+ end
771
+ options[:symbolize] ? (res + ".to_sym") : res
536
772
  end
537
773
  end
538
774
 
@@ -545,41 +781,48 @@ module Hobo::Dryml
545
781
  return nil
546
782
  end
547
783
 
548
- def require_toplevel(el)
549
- dryml_exception("<#{el.name}> can only be at the top level", el) if el.parent != @doc.root
784
+ def require_toplevel(el, message=nil)
785
+ message ||= "can only be at the top level"
786
+ dryml_exception("<#{el.dryml_name}> #{message}", el) if el.parent != @doc.root
550
787
  end
551
788
 
552
789
  def require_attribute(el, name, rx=nil, optional=false)
553
790
  val = el.attributes[name]
554
791
  if val
555
- dryml_exception("invalid #{name}=\"#{val}\" attribute on <#{el.name}>", el) unless val =~ rx
792
+ dryml_exception("invalid #{name}=\"#{val}\" attribute on <#{el.dryml_name}>", el) unless rx && val =~ rx
556
793
  else
557
- dryml_exception("missing #{name} attribute on <#{el.name}>", el) unless optional
794
+ dryml_exception("missing #{name} attribute on <#{el.dryml_name}>", el) unless optional
558
795
  end
559
796
  end
560
797
 
561
798
  def dryml_exception(message, el=nil)
562
799
  el ||= @last_element
563
- raise DrymlException.new(message + " -- at #{template_path}:#{element_line_num(el)}")
800
+ raise DrymlException.new(message, template_path, element_line_num(el))
564
801
  end
565
802
 
566
803
  def element_line_num(el)
567
- offset = el.instance_variable_get("@source_offset")
804
+ offset = el.source_offset
568
805
  line_no = @xmlsrc[0..offset].count("\n") + 1
569
806
  end
570
807
 
571
808
  def tag_newlines(el)
572
- src = el.instance_variable_get("@start_tag_source")
809
+ src = el.start_tag_source
573
810
  "\n" * src.count("\n")
574
811
  end
575
812
 
576
813
  def is_code_attribute?(attr_value)
577
- attr_value.starts_with?("#")
814
+ attr_value =~ /^\&/ && attr_value !~ /^\&\S+;/
578
815
  end
579
816
 
580
817
  def logger
581
818
  ActionController::Base.logger rescue nil
582
819
  end
820
+
821
+ def gensym(name="__tmp")
822
+ @gensym_counter ||= 0
823
+ @gensym_counter += 1
824
+ "#{name}_#{@gensym_counter}"
825
+ end
583
826
 
584
827
  end
585
828