volt 0.6.5 → 0.7.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 (145) hide show
  1. checksums.yaml +4 -4
  2. data/Readme.md +47 -40
  3. data/VERSION +1 -1
  4. data/app/volt/controllers/notices_controller.rb +3 -3
  5. data/app/volt/tasks/live_query/data_store.rb +2 -2
  6. data/app/volt/tasks/live_query/live_query.rb +20 -20
  7. data/app/volt/tasks/live_query/live_query_pool.rb +6 -6
  8. data/app/volt/tasks/live_query/query_tracker.rb +15 -15
  9. data/app/volt/tasks/query_tasks.rb +13 -13
  10. data/app/volt/tasks/store_tasks.rb +7 -7
  11. data/app/volt/views/notices/index.html +17 -18
  12. data/lib/volt/assets/test.rb +2 -2
  13. data/lib/volt/benchmark/benchmark.rb +25 -23
  14. data/lib/volt/cli/asset_compile.rb +11 -0
  15. data/lib/volt/cli/new_gem.rb +16 -16
  16. data/lib/volt/cli.rb +14 -12
  17. data/lib/volt/console.rb +5 -6
  18. data/lib/volt/controllers/model_controller.rb +18 -18
  19. data/lib/volt/extra_core/array.rb +4 -4
  20. data/lib/volt/extra_core/hash.rb +3 -3
  21. data/lib/volt/extra_core/object.rb +6 -6
  22. data/lib/volt/extra_core/string.rb +6 -6
  23. data/lib/volt/extra_core/symbol.rb +5 -5
  24. data/lib/volt/extra_core/time.rb +4 -4
  25. data/lib/volt/extra_core/true_false.rb +6 -6
  26. data/lib/volt/extra_core/try.rb +9 -9
  27. data/lib/volt/models/array_model.rb +26 -26
  28. data/lib/volt/models/model.rb +35 -35
  29. data/lib/volt/models/model_hash_behaviour.rb +15 -15
  30. data/lib/volt/models/model_helpers.rb +8 -8
  31. data/lib/volt/models/model_wrapper.rb +6 -6
  32. data/lib/volt/models/persistors/array_store.rb +36 -36
  33. data/lib/volt/models/persistors/base.rb +6 -6
  34. data/lib/volt/models/persistors/flash.rb +5 -5
  35. data/lib/volt/models/persistors/model_identity_map.rb +2 -2
  36. data/lib/volt/models/persistors/model_store.rb +22 -22
  37. data/lib/volt/models/persistors/params.rb +3 -3
  38. data/lib/volt/models/persistors/query/query_listener.rb +14 -14
  39. data/lib/volt/models/persistors/query/query_listener_pool.rb +2 -2
  40. data/lib/volt/models/persistors/store.rb +8 -8
  41. data/lib/volt/models/persistors/store_factory.rb +2 -2
  42. data/lib/volt/models/url.rb +37 -37
  43. data/lib/volt/page/bindings/attribute_binding.rb +14 -14
  44. data/lib/volt/page/bindings/base_binding.rb +9 -9
  45. data/lib/volt/page/bindings/component_binding.rb +7 -7
  46. data/lib/volt/page/bindings/content_binding.rb +3 -3
  47. data/lib/volt/page/bindings/each_binding.rb +13 -13
  48. data/lib/volt/page/bindings/event_binding.rb +4 -4
  49. data/lib/volt/page/bindings/if_binding.rb +12 -12
  50. data/lib/volt/page/bindings/template_binding.rb +30 -30
  51. data/lib/volt/page/channel.rb +19 -19
  52. data/lib/volt/page/channel_stub.rb +6 -6
  53. data/lib/volt/page/document.rb +2 -2
  54. data/lib/volt/page/document_events.rb +4 -4
  55. data/lib/volt/page/draw_cycle.rb +3 -3
  56. data/lib/volt/page/memory_test.rb +6 -6
  57. data/lib/volt/page/page.rb +19 -19
  58. data/lib/volt/page/reactive_template.rb +9 -9
  59. data/lib/volt/page/sub_context.rb +5 -5
  60. data/lib/volt/page/targets/attribute_section.rb +9 -9
  61. data/lib/volt/page/targets/attribute_target.rb +3 -3
  62. data/lib/volt/page/targets/base_section.rb +2 -2
  63. data/lib/volt/page/targets/binding_document/component_node.rb +23 -23
  64. data/lib/volt/page/targets/binding_document/html_node.rb +2 -2
  65. data/lib/volt/page/targets/dom_section.rb +40 -38
  66. data/lib/volt/page/targets/dom_target.rb +2 -2
  67. data/lib/volt/page/tasks.rb +12 -12
  68. data/lib/volt/page/template_renderer.rb +4 -4
  69. data/lib/volt/page/url_tracker.rb +6 -6
  70. data/lib/volt/reactive/array_extensions.rb +2 -2
  71. data/lib/volt/reactive/destructive_methods.rb +5 -5
  72. data/lib/volt/reactive/event_chain.rb +25 -25
  73. data/lib/volt/reactive/events.rb +33 -33
  74. data/lib/volt/reactive/object_tracker.rb +21 -21
  75. data/lib/volt/reactive/object_tracking.rb +2 -2
  76. data/lib/volt/reactive/reactive_array.rb +57 -57
  77. data/lib/volt/reactive/reactive_tags.rb +16 -16
  78. data/lib/volt/reactive/reactive_value.rb +72 -72
  79. data/lib/volt/reactive/string_extensions.rb +3 -3
  80. data/lib/volt/router/routes.rb +22 -23
  81. data/lib/volt/server/component_handler.rb +5 -5
  82. data/lib/volt/server/component_templates.rb +14 -11
  83. data/lib/volt/server/html_parser/attribute_scope.rb +116 -0
  84. data/lib/volt/server/html_parser/each_scope.rb +18 -0
  85. data/lib/volt/server/html_parser/if_view_scope.rb +71 -0
  86. data/lib/volt/server/html_parser/sandlebars_parser.rb +219 -0
  87. data/lib/volt/server/html_parser/textarea_scope.rb +31 -0
  88. data/lib/volt/server/html_parser/view_handler.rb +82 -0
  89. data/lib/volt/server/html_parser/view_parser.rb +23 -0
  90. data/lib/volt/server/html_parser/view_scope.rb +145 -0
  91. data/lib/volt/server/rack/asset_files.rb +17 -17
  92. data/lib/volt/server/rack/component_paths.rb +18 -18
  93. data/lib/volt/server/rack/index_files.rb +8 -8
  94. data/lib/volt/server/rack/opal_files.rb +11 -11
  95. data/lib/volt/server/socket_connection_handler.rb +13 -13
  96. data/lib/volt/server/socket_connection_handler_stub.rb +2 -2
  97. data/lib/volt/server.rb +18 -18
  98. data/lib/volt/tasks/dispatcher.rb +5 -5
  99. data/lib/volt/utils/ejson.rb +2 -2
  100. data/lib/volt/utils/generic_counting_pool.rb +8 -8
  101. data/lib/volt/utils/generic_pool.rb +16 -16
  102. data/lib/volt/volt/environment.rb +4 -4
  103. data/lib/volt.rb +6 -6
  104. data/spec/integration/test_integration_spec.rb +2 -2
  105. data/spec/models/event_chain_spec.rb +38 -38
  106. data/spec/models/model_spec.rb +128 -128
  107. data/spec/models/old_model_spec.rb +17 -17
  108. data/spec/models/persistors/params_spec.rb +3 -3
  109. data/spec/models/persistors/store_spec.rb +7 -7
  110. data/spec/models/reactive_array_spec.rb +82 -82
  111. data/spec/models/reactive_generator_spec.rb +11 -11
  112. data/spec/models/reactive_tags_spec.rb +6 -6
  113. data/spec/models/reactive_value_spec.rb +70 -70
  114. data/spec/models/store_spec.rb +4 -4
  115. data/spec/models/string_extensions_spec.rb +13 -13
  116. data/spec/page/bindings/content_binding_spec.rb +6 -6
  117. data/spec/page/sub_context_spec.rb +1 -1
  118. data/spec/router/routes_spec.rb +3 -3
  119. data/spec/server/html_parser/sample_page.html +595 -0
  120. data/spec/server/html_parser/sandlebars_parser_spec.rb +192 -0
  121. data/spec/server/html_parser/view_parser_spec.rb +286 -0
  122. data/spec/server/rack/asset_files_spec.rb +6 -6
  123. data/spec/server/rack/component_paths_spec.rb +5 -5
  124. data/spec/spec_helper.rb +4 -5
  125. data/spec/store/mongo_spec.rb +3 -3
  126. data/spec/tasks/live_query_spec.rb +6 -6
  127. data/spec/tasks/query_tasks.rb +4 -4
  128. data/spec/tasks/query_tracker_spec.rb +20 -20
  129. data/spec/templates/targets/binding_document/component_node_spec.rb +4 -4
  130. data/spec/templates/template_binding_spec.rb +28 -28
  131. data/spec/utils/generic_counting_pool_spec.rb +5 -5
  132. data/spec/utils/generic_pool_spec.rb +14 -14
  133. data/templates/newgem/app/newgem/views/index/index.html +1 -2
  134. data/templates/project/app/home/config/dependencies.rb +1 -1
  135. data/templates/project/app/home/controllers/index_controller.rb +1 -1
  136. data/templates/project/app/home/views/index/about.html +4 -6
  137. data/templates/project/app/home/views/index/home.html +4 -5
  138. data/templates/project/app/home/views/index/index.html +8 -9
  139. data/templates/project/spec/spec_helper.rb +1 -1
  140. metadata +17 -8
  141. data/lib/volt/server/binding_setup.rb +0 -2
  142. data/lib/volt/server/if_binding_setup.rb +0 -31
  143. data/lib/volt/server/scope.rb +0 -43
  144. data/lib/volt/server/template_parser.rb +0 -453
  145. data/spec/server/template_parser_spec.rb +0 -50
