volt 0.2.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +21 -0
- data/.rspec +2 -0
- data/.ruby-version +1 -0
- data/Gemfile +37 -0
- data/Guardfile +9 -0
- data/LICENSE.txt +22 -0
- data/Rakefile +23 -0
- data/Readme.md +34 -0
- data/VERSION +1 -0
- data/bin/volt +4 -0
- data/docs/GETTING_STARTED.md +7 -0
- data/docs/GUIDE.md +33 -0
- data/lib/volt.rb +15 -0
- data/lib/volt/benchmark/benchmark.rb +25 -0
- data/lib/volt/cli.rb +34 -0
- data/lib/volt/console.rb +19 -0
- data/lib/volt/controllers/model_controller.rb +29 -0
- data/lib/volt/extra_core/array.rb +10 -0
- data/lib/volt/extra_core/blank.rb +88 -0
- data/lib/volt/extra_core/extra_core.rb +7 -0
- data/lib/volt/extra_core/numeric.rb +9 -0
- data/lib/volt/extra_core/object.rb +36 -0
- data/lib/volt/extra_core/string.rb +29 -0
- data/lib/volt/extra_core/stringify_keys.rb +7 -0
- data/lib/volt/extra_core/true_false.rb +44 -0
- data/lib/volt/extra_core/try.rb +31 -0
- data/lib/volt/models.rb +5 -0
- data/lib/volt/models/array_model.rb +37 -0
- data/lib/volt/models/model.rb +210 -0
- data/lib/volt/models/model_wrapper.rb +23 -0
- data/lib/volt/models/params.rb +67 -0
- data/lib/volt/models/url.rb +192 -0
- data/lib/volt/page/url_tracker.rb +36 -0
- data/lib/volt/reactive/array_extensions.rb +13 -0
- data/lib/volt/reactive/event_chain.rb +126 -0
- data/lib/volt/reactive/events.rb +283 -0
- data/lib/volt/reactive/object_tracker.rb +99 -0
- data/lib/volt/reactive/object_tracking.rb +15 -0
- data/lib/volt/reactive/reactive_array.rb +222 -0
- data/lib/volt/reactive/reactive_tags.rb +64 -0
- data/lib/volt/reactive/reactive_value.rb +368 -0
- data/lib/volt/reactive/string_extensions.rb +34 -0
- data/lib/volt/router/routes.rb +83 -0
- data/lib/volt/server.rb +121 -0
- data/lib/volt/server/binding_setup.rb +2 -0
- data/lib/volt/server/channel_handler.rb +31 -0
- data/lib/volt/server/component_handler.rb +88 -0
- data/lib/volt/server/if_binding_setup.rb +29 -0
- data/lib/volt/server/request_handler.rb +16 -0
- data/lib/volt/server/scope.rb +43 -0
- data/lib/volt/server/source_map_server.rb +31 -0
- data/lib/volt/server/template_parser.rb +452 -0
- data/lib/volt/store/mongo.rb +5 -0
- data/lib/volt/templates/attribute_binding.rb +110 -0
- data/lib/volt/templates/base_binding.rb +37 -0
- data/lib/volt/templates/channel.rb +48 -0
- data/lib/volt/templates/content_binding.rb +35 -0
- data/lib/volt/templates/document_events.rb +80 -0
- data/lib/volt/templates/each_binding.rb +115 -0
- data/lib/volt/templates/event_binding.rb +51 -0
- data/lib/volt/templates/if_binding.rb +74 -0
- data/lib/volt/templates/memory_test.rb +26 -0
- data/lib/volt/templates/page.rb +146 -0
- data/lib/volt/templates/reactive_template.rb +38 -0
- data/lib/volt/templates/render_queue.rb +5 -0
- data/lib/volt/templates/sub_context.rb +23 -0
- data/lib/volt/templates/targets/attribute_section.rb +33 -0
- data/lib/volt/templates/targets/attribute_target.rb +18 -0
- data/lib/volt/templates/targets/base_section.rb +14 -0
- data/lib/volt/templates/targets/binding_document/base_node.rb +3 -0
- data/lib/volt/templates/targets/binding_document/component_node.rb +112 -0
- data/lib/volt/templates/targets/binding_document/html_node.rb +11 -0
- data/lib/volt/templates/targets/dom_section.rb +147 -0
- data/lib/volt/templates/targets/dom_target.rb +11 -0
- data/lib/volt/templates/template_binding.rb +159 -0
- data/lib/volt/templates/template_renderer.rb +50 -0
- data/spec/models/event_chain_spec.rb +129 -0
- data/spec/models/model_spec.rb +340 -0
- data/spec/models/old_model_spec.rb +109 -0
- data/spec/models/reactive_array_spec.rb +262 -0
- data/spec/models/reactive_tags_spec.rb +35 -0
- data/spec/models/reactive_value_spec.rb +336 -0
- data/spec/models/string_extensions_spec.rb +57 -0
- data/spec/router/routes_spec.rb +24 -0
- data/spec/server/template_parser_spec.rb +50 -0
- data/spec/spec_helper.rb +20 -0
- data/spec/store/mongo_spec.rb +4 -0
- data/spec/templates/targets/binding_document/component_node_spec.rb +18 -0
- data/spec/templates/template_binding_spec.rb +98 -0
- data/templates/.gitignore +12 -0
- data/templates/Gemfile.tt +8 -0
- data/templates/app/.empty_directory +0 -0
- data/templates/app/home/config/routes.rb +1 -0
- data/templates/app/home/controllers/index_controller.rb +5 -0
- data/templates/app/home/css/.empty_directory +0 -0
- data/templates/app/home/models/.empty_directory +0 -0
- data/templates/app/home/views/index/about.html +9 -0
- data/templates/app/home/views/index/home.html +7 -0
- data/templates/app/home/views/index/index.html +28 -0
- data/templates/config.ru +4 -0
- data/templates/public/css/ansi.css +0 -0
- data/templates/public/css/bootstrap-theme.css +459 -0
- data/templates/public/css/bootstrap.css +7098 -0
- data/templates/public/css/jumbotron.css +79 -0
- data/templates/public/fonts/glyphicons-halflings-regular.eot +0 -0
- data/templates/public/fonts/glyphicons-halflings-regular.svg +229 -0
- data/templates/public/fonts/glyphicons-halflings-regular.ttf +0 -0
- data/templates/public/fonts/glyphicons-halflings-regular.woff +0 -0
- data/templates/public/index.html +25 -0
- data/templates/public/js/bootstrap.js +0 -0
- data/templates/public/js/jquery-2.0.3.js +8829 -0
- data/templates/public/js/sockjs-0.2.1.min.js +27 -0
- data/templates/spec/spec_helper.rb +20 -0
- data/volt.gemspec +41 -0
- metadata +412 -0
@@ -0,0 +1,34 @@
|
|
1
|
+
class String
|
2
|
+
include ReactiveTags
|
3
|
+
|
4
|
+
alias :__old_plus :+
|
5
|
+
if RUBY_PLATFORM != 'opal'
|
6
|
+
alias :__old_concat :<<
|
7
|
+
end
|
8
|
+
# alias :concat :__old_concat
|
9
|
+
|
10
|
+
# In volt, we want a value + reactive strings to return a reactive string. So we
|
11
|
+
# over-ride + to check for when we are adding a reactive string to a string.
|
12
|
+
def +(val)
|
13
|
+
result = __old_plus(val.cur)
|
14
|
+
if val.reactive? && !result.reactive?
|
15
|
+
result = ReactiveValue.new(result)
|
16
|
+
end
|
17
|
+
|
18
|
+
return result
|
19
|
+
end
|
20
|
+
|
21
|
+
if RUBY_PLATFORM != 'opal'
|
22
|
+
tag_method(:<<) do
|
23
|
+
destructive!
|
24
|
+
end
|
25
|
+
def <<(val)
|
26
|
+
if val.reactive?
|
27
|
+
raise "Cannot append a reactive string to non-reactive string. Use + instead"
|
28
|
+
end
|
29
|
+
result = __old_concat(val)
|
30
|
+
|
31
|
+
return result
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
class Routes
|
2
|
+
attr_reader :routes
|
3
|
+
|
4
|
+
def initialize
|
5
|
+
@routes = []
|
6
|
+
end
|
7
|
+
|
8
|
+
def define(&block)
|
9
|
+
instance_eval(&block)
|
10
|
+
|
11
|
+
return self
|
12
|
+
end
|
13
|
+
|
14
|
+
def get(path, options)
|
15
|
+
if path.index(':')
|
16
|
+
sections = path.split(/([:][^:\/]+)/)
|
17
|
+
|
18
|
+
sections.each do |section|
|
19
|
+
if section[0] == ':'
|
20
|
+
options[section[1..-1]] = nil
|
21
|
+
end
|
22
|
+
end
|
23
|
+
path = Proc.new do |params|
|
24
|
+
# Create a path using the params in the path
|
25
|
+
sections.map do |section|
|
26
|
+
if section[0] == ':'
|
27
|
+
params[section[1..-1]]
|
28
|
+
else
|
29
|
+
section
|
30
|
+
end
|
31
|
+
end.join('')
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
@routes << [path, options]
|
36
|
+
end
|
37
|
+
|
38
|
+
# Takes in params and generates a path and the remaining params
|
39
|
+
# that should be shown in the url.
|
40
|
+
def url_for_params(params)
|
41
|
+
routes.each do |route|
|
42
|
+
if params_match_options?(params, route[1])
|
43
|
+
return path_and_params(params, route[0], route[1])
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
return '/', params
|
48
|
+
end
|
49
|
+
|
50
|
+
# Takes in a path and returns the matching params.
|
51
|
+
def params_for_path(path)
|
52
|
+
routes.each do |route|
|
53
|
+
if route[0] == path
|
54
|
+
# Found the matching route
|
55
|
+
return route[1]
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
return {}
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
def path_and_params(params, path, options)
|
64
|
+
params = params.attributes.dup
|
65
|
+
path = path.call(params) if path.class == Proc
|
66
|
+
|
67
|
+
options.keys.each do |key|
|
68
|
+
params.delete(key)
|
69
|
+
end
|
70
|
+
|
71
|
+
return path, params
|
72
|
+
end
|
73
|
+
|
74
|
+
def params_match_options?(params, options)
|
75
|
+
options.each_pair do |key, value|
|
76
|
+
if value == nil || value != params.send(key)
|
77
|
+
return false
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
return true
|
82
|
+
end
|
83
|
+
end
|
data/lib/volt/server.rb
ADDED
@@ -0,0 +1,121 @@
|
|
1
|
+
require 'opal'
|
2
|
+
require "rack"
|
3
|
+
if RUBY_PLATFORM != 'java'
|
4
|
+
require "rack/sockjs"
|
5
|
+
require "eventmachine"
|
6
|
+
end
|
7
|
+
require "sprockets-sass"
|
8
|
+
require "sass"
|
9
|
+
|
10
|
+
require 'volt/extra_core/extra_core'
|
11
|
+
require 'volt/server/request_handler'
|
12
|
+
require 'volt/server/component_handler'
|
13
|
+
if RUBY_PLATFORM != 'java'
|
14
|
+
require 'volt/server/channel_handler'
|
15
|
+
end
|
16
|
+
require 'volt/server/source_map_server'
|
17
|
+
|
18
|
+
class Index
|
19
|
+
|
20
|
+
def initialize(app, files)
|
21
|
+
@app = app
|
22
|
+
@files = files
|
23
|
+
end
|
24
|
+
|
25
|
+
def call(env)
|
26
|
+
if %w[/ /demo /blog /todos /page3 /page4].include?(env['PATH_INFO']) || env['PATH_INFO'][0..5] == '/todos'
|
27
|
+
[200, { 'Content-Type' => 'text/html' }, [html]]
|
28
|
+
else
|
29
|
+
@app.call env
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def html
|
34
|
+
index_path = File.expand_path(File.join(Dir.pwd, "public/index.html"))
|
35
|
+
html = File.read(index_path)
|
36
|
+
|
37
|
+
ERB.new(html).result(binding)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
|
42
|
+
|
43
|
+
SOURCE_MAPS = !!ENV['MAPS']
|
44
|
+
|
45
|
+
Opal::Processor.source_map_enabled = SOURCE_MAPS
|
46
|
+
# Opal::Processor.arity_check_enabled = true
|
47
|
+
# Opal::Processor.dynamic_require_severity = :raise
|
48
|
+
|
49
|
+
|
50
|
+
|
51
|
+
class Server
|
52
|
+
def self.app
|
53
|
+
|
54
|
+
builder = Rack::Builder.new do
|
55
|
+
use Rack::CommonLogger
|
56
|
+
# run RequestHandler.new
|
57
|
+
|
58
|
+
use Rack::ShowExceptions
|
59
|
+
|
60
|
+
map '/components' do
|
61
|
+
run ComponentHandler.new
|
62
|
+
end
|
63
|
+
|
64
|
+
environment = Opal::Environment.new
|
65
|
+
|
66
|
+
app_path = File.expand_path(File.join(Dir.pwd, "app"))
|
67
|
+
environment.append_path(app_path)
|
68
|
+
|
69
|
+
volt_gem_lib_path = File.expand_path(File.join(File.dirname(__FILE__), ".."))
|
70
|
+
environment.append_path(volt_gem_lib_path)
|
71
|
+
|
72
|
+
# Add the opal load paths
|
73
|
+
Opal.paths.each do |path|
|
74
|
+
environment.append_path(path)
|
75
|
+
end
|
76
|
+
|
77
|
+
# opal-jquery gem
|
78
|
+
spec = Gem::Specification.find_by_name("opal-jquery")
|
79
|
+
environment.append_path(spec.gem_dir + "/opal")
|
80
|
+
|
81
|
+
|
82
|
+
map '/assets' do
|
83
|
+
run environment
|
84
|
+
end
|
85
|
+
|
86
|
+
if SOURCE_MAPS
|
87
|
+
source_maps = SourceMapServer.new(environment)
|
88
|
+
|
89
|
+
map(source_maps.prefix) do
|
90
|
+
run source_maps
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
if RUBY_PLATFORM != 'java'
|
95
|
+
map "/channel" do
|
96
|
+
run Rack::SockJS.new(ChannelHandler)#, :websocket => false
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
if SOURCE_MAPS
|
101
|
+
files = environment['volt/templates/page'].to_a.map {|v| v.logical_path }
|
102
|
+
else
|
103
|
+
files = []
|
104
|
+
end
|
105
|
+
|
106
|
+
use Index, files
|
107
|
+
|
108
|
+
use Rack::Static,
|
109
|
+
:urls => ["/"],
|
110
|
+
:root => "public",
|
111
|
+
:index => "",
|
112
|
+
:header_rules => [
|
113
|
+
[:all, {'Cache-Control' => 'public, max-age=86400'}]
|
114
|
+
]
|
115
|
+
|
116
|
+
run lambda{ |env| [ 404, { 'Content-Type' => 'text/html' }, ['404 - page not found'] ] }
|
117
|
+
end
|
118
|
+
|
119
|
+
return builder
|
120
|
+
end
|
121
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'sockjs/session'
|
3
|
+
|
4
|
+
class ChannelHandler < SockJS::Session
|
5
|
+
def self.message_all
|
6
|
+
@@channels.each do |channel|
|
7
|
+
channel.send(message)
|
8
|
+
end
|
9
|
+
|
10
|
+
end
|
11
|
+
|
12
|
+
def initialize(session, *args)
|
13
|
+
@session = session
|
14
|
+
|
15
|
+
@@channels ||= []
|
16
|
+
@@channels << self
|
17
|
+
|
18
|
+
super
|
19
|
+
end
|
20
|
+
|
21
|
+
def process_message(message)
|
22
|
+
puts "Process: #{message}"
|
23
|
+
self.class.message_all(message)
|
24
|
+
end
|
25
|
+
|
26
|
+
def closed
|
27
|
+
# Remove ourself from the available channels
|
28
|
+
@@channels.delete(self)
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
require 'stringio'
|
2
|
+
require 'volt'
|
3
|
+
require 'volt/server/template_parser'
|
4
|
+
|
5
|
+
class ComponentHandler
|
6
|
+
def call(env)
|
7
|
+
req = Rack::Request.new(env)
|
8
|
+
|
9
|
+
# TODO: Sanatize template path
|
10
|
+
@component_path = req.path.strip.gsub(/^\/components\//, '').gsub(/[.]js$/, '')
|
11
|
+
|
12
|
+
code = generate_controller_code + generate_view_code + generate_model_code + generate_routes_code
|
13
|
+
|
14
|
+
javascript_code = Opal.compile(code)
|
15
|
+
|
16
|
+
# puts "ENV: #{env.inspect}"
|
17
|
+
[200, {"Content-Type" => "text/html"}, StringIO.new(javascript_code)]
|
18
|
+
end
|
19
|
+
|
20
|
+
def generate_view_code
|
21
|
+
code = ''
|
22
|
+
views_path = Volt.root + "/app/#{@component_path}/views/"
|
23
|
+
|
24
|
+
# Load all templates in the folder
|
25
|
+
Dir["#{views_path}*/*.html"].each do |view_path|
|
26
|
+
# Get the path for the template, supports templates in folders
|
27
|
+
template_path = view_path[views_path.size..((-1 * ('.html'.size + 1)))]
|
28
|
+
template_path = "#{@component_path}/#{template_path}"
|
29
|
+
# puts "Template Path: #{template_path.inspect}"
|
30
|
+
|
31
|
+
all_templates = TemplateParser.new(File.read(view_path), template_path)
|
32
|
+
|
33
|
+
binding_initializers = []
|
34
|
+
all_templates.templates.each_pair do |name, template|
|
35
|
+
binding_code = []
|
36
|
+
|
37
|
+
template['bindings'].each_pair do |key,value|
|
38
|
+
binding_code << "#{key.inspect} => [#{value.join(', ')}]"
|
39
|
+
end
|
40
|
+
|
41
|
+
binding_code = "{#{binding_code.join(', ')}}"
|
42
|
+
|
43
|
+
code << "$page.add_template(#{name.inspect}, #{template['html'].inspect}, #{binding_code})\n"
|
44
|
+
end
|
45
|
+
end
|
46
|
+
# puts "--------------"
|
47
|
+
# puts "CODE: #{code}"
|
48
|
+
|
49
|
+
return code
|
50
|
+
end
|
51
|
+
|
52
|
+
def generate_controller_code
|
53
|
+
code = ''
|
54
|
+
controllers_path = Volt.root + "/app/#{@component_path}/controllers/"
|
55
|
+
|
56
|
+
Dir["#{controllers_path}*_controller.rb"].each do |controller_path|
|
57
|
+
code << File.read(controller_path) + "\n\n"
|
58
|
+
end
|
59
|
+
|
60
|
+
return code
|
61
|
+
end
|
62
|
+
|
63
|
+
def generate_model_code
|
64
|
+
code = ''
|
65
|
+
models_path = Volt.root + "/app/#{@component_path}/models/"
|
66
|
+
|
67
|
+
Dir["#{models_path}*.rb"].each do |model_path|
|
68
|
+
code << File.read(model_path) + "\n\n"
|
69
|
+
|
70
|
+
model_name = model_path.match(/([^\/]+)[.]rb$/)[1]
|
71
|
+
|
72
|
+
code << "$page.add_model(#{model_name.inspect})\n\n"
|
73
|
+
end
|
74
|
+
|
75
|
+
return code
|
76
|
+
end
|
77
|
+
|
78
|
+
def generate_routes_code
|
79
|
+
code = ''
|
80
|
+
routes_path = Volt.root + "/app/#{@component_path}/config/routes.rb"
|
81
|
+
|
82
|
+
code << "$page.add_routes do\n"
|
83
|
+
code << "\n" + File.read(routes_path) + "\n"
|
84
|
+
code << "end\n\n"
|
85
|
+
|
86
|
+
return code
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# With if bindings, we need to track each branch, which is on the
|
2
|
+
# same scope level as the original if statement. We use this class
|
3
|
+
# to track each branch.
|
4
|
+
|
5
|
+
require 'volt/server/binding_setup'
|
6
|
+
class IfBindingSetup < BindingSetup
|
7
|
+
def initialize
|
8
|
+
@branches = []
|
9
|
+
end
|
10
|
+
|
11
|
+
def add_branch(content, template_name)
|
12
|
+
@branches << [content, template_name]
|
13
|
+
end
|
14
|
+
|
15
|
+
def to_setup_code
|
16
|
+
branches = @branches.map do |branch|
|
17
|
+
content = branch[0]
|
18
|
+
if content == nil
|
19
|
+
content = nil.inspect
|
20
|
+
else
|
21
|
+
content = "Proc.new { #{branch[0]} }"
|
22
|
+
end
|
23
|
+
|
24
|
+
"[#{content}, #{branch[1].inspect}]"
|
25
|
+
end.join(', ')
|
26
|
+
|
27
|
+
"lambda { |target, context, id| IfBinding.new(target, context, id, [#{branches}]) }"
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'stringio'
|
2
|
+
|
3
|
+
class RequestHandler
|
4
|
+
def call(env)
|
5
|
+
req = Rack::Request.new(env)
|
6
|
+
# puts env.inspect
|
7
|
+
# puts req.inspect
|
8
|
+
|
9
|
+
puts req.path
|
10
|
+
req.post?
|
11
|
+
puts req.params["data"]
|
12
|
+
|
13
|
+
# puts "ENV: #{env.inspect}"
|
14
|
+
[200, {"Content-Type" => "text/html"}, StringIO.new("Hello Rack!")]
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# Template parsing is fairly simple at the moment. Basically we walk the dom and
|
2
|
+
# do two types of replacements.
|
3
|
+
# 1) replacement in text nodes
|
4
|
+
# 2) attribute replacements
|
5
|
+
|
6
|
+
class Scope
|
7
|
+
attr_accessor :bindings, :outer_binding_number, :closed_block_scopes, :last_if_binding
|
8
|
+
|
9
|
+
def initialize(outer_binding_number=nil)
|
10
|
+
# For block bindings, the outer binding number lets us know what the name
|
11
|
+
# of the comments are that go before/after this scope block.
|
12
|
+
@outer_binding_number = outer_binding_number
|
13
|
+
@bindings = {}
|
14
|
+
end
|
15
|
+
|
16
|
+
def add_closed_child_scope(scope)
|
17
|
+
@closed_block_scopes ||= []
|
18
|
+
@closed_block_scopes << scope
|
19
|
+
end
|
20
|
+
|
21
|
+
def add_binding(binding_name, setup_code)
|
22
|
+
@bindings[binding_name] ||= []
|
23
|
+
@bindings[binding_name] << setup_code
|
24
|
+
end
|
25
|
+
|
26
|
+
def start_if_binding(binding_name, if_binding_setup)
|
27
|
+
@last_if_binding = [binding_name, if_binding_setup]
|
28
|
+
end
|
29
|
+
|
30
|
+
def current_if_binding
|
31
|
+
@last_if_binding
|
32
|
+
end
|
33
|
+
|
34
|
+
def close_if_binding!
|
35
|
+
if @last_if_binding
|
36
|
+
binding_name, if_binding_setup = @last_if_binding
|
37
|
+
@last_if_binding = nil
|
38
|
+
|
39
|
+
add_binding(binding_name, if_binding_setup.to_setup_code)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|