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.
- checksums.yaml +4 -4
- data/Readme.md +47 -40
- data/VERSION +1 -1
- data/app/volt/controllers/notices_controller.rb +3 -3
- data/app/volt/tasks/live_query/data_store.rb +2 -2
- data/app/volt/tasks/live_query/live_query.rb +20 -20
- data/app/volt/tasks/live_query/live_query_pool.rb +6 -6
- data/app/volt/tasks/live_query/query_tracker.rb +15 -15
- data/app/volt/tasks/query_tasks.rb +13 -13
- data/app/volt/tasks/store_tasks.rb +7 -7
- data/app/volt/views/notices/index.html +17 -18
- data/lib/volt/assets/test.rb +2 -2
- data/lib/volt/benchmark/benchmark.rb +25 -23
- data/lib/volt/cli/asset_compile.rb +11 -0
- data/lib/volt/cli/new_gem.rb +16 -16
- data/lib/volt/cli.rb +14 -12
- data/lib/volt/console.rb +5 -6
- data/lib/volt/controllers/model_controller.rb +18 -18
- data/lib/volt/extra_core/array.rb +4 -4
- data/lib/volt/extra_core/hash.rb +3 -3
- data/lib/volt/extra_core/object.rb +6 -6
- data/lib/volt/extra_core/string.rb +6 -6
- data/lib/volt/extra_core/symbol.rb +5 -5
- data/lib/volt/extra_core/time.rb +4 -4
- data/lib/volt/extra_core/true_false.rb +6 -6
- data/lib/volt/extra_core/try.rb +9 -9
- data/lib/volt/models/array_model.rb +26 -26
- data/lib/volt/models/model.rb +35 -35
- data/lib/volt/models/model_hash_behaviour.rb +15 -15
- data/lib/volt/models/model_helpers.rb +8 -8
- data/lib/volt/models/model_wrapper.rb +6 -6
- data/lib/volt/models/persistors/array_store.rb +36 -36
- data/lib/volt/models/persistors/base.rb +6 -6
- data/lib/volt/models/persistors/flash.rb +5 -5
- data/lib/volt/models/persistors/model_identity_map.rb +2 -2
- data/lib/volt/models/persistors/model_store.rb +22 -22
- data/lib/volt/models/persistors/params.rb +3 -3
- data/lib/volt/models/persistors/query/query_listener.rb +14 -14
- data/lib/volt/models/persistors/query/query_listener_pool.rb +2 -2
- data/lib/volt/models/persistors/store.rb +8 -8
- data/lib/volt/models/persistors/store_factory.rb +2 -2
- data/lib/volt/models/url.rb +37 -37
- data/lib/volt/page/bindings/attribute_binding.rb +14 -14
- data/lib/volt/page/bindings/base_binding.rb +9 -9
- data/lib/volt/page/bindings/component_binding.rb +7 -7
- data/lib/volt/page/bindings/content_binding.rb +3 -3
- data/lib/volt/page/bindings/each_binding.rb +13 -13
- data/lib/volt/page/bindings/event_binding.rb +4 -4
- data/lib/volt/page/bindings/if_binding.rb +12 -12
- data/lib/volt/page/bindings/template_binding.rb +30 -30
- data/lib/volt/page/channel.rb +19 -19
- data/lib/volt/page/channel_stub.rb +6 -6
- data/lib/volt/page/document.rb +2 -2
- data/lib/volt/page/document_events.rb +4 -4
- data/lib/volt/page/draw_cycle.rb +3 -3
- data/lib/volt/page/memory_test.rb +6 -6
- data/lib/volt/page/page.rb +19 -19
- data/lib/volt/page/reactive_template.rb +9 -9
- data/lib/volt/page/sub_context.rb +5 -5
- data/lib/volt/page/targets/attribute_section.rb +9 -9
- data/lib/volt/page/targets/attribute_target.rb +3 -3
- data/lib/volt/page/targets/base_section.rb +2 -2
- data/lib/volt/page/targets/binding_document/component_node.rb +23 -23
- data/lib/volt/page/targets/binding_document/html_node.rb +2 -2
- data/lib/volt/page/targets/dom_section.rb +40 -38
- data/lib/volt/page/targets/dom_target.rb +2 -2
- data/lib/volt/page/tasks.rb +12 -12
- data/lib/volt/page/template_renderer.rb +4 -4
- data/lib/volt/page/url_tracker.rb +6 -6
- data/lib/volt/reactive/array_extensions.rb +2 -2
- data/lib/volt/reactive/destructive_methods.rb +5 -5
- data/lib/volt/reactive/event_chain.rb +25 -25
- data/lib/volt/reactive/events.rb +33 -33
- data/lib/volt/reactive/object_tracker.rb +21 -21
- data/lib/volt/reactive/object_tracking.rb +2 -2
- data/lib/volt/reactive/reactive_array.rb +57 -57
- data/lib/volt/reactive/reactive_tags.rb +16 -16
- data/lib/volt/reactive/reactive_value.rb +72 -72
- data/lib/volt/reactive/string_extensions.rb +3 -3
- data/lib/volt/router/routes.rb +22 -23
- data/lib/volt/server/component_handler.rb +5 -5
- data/lib/volt/server/component_templates.rb +14 -11
- data/lib/volt/server/html_parser/attribute_scope.rb +116 -0
- data/lib/volt/server/html_parser/each_scope.rb +18 -0
- data/lib/volt/server/html_parser/if_view_scope.rb +71 -0
- data/lib/volt/server/html_parser/sandlebars_parser.rb +219 -0
- data/lib/volt/server/html_parser/textarea_scope.rb +31 -0
- data/lib/volt/server/html_parser/view_handler.rb +82 -0
- data/lib/volt/server/html_parser/view_parser.rb +23 -0
- data/lib/volt/server/html_parser/view_scope.rb +145 -0
- data/lib/volt/server/rack/asset_files.rb +17 -17
- data/lib/volt/server/rack/component_paths.rb +18 -18
- data/lib/volt/server/rack/index_files.rb +8 -8
- data/lib/volt/server/rack/opal_files.rb +11 -11
- data/lib/volt/server/socket_connection_handler.rb +13 -13
- data/lib/volt/server/socket_connection_handler_stub.rb +2 -2
- data/lib/volt/server.rb +18 -18
- data/lib/volt/tasks/dispatcher.rb +5 -5
- data/lib/volt/utils/ejson.rb +2 -2
- data/lib/volt/utils/generic_counting_pool.rb +8 -8
- data/lib/volt/utils/generic_pool.rb +16 -16
- data/lib/volt/volt/environment.rb +4 -4
- data/lib/volt.rb +6 -6
- data/spec/integration/test_integration_spec.rb +2 -2
- data/spec/models/event_chain_spec.rb +38 -38
- data/spec/models/model_spec.rb +128 -128
- data/spec/models/old_model_spec.rb +17 -17
- data/spec/models/persistors/params_spec.rb +3 -3
- data/spec/models/persistors/store_spec.rb +7 -7
- data/spec/models/reactive_array_spec.rb +82 -82
- data/spec/models/reactive_generator_spec.rb +11 -11
- data/spec/models/reactive_tags_spec.rb +6 -6
- data/spec/models/reactive_value_spec.rb +70 -70
- data/spec/models/store_spec.rb +4 -4
- data/spec/models/string_extensions_spec.rb +13 -13
- data/spec/page/bindings/content_binding_spec.rb +6 -6
- data/spec/page/sub_context_spec.rb +1 -1
- data/spec/router/routes_spec.rb +3 -3
- data/spec/server/html_parser/sample_page.html +595 -0
- data/spec/server/html_parser/sandlebars_parser_spec.rb +192 -0
- data/spec/server/html_parser/view_parser_spec.rb +286 -0
- data/spec/server/rack/asset_files_spec.rb +6 -6
- data/spec/server/rack/component_paths_spec.rb +5 -5
- data/spec/spec_helper.rb +4 -5
- data/spec/store/mongo_spec.rb +3 -3
- data/spec/tasks/live_query_spec.rb +6 -6
- data/spec/tasks/query_tasks.rb +4 -4
- data/spec/tasks/query_tracker_spec.rb +20 -20
- data/spec/templates/targets/binding_document/component_node_spec.rb +4 -4
- data/spec/templates/template_binding_spec.rb +28 -28
- data/spec/utils/generic_counting_pool_spec.rb +5 -5
- data/spec/utils/generic_pool_spec.rb +14 -14
- data/templates/newgem/app/newgem/views/index/index.html +1 -2
- data/templates/project/app/home/config/dependencies.rb +1 -1
- data/templates/project/app/home/controllers/index_controller.rb +1 -1
- data/templates/project/app/home/views/index/about.html +4 -6
- data/templates/project/app/home/views/index/home.html +4 -5
- data/templates/project/app/home/views/index/index.html +8 -9
- data/templates/project/spec/spec_helper.rb +1 -1
- metadata +17 -8
- data/lib/volt/server/binding_setup.rb +0 -2
- data/lib/volt/server/if_binding_setup.rb +0 -31
- data/lib/volt/server/scope.rb +0 -43
- data/lib/volt/server/template_parser.rb +0 -453
- 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
|