volt 0.7.1 → 0.7.2

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 (103) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +1 -2
  3. data/Readme.md +97 -56
  4. data/VERSION +1 -1
  5. data/app/volt/assets/js/sockjs-0.3.4.min.js +27 -0
  6. data/app/volt/assets/js/vertxbus.js +216 -0
  7. data/app/volt/tasks/live_query/live_query.rb +5 -5
  8. data/app/volt/tasks/live_query/live_query_pool.rb +1 -1
  9. data/app/volt/tasks/query_tasks.rb +5 -0
  10. data/app/volt/tasks/store_tasks.rb +44 -18
  11. data/docs/WHY.md +10 -0
  12. data/lib/volt/cli.rb +18 -7
  13. data/lib/volt/controllers/model_controller.rb +30 -8
  14. data/lib/volt/extra_core/inflections.rb +63 -0
  15. data/lib/volt/extra_core/inflector/inflections.rb +203 -0
  16. data/lib/volt/extra_core/inflector/methods.rb +63 -0
  17. data/lib/volt/extra_core/inflector.rb +4 -0
  18. data/lib/volt/extra_core/object.rb +9 -0
  19. data/lib/volt/extra_core/string.rb +10 -14
  20. data/lib/volt/models/array_model.rb +45 -27
  21. data/lib/volt/models/cursor.rb +6 -0
  22. data/lib/volt/models/model.rb +127 -12
  23. data/lib/volt/models/model_hash_behaviour.rb +8 -5
  24. data/lib/volt/models/model_helpers.rb +4 -4
  25. data/lib/volt/models/model_state.rb +22 -0
  26. data/lib/volt/models/persistors/array_store.rb +49 -35
  27. data/lib/volt/models/persistors/base.rb +3 -3
  28. data/lib/volt/models/persistors/model_store.rb +17 -6
  29. data/lib/volt/models/persistors/query/query_listener.rb +0 -2
  30. data/lib/volt/models/persistors/store.rb +0 -4
  31. data/lib/volt/models/persistors/store_state.rb +27 -0
  32. data/lib/volt/models/url.rb +2 -2
  33. data/lib/volt/models/validations/errors.rb +0 -0
  34. data/lib/volt/models/validations/length.rb +13 -0
  35. data/lib/volt/models/validations/validations.rb +82 -0
  36. data/lib/volt/models.rb +1 -1
  37. data/lib/volt/page/bindings/attribute_binding.rb +29 -14
  38. data/lib/volt/page/bindings/base_binding.rb +2 -2
  39. data/lib/volt/page/bindings/component_binding.rb +29 -25
  40. data/lib/volt/page/bindings/content_binding.rb +1 -0
  41. data/lib/volt/page/bindings/each_binding.rb +25 -33
  42. data/lib/volt/page/bindings/event_binding.rb +0 -1
  43. data/lib/volt/page/bindings/if_binding.rb +3 -1
  44. data/lib/volt/page/bindings/template_binding.rb +61 -28
  45. data/lib/volt/page/document_events.rb +3 -1
  46. data/lib/volt/page/draw_cycle.rb +22 -0
  47. data/lib/volt/page/page.rb +10 -1
  48. data/lib/volt/page/reactive_template.rb +23 -16
  49. data/lib/volt/page/sub_context.rb +1 -1
  50. data/lib/volt/page/targets/attribute_section.rb +3 -2
  51. data/lib/volt/page/targets/attribute_target.rb +0 -4
  52. data/lib/volt/page/targets/base_section.rb +25 -0
  53. data/lib/volt/page/targets/binding_document/component_node.rb +13 -14
  54. data/lib/volt/page/targets/binding_document/html_node.rb +4 -0
  55. data/lib/volt/page/targets/dom_section.rb +16 -67
  56. data/lib/volt/page/targets/dom_template.rb +99 -0
  57. data/lib/volt/page/targets/helpers/comment_searchers.rb +29 -0
  58. data/lib/volt/page/template_renderer.rb +2 -14
  59. data/lib/volt/reactive/array_extensions.rb +0 -1
  60. data/lib/volt/reactive/event_chain.rb +9 -2
  61. data/lib/volt/reactive/events.rb +44 -37
  62. data/lib/volt/reactive/object_tracking.rb +1 -1
  63. data/lib/volt/reactive/reactive_array.rb +18 -0
  64. data/lib/volt/reactive/reactive_count.rb +108 -0
  65. data/lib/volt/reactive/reactive_generator.rb +44 -0
  66. data/lib/volt/reactive/reactive_value.rb +73 -73
  67. data/lib/volt/reactive/string_extensions.rb +1 -1
  68. data/lib/volt/router/routes.rb +205 -88
  69. data/lib/volt/server/component_handler.rb +3 -1
  70. data/lib/volt/server/html_parser/view_parser.rb +20 -4
  71. data/lib/volt/server/rack/component_paths.rb +13 -10
  72. data/lib/volt/server/rack/index_files.rb +4 -4
  73. data/lib/volt/server/socket_connection_handler.rb +5 -1
  74. data/lib/volt/server.rb +10 -3
  75. data/spec/apps/kitchen_sink/.gitignore +8 -0
  76. data/spec/apps/kitchen_sink/Gemfile +32 -0
  77. data/spec/apps/kitchen_sink/app/home/views/index/index.html +3 -5
  78. data/spec/apps/kitchen_sink/config.ru +4 -0
  79. data/spec/apps/kitchen_sink/public/index.html +2 -2
  80. data/spec/extra_core/inflector_spec.rb +8 -0
  81. data/spec/models/event_chain_spec.rb +18 -0
  82. data/spec/models/model_buffers_spec.rb +9 -0
  83. data/spec/models/model_spec.rb +22 -9
  84. data/spec/models/reactive_array_spec.rb +26 -1
  85. data/spec/models/reactive_call_times_spec.rb +28 -0
  86. data/spec/models/reactive_value_spec.rb +19 -0
  87. data/spec/models/validations_spec.rb +39 -0
  88. data/spec/page/bindings/content_binding_spec.rb +1 -0
  89. data/spec/{templates → page/bindings}/template_binding_spec.rb +54 -0
  90. data/spec/router/routes_spec.rb +156 -8
  91. data/spec/server/html_parser/sandlebars_parser_spec.rb +55 -47
  92. data/spec/server/html_parser/view_parser_spec.rb +3 -0
  93. data/spec/server/rack/asset_files_spec.rb +1 -1
  94. data/spec/spec_helper.rb +25 -11
  95. data/spec/templates/targets/binding_document/component_node_spec.rb +12 -0
  96. data/templates/project/Gemfile.tt +11 -0
  97. data/templates/project/app/home/config/routes.rb +1 -1
  98. data/templates/project/app/home/controllers/index_controller.rb +5 -5
  99. data/templates/project/app/home/views/index/index.html +6 -6
  100. data/volt.gemspec +5 -6
  101. metadata +34 -76
  102. data/app/volt/assets/js/sockjs-0.2.1.min.js +0 -27
  103. data/lib/volt/reactive/object_tracker.rb +0 -107