@@ -0,0 +1,116 @@
1
+ # Included into ViewScope to provide processing for attributes
2
+ module AttributeScope
3
+ # Take the attributes and create any bindings
4
+ def process_attributes(tag_name, attributes)
5
+ new_attributes = attributes.dup
6
+
7
+ attributes.each_pair do |name, value|
8
+ if name[0..1] == 'e-'
9
+ process_event_binding(tag_name, new_attributes, name, value)
10
+ else
11
+ process_attribute(tag_name, new_attributes, name, value)
12
+ end
13
+ end
14
+
15
+ return new_attributes
16
+ end
17
+
18
+ def process_event_binding(tag_name, attributes, name, value)
19
+ id = add_id_to_attributes(attributes)
20
+
21
+ event = name[2..-1]
22
+
23
+ if tag_name == 'a'
24
+ # For links, we need to add blank href to make it clickable.
25
+ attributes['href'] ||= ''
26
+ end
27
+
28
+ # Remove the e- attribute
29
+ attributes.delete(name)
30
+
31
+ save_binding(id, "lambda { |__p, __t, __c, __id| EventBinding.new(__p, __t, __c, __id, #{event.inspect}, Proc.new {|event| #{value} })}")
32
+ end
33
+
34
+ def process_attribute(tag_name, attributes, attribute_name, value)
35
+ parts = value.split(/(\{[^\}]+\})/).reject(&:blank?)
36
+ binding_count = parts.count {|p| p[0] == '{' && p[-1] == '}'}
37
+
38
+ # if this attribute has bindings
39
+ if binding_count > 0
40
+ # Setup an id
41
+ id = add_id_to_attributes(attributes)
42
+
43
+ if parts.size > 1
44
+ # Multiple bindings
45
+ add_multiple_attribute(tag_name, id, attribute_name, parts, value)
46
+ elsif parts.size == 1 && binding_count == 1
47
+ # A single binding
48
+ add_single_attribute(id, attribute_name, parts)
49
+ end
50
+
51
+ # Remove the attribute
52
+ attributes.delete(attribute_name)
53
+ end
54
+ end
55
+
56
+ # Add an attribute binding on the tag, bind directly to the getter in the binding
57
+ def add_single_attribute(id, attribute_name, parts)
58
+ getter = parts[0][1..-2]
59
+
60
+ save_binding(id, "lambda { |__p, __t, __c, __id| AttributeBinding.new(__p, __t, __c, __id, #{attribute_name.inspect}, Proc.new { #{getter} }) }")
61
+ end
62
+
63
+
64
+ def add_multiple_attribute(tag_name, id, attribute_name, parts, content)
65
+ case attribute_name
66
+ when 'checked', 'value'
67
+ if parts.size > 1
68
+ if tag_name == 'textarea'
69
+ raise "The content of text area's can not be bound to multiple bindings."
70
+ else
71
+ # Multiple ReactiveValue's can not be passed to value or checked attributes.
72
+ raise "Multiple bindings can not be passed to a #{attribute_name} binding."
73
+ end
74
+ end
75
+ end
76
+
77
+ reactive_template_path = add_reactive_template(content)
78
+
79
+ save_binding(id, "lambda { |__p, __t, __c, __id| AttributeBinding.new(__p, __t, __c, __id, #{attribute_name.inspect}, Proc.new { ReactiveTemplate.new(__p, __c, #{reactive_template_path.inspect}) }) }")
80
+ end
81
+
82
+ def add_reactive_template(content)
83
+ path = @path + "/_rv#{@binding_number}"
84
+ new_handler = ViewHandler.new(path, false)
85
+ @binding_number += 1
86
+
87
+ SandlebarsParser.new(content, new_handler)
88
+
89
+ # Close out the last scope
90
+ new_handler.scope.last.close_scope
91
+
92
+ # Copy in the templates from the new handler
93
+ new_handler.templates.each_pair do |key, value|
94
+ @handler.templates[key] = value
95
+ end
96
+
97
+ return path
98
+ end
99
+
100
+ def add_id_to_attributes(attributes)
101
+ id = attributes['id'] ||= "id#{@binding_number}"
102
+ @binding_number += 1
103
+
104
+ return id.to_s
105
+ end
106
+
107
+ def attribute_string(attributes)
108
+ attr_str = attributes.map {|v| "#{v[0]}=\"#{v[1]}\"" }.join(' ')
109
+ if attr_str.size > 0
110
+ # extra space
111
+ attr_str = " " + attr_str
112
+ end
113
+
114
+ return attr_str
115
+ end
116
+ end
@@ -0,0 +1,18 @@
1
+ class EachScope < ViewScope
2
+ def initialize(handler, path, content)
3
+ super(handler, path)
4
+ @content, @variable_name = content.strip.split(/ as /)
5
+ end
6
+
7
+ def close_scope
8
+ binding_number = @handler.scope[-2].binding_number
9
+ @handler.scope[-2].binding_number += 1
10
+ @path += "/__template/#{binding_number}"
11
+
12
+ super
13
+
14
+ @handler.html << "<!-- $#{binding_number} --><!-- $/#{binding_number} -->"
15
+ @handler.scope.last.save_binding(binding_number, "lambda { |__p, __t, __c, __id| EachBinding.new(__p, __t, __c, __id, Proc.new { #{@content} }, #{@variable_name.inspect}, #{@path.inspect}) }")
16
+
17
+ end
18
+ end
@@ -0,0 +1,71 @@
1
+ class IfViewScope < ViewScope
2
+ def initialize(handler, path, content)
3
+ super(handler, path)
4
+
5
+ @original_path = @path
6
+
7
+ @last_content = content
8
+ @branches = []
9
+
10
+ # We haven't added the if yet
11
+ @if_binding_number = @handler.last.binding_number
12
+ @handler.last.binding_number += 1
13
+
14
+ @path_number = 0
15
+
16
+ new_path
17
+ end
18
+
19
+ def new_path
20
+ @path = @original_path + "/__if#{@path_number}"
21
+ @path_number += 1
22
+ end
23
+
24
+ # When we reach an else block, we basically commit the current html
25
+ # and template, and start a new one.
26
+ def add_else(content)
27
+ close_scope(false)
28
+
29
+ @last_content = content
30
+
31
+ # Clear existing
32
+ @html = ''
33
+ @bindings = {}
34
+
35
+ # Close scope removes us, so lets add it back.
36
+ @handler.scope << self
37
+
38
+ @binding_number = 0
39
+
40
+ # Generate a new template path for this section.
41
+ new_path
42
+ end
43
+
44
+ def close_scope(final=true)
45
+ @branches << [@last_content, path]
46
+
47
+ super()
48
+
49
+ if final
50
+ # Add the binding to the parent
51
+ branches = @branches.map do |branch|
52
+ content = branch[0]
53
+ if content == nil
54
+ content = nil.inspect
55
+ else
56
+ content = "Proc.new { #{branch[0]} }"
57
+ end
58
+
59
+ "[#{content}, #{branch[1].inspect}]"
60
+ end.join(', ')
61
+
62
+ new_scope = @handler.last
63
+
64
+ # variables are captured for branches, so we must prefix them so they don't conflict.
65
+ # page, target, context, id
66
+ new_scope.save_binding(@if_binding_number, "lambda { |__p, __t, __c, __id| IfBinding.new(__p, __t, __c, __id, [#{branches}]) }")
67
+
68
+ new_scope.html << "<!-- $#{@if_binding_number} --><!-- $/#{@if_binding_number} -->"
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,219 @@
1
+ require 'strscan'
2
+
3
+ class HTMLParseError < RuntimeError
4
+ end
5
+ # Parses html and bindings
6
+ # based on http://ejohn.org/files/htmlparser.js
7
+ #
8
+ # takes the html and a handler object that will have the following methods
9
+ # called as each is seen: comment, text, binding, start_tag, end_tag
10
+ #
11
+ # This is not a full html parser, but should cover most common cases.
12
+ class SandlebarsParser
13
+ def self.truth_hash(array)
14
+ hash = {}
15
+ array.each {|v| hash[v] = true }
16
+
17
+ return hash
18
+ end
19
+
20
+ # regex matchers
21
+ START_TAG = /^<([-!\:A-Za-z0-9_]+)((?:\s+[\w\-]+(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))?)*)\s*(\/?)>/
22
+ END_TAG = /^<\/([-!\:A-Za-z0-9_]+)[^>]*>/
23
+ ATTRIBUTES = /([-\:A-Za-z0-9_]+)(?:\s*=\s*(?:(?:"((?:\\.|[^"])*)")|(?:'((?:\\.|[^'])*)')|([^>\s]+)))?/
24
+
25
+ # Types of elements
26
+ BLOCK = truth_hash(%w{address applet blockquote button center dd del dir div dl dt fieldset form frameset hr iframe ins isindex li map menu noframes noscript object ol p pre script table tbody td tfoot th thead tr ul})
27
+ EMPTY = truth_hash(%w{area base basefont br col frame hr img input isindex link meta param embed})
28
+ INLINE = truth_hash(%w{a abbr acronym applet b basefont bdo big br button cite code del dfn em font i iframe img input ins kbd label map object q s samp script select small span strike strong sub sup textarea tt u var})
29
+ CLOSE_SELF = truth_hash(%w{colgroup dd dt li options p td tfoot th thead tr})
30
+ SPECIAL = truth_hash(%w{script style})
31
+
32
+ FILL_IN_ATTRIBUTES = truth_hash(%w{checked compact declare defer disabled ismap multiple nohref noresize noshade nowrap readonly selected})
33
+
34
+ def initialize(html, handler, file_path=nil)
35
+ @html = StringScanner.new(html)
36
+ @handler = handler
37
+ @file_path = file_path
38
+
39
+ @stack = []
40
+
41
+ parse
42
+ end
43
+
44
+ def last
45
+ @stack.last
46
+ end
47
+
48
+ def parse
49
+ loop do
50
+ if last && SPECIAL[last]
51
+ # In a script or style tag, just look for the first end
52
+ close_tag = "</#{last}>"
53
+ body = @html.scan_until(/#{close_tag}/)
54
+ body = body[0..((-1 * close_tag.size)-1)]
55
+
56
+ body = body.gsub(/\<\!--(.*?)--\>/, "\\1").gsub(/\<\!\[CDATA\[(.*?)\]\]\>/, "\\1")
57
+
58
+ text(body)
59
+
60
+ end_tag(last, last)
61
+ elsif @html.scan(/\<\!--/)
62
+ # start comment
63
+ comment = @html.scan_until(/--\>/)
64
+ comment = comment[0..-4]
65
+
66
+ @handler.comment(comment) if @handler.respond_to?(:comment)
67
+ elsif (tag = @html.scan(START_TAG))
68
+ tag_name = @html[1]
69
+ rest = @html[2]
70
+ unary = @html[3]
71
+
72
+ start_tag(tag, tag_name, rest, unary)
73
+ elsif @html.scan(END_TAG)
74
+ tag_name = @html[1]
75
+
76
+ end_tag(tag_name, tag_name)
77
+ elsif (escaped = @html.scan(/\{\{\{(.*?)\}\}\}([^\}]|$)/))
78
+ # Anything between {{{ and }}} is escaped and not processed (treaded as text)
79
+ if escaped[-1] != '}'
80
+ # Move back if we matched a new non } for close, skip if we hit the end
81
+ @html.pos = @html.pos - 1
82
+ end
83
+
84
+ text(@html[1])
85
+ elsif (binding = @html.scan(/\{/))
86
+ # We are in text mode and matched the start of a binding
87
+ start_binding
88
+ elsif (text = @html.scan(/(?:[^\<\{]+)/))
89
+ # matched text up until the next html tag
90
+ text(text)
91
+ else
92
+ # Nothing left
93
+ break
94
+ end
95
+ end
96
+
97
+ end_tag(nil, nil)
98
+ end
99
+
100
+ def text(text)
101
+ @handler.text(text) if @handler.respond_to?(:text)
102
+ end
103
+
104
+ # Findings the end of a binding
105
+ def start_binding
106
+ binding = ''
107
+ open_count = 1
108
+
109
+ # scan until we reach a { or }
110
+ loop do
111
+ binding << @html.scan_until(/([\{\}\n]|\Z)/)
112
+
113
+ match = @html[1]
114
+ if match == '}'
115
+ # close
116
+ open_count -= 1
117
+ break if open_count == 0
118
+ elsif match == '{'
119
+ # open more
120
+ open_count += 1
121
+ elsif match == "\n" || @html.eos?
122
+ # Starting new tag, should be closed before this
123
+ # or end of doc before closed binding
124
+ raise_parse_error("unclosed binding: {#{binding.strip}")
125
+ else
126
+ raise "should not reach here"
127
+ end
128
+ end
129
+
130
+ binding = binding[0..-2]
131
+ @handler.binding(binding) if @handler.respond_to?(:binding)
132
+ end
133
+
134
+ def raise_parse_error(error)
135
+ line_number = @html.pre_match.count("\n") + 1
136
+
137
+ error_str = error + " on line: #{line_number}"
138
+ error_str += " of #{@file_path}" if @file_path
139
+
140
+ raise HTMLParseError, error_str
141
+ end
142
+
143
+ def start_tag(tag, tag_name, rest, unary)
144
+ section_tag = tag_name[0] == ':' && tag_name[1] =~ /[A-Z]/
145
+
146
+ tag_name = tag_name.downcase
147
+
148
+ # handle doctype so we get it output exactly the same way
149
+ if tag_name == '!doctype'
150
+ @handler.text(tag) if @handler.respond_to?(:start_tag)
151
+ return
152
+ end
153
+
154
+ # Auto-close the last inline tag if we started a new block
155
+ if BLOCK[tag_name]
156
+ if last && INLINE[last]
157
+ end_tag(nil, last)
158
+ end
159
+ end
160
+
161
+ # Some tags close themselves when a new one of themselves is reached.
162
+ # ex, a tr will close the previous tr
163
+ if CLOSE_SELF[tag_name] && last == tag_name
164
+ end_tag(nil, tag_name)
165
+ end
166
+
167
+ unary = EMPTY[tag_name] || !unary.blank?
168
+
169
+ # Section tag's are also unary
170
+ unless unary || section_tag
171
+ @stack.push(tag_name)
172
+ end
173
+
174
+ if @handler.respond_to?(:start_tag)
175
+ attributes = {}
176
+
177
+ # Take the rest string and extract the attributes, filling in any
178
+ # "fill in" attribute values if not provided.
179
+ rest.scan(ATTRIBUTES).each do |match|
180
+ name = match[0]
181
+
182
+ value = match[1] || match[2] || match[3] || FILL_IN_ATTRIBUTES[name] || ''
183
+
184
+ attributes[name] = value
185
+ end
186
+
187
+ if section_tag
188
+ @handler.start_section(tag_name, attributes, unary)
189
+ else
190
+ @handler.start_tag(tag_name, attributes, unary)
191
+ end
192
+ end
193
+ end
194
+
195
+ def end_tag(tag, tag_name)
196
+ # If no tag name is provided, close all the way up
197
+ new_size = 0
198
+
199
+ if tag
200
+ # Find the closest tag that closes.
201
+ (@stack.size-1).downto(0) do |index|
202
+ if @stack[index] == tag_name
203
+ new_size = index
204
+ break
205
+ end
206
+ end
207
+ end
208
+
209
+ if new_size >= 0
210
+ if @handler.respond_to?(:end_tag)
211
+ (@stack.size-1).downto(new_size) do |index|
212
+ @handler.end_tag(@stack[index])
213
+ end
214
+ end
215
+
216
+ @stack = @stack[0...new_size]
217
+ end
218
+ end
219
+ end
@@ -0,0 +1,31 @@
1
+ class TextareaScope < ViewScope
2
+ def initialize(handler, path, attributes)
3
+ super(handler, path)
4
+
5
+ @attributes = attributes
6
+ end
7
+
8
+ def add_binding(content)
9
+ @html << "{#{content}}"
10
+ end
11
+
12
+ def close_scope(pop=true)
13
+ # Remove from the scope
14
+ @handler.scope.pop
15
+
16
+ attributes = @attributes
17
+
18
+ if @html[/\{[^\}]+\}/]
19
+ # If the html inside the textarea has a binding, process it as
20
+ # a value attribute.
21
+ attributes['value'] = @html
22
+ @html = ''
23
+ end
24
+
25
+ # Normal tag
26
+ attributes = @handler.last.process_attributes('textarea', attributes)
27
+
28
+ @handler.last.html << "<textarea#{attribute_string(attributes)}>#{@html}</textarea>"
29
+
30
+ end
31
+ end
@@ -0,0 +1,82 @@
1
+ class ViewHandler
2
+ attr_reader :templates, :scope
3
+
4
+ def html
5
+ last.html
6
+ end
7
+
8
+ def last
9
+ @scope.last
10
+ end
11
+
12
+ def initialize(initial_path, allow_sections=true)
13
+ @original_path = initial_path
14
+
15
+ # Default to the body section
16
+ initial_path += '/body' if allow_sections
17
+
18
+ @scope = [ViewScope.new(self, initial_path)]
19
+ @templates = {}
20
+ end
21
+
22
+ def comment(comment)
23
+ last << "<!--#{comment}-->"
24
+ end
25
+
26
+ def text(text)
27
+ last << text
28
+ end
29
+
30
+ def binding(binding)
31
+ @scope.last.add_binding(binding)
32
+ end
33
+
34
+ def start_tag(tag_name, attributes, unary)
35
+ case tag_name[0]
36
+ when ':'
37
+ # Component
38
+ last.add_component(tag_name, attributes, unary)
39
+ else
40
+ if tag_name == 'textarea'
41
+ @in_textarea = true
42
+ last.add_textarea(tag_name, attributes, unary)
43
+ else
44
+
45
+ # Normal tag
46
+ attributes = last.process_attributes(tag_name, attributes)
47
+ attr_str = last.attribute_string(attributes)
48
+
49
+ last << "<#{tag_name}#{attr_str}#{unary ? ' /' : ''}>"
50
+ end
51
+ end
52
+ end
53
+
54
+ def end_tag(tag_name)
55
+ if @in_textarea && tag_name == 'textarea'
56
+ last.close_scope
57
+ @in_textarea = nil
58
+ else
59
+ last << "</#{tag_name}>"
60
+ end
61
+ end
62
+
63
+ def start_section(tag_name, attributes, unary)
64
+ path = last.path
65
+ # Start of section
66
+ if @in_section
67
+ # Close any previous sections
68
+ last.close_scope
69
+ else
70
+ # This is the first time we've hit a section header, everything
71
+ # outside of the headers should be removed
72
+ @templates = {}
73
+ end
74
+
75
+ @in_section = tag_name[1..-1]
76
+
77
+ # Set the new path to include the section
78
+ new_path = @original_path + '/' + @in_section
79
+ @scope = [ViewScope.new(self, new_path)]
80
+ end
81
+
82
+ end
@@ -0,0 +1,23 @@
1
+ require 'volt/server/html_parser/sandlebars_parser'
2
+ require 'volt/server/html_parser/view_scope'
3
+ require 'volt/server/html_parser/if_view_scope'
4
+ require 'volt/server/html_parser/view_handler'
5
+ require 'volt/server/html_parser/each_scope'
6
+ require 'volt/server/html_parser/textarea_scope'
7
+
8
+ class ViewParser
9
+ attr_reader :templates
10
+
11
+ def initialize(html, template_path)
12
+ @template_path = template_path
13
+
14
+ handler = ViewHandler.new(template_path)
15
+
16
+ SandlebarsParser.new(html, handler)
17
+
18
+ # Close out the last scope
19
+ handler.scope.last.close_scope
20
+
21
+ @templates = handler.templates
22
+ end
23
+ end
@@ -0,0 +1,145 @@
1
+ require 'volt/server/html_parser/attribute_scope'
2
+
3
+ class ViewScope
4
+ include AttributeScope
5
+
6
+ attr_reader :html, :bindings
7
+ attr_accessor :path, :binding_number
8
+
9
+ def initialize(handler, path)
10
+ @handler = handler
11
+ @path = path
12
+
13
+ @html = ''
14
+ @bindings = {}
15
+ @binding_number = 0
16
+ end
17
+
18
+ def <<(html)
19
+ @html << html
20
+ end
21
+
22
+ def add_binding(content)
23
+ case content[0]
24
+ when '#'
25
+ command, *content = content.split(/ /)
26
+ content = content.join(' ')
27
+
28
+ case command
29
+ when '#if'
30
+ add_if(content)
31
+ when '#elsif'
32
+ add_else(content)
33
+ when '#else'
34
+ if content.blank?
35
+ add_else(nil)
36
+ else
37
+ raise "#else does not take a conditional #{content} was provided."
38
+ end
39
+ when '#each'
40
+ add_each(content)
41
+ when '#template'
42
+ add_template(content)
43
+ end
44
+ when '/'
45
+ # close binding
46
+ close_scope
47
+ else
48
+ # content
49
+ add_content_binding(content)
50
+ end
51
+ end
52
+
53
+ def add_content_binding(content)
54
+ @handler.html << "<!-- $#{@binding_number} --><!-- $/#{@binding_number} -->"
55
+ save_binding(@binding_number, "lambda { |__p, __t, __c, __id| ContentBinding.new(__p, __t, __c, __id, Proc.new { #{content} }) }")
56
+ @binding_number += 1
57
+ end
58
+
59
+ def add_if(content)
60
+ # Add with path for if group.
61
+ @handler.scope << IfViewScope.new(@handler, @path + "/__ifg#{@binding_number}", content)
62
+ @binding_number += 1
63
+ end
64
+
65
+ def add_else(content)
66
+ raise "#else can only be added inside of an if block"
67
+ end
68
+
69
+ def add_each(content)
70
+ @handler.scope << EachScope.new(@handler, @path, content)
71
+ end
72
+
73
+ def add_template(content)
74
+ @handler.html << "<!-- $#{@binding_number} --><!-- $/#{@binding_number} -->"
75
+ save_binding(@binding_number, "lambda { |__p, __t, __c, __id| TemplateBinding.new(__p, __t, __c, __id, #{@path.inspect}, Proc.new { [#{content}] }) }")
76
+
77
+ @binding_number += 1
78
+ end
79
+
80
+ def add_component(tag_name, attributes, unary)
81
+ component_name = tag_name[1..-1].gsub(':', '/')
82
+
83
+ @handler.html << "<!-- $#{@binding_number} --><!-- $/#{@binding_number} -->"
84
+
85
+ data_hash = []
86
+ attributes.each_pair do |name, value|
87
+ parts = value.split(/(\{[^\}]+\})/).reject(&:blank?)
88
+ binding_count = parts.count {|p| p[0] == '{' && p[-1] == '}'}
89
+
90
+ # if this attribute has bindings
91
+ if binding_count > 0
92
+ if binding_count > 1
93
+ # Multiple bindings
94
+ elsif parts.size == 1 && binding_count == 1
95
+ # A single binding
96
+ data_hash << "#{name.inspect} => Proc.new { #{value[1..-2]} }"
97
+ end
98
+ else
99
+ # String
100
+ data_hash << "#{name.inspect} => #{value.inspect}"
101
+ end
102
+ end
103
+
104
+ arguments = "#{component_name.inspect}, { #{data_hash.join(',')} }"
105
+
106
+ save_binding(@binding_number, "lambda { |__p, __t, __c, __id| ComponentBinding.new(__p, __t, __c, __id, #{@path.inspect}, Proc.new { [#{arguments}] }) }")
107
+
108
+ @binding_number += 1
109
+ end
110
+
111
+ def add_textarea(tag_name, attributes, unary)
112
+ @handler.scope << TextareaScope.new(@handler, @path + "/__txtarea#{@binding_number}", attributes)
113
+ @binding_number += 1
114
+
115
+ # close right away if unary
116
+ @handler.last.close_scope if unary
117
+ end
118
+
119
+ # Called when this scope should be closed out
120
+ def close_scope(pop=true)
121
+ if pop
122
+ scope = @handler.scope.pop
123
+ else
124
+ scope = @handler.last
125
+ end
126
+
127
+ raise "template path already exists: #{scope.path}" if @handler.templates[scope.path]
128
+
129
+ template = {
130
+ 'html' => scope.html
131
+ }
132
+
133
+ if scope.bindings.size > 0
134
+ # Add the bindings if there are any
135
+ template['bindings'] = scope.bindings
136
+ end
137
+
138
+ @handler.templates[scope.path] = template
139
+ end
140
+
141
+ def save_binding(binding_number, code)
142
+ @bindings[binding_number] ||= []
143
+ @bindings[binding_number] << code
144
+ end
145
+ end