volt 0.7.1 → 0.7.2

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