@@ -3,8 +3,8 @@ require 'volt/reactive/reactive_tags'
3
3
  require 'volt/reactive/string_extensions'
4
4
  require 'volt/reactive/array_extensions'
5
5
  require 'volt/reactive/reactive_array'
6
- require 'volt/reactive/object_tracker'
7
6
  require 'volt/reactive/destructive_methods'
7
+ require 'volt/reactive/reactive_generator'
8
8
 
9
9
  class Object
10
10
  def cur
@@ -29,10 +29,11 @@ class ReactiveValue < BasicObject
29
29
  # :methods- needs to return a straight up array to work with irb tab completion
30
30
  # :eql? - needed for .uniq to work correctly
31
31
  # :to_ary - in some places ruby expects to get an array back from this method
32
- SKIP_METHODS = [:hash, :methods, :eql?, :respond_to?, :respond_to_missing?, :to_ary, :to_int]#, :instance_of?, :kind_of?, :to_s, :to_str]
32
+ SKIP_METHODS = [:object_id, :hash, :methods, :eql?, :respond_to?, :respond_to_missing?, :to_ary, :to_int]#, :instance_of?, :kind_of?, :to_s, :to_str]
33
33
 
34
34
  def initialize(getter, setter=nil, scope=nil)
35
- @reactive_manager = ::ReactiveManager.new(getter, setter, scope)
35
+ @reactive_manager = ::ReactiveManager.new(self, getter, setter, scope)
36
+ # @reactive_cache = {}
36
37
  end
37
38
 
38
39
  def reactive?
@@ -42,7 +43,7 @@ class ReactiveValue < BasicObject
42
43
  # Proxy methods to the ReactiveManager. We want to have as few
