volt 0.6.5 → 0.7.0

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