rack-livereload 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,4 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in rack-livereload.gemspec
4
+ gemspec
@@ -0,0 +1,9 @@
1
+ # A sample Guardfile
2
+ # More info at https://github.com/guard/guard#readme
3
+
4
+ guard 'rspec', :version => 2 do
5
+ watch(%r{^spec/.+_spec\.rb$})
6
+ watch(%r{^lib/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
7
+ watch('spec/spec_helper.rb') { "spec" }
8
+ end
9
+
@@ -0,0 +1,47 @@
1
+ Hey, you've got [LiveReload](http://www.livereload.com/) in my [Rack](http://rack.rubyforge.org/)!
2
+ No need for browser extensions anymore! Just plug it in your middleware stack and go!
3
+
4
+ Use this with [guard-livereload](http://github.com/guard/guard-livereload) for maximum fun!
5
+
6
+ ## Using in...
7
+
8
+ ### Rails
9
+
10
+ In `config/environments/development.rb`:
11
+
12
+ ``` ruby
13
+ MyApp::Application.configure do
14
+ config.middleware.insert_before(Rack::Lock, Rack::LiveReload)
15
+
16
+ # ...or, change some options...
17
+
18
+ config.middleware.insert_before(
19
+ Rack::Lock, Rack::LiveReload,
20
+ :min_delay => 500,
21
+ :max_delay => 10000,
22
+ :port => 56789,
23
+ :host => 'myhost.cool.wow'
24
+ )
25
+ end
26
+ ```
27
+
28
+ ### config.ru/Sinatra
29
+
30
+ ``` ruby
31
+ require 'rack-livereload'
32
+
33
+ use Rack::LiveReload
34
+ # ...or...
35
+ use Rack::LiveReload, :min_delay => 500, ...
36
+ ```
37
+
38
+ ## How it works
39
+
40
+ The necessary `script` tag to bring in a vendored copy of [livereload.js](https://github.com/livereload/livereload-js) is
41
+ injected right before the closing `head` tag in any `text/html` pages that come through. The `script` tag is built in
42
+ such a way that the `HTTP_HOST` is used as the LiveReload host, so you can connect from external machines (say, to
43
+ `mycomputer:3000` instead of `localhost:3000`) and as long as the LiveReload port is accessible from the external machine,
44
+ you'll connect and be LiveReloading away!
45
+
46
+ As usual, super-alpha!
47
+
@@ -0,0 +1,11 @@
1
+ require "bundler/gem_tasks"
2
+
3
+ desc 'Update livereload.js'
4
+ task :update_livereload_js do
5
+ require 'httparty'
6
+
7
+ File.open('js/livereload.js', 'wb') { |fh|
8
+ fh.print HTTParty.get('https://raw.github.com/livereload/livereload-js/master/dist/livereload.js').body
9
+ }
10
+ end
11
+
@@ -0,0 +1,13 @@
1
+ require 'sinatra'
2
+ $: << 'lib'
3
+
4
+ require 'rack/livereload'
5
+
6
+ use Rack::LiveReload
7
+
8
+ get '/' do
9
+ "<html><head><title>Hi</title></head><body>Hi</body></html>"
10
+ end
11
+
12
+ run Sinatra::Application
13
+
@@ -0,0 +1,804 @@
1
+ (function() {
2
+ var __customevents = {}, __protocol = {}, __connector = {}, __timer = {}, __options = {}, __reloader = {}, __livereload = {}, __startup = {};
3
+
4
+ // customevents
5
+ (function() {
6
+ var CustomEvents;
7
+ CustomEvents = {
8
+ bind: function(element, eventName, handler) {
9
+ if (element.addEventListener) {
10
+ return element.addEventListener(eventName, handler, false);
11
+ } else if (element.attachEvent) {
12
+ element[eventName] = 1;
13
+ return element.attachEvent('onpropertychange', function(event) {
14
+ if (event.propertyName === eventName) {
15
+ return handler();
16
+ }
17
+ });
18
+ } else {
19
+ throw new Error("Attempt to attach custom event " + eventName + " to something which isn't a DOMElement");
20
+ }
21
+ },
22
+ fire: function(element, eventName) {
23
+ var event;
24
+ if (element.addEventListener) {
25
+ event = document.createEvent('HTMLEvents');
26
+ event.initEvent(eventName, true, true);
27
+ return document.dispatchEvent(event);
28
+ } else if (element.attachEvent) {
29
+ if (element[eventName]) {
30
+ return element[eventName]++;
31
+ }
32
+ } else {
33
+ throw new Error("Attempt to fire custom event " + eventName + " on something which isn't a DOMElement");
34
+ }
35
+ }
36
+ };
37
+ __customevents.bind = CustomEvents.bind;
38
+ __customevents.fire = CustomEvents.fire;
39
+ }).call(this);
40
+
41
+ // protocol
42
+ (function() {
43
+ var PROTOCOL_6, PROTOCOL_7, Parser, ProtocolError;
44
+ var __indexOf = Array.prototype.indexOf || function(item) {
45
+ for (var i = 0, l = this.length; i < l; i++) {
46
+ if (this[i] === item) return i;
47
+ }
48
+ return -1;
49
+ };
50
+ __protocol.PROTOCOL_6 = PROTOCOL_6 = 'http://livereload.com/protocols/official-6';
51
+ __protocol.PROTOCOL_7 = PROTOCOL_7 = 'http://livereload.com/protocols/official-7';
52
+ __protocol.ProtocolError = ProtocolError = (function() {
53
+ function ProtocolError(reason, data) {
54
+ this.message = "LiveReload protocol error (" + reason + ") after receiving data: \"" + data + "\".";
55
+ }
56
+ return ProtocolError;
57
+ })();
58
+ __protocol.Parser = Parser = (function() {
59
+ function Parser(handlers) {
60
+ this.handlers = handlers;
61
+ this.reset();
62
+ }
63
+ Parser.prototype.reset = function() {
64
+ return this.protocol = null;
65
+ };
66
+ Parser.prototype.process = function(data) {
67
+ var command, message, options, _ref;
68
+ try {
69
+ if (!(this.protocol != null)) {
70
+ if (data.match(/^!!ver:([\d.]+)$/)) {
71
+ this.protocol = 6;
72
+ } else if (message = this._parseMessage(data, ['hello'])) {
73
+ if (!message.protocols.length) {
74
+ throw new ProtocolError("no protocols specified in handshake message");
75
+ } else if (__indexOf.call(message.protocols, PROTOCOL_7) >= 0) {
76
+ this.protocol = 7;
77
+ } else if (__indexOf.call(message.protocols, PROTOCOL_6) >= 0) {
78
+ this.protocol = 6;
79
+ } else {
80
+ throw new ProtocolError("no supported protocols found");
81
+ }
82
+ }
83
+ return this.handlers.connected(this.protocol);
84
+ } else if (this.protocol === 6) {
85
+ message = JSON.parse(data);
86
+ if (!message.length) {
87
+ throw new ProtocolError("protocol 6 messages must be arrays");
88
+ }
89
+ command = message[0], options = message[1];
90
+ if (command !== 'refresh') {
91
+ throw new ProtocolError("unknown protocol 6 command");
92
+ }
93
+ return this.handlers.message({
94
+ command: 'reload',
95
+ path: options.path,
96
+ liveCSS: (_ref = options.apply_css_live) != null ? _ref : true
97
+ });
98
+ } else {
99
+ message = this._parseMessage(data, ['reload', 'alert']);
100
+ return this.handlers.message(message);
101
+ }
102
+ } catch (e) {
103
+ if (e instanceof ProtocolError) {
104
+ return this.handlers.error(e);
105
+ } else {
106
+ throw e;
107
+ }
108
+ }
109
+ };
110
+ Parser.prototype._parseMessage = function(data, validCommands) {
111
+ var message, _ref;
112
+ try {
113
+ message = JSON.parse(data);
114
+ } catch (e) {
115
+ throw new ProtocolError('unparsable JSON', data);
116
+ }
117
+ if (!message.command) {
118
+ throw new ProtocolError('missing "command" key', data);
119
+ }
120
+ if (_ref = message.command, __indexOf.call(validCommands, _ref) < 0) {
121
+ throw new ProtocolError("invalid command '" + message.command + "', only valid commands are: " + (validCommands.join(', ')) + ")", data);
122
+ }
123
+ return message;
124
+ };
125
+ return Parser;
126
+ })();
127
+ }).call(this);
128
+
129
+ // connector
130
+ (function() {
131
+ var Connector, PROTOCOL_6, PROTOCOL_7, Parser, _ref;
132
+ var __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
133
+ _ref = __protocol, Parser = _ref.Parser, PROTOCOL_6 = _ref.PROTOCOL_6, PROTOCOL_7 = _ref.PROTOCOL_7;
134
+ __connector.Connector = Connector = (function() {
135
+ function Connector(options, WebSocket, Timer, handlers) {
136
+ this.options = options;
137
+ this.WebSocket = WebSocket;
138
+ this.Timer = Timer;
139
+ this.handlers = handlers;
140
+ this._uri = "ws://" + this.options.host + ":" + this.options.port + "/livereload";
141
+ this._nextDelay = this.options.mindelay;
142
+ this._connectionDesired = false;
143
+ this.protocolParser = new Parser({
144
+ connected: __bind(function(protocol) {
145
+ this._handshakeTimeout.stop();
146
+ this._nextDelay = this.options.mindelay;
147
+ this._disconnectionReason = 'broken';
148
+ return this.handlers.connected(protocol);
149
+ }, this),
150
+ error: __bind(function(e) {
151
+ this.handlers.error(e);
152
+ return this._closeOnError();
153
+ }, this),
154
+ message: __bind(function(message) {
155
+ return this.handlers.message(message);
156
+ }, this)
157
+ });
158
+ this._handshakeTimeout = new Timer(__bind(function() {
159
+ if (!this._isSocketConnected()) {
160
+ return;
161
+ }
162
+ this._disconnectionReason = 'handshake-timeout';
163
+ return this.socket.close();
164
+ }, this));
165
+ this._reconnectTimer = new Timer(__bind(function() {
166
+ if (!this._connectionDesired) {
167
+ return;
168
+ }
169
+ return this.connect();
170
+ }, this));
171
+ this.connect();
172
+ }
173
+ Connector.prototype._isSocketConnected = function() {
174
+ return this.socket && this.socket.readyState === this.WebSocket.OPEN;
175
+ };
176
+ Connector.prototype.connect = function() {
177
+ this._connectionDesired = true;
178
+ if (this._isSocketConnected()) {
179
+ return;
180
+ }
181
+ if (this._reconnectTimer) {
182
+ clearTimeout(this._reconnectTimer);
183
+ }
184
+ this._disconnectionReason = 'cannot-connect';
185
+ this.protocolParser.reset();
186
+ this.handlers.connecting();
187
+ this.socket = new this.WebSocket(this._uri);
188
+ this.socket.onopen = __bind(function(e) {
189
+ return this._onopen(e);
190
+ }, this);
191
+ this.socket.onclose = __bind(function(e) {
192
+ return this._onclose(e);
193
+ }, this);
194
+ this.socket.onmessage = __bind(function(e) {
195
+ return this._onmessage(e);
196
+ }, this);
197
+ return this.socket.onerror = __bind(function(e) {
198
+ return this._onerror(e);
199
+ }, this);
200
+ };
201
+ Connector.prototype.disconnect = function() {
202
+ this._connectionDesired = false;
203
+ this._reconnectTimer.stop();
204
+ if (!this._isSocketConnected()) {
205
+ return;
206
+ }
207
+ this._disconnectionReason = 'manual';
208
+ return this.socket.close();
209
+ };
210
+ Connector.prototype._scheduleReconnection = function() {
211
+ if (!this._connectionDesired) {
212
+ return;
213
+ }
214
+ if (!this._reconnectTimer.running) {
215
+ this._reconnectTimer.start(this._nextDelay);
216
+ return this._nextDelay = Math.min(this.options.maxdelay, this._nextDelay * 2);
217
+ }
218
+ };
219
+ Connector.prototype.sendCommand = function(command) {
220
+ if (this.protocol == null) {
221
+ return;
222
+ }
223
+ return this._sendCommand(command);
224
+ };
225
+ Connector.prototype._sendCommand = function(command) {
226
+ return this.socket.send(JSON.stringify(command));
227
+ };
228
+ Connector.prototype._closeOnError = function() {
229
+ this._handshakeTimeout.stop();
230
+ this._disconnectionReason = 'error';
231
+ return this.socket.close();
232
+ };
233
+ Connector.prototype._onopen = function(e) {
234
+ this.handlers.socketConnected();
235
+ this._disconnectionReason = 'handshake-failed';
236
+ this._sendCommand({
237
+ command: 'hello',
238
+ protocols: [PROTOCOL_6, PROTOCOL_7]
239
+ });
240
+ return this._handshakeTimeout.start(this.options.handshake_timeout);
241
+ };
242
+ Connector.prototype._onclose = function(e) {
243
+ this.handlers.disconnected(this._disconnectionReason, this._nextDelay);
244
+ return this._scheduleReconnection();
245
+ };
246
+ Connector.prototype._onerror = function(e) {};
247
+ Connector.prototype._onmessage = function(e) {
248
+ return this.protocolParser.process(e.data);
249
+ };
250
+ return Connector;
251
+ })();
252
+ }).call(this);
253
+
254
+ // timer
255
+ (function() {
256
+ var Timer;
257
+ var __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
258
+ __timer.Timer = Timer = (function() {
259
+ function Timer(func) {
260
+ this.func = func;
261
+ this.running = false;
262
+ this.id = null;
263
+ this._handler = __bind(function() {
264
+ this.running = false;
265
+ this.id = null;
266
+ return this.func();
267
+ }, this);
268
+ }
269
+ Timer.prototype.start = function(timeout) {
270
+ if (this.running) {
271
+ clearTimeout(this.id);
272
+ }
273
+ this.id = setTimeout(this._handler, timeout);
274
+ return this.running = true;
275
+ };
276
+ Timer.prototype.stop = function() {
277
+ if (this.running) {
278
+ clearTimeout(this.id);
279
+ this.running = false;
280
+ return this.id = null;
281
+ }
282
+ };
283
+ return Timer;
284
+ })();
285
+ Timer.start = function(timeout, func) {
286
+ return setTimeout(func, timeout);
287
+ };
288
+ }).call(this);
289
+
290
+ // options
291
+ (function() {
292
+ var Options;
293
+ __options.Options = Options = (function() {
294
+ function Options() {
295
+ this.host = null;
296
+ this.port = 35729;
297
+ this.snipver = null;
298
+ this.ext = null;
299
+ this.extver = null;
300
+ this.mindelay = 1000;
301
+ this.maxdelay = 60000;
302
+ this.handshake_timeout = 5000;
303
+ }
304
+ Options.prototype.set = function(name, value) {
305
+ switch (typeof this[name]) {
306
+ case 'undefined':
307
+ break;
308
+ case 'number':
309
+ return this[name] = +value;
310
+ default:
311
+ return this[name] = value;
312
+ }
313
+ };
314
+ return Options;
315
+ })();
316
+ Options.extract = function(document) {
317
+ var element, keyAndValue, m, mm, options, pair, src, _i, _j, _len, _len2, _ref, _ref2;
318
+ _ref = document.getElementsByTagName('script');
319
+ for (_i = 0, _len = _ref.length; _i < _len; _i++) {
320
+ element = _ref[_i];
321
+ if ((src = element.src) && (m = src.match(/^[^:]+:\/\/(.*)\/z?livereload\.js(?:\?(.*))?$/))) {
322
+ options = new Options();
323
+ if (mm = m[1].match(/^([^\/:]+)(?::(\d+))?$/)) {
324
+ options.host = mm[1];
325
+ if (mm[2]) {
326
+ options.port = parseInt(mm[2], 10);
327
+ }
328
+ }
329
+ if (m[2]) {
330
+ _ref2 = m[2].split('&');
331
+ for (_j = 0, _len2 = _ref2.length; _j < _len2; _j++) {
332
+ pair = _ref2[_j];
333
+ if ((keyAndValue = pair.split('=')).length > 1) {
334
+ options.set(keyAndValue[0].replace(/-/g, '_'), keyAndValue.slice(1).join('='));
335
+ }
336
+ }
337
+ }
338
+ return options;
339
+ }
340
+ }
341
+ return null;
342
+ };
343
+ }).call(this);
344
+
345
+ // reloader
346
+ (function() {
347
+ var IMAGE_STYLES, Reloader, numberOfMatchingSegments, pathFromUrl, pathsMatch, pickBestMatch, splitUrl;
348
+ var __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
349
+ splitUrl = function(url) {
350
+ var hash, index, params;
351
+ if ((index = url.indexOf('#')) >= 0) {
352
+ hash = url.slice(index);
353
+ url = url.slice(0, index);
354
+ } else {
355
+ hash = '';
356
+ }
357
+ if ((index = url.indexOf('?')) >= 0) {
358
+ params = url.slice(index);
359
+ url = url.slice(0, index);
360
+ } else {
361
+ params = '';
362
+ }
363
+ return {
364
+ url: url,
365
+ params: params,
366
+ hash: hash
367
+ };
368
+ };
369
+ pathFromUrl = function(url) {
370
+ var path;
371
+ url = splitUrl(url).url;
372
+ if (url.indexOf('file://') === 0) {
373
+ path = url.replace(/^file:\/\/(localhost)?/, '');
374
+ } else {
375
+ path = url.replace(/^([^:]+:)?\/\/([^:\/]+)(:\d*)?\//, '/');
376
+ }
377
+ return decodeURIComponent(path);
378
+ };
379
+ pickBestMatch = function(path, objects, pathFunc) {
380
+ var bestMatch, object, score, _i, _len;
381
+ bestMatch = {
382
+ score: 0
383
+ };
384
+ for (_i = 0, _len = objects.length; _i < _len; _i++) {
385
+ object = objects[_i];
386
+ score = numberOfMatchingSegments(path, pathFunc(object));
387
+ if (score > bestMatch.score) {
388
+ bestMatch = {
389
+ object: object,
390
+ score: score
391
+ };
392
+ }
393
+ }
394
+ if (bestMatch.score > 0) {
395
+ return bestMatch;
396
+ } else {
397
+ return null;
398
+ }
399
+ };
400
+ numberOfMatchingSegments = function(path1, path2) {
401
+ var comps1, comps2, eqCount, len;
402
+ path1 = path1.replace(/^\/+/, '').toLowerCase();
403
+ path2 = path2.replace(/^\/+/, '').toLowerCase();
404
+ if (path1 === path2) {
405
+ return 10000;
406
+ }
407
+ comps1 = path1.split('/').reverse();
408
+ comps2 = path2.split('/').reverse();
409
+ len = Math.min(comps1.length, comps2.length);
410
+ eqCount = 0;
411
+ while (eqCount < len && comps1[eqCount] === comps2[eqCount]) {
412
+ ++eqCount;
413
+ }
414
+ return eqCount;
415
+ };
416
+ pathsMatch = function(path1, path2) {
417
+ return numberOfMatchingSegments(path1, path2) > 0;
418
+ };
419
+ IMAGE_STYLES = [
420
+ {
421
+ selector: 'background',
422
+ styleNames: ['backgroundImage']
423
+ }, {
424
+ selector: 'border',
425
+ styleNames: ['borderImage', 'webkitBorderImage', 'MozBorderImage']
426
+ }
427
+ ];
428
+ __reloader.Reloader = Reloader = (function() {
429
+ function Reloader(window, console, Timer) {
430
+ this.window = window;
431
+ this.console = console;
432
+ this.Timer = Timer;
433
+ this.document = this.window.document;
434
+ this.stylesheetGracePeriod = 200;
435
+ this.importCacheWaitPeriod = 200;
436
+ }
437
+ Reloader.prototype.reload = function(path, options) {
438
+ if (options.liveCSS) {
439
+ if (path.match(/\.css$/i)) {
440
+ if (this.reloadStylesheet(path)) {
441
+ return;
442
+ }
443
+ }
444
+ if (path.match(/\.less$/i) && this.window.less && this.window.less.refresh) {
445
+ this.window.less.refresh(true);
446
+ return;
447
+ }
448
+ }
449
+ if (options.liveImg) {
450
+ if (path.match(/\.(jpe?g|png|gif)$/i)) {
451
+ this.reloadImages(path);
452
+ return;
453
+ }
454
+ }
455
+ return this.reloadPage();
456
+ };
457
+ Reloader.prototype.reloadPage = function() {
458
+ return this.window.document.location.reload();
459
+ };
460
+ Reloader.prototype.reloadImages = function(path) {
461
+ var expando, img, selector, styleNames, styleSheet, _i, _j, _k, _l, _len, _len2, _len3, _len4, _ref, _ref2, _ref3, _ref4, _results;
462
+ expando = this.generateUniqueString();
463
+ _ref = this.document.images;
464
+ for (_i = 0, _len = _ref.length; _i < _len; _i++) {
465
+ img = _ref[_i];
466
+ if (pathsMatch(path, pathFromUrl(img.src))) {
467
+ img.src = this.generateCacheBustUrl(img.src, expando);
468
+ }
469
+ }
470
+ if (this.document.querySelectorAll) {
471
+ for (_j = 0, _len2 = IMAGE_STYLES.length; _j < _len2; _j++) {
472
+ _ref2 = IMAGE_STYLES[_j], selector = _ref2.selector, styleNames = _ref2.styleNames;
473
+ _ref3 = this.document.querySelectorAll("[style*=" + selector + "]");
474
+ for (_k = 0, _len3 = _ref3.length; _k < _len3; _k++) {
475
+ img = _ref3[_k];
476
+ this.reloadStyleImages(img.style, styleNames, path, expando);
477
+ }
478
+ }
479
+ }
480
+ if (this.document.styleSheets) {
481
+ _ref4 = this.document.styleSheets;
482
+ _results = [];
483
+ for (_l = 0, _len4 = _ref4.length; _l < _len4; _l++) {
484
+ styleSheet = _ref4[_l];
485
+ _results.push(this.reloadStylesheetImages(styleSheet, path, expando));
486
+ }
487
+ return _results;
488
+ }
489
+ };
490
+ Reloader.prototype.reloadStylesheetImages = function(styleSheet, path, expando) {
491
+ var rule, rules, styleNames, _i, _j, _len, _len2;
492
+ try {
493
+ rules = styleSheet != null ? styleSheet.cssRules : void 0;
494
+ } catch (e) {
495
+
496
+ }
497
+ if (!rules) {
498
+ return;
499
+ }
500
+ for (_i = 0, _len = rules.length; _i < _len; _i++) {
501
+ rule = rules[_i];
502
+ switch (rule.type) {
503
+ case CSSRule.IMPORT_RULE:
504
+ this.reloadStylesheetImages(rule.styleSheet, path, expando);
505
+ break;
506
+ case CSSRule.STYLE_RULE:
507
+ for (_j = 0, _len2 = IMAGE_STYLES.length; _j < _len2; _j++) {
508
+ styleNames = IMAGE_STYLES[_j].styleNames;
509
+ this.reloadStyleImages(rule.style, styleNames, path, expando);
510
+ }
511
+ break;
512
+ case CSSRule.MEDIA_RULE:
513
+ this.reloadStylesheetImages(rule, path, expando);
514
+ }
515
+ }
516
+ };
517
+ Reloader.prototype.reloadStyleImages = function(style, styleNames, path, expando) {
518
+ var newValue, styleName, value, _i, _len;
519
+ for (_i = 0, _len = styleNames.length; _i < _len; _i++) {
520
+ styleName = styleNames[_i];
521
+ value = style[styleName];
522
+ if (typeof value === 'string') {
523
+ newValue = value.replace(/\burl\s*\(([^)]*)\)/, __bind(function(match, src) {
524
+ if (pathsMatch(path, pathFromUrl(src))) {
525
+ return "url(" + (this.generateCacheBustUrl(src, expando)) + ")";
526
+ } else {
527
+ return match;
528
+ }
529
+ }, this));
530
+ if (newValue !== value) {
531
+ style[styleName] = newValue;
532
+ }
533
+ }
534
+ }
535
+ };
536
+ Reloader.prototype.reloadStylesheet = function(path) {
537
+ var imported, link, links, match, _i, _j, _len, _len2;
538
+ links = (function() {
539
+ var _i, _len, _ref, _results;
540
+ _ref = this.document.getElementsByTagName('link');
541
+ _results = [];
542
+ for (_i = 0, _len = _ref.length; _i < _len; _i++) {
543
+ link = _ref[_i];
544
+ if (link.rel === 'stylesheet' && !link.__LiveReload_pendingRemoval) {
545
+ _results.push(link);
546
+ }
547
+ }
548
+ return _results;
549
+ }).call(this);
550
+ imported = [];
551
+ for (_i = 0, _len = links.length; _i < _len; _i++) {
552
+ link = links[_i];
553
+ this.collectImportedStylesheets(link, link.sheet, imported);
554
+ }
555
+ this.console.log("LiveReload found " + links.length + " LINKed stylesheets, " + imported.length + " @imported stylesheets");
556
+ match = pickBestMatch(path, links.concat(imported), function(l) {
557
+ return pathFromUrl(l.href);
558
+ });
559
+ if (match) {
560
+ if (match.object.rule) {
561
+ this.console.log("LiveReload is reloading imported stylesheet: " + match.object.href);
562
+ this.reattachImportedRule(match.object);
563
+ } else {
564
+ this.console.log("LiveReload is reloading stylesheet: " + match.object.href);
565
+ this.reattachStylesheetLink(match.object);
566
+ }
567
+ } else {
568
+ this.console.log("LiveReload will reload all stylesheets because path '" + path + "' did not match any specific one");
569
+ for (_j = 0, _len2 = links.length; _j < _len2; _j++) {
570
+ link = links[_j];
571
+ this.reattachStylesheetLink(link);
572
+ }
573
+ }
574
+ return true;
575
+ };
576
+ Reloader.prototype.collectImportedStylesheets = function(link, styleSheet, result) {
577
+ var index, rule, rules, _len;
578
+ try {
579
+ rules = styleSheet != null ? styleSheet.cssRules : void 0;
580
+ } catch (e) {
581
+
582
+ }
583
+ if (rules && rules.length) {
584
+ for (index = 0, _len = rules.length; index < _len; index++) {
585
+ rule = rules[index];
586
+ switch (rule.type) {
587
+ case CSSRule.CHARSET_RULE:
588
+ continue;
589
+ case CSSRule.IMPORT_RULE:
590
+ result.push({
591
+ link: link,
592
+ rule: rule,
593
+ index: index,
594
+ href: rule.href
595
+ });
596
+ this.collectImportedStylesheets(link, rule.styleSheet, result);
597
+ break;
598
+ default:
599
+ break;
600
+ }
601
+ }
602
+ }
603
+ };
604
+ Reloader.prototype.reattachStylesheetLink = function(link) {
605
+ var clone, parent, timer;
606
+ if (link.__LiveReload_pendingRemoval) {
607
+ return;
608
+ }
609
+ link.__LiveReload_pendingRemoval = true;
610
+ clone = link.cloneNode(false);
611
+ clone.href = this.generateCacheBustUrl(link.href);
612
+ parent = link.parentNode;
613
+ if (parent.lastChild === link) {
614
+ parent.appendChild(clone);
615
+ } else {
616
+ parent.insertBefore(clone, link.nextSibling);
617
+ }
618
+ timer = new this.Timer(function() {
619
+ if (link.parentNode) {
620
+ return link.parentNode.removeChild(link);
621
+ }
622
+ });
623
+ return timer.start(this.stylesheetGracePeriod);
624
+ };
625
+ Reloader.prototype.reattachImportedRule = function(_arg) {
626
+ var href, index, link, media, newRule, parent, rule, tempLink;
627
+ rule = _arg.rule, index = _arg.index, link = _arg.link;
628
+ parent = rule.parentStyleSheet;
629
+ href = this.generateCacheBustUrl(rule.href);
630
+ media = rule.media.length ? [].join.call(rule.media, ', ') : '';
631
+ newRule = "@import url(\"" + href + "\") " + media + ";";
632
+ rule.__LiveReload_newHref = href;
633
+ tempLink = this.document.createElement("link");
634
+ tempLink.rel = 'stylesheet';
635
+ tempLink.href = href;
636
+ tempLink.__LiveReload_pendingRemoval = true;
637
+ if (link.parentNode) {
638
+ link.parentNode.insertBefore(tempLink, link);
639
+ }
640
+ return this.Timer.start(this.importCacheWaitPeriod, __bind(function() {
641
+ if (tempLink.parentNode) {
642
+ tempLink.parentNode.removeChild(tempLink);
643
+ }
644
+ if (rule.__LiveReload_newHref !== href) {
645
+ return;
646
+ }
647
+ parent.insertRule(newRule, index);
648
+ parent.deleteRule(index + 1);
649
+ rule = parent.cssRules[index];
650
+ rule.__LiveReload_newHref = href;
651
+ return this.Timer.start(this.importCacheWaitPeriod, __bind(function() {
652
+ if (rule.__LiveReload_newHref !== href) {
653
+ return;
654
+ }
655
+ parent.insertRule(newRule, index);
656
+ return parent.deleteRule(index + 1);
657
+ }, this));
658
+ }, this));
659
+ };
660
+ Reloader.prototype.generateUniqueString = function() {
661
+ return 'livereload=' + Date.now();
662
+ };
663
+ Reloader.prototype.generateCacheBustUrl = function(url, expando) {
664
+ var hash, oldParams, params, _ref;
665
+ if (expando == null) {
666
+ expando = this.generateUniqueString();
667
+ }
668
+ _ref = splitUrl(url), url = _ref.url, hash = _ref.hash, oldParams = _ref.params;
669
+ params = oldParams.replace(/(\?|&)livereload=(\d+)/, function(match, sep) {
670
+ return "" + sep + expando;
671
+ });
672
+ if (params === oldParams) {
673
+ if (oldParams.length === 0) {
674
+ params = "?" + expando;
675
+ } else {
676
+ params = "" + oldParams + "&" + expando;
677
+ }
678
+ }
679
+ return url + params + hash;
680
+ };
681
+ return Reloader;
682
+ })();
683
+ }).call(this);
684
+
685
+ // livereload
686
+ (function() {
687
+ var Connector, LiveReload, Options, Reloader, Timer;
688
+ var __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
689
+ Connector = __connector.Connector;
690
+ Timer = __timer.Timer;
691
+ Options = __options.Options;
692
+ Reloader = __reloader.Reloader;
693
+ __livereload.LiveReload = LiveReload = (function() {
694
+ function LiveReload(window) {
695
+ this.window = window;
696
+ this.listeners = {};
697
+ this.console = this.window.console && this.window.console.log && this.window.console.error ? this.window.console : {
698
+ log: function() {},
699
+ error: function() {}
700
+ };
701
+ if (!(this.WebSocket = this.window.WebSocket || this.window.MozWebSocket)) {
702
+ console.error("LiveReload disabled because the browser does not seem to support web sockets");
703
+ return;
704
+ }
705
+ if (!(this.options = Options.extract(this.window.document))) {
706
+ console.error("LiveReload disabled because it could not find its own <SCRIPT> tag");
707
+ return;
708
+ }
709
+ this.reloader = new Reloader(this.window, this.console, Timer);
710
+ this.connector = new Connector(this.options, this.WebSocket, Timer, {
711
+ connecting: __bind(function() {}, this),
712
+ socketConnected: __bind(function() {}, this),
713
+ connected: __bind(function(protocol) {
714
+ var _base;
715
+ if (typeof (_base = this.listeners).connect === "function") {
716
+ _base.connect();
717
+ }
718
+ return this.log("LiveReload is connected to " + this.options.host + ":" + this.options.port + " (protocol v" + protocol + ").");
719
+ }, this),
720
+ error: __bind(function(e) {
721
+ if (e instanceof ProtocolError) {
722
+ return console.log("" + e.message + ".");
723
+ } else {
724
+ return console.log("LiveReload internal error: " + e.message);
725
+ }
726
+ }, this),
727
+ disconnected: __bind(function(reason, nextDelay) {
728
+ var _base;
729
+ if (typeof (_base = this.listeners).disconnect === "function") {
730
+ _base.disconnect();
731
+ }
732
+ switch (reason) {
733
+ case 'cannot-connect':
734
+ return this.log("LiveReload cannot connect to " + this.options.host + ":" + this.options.port + ", will retry in " + nextDelay + " sec.");
735
+ case 'broken':
736
+ return this.log("LiveReload disconnected from " + this.options.host + ":" + this.options.port + ", reconnecting in " + nextDelay + " sec.");
737
+ case 'handshake-timeout':
738
+ return this.log("LiveReload cannot connect to " + this.options.host + ":" + this.options.port + " (handshake timeout), will retry in " + nextDelay + " sec.");
739
+ case 'handshake-failed':
740
+ return this.log("LiveReload cannot connect to " + this.options.host + ":" + this.options.port + " (handshake failed), will retry in " + nextDelay + " sec.");
741
+ case 'manual':
742
+ break;
743
+ case 'error':
744
+ break;
745
+ default:
746
+ return this.log("LiveReload disconnected from " + this.options.host + ":" + this.options.port + " (" + reason + "), reconnecting in " + nextDelay + " sec.");
747
+ }
748
+ }, this),
749
+ message: __bind(function(message) {
750
+ switch (message.command) {
751
+ case 'reload':
752
+ return this.performReload(message);
753
+ case 'alert':
754
+ return this.performAlert(message);
755
+ }
756
+ }, this)
757
+ });
758
+ }
759
+ LiveReload.prototype.on = function(eventName, handler) {
760
+ return this.listeners[eventName] = handler;
761
+ };
762
+ LiveReload.prototype.log = function(message) {
763
+ return this.console.log("" + message);
764
+ };
765
+ LiveReload.prototype.performReload = function(message) {
766
+ var _ref, _ref2;
767
+ this.log("LiveReload received reload request for " + message.path + ".");
768
+ return this.reloader.reload(message.path, {
769
+ liveCSS: (_ref = message.liveCSS) != null ? _ref : true,
770
+ liveImg: (_ref2 = message.liveImg) != null ? _ref2 : true
771
+ });
772
+ };
773
+ LiveReload.prototype.performAlert = function(message) {
774
+ return alert(message.message);
775
+ };
776
+ LiveReload.prototype.shutDown = function() {
777
+ var _base;
778
+ this.connector.disconnect();
779
+ this.log("LiveReload disconnected.");
780
+ return typeof (_base = this.listeners).shutdown === "function" ? _base.shutdown() : void 0;
781
+ };
782
+ return LiveReload;
783
+ })();
784
+ }).call(this);
785
+
786
+ // startup
787
+ (function() {
788
+ var CustomEvents, LiveReload;
789
+ CustomEvents = __customevents;
790
+ LiveReload = window.LiveReload = new (__livereload.LiveReload)(window);
791
+ LiveReload.on('shutdown', function() {
792
+ return delete window.LiveReload;
793
+ });
794
+ LiveReload.on('connect', function() {
795
+ return CustomEvents.fire(document, 'LiveReloadConnect');
796
+ });
797
+ LiveReload.on('disconnect', function() {
798
+ return CustomEvents.fire(document, 'LiveReloadDisconnect');
799
+ });
800
+ CustomEvents.bind(document, 'LiveReloadShutDown', function() {
801
+ return LiveReload.shutDown();
802
+ });
803
+ }).call(this);
804
+ })();
@@ -0,0 +1,6 @@
1
+ require "rack/livereload"
2
+
3
+ class Rack::LiveReload
4
+ VERSION = '0.1.0'
5
+ end
6
+
@@ -0,0 +1,55 @@
1
+ module Rack
2
+ class LiveReload
3
+ LIVERELOAD_JS_PATH = '/__rack/livereload.js'
4
+
5
+ attr_reader :app
6
+
7
+ def initialize(app, options = {})
8
+ @app = app
9
+ @options = options
10
+ end
11
+
12
+ def call(env)
13
+ if env['PATH_INFO'] == LIVERELOAD_JS_PATH
14
+ deliver_file(::File.expand_path('../../../js/livereload.js', __FILE__))
15
+ else
16
+ status, headers, body = @app.call(env)
17
+
18
+ case headers['Content-Type']
19
+ when %r{text/html}
20
+ content_length = 0
21
+
22
+ body.each do |line|
23
+ if !headers['X-Rack-LiveReload'] && line['</head>']
24
+ src = LIVERELOAD_JS_PATH.dup
25
+ if @options[:host]
26
+ src << "?host=#{@options[:host]}"
27
+ else
28
+ src << "?host=#{env['HTTP_HOST'].gsub(%r{:.*}, '')}" if env['HTTP_HOST']
29
+ end
30
+ src << "&mindelay=#{@options[:min_delay]}" if @options[:min_delay]
31
+ src << "&maxdelay=#{@options[:max_delay]}" if @options[:max_delay]
32
+ src << "&port=#{@options[:port]}" if @options[:port]
33
+
34
+ line.gsub!('</head>', %{<script type="text/javascript" src="#{src}"></script></head>})
35
+
36
+ headers["X-Rack-LiveReload"] = '1'
37
+ end
38
+
39
+ content_length += line.length
40
+ end
41
+
42
+ headers['Content-Length'] = content_length.to_s
43
+ end
44
+
45
+ [ status, headers, body ]
46
+ end
47
+ end
48
+
49
+ private
50
+ def deliver_file(file)
51
+ [ 200, { 'Content-Type' => 'text/javascript', 'Content-Length' => ::File.size(file).to_s }, [ ::File.read(file) ] ]
52
+ end
53
+ end
54
+ end
55
+
@@ -0,0 +1,34 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "rack-livereload"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "rack-livereload"
7
+ s.version = Rack::LiveReload::VERSION
8
+ s.authors = ["John Bintz"]
9
+ s.email = ["john@coswellproductions.com"]
10
+ s.homepage = ""
11
+ s.summary = %q{Insert LiveReload into your app easily as Rack middleware}
12
+ s.description = %q{Insert LiveReload into your app easily as Rack middleware}
13
+
14
+ s.rubyforge_project = "rack-livereload"
15
+
16
+ s.files = `git ls-files`.split("\n")
17
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
18
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
19
+ s.require_paths = ["lib"]
20
+
21
+ # specify any dependencies here; for example:
22
+ s.add_development_dependency "rspec"
23
+ s.add_development_dependency "httparty"
24
+ s.add_development_dependency "sinatra"
25
+ s.add_development_dependency "shotgun"
26
+ s.add_development_dependency "thin"
27
+ s.add_development_dependency "rake"
28
+ s.add_development_dependency "mocha"
29
+ s.add_development_dependency "guard"
30
+ s.add_development_dependency "guard-rspec"
31
+
32
+ s.add_runtime_dependency "rack"
33
+ end
34
+
@@ -0,0 +1,74 @@
1
+ require 'spec_helper'
2
+
3
+ describe Rack::LiveReload do
4
+ let(:middleware) { described_class.new(app) }
5
+ let(:app) { stub }
6
+
7
+ subject { middleware }
8
+
9
+ its(:app) { should == app }
10
+
11
+ let(:env) { {} }
12
+
13
+ context 'not text/html' do
14
+ let(:ret) { [ 200, { 'Content-Type' => 'image/png' }, [ '<head></head>' ] ] }
15
+
16
+ before do
17
+ app.stubs(:call).with(env).returns(ret)
18
+ end
19
+
20
+ it 'should pass through' do
21
+ middleware.call(env).should == ret
22
+ end
23
+ end
24
+
25
+ context 'text/html' do
26
+ before do
27
+ app.stubs(:call).with(env).returns([ 200, { 'Content-Type' => 'text/html', 'Content-Length' => 0 }, [ '<head></head>' ] ])
28
+ end
29
+
30
+ let(:host) { 'host' }
31
+ let(:env) { { 'HTTP_HOST' => host } }
32
+
33
+ let(:ret) { middleware.call(env) }
34
+ let(:body) { ret.last.join }
35
+ let(:length) { ret[1]['Content-Length'] }
36
+
37
+ it 'should add the livereload js script tag' do
38
+ body.should include("script")
39
+ body.should include(described_class::LIVERELOAD_JS_PATH)
40
+
41
+ length.should == body.length.to_s
42
+
43
+ described_class::LIVERELOAD_JS_PATH.should_not include(host)
44
+ end
45
+
46
+ context 'set options' do
47
+ let(:middleware) { described_class.new(app, :host => new_host, :port => port, :min_delay => min_delay, :max_delay => max_delay) }
48
+ let(:min_delay) { 5 }
49
+ let(:max_delay) { 10 }
50
+ let(:port) { 23 }
51
+ let(:new_host) { 'myhost' }
52
+
53
+ it 'should add the livereload.js script tag' do
54
+ body.should include("mindelay=#{min_delay}")
55
+ body.should include("maxdelay=#{max_delay}")
56
+ body.should include("port=#{port}")
57
+ body.should include("host=#{new_host}")
58
+ end
59
+ end
60
+ end
61
+
62
+ context '/__rack/livereload.js' do
63
+ let(:env) { { 'PATH_INFO' => described_class::LIVERELOAD_JS_PATH } }
64
+
65
+ before do
66
+ middleware.expects(:deliver_file).returns(true)
67
+ end
68
+
69
+ it 'should return the js file' do
70
+ middleware.call(env).should be_true
71
+ end
72
+ end
73
+ end
74
+
@@ -0,0 +1,7 @@
1
+ require 'mocha'
2
+ require 'rack-livereload'
3
+
4
+ RSpec.configure do |c|
5
+ c.mock_with :mocha
6
+ end
7
+
metadata ADDED
@@ -0,0 +1,169 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rack-livereload
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - John Bintz
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2011-11-07 00:00:00.000000000Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: rspec
16
+ requirement: &2153313940 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :development
23
+ prerelease: false
24
+ version_requirements: *2153313940
25
+ - !ruby/object:Gem::Dependency
26
+ name: httparty
27
+ requirement: &2153313340 !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - ! '>='
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: *2153313340
36
+ - !ruby/object:Gem::Dependency
37
+ name: sinatra
38
+ requirement: &2153312520 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ! '>='
42
+ - !ruby/object:Gem::Version
43
+ version: '0'
44
+ type: :development
45
+ prerelease: false
46
+ version_requirements: *2153312520
47
+ - !ruby/object:Gem::Dependency
48
+ name: shotgun
49
+ requirement: &2153311420 !ruby/object:Gem::Requirement
50
+ none: false
51
+ requirements:
52
+ - - ! '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ type: :development
56
+ prerelease: false
57
+ version_requirements: *2153311420
58
+ - !ruby/object:Gem::Dependency
59
+ name: thin
60
+ requirement: &2153309720 !ruby/object:Gem::Requirement
61
+ none: false
62
+ requirements:
63
+ - - ! '>='
64
+ - !ruby/object:Gem::Version
65
+ version: '0'
66
+ type: :development
67
+ prerelease: false
68
+ version_requirements: *2153309720
69
+ - !ruby/object:Gem::Dependency
70
+ name: rake
71
+ requirement: &2153299320 !ruby/object:Gem::Requirement
72
+ none: false
73
+ requirements:
74
+ - - ! '>='
75
+ - !ruby/object:Gem::Version
76
+ version: '0'
77
+ type: :development
78
+ prerelease: false
79
+ version_requirements: *2153299320
80
+ - !ruby/object:Gem::Dependency
81
+ name: mocha
82
+ requirement: &2153298900 !ruby/object:Gem::Requirement
83
+ none: false
84
+ requirements:
85
+ - - ! '>='
86
+ - !ruby/object:Gem::Version
87
+ version: '0'
88
+ type: :development
89
+ prerelease: false
90
+ version_requirements: *2153298900
91
+ - !ruby/object:Gem::Dependency
92
+ name: guard
93
+ requirement: &2153298480 !ruby/object:Gem::Requirement
94
+ none: false
95
+ requirements:
96
+ - - ! '>='
97
+ - !ruby/object:Gem::Version
98
+ version: '0'
99
+ type: :development
100
+ prerelease: false
101
+ version_requirements: *2153298480
102
+ - !ruby/object:Gem::Dependency
103
+ name: guard-rspec
104
+ requirement: &2153298000 !ruby/object:Gem::Requirement
105
+ none: false
106
+ requirements:
107
+ - - ! '>='
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
110
+ type: :development
111
+ prerelease: false
112
+ version_requirements: *2153298000
113
+ - !ruby/object:Gem::Dependency
114
+ name: rack
115
+ requirement: &2153297500 !ruby/object:Gem::Requirement
116
+ none: false
117
+ requirements:
118
+ - - ! '>='
119
+ - !ruby/object:Gem::Version
120
+ version: '0'
121
+ type: :runtime
122
+ prerelease: false
123
+ version_requirements: *2153297500
124
+ description: Insert LiveReload into your app easily as Rack middleware
125
+ email:
126
+ - john@coswellproductions.com
127
+ executables: []
128
+ extensions: []
129
+ extra_rdoc_files: []
130
+ files:
131
+ - .gitignore
132
+ - Gemfile
133
+ - Guardfile
134
+ - README.md
135
+ - Rakefile
136
+ - config.ru
137
+ - js/livereload.js
138
+ - lib/rack-livereload.rb
139
+ - lib/rack/livereload.rb
140
+ - rack-livereload.gemspec
141
+ - spec/rack/livereload_spec.rb
142
+ - spec/spec_helper.rb
143
+ homepage: ''
144
+ licenses: []
145
+ post_install_message:
146
+ rdoc_options: []
147
+ require_paths:
148
+ - lib
149
+ required_ruby_version: !ruby/object:Gem::Requirement
150
+ none: false
151
+ requirements:
152
+ - - ! '>='
153
+ - !ruby/object:Gem::Version
154
+ version: '0'
155
+ required_rubygems_version: !ruby/object:Gem::Requirement
156
+ none: false
157
+ requirements:
158
+ - - ! '>='
159
+ - !ruby/object:Gem::Version
160
+ version: '0'
161
+ requirements: []
162
+ rubyforge_project: rack-livereload
163
+ rubygems_version: 1.8.11
164
+ signing_key:
165
+ specification_version: 3
166
+ summary: Insert LiveReload into your app easily as Rack middleware
167
+ test_files:
168
+ - spec/rack/livereload_spec.rb
169
+ - spec/spec_helper.rb