hobo 0.5.3 → 0.6

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