rack-livereload 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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