goat 0.3.45 → 0.3.46
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/bin/state-srv +23 -11
- data/goat.gemspec +2 -2
- data/lib/goat.rb +96 -208
- data/lib/goat/common.rb +13 -4
- data/lib/goat/dom.rb +627 -0
- data/lib/goat/extn.rb +1 -1
- data/lib/goat/goat.js +61 -39
- data/lib/goat/state-srv.rb +47 -7
- metadata +5 -5
- data/lib/goat/html.rb +0 -301
data/lib/goat/extn.rb
CHANGED
@@ -39,7 +39,7 @@ class Object
|
|
39
39
|
while c
|
40
40
|
cs << c
|
41
41
|
return cs if c == upto
|
42
|
-
raise "#{
|
42
|
+
raise "#{start.inspect} isn't in the hierachy of #{upto}" if c == Object && upto != Object
|
43
43
|
|
44
44
|
c = c.superclass
|
45
45
|
end
|
data/lib/goat/goat.js
CHANGED
@@ -53,34 +53,40 @@ var Goat = {}
|
|
53
53
|
Goat.RT = {
|
54
54
|
version: 0,
|
55
55
|
pendingTxns: {},
|
56
|
+
completedTxns: {},
|
56
57
|
ops: {},
|
57
58
|
consecutiveFailures: 0
|
58
59
|
};
|
59
60
|
|
60
61
|
(function(rt) {
|
61
62
|
function updateFailed(msg) {
|
62
|
-
|
63
|
+
Goat.error('updateFailed', msg);
|
63
64
|
}
|
64
65
|
|
65
66
|
function node(par, pos) {
|
66
|
-
|
67
|
+
Goat.debug('node', par, pos, pos === null)
|
68
|
+
if(pos === null || pos === undefined)
|
67
69
|
return $('#' + par);
|
68
70
|
else
|
69
71
|
return $("#" + par + " > :nth-child(" + (pos + 1) + ")");
|
70
72
|
}
|
71
73
|
|
72
74
|
function removeNode(par, pos, html) {
|
75
|
+
Goat.debug('removeNode', node(par, pos));
|
73
76
|
node(par, pos).remove();
|
74
77
|
}
|
75
78
|
|
76
79
|
function addNode(par, pos, html) {
|
77
|
-
node(par, pos)
|
80
|
+
Goat.debug('addNode', node(par, pos), html);
|
81
|
+
if(pos == 0)
|
82
|
+
node(par).prepend(html)
|
83
|
+
else
|
84
|
+
node(par, pos - 1).after(html);
|
78
85
|
}
|
79
86
|
|
80
87
|
function replaceNode(par, pos, html) {
|
81
|
-
|
82
|
-
|
83
|
-
node(par, pos).html(html);
|
88
|
+
Goat.warn('Replace fault', node(par, pos));
|
89
|
+
node(par, pos).replaceWith(html);
|
84
90
|
if(pos === null)
|
85
91
|
delete Goat.components[par];
|
86
92
|
}
|
@@ -92,9 +98,10 @@ Goat.RT = {
|
|
92
98
|
var html = up.html;
|
93
99
|
var js = up.js;
|
94
100
|
var css = up.css;
|
101
|
+
var id = up.id; // only for rem
|
95
102
|
|
96
103
|
if(t == "rem") {
|
97
|
-
removeNode(par, pos,
|
104
|
+
removeNode(par, pos, id);
|
98
105
|
} else if(t == "add") {
|
99
106
|
addNode(par, pos, html);
|
100
107
|
} else if(t == "rep") {
|
@@ -121,8 +128,6 @@ Goat.RT = {
|
|
121
128
|
script.innerHTML = js;
|
122
129
|
addToHead(script);
|
123
130
|
Goat.loadComponents();
|
124
|
-
/*console.log("Eval " + js);
|
125
|
-
eval(js);*/
|
126
131
|
}
|
127
132
|
|
128
133
|
function updateReceived(m) {
|
@@ -164,31 +169,30 @@ Goat.RT = {
|
|
164
169
|
return pending;
|
165
170
|
}
|
166
171
|
|
167
|
-
function completeTxn(
|
168
|
-
if(rt.pendingTxns[
|
169
|
-
|
172
|
+
function completeTxn(txnid) {
|
173
|
+
if(rt.pendingTxns[txnid]) {
|
174
|
+
var txn = rt.ops[txnid];
|
175
|
+
|
176
|
+
delete rt.pendingTxns[txnid];
|
177
|
+
rt.completedTxns[txnid] = true
|
170
178
|
|
171
179
|
if(pendingCount() == 0) {
|
172
180
|
Goat.LoadingIndicator.hide();
|
173
181
|
}
|
174
182
|
|
175
183
|
if(txn.complete) txn.complete();
|
184
|
+
} else if(rt.completedTxns[txnid]) {
|
185
|
+
Goat.warn('Duplicate txn_complete message for txn ' + txnid);
|
176
186
|
} else {
|
177
|
-
|
187
|
+
Goat.error('Can\'t complete txn ' + txn + ': no such txn was started');
|
178
188
|
}
|
179
189
|
}
|
180
190
|
|
181
|
-
function txnCompleteMsg(msg) {
|
182
|
-
console.log('txnCompleteMsg');
|
183
|
-
console.log(msg);
|
184
|
-
completeTxn(msg['txn']);
|
185
|
-
}
|
186
|
-
|
187
191
|
rt.updateReceived = updateReceived;
|
188
192
|
rt.node = node;
|
189
193
|
rt.newTxn = newTxn;
|
190
194
|
rt.beginTxn = beginTxn;
|
191
|
-
rt.
|
195
|
+
rt.completeTxn = completeTxn;
|
192
196
|
|
193
197
|
return rt;
|
194
198
|
})(Goat.RT);
|
@@ -203,7 +207,7 @@ Goat.LoadingIndicator = {
|
|
203
207
|
(function(ld) {
|
204
208
|
function initIndicator() {
|
205
209
|
if(!ld.url || !ld.where) {
|
206
|
-
|
210
|
+
Goat.error("Loading indicator not configured");
|
207
211
|
return;
|
208
212
|
}
|
209
213
|
ld.indicator = $("<img id=\"loading_indicator\" src=\"" + ld.url + "\">");
|
@@ -278,7 +282,7 @@ Goat.RPC = Class.extend({
|
|
278
282
|
},
|
279
283
|
|
280
284
|
rpcFailure: function() {
|
281
|
-
|
285
|
+
Goat.error("RPC error: couldn't load " + this.name);
|
282
286
|
alert("An error was encountered connecting to the server. This could be because of a problem with your internet connection, or with the server itself. You should try refreshing this page.");
|
283
287
|
if(this.rpcError) this.rpcError(this);
|
284
288
|
},
|
@@ -351,14 +355,28 @@ $.extend(Goat, {
|
|
351
355
|
activeChannel: null,
|
352
356
|
pageDead: false,
|
353
357
|
components: {},
|
354
|
-
page_id: null
|
358
|
+
page_id: null,
|
355
359
|
});
|
356
360
|
|
357
361
|
$.extend(Goat, {
|
362
|
+
isLocal: function() {
|
363
|
+
return window.location.host.match(/127\.0\.0\.1/) || window.location.host.match(/localhost/)
|
364
|
+
},
|
365
|
+
|
366
|
+
console_apply: function(f, a) {
|
367
|
+
if(this.isLocal() || f == 'warn' || f == 'error')
|
368
|
+
console[f].apply(console, a);
|
369
|
+
},
|
370
|
+
|
371
|
+
debug: function() { this.console_apply('debug', arguments); },
|
372
|
+
warn: function() { this.console_apply('warn', arguments); },
|
373
|
+
error: function() { this.console_apply('error', arguments); },
|
374
|
+
log: function() { this.console_apply('log', arguments); },
|
375
|
+
|
358
376
|
closure: function(fn) { return closure(this, fn); },
|
359
377
|
|
360
378
|
channelURL: function() {
|
361
|
-
if(
|
379
|
+
if(this.isLocal())
|
362
380
|
return 'http://127.0.0.1:8050/channel';
|
363
381
|
else
|
364
382
|
return '/channel';
|
@@ -378,34 +396,34 @@ $.extend(Goat, {
|
|
378
396
|
},
|
379
397
|
|
380
398
|
messageReceived: function(m) {
|
381
|
-
|
399
|
+
Goat.log('messageReceived', m.type, m);
|
382
400
|
|
383
401
|
var t = m['type'];
|
384
402
|
if(t == 'component_updated') {
|
385
403
|
Goat.RT.updateReceived(m);
|
386
404
|
} else if(t == 'redirect') {
|
387
405
|
if(m["location"]) {
|
388
|
-
|
406
|
+
Goat.log('Redirecting to ' + m['location']);
|
389
407
|
sleep(2);
|
390
408
|
window.location = m['location'];
|
391
409
|
} else {
|
392
|
-
|
410
|
+
Goat.error('Tried to redirect to null. Buggy message: ', m);
|
393
411
|
}
|
394
412
|
} else if(t == 'page_expired') {
|
395
413
|
alert("Due to inactivity, you'll need to refresh this page.");
|
396
414
|
Goat.setPageDead();
|
397
415
|
} else if(t == 'txn_complete') {
|
398
|
-
Goat.RT.
|
416
|
+
Goat.RT.completeTxn(m['txn']);
|
399
417
|
} else if(t == 'alert') {
|
400
418
|
this.showAlert(m);
|
401
419
|
} else {
|
402
|
-
|
420
|
+
Goat.error('Unknown message type: ', m);
|
403
421
|
}
|
404
422
|
},
|
405
423
|
|
406
424
|
channelDataReceived: function(data) {
|
407
425
|
if(!data) {
|
408
|
-
|
426
|
+
Goat.error('Null data received in channelDataReceived')
|
409
427
|
this.reopenChannel();
|
410
428
|
return;
|
411
429
|
}
|
@@ -415,12 +433,11 @@ $.extend(Goat, {
|
|
415
433
|
if(data['messages']) {
|
416
434
|
$(data['messages']).each(this.closure(function(i, m) { this.messageReceived(m) }));
|
417
435
|
} else {
|
418
|
-
|
436
|
+
Goat.error('Bad channel data: ', data)
|
419
437
|
}
|
420
438
|
},
|
421
439
|
|
422
440
|
reopenChannel: function() {
|
423
|
-
console.log("reopenChannel()");
|
424
441
|
var fails = this.channelOpenFails;
|
425
442
|
setTimeout(function() { Goat.openChannel(); }, Math.min(30000, Math.max(1000, Math.pow(1.5, fails))));
|
426
443
|
this.channelOpenFails++;
|
@@ -428,14 +445,14 @@ $.extend(Goat, {
|
|
428
445
|
|
429
446
|
openChannel: function() {
|
430
447
|
if(Goat.pageDead) {
|
431
|
-
|
448
|
+
Goat.log("not opening channel: page dead");
|
432
449
|
return;
|
433
450
|
}
|
434
451
|
|
435
|
-
|
452
|
+
Goat.debug("opening channel");
|
436
453
|
|
437
454
|
if(this.activeChannel) {
|
438
|
-
|
455
|
+
Goat.error('can\'t open channel: channel already open');
|
439
456
|
return;
|
440
457
|
}
|
441
458
|
|
@@ -456,7 +473,7 @@ $.extend(Goat, {
|
|
456
473
|
function injectScriptTag(tag) {
|
457
474
|
|
458
475
|
var logError = closure(this, function() {
|
459
|
-
|
476
|
+
Goat.error('Goat: error loading ', src);
|
460
477
|
});
|
461
478
|
|
462
479
|
var base = Goat.channelURL();
|
@@ -487,8 +504,7 @@ $.extend(Goat, {
|
|
487
504
|
cache: false,
|
488
505
|
success: function(data) {},
|
489
506
|
error: function(req, stat, err) {
|
490
|
-
|
491
|
-
console.log({req: req, status: stat, err: err});
|
507
|
+
Goat.error('submit form error', req, stat, err);
|
492
508
|
}
|
493
509
|
});
|
494
510
|
|
@@ -499,6 +515,12 @@ $.extend(Goat, {
|
|
499
515
|
this.components[id] = obj;
|
500
516
|
},
|
501
517
|
|
518
|
+
init: function() {
|
519
|
+
if(Goat.isLocal())
|
520
|
+
Goat.log('Running in debug mode');
|
521
|
+
this.loadComponents();
|
522
|
+
},
|
523
|
+
|
502
524
|
loadComponents: function() {
|
503
525
|
$.each(Goat.components, function(_, c) {
|
504
526
|
if(!c.isLoaded())
|
@@ -508,7 +530,7 @@ $.extend(Goat, {
|
|
508
530
|
});
|
509
531
|
|
510
532
|
$(document).ready(function() {
|
511
|
-
Goat.
|
533
|
+
Goat.init();
|
512
534
|
setTimeout(function() { if(Goat.page_id) Goat.openChannel(); }, 1000);
|
513
535
|
});
|
514
536
|
|
data/lib/goat/state-srv.rb
CHANGED
@@ -22,6 +22,14 @@ module Goat
|
|
22
22
|
send_message('register_page', 'pgid' => pgid, 'user' => user, 'components' => cs.map(&:to_hash))
|
23
23
|
end
|
24
24
|
|
25
|
+
def self.update_page(pgid, txn, updates)
|
26
|
+
send_message('update_page',
|
27
|
+
'pgid' => pgid,
|
28
|
+
'txn' => txn,
|
29
|
+
'updates' => updates.map(&:to_hash)
|
30
|
+
)
|
31
|
+
end
|
32
|
+
|
25
33
|
def self.live_components(cls, spec)
|
26
34
|
resp = send_message('live_components', {'class' => cls, 'spec' => spec}, true)
|
27
35
|
JSON.load(resp)['response'].map{|h| ComponentSkeleton.from_hash(h)}
|
@@ -36,17 +44,23 @@ module Goat
|
|
36
44
|
components_updated(txn, pgid, [update])
|
37
45
|
end
|
38
46
|
|
47
|
+
def self.components_update_completed(cs)
|
48
|
+
puts "Ack for components_updated: #{cs.inspect}"
|
49
|
+
end
|
50
|
+
|
39
51
|
def self.components_updated(txn, pgid, updates)
|
40
|
-
send_message('components_updated',
|
52
|
+
send_message('components_updated', {
|
41
53
|
'txn' => txn,
|
42
54
|
'pgid' => pgid,
|
43
|
-
'updates' => updates.map(&:to_hash)
|
55
|
+
'updates' => updates.map(&:to_hash)},
|
56
|
+
true
|
44
57
|
)
|
45
58
|
end
|
46
59
|
end
|
47
60
|
|
48
61
|
class NoStateSrvConnectionError < RuntimeError; end
|
49
62
|
|
63
|
+
# this is an ugly mix of sync and async stuff
|
50
64
|
class StateSrvConnection < EM::Connection
|
51
65
|
include EM::P::LineText2
|
52
66
|
|
@@ -66,13 +80,29 @@ module Goat
|
|
66
80
|
self.connect(@host, @port)
|
67
81
|
end
|
68
82
|
|
69
|
-
def self.
|
70
|
-
|
71
|
-
|
72
|
-
|
83
|
+
def self.reconnect_sync
|
84
|
+
@sock = TCPSocket.open(@host, @port)
|
85
|
+
end
|
86
|
+
|
87
|
+
def self.sync_connection_active?
|
88
|
+
@sock && !@sock.closed?
|
89
|
+
end
|
90
|
+
|
91
|
+
def self.send_message_sync(msg, failed_last_time=false)
|
92
|
+
reconnect_sync unless sync_connection_active?
|
93
|
+
@sock.write(msg.to_json + "\n")
|
94
|
+
resp = @sock.readline
|
73
95
|
Goat.logd("=> #{resp.inspect}") if $verbose
|
74
|
-
s.close
|
75
96
|
resp
|
97
|
+
rescue Errno::ECONNRESET, EOFError => e
|
98
|
+
# almost certainly connection was closed and we didn't notice
|
99
|
+
if failed_last_time
|
100
|
+
raise e
|
101
|
+
else
|
102
|
+
Goat.logw "Reinitializing sync connection to state-srv (#{e.inspect})"
|
103
|
+
reconnect_sync
|
104
|
+
send_message_sync(msg, true)
|
105
|
+
end
|
76
106
|
end
|
77
107
|
|
78
108
|
def self.send_message(*args)
|
@@ -103,6 +133,16 @@ module Goat
|
|
103
133
|
end
|
104
134
|
end
|
105
135
|
|
136
|
+
def message_received(msg)
|
137
|
+
msg = msg['response']
|
138
|
+
|
139
|
+
if msg['type'] = 'update_ack'
|
140
|
+
StateSrvClient.components_update_completed(msg['components'])
|
141
|
+
else
|
142
|
+
raise "Unknown message type: #{msg['type']}"
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
106
146
|
def send_message(t, msg, sync=false)
|
107
147
|
msg = msg.merge('type' => t)
|
108
148
|
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: goat
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash:
|
4
|
+
hash: 79
|
5
5
|
prerelease: false
|
6
6
|
segments:
|
7
7
|
- 0
|
8
8
|
- 3
|
9
|
-
-
|
10
|
-
version: 0.3.
|
9
|
+
- 46
|
10
|
+
version: 0.3.46
|
11
11
|
platform: ruby
|
12
12
|
authors:
|
13
13
|
- Patrick Collison
|
@@ -15,7 +15,7 @@ autorequire:
|
|
15
15
|
bindir: bin
|
16
16
|
cert_chain: []
|
17
17
|
|
18
|
-
date: 2011-03-
|
18
|
+
date: 2011-03-09 00:00:00 +00:00
|
19
19
|
default_executable:
|
20
20
|
dependencies:
|
21
21
|
- !ruby/object:Gem::Dependency
|
@@ -151,7 +151,7 @@ files:
|
|
151
151
|
- lib/goat/dynamic.rb
|
152
152
|
- lib/goat/extn.rb
|
153
153
|
- lib/goat/goat.js
|
154
|
-
- lib/goat/
|
154
|
+
- lib/goat/dom.rb
|
155
155
|
- lib/goat/js/component.js
|
156
156
|
- lib/goat/mongo.rb
|
157
157
|
- lib/goat/net-common.rb
|
data/lib/goat/html.rb
DELETED
@@ -1,301 +0,0 @@
|
|
1
|
-
require 'rack/utils'
|
2
|
-
|
3
|
-
module Goat
|
4
|
-
module DOMTools
|
5
|
-
class Traverser
|
6
|
-
def initialize(tree, dlg, transpose)
|
7
|
-
@tree = tree
|
8
|
-
@dlg = dlg
|
9
|
-
@transpose = transpose
|
10
|
-
end
|
11
|
-
|
12
|
-
def dom_node?(node)
|
13
|
-
node.is_a?(Array) && node.first.is_a?(Symbol)
|
14
|
-
end
|
15
|
-
|
16
|
-
def tag(node); node[0]; end
|
17
|
-
def attrs(node); node[1] if node[1].is_a?(Hash); end
|
18
|
-
def body(node); node[1].is_a?(Hash) ? node[2..-1] : node[1..-1]; end
|
19
|
-
def domid(node); attrs(node) ? attrs(node)[:id] : nil; end
|
20
|
-
|
21
|
-
def to_node(tag, attrs, body)
|
22
|
-
tag = [tag]
|
23
|
-
tag << attrs if attrs
|
24
|
-
tag << body
|
25
|
-
end
|
26
|
-
|
27
|
-
def replacement_block
|
28
|
-
@rep = nil
|
29
|
-
@replacement_block ||= lambda {|new| @rep = new}
|
30
|
-
end
|
31
|
-
|
32
|
-
def traverse(node)
|
33
|
-
if node.is_a?(String)
|
34
|
-
@dlg.string(node, &replacement_block)
|
35
|
-
@rep || node
|
36
|
-
elsif node == nil
|
37
|
-
@dlg.string('', &replacement_block)
|
38
|
-
@rep || nil
|
39
|
-
elsif dom_node?(node)
|
40
|
-
@dlg.node(node, &replacement_block)
|
41
|
-
rep = @rep || node
|
42
|
-
|
43
|
-
if @transpose
|
44
|
-
if rep != node
|
45
|
-
rep
|
46
|
-
else
|
47
|
-
to_node(tag(node), attrs(node), traverse(body(node)))
|
48
|
-
end
|
49
|
-
else
|
50
|
-
traverse(body(node))
|
51
|
-
end
|
52
|
-
elsif node.is_a?(Array)
|
53
|
-
if node.size == 1
|
54
|
-
traverse(node.first)
|
55
|
-
else
|
56
|
-
if @transpose
|
57
|
-
node.map{|x| traverse(x)}
|
58
|
-
else
|
59
|
-
node.each{|x| traverse(x)}
|
60
|
-
end
|
61
|
-
end
|
62
|
-
elsif node.kind_of?(Component)
|
63
|
-
@dlg.component(node, &replacement_block)
|
64
|
-
@rep || node
|
65
|
-
else
|
66
|
-
raise "Unknown object in the dom: #{node.inspect}"
|
67
|
-
end
|
68
|
-
end
|
69
|
-
|
70
|
-
def traverse!
|
71
|
-
traverse(@tree)
|
72
|
-
end
|
73
|
-
|
74
|
-
class BlockTraverser
|
75
|
-
# would be infinitely nicer if you could just yield from a block
|
76
|
-
def initialize(blk, transpose)
|
77
|
-
@blk = blk
|
78
|
-
@transpose = transpose
|
79
|
-
end
|
80
|
-
|
81
|
-
def node(node, &blk)
|
82
|
-
if @transpose
|
83
|
-
@blk.call(node, blk)
|
84
|
-
else
|
85
|
-
@blk.call(node)
|
86
|
-
end
|
87
|
-
end
|
88
|
-
|
89
|
-
def string(str, &blk)
|
90
|
-
if @transpose
|
91
|
-
@blk.call(str, blk)
|
92
|
-
else
|
93
|
-
@blk.call(str)
|
94
|
-
end
|
95
|
-
end
|
96
|
-
|
97
|
-
def component(c, &blk)
|
98
|
-
if @transpose
|
99
|
-
@blk.call(c, blk)
|
100
|
-
else
|
101
|
-
@blk.call(c)
|
102
|
-
end
|
103
|
-
end
|
104
|
-
end
|
105
|
-
|
106
|
-
def self.traverse(tree, dlg=nil, transpose=false, &blk)
|
107
|
-
d = nil
|
108
|
-
|
109
|
-
if dlg
|
110
|
-
d = dlg
|
111
|
-
elsif blk
|
112
|
-
d = BlockTraverser.new(blk, transpose)
|
113
|
-
else
|
114
|
-
raise "Need a delegate"
|
115
|
-
end
|
116
|
-
|
117
|
-
self.new(tree, d, transpose).traverse!
|
118
|
-
end
|
119
|
-
|
120
|
-
def self.transpose(tree, dlg=nil, &blk)
|
121
|
-
traverse(tree, dlg, true, &blk)
|
122
|
-
end
|
123
|
-
end
|
124
|
-
|
125
|
-
def self.traverse(tree, dlg=nil, &blk); Traverser.traverse(tree, dlg, &blk); end
|
126
|
-
def self.transpose(tree, dlg=nil, &blk); Traverser.transpose(tree, dlg, &blk); end
|
127
|
-
|
128
|
-
def self.expanded_dom(dom)
|
129
|
-
DOMTools.transpose(dom) do |elt, update|
|
130
|
-
if elt.kind_of?(Component)
|
131
|
-
raise "Component #{elt} has no ID: was super's initialize called?" unless elt.id
|
132
|
-
Dynamic[:expander].component_used(elt)
|
133
|
-
update.call(elt.component(elt.expanded_dom))
|
134
|
-
end
|
135
|
-
end
|
136
|
-
end
|
137
|
-
|
138
|
-
def self.inject_prefixes(id, dom)
|
139
|
-
DOMTools.traverse(dom) do |elt|
|
140
|
-
if elt.kind_of?(Array) && elt.first.is_a?(Symbol) && elt[1].is_a?(Hash)
|
141
|
-
attrs = elt[1]
|
142
|
-
elt[1] = attrs.map_to_hash do |k, v|
|
143
|
-
if v.kind_of?(String)
|
144
|
-
[k, v.prefix_ns(id)]
|
145
|
-
elsif v.kind_of?(Array) && HTMLBuilder::ARRAY_ATTRS.include?(k)
|
146
|
-
[k, v]
|
147
|
-
elsif v.kind_of?(Integer) && HTMLBuilder::INTEGER_ATTRS.include?(k)
|
148
|
-
[k, v]
|
149
|
-
else
|
150
|
-
raise "Invalid object #{v.inspect} to get a prefix in dom:\n#{dom.inspect}"
|
151
|
-
end
|
152
|
-
end
|
153
|
-
end
|
154
|
-
end
|
155
|
-
dom
|
156
|
-
end
|
157
|
-
|
158
|
-
class ::String
|
159
|
-
def to_html(builder)
|
160
|
-
Rack::Utils.escape_html(self)
|
161
|
-
end
|
162
|
-
end
|
163
|
-
|
164
|
-
class ::NilClass
|
165
|
-
def to_html(builder)
|
166
|
-
''
|
167
|
-
end
|
168
|
-
end
|
169
|
-
|
170
|
-
class ::Goat::HTMLString < String
|
171
|
-
def to_html(builder)
|
172
|
-
self
|
173
|
-
end
|
174
|
-
end
|
175
|
-
|
176
|
-
class ::Array
|
177
|
-
def to_html(builder)
|
178
|
-
builder.array_to_html(self)
|
179
|
-
end
|
180
|
-
end
|
181
|
-
|
182
|
-
class InvalidBodyError < RuntimeError
|
183
|
-
attr_reader :body
|
184
|
-
|
185
|
-
def initialize(body)
|
186
|
-
super("Invalid body: #{body.inspect}")
|
187
|
-
@body = body
|
188
|
-
end
|
189
|
-
end
|
190
|
-
|
191
|
-
class HTMLBuilder
|
192
|
-
ARRAY_ATTRS = [:class]
|
193
|
-
INTEGER_ATTRS = [:colspan, :rowspan]
|
194
|
-
|
195
|
-
class TagBuilder
|
196
|
-
# TODO: gmail trick of only a single onclick() handler
|
197
|
-
|
198
|
-
def self.build(tag, attrs, body)
|
199
|
-
self.new(tag, attrs, body).dispatch
|
200
|
-
end
|
201
|
-
|
202
|
-
def initialize(tag, attrs, body)
|
203
|
-
@tag = tag
|
204
|
-
@attrs = attrs
|
205
|
-
@body = body
|
206
|
-
|
207
|
-
rewrite_attrs
|
208
|
-
end
|
209
|
-
|
210
|
-
def rewrite_attrs
|
211
|
-
new = {}
|
212
|
-
|
213
|
-
@attrs.map_to_hash do |k, v|
|
214
|
-
if k == :class && v.kind_of?(Array)
|
215
|
-
new[:class] = @attrs[:class].join(' ')
|
216
|
-
else
|
217
|
-
new[k] = v
|
218
|
-
end
|
219
|
-
end
|
220
|
-
|
221
|
-
@attrs = new
|
222
|
-
end
|
223
|
-
|
224
|
-
def build_node
|
225
|
-
[@tag, @attrs, @body]
|
226
|
-
end
|
227
|
-
|
228
|
-
def a_tag
|
229
|
-
unless @attrs.include?(:href)
|
230
|
-
@attrs[:href] = 'javascript:void(0)'
|
231
|
-
end
|
232
|
-
|
233
|
-
build_node
|
234
|
-
end
|
235
|
-
|
236
|
-
def input_tag
|
237
|
-
unless @attrs.include?(:name)
|
238
|
-
$stderr.puts "Warning: no name for <#{@tag} #{@attrs.inspect}>#{@body.inspect}</#{@tag}>"
|
239
|
-
|
240
|
-
# this is somewhat ungainly: we generate a name automatically by hashing the values of the
|
241
|
-
# other attrs. this may conflict - ideally would track per-page state for these.
|
242
|
-
# purpose is to have name-less inputs preserve their values when user goes back to page.
|
243
|
-
# webkit in safari 5 gets confused when inputs are nameless.
|
244
|
-
unless (vals = @attrs.values{|k, v| v.kind_of?(String)}).empty?
|
245
|
-
$stderr.puts "Generating a name automatically..."
|
246
|
-
@attrs[:name] = vals.join.md5[0..10]
|
247
|
-
end
|
248
|
-
end
|
249
|
-
|
250
|
-
build_node
|
251
|
-
end
|
252
|
-
|
253
|
-
def dispatch
|
254
|
-
meth = "#{@tag}_tag".to_sym
|
255
|
-
|
256
|
-
if self.respond_to?(meth)
|
257
|
-
self.send(meth)
|
258
|
-
else
|
259
|
-
build_node
|
260
|
-
end
|
261
|
-
end
|
262
|
-
end
|
263
|
-
|
264
|
-
def standalone_tags
|
265
|
-
%w{br img input}
|
266
|
-
end
|
267
|
-
|
268
|
-
def attrs_to_html(attrs)
|
269
|
-
attrs.map {|k, v| "#{k}=\"#{v}\""}.join(' ')
|
270
|
-
end
|
271
|
-
|
272
|
-
def array_to_html(ar)
|
273
|
-
if ar.first.kind_of?(Symbol)
|
274
|
-
tag = ar[0]
|
275
|
-
have_attrs = ar[1].is_a?(Hash)
|
276
|
-
attrs = have_attrs ? ar[1] : {}
|
277
|
-
body = ar[(have_attrs ? 2 : 1)..-1]
|
278
|
-
|
279
|
-
tag, attrs, body = TagBuilder.build(tag, attrs, body)
|
280
|
-
lonely = standalone_tags.include?(tag.to_s)
|
281
|
-
|
282
|
-
open_tag = "<#{tag}#{attrs.empty? ? '' : (' ' + attrs_to_html(attrs))}>"
|
283
|
-
body_html = body.empty? ? '' : body.map{|x| x.to_html(self)}.join
|
284
|
-
close_tag = (lonely ? '' : "</#{tag}>")
|
285
|
-
|
286
|
-
"#{open_tag}#{body_html}#{close_tag}"
|
287
|
-
else
|
288
|
-
ar.map{|x| x.to_html(self)}.join
|
289
|
-
end
|
290
|
-
end
|
291
|
-
|
292
|
-
def initialize(input)
|
293
|
-
@input = input
|
294
|
-
end
|
295
|
-
|
296
|
-
def html
|
297
|
-
@input.to_html(self)
|
298
|
-
end
|
299
|
-
end
|
300
|
-
end
|
301
|
-
end
|