cml 1.4.2 → 1.5.0

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 (57) hide show
  1. data/.rspec +1 -0
  2. data/Gemfile +4 -0
  3. data/Gemfile.lock +33 -0
  4. data/README.rdoc +13 -3
  5. data/Rakefile +9 -49
  6. data/cml.gemspec +23 -125
  7. data/lib/cml/converters/jsawesome.rb +3 -3
  8. data/lib/cml/gold.rb +12 -7
  9. data/lib/cml/liquid_filters.rb +4 -0
  10. data/lib/cml/logic.rb +424 -0
  11. data/lib/cml/logic_tree/graph.rb +107 -0
  12. data/lib/cml/logic_tree/solver.rb +43 -0
  13. data/lib/cml/parser.rb +47 -7
  14. data/lib/cml/tag.rb +42 -21
  15. data/lib/cml/tags/checkbox.rb +14 -4
  16. data/lib/cml/tags/checkboxes.rb +4 -0
  17. data/lib/cml/tags/group.rb +4 -0
  18. data/lib/cml/tags/hours.rb +263 -0
  19. data/lib/cml/tags/iterate.rb +4 -0
  20. data/lib/cml/tags/meta.rb +4 -0
  21. data/lib/cml/tags/option.rb +4 -0
  22. data/lib/cml/tags/radio.rb +13 -4
  23. data/lib/cml/tags/radios.rb +4 -0
  24. data/lib/cml/tags/ratings.rb +9 -1
  25. data/lib/cml/tags/search.rb +8 -2
  26. data/lib/cml/tags/select.rb +6 -2
  27. data/lib/cml/tags/taxonomy.rb +148 -0
  28. data/lib/cml/tags/thumb.rb +4 -0
  29. data/lib/cml/tags/unknown.rb +4 -0
  30. data/lib/cml/version.rb +3 -0
  31. data/lib/cml.rb +3 -0
  32. data/spec/converters/jsawesome_spec.rb +0 -9
  33. data/spec/fixtures/logic_broken.cml +5 -0
  34. data/spec/fixtures/logic_circular.cml +5 -0
  35. data/spec/fixtures/logic_grouped.cml +7 -0
  36. data/spec/fixtures/logic_nested.cml +7 -0
  37. data/spec/fixtures/logic_none.cml +7 -0
  38. data/spec/fixtures/logic_not_nested.cml +7 -0
  39. data/spec/liquid_filter_spec.rb +19 -0
  40. data/spec/logic_depends_on_spec.rb +242 -0
  41. data/spec/logic_spec.rb +207 -0
  42. data/spec/logic_tree_graph_spec.rb +465 -0
  43. data/spec/logic_tree_solver_spec.rb +58 -0
  44. data/spec/meta_spec.rb +12 -2
  45. data/spec/show_data_spec.rb +3 -2
  46. data/spec/spec_helper.rb +22 -6
  47. data/spec/tags/checkboxes_spec.rb +2 -2
  48. data/spec/tags/group_spec.rb +5 -5
  49. data/spec/tags/hours_spec.rb +404 -0
  50. data/spec/tags/radios_spec.rb +2 -2
  51. data/spec/tags/ratings_spec.rb +1 -1
  52. data/spec/tags/select_spec.rb +45 -0
  53. data/spec/tags/tag_spec.rb +25 -0
  54. data/spec/tags/taxonomy_spec.rb +112 -0
  55. data/spec/validation_spec.rb +52 -0
  56. metadata +112 -17
  57. data/VERSION +0 -1
