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