43
44
  # as possible methods on reactive values, so all other methods
44
45
  # are forwarded to the object the reactive value points to.
45
- [:cur, :cur=, :deep_cur, :on, :trigger!, :trigger_by_scope!].each do |method_name|
46
+ [:cur, :cur=, :deep_cur, :on, :trigger!, :trigger_by_scope!, :with].each do |method_name|
46
47
  define_method(method_name) do |*args, &block|
47
48
  @reactive_manager.send(method_name, *args, &block)
48
49
  end
@@ -80,6 +81,9 @@ class ReactiveValue < BasicObject
80
81
  method_name, *args = args
81
82
  end
82
83
 
84
+ # result = @reactive_cache[[method_name, args.map(&:object_id)]]
85
+ # return result if result
86
+
83
87
  # For some methods, we pass directly to the current object. This
84
88
  # helps ReactiveValue's be well behaved ruby citizens.
85
89
  # Also skip if this is a destructive method
@@ -109,6 +113,11 @@ class ReactiveValue < BasicObject
109
113
 
110
114
  # result = result.with(block_reactives) if block
111
115
 
116
+ # if args.size == 0 || method_name == :[]
117
+ # puts "STORE: #{method_name} - #{args.inspect}"
118
+ # @reactive_cache[[method_name, args.map(&:object_id)]] = result
119
+ # end
120
+
112
121
  return result
113
122
  end
114
123
 
@@ -135,13 +144,10 @@ class ReactiveValue < BasicObject
135
144
  # end
136
145
 
137
146
  def respond_to_missing?(name, include_private=false)
147
+ puts "RT"
138
148
  cur.respond_to?(name)
139
149
  end
140
150
 
141
- def with(*args, &block)
142
- return @reactive_manager.with(*args, &block)
143
- end
144
-
145
151
  def inspect
146
152
  "@#{cur.inspect}"
147
153
  end
@@ -187,58 +193,14 @@ class ReactiveValue < BasicObject
187
193
  end
188
194
  end
189
195
 
190
- class ReactiveGenerator
191
- # Takes a hash and returns a ReactiveValue that depends on
192
- # any ReactiveValue's inside of the hash (or children).
193
- def self.from_hash(hash, skip_if_no_reactives=false)
194
- reactives = find_reactives(hash)
195
-
196
- if skip_if_no_reactives && reactives.size == 0
197
- # There weren't any reactives, we can just use the hash
198
- return hash
199
- else
200
- # Create a new reactive value that listens on all of its
201
- # child reactive values.
202
- value = ReactiveValue.new(hash)
203
-
204
- reactives.each do |child|
205
- value.reactive_manager.add_parent!(child)
206
- end
207
-
208
- return value
209
- end
210
- end
211
-
212
- # Recursively loop through the data, returning a list of all
213
- # reactive values in the hash, array, etc..
214
- def self.find_reactives(object)
215
- found = []
216
- if object.reactive?
217
- found << object
218
-
219
- found += find_reactives(object.cur)
220
- elsif object.is_a?(Array)
221
- object.each do |item|
222
- found += find_reactives(item)
223
- end
224
- elsif object.is_a?(Hash)
225
- object.each_pair do |key, value|
226
- found += find_reactives(key)
227
- found += find_reactives(value)
228
- end
229
- end
230
-
231
- return found.flatten
232
- end
233
- end
234
-
235
196
  class ReactiveManager
236
197
  include ::Events
237
198
 
238
199
  attr_reader :scope, :parents
239
200
 
240
201
  # When created, ReactiveValue's get a getter (a proc)
241
- def initialize(getter, setter=nil, scope=nil)
202
+ def initialize(reactive_value, getter, setter=nil, scope=nil)
203
+ @reactive_value = reactive_value
242
204
  @getter = getter
243
205
  @setter = setter
244
206
  @scope = scope
@@ -246,6 +208,10 @@ class ReactiveManager
246
208
  @parents = []
247
209
  end
248
210
 
211
+ def reactive_value
212
+ @reactive_value
213
+ end
214
+
249
215
  def reactive?
250
216
  true
251
217
  end
@@ -259,31 +225,28 @@ class ReactiveManager
259
225
  end
260
226
 
261
227
 
262
- def event_added(event, scope, first)
228
+ def event_added(event, scope, first, first_for_event)
263
229
  # When the first event is registered, we need to start listening on our current object