data/lib/cml/logic.rb ADDED
@@ -0,0 +1,424 @@
1
+ module CML
2
+ # Build and calculate things based on some CML's dependendency tree as
3
+ # dictated by `only-if` CML logic.
4
+ class LogicTree
5
+ attr_reader :root_nodes, :nodes, :parser, :errors
6
+
7
+ DEPTH_LIMIT = 10
8
+
9
+ def initialize( parser )
10
+ @parser = parser
11
+ @errors = []
12
+ # Hash of Node instances, keyed by tag names.
13
+ @nodes = @parser.tags.inject( {} ) do |memo, tag|
14
+ memo[tag.name] = Node.new( self, tag )
15
+ memo
16
+ end
17
+ end
18
+
19
+ # Recursively determine the depth of a Node based on logic. For example:
20
+ #
21
+ # A
22
+ # / \
23
+ # B C
24
+ # / \
25
+ # D E
26
+ #
27
+ # Which gives depths of:
28
+ #
29
+ # A = 1
30
+ # B, C = 2
31
+ # D, E = 3
32
+ #
33
+ # If we've recursed 10 times, we're probably in a cycle, so we just return
34
+ # a max of 10 here.
35
+ def depth( node, recurse_depth = 1 )
36
+ return 1 unless node.has_logic? && recurse_depth < DEPTH_LIMIT
37
+ depth = 1 + node.depends_on.map do |depended_node|
38
+ self.depth( depended_node, recurse_depth + 1 )
39
+ end.max
40
+ end
41
+
42
+ def max_depth
43
+ @nodes.values.map do |node|
44
+ node.depth
45
+ end.max
46
+ rescue CML::TagLogic::Error, CML::LogicTree::Graph::Error
47
+ # When logic contains errors, return depth of 1 (maintains backwards compatibility)
48
+ 1
49
+ end
50
+
51
+ def has_grouped_logic?
52
+ @nodes.values.any? {|node| node.has_grouped_logic? }
53
+ rescue CML::TagLogic::Error, CML::LogicTree::Graph::Error
54
+ # When logic contains errors, return no grouping (maintains backwards compatibility)
55
+ false
56
+ end
57
+
58
+ def has_liquid_logic?
59
+ @nodes.values.any? {|node| node.has_liquid_logic? }
60
+ end
61
+
62
+ def valid?
63
+ @errors = []
64
+ graph
65
+ true
66
+ rescue CML::TagLogic::Error, CML::LogicTree::Graph::Error => e
67
+ @errors << e.message
68
+ false
69
+ rescue
70
+ @errors << "CML only-if logic cannot be parsed."
71
+ false
72
+ end
73
+
74
+ def graph
75
+ @graph ||= Graph.new(self)
76
+ @graph.structure
77
+ end
78
+
79
+ # A node in CML::LogicTree, containing a CML::Tag in @tag
80
+ class Node
81
+ attr_reader :tag, :tree
82
+
83
+ def initialize( tree, tag )
84
+ @tree = tree
85
+ @tag = tag
86
+ @tag.logic_tree = @tree
87
+ self
88
+ end
89
+
90
+ def depth
91
+ @depth ||= @tree.depth( self )
92
+ end
93
+
94
+ # Hash of tag dependency details (name/is_not/match_key) on (only-if logic)
95
+ def dependencies_on(&block)
96
+ if block_given?
97
+ @tag.dependencies_on_fields(&block)
98
+ else
99
+ @dependencies_on_nodes ||= @tag.dependencies_on_fields
100
+ end
101
+ end
102
+
103
+ # Array of Node instances the tag depends on (only-if logic)
104
+ def depends_on
105
+ @depends_on_nodes ||= @tag.depends_on_fields.map do |field_name|
106
+ @tree.nodes[field_name]
107
+ end.compact.uniq
108
+ end
109
+
110
+ def has_logic?
111
+ self.depends_on.size > 0
112
+ end
113
+
114
+ def has_grouped_logic?
115
+ @tag.depends_on_fields
116
+ !!@tag.has_grouped_logic
117
+ end
118
+
119
+ def has_liquid_logic?
120
+ @tag.depends_on_fields
121
+ !!@tag.has_liquid_logic
122
+ end
123
+ end
124
+ end
125
+
126
+ # Logic behavior included in CML::Parser
127
+ module ParserLogic
128
+ def has_nested_logic?
129
+ self.logic_tree.max_depth > 2
130
+ end
131
+
132
+ def has_grouped_logic?
133
+ self.logic_tree.has_grouped_logic?
134
+ end
135
+
136
+ def has_liquid_logic?
137
+ self.logic_tree.has_liquid_logic?
138
+ end
139
+
140
+ def logic_tree
141
+ @logic_tree ||= CML::LogicTree.new( self )
142
+ end
143
+ end
144
+
145
+ # Logic behavior included in CML::Tag
146
+ module TagLogic
147
+ attr_accessor :logic_tree, :has_grouped_logic, :has_liquid_logic, :errors
148
+
149
+ # These Errors cause CML::Parser#valid? failures; their messages are
150
+ # end-user-facing and should help the user correct their mistakes.
151
+ class Error < StandardError; end
152
+ class NodeNotFound < Error; end
153
+
154
+ # Override in Tags classes that should be omitted from the logic graph.
155
+ def in_logic_graph?
156
+ true
157
+ end
158
+
159
+ def only_if
160
+ @only_if ||= @attrs["only-if"].to_s
161
+ end
162
+
163
+ # For each only-if token, e.g, `omg`, `!omg`, `omg:[0]`, `omg:[wtf]`, etc.,
164
+ # call the block with args field_name, match_key & is_not.
165
+ def each_logic_token_in(logic_expression)
166
+ return unless block_given?
167
+ logic_expression.split( /\+\+|\|\|/ ).each do |logic_token|
168
+ field_name, match_key = logic_token.split( ":" )
169
+
170
+ # prune the possibly-dangling open paren from logic grouping
171
+ field_name.gsub!( /^\(/, '' )
172
+ # prune the possibly-dangling close paren from logic grouping
173
+ match_key.gsub!( /\)$/, '') if match_key
174
+
175
+ # detect NOT logic
176
+ is_not = if /^!/===field_name
177
+ field_name = field_name.gsub( /^!/, '' )
178
+ true
179
+ else
180
+ false
181
+ end
182
+
183
+ yield field_name, match_key, is_not
184
+ end
185
+ end
186
+
187
+ # Generate the full logic structure for an only-if.
188
+ #
189
+ # The optional block can be used as a flat collector of all fields; its return value is ignored.
190
+ #
191
+ def expand_logic(only_if, &block)
192
+ parsed = CML::TagLogic.parse_expression(only_if)
193
+ expand_parsed_expression(parsed, &block)
194
+ end
195
+
196
+ # Generate the full logic structure for a parsed only-if.
197
+ #
198
+ # The optional block can be used as a flat collector of all fields; its return value is ignored.
199
+ #
200
+ def expand_parsed_expression(parsed_expression, &block)
201
+ expanded_tree = {}
202
+ parsed_expression.each_with_index do |expression_array, i|
203
+ combinator, phrase = expression_array
204
+ combinator_key = CML::TagLogic.resolve_combinator(parsed_expression, i)
205
+ branch = expanded_tree[combinator_key] ||= []
206
+
207
+ if Array===phrase
208
+ # Recurse for grouped logic
209
+ branch << expand_parsed_expression(phrase, &block)
210
+ else
211
+ name, descs = describe_logic_token(phrase)
212
+ yield(name, descs) if block_given?
213
+ value ||= descs
214
+ branch << { name => value }
215
+ end
216
+ end
217
+ expanded_tree
218
+ end
219
+
220
+ Or = '||'.freeze
221
+ And = '&&'.freeze
222
+ CombinatorDefault = Or
223
+ CombinatorDict = {
224
+ '||' => Or,
225
+ '++' => And
226
+ }
227
+
228
+ # Return the effective boolean combinator for the given index in the parsed logic.
229
+ #
230
+ def self.resolve_combinator(parsed_expression, i, parent_combinator=nil)
231
+ combinator = parsed_expression[i] && parsed_expression[i][0]
232
+ selected = if (combinator.nil? || combinator.size==0) && parent_combinator
233
+ CombinatorDict[parent_combinator]
234
+ elsif !combinator.nil? && combinator.size>0
235
+ # Use the current phrase's combinator.
236
+ CombinatorDict[combinator]
237
+ else
238
+ if parsed_expression[i+1]
239
+ # Use the next phrase's combinator
240
+ CombinatorDict[ parsed_expression[i+1][0] ]
241
+ else
242
+ CombinatorDefault
243
+ end
244
+ end
245
+ raise(RuntimeError, "Combinator for index #{i} could not be selected from: #{parsed_expression.inspect}") if
246
+ selected.nil? || selected.size==0
247
+ selected
248
+ end
249
+
250
+ CombinatorExp = '(\+\+||\|\||)'.freeze
251
+ OrCombinatorExp = '(\|\||)'.freeze
252
+ GroupExp = '\(([^\(\)]+)\)'.freeze
253
+ AndPhraseExp = '([^\(\)\|]+\+\+[^\(\)\|]+)'.freeze
254
+ TokenExp = '([^\(\)\+\|]+)'.freeze
255
+ PrecedenceRegexp = /#{CombinatorExp}#{GroupExp}|#{CombinatorExp}#{AndPhraseExp}|#{CombinatorExp}#{TokenExp}/
256
+ TokenRegexp = /#{CombinatorExp}#{TokenExp}/
257
+
258
+ def self.parse_expression(logic_expression)
259
+ parsed_expression = []
260
+ logic_expression.scan( PrecedenceRegexp ) do |group_combinator, group_phrase, and_combinator, and_phrase, combinator, token|
261
+ if group_phrase
262
+ parsed_expression << [group_combinator, parse_expression(group_phrase)]
263
+ elsif and_phrase
264
+ ands = []
265
+ and_phrase.scan( TokenRegexp ) do |precedence_combinator, precedence_token|
266
+ ands << [precedence_combinator, precedence_token]
267
+ end
268
+ parsed_expression << [and_combinator, ands]
269
+ else
270
+ parsed_expression << [combinator, token]
271
+ end
272
+ end
273
+ parsed_expression
274
+ end
275
+
276
+ # A hash of the Tag's dependencies with "only-if" logic.
277
+ #
278
+ # Resolves indexed logic selector keys (e.g. `only-if="pick_a_number:[0]"`)
279
+ # into actual values from parents.
280
+ #
281
+ # The optional block can be used as a flat collector of all fields; its return value is ignored.
282
+ #
283
+ def dependencies_on_fields(&block)
284
+ return @dependencies_on_fields if @dependencies_on_fields && !block_given?
285
+ @dependencies_on_fields = {}
286
+
287
+ keep_merge!(dependencies_through_cml_group(&block), @dependencies_on_fields)
288
+ return @dependencies_on_fields if only_if.empty? || detect_liquid_logic(only_if) || @logic_tree.nil?
289
+
290
+ detect_grouped_logic(only_if)
291
+
292
+ expanded = expand_logic(only_if, &block)
293
+
294
+ keep_merge!(expanded, @dependencies_on_fields)
295
+ @dependencies_on_fields
296
+ end
297
+
298
+ # Returns array of field names that this tag depends on with "only-if"
299
+ # logic.
300
+ def depends_on_fields
301
+ return @depends_on_fields if @depends_on_fields
302
+ @depends_on_fields = []
303
+
304
+ dependencies_on_fields do |name, descs|
305
+ @depends_on_fields << name
306
+ end
307
+
308
+ @depends_on_fields
309
+ end
310
+
311
+ # The <cml::group> only-if logic dependencies
312
+ #
313
+ # The optional block can be used as a flat collector of all fields; its return value is ignored.
314
+ #
315
+ def dependencies_through_cml_group(&block)
316
+ return @dependencies_through_cml_group if @dependencies_through_cml_group
317
+ @dependencies_through_cml_group = {}
318
+
319
+ @logic_tree.parser.each_cml_group_descendant do |group_node, cml_tag, i|
320
+ next if group_node.attributes['only-if'].nil? ||
321
+ group_node.attributes['only-if'].value.size <= 0 ||
322
+ self != cml_tag
323
+ group_only_if = group_node.attributes['only-if'].value.to_s
324
+
325
+ expanded = expand_logic(group_only_if, &block)
326
+ keep_merge!(expanded, @dependencies_through_cml_group)
327
+ end
328
+
329
+ @dependencies_through_cml_group
330
+ end
331
+
332
+ # For an only-if value, e.g, `omg`, `!omg`, `omg:[0]`, `omg:[wtf]`, etc.,
333
+ # return an array of the referenced name & an array of hashes of logic properties.
334
+ def describe_logic_token(logic_token)
335
+ field_name, match_key = logic_token.split( ":" )
336
+
337
+ # detect NOT logic
338
+ is_not = if /^!/===field_name
339
+ field_name = field_name.gsub( /^!/, '' )
340
+ true
341
+ else
342
+ false
343
+ end
344
+
345
+ unless @logic_tree.nodes[field_name]
346
+ raise CML::TagLogic::NodeNotFound, "CML element '#{name}' contains only-if logic that references a missing field '#{field_name}'."
347
+ end
348
+
349
+ descs = []
350
+
351
+ if match_key
352
+ # unwrap the match key from it's square brackets
353
+ match_key = match_key.gsub( /^\[([^\]]+)\]$/, "\\1")
354
+
355
+ # when an integer index, set `match_key` to the literal value in the parent tag
356
+ if /^\d+$/===match_key
357
+ unless @logic_tree.nodes[field_name].tag.children
358
+ raise CML::TagLogic::NodeNotFound, "CML element '#{name}' contains only-if logic that references a child index, '#{field_name}:[#{match_key}]', but '#{field_name}' contains no child elements."
359
+ end
360
+ if (tag_at_index = @logic_tree.nodes[field_name].tag.children[$~.to_s.to_i])
361
+ descs << { :is_not => is_not, :match_key => tag_at_index.value }
362
+ end
363
+
364
+ # when 'unchecked' logic "is not the checkbox's value or any of the checkboxes/checkbox values";
365
+ # is_not logic gets inverted instead of just setting true, just incase we're notting a not
366
+ elsif 'unchecked'==match_key
367
+ if @logic_tree.nodes[field_name].tag.tag == 'checkbox'
368
+ checkbox = @logic_tree.nodes[field_name].tag
369
+ descs << { :is_not => !is_not, :match_key => checkbox.value }
370
+ else
371
+ unless @logic_tree.nodes[field_name].tag.children
372
+ raise CML::TagLogic::NodeNotFound, "CML element '#{name}' contains only-if logic that references a checked value, '#{field_name}:#{match_key}', but '#{field_name}' is not a checkbox nor contains checkboxes."
373
+ end
374
+ @logic_tree.nodes[field_name].tag.children.each do |child|
375
+ next unless child.tag == 'checkbox'
376
+ descs << { :is_not => !is_not, :match_key => child.value }
377
+ end
378
+ end
379
+ end
380
+ end
381
+
382
+ if descs.empty?
383
+ # When matcher is nil, invert the is_not logic to be semantically accurate.
384
+ #
385
+ # i.e. a nil match key actually means "is not blank,"
386
+ # while a negated nil matcher actually means "is blank"
387
+ #
388
+ is_not_with_nil = match_key.nil? ? !is_not : is_not
389
+ descs << { :is_not => is_not_with_nil, :match_key => match_key }
390
+ end
391
+
392
+ [field_name, descs]
393
+ end
394
+
395
+ # Detect parenthetical grouping in only-if logic expressions
396
+ def detect_grouped_logic(only_if)
397
+ @errors ||= []
398
+ if /\([^\(\)]+\)/===only_if
399
+ @has_grouped_logic = true
400
+ end
401
+ end
402
+
403
+ # Detect Liquid tags in only-if logic expressions
404
+ def detect_liquid_logic(only_if)
405
+ @errors ||= []
406
+ if /\{\{/===only_if
407
+ @errors << "The logic tree cannot be constructed when only-if contains a Liquid tag: #{only_if}"
408
+ @has_liquid_logic = true
409
+ end
410
+ end
411
+
412
+
413
+ def keep_merge!(hash, target)
414
+ hash.keys.each do |key|
415
+ if hash[key].is_a? Hash and self[key].is_a? Hash
416
+ target[key] = target[key].keep_merge(hash[key])
417
+ next
418
+ end
419
+ target.update(hash) { |key, *values| values.flatten.uniq }
420
+ end
421
+ target
422
+ end
423
+ end
424
+ end
@@ -0,0 +1,107 @@
1
+ module CML
2
+ class LogicTree
3
+
4
+ # Generates a DAG, directed acyclic graph of the logic tree.
5
+ class Graph
6
+
7
+ # These Errors cause CML::Parser#valid? failures; their messages are
8
+ # end-user-facing and should help the user correct their mistakes.
9
+ class Error < StandardError; end
10
+ class UnresolvableDependency < Error; end
11
+
12
+ attr_accessor :logic_tree, :structure
13
+
14
+ def initialize(logic_tree)
15
+ raise(ArgumentError, "Requires a CML::LogicTree, instead got #{logic_tree.inspect}") unless CML::LogicTree===logic_tree
16
+ @logic_tree = logic_tree
17
+ create_structure
18
+ end
19
+
20
+ # Generate a hash of field names with hashes of properties, e.g.:
21
+ # {
22
+ # "omg"=>{
23
+ # :outbound_count=>1, :outbound_names=>["w_t_f"] },
24
+ # "w_t_f"=>{
25
+ # :inbound_count=>1, :inbound_names=>["omg"], :inbound_matches=>{"||" => [{"omg"=>[{:match_key => nil, :is_not=>true}]}]} }
26
+ # }
27
+ #
28
+ # For inbound matches, the match key tests the value of the remote vertex. Boolean combinators are represented here.
29
+ #
30
+ def create_structure
31
+ # First, inbound links.
32
+ @structure = map_dependencies do |field, cml_tag, dependency_names, dependencies|
33
+ if dependencies
34
+ dependency_names.uniq!
35
+ { :inbound_count => dependency_names.size, :inbound_names => dependency_names, :inbound_matches => dependencies }
36
+ else
37
+ {}
38
+ end
39
+ end
40
+ # Map outbound links by iterating over each node's inbound.
41
+ @structure.each do |vertex, props|
42
+ next if props.nil? || props[:inbound_names].nil?
43
+ props[:inbound_names].each do |inbound_name|
44
+ x = @structure[inbound_name]
45
+ c, n = x[:outbound_count], x[:outbound_names]
46
+ x[:outbound_names] = (n ? n<<vertex : [vertex]).sort.uniq
47
+ x[:outbound_count] = x[:outbound_names].size
48
+ end
49
+ end
50
+ @structure
51
+ end
52
+
53
+ # Returns a hash of the logic tree's nodes.
54
+ #
55
+ # When a block is given, each node's entry is set to the return value of the block.
56
+ #
57
+ def map_dependencies
58
+ structure = {}
59
+ deferred_vertex_procs = []
60
+ deferred_vertex_counts = Hash.new(0)
61
+ created_vertex_for_fields = []
62
+ @logic_tree.nodes.select {|k,v| v.tag.in_logic_graph? }.each do |field, cml_tag|
63
+
64
+ # This proc is used to generate each vertex.
65
+ map_vertex = lambda do |cml_tag, dependency_names, dependencies|
66
+ if block_given?
67
+ structure[field] = yield(field, cml_tag, dependency_names, dependencies)
68
+ else
69
+ structure[field] = { :cml_tag => cml_tag, :dependency_names => dependency_names, :dependencies => dependencies }
70
+ end
71
+ created_vertex_for_fields << field
72
+ end
73
+
74
+ # Expand dependencies & also collect a simple list of dependency names.
75
+ dependency_names = []
76
+ dependencies = cml_tag.dependencies_on {|name, descs| dependency_names << name }
77
+
78
+ # Defer when a CML tag has unsatisfied dependencies, or just generate its vertex.
79
+ if dependency_names.empty?
80
+ map_vertex.call(cml_tag, nil, nil)
81
+ else
82
+ deferred_vertex_procs << lambda do |this_proc|
83
+ if dependency_names.all? {|name| created_vertex_for_fields.include? name }
84
+ map_vertex.call(cml_tag, dependency_names, dependencies)
85
+ else
86
+ # Push onto bottom of stack to try again last.
87
+ raise(UnresolvableDependency, "CML element '#{field}' contains invalid only-if logic.") if
88
+ deferred_vertex_counts[field] > DEPTH_LIMIT
89
+ deferred_vertex_counts[field] += 1
90
+ deferred_vertex_procs.unshift(this_proc)
91
+ end
92
+ end
93
+ end
94
+ end
95
+
96
+ # Iterate through deferred dependencies, filling out the structure as dependencies are satisfied.
97
+ while !deferred_vertex_procs.empty? do
98
+ p = deferred_vertex_procs.pop
99
+ p.call(p) # pass the Proc itself, in case it needs to defer again
100
+ end
101
+
102
+ structure
103
+ end
104
+
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,43 @@
1
+ module CML
2
+ class LogicTree
3
+
4
+ # A utility to solve the logic structures provided by the logic tree.
5
+ #
6
+ class Solver
7
+
8
+ # Traverse the tree in logic order, until a solution is found.
9
+ #
10
+ # A block is required to process each node, returning nil/false or a solution.
11
+ #
12
+ # Returns one value for a single-OR solution or an array for multiple-AND solutions.
13
+ #
14
+ def self.recurse(dependencies, dependent_name, &block)
15
+ solution = nil
16
+ dependencies.each do |k,v|
17
+ s = case k
18
+ when '||'
19
+ ss = nil
20
+ v.detect {|vv| ss = recurse(vv, dependent_name, &block) }
21
+ ss if ss
22
+ when '&&'
23
+ catch(:unfullfilled_and) do
24
+ v.collect do |vv|
25
+ rr = recurse(vv, dependent_name, &block)
26
+ throw(:unfullfilled_and) unless rr
27
+ rr
28
+ end
29
+ end
30
+ else
31
+ yield(dependent_name, k, v[0]) # for a non-boolean node, use the first & only value
32
+ end
33
+ if s
34
+ solution = s
35
+ break
36
+ end
37
+ end
38
+ solution
39
+ end
40
+
41
+ end
42
+ end
43
+ end
data/lib/cml/parser.rb CHANGED
@@ -1,7 +1,14 @@
1
1
  module CML
2
2
  class Parser
3
- attr_reader :doc, :cftags, :tags, :errors
4
- BASE_TAGS_XPATH = ".//cml:*[not(ancestor::cml:checkboxes or ancestor::cml:ratings or ancestor::cml:select or ancestor::cml:radios or self::cml:gold or self::cml:instructions)]"
3
+
4
+ include CML::ParserLogic
5
+
6
+ attr_reader :doc, :cftags, :tags, :errors, :cml_tag_map
7
+
8
+ TAG_XPATH = '//cml:*'
9
+ BASE_TAGS_XPATH = ".#{TAG_XPATH}[not(ancestor::cml:checkboxes or ancestor::cml:ratings or ancestor::cml:select or ancestor::cml:radios or self::cml:gold or self::cml:instructions)]"
10
+ GROUP_XPATH = "//cml:group"
11
+ GROUP_DESCENDANTS_XPATH = ".#{GROUP_XPATH}#{TAG_XPATH}"
5
12
 
6
13
  def initialize(content, opts = {})
7
14
  @opts = opts.merge(:parser => self)
@@ -16,8 +23,10 @@ module CML
16
23
  #Pull all cml tags that aren't children
17
24
  @cftags = @doc.xpath(BASE_TAGS_XPATH, "xmlns:cml"=>"http://crowdflower.com")
18
25
  normalize if opts[:normalize]
26
+ @cml_tag_map = {}
19
27
  @tags = @cftags.map do |t|
20
- t = Parser.tag_class(t.name).new(t, @opts)
28
+ cml_tag = Parser.tag_class(t.name).new(t, @opts)
29
+ @cml_tag_map[t.object_id] = cml_tag
21
30
  end.flatten
22
31
  end
23
32
 
@@ -31,13 +40,31 @@ module CML
31
40
  def convert(opts = nil)
32
41
  @opts.merge!(opts) if opts
33
42
  cloned = @doc.dup
34
- cloned.xpath(BASE_TAGS_XPATH, "xmlns:cml"=>"http://crowdflower.com").each_with_index do |t,i|
35
- t.namespace = nil
36
- t.replace(@tags[i].convert(opts))
43
+
44
+ base_nodes = cloned.xpath(BASE_TAGS_XPATH, "xmlns:cml"=>"http://crowdflower.com")
45
+ group_children_nodes = cloned.xpath(GROUP_DESCENDANTS_XPATH, "xmlns:cml"=>"http://crowdflower.com")
46
+
47
+ (base_nodes - group_children_nodes).each_with_index do |node, i|
48
+ node.namespace = nil
49
+ real_index = base_nodes.index( node )
50
+ node.replace(self.tags[real_index].convert(opts))
37
51
  end
38
52
  cloned
39
53
  end
40
54
 
55
+ # Do something for each <cml::group> descendant "base" CML::Tag
56
+ def each_cml_group_descendant
57
+ return unless block_given?
58
+ group_nodes = @doc.xpath(".#{GROUP_XPATH}", "xmlns:cml"=>"http://crowdflower.com")
59
+ group_nodes.each do |group_node|
60
+ group_descendant_nodes = group_node.xpath(BASE_TAGS_XPATH, "xmlns:cml"=>"http://crowdflower.com")
61
+ group_descendant_nodes.each_with_index do |node, i|
62
+ cml_tag = @cml_tag_map[node.object_id]
63
+ yield group_node, cml_tag, i
64
+ end
65
+ end
66
+ end
67
+
41
68
  def normalize
42
69
  @cftags.each do |t|
43
70
  if ["radios", "select"].include?(t.name)
@@ -59,6 +86,16 @@ module CML
59
86
  @fields
60
87
  end
61
88
 
89
+ def finite_fields
90
+ return @finite_fields if @finite_fields
91
+ @finite_fields = {}
92
+ @tags.each do |t|
93
+ next unless t.finite_value?
94
+ @finite_fields[t.name] = t
95
+ end
96
+ @finite_fields
97
+ end
98
+
62
99
  def golds(rich = false, opts={})
63
100
  unless @golds
64
101
  @golds = @tags.map do |tag|
@@ -108,6 +145,9 @@ module CML
108
145
  @errors << "#{c} a child of #{t.to_s.split("\n")[0]} has a duplicated value, please specify a unique value attribute."
109
146
  end
110
147
  end
148
+ if !logic_tree.valid?
149
+ @errors += logic_tree.errors
150
+ end
111
151
  @errors.length == 0
112
152
  end
113
153
 
@@ -151,7 +191,7 @@ module CML
151
191
  gsub( />/, "&gt;" ).
152
192
  gsub( /"/, "&quot;" )
153
193
  end
154
-
194
+
155
195
  #This takes the name of the tag and converts it to the appropriate tag class
156
196
  def self.tag_class(name)
157
197
  CML::TagClasses[name] || CML::Tags::Unknown