goat 0.2.12 → 0.3.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.
- data/README.md +45 -0
- data/bin/channel-srv +150 -0
- data/bin/press +29 -0
- data/bin/state-srv +390 -0
- data/bin/sync +53 -0
- data/bin/yodel +1 -1
- data/goat.gemspec +37 -0
- data/lib/goat.rb +774 -618
- data/lib/goat/common.rb +53 -0
- data/lib/goat/dynamic.rb +91 -0
- data/lib/goat/extn.rb +94 -5
- data/lib/goat/goat.js +348 -119
- data/lib/goat/html.rb +221 -103
- data/lib/goat/js/component.js +18 -15
- data/lib/goat/net-common.rb +38 -0
- data/lib/goat/notifications.rb +51 -46
- data/lib/goat/state-srv.rb +119 -0
- data/lib/goat/yodel.rb +3 -2
- data/lib/views/plain_layout.erb +7 -3
- metadata +18 -10
- data/lib/goat/logger.rb +0 -39
- data/lib/goat/sinatra.rb +0 -11
data/bin/sync
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
module Goat
|
4
|
+
module Sync
|
5
|
+
NotifChannel = EM::Channel.new
|
6
|
+
|
7
|
+
@viewports = {}
|
8
|
+
|
9
|
+
class Viewport
|
10
|
+
attr_reader :created, :viewport, :components
|
11
|
+
|
12
|
+
def initialize(v, cs)
|
13
|
+
@created = Time.now
|
14
|
+
@viewport = v
|
15
|
+
@components = cs
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
class ComponentServer < EM::Connection
|
20
|
+
def self.start(host='127.0.0.1', port=8010)
|
21
|
+
EM.start_server(host, port, self)
|
22
|
+
end
|
23
|
+
|
24
|
+
def viewport_components(v, cs)
|
25
|
+
@viewports[v] = Viewport.new(v, cs)
|
26
|
+
end
|
27
|
+
|
28
|
+
def receive_line(line)
|
29
|
+
msg = JSON.load(line)
|
30
|
+
meth = msg['type']
|
31
|
+
args = msg['data']
|
32
|
+
self.send(meth.to_sym, *args)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
NotificationServer.subscribe() do |notif|
|
37
|
+
|
38
|
+
end
|
39
|
+
|
40
|
+
class RecvServer < EM::Connection
|
41
|
+
include EM::P::LineText2
|
42
|
+
|
43
|
+
def self.start(host='127.0.0.1', port=8001)
|
44
|
+
EM.start_server(host, port, self)
|
45
|
+
end
|
46
|
+
|
47
|
+
def receive_line(line)
|
48
|
+
$stdout.puts "got #{line}" if $verbose
|
49
|
+
NotifChannel << line
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
data/bin/yodel
CHANGED
data/goat.gemspec
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
spec = Gem::Specification.new do |s|
|
2
|
+
s.name = 'goat'
|
3
|
+
s.version = '0.3.0'
|
4
|
+
s.summary = 'Pre-release beta version of Goat'
|
5
|
+
s.author = 'Patrick Collison'
|
6
|
+
s.email = 'patrick@collison.ie'
|
7
|
+
s.homepage = 'http://goatweb.org'
|
8
|
+
s.description = 'Sinatra live'
|
9
|
+
s.rubyforge_project = 'goat'
|
10
|
+
s.executables = %w{yodel}
|
11
|
+
s.require_paths = %w{lib}
|
12
|
+
|
13
|
+
s.files = %w{
|
14
|
+
README.md
|
15
|
+
bin/channel-srv
|
16
|
+
bin/press
|
17
|
+
bin/state-srv
|
18
|
+
bin/sync
|
19
|
+
bin/yodel
|
20
|
+
goat.gemspec
|
21
|
+
lib/goat/autobind.rb
|
22
|
+
lib/goat/common.rb
|
23
|
+
lib/goat/dynamic.rb
|
24
|
+
lib/goat/extn.rb
|
25
|
+
lib/goat/goat.js
|
26
|
+
lib/goat/html.rb
|
27
|
+
lib/goat/js/component.js
|
28
|
+
lib/goat/mongo.rb
|
29
|
+
lib/goat/net-common.rb
|
30
|
+
lib/goat/notifications.rb
|
31
|
+
lib/goat/state-srv.rb
|
32
|
+
lib/goat/static.rb
|
33
|
+
lib/goat/yodel.rb
|
34
|
+
lib/goat.rb
|
35
|
+
lib/views/plain_layout.erb
|
36
|
+
}
|
37
|
+
end
|
data/lib/goat.rb
CHANGED
@@ -1,3 +1,12 @@
|
|
1
|
+
#
|
2
|
+
# Author: Patrick Collison <patrick@collison.ie>
|
3
|
+
# Date: Spring-Fall 2010
|
4
|
+
#
|
5
|
+
# See http://goatweb.org
|
6
|
+
#
|
7
|
+
# License: GNU General Public License v2
|
8
|
+
|
9
|
+
require 'rubygems'
|
1
10
|
require 'set'
|
2
11
|
require 'cgi'
|
3
12
|
require 'json'
|
@@ -5,162 +14,99 @@ require 'eventmachine'
|
|
5
14
|
require 'thin'
|
6
15
|
require 'term/ansicolor'
|
7
16
|
require 'tilt'
|
17
|
+
require 'diff/lcs'
|
18
|
+
require 'inline'
|
8
19
|
|
9
|
-
|
10
|
-
require File.join(File.dirname(__FILE__), 'goat', file)
|
11
|
-
end
|
20
|
+
$:.unshift(File.dirname(__FILE__))
|
12
21
|
|
13
|
-
|
22
|
+
require 'goat/common'
|
23
|
+
require 'goat/net-common'
|
24
|
+
require 'goat/notifications'
|
25
|
+
require 'goat/extn'
|
26
|
+
require 'goat/html'
|
27
|
+
require 'goat/static'
|
28
|
+
require 'goat/autobind'
|
29
|
+
require 'goat/state-srv'
|
30
|
+
require 'goat/dynamic'
|
14
31
|
|
15
|
-
|
16
|
-
class ActionProc < Proc; end
|
32
|
+
$verbose ||= ARGV.include?('-v')
|
17
33
|
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
end
|
34
|
+
class Profile
|
35
|
+
# TODO make sure not already defined
|
36
|
+
def self.in(*args); end
|
37
|
+
def self.out(*args); end
|
38
|
+
def self.request_start(*args); end
|
39
|
+
def self.request_end(*args); end
|
40
|
+
end
|
23
41
|
|
24
|
-
class Object
|
25
|
-
def self.make_me
|
26
|
-
meth = nil
|
27
|
-
if caller[1] =~ /`(.+)'/
|
28
|
-
meth = $1
|
29
|
-
end
|
30
|
-
|
31
|
-
raise("Subclass #{self.name} should implement" + (meth ? " #{meth}" : ''))
|
32
|
-
end
|
33
42
|
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
def self.kind_of?(cls)
|
39
|
-
return true if self.old_kind_of?(Class) && cls == Class
|
40
|
-
self == cls || \
|
41
|
-
(self == Object ? \
|
42
|
-
false : \
|
43
|
-
(self.superclass && self.superclass != self && self.superclass.kind_of?(cls)))
|
44
|
-
end
|
45
|
-
|
46
|
-
def glimpse(n=100)
|
47
|
-
ins = self.inspect
|
48
|
-
if ins =~ />$/ && ins.size > n
|
49
|
-
"#{ins[0..n]}...>"
|
50
|
-
else
|
51
|
-
ins
|
52
|
-
end
|
43
|
+
class String
|
44
|
+
def prefix_ns(ns)
|
45
|
+
self.gsub(/^%(.+)$/, "#{ns}_\\1")
|
53
46
|
end
|
54
|
-
end
|
55
47
|
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
path = str.to_s.split('::')
|
60
|
-
from_root = path[0].empty?
|
61
|
-
if from_root
|
62
|
-
from_root = []
|
63
|
-
path = path[1..-1]
|
64
|
-
else
|
65
|
-
start_ns = ((Class === self)||(Module === self)) ? self : self.class
|
66
|
-
from_root = start_ns.to_s.split('::')
|
67
|
-
end
|
68
|
-
until from_root.empty?
|
69
|
-
begin
|
70
|
-
return (from_root+path).inject(Object) { |ns,name| ns.const_get(name) }
|
71
|
-
rescue NameError
|
72
|
-
from_root.delete_at(-1)
|
73
|
-
end
|
74
|
-
end
|
75
|
-
path.inject(Object) { |ns,name| ns.const_get(name) }
|
48
|
+
def h!
|
49
|
+
# TODO is this even used any more?
|
50
|
+
Goat::HTMLString.new(self)
|
76
51
|
end
|
77
|
-
end
|
78
52
|
|
79
|
-
|
80
|
-
|
81
|
-
"#<Set: #{self.map{|x| x.glimpse(n)}.join(', ')}>"
|
53
|
+
def handle_request(req)
|
54
|
+
[200, {}, self]
|
82
55
|
end
|
83
56
|
end
|
84
57
|
|
85
|
-
|
86
|
-
|
87
|
-
"[" + self.map{|x| x.glimpse(n)}.join(', ') + "]"
|
88
|
-
end
|
58
|
+
def h!(str)
|
59
|
+
Goat::HTMLString.new(str)
|
89
60
|
end
|
90
61
|
|
91
|
-
|
92
|
-
|
93
|
-
"{" + self.map{|k, v| k.glimpse + "=>" + v.glimpse}.join(', ') + "}"
|
94
|
-
end
|
95
|
-
end
|
62
|
+
module Goat
|
63
|
+
class HTMLString < String; end
|
96
64
|
|
97
|
-
class
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
65
|
+
class << self
|
66
|
+
inline do |builder|
|
67
|
+
builder.c 'VALUE new_without_initialize(VALUE klass) {
|
68
|
+
/* The 1 here is a special constant in ruby that corresponds to ID_ALLOCATOR */
|
69
|
+
return rb_funcall(klass, 1, 0, 0);
|
70
|
+
}'
|
103
71
|
end
|
104
|
-
h
|
105
72
|
end
|
106
|
-
end
|
107
73
|
|
108
|
-
class String
|
109
|
-
def prefix_ns(ns)
|
110
|
-
self.gsub(/^%(.+)$/, "#{ns}_\\1")
|
111
|
-
end
|
112
|
-
end
|
113
|
-
|
114
|
-
module Goat
|
115
74
|
def self.version
|
116
|
-
|
117
|
-
|
118
|
-
elsif !@tried_to_detect_version
|
119
|
-
@tried_to_detect_version = true
|
120
|
-
path = File.join(File.dirname(__FILE__), '../goat.gemspec')
|
121
|
-
if File.exists?(path)
|
122
|
-
v = File.read(path).split("\n").select{|l| l =~ /version/}.first
|
123
|
-
if v && v =~ /["']([0-9\.]+)["']/
|
124
|
-
@version = $1
|
125
|
-
end
|
126
|
-
elsif
|
127
|
-
dir = File.expand_path(File.dirname(__FILE__))
|
128
|
-
if dir =~ /goat-([0-9\.]+)\//
|
129
|
-
@version = $1
|
130
|
-
end
|
131
|
-
end
|
132
|
-
else
|
133
|
-
nil
|
134
|
-
end
|
75
|
+
specpath = File.join(File.dirname(__FILE__), '../goat.gemspec')
|
76
|
+
Gem::Specification.load(specpath).version.to_s
|
135
77
|
end
|
136
|
-
|
137
|
-
def self.goat_path(f); File.join(File.dirname(__FILE__), 'goat', f); end
|
138
78
|
|
139
|
-
def self.
|
140
|
-
require goat_path('sinatra')
|
141
|
-
end
|
79
|
+
def self.goat_path(f); File.join(File.dirname(__FILE__), 'goat', f); end
|
142
80
|
|
143
81
|
def self.extend_mongo
|
144
82
|
require goat_path('mongo')
|
145
83
|
end
|
146
|
-
|
84
|
+
|
147
85
|
def self.enable_notifications(opts={})
|
148
86
|
NotificationCenter.configure(opts)
|
149
87
|
end
|
150
|
-
|
88
|
+
|
89
|
+
def self.enable_statesrv(opts={})
|
90
|
+
StateSrvClient.configure(opts)
|
91
|
+
end
|
92
|
+
|
93
|
+
def self.enable_appsrv(opts={})
|
94
|
+
UpdateDispatcher.enable
|
95
|
+
end
|
96
|
+
|
151
97
|
def self.load_all(dir_fragment)
|
152
98
|
dir = File.join(Goat.setting!(:root), dir_fragment)
|
153
99
|
if File.directory?(dir)
|
154
100
|
Dir.entries(dir).select{|f| f =~ /\.rb$/}.each {|f| require(File.join(dir, f))}
|
155
101
|
end
|
156
102
|
end
|
157
|
-
|
103
|
+
|
158
104
|
@settings = {}
|
159
105
|
def self.setting(opt)
|
160
106
|
@settings[opt]
|
161
107
|
end
|
162
|
-
|
163
|
-
def self.setting!(opt)
|
108
|
+
|
109
|
+
def self.setting!(opt)
|
164
110
|
if @settings.include?(opt)
|
165
111
|
@settings[opt]
|
166
112
|
else
|
@@ -169,63 +115,57 @@ module Goat
|
|
169
115
|
end
|
170
116
|
|
171
117
|
def self.settings; @settings; end
|
172
|
-
|
118
|
+
|
173
119
|
def self.add_component_helpers(modul)
|
174
120
|
Goat::Component.send(:include, modul)
|
175
121
|
end
|
176
122
|
|
177
123
|
def self.configure(&blk)
|
178
124
|
blk.call
|
179
|
-
|
125
|
+
|
180
126
|
load_all('components')
|
181
|
-
load_all('pages')
|
182
|
-
|
127
|
+
load_all('pages')
|
128
|
+
|
183
129
|
Goat.extend_mongo if Goat.setting(:mongo)
|
184
|
-
|
185
|
-
|
130
|
+
|
186
131
|
if p = Goat.setting(:press)
|
187
132
|
Goat::Static.press = p
|
188
133
|
end
|
189
|
-
|
134
|
+
|
190
135
|
if Goat.setting(:debug)
|
191
136
|
if defined?(Thin)
|
192
137
|
Thin::Logging.debug = true
|
193
138
|
end
|
194
|
-
|
195
|
-
|
196
|
-
end
|
197
|
-
|
139
|
+
end
|
140
|
+
|
198
141
|
NotificationCenter.configure(Goat.setting(:notifications)) if Goat.setting(:notifications)
|
199
142
|
end
|
200
|
-
|
143
|
+
|
201
144
|
def self.rack_builder(app)
|
202
|
-
Rack::Builder.new do
|
145
|
+
Rack::Builder.new do
|
203
146
|
if cookies = Goat.setting(:cookies)
|
204
147
|
use Rack::Session::Cookie, cookies
|
205
148
|
end
|
206
|
-
|
149
|
+
|
207
150
|
if static = Goat.setting(:static)
|
208
151
|
use Rack::Static, :urls => static.fetch(:urls), :root => static.fetch(:root)
|
209
152
|
end
|
210
|
-
|
153
|
+
|
211
154
|
use Rack::Flash if defined?(Rack::Flash) # TODO hack
|
212
|
-
|
213
|
-
|
214
|
-
use Rack::Reloader
|
215
|
-
end
|
216
|
-
|
155
|
+
use Rack::CommonLogger
|
156
|
+
|
217
157
|
run app
|
218
158
|
end
|
219
159
|
end
|
220
|
-
|
160
|
+
|
221
161
|
class NotFoundError < RuntimeError
|
222
162
|
attr_reader :path
|
223
|
-
|
163
|
+
|
224
164
|
def initialize(path)
|
225
165
|
@path = path
|
226
166
|
end
|
227
167
|
end
|
228
|
-
|
168
|
+
|
229
169
|
class ChannelPusher
|
230
170
|
include EM::Deferrable
|
231
171
|
|
@@ -236,7 +176,7 @@ module Goat
|
|
236
176
|
@messages = []
|
237
177
|
|
238
178
|
self.errback do
|
239
|
-
|
179
|
+
logd "Channel closed"
|
240
180
|
@finished = true
|
241
181
|
end
|
242
182
|
|
@@ -279,26 +219,26 @@ module Goat
|
|
279
219
|
@body_callback = blk
|
280
220
|
end
|
281
221
|
end
|
282
|
-
|
222
|
+
|
283
223
|
class Halt < Exception
|
284
224
|
attr_reader :response
|
285
|
-
|
225
|
+
|
286
226
|
def initialize(response)
|
287
227
|
@response = response
|
288
228
|
end
|
289
229
|
end
|
290
230
|
|
291
231
|
class ReqHandler
|
292
|
-
|
293
|
-
# we can't know this at initialize time because we start using
|
232
|
+
|
233
|
+
# we can't know this at initialize time because we start using
|
294
234
|
# the req handler in context of Goat::App, which would lead to
|
295
235
|
# app = Goat::App, which is wrong
|
296
236
|
attr_accessor :app_class
|
297
|
-
|
237
|
+
|
298
238
|
def initialize
|
299
239
|
@around_handler_bindings = {}
|
300
240
|
end
|
301
|
-
|
241
|
+
|
302
242
|
class ::Proc
|
303
243
|
def handle_request(app)
|
304
244
|
self.call(app)
|
@@ -329,7 +269,7 @@ module Goat
|
|
329
269
|
|
330
270
|
mappings[method][path] = hook
|
331
271
|
end
|
332
|
-
|
272
|
+
|
333
273
|
def around_handler_binding(handler, type)
|
334
274
|
if type == :before
|
335
275
|
@around_handler_bindings[handler] ||= App.bind(handler)
|
@@ -338,20 +278,20 @@ module Goat
|
|
338
278
|
else
|
339
279
|
raise 'bad handler type'
|
340
280
|
end
|
341
|
-
end
|
342
|
-
|
281
|
+
end
|
282
|
+
|
343
283
|
def run_around_handlers(app, type)
|
344
284
|
app.class.around_handlers.select{|h| h.first == type}.each do |handler|
|
345
285
|
around_handler_binding(handler[1], type).call(app)
|
346
286
|
end
|
347
287
|
end
|
348
|
-
|
288
|
+
|
349
289
|
def run_before_handlers(app); run_around_handlers(app, :before); end
|
350
290
|
def run_after_handlers(app); run_around_handlers(app, :after); end
|
351
|
-
|
352
|
-
def resp_for_error(e, app)
|
291
|
+
|
292
|
+
def resp_for_error(e, app)
|
353
293
|
resp = nil
|
354
|
-
|
294
|
+
|
355
295
|
if e.kind_of?(NotFoundError)
|
356
296
|
# 404
|
357
297
|
if app.class.not_found_handler
|
@@ -360,43 +300,58 @@ module Goat
|
|
360
300
|
else
|
361
301
|
resp = [404, {}, 'not found']
|
362
302
|
end
|
363
|
-
else
|
364
|
-
# not a 404 -- an actual problem
|
303
|
+
else
|
304
|
+
# not a 404 -- an actual problem
|
365
305
|
if app.class.error_handler
|
366
306
|
@error_handler_binding ||= App.bind(app.class.error_handler)
|
367
307
|
resp = @error_handler_binding.call(app, e)
|
368
308
|
else
|
369
|
-
|
370
|
-
|
371
|
-
|
309
|
+
loge e.inspect
|
310
|
+
loge e.backtrace.join("\n")
|
311
|
+
|
372
312
|
resp = Rack::Response.new
|
373
313
|
resp.status = 500
|
374
|
-
resp['Content-Type'] = 'text/plain'
|
314
|
+
resp['Content-Type'] = 'text/plain'
|
375
315
|
resp.body = e.inspect + "\n" + e.backtrace.join("\n")
|
376
316
|
end
|
377
317
|
end
|
378
|
-
|
318
|
+
|
379
319
|
resp
|
380
320
|
end
|
381
|
-
|
382
|
-
def
|
321
|
+
|
322
|
+
def find_hook(meth, path)
|
323
|
+
hooks = mappings[meth.downcase.to_sym]
|
324
|
+
if h = hooks[path]
|
325
|
+
return h
|
326
|
+
else
|
327
|
+
hooks.each do |pathspec, hook|
|
328
|
+
if pathspec.kind_of?(Regexp) && pathspec =~ path
|
329
|
+
return hook
|
330
|
+
end
|
331
|
+
end
|
332
|
+
end
|
333
|
+
|
334
|
+
nil
|
335
|
+
end
|
336
|
+
|
337
|
+
def handle_request(env)
|
383
338
|
path = env['PATH_INFO']
|
384
339
|
meth = env['REQUEST_METHOD']
|
385
|
-
hook =
|
340
|
+
hook = find_hook(meth, path)
|
386
341
|
hdrs = {}
|
387
342
|
resp = nil
|
388
|
-
|
343
|
+
|
389
344
|
begin
|
390
|
-
req = Rack::Request.new(env)
|
391
|
-
|
345
|
+
req = Rack::Request.new(env)
|
346
|
+
|
392
347
|
app = @app_class.new(req)
|
393
|
-
|
348
|
+
|
394
349
|
begin
|
395
350
|
run_before_handlers(app)
|
396
351
|
rescue Halt => halt
|
397
352
|
return halt.response.to_a
|
398
353
|
end
|
399
|
-
|
354
|
+
|
400
355
|
if hook
|
401
356
|
resp = hook.handle_request(app)
|
402
357
|
else
|
@@ -407,40 +362,40 @@ module Goat
|
|
407
362
|
rescue Exception => e
|
408
363
|
resp = resp_for_error(e, app)
|
409
364
|
end
|
410
|
-
|
365
|
+
|
411
366
|
run_after_handlers(app)
|
412
367
|
|
413
368
|
resp.to_a
|
414
369
|
end
|
415
370
|
end
|
416
|
-
|
371
|
+
|
417
372
|
module HTMLHelpers
|
418
373
|
include Rack::Utils
|
419
374
|
alias_method :h, :escape_html
|
420
375
|
alias_method :e, :escape # for URIs
|
421
|
-
|
422
|
-
def jsesc(x); x.gsub('\\', '\\\\\\').gsub('"', '\"'); end
|
423
|
-
end
|
424
|
-
|
376
|
+
|
377
|
+
def jsesc(x); x.gsub('\\', '\\\\\\').gsub('"', '\"'); end
|
378
|
+
end
|
379
|
+
|
425
380
|
module FlashHelper
|
426
381
|
def flash
|
427
382
|
request.env['x-rack.flash']
|
428
|
-
end
|
429
|
-
end
|
430
|
-
|
383
|
+
end
|
384
|
+
end
|
385
|
+
|
431
386
|
class ERBRunner
|
432
387
|
include HTMLHelpers
|
433
388
|
include FlashHelper
|
434
|
-
|
389
|
+
|
435
390
|
def initialize(req, resp, params)
|
436
391
|
@request = req
|
437
392
|
@response = resp
|
438
393
|
@params = params
|
439
394
|
end
|
440
|
-
|
395
|
+
|
441
396
|
attr_reader :request, :response, :params
|
442
397
|
attr_accessor :delegate
|
443
|
-
|
398
|
+
|
444
399
|
def take_delegate_ivars
|
445
400
|
@delegate.instance_variables.each do |ivar|
|
446
401
|
unless self.instance_variables.include?(ivar)
|
@@ -448,7 +403,7 @@ module Goat
|
|
448
403
|
end
|
449
404
|
end
|
450
405
|
end
|
451
|
-
|
406
|
+
|
452
407
|
def method_missing(meth, *args)
|
453
408
|
if @delegate
|
454
409
|
@delegate.send(meth, *args)
|
@@ -456,26 +411,26 @@ module Goat
|
|
456
411
|
super
|
457
412
|
end
|
458
413
|
end
|
459
|
-
|
414
|
+
|
460
415
|
def erb(name, opts={}, &blk)
|
461
416
|
take_delegate_ivars if @delegate
|
462
|
-
|
417
|
+
|
463
418
|
opts = {
|
464
419
|
:partial => false,
|
465
420
|
:layout => true,
|
466
421
|
:locals => {}
|
467
422
|
}.merge(opts)
|
468
|
-
|
423
|
+
|
469
424
|
if self.kind_of?(Page)
|
470
425
|
opts[:locals][:page] ||= self
|
471
426
|
end
|
472
|
-
|
427
|
+
|
473
428
|
partial = opts[:partial]
|
474
429
|
use_layout = opts[:layout]
|
475
430
|
locals = opts[:locals]
|
476
|
-
|
431
|
+
|
477
432
|
if partial
|
478
|
-
name = name.to_s
|
433
|
+
name = name.to_s
|
479
434
|
d = File.dirname(name)
|
480
435
|
d = d == '.' ? '' : "#{d}/"
|
481
436
|
f = File.basename(name)
|
@@ -483,19 +438,19 @@ module Goat
|
|
483
438
|
# slashes are actually allowed in syms
|
484
439
|
name = "#{d}_#{f}".to_sym
|
485
440
|
end
|
486
|
-
|
441
|
+
|
487
442
|
if name =~ /\.erb$/ # allow an absolute path to be passed
|
488
443
|
erbf = name
|
489
444
|
else
|
490
445
|
erbf = File.join(Goat.setting!(:root), 'views', "#{name}.erb")
|
491
446
|
end
|
492
|
-
|
447
|
+
|
493
448
|
layf = File.join(Goat.setting!(:root), 'views', 'layout.erb')
|
494
449
|
template = Tilt[:erb].new(erbf) { File.read(erbf) }
|
495
|
-
|
450
|
+
|
496
451
|
layout = File.read(layf) if File.exists?(layf) && !partial && use_layout
|
497
452
|
out = template.render(self, locals, &blk)
|
498
|
-
|
453
|
+
|
499
454
|
if layout
|
500
455
|
laytpl = Tilt[:erb].new(layf) { layout }
|
501
456
|
laytpl.render(self, locals) { out }
|
@@ -503,32 +458,29 @@ module Goat
|
|
503
458
|
out
|
504
459
|
end
|
505
460
|
end
|
506
|
-
|
461
|
+
|
507
462
|
def partial_erb(name, opts={})
|
508
463
|
erb(name, {:partial => true}.merge(opts))
|
509
464
|
end
|
510
|
-
end
|
511
|
-
|
465
|
+
end
|
466
|
+
|
512
467
|
module AppHelpers
|
513
|
-
def halt
|
468
|
+
def halt(body=nil)
|
469
|
+
response.body = body if body
|
514
470
|
raise Halt.new(response)
|
515
471
|
end
|
516
|
-
|
472
|
+
|
517
473
|
def redirect(url)
|
518
474
|
response.status = 302
|
519
475
|
response['Location'] = url
|
520
476
|
halt
|
521
477
|
end
|
522
|
-
|
478
|
+
|
523
479
|
def session
|
524
480
|
request.env['rack.session'] ||= {}
|
525
|
-
end
|
526
|
-
|
527
|
-
def render_component(c)
|
528
|
-
c.processed_html
|
529
481
|
end
|
530
482
|
end
|
531
|
-
|
483
|
+
|
532
484
|
class IndifferentHash < Hash
|
533
485
|
def self.from_hash(hash)
|
534
486
|
ih = self.new
|
@@ -537,56 +489,26 @@ module Goat
|
|
537
489
|
end
|
538
490
|
ih
|
539
491
|
end
|
540
|
-
|
492
|
+
|
541
493
|
def [](k)
|
542
494
|
if k.kind_of?(Symbol)
|
543
495
|
k_sym = k
|
544
496
|
k_str = k.to_s
|
545
497
|
raise 'Invalid hash' if self.include?(k_sym) && self.include?(k_str)
|
546
|
-
|
498
|
+
|
547
499
|
self.include?(k_str) ? self.fetch(k_str) : self.fetch(k_sym, nil)
|
548
500
|
else
|
549
501
|
super(k)
|
550
502
|
end
|
551
503
|
end
|
552
504
|
end
|
553
|
-
|
505
|
+
|
554
506
|
class App
|
555
507
|
class << self
|
556
|
-
|
557
|
-
MAX_ACTIVE_PAGES = 100
|
508
|
+
|
558
509
|
@@error_handler = nil
|
559
510
|
@@not_found_handler = nil
|
560
|
-
@@active_pages = {}
|
561
|
-
@@active_page_queue = Queue.new
|
562
511
|
@@around_handlers = []
|
563
|
-
|
564
|
-
def active_pages; @@active_pages; end
|
565
|
-
def active_page_queue; @@active_page_queue; end
|
566
|
-
|
567
|
-
def add_active_page(pg)
|
568
|
-
pgid = pg.id
|
569
|
-
active_page_queue << pg.id
|
570
|
-
self.active_pages[pg.id] = pg
|
571
|
-
end
|
572
|
-
|
573
|
-
def active_page_gc
|
574
|
-
deleted = 0
|
575
|
-
while active_page_queue.size > MAX_ACTIVE_PAGES
|
576
|
-
pgid = active_page_queue.pop
|
577
|
-
pg = active_pages[pgid]
|
578
|
-
pg.mark_dead!
|
579
|
-
NotificationCenter.delegate_gc
|
580
|
-
active_pages.delete(pgid)
|
581
|
-
Logger.log :gc, "Removing page #{pgid}"
|
582
|
-
deleted += 1
|
583
|
-
end
|
584
|
-
ObjectSpace.garbage_collect
|
585
|
-
Logger.log :gc, "Page GC ejected #{deleted} pages"
|
586
|
-
rescue Exception => e
|
587
|
-
Logger.error(:gc, e.inspect)
|
588
|
-
Logger.error(:gc, e.backtrace.join("\n"))
|
589
|
-
end
|
590
512
|
|
591
513
|
def req_handler
|
592
514
|
@@reqhandler ||= ReqHandler.new
|
@@ -605,13 +527,13 @@ module Goat
|
|
605
527
|
def map(opts)
|
606
528
|
req_handler.add_mapping(opts)
|
607
529
|
end
|
608
|
-
|
609
|
-
def around_handlers; @@around_handlers; end
|
610
|
-
|
530
|
+
|
531
|
+
def around_handlers; @@around_handlers; end
|
532
|
+
|
611
533
|
def before(&blk)
|
612
534
|
around_handlers << [:before, blk]
|
613
535
|
end
|
614
|
-
|
536
|
+
|
615
537
|
def after(&blk)
|
616
538
|
around_handlers << [:after, blk]
|
617
539
|
end
|
@@ -619,145 +541,101 @@ module Goat
|
|
619
541
|
def enable_notifications(opts={})
|
620
542
|
NotificationCenter.configure(opts)
|
621
543
|
end
|
622
|
-
|
623
|
-
def debug_pages
|
624
|
-
active_pages.each do |i, pg|
|
625
|
-
Logger.log :gc, [i, pg.dead?].inspect
|
626
|
-
end
|
627
|
-
end
|
628
|
-
|
629
|
-
def handle_channel(req)
|
630
|
-
id = req['_id']
|
631
|
-
jsonp = req['jsonp']
|
632
|
-
pg = active_pages[id]
|
633
|
-
|
634
|
-
active_page_gc # we do GC here since a slight delay in opening channel is better than in loading page
|
635
|
-
|
636
|
-
if !jsonp
|
637
|
-
respond_failed
|
638
|
-
end
|
639
|
-
|
640
|
-
if pg
|
641
|
-
pg.mark_alive!
|
642
|
-
body = ChannelPusher.new(pg.channel, jsonp)
|
643
544
|
|
644
|
-
|
645
|
-
|
646
|
-
end
|
647
|
-
|
648
|
-
body.callback { pg.mark_dead! }
|
649
|
-
body.errback { pg.mark_dead! }
|
545
|
+
def respond_success; [200, {}, ['ok']]; end
|
546
|
+
def respond_failed; [500, {}, ['failed']]; end
|
650
547
|
|
651
|
-
|
548
|
+
def handle_rpc(app)
|
549
|
+
req = app.request
|
652
550
|
|
653
|
-
|
654
|
-
|
655
|
-
|
551
|
+
cls = req['cls']
|
552
|
+
cid = req['id']
|
553
|
+
rpc = req['rpc']
|
554
|
+
txn = req['rttxn']
|
555
|
+
pgid = req['pgid']
|
556
|
+
reqargs = JSON.load(req['args'])
|
656
557
|
|
657
|
-
|
658
|
-
|
659
|
-
end
|
558
|
+
have_handler = false
|
559
|
+
resp = nil
|
660
560
|
|
661
|
-
|
662
|
-
|
561
|
+
Goat::NotificationCenter.notify(
|
562
|
+
'type' => 'txn_start',
|
563
|
+
'txn' => txn,
|
564
|
+
'pgid' => pgid
|
565
|
+
)
|
663
566
|
|
664
|
-
|
665
|
-
|
666
|
-
|
667
|
-
|
668
|
-
|
669
|
-
|
670
|
-
|
671
|
-
|
672
|
-
|
673
|
-
|
674
|
-
|
675
|
-
|
676
|
-
|
677
|
-
|
678
|
-
|
679
|
-
|
567
|
+
if comp = Goat.rpc_handlers[cls]
|
568
|
+
if comp.include?(rpc)
|
569
|
+
Dynamic.let(:txn => txn, :txn_pgid => pgid) do
|
570
|
+
have_handler = true
|
571
|
+
opts = comp[rpc]
|
572
|
+
args = []
|
573
|
+
if opts[:live]
|
574
|
+
if skel = StateSrvClient.fetch_component(cid)
|
575
|
+
component = Kernel.fetch_class(cls).from_skel(skel)
|
576
|
+
component.deserialize(skel.state)
|
577
|
+
|
578
|
+
args << component
|
579
|
+
else
|
580
|
+
return respond_failed
|
581
|
+
end
|
582
|
+
end
|
680
583
|
|
681
|
-
|
682
|
-
end
|
683
|
-
end
|
584
|
+
args << IndifferentHash.from_hash(reqargs)
|
684
585
|
|
685
|
-
|
686
|
-
|
687
|
-
comp.submitted(req)
|
688
|
-
respond_success
|
689
|
-
end
|
690
|
-
end
|
586
|
+
resp = app.respond_with_hook("rpc_#{rpc}", *args)
|
587
|
+
resp = opts[:is_get] ? resp.to_json : [200, {}, '']
|
691
588
|
|
692
|
-
|
693
|
-
|
694
|
-
|
695
|
-
|
696
|
-
comp.run_callback(k)
|
697
|
-
respond_success
|
698
|
-
else
|
699
|
-
Logger.error(:req, "Couldn't find handler #{k.inspect}")
|
700
|
-
respond_failed
|
701
|
-
end
|
702
|
-
end
|
703
|
-
end
|
704
|
-
|
705
|
-
def handle_rpc(app)
|
706
|
-
req = app.request
|
707
|
-
|
708
|
-
c = req['_component']
|
709
|
-
id = req['_id']
|
710
|
-
rpc = req['_rpc']
|
711
|
-
|
712
|
-
if comp = Goat.rpc_handlers[c]
|
713
|
-
if comp[rpc]
|
714
|
-
return app.respond_with_hook("rpc_#{rpc}", IndifferentHash.from_hash(req.params))
|
589
|
+
if opts[:live]
|
590
|
+
component.update
|
591
|
+
end
|
592
|
+
end
|
715
593
|
end
|
716
594
|
end
|
717
|
-
|
718
|
-
|
595
|
+
|
596
|
+
Goat::NotificationCenter.notify(
|
597
|
+
'type' => 'txn_complete',
|
598
|
+
'txn' => txn
|
599
|
+
)
|
600
|
+
|
601
|
+
have_handler ? resp : [500, {}, {'success' => false}.to_json]
|
719
602
|
end
|
720
|
-
|
603
|
+
|
721
604
|
def error(&blk)
|
722
605
|
@@error_handler = blk
|
723
606
|
end
|
724
|
-
|
607
|
+
|
725
608
|
def error_handler; @@error_handler; end
|
726
|
-
|
609
|
+
|
727
610
|
def not_found(&blk)
|
728
611
|
@@not_found_handler = blk
|
729
612
|
end
|
730
|
-
|
613
|
+
|
731
614
|
def not_found_handler; @@not_found_handler; end
|
732
|
-
|
615
|
+
|
733
616
|
end # end class << self
|
734
|
-
|
617
|
+
|
735
618
|
def self.call(env)
|
736
|
-
# TODO better place to put this?
|
737
|
-
NotificationCenter.init # will do nothing if not enabled
|
738
|
-
|
739
619
|
self.req_handler.app_class = self
|
740
|
-
|
741
620
|
self.req_handler.handle_request(env)
|
742
621
|
end
|
743
|
-
|
622
|
+
|
744
623
|
include AppHelpers
|
745
624
|
include FlashHelper
|
746
|
-
|
625
|
+
|
747
626
|
def self.bind(hook, name=nil, for_response=true)
|
748
627
|
mname = name || String.random
|
749
|
-
|
628
|
+
|
750
629
|
lambda do |app, *args|
|
751
630
|
kls = app.class
|
752
|
-
|
631
|
+
|
753
632
|
unless kls.instance_methods.include?(mname)
|
754
|
-
|
633
|
+
logd "defining #{mname} on #{kls}"
|
755
634
|
kls.send(:define_method, mname, hook)
|
756
635
|
hook = kls.instance_method(mname)
|
757
636
|
end
|
758
|
-
|
759
|
-
|
760
|
-
if for_response
|
637
|
+
|
638
|
+
if for_response
|
761
639
|
# sets the body
|
762
640
|
app.respond_with_hook(mname, *args)
|
763
641
|
else
|
@@ -765,35 +643,37 @@ module Goat
|
|
765
643
|
end
|
766
644
|
end
|
767
645
|
end
|
768
|
-
|
646
|
+
|
769
647
|
def initialize(req, meth=nil)
|
770
648
|
@req = req
|
771
649
|
@meth = meth
|
772
650
|
@response = Rack::Response.new
|
773
651
|
@params = IndifferentHash.from_hash(req.params)
|
774
652
|
end
|
775
|
-
|
653
|
+
|
776
654
|
def response; @response; end
|
777
655
|
def request; @req; end
|
778
656
|
def params; @params; end
|
779
|
-
|
657
|
+
|
780
658
|
def respond_with_hook(hook, *args)
|
781
659
|
response.body = self.send(hook, *args) || ''
|
782
660
|
response.finish
|
783
661
|
end
|
784
|
-
|
662
|
+
|
785
663
|
def erb(name, opts={}, &blk)
|
786
664
|
e = ERBRunner.new(@req, @response, @params)
|
787
665
|
e.delegate = self
|
788
666
|
e.erb(name, {:locals => {:page => nil}}.merge(opts), &blk)
|
789
667
|
end
|
790
|
-
|
791
|
-
map :path => '/channel', :metal => true, :hook => lambda {|app| handle_channel(app.request)}
|
792
|
-
map :path => '/post', :metal => true, :hook => lambda {|app| handle_post(app.request) }
|
793
|
-
map :path => '/dispatch', :metal => true, :hook => lambda {|app| handle_dispatch(app.request) }
|
668
|
+
|
794
669
|
map :path => '/rpc', :method => :post, :metal => true, :hook => lambda {|app| handle_rpc(app)}
|
795
670
|
end
|
796
671
|
|
672
|
+
class BasicApp < App
|
673
|
+
get '/static/jquery.js', File.read(Goat.goat_path('../../examples/jq.js'))
|
674
|
+
get '/static/goat.js', File.read(Goat.goat_path('goat.js'))
|
675
|
+
end
|
676
|
+
|
797
677
|
class Channel
|
798
678
|
# thin wrapper for EM::Channel
|
799
679
|
|
@@ -804,331 +684,623 @@ module Goat
|
|
804
684
|
end
|
805
685
|
|
806
686
|
def send_message(msg)
|
807
|
-
Logger.log :live, "Pushing #{msg.inspect}"
|
808
687
|
@emchannel.push(msg)
|
809
688
|
end
|
810
689
|
end
|
811
|
-
|
690
|
+
|
812
691
|
def self.rpc_handlers; @rpc_handlers ||= {}; end
|
813
692
|
|
693
|
+
class DOMDistiller
|
694
|
+
def initialize(dom, cs)
|
695
|
+
@dom = dom
|
696
|
+
@components = cs
|
697
|
+
end
|
698
|
+
|
699
|
+
def all_components
|
700
|
+
@components
|
701
|
+
end
|
702
|
+
|
703
|
+
def all_component_classes
|
704
|
+
@all_component_classes ||= \
|
705
|
+
all_components.map(&:class).uniq.\
|
706
|
+
map{|cls| cls.superclasses(Component)}.uniq.flatten
|
707
|
+
end
|
708
|
+
|
709
|
+
def ordered_component_classes
|
710
|
+
cs = all_component_classes
|
711
|
+
|
712
|
+
i = 0
|
713
|
+
# invariant: left of i has no superclasses to right
|
714
|
+
while i < cs.size
|
715
|
+
c = cs[i]
|
716
|
+
if cs[(i+1)..-1].any?{|sup| c.subclass_of?(sup)}
|
717
|
+
cs.delete(c)
|
718
|
+
cs << c
|
719
|
+
else
|
720
|
+
i += 1
|
721
|
+
end
|
722
|
+
end
|
723
|
+
|
724
|
+
cs
|
725
|
+
end
|
726
|
+
|
727
|
+
def unpressed_component_classes
|
728
|
+
ordered_component_classes.to_a.reject do |cls|
|
729
|
+
Goat.setting(:press) && Goat::Static.pressed?(cls)
|
730
|
+
end
|
731
|
+
end
|
732
|
+
|
733
|
+
def script
|
734
|
+
cs = unpressed_component_classes
|
735
|
+
|
736
|
+
[
|
737
|
+
cs.map(&:__script),
|
738
|
+
all_components.select{|c| c.class.wired?}.map(&:wire_script),
|
739
|
+
all_components.map(&:__script)
|
740
|
+
].flatten.compact.join(';')
|
741
|
+
end
|
742
|
+
|
743
|
+
def style
|
744
|
+
unpressed_component_classes.select(&:__css).map(&:scoped_css).join
|
745
|
+
end
|
746
|
+
end
|
747
|
+
|
814
748
|
class Page
|
815
749
|
attr_reader :canvas, :request, :id, :canvas, :layout, :params, :channel
|
816
|
-
|
750
|
+
|
817
751
|
@live_updates = true
|
818
|
-
|
752
|
+
|
819
753
|
def self.disable_live_updates; @live_updates = false; end
|
820
754
|
def self.enable_live_updates; @live_updates = true; end
|
821
755
|
def self.live_updates_enabled?; @live_updates != false; end
|
822
|
-
|
756
|
+
|
823
757
|
include FlashHelper
|
824
|
-
|
758
|
+
|
825
759
|
def erb(name, opts={}, &blk)
|
826
760
|
e = ERBRunner.new(@request, @response, @params)
|
827
761
|
e.delegate = self
|
828
762
|
e.erb(name, {:partial => false}.merge(opts), &blk)
|
829
763
|
end
|
830
|
-
|
764
|
+
|
831
765
|
def self.handle_request(app)
|
832
|
-
pg = self.new(app)
|
833
|
-
|
834
|
-
resp = pg.response
|
835
|
-
|
836
|
-
# app.class.active_page_gc
|
837
|
-
|
838
|
-
action_hashes = 0
|
839
|
-
|
840
|
-
ObjectSpace.each_object do |obj|
|
841
|
-
if obj.kind_of?(Page) && obj.class != Class
|
842
|
-
#puts "Page: #{obj.glimpse}"
|
843
|
-
elsif obj.kind_of?(Component) && obj.class != Class
|
844
|
-
#puts "Component: #{obj.glimpse}"
|
845
|
-
elsif obj.kind_of?(ActionProc) && obj.class != Class
|
846
|
-
action_hashes += 1
|
847
|
-
end
|
848
|
-
end
|
849
|
-
|
850
|
-
puts "ActionHashes: #{action_hashes}"
|
851
|
-
|
852
|
-
resp
|
766
|
+
pg = self.new(app)
|
767
|
+
pg.response
|
853
768
|
end
|
854
769
|
|
855
770
|
def initialize(app)
|
856
771
|
@request = app.request
|
857
772
|
@channel = Channel.new
|
858
|
-
@
|
859
|
-
@id = String.random(10, :alpha => true)
|
773
|
+
@id = 'pg_' + String.random(10)
|
860
774
|
@params = IndifferentHash.from_hash(request.params)
|
861
775
|
end
|
862
|
-
|
863
|
-
def components; @canvas.all_components; end
|
864
|
-
|
776
|
+
|
865
777
|
def empty_response
|
866
778
|
Rack::Response.new
|
867
779
|
end
|
868
|
-
|
780
|
+
|
869
781
|
def halt
|
870
782
|
raise Halt.new(empty_response)
|
871
783
|
end
|
872
|
-
|
873
|
-
def
|
874
|
-
|
875
|
-
|
876
|
-
|
877
|
-
|
878
|
-
|
784
|
+
|
785
|
+
def register_page(distiller)
|
786
|
+
live = distiller.all_components.select(&:live_enabled?)
|
787
|
+
pg_spec = live.map do |c|
|
788
|
+
c.pgid = @id
|
789
|
+
c.skel
|
790
|
+
end
|
791
|
+
|
792
|
+
StateSrvClient.register_page(@id, pg_spec)
|
879
793
|
end
|
880
|
-
|
881
|
-
def
|
882
|
-
|
794
|
+
|
795
|
+
def html_from_erb
|
796
|
+
html = self.erb(@erb, :layout => false)
|
883
797
|
end
|
884
|
-
|
885
|
-
|
886
|
-
|
887
|
-
|
888
|
-
|
889
|
-
|
890
|
-
|
891
|
-
comp_classes = Set.new
|
892
|
-
@canvas.all_components.map(&:class).uniq.map do |cls|
|
893
|
-
c = cls
|
894
|
-
|
895
|
-
while c
|
896
|
-
if c == Object
|
897
|
-
raise "Error traversing class hierarchy to find necessary JS"
|
898
|
-
end
|
899
|
-
|
900
|
-
comp_classes << c
|
901
|
-
|
902
|
-
if c == Component
|
903
|
-
break
|
904
|
-
else
|
905
|
-
c = c.superclass
|
906
|
-
end
|
907
|
-
end
|
908
|
-
end
|
909
|
-
|
910
|
-
comp_classes
|
911
|
-
end
|
912
|
-
|
913
|
-
def ordered_component_classes
|
914
|
-
cs = all_component_classes.to_a
|
915
|
-
|
916
|
-
i = 0
|
917
|
-
# invariant: left of i has no superclasses to right
|
918
|
-
while i < cs.size
|
919
|
-
c = cs[i]
|
920
|
-
if cs[(i+1)..-1].any?{|sup| c.kind_of?(sup)}
|
921
|
-
cs.delete(c)
|
922
|
-
cs << c
|
923
|
-
else
|
924
|
-
i += 1
|
925
|
-
end
|
926
|
-
end
|
927
|
-
|
928
|
-
cs
|
929
|
-
end
|
930
|
-
|
931
|
-
def unpressed_component_classes
|
932
|
-
ordered_component_classes.to_a.reject do |cls|
|
933
|
-
Goat.setting(:press) && Goat::Static.pressed?(cls)
|
934
|
-
end
|
935
|
-
end
|
936
|
-
|
937
|
-
def script
|
938
|
-
cs = unpressed_component_classes
|
939
|
-
|
940
|
-
[
|
941
|
-
cs.map(&:__script),
|
942
|
-
@canvas.all_components.select{|c| c.class.wired?}.map(&:wire_script),
|
943
|
-
@canvas.all_components.map(&:__script)
|
944
|
-
].flatten.compact.join(';')
|
945
|
-
end
|
946
|
-
|
947
|
-
def style
|
948
|
-
unpressed_component_classes.select(&:__css).map(&:scoped_css).join
|
798
|
+
|
799
|
+
def inject_html_from_dom(canvas)
|
800
|
+
exp = nil
|
801
|
+
helper = ExpansionHelper.new
|
802
|
+
|
803
|
+
Dynamic.let(:expander => helper) do
|
804
|
+
exp = DOMTools.expanded_dom(self.dom)
|
949
805
|
end
|
806
|
+
|
807
|
+
distiller = DOMDistiller.new(exp, helper.components)
|
808
|
+
|
809
|
+
canvas.html = DOMTools::HTMLBuilder.new(exp).html
|
810
|
+
canvas.style << distiller.style
|
811
|
+
canvas.script << distiller.script
|
812
|
+
|
813
|
+
EM.next_tick { register_page(distiller) }
|
950
814
|
end
|
951
|
-
|
815
|
+
|
952
816
|
def html
|
953
817
|
layout = self.layout
|
954
|
-
|
955
|
-
|
956
|
-
|
957
|
-
|
958
|
-
if @
|
959
|
-
html =
|
960
|
-
elsif
|
961
|
-
|
962
|
-
if item.kind_of?(Component)
|
963
|
-
item.processed_html
|
964
|
-
elsif item.kind_of?(String)
|
965
|
-
item
|
966
|
-
else
|
967
|
-
raise "Unknown body item: #{item.inspect}"
|
968
|
-
end
|
969
|
-
end).join
|
818
|
+
|
819
|
+
canvas = PageCanvas.new
|
820
|
+
canvas.title = @title
|
821
|
+
|
822
|
+
if @erb
|
823
|
+
canvas.html = html_from_erb
|
824
|
+
elsif methods.include?('dom')
|
825
|
+
inject_html_from_dom(canvas)
|
970
826
|
else
|
971
|
-
raise "You
|
972
|
-
end
|
973
|
-
|
974
|
-
|
975
|
-
@canvas.script << distiller.script
|
976
|
-
|
977
|
-
render_to_layout(html, layout)
|
827
|
+
raise "You should over-ride Page#dom or supply an erb template"
|
828
|
+
end
|
829
|
+
|
830
|
+
render_to_layout(canvas, layout)
|
978
831
|
end
|
979
|
-
|
980
|
-
def render_component(c)
|
981
|
-
|
982
|
-
|
983
|
-
end
|
832
|
+
|
833
|
+
#def render_component(c)
|
834
|
+
# canvas.components << c
|
835
|
+
# c.html
|
836
|
+
#end
|
984
837
|
|
985
838
|
def response
|
986
839
|
Rack::Response.new(self.html, 200, {})
|
987
840
|
end
|
988
|
-
|
989
|
-
def render_to_layout(
|
841
|
+
|
842
|
+
def render_to_layout(canvas, layout)
|
990
843
|
layout ||= File.join(File.dirname(__FILE__), 'views/plain_layout.erb')
|
991
|
-
|
844
|
+
|
992
845
|
erb(layout,
|
993
|
-
:locals => {:page => self},
|
846
|
+
:locals => {:page => self, :canvas => canvas},
|
994
847
|
# don't want a layout in our layout so we can layout while we layout
|
995
|
-
:layout => false)
|
996
|
-
|
997
|
-
|
998
|
-
|
848
|
+
:layout => false) { canvas.html }
|
849
|
+
end
|
850
|
+
|
851
|
+
def erb=(erb); @erb = erb; #TODO make it work properly, but not support embenned components
|
852
|
+
end
|
853
|
+
def title=(title); @title = title; end
|
999
854
|
end
|
1000
855
|
|
1001
856
|
class PageCanvas
|
1002
|
-
attr_accessor :
|
857
|
+
attr_accessor :title, :script, :style, :html
|
1003
858
|
|
1004
859
|
def initialize
|
1005
|
-
@body = []
|
1006
|
-
@components = []
|
1007
860
|
@script = []
|
1008
861
|
@style = []
|
1009
862
|
end
|
1010
|
-
|
863
|
+
|
1011
864
|
def flattened_style; @style.join; end
|
1012
865
|
def flattened_script; @script.join; end
|
1013
|
-
|
1014
|
-
|
1015
|
-
|
1016
|
-
|
866
|
+
end
|
867
|
+
|
868
|
+
class JustRerenderError < RuntimeError; end
|
869
|
+
|
870
|
+
class DOMDiff
|
871
|
+
def self.diff(old, new, id)
|
872
|
+
self.new(old, new, id).diff
|
873
|
+
end
|
874
|
+
|
875
|
+
def initialize(old, new, id)
|
876
|
+
@old = old
|
877
|
+
@new = new
|
878
|
+
@id = id
|
879
|
+
@diffs = []
|
880
|
+
end
|
881
|
+
|
882
|
+
def diff
|
883
|
+
nested_application(minimized_changes(desc(@old, @new, @id)))
|
884
|
+
end
|
885
|
+
|
886
|
+
def nested_application(ch)
|
887
|
+
applied = Set.new
|
888
|
+
|
889
|
+
ch.sort_by{|x| x[3]}.each do |c|
|
890
|
+
if (c[0] == :rem || c[0] == :add) && !applied.include?(c)
|
891
|
+
if n = ch.detect{|x| x != c && x[2] == c[2] && x[3] >= c[3]}
|
892
|
+
n[3] += (c[0] == :rem) ? -1 : 1
|
893
|
+
end
|
894
|
+
end
|
895
|
+
|
896
|
+
applied << c
|
897
|
+
end
|
898
|
+
|
899
|
+
ch
|
900
|
+
end
|
901
|
+
|
902
|
+
def minimized_changes(ch)
|
903
|
+
new = []
|
904
|
+
merged = Set.new
|
905
|
+
ch.each do |c|
|
906
|
+
if c[0] == :rem
|
907
|
+
if n = ch.detect{|x| x[0] == :add && x[2] == c[2] && x[3] == c[3]}
|
908
|
+
new << [:rep, *n[1..3]]
|
909
|
+
merged << n
|
910
|
+
else
|
911
|
+
new << c
|
912
|
+
end
|
913
|
+
else
|
914
|
+
new << c
|
915
|
+
end
|
916
|
+
end
|
917
|
+
new.reject{|c| merged.include?(c)}
|
918
|
+
end
|
919
|
+
|
920
|
+
def added(new, par, pos=nil); [:add, new, par, pos]; end
|
921
|
+
def removed(old, par, pos=nil); [:rem, old, par, pos]; end
|
922
|
+
|
923
|
+
def dom_node?(node)
|
924
|
+
node.is_a?(Array) && node.first.is_a?(Symbol)
|
925
|
+
end
|
926
|
+
def tag(node); node[0]; end
|
927
|
+
def attrs(node); node[1] if node[1].is_a?(Hash); end
|
928
|
+
def body(node); node[1].is_a?(Hash) ? node[2..-1] : node[1..-1]; end
|
929
|
+
def domid(node); attrs(node) ? attrs(node)[:id] : nil; end
|
930
|
+
|
931
|
+
def localized_change(dold, dnew, par, changes)
|
932
|
+
old = dold.element
|
933
|
+
new = dnew.element
|
934
|
+
#$stderr.puts "changes: #{changes.inspect} / #{old.inspect} / #{par.inspect}"
|
935
|
+
if changes.all?{|ch| ch[2].nil?} && par
|
936
|
+
[added(new, par, dold.position), removed(old, par, dnew.position)]
|
937
|
+
else
|
938
|
+
changes
|
939
|
+
end
|
940
|
+
end
|
941
|
+
|
942
|
+
def is_replacement?(d1, d2)
|
943
|
+
d1 && d2 && (
|
944
|
+
(d1.action == '+' && d2.action == '-') ||
|
945
|
+
(d1.action == '-' && d2.action == '+'))
|
946
|
+
end
|
947
|
+
|
948
|
+
def old_and_new(d1, d2)
|
949
|
+
d1.action == '+' ? [d2, d1] : [d1, d2]
|
950
|
+
end
|
951
|
+
|
952
|
+
def array_desc(old, new, par)
|
953
|
+
#$stderr.puts "array_desc #{old.inspect} #{new.inspect} #{par.inspect}"
|
954
|
+
|
955
|
+
chgs = Diff::LCS.diff(old, new).map do |diff|
|
956
|
+
#$stderr.puts "diff: #{diff.inspect}"
|
957
|
+
if is_replacement?(diff[0], diff[1])
|
958
|
+
dold, dnew = old_and_new(diff[0], diff[1])
|
959
|
+
old = dold.element
|
960
|
+
new = dnew.element
|
961
|
+
if dom_node?(old) && dom_node?(new) && tag(old) == tag(new) && compare_attrs(old, new)
|
962
|
+
localized_change(dold, dnew, par, desc(body(old), body(new), domid(old)))
|
963
|
+
elsif old.is_a?(Array) && new.is_a?(Array) #&& old.all?{|x| !dom_node?(x)} && new.all?{|x| !dom_node?(x)}
|
964
|
+
array_desc(old, new, par)
|
965
|
+
else
|
966
|
+
[added(new, par, diff[1].position), removed(old, par, diff[0].position)]
|
967
|
+
end
|
968
|
+
else
|
969
|
+
if diff.size == 1
|
970
|
+
diff = diff[0]
|
971
|
+
if diff.action == '+'
|
972
|
+
[added(diff.element, par, diff.position)]
|
973
|
+
elsif diff.action == '-'
|
974
|
+
[removed(diff.element, par, diff.position)]
|
975
|
+
else
|
976
|
+
raise "Don't understand diff in a bad way"
|
977
|
+
end
|
978
|
+
else
|
979
|
+
$stderr.puts "Don't understand diff"
|
980
|
+
#$stderr.puts "Diff failed"
|
981
|
+
raise JustRerenderError
|
982
|
+
end
|
983
|
+
end
|
984
|
+
end
|
985
|
+
|
986
|
+
chgs.flatten(1)
|
987
|
+
end
|
988
|
+
|
989
|
+
def compare_attrs(a, b)
|
990
|
+
if a.is_a?(Hash) && b.is_a?(Hash)
|
991
|
+
a_, b_ = a.clone, b.clone
|
992
|
+
a.select{|k,v| a_.delete(k) if v =~ /^dom_/}
|
993
|
+
b.select{|k,v| b_.delete(k) if v =~ /^dom_/}
|
994
|
+
a_ == b_
|
995
|
+
else
|
996
|
+
false
|
997
|
+
end
|
998
|
+
end
|
999
|
+
|
1000
|
+
def node_desc(old, new, par)
|
1001
|
+
#$stderr.puts "node_desc #{old.inspect} #{new.inspect} #{par.inspect}"
|
1002
|
+
|
1003
|
+
if tag(old) != tag(new) || !compare_attrs(old, new)
|
1004
|
+
[removed(old, par), added(new, par)]
|
1005
|
+
else # only body changed (maybe)
|
1006
|
+
bold = body(old)
|
1007
|
+
bnew = body(new)
|
1008
|
+
if bold && bnew
|
1009
|
+
desc(bold, bnew, domid(old))
|
1010
|
+
elsif bold && !bnew
|
1011
|
+
[removed(bold, domid(old))]
|
1012
|
+
else
|
1013
|
+
[added(bnew, domid(old))]
|
1014
|
+
end
|
1015
|
+
end
|
1016
|
+
end
|
1017
|
+
|
1018
|
+
def desc(old, new, par)
|
1019
|
+
#$stderr.puts "desc #{old.inspect} #{new.inspect} #{par.inspect}"
|
1020
|
+
#$stderr.puts "desc #{self.class.name} #{par.inspect}"
|
1021
|
+
|
1022
|
+
if old.class != new.class
|
1023
|
+
[added(new, par), removed(old, par)]
|
1024
|
+
elsif old.is_a?(Array)
|
1025
|
+
dom_node?(old) ? node_desc(old, new, domid(old)) : array_desc(old, new, par)
|
1026
|
+
elsif old.is_a?(String)
|
1027
|
+
if old != new
|
1028
|
+
[added(new, par), removed(old, par)]
|
1029
|
+
end
|
1030
|
+
else
|
1031
|
+
raise TypeError.new("Unknown object in the DOM: #{old.class} #{new.class}")
|
1032
|
+
end
|
1017
1033
|
end
|
1018
1034
|
end
|
1019
1035
|
|
1020
|
-
class
|
1021
|
-
attr_reader :
|
1022
|
-
|
1023
|
-
|
1024
|
-
|
1025
|
-
def initialize(page)
|
1026
|
-
@id = String.random(10, :alpha => true)
|
1027
|
-
@page = page
|
1028
|
-
@callbacks = {}
|
1029
|
-
@params = page.params if page
|
1030
|
-
@dead = false
|
1031
|
-
end
|
1032
|
-
|
1033
|
-
def erb(*args, &blk)
|
1034
|
-
ERBRunner.new(@page.request, nil, @params).erb(*args, &blk)
|
1036
|
+
class ExpansionHelper
|
1037
|
+
attr_reader :components
|
1038
|
+
|
1039
|
+
def initialize
|
1040
|
+
@components = Set.new
|
1035
1041
|
end
|
1036
|
-
|
1037
|
-
def
|
1038
|
-
|
1042
|
+
|
1043
|
+
def component_used(c)
|
1044
|
+
@components << c
|
1039
1045
|
end
|
1040
|
-
|
1041
|
-
def halt; @page.halt; end
|
1046
|
+
end
|
1042
1047
|
|
1043
|
-
|
1044
|
-
def
|
1048
|
+
class UpdateDispatcher
|
1049
|
+
def self.enable
|
1050
|
+
Goat::NotificationCenter.subscribe(self, :txn_start, 'type' => 'txn_start')
|
1051
|
+
Goat::NotificationCenter.subscribe(self, :txn_complete, 'type' => 'txn_complete')
|
1052
|
+
end
|
1053
|
+
|
1054
|
+
@active_txns = {}
|
1055
|
+
|
1056
|
+
def self.txn_start(n)
|
1057
|
+
@active_txns[n['txn']] = {
|
1058
|
+
:page_updates => [],
|
1059
|
+
:other_updates => [],
|
1060
|
+
:pgid => n['pgid']
|
1061
|
+
}
|
1062
|
+
end
|
1063
|
+
|
1064
|
+
def self.txn_complete(n)
|
1065
|
+
finish_txn(n['txn'])
|
1066
|
+
end
|
1067
|
+
|
1068
|
+
def self.finish_txn(txn_id)
|
1069
|
+
if txn = @active_txns[txn_id]
|
1070
|
+
pgid = txn[:pgid]
|
1071
|
+
dispatch_updates(txn_id, pgid, txn[:page_updates])
|
1072
|
+
txn[:other_updates].each{|u| dispatch_update(u)}
|
1073
|
+
end
|
1074
|
+
end
|
1045
1075
|
|
1046
|
-
def
|
1047
|
-
|
1048
|
-
|
1049
|
-
|
1050
|
-
|
1076
|
+
def self.dispatch_updates(txn, pgid, ups)
|
1077
|
+
StateSrvClient.components_updated(
|
1078
|
+
txn,
|
1079
|
+
pgid,
|
1080
|
+
ups
|
1051
1081
|
)
|
1052
1082
|
end
|
1053
|
-
|
1054
|
-
def
|
1055
|
-
|
1056
|
-
|
1057
|
-
|
1083
|
+
|
1084
|
+
def self.dispatch_update(update)
|
1085
|
+
StateSrvClient.component_updated(nil, update.skel.pgid, update)
|
1086
|
+
end
|
1087
|
+
|
1088
|
+
def self.component_updated(pgid, update)
|
1089
|
+
if Dynamic.variable?(:txn)
|
1090
|
+
txn = Dynamic[:txn]
|
1091
|
+
txn_pgid = Dynamic[:txn_pgid]
|
1092
|
+
if updates = @active_txns[txn]
|
1093
|
+
if pgid == txn_pgid
|
1094
|
+
updates[:page_updates] << update
|
1095
|
+
else
|
1096
|
+
updates[:other_updates] << update
|
1097
|
+
end
|
1098
|
+
else
|
1099
|
+
raise "Got an update for an un-started txn (#{txn.inspect}, #{pgid})"
|
1100
|
+
end
|
1101
|
+
else
|
1102
|
+
dispatch_update(update)
|
1103
|
+
end
|
1104
|
+
end
|
1105
|
+
end
|
1106
|
+
|
1107
|
+
class Component
|
1108
|
+
attr_reader :id, :handlers, :page, :params, :initargs
|
1109
|
+
|
1110
|
+
include HTMLHelpers # sure just shit it in with everything else - TODO remove?
|
1111
|
+
|
1112
|
+
def self.live_enabled; @live_enabled = true; end
|
1113
|
+
def self.live_enabled?; @live_enabled; end
|
1114
|
+
def live_enabled?; self.class.live_enabled?; end
|
1115
|
+
|
1116
|
+
def self.rerender_and_update(spec)
|
1117
|
+
cls = self.name
|
1118
|
+
|
1119
|
+
Profile.request_start
|
1120
|
+
|
1121
|
+
Profile.in(:render_and_update)
|
1122
|
+
StateSrvClient.live_components(cls, spec).each do |skel|
|
1123
|
+
Profile.in(:cls_rerender)
|
1124
|
+
rerender(skel)
|
1125
|
+
Profile.out(:cls_rerender)
|
1126
|
+
end
|
1127
|
+
Profile.out(:render_and_update)
|
1128
|
+
|
1129
|
+
Profile.request_end
|
1130
|
+
end
|
1131
|
+
|
1132
|
+
def self.rerender(skel)
|
1133
|
+
Profile.in(:create_component)
|
1134
|
+
c = Kernel.fetch_class(skel.cls).from_skel(skel)
|
1135
|
+
c.deserialize(skel.state)
|
1136
|
+
Profile.out(:create_component)
|
1137
|
+
|
1138
|
+
c.rerender
|
1139
|
+
end
|
1140
|
+
|
1141
|
+
def update
|
1142
|
+
rerender
|
1143
|
+
end
|
1144
|
+
|
1145
|
+
def rerender
|
1146
|
+
Profile.in(:rerender)
|
1147
|
+
|
1148
|
+
helper = ExpansionHelper.new
|
1149
|
+
|
1150
|
+
Profile.in(:expansion)
|
1151
|
+
Dynamic.let(:expander => helper) do
|
1152
|
+
@expanded_dom = self.expanded_dom
|
1153
|
+
end
|
1154
|
+
Profile.out(:expansion)
|
1155
|
+
|
1156
|
+
Profile.in(:diff)
|
1157
|
+
diff = DOMDiff.diff(@old_dom, @expanded_dom, @id)
|
1158
|
+
Profile.out(:diff)
|
1159
|
+
|
1160
|
+
if diff.size == 0
|
1161
|
+
# pass
|
1162
|
+
$stderr.puts "No changes"
|
1163
|
+
elsif diff.size < 0 # TODO constant
|
1164
|
+
rerender_partially(diff)
|
1165
|
+
else
|
1166
|
+
rerender_fully(helper.components)
|
1167
|
+
end
|
1168
|
+
rescue JustRerenderError # partial updater gave up
|
1169
|
+
rerender_fully(helper.components)
|
1170
|
+
ensure
|
1171
|
+
Profile.out(:rerender)
|
1172
|
+
end
|
1173
|
+
|
1174
|
+
def pgid=(id); @pgid = id; end
|
1175
|
+
|
1176
|
+
def skel
|
1177
|
+
ComponentSkeleton.new(
|
1178
|
+
@pgid,
|
1179
|
+
self.class.name,
|
1180
|
+
@id,
|
1181
|
+
live_spec,
|
1182
|
+
serialize,
|
1183
|
+
@expanded_dom
|
1058
1184
|
)
|
1059
1185
|
end
|
1060
1186
|
|
1061
|
-
def
|
1062
|
-
|
1063
|
-
|
1064
|
-
|
1187
|
+
def self.from_skel(skel)
|
1188
|
+
inst = Goat.new_without_initialize(self)
|
1189
|
+
inst.load_skel(skel)
|
1190
|
+
inst
|
1191
|
+
end
|
1192
|
+
|
1193
|
+
def load_skel(skel)
|
1194
|
+
@id = skel.id
|
1195
|
+
@pgid = skel.pgid
|
1196
|
+
@old_dom = skel.dom
|
1197
|
+
@live_spec = skel.spec
|
1198
|
+
end
|
1199
|
+
|
1200
|
+
def rerender_partially(diff)
|
1201
|
+
$stderr.puts "should do #{diff.inspect} in #{self.class.name}/#{@id}"
|
1202
|
+
StateSrvClient.component_updated(
|
1203
|
+
self.class.name,
|
1204
|
+
@id,
|
1205
|
+
@expanded_dom
|
1065
1206
|
)
|
1066
1207
|
end
|
1067
|
-
|
1068
|
-
def
|
1069
|
-
|
1208
|
+
|
1209
|
+
def rerender_fully(cs)
|
1210
|
+
Profile.in(:distillation)
|
1211
|
+
distiller = DOMDistiller.new(@expanded_dom, cs + [self])
|
1212
|
+
js = distiller.script
|
1213
|
+
css = distiller.style
|
1214
|
+
Profile.out(:distillation)
|
1215
|
+
|
1216
|
+
Profile.in(:update_send)
|
1217
|
+
|
1218
|
+
UpdateDispatcher.component_updated(
|
1219
|
+
@pgid,
|
1220
|
+
ComponentUpdate.new(
|
1221
|
+
self.skel,
|
1222
|
+
[{'type' => 'rep',
|
1223
|
+
'html' => dom_html(@expanded_dom),
|
1224
|
+
'js' => js,
|
1225
|
+
'css' => css,
|
1226
|
+
'parent' => @id,
|
1227
|
+
'position' => nil}]))
|
1228
|
+
|
1229
|
+
Profile.out(:update_send)
|
1070
1230
|
end
|
1071
|
-
|
1072
|
-
def
|
1073
|
-
@
|
1231
|
+
|
1232
|
+
def initialize
|
1233
|
+
@id = 'dom_' + String.random(10)
|
1074
1234
|
end
|
1075
|
-
|
1076
|
-
def dead?; @dead; end
|
1077
1235
|
|
1078
|
-
def
|
1079
|
-
|
1236
|
+
def live_for(spec)
|
1237
|
+
@live_spec = spec
|
1080
1238
|
end
|
1081
1239
|
|
1082
|
-
def
|
1083
|
-
|
1240
|
+
def serialize
|
1241
|
+
{}
|
1084
1242
|
end
|
1085
1243
|
|
1086
|
-
def
|
1244
|
+
def deserialize(state)
|
1245
|
+
end
|
1087
1246
|
|
1088
|
-
def
|
1089
|
-
|
1090
|
-
c = @callbacks[k].call
|
1091
|
-
c[:target].send(c[:method], *c[:args])
|
1247
|
+
def live_spec
|
1248
|
+
@live_spec ? @live_spec : raise("No live_spec specified")
|
1092
1249
|
end
|
1093
|
-
|
1250
|
+
|
1251
|
+
def erb(*args, &blk)
|
1252
|
+
ERBRunner.new(nil, nil, @params).erb(*args, &blk)
|
1253
|
+
end
|
1254
|
+
|
1255
|
+
def partial_erb(*args, &blk)
|
1256
|
+
ERBRunner.new(nil, nil, @params).partial_erb(*args, &blk)
|
1257
|
+
end
|
1258
|
+
|
1259
|
+
def id; @id; end
|
1260
|
+
|
1094
1261
|
def self.script(script); @script = AutoBind.process(script); end
|
1095
1262
|
def script(script); @script = AutoBind.process(script); end
|
1096
|
-
def self.__script; @script; end
|
1263
|
+
def self.__script; @script; end
|
1097
1264
|
def __script; @script; end
|
1098
|
-
|
1265
|
+
|
1099
1266
|
def wire_script
|
1100
1267
|
"Goat.wireComponent('#{id}', #{clientside_instance})"
|
1101
1268
|
end
|
1102
|
-
|
1269
|
+
|
1103
1270
|
def self.css(css); @css = css; end
|
1104
|
-
def css(css); @css = css; end
|
1271
|
+
def css(css); @css = css; end
|
1105
1272
|
def self.__css; @css; end
|
1106
|
-
def __css; @css; end
|
1107
|
-
|
1273
|
+
def __css; @css; end
|
1274
|
+
|
1108
1275
|
def self.clientside(js)
|
1109
1276
|
script(js)
|
1110
1277
|
@wired = true
|
1111
1278
|
end
|
1112
|
-
|
1279
|
+
|
1113
1280
|
def self.wired?; @wired; end
|
1114
|
-
|
1281
|
+
|
1115
1282
|
def clientside_args; []; end
|
1116
|
-
|
1283
|
+
|
1117
1284
|
def clientside_instance
|
1118
|
-
args = [id,
|
1119
|
-
"new #{self.class.name}('#{self.class.name}', #{args})"
|
1285
|
+
args = [id, clientside_args].to_json[1..-2]
|
1286
|
+
"(new #{self.class.name}('#{self.class.name}', #{args}))"
|
1120
1287
|
end
|
1121
|
-
|
1122
|
-
def self.rpc(name, &blk)
|
1288
|
+
|
1289
|
+
def self.rpc(name, opts={}, &blk)
|
1123
1290
|
Goat.rpc_handlers[self.name.to_s] ||= {}
|
1124
|
-
Goat.rpc_handlers[self.name.to_s][name.to_s] =
|
1125
|
-
|
1126
|
-
App.send(:define_method, "
|
1127
|
-
|
1128
|
-
|
1129
|
-
|
1291
|
+
Goat.rpc_handlers[self.name.to_s][name.to_s] = opts
|
1292
|
+
|
1293
|
+
App.send(:define_method, "rpc_#{name}", blk)
|
1294
|
+
end
|
1295
|
+
|
1296
|
+
def self.live_rpc(name, opts={}, &blk)
|
1297
|
+
rpc(name, opts.merge(:live => true), &blk)
|
1298
|
+
end
|
1299
|
+
|
1300
|
+
def self.get_rpc(name, opts={}, &blk)
|
1301
|
+
rpc(name, opts.merge(:is_get => true), &blk)
|
1130
1302
|
end
|
1131
|
-
|
1303
|
+
|
1132
1304
|
def component_name_hierarchy(cls=self.class)
|
1133
1305
|
if cls == Component
|
1134
1306
|
[]
|
@@ -1136,18 +1308,28 @@ module Goat
|
|
1136
1308
|
component_name_hierarchy(cls.superclass) + [cls.name]
|
1137
1309
|
end
|
1138
1310
|
end
|
1139
|
-
|
1311
|
+
|
1312
|
+
def inject_prefixes(dom)
|
1313
|
+
DOMTools.inject_prefixes(self.id, dom)
|
1314
|
+
end
|
1315
|
+
|
1140
1316
|
def component(body)
|
1141
|
-
[:div, {:id => id, :class => component_name_hierarchy.join(' ')},
|
1317
|
+
[:div, {:id => id, :class => component_name_hierarchy.join(' ')},
|
1318
|
+
body]
|
1319
|
+
end
|
1320
|
+
|
1321
|
+
def expanded_dom
|
1322
|
+
@expanded_dom ||= DOMTools.expanded_dom(inject_prefixes(self.dom))
|
1142
1323
|
end
|
1143
1324
|
|
1144
|
-
def
|
1325
|
+
def dom_as_expanded
|
1326
|
+
@expanded_dom
|
1327
|
+
end
|
1145
1328
|
|
1146
|
-
def
|
1147
|
-
HTMLBuilder.new(
|
1148
|
-
(__css ? "<style>#{scoped_css}</style>" : '')
|
1329
|
+
def dom_html(dom)
|
1330
|
+
DOMTools::HTMLBuilder.new(dom).html
|
1149
1331
|
end
|
1150
|
-
|
1332
|
+
|
1151
1333
|
def self.scope_css(css, prefix, dot_or_hash)
|
1152
1334
|
# #%foo, .%bar, etc
|
1153
1335
|
rep = css.gsub(/(.)%(\w+)([\ \t\{])/) do |str|
|
@@ -1158,13 +1340,13 @@ module Goat
|
|
1158
1340
|
end
|
1159
1341
|
rep.gsub(/(^|\W)%([\W\{])/) do |str|
|
1160
1342
|
"#{$1}#{dot_or_hash}#{prefix}#{$2}"
|
1161
|
-
end
|
1343
|
+
end
|
1162
1344
|
end
|
1163
1345
|
|
1164
1346
|
def self.scoped_css
|
1165
1347
|
scope_css(self.__css, self.name, '.')
|
1166
1348
|
end
|
1167
|
-
|
1349
|
+
|
1168
1350
|
def scoped_css
|
1169
1351
|
self.class.scope_css(self.__css, @id, '#')
|
1170
1352
|
end
|
@@ -1172,7 +1354,7 @@ module Goat
|
|
1172
1354
|
def model_changed(item, notif)
|
1173
1355
|
render
|
1174
1356
|
end
|
1175
|
-
|
1357
|
+
|
1176
1358
|
def register_callback(callback)
|
1177
1359
|
# if @callbacks.values.include?(callback)
|
1178
1360
|
# @callbacks.to_a.detect{|k, v| k if v == callback}
|
@@ -1182,33 +1364,7 @@ module Goat
|
|
1182
1364
|
key
|
1183
1365
|
# end
|
1184
1366
|
end
|
1185
|
-
|
1186
|
-
def children; []; end
|
1187
|
-
|
1188
|
-
def all_components
|
1189
|
-
[self] + self.children
|
1190
|
-
end
|
1191
|
-
|
1192
|
-
def to_html(builder, comp)
|
1193
|
-
processed_html # for html.rb
|
1194
|
-
end
|
1195
|
-
|
1196
|
-
script File.read(File.join(File.dirname(__FILE__), 'goat/js/component.js'))
|
1197
|
-
end
|
1198
|
-
|
1199
|
-
class Model
|
1200
|
-
def initialize
|
1201
|
-
@delegates = Set.new
|
1202
|
-
end
|
1203
|
-
|
1204
|
-
def data_changed(notif=nil)
|
1205
|
-
delegates.each {|d| d.model_changed(self, notif)}
|
1206
|
-
end
|
1207
1367
|
|
1208
|
-
|
1209
|
-
|
1210
|
-
def add_delegate(d)
|
1211
|
-
@delegates << d
|
1212
|
-
end
|
1368
|
+
script File.read(File.join(File.dirname(__FILE__), 'goat/js/component.js'))
|
1213
1369
|
end
|
1214
1370
|
end
|