264
230
  # for it to publish events.
265
- object_tracker.enable! if first
231
+
232
+ update_followers if first
266
233
  end
267
234
 
268
- def event_removed(event, last)
235
+ def event_removed(event, last, last_for_event)
269
236
  # If no one is listening on the reactive value, then we don't need to listen on our
270
237
  # current object for events, because no one cares.
271
- object_tracker.disable! if @listeners.size == 0
272
- end
273
238
 
274
- def object_tracker
275
- @object_tracker ||= ::ObjectTracker.new(self)
239
+ remove_followers if last
276
240
  end
277
241
 
278
242
 
279
243
  # Fetch the current value
280
- def cur
281
- # @@cur_count ||= 0
282
- # @@cur_count += 1
283
- # puts "Cur: #{@@cur_count}"# if @@cur_count % 100 == 0
284
- # if ObjectTracker.cache_version == @cached_version
285
- # return @cached_obj
286
- # end
244
+ def cur(shallow=false, ignore_cache=false)
245
+ # Return from cache if it is cached
246
+ if @cur_cache && !shallow && !ignore_cache
247
+ # puts "From Cache: #{@cur_cache.inspect}"
248
+ return @cur_cache
249
+ end
287
250
 
288
251
  if @getter.class == ::Proc
289
252
  # Get the current value, capture any errors
@@ -297,30 +260,67 @@ class ReactiveManager
297
260
  result = @getter
298
261
  end
299
262
 
300
- if result.reactive?
263
+ if !shallow && result.reactive?
301
264
  # Unwrap any stored reactive values
302
265
  result = result.cur
303
266
  end
304
267
 
305
- # if ObjectTracker.cache_enabled
306
- # @cached_obj = result
307
- # @cached_version = ObjectTracker.cache_version
308
- # end
268
+ # puts "CUR FOR: #{result.inspect}"
309
269
 
310
270
  return result
311
271
  end
312
272
 
273
+
274
+ def update_followers
275
+ if has_listeners?
276
+ current_obj = cur(false, true)
277
+ should_attach = current_obj.respond_to?(:on)
278
+ # puts "SA #{should_attach} - #{current_obj.inspect}"
279
+
280
+ if should_attach
281
+ if !@cur_cache || current_obj.object_id != @cur_cache.object_id
282
+ # puts "CHANGED FROM: #{@cur_cache.inspect} to #{current_obj.inspect} - #{current_obj.object_id} vs #{@cur_cache.object_id}"
283
+ remove_followers
284
+
285
+ # puts "SET TO: #{current_obj.inspect} on #{self.inspect}"
286
+ @cur_cache_chain_listener = self.event_chain.add_object(current_obj)
287
+ end
288
+ else
289
+ remove_followers
290
+ end
291
+
292
+ # Store current if we have listeners
293
+ @cur_cache = current_obj
294
+ end
295
+ end
296
+
297
+ def remove_followers
298
+ # puts "REMOVE FOLLOWERS: #{@cur_cache.inspect} on #{self.inspect}"
299
+ # Remove from previous
300
+ if @cur_cache
301
+ @cur_cache = nil
302
+ end
303
+
304
+ if @cur_cache_chain_listener
305
+ @cur_cache_chain_listener.remove
306
+ @cur_cache_chain_listener = nil
307
+ end
308
+ end
309
+
313
310
  def cur=(val)
314
311
  if @setter
315
312
  @setter.call(val)
313
+ # update_followers
316
314
  elsif @scope == nil
317
315
  @getter = val
318
316
  @setter = nil
319
317
 
318
+ # update_followers
320
319
  trigger!('changed')
321
320
  else
322
321
  raise "Value can not be updated"
323
322
  end
323
+
324
324
  end
325
325
 
326
326
  # Returns a copy of the object with where all ReactiveValue's are replaced
@@ -1,5 +1,5 @@
1
1
  class String
2
- include ReactiveTags
2
+ # include ReactiveTags
3
3
 
4
4
  alias :__old_plus :+
5
5
  if RUBY_PLATFORM != 'opal'
@@ -1,14 +1,53 @@
1
1
  require 'volt'
2
2
 
3
- class Routes
4
- attr_reader :routes, :path_matchers
3
+ # The Routes class takes a set of routes and sets up methods to go from
4
+ # a url to params, and params to url.
5
+ # routes do
6
+ # get "/about", _view: 'about'
7
+ # get "/blog/{_id}/edit", _view: 'blog/edit', _action: 'edit'
8
+ # get "/blog/{_id}", _view: 'blog/show', _action: 'show'
9
+ # get "/blog", _view: 'blog'
10
+ # get "/blog/new", _view: 'blog/new', _action: 'new'
11
+ # get "/cool/{_name}", _view: 'cool'
12
+ # end
13
+ #
14
+ # Using the routes above, we would generate the following:
15
+ #
16
+ # @direct_routes = {
17
+ # '/about' => {_view: 'about'},
18
+ # '/blog' => {_view: 'blog'}
19
+ # '/blog/new' => {_view: 'blog/new', _action: 'new'}
20
+ # }
21
+ #
22
+ # -- nil represents a terminal
23
+ # -- * represents any match
24
+ # -- a number for a parameter means use the value in that number section
25
+ #
26
+ # @indirect_routes = {
27
+ # '*' => {
28
+ # 'edit' => {
29
+ # nil => {_id: 1, _view: 'blog/edit', _action: 'edit'}
30
+ # }
31
+ # nil => {_id: 1, _view: 'blog/show', _action: 'show'}
32
+ # }
33
+ # }
34
+ # }
35
+ #
36
+ # Match for params
37
+ # @param_matches = [
38
+ # {_id: nil, _view: 'blog/edit', _action: 'edit'} => Proc.new {|params| "/blog/#{params.id}/edit", params.reject {|k,v| k == :id }}
39
+ # ]
5
40
 
41
+ class Routes
6
42
  def initialize
7
- @routes = []
43
+ # Paths where there are no bindings (an optimization)
44
+ @direct_routes = {}
8
45
 
9
- if Volt.server?
10
- @path_matchers = []
11
- end
46
+ # Paths with bindings
47
+ @indirect_routes = {}
48
+
49
+ # Matcher for going from params to url
50
+ @param_matches = []
12
51
  end
13
52
 
14
53
  def define(&block)
@@ -17,125 +56,203 @@ class Routes
17
56
  return self
18
57
  end
19
58
 
20
- def get(path, options={})
21
- if path.index('{') && path.index('}')
22
- # The path contains bindings.
23
- path = build_path_matcher(path, options)
59
+ # Add a route
60
+ def get(path, params={})
61
+ params = params.symbolize_keys
62
+ if has_binding?(path)
63
+ add_indirect_path(path, params)
24
64
  else
25
- add_path_matcher([path]) if Volt.server?
65
+ @direct_routes[path] = params
26
66
  end
27
67
 
28
- @routes << [path, options]
68
+ add_param_matcher(path, params)
29
69
  end
30
70
 
31
- # Takes the path and splits it up into sections around any
32
- # bindings in the path. Those are then used to create a proc
33
- # that will return the path with the current params in it.
34
- # If it matches it will be used.
35
- def build_path_matcher(path, options)
36
- sections = path.split(/(\{[^\}]+\})/)
37
- sections = sections.reject {|v| v == '' }
38
-
39
- sections.each do |section|
40
- if section[0] == '{' && section[-1] == '}'
41
- options[section[1..-2]] = nil
71
+ # Takes in params and generates a path and the remaining params
72
+ # that should be shown in the url. The extra "unused" params
73
+ # will be tacked onto the end of the url ?param1=value1, etc...
74
+ #
75
+ # returns the url and new params, or nil, nil if no match is found.
76
+ def params_to_url(test_params)
77
+ @param_matches.each do |param_matcher|
78
+ # TODO: Maybe a deep dup?
79
+ result, new_params = check_params_match(test_params.dup, param_matcher[0])
80
+
81
+ if result
82
+ return param_matcher[1].call(new_params)
42
83
  end
43
84
  end
44
85
 
45
- add_path_matcher(sections) if Volt.server?
86
+ return nil, nil
87
+ end
88
+
89
+ # Takes in a path and returns the matching params.
90
+ # returns params as a hash
91
+ def url_to_params(path)
92
+ # First try a direct match
93
+ result = @direct_routes[path]
94
+ return result if result
95
+
96
+ # Next, split the url and walk the sections
97
+ parts = url_parts(path)
98
+
99
+ return match_path(parts, parts, @indirect_routes)
100
+ end
101
+
102
+ private
103
+ # Recursively walk the @indirect_routes hash, return the params for a route, return
104
+ # false for non-matches.
105
+ def match_path(original_parts, remaining_parts, node)
106
+ # Take off the top part and get the rest into a new array
107
+ # part will be nil if we are out of parts (fancy how that works out, now
108
+ # stand in wonder about how much someone thought this through, though
109
+ # really I just got lucky)
110
+ part, *parts = remaining_parts
46
111
 
47
- # Create a path that takes in the params and returns the main
48
- # part of the url with the params filled in.
49
- path = Proc.new do |params|
50
- sections.map do |section|
51
- if section[0] == '{' && section[-1] == '}'
52
- params[section[1..-2]]
112
+ if part == nil
113
+ if node[part]
114
+ # We found a match, replace the bindings and return
115
+ # TODO: Handvle nested
116
+ return setup_bindings_in_params(original_parts, node[part])
53
117
  else
54
- section
118
+ return false
55
119
  end
56
- end.join('')
120
+ elsif (new_node = node[part])
121
+ # Direct match for section, continue
122
+ return match_path(original_parts, parts, new_node)
123
+ elsif (new_node = node['*'])
124
+ # Match on binding section
125
+ return match_path(original_parts, parts, new_node)
126
+ end
57
127
  end
58
128
 
59
- return path
60
- end
129
+ # The params out of match_path will have integers in the params that came from bindings
130
+ # in the url. This replaces those with the values from the url.
131
+ def setup_bindings_in_params(original_parts, params)
132
+ # Create a copy of the params we can modify and return
133
+ params = params.dup
61
134
 
62
- # TODO: This is slow, optimize with a DFA or NFA
63
- def add_path_matcher(sections)
64
- match_path = ''
65
- sections.each do |section|
66
- if section[0] == '{' && section[-1] == '}'
67
- match_path = match_path + "[^\\/]+"
68
- else
69
- match_path = match_path + section
135
+ params.each_pair do |key, value|
136
+ if value.is_a?(Fixnum)
137
+ # Lookup the param's value in the original url parts
138
+ params[key] = original_parts[value]
139
+ end
70
140
  end
141
+
142
+ return params
71
143
  end
72
144
 
73
- @path_matchers << (/^#{match_path}$/)
74
- end
75
145
 
76
- # Takes in params and generates a path and the remaining params
77
- # that should be shown in the url.
78
- def url_for_params(params)
79
- routes.each do |route|
80
- if params_match_options?(params, route[1])
81
- return path_and_params(params, route[0], route[1])
146
+ # Build up the @indirect_routes data structure.
147
+ # '*' means wildcard match anything
148
+ # nil means a terminal, who's value will be the params.
149
+ #
150
+ # In the params, an integer vaule means the index of the wildcard
151
+ def add_indirect_path(path, params)
152
+ node = @indirect_routes
153
+
154
+ parts = url_parts(path)
155
+
156
+ parts.each_with_index do |part, index|
157
+ if has_binding?(part)
158
+ params[part[1..-2].to_sym] = index
159
+
160
+ # Set the part to be '*' (anything matcher)
161
+ part = '*'
162
+ end
163
+
164
+ node = (node[part] ||= {})
82
165
  end
166
+
167
+ node[nil] = params
83
168
  end
84
169
 
85
- return '/', params
86
- end
87
170
 
88
- # Takes in a path and returns the matching params.
89
- # TODO: Slow, need dfa
90
- def params_for_path(path)
91
- routes.each do |route|
92
- # TODO: Finish nested routes
93
- if false && route[0].class == Proc
94
- # puts route[0].call(params).inspect
95
-
96
- return false
97
- elsif route[0] == path
98
- # Found the matching route
99
- return route[1]
171
+ def add_param_matcher(path, params)
172
+ params = params.dup
173
+ parts = url_parts(path)
174
+
175
+ parts.each_with_index do |part, index|
176
+ if has_binding?(part)
177
+ # Setup a nil param that can match anything, but gets
178
+ # assigned into the url
179
+ params[part[1..-2].to_sym] = nil
180
+ end
100
181
  end
182
+
183
+ path_transformer = create_path_transformer(parts)
184
+
185
+ @param_matches << [params, path_transformer]
101
186
  end
102
187
 
103
- return {}
104
- end
188
+ # Takes in url parts and returns a proc that takes in params and returns
189
+ # a url with the bindings filled in, and params with the binding params
190
+ # removed. (So the remaining can be added onto the end of the url ?params1=...)
191
+ def create_path_transformer(parts)
192
+ return lambda do |input_params|
193
+ input_params = input_params.dup
105
194
 
106
- private
107
- def path_and_params(params, path, options)
108
- params = params.attributes.dup
109
- path = path.call(params) if path.class == Proc
195
+ url = parts.map do |part|
196
+ val = if has_binding?(part)
197
+ # Get the
198
+ binding = part[1..-2].to_sym
199
+ input_params.delete(binding)
200
+ else
201
+ part
202
+ end
110
203
 
111
- options.keys.each do |key|
112
- params.delete(key)
113
- end
204
+ val
205
+ end.join('/')
114
206
 
115
- return path, params
207
+ return '/' + url, input_params
208
+ end
116
209
  end
117
210
 
118
- # Match one route against the current params.
119
- def params_match_options?(params, options)
120
- options.each_pair do |key, value|
121
- # If the value is a hash, we have a nested route. Get the
122
- # matching section in the parameter and loop down to check
123
- # the values down.
211
+ # Takes in a hash of params and checks to make sure keys in param_matcher
212
+ # are in test_params. Checks for equal value unless value in param_matcher
213
+ # is nil.
214
+ #
215
+ # returns false or true, new_params - where the new params are a the params not
216
+ # used in the basic match. Later some of these may be inserted into the url.
217
+ def check_params_match(test_params, param_matcher)
218
+ param_matcher.each_pair do |key, value|
124
219
  if value.is_a?(Hash)
125
- sub_params = params.send(key)
220
+ if test_params[key]
221
+ result = check_params_match(test_params[key], value)
126
222
 
127
- if sub_params
128
- return params_match_options?(sub_params, value)
223
+ if result == false
224
+ return false
225
+ else
226
+ test_params.delete(key)
227
+ end
228
+ else
229
+ # test_params did not have matching key
230
+ return false
231
+ end
232
+ elsif value == nil
233
+ unless test_params.has_key?(key)
234
+ return false
235
+ end
236
+ else
237
+ if test_params[key] == value
238
+ test_params.delete(key)
129
239
  else
130
240
  return false
131
241
  end
132
- elsif value != nil && value != params.send(key)
133
- # A nil value means it can match anything, so we don't want to
134
- # fail on nil.
135
- return false
136
242
  end
137
243
  end
138
244
 
139
- return true
245
+ return true, test_params
140
246
  end
247
+
248
+ def url_parts(path)
249
+ return path.split('/').reject(&:blank?)
250
+ end
251
+
252
+
253
+ # Check if a string has a binding in it
254
+ def has_binding?(string)
255
+ string.index('{') && string.index('}')
256
+ end
257
+
141
258
  end
@@ -15,10 +15,12 @@ class ComponentHandler
15
15
 
16
16
  code = ComponentCode.new(component_name, @component_paths).code
17
17
 
18
+ # puts "CODE: #{code}"
19
+
18
20
  javascript_code = Opal.compile(code)
19
21
 
20
22
  # puts "ENV: #{env.inspect}"
21
- [200, {"Content-Type" => "text/html"}, StringIO.new(javascript_code)]
23
+ [200, {"Content-Type" => "text/html; charset=utf-8"}, StringIO.new(javascript_code)]
22
24
  end
23
25
 
24
26
 
@@ -7,17 +7,33 @@ require 'volt/server/html_parser/textarea_scope'
7
7
 
8
8
  class ViewParser
9
9
  attr_reader :templates
10
-
10
+
11
11
  def initialize(html, template_path)
12
12
  @template_path = template_path
13
13
 
14
14
  handler = ViewHandler.new(template_path)
15
-
15
+
16
16
  SandlebarsParser.new(html, handler)
17
-
17
+
18
18
  # Close out the last scope
19
19
  handler.scope.last.close_scope
20
-
20
+
21
21
  @templates = handler.templates
22
22
  end
23
+
24
+ # Returns a parsed version of the data (useful for backend rendering
25
+ # and testing)
26
+ def data
27
+ templates = @templates.deep_clone
28
+
29
+ templates.each_pair do |name, value|
30
+ if value['bindings']
31
+ value['bindings'].each_pair do |number, binding|
32
+ value['bindings'][number] = binding.map {|code| eval(code) }
33
+ end
34
+ end
35
+ end
36
+
37
+ return templates
38
+ end
23
39
  end