first implementation of the LiveReload, it works but it needs improvements
did
committed May 25, 2014
commit d02f62d057692f3c703dad62aa1db2a877dfbc06
Showing 14
changed files with
1433 additions
and 33 deletions
bin/wagon
+1
-0
| @@ | @@ -2,6 +2,7 @@ |
| # needed if you launch it without bundler | |
| $:.unshift(File.expand_path(File.dirname(__FILE__) + '/../lib')) | |
| + | $:.unshift(File.expand_path(File.dirname(__FILE__) + '/../vendor')) |
| $stdout.sync = true | |
locomotive/wagon.rb b/lib/locomotive/wagon.rb
+32
-10
| @@ | @@ -75,24 +75,23 @@ module Locomotive |
| if reader = self.require_mounter(path, true) | |
| Bundler.require 'misc' | |
| - | require 'locomotive/wagon/server' |
| - | app = Locomotive::Wagon::Server.new(reader) |
| use_listen = !options[:disable_listen] | |
| - | # TODO: new feature -> pick the right Rack handler (Thin, Puma, ...etc) |
| - | |
| - | require 'thin' |
| - | server = Thin::Server.new(options[:host], options[:port], app) |
| - | server.threaded = true |
| + | # initialize the guard livereload server |
| + | require 'locomotive/wagon/misc/livereload' |
| + | livereload = Locomotive::Wagon::LiveReload.new(options.slice(:host)) |
| if options[:force] | |
| begin | |
| self.stop(path) | |
| - | sleep(2) # make sure we wait enough for the Thin process to stop |
| + | sleep(2) # make sure we wait enough for the server process to stop |
| rescue | |
| end | |
| end | |
| + | # TODO: new feature -> pick the right Rack handler (Thin, Puma, ...etc) |
| + | server = self.thin_server(reader, options.slice(:host, :port, :disable_listen).merge(live_reload_port: livereload.port)) |
| + | |
| if options[:daemonize] | |
| # very important to get the parent pid in order to differenciate the sub process from the parent one | |
| parent_pid = Process.pid | |
| @@ | @@ -104,9 +103,21 @@ module Locomotive |
| use_listen = Process.pid != parent_pid && !options[:disable_listen] | |
| end | |
| - | Locomotive::Wagon::Listen.instance.start(reader) if use_listen |
| + | listen_thread = Thread.new do |
| + | if use_listen |
| + | Locomotive::Wagon::Listen.instance.start(reader, livereload) |
| + | livereload.start |
| + | end |
| + | end |
| + | |
| + | server_thread = Thread.new { server.start } |
| + | |
| + | # hit Control + C to stop |
| + | Signal.trap('INT') { EventMachine.stop } |
| + | Signal.trap('TERM') { EventMachine.stop } |
| - | server.start |
| + | listen_thread.join |
| + | server_thread.join |
| end | |
| end | |
| @@ | @@ -252,6 +263,17 @@ module Locomotive |
| protected | |
| + | def self.thin_server(reader, options) |
| + | require 'locomotive/wagon/server' |
| + | app = Locomotive::Wagon::Server.new(reader, options) |
| + | |
| + | # TODO: new feature -> pick the right Rack handler (Thin, Puma, ...etc) |
| + | require 'thin' |
| + | Thin::Server.new(options[:host], options[:port], { signals: false }, app).tap do |server| |
| + | server.threaded = true |
| + | end |
| + | end |
| + | |
| def self.validate_resources(resources, writers_or_readers) | |
| return if resources.nil? | |
locomotive/wagon/listen.rb b/lib/locomotive/wagon/listen.rb
+41
-18
| @@ | @@ -3,21 +3,15 @@ require 'listen' |
| module Locomotive::Wagon | |
| class Listen | |
| - | attr_accessor :reader |
| + | attr_accessor :reader, :livereload |
| def self.instance | |
| @@instance = new | |
| end | |
| - | def start(reader) |
| - | # if $parent_pid && $parent_pid == Process.pid |
| - | # puts "bypassing Listen in the parent process" |
| - | # return false |
| - | # end |
| - | |
| - | puts "Listening here: #{Process.pid}" |
| - | |
| - | self.reader = reader |
| + | def start(reader, livereload) |
| + | self.reader = reader |
| + | self.livereload = livereload |
| self.definitions.each do |definition| | |
| self.apply(definition) | |
| @@ | @@ -27,9 +21,10 @@ module Locomotive::Wagon |
| def definitions | |
| [ | |
| ['config', /\.yml/, [:site, :content_types, :pages, :snippets, :content_entries, :translations]], | |
| - | ['app/views', /\.liquid/, [:pages, :snippets]], |
| + | ['app/views', %r{(pages|snippets)/(.+\.liquid).*}, [:pages, :snippets]], |
| ['app/content_types', /\.yml/, [:content_types, :content_entries]], | |
| - | ['data', /\.yml/, :content_entries] |
| + | ['data', /\.yml/, :content_entries], |
| + | ['public', %r{((stylesheets|javascripts)/(.+\.(css|js))).*}, []] |
| ] | |
| end | |
| @@ | @@ -40,12 +35,16 @@ module Locomotive::Wagon |
| resources = [*definition.last] | |
| names = resources.map { |n| "\"#{n}\"" }.join(', ') | |
| - | Locomotive::Wagon::Logger.info "* Reloaded #{names} at #{Time.now}" |
| + | notify_livereload(definition, added + modified) |
| + | |
| + | unless resources.empty? |
| + | Locomotive::Wagon::Logger.info "* Reloaded #{names} at #{Time.now}" |
| - | begin |
| - | reader.reload(resources) |
| - | rescue Exception => e |
| - | Locomotive::Wagon::MounterException.new('Unable to reload', e) |
| + | begin |
| + | reader.reload(resources) |
| + | rescue Exception => e |
| + | Locomotive::Wagon::MounterException.new('Unable to reload', e) |
| + | end |
| end | |
| end | |
| @@ | @@ -56,7 +55,31 @@ module Locomotive::Wagon |
| listener = ::Listen.to(path, only: filter, &reloader) | |
| # non blocking listener | |
| - | listener.start #(false) |
| + | listener.start |
| + | end |
| + | |
| + | def notify_livereload(definition, files) |
| + | transformer = (case definition.first |
| + | when 'public' then lambda { |m| "/#{m[1]}"} |
| + | when 'app/views' then lambda { |m| "/#{m[2]}".sub(/\.liquid$/, '.html') } |
| + | else |
| + | nil |
| + | end) |
| + | |
| + | paths = files.map do |file| |
| + | if transformer && (matches = file.match(definition[1])) |
| + | transformer.call(matches) |
| + | else |
| + | file |
| + | end |
| + | end |
| + | |
| + | livereload.run_on_modifications(paths) |
| + | end |
| + | |
| + | def relative_path(path) |
| + | base_path = self.reader.mounting_point.path |
| + | relative_path = path.sub(base_path, '') |
| end | |
| end | |
locomotive/wagon/misc/livereload.rb b/lib/locomotive/wagon/misc/livereload.rb
+43
-0
| @@ | @@ -0,0 +1,43 @@ |
| + | # Stub Guard |
| + | module Guard |
| + | class Plugin |
| + | def initialize(options = {}) |
| + | end |
| + | end |
| + | |
| + | module UI |
| + | class << self |
| + | def method_missing(meth, *args) |
| + | Locomotive::Wagon::Logger.send(meth, *args) |
| + | end |
| + | end |
| + | end |
| + | end |
| + | |
| + | require 'guard/livereload' |
| + | require 'locomotive/wagon/misc/tcp_port' |
| + | |
| + | module Locomotive |
| + | module Wagon |
| + | |
| + | class LiveReload |
| + | |
| + | extend Forwardable |
| + | |
| + | def_delegators :@livereload, :start, :stop, :run_on_modifications |
| + | |
| + | attr_reader :port |
| + | |
| + | def initialize(options = {}) |
| + | tcp_port = Locomotive::Wagon::TcpPort.new(options[:host], 35729) |
| + | @port = tcp_port.first |
| + | |
| + | Locomotive::Wagon::Logger.debug "Run LiveReload on port '#{@port}'" |
| + | |
| + | @livereload = Guard::LiveReload.new(options.merge(port: @port)) |
| + | end |
| + | |
| + | end |
| + | |
| + | end |
| + | end |
| \ No newline at end of file | |
locomotive/wagon/misc/tcp_port.rb b/lib/locomotive/wagon/misc/tcp_port.rb
+42
-0
| @@ | @@ -0,0 +1,42 @@ |
| + | require 'socket' |
| + | |
| + | module Locomotive |
| + | module Wagon |
| + | |
| + | class TcpPort |
| + | |
| + | MAX_ATTEMPTS = 1000 |
| + | |
| + | def initialize(host, from) |
| + | @host = host |
| + | @from = from |
| + | end |
| + | |
| + | def first |
| + | current = @from.to_i |
| + | max = current + MAX_ATTEMPTS |
| + | while open_port(@host, current) |
| + | current += 1 |
| + | raise "No available ports from #{@from}" if current >= max |
| + | end |
| + | current.to_s |
| + | end |
| + | |
| + | private |
| + | |
| + | def open_port(host, port) |
| + | sock = Socket.new(:INET, :STREAM) |
| + | raw = Socket.sockaddr_in(port, host) |
| + | sock.connect(raw) |
| + | sock.close if sock |
| + | true |
| + | rescue Errno::ECONNREFUSED |
| + | false |
| + | rescue Errno::ETIMEDOUT |
| + | false |
| + | end |
| + | |
| + | end |
| + | |
| + | end |
| + | end |
| \ No newline at end of file | |
locomotive/wagon/server.rb b/lib/locomotive/wagon/server.rb
+9
-3
| @@ | @@ -1,3 +1,4 @@ |
| + | require 'rack-livereload' |
| require 'better_errors' | |
| require 'coffee_script' | |
| @@ | @@ -20,13 +21,16 @@ require 'locomotive/wagon/misc' |
| module Locomotive::Wagon | |
| class Server | |
| + | attr_reader :options |
| + | |
| def initialize(reader, options = {}) | |
| Locomotive::Wagon::Dragonfly.setup!(reader.mounting_point.path) | |
| Sprockets::Sass.add_sass_functions = false | |
| - | @reader = reader |
| - | @app = self.create_rack_app(@reader) |
| + | @reader = reader |
| + | @options = options |
| + | @app = self.create_rack_app(@reader, @options) |
| BetterErrors.application_root = reader.mounting_point.path | |
| end | |
| @@ | @@ -38,8 +42,10 @@ module Locomotive::Wagon |
| protected | |
| - | def create_rack_app(reader) |
| + | def create_rack_app(reader, options) |
| Rack::Builder.new do | |
| + | use Rack::LiveReload, live_reload_port: options[:live_reload_port] unless options[:use_listen] |
| + | |
| use Rack::Lint | |
| use BetterErrors::MiddlewareWrapper | |
locomotivecms_wagon.gemspec
+5
-2
| @@ | @@ -15,7 +15,7 @@ Gem::Specification.new do |gem| |
| gem.files = `git ls-files`.split($/) | |
| gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) } | |
| gem.test_files = gem.files.grep(%r{^(test|spec|features)/}) | |
| - | gem.require_paths = ['lib'] |
| + | gem.require_paths = ['lib', 'vendor'] |
| gem.executables = ['wagon'] | |
| gem.add_dependency 'thor' | |
| @@ | @@ -32,7 +32,10 @@ Gem::Specification.new do |gem| |
| gem.add_dependency 'rubyzip', '~> 1.1.0' | |
| gem.add_dependency 'netrc', '~> 0.7.7' | |
| - | gem.add_dependency 'listen', '~> 2.4.0' |
| + | gem.add_dependency 'listen', '~> 2.7.5' |
| + | gem.add_dependency 'em-websocket', '~> 0.5' |
| + | gem.add_dependency 'rack-livereload', '~> 0.3.15' |
| + | # gem.add_dependency 'multi_json', '~> 1.8' |
| gem.add_dependency 'httmultiparty', '0.3.10' | |
| gem.add_dependency 'will_paginate', '~> 3.0.3' | |
vendor/guard/LICENSE.txt
+22
-0
| @@ | @@ -0,0 +1,22 @@ |
| + | Copyright (c) 2013 Thibaud Guillaume-Gentil |
| + | |
| + | MIT License |
| + | |
| + | Permission is hereby granted, free of charge, to any person obtaining |
| + | a copy of this software and associated documentation files (the |
| + | "Software"), to deal in the Software without restriction, including |
| + | without limitation the rights to use, copy, modify, merge, publish, |
| + | distribute, sublicense, and/or sell copies of the Software, and to |
| + | permit persons to whom the Software is furnished to do so, subject to |
| + | the following conditions: |
| + | |
| + | The above copyright notice and this permission notice shall be |
| + | included in all copies or substantial portions of the Software. |
| + | |
| + | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, |
| + | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF |
| + | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND |
| + | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE |
| + | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION |
| + | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION |
| + | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. |
vendor/guard/livereload.rb
+36
-0
| @@ | @@ -0,0 +1,36 @@ |
| + | # Copied from https://github.com/guard/guard-livereload |
| + | # and customized to make it work outside the whole Guard stack. |
| + | |
| + | module Guard |
| + | class LiveReload < Plugin |
| + | require 'guard/livereload/websocket' |
| + | require 'guard/livereload/reactor' |
| + | |
| + | attr_accessor :reactor, :options |
| + | |
| + | def initialize(options = {}) |
| + | super |
| + | @options = { |
| + | host: '0.0.0.0', |
| + | port: '35729', |
| + | apply_css_live: true, |
| + | override_url: false, |
| + | grace_period: 0 |
| + | }.merge(options) |
| + | end |
| + | |
| + | def start |
| + | @reactor = Reactor.new(options) |
| + | end |
| + | |
| + | def stop |
| + | reactor.stop |
| + | end |
| + | |
| + | def run_on_modifications(paths) |
| + | sleep options[:grace_period] |
| + | reactor.reload_browser(paths) |
| + | end |
| + | |
| + | end |
| + | end |
vendor/guard/livereload/js/livereload.js
+1055
-0
| @@ | @@ -0,0 +1,1055 @@ |
| + | (function() { |
| + | var __customevents = {}, __protocol = {}, __connector = {}, __timer = {}, __options = {}, __reloader = {}, __livereload = {}, __less = {}, __startup = {}; |
| + | |
| + | // customevents |
| + | var CustomEvents; |
| + | CustomEvents = { |
| + | bind: function(element, eventName, handler) { |
| + | if (element.addEventListener) { |
| + | return element.addEventListener(eventName, handler, false); |
| + | } else if (element.attachEvent) { |
| + | element[eventName] = 1; |
| + | return element.attachEvent('onpropertychange', function(event) { |
| + | if (event.propertyName === eventName) { |
| + | return handler(); |
| + | } |
| + | }); |
| + | } else { |
| + | throw new Error("Attempt to attach custom event " + eventName + " to something which isn't a DOMElement"); |
| + | } |
| + | }, |
| + | fire: function(element, eventName) { |
| + | var event; |
| + | if (element.addEventListener) { |
| + | event = document.createEvent('HTMLEvents'); |
| + | event.initEvent(eventName, true, true); |
| + | return document.dispatchEvent(event); |
| + | } else if (element.attachEvent) { |
| + | if (element[eventName]) { |
| + | return element[eventName]++; |
| + | } |
| + | } else { |
| + | throw new Error("Attempt to fire custom event " + eventName + " on something which isn't a DOMElement"); |
| + | } |
| + | } |
| + | }; |
| + | __customevents.bind = CustomEvents.bind; |
| + | __customevents.fire = CustomEvents.fire; |
| + | |
| + | // protocol |
| + | var PROTOCOL_6, PROTOCOL_7, Parser, ProtocolError; |
| + | var __indexOf = Array.prototype.indexOf || function(item) { |
| + | for (var i = 0, l = this.length; i < l; i++) { |
| + | if (this[i] === item) return i; |
| + | } |
| + | return -1; |
| + | }; |
| + | __protocol.PROTOCOL_6 = PROTOCOL_6 = 'http://livereload.com/protocols/official-6'; |
| + | __protocol.PROTOCOL_7 = PROTOCOL_7 = 'http://livereload.com/protocols/official-7'; |
| + | __protocol.ProtocolError = ProtocolError = (function() { |
| + | function ProtocolError(reason, data) { |
| + | this.message = "LiveReload protocol error (" + reason + ") after receiving data: \"" + data + "\"."; |
| + | } |
| + | return ProtocolError; |
| + | })(); |
| + | __protocol.Parser = Parser = (function() { |
| + | function Parser(handlers) { |
| + | this.handlers = handlers; |
| + | this.reset(); |
| + | } |
| + | Parser.prototype.reset = function() { |
| + | return this.protocol = null; |
| + | }; |
| + | Parser.prototype.process = function(data) { |
| + | var command, message, options, _ref; |
| + | try { |
| + | if (!(this.protocol != null)) { |
| + | if (data.match(/^!!ver:([\d.]+)$/)) { |
| + | this.protocol = 6; |
| + | } else if (message = this._parseMessage(data, ['hello'])) { |
| + | if (!message.protocols.length) { |
| + | throw new ProtocolError("no protocols specified in handshake message"); |
| + | } else if (__indexOf.call(message.protocols, PROTOCOL_7) >= 0) { |
| + | this.protocol = 7; |
| + | } else if (__indexOf.call(message.protocols, PROTOCOL_6) >= 0) { |
| + | this.protocol = 6; |
| + | } else { |
| + | throw new ProtocolError("no supported protocols found"); |
| + | } |
| + | } |
| + | return this.handlers.connected(this.protocol); |
| + | } else if (this.protocol === 6) { |
| + | message = JSON.parse(data); |
| + | if (!message.length) { |
| + | throw new ProtocolError("protocol 6 messages must be arrays"); |
| + | } |
| + | command = message[0], options = message[1]; |
| + | if (command !== 'refresh') { |
| + | throw new ProtocolError("unknown protocol 6 command"); |
| + | } |
| + | return this.handlers.message({ |
| + | command: 'reload', |
| + | path: options.path, |
| + | liveCSS: (_ref = options.apply_css_live) != null ? _ref : true |
| + | }); |
| + | } else { |
| + | message = this._parseMessage(data, ['reload', 'alert']); |
| + | return this.handlers.message(message); |
| + | } |
| + | } catch (e) { |
| + | if (e instanceof ProtocolError) { |
| + | return this.handlers.error(e); |
| + | } else { |
| + | throw e; |
| + | } |
| + | } |
| + | }; |
| + | Parser.prototype._parseMessage = function(data, validCommands) { |
| + | var message, _ref; |
| + | try { |
| + | message = JSON.parse(data); |
| + | } catch (e) { |
| + | throw new ProtocolError('unparsable JSON', data); |
| + | } |
| + | if (!message.command) { |
| + | throw new ProtocolError('missing "command" key', data); |
| + | } |
| + | if (_ref = message.command, __indexOf.call(validCommands, _ref) < 0) { |
| + | throw new ProtocolError("invalid command '" + message.command + "', only valid commands are: " + (validCommands.join(', ')) + ")", data); |
| + | } |
| + | return message; |
| + | }; |
| + | return Parser; |
| + | })(); |
| + | |
| + | // connector |
| + | // Generated by CoffeeScript 1.3.3 |
| + | var Connector, PROTOCOL_6, PROTOCOL_7, Parser, Version, _ref; |
| + | |
| + | _ref = __protocol, Parser = _ref.Parser, PROTOCOL_6 = _ref.PROTOCOL_6, PROTOCOL_7 = _ref.PROTOCOL_7; |
| + | |
| + | Version = '2.0.8'; |
| + | |
| + | __connector.Connector = Connector = (function() { |
| + | |
| + | function Connector(options, WebSocket, Timer, handlers) { |
| + | var _this = this; |
| + | this.options = options; |
| + | this.WebSocket = WebSocket; |
| + | this.Timer = Timer; |
| + | this.handlers = handlers; |
| + | this._uri = "ws://" + this.options.host + ":" + this.options.port + "/livereload"; |
| + | this._nextDelay = this.options.mindelay; |
| + | this._connectionDesired = false; |
| + | this.protocol = 0; |
| + | this.protocolParser = new Parser({ |
| + | connected: function(protocol) { |
| + | _this.protocol = protocol; |
| + | _this._handshakeTimeout.stop(); |
| + | _this._nextDelay = _this.options.mindelay; |
| + | _this._disconnectionReason = 'broken'; |
| + | return _this.handlers.connected(protocol); |
| + | }, |
| + | error: function(e) { |
| + | _this.handlers.error(e); |
| + | return _this._closeOnError(); |
| + | }, |
| + | message: function(message) { |
| + | return _this.handlers.message(message); |
| + | } |
| + | }); |
| + | this._handshakeTimeout = new Timer(function() { |
| + | if (!_this._isSocketConnected()) { |
| + | return; |
| + | } |
| + | _this._disconnectionReason = 'handshake-timeout'; |
| + | return _this.socket.close(); |
| + | }); |
| + | this._reconnectTimer = new Timer(function() { |
| + | if (!_this._connectionDesired) { |
| + | return; |
| + | } |
| + | return _this.connect(); |
| + | }); |
| + | this.connect(); |
| + | } |
| + | |
| + | Connector.prototype._isSocketConnected = function() { |
| + | return this.socket && this.socket.readyState === this.WebSocket.OPEN; |
| + | }; |
| + | |
| + | Connector.prototype.connect = function() { |
| + | var _this = this; |
| + | this._connectionDesired = true; |
| + | if (this._isSocketConnected()) { |
| + | return; |
| + | } |
| + | this._reconnectTimer.stop(); |
| + | this._disconnectionReason = 'cannot-connect'; |
| + | this.protocolParser.reset(); |
| + | this.handlers.connecting(); |
| + | this.socket = new this.WebSocket(this._uri); |
| + | this.socket.onopen = function(e) { |
| + | return _this._onopen(e); |
| + | }; |
| + | this.socket.onclose = function(e) { |
| + | return _this._onclose(e); |
| + | }; |
| + | this.socket.onmessage = function(e) { |
| + | return _this._onmessage(e); |
| + | }; |
| + | return this.socket.onerror = function(e) { |
| + | return _this._onerror(e); |
| + | }; |
| + | }; |
| + | |
| + | Connector.prototype.disconnect = function() { |
| + | this._connectionDesired = false; |
| + | this._reconnectTimer.stop(); |
| + | if (!this._isSocketConnected()) { |
| + | return; |
| + | } |
| + | this._disconnectionReason = 'manual'; |
| + | return this.socket.close(); |
| + | }; |
| + | |
| + | Connector.prototype._scheduleReconnection = function() { |
| + | if (!this._connectionDesired) { |
| + | return; |
| + | } |
| + | if (!this._reconnectTimer.running) { |
| + | this._reconnectTimer.start(this._nextDelay); |
| + | return this._nextDelay = Math.min(this.options.maxdelay, this._nextDelay * 2); |
| + | } |
| + | }; |
| + | |
| + | Connector.prototype.sendCommand = function(command) { |
| + | if (this.protocol == null) { |
| + | return; |
| + | } |
| + | return this._sendCommand(command); |
| + | }; |
| + | |
| + | Connector.prototype._sendCommand = function(command) { |
| + | return this.socket.send(JSON.stringify(command)); |
| + | }; |
| + | |
| + | Connector.prototype._closeOnError = function() { |
| + | this._handshakeTimeout.stop(); |
| + | this._disconnectionReason = 'error'; |
| + | return this.socket.close(); |
| + | }; |
| + | |
| + | Connector.prototype._onopen = function(e) { |
| + | var hello; |
| + | this.handlers.socketConnected(); |
| + | this._disconnectionReason = 'handshake-failed'; |
| + | hello = { |
| + | command: 'hello', |
| + | protocols: [PROTOCOL_6, PROTOCOL_7] |
| + | }; |
| + | hello.ver = Version; |
| + | if (this.options.ext) { |
| + | hello.ext = this.options.ext; |
| + | } |
| + | if (this.options.extver) { |
| + | hello.extver = this.options.extver; |
| + | } |
| + | if (this.options.snipver) { |
| + | hello.snipver = this.options.snipver; |
| + | } |
| + | this._sendCommand(hello); |
| + | return this._handshakeTimeout.start(this.options.handshake_timeout); |
| + | }; |
| + | |
| + | Connector.prototype._onclose = function(e) { |
| + | this.protocol = 0; |
| + | this.handlers.disconnected(this._disconnectionReason, this._nextDelay); |
| + | return this._scheduleReconnection(); |
| + | }; |
| + | |
| + | Connector.prototype._onerror = function(e) {}; |
| + | |
| + | Connector.prototype._onmessage = function(e) { |
| + | return this.protocolParser.process(e.data); |
| + | }; |
| + | |
| + | return Connector; |
| + | |
| + | })(); |
| + | |
| + | // timer |
| + | var Timer; |
| + | var __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; |
| + | __timer.Timer = Timer = (function() { |
| + | function Timer(func) { |
| + | this.func = func; |
| + | this.running = false; |
| + | this.id = null; |
| + | this._handler = __bind(function() { |
| + | this.running = false; |
| + | this.id = null; |
| + | return this.func(); |
| + | }, this); |
| + | } |
| + | Timer.prototype.start = function(timeout) { |
| + | if (this.running) { |
| + | clearTimeout(this.id); |
| + | } |
| + | this.id = setTimeout(this._handler, timeout); |
| + | return this.running = true; |
| + | }; |
| + | Timer.prototype.stop = function() { |
| + | if (this.running) { |
| + | clearTimeout(this.id); |
| + | this.running = false; |
| + | return this.id = null; |
| + | } |
| + | }; |
| + | return Timer; |
| + | })(); |
| + | Timer.start = function(timeout, func) { |
| + | return setTimeout(func, timeout); |
| + | }; |
| + | |
| + | // options |
| + | var Options; |
| + | __options.Options = Options = (function() { |
| + | function Options() { |
| + | this.host = null; |
| + | this.port = 35729; |
| + | this.snipver = null; |
| + | this.ext = null; |
| + | this.extver = null; |
| + | this.mindelay = 1000; |
| + | this.maxdelay = 60000; |
| + | this.handshake_timeout = 5000; |
| + | } |
| + | Options.prototype.set = function(name, value) { |
| + | switch (typeof this[name]) { |
| + | case 'undefined': |
| + | break; |
| + | case 'number': |
| + | return this[name] = +value; |
| + | default: |
| + | return this[name] = value; |
| + | } |
| + | }; |
| + | return Options; |
| + | })(); |
| + | Options.extract = function(document) { |
| + | var element, keyAndValue, m, mm, options, pair, src, _i, _j, _len, _len2, _ref, _ref2; |
| + | _ref = document.getElementsByTagName('script'); |
| + | for (_i = 0, _len = _ref.length; _i < _len; _i++) { |
| + | element = _ref[_i]; |
| + | if ((src = element.src) && (m = src.match(/^[^:]+:\/\/(.*)\/z?livereload\.js(?:\?(.*))?$/))) { |
| + | options = new Options(); |
| + | if (mm = m[1].match(/^([^\/:]+)(?::(\d+))?$/)) { |
| + | options.host = mm[1]; |
| + | if (mm[2]) { |
| + | options.port = parseInt(mm[2], 10); |
| + | } |
| + | } |
| + | if (m[2]) { |
| + | _ref2 = m[2].split('&'); |
| + | for (_j = 0, _len2 = _ref2.length; _j < _len2; _j++) { |
| + | pair = _ref2[_j]; |
| + | if ((keyAndValue = pair.split('=')).length > 1) { |
| + | options.set(keyAndValue[0].replace(/-/g, '_'), keyAndValue.slice(1).join('=')); |
| + | } |
| + | } |
| + | } |
| + | return options; |
| + | } |
| + | } |
| + | return null; |
| + | }; |
| + | |
| + | // reloader |
| + | // Generated by CoffeeScript 1.3.1 |
| + | (function() { |
| + | var IMAGE_STYLES, Reloader, numberOfMatchingSegments, pathFromUrl, pathsMatch, pickBestMatch, splitUrl; |
| + | |
| + | splitUrl = function(url) { |
| + | var hash, index, params; |
| + | if ((index = url.indexOf('#')) >= 0) { |
| + | hash = url.slice(index); |
| + | url = url.slice(0, index); |
| + | } else { |
| + | hash = ''; |
| + | } |
| + | if ((index = url.indexOf('?')) >= 0) { |
| + | params = url.slice(index); |
| + | url = url.slice(0, index); |
| + | } else { |
| + | params = ''; |
| + | } |
| + | return { |
| + | url: url, |
| + | params: params, |
| + | hash: hash |
| + | }; |
| + | }; |
| + | |
| + | pathFromUrl = function(url) { |
| + | var path; |
| + | url = splitUrl(url).url; |
| + | if (url.indexOf('file://') === 0) { |
| + | path = url.replace(/^file:\/\/(localhost)?/, ''); |
| + | } else { |
| + | path = url.replace(/^([^:]+:)?\/\/([^:\/]+)(:\d*)?\//, '/'); |
| + | } |
| + | return decodeURIComponent(path); |
| + | }; |
| + | |
| + | pickBestMatch = function(path, objects, pathFunc) { |
| + | var bestMatch, object, score, _i, _len; |
| + | bestMatch = { |
| + | score: 0 |
| + | }; |
| + | for (_i = 0, _len = objects.length; _i < _len; _i++) { |
| + | object = objects[_i]; |
| + | score = numberOfMatchingSegments(path, pathFunc(object)); |
| + | if (score > bestMatch.score) { |
| + | bestMatch = { |
| + | object: object, |
| + | score: score |
| + | }; |
| + | } |
| + | } |
| + | if (bestMatch.score > 0) { |
| + | return bestMatch; |
| + | } else { |
| + | return null; |
| + | } |
| + | }; |
| + | |
| + | numberOfMatchingSegments = function(path1, path2) { |
| + | var comps1, comps2, eqCount, len; |
| + | path1 = path1.replace(/^\/+/, '').toLowerCase(); |
| + | path2 = path2.replace(/^\/+/, '').toLowerCase(); |
| + | if (path1 === path2) { |
| + | return 10000; |
| + | } |
| + | comps1 = path1.split('/').reverse(); |
| + | comps2 = path2.split('/').reverse(); |
| + | len = Math.min(comps1.length, comps2.length); |
| + | eqCount = 0; |
| + | while (eqCount < len && comps1[eqCount] === comps2[eqCount]) { |
| + | ++eqCount; |
| + | } |
| + | return eqCount; |
| + | }; |
| + | |
| + | pathsMatch = function(path1, path2) { |
| + | return numberOfMatchingSegments(path1, path2) > 0; |
| + | }; |
| + | |
| + | IMAGE_STYLES = [ |
| + | { |
| + | selector: 'background', |
| + | styleNames: ['backgroundImage'] |
| + | }, { |
| + | selector: 'border', |
| + | styleNames: ['borderImage', 'webkitBorderImage', 'MozBorderImage'] |
| + | } |
| + | ]; |
| + | |
| + | __reloader.Reloader = Reloader = (function() { |
| + | |
| + | Reloader.name = 'Reloader'; |
| + | |
| + | function Reloader(window, console, Timer) { |
| + | this.window = window; |
| + | this.console = console; |
| + | this.Timer = Timer; |
| + | this.document = this.window.document; |
| + | this.importCacheWaitPeriod = 200; |
| + | this.plugins = []; |
| + | } |
| + | |
| + | Reloader.prototype.addPlugin = function(plugin) { |
| + | return this.plugins.push(plugin); |
| + | }; |
| + | |
| + | Reloader.prototype.analyze = function(callback) { |
| + | return results; |
| + | }; |
| + | |
| + | Reloader.prototype.reload = function(path, options) { |
| + | var plugin, _base, _i, _len, _ref; |
| + | this.options = options; |
| + | if ((_base = this.options).stylesheetReloadTimeout == null) { |
| + | _base.stylesheetReloadTimeout = 15000; |
| + | } |
| + | _ref = this.plugins; |
| + | for (_i = 0, _len = _ref.length; _i < _len; _i++) { |
| + | plugin = _ref[_i]; |
| + | if (plugin.reload && plugin.reload(path, options)) { |
| + | return; |
| + | } |
| + | } |
| + | if (options.liveCSS) { |
| + | if (path.match(/\.css$/i)) { |
| + | if (this.reloadStylesheet(path)) { |
| + | return; |
| + | } |
| + | } |
| + | } |
| + | if (options.liveImg) { |
| + | if (path.match(/\.(jpe?g|png|gif)$/i)) { |
| + | this.reloadImages(path); |
| + | return; |
| + | } |
| + | } |
| + | return this.reloadPage(); |
| + | }; |
| + | |
| + | Reloader.prototype.reloadPage = function() { |
| + | return this.window.document.location.reload(); |
| + | }; |
| + | |
| + | Reloader.prototype.reloadImages = function(path) { |
| + | var expando, img, selector, styleNames, styleSheet, _i, _j, _k, _l, _len, _len1, _len2, _len3, _ref, _ref1, _ref2, _ref3, _results; |
| + | expando = this.generateUniqueString(); |
| + | _ref = this.document.images; |
| + | for (_i = 0, _len = _ref.length; _i < _len; _i++) { |
| + | img = _ref[_i]; |
| + | if (pathsMatch(path, pathFromUrl(img.src))) { |
| + | img.src = this.generateCacheBustUrl(img.src, expando); |
| + | } |
| + | } |
| + | if (this.document.querySelectorAll) { |
| + | for (_j = 0, _len1 = IMAGE_STYLES.length; _j < _len1; _j++) { |
| + | _ref1 = IMAGE_STYLES[_j], selector = _ref1.selector, styleNames = _ref1.styleNames; |
| + | _ref2 = this.document.querySelectorAll("[style*=" + selector + "]"); |
| + | for (_k = 0, _len2 = _ref2.length; _k < _len2; _k++) { |
| + | img = _ref2[_k]; |
| + | this.reloadStyleImages(img.style, styleNames, path, expando); |
| + | } |
| + | } |
| + | } |
| + | if (this.document.styleSheets) { |
| + | _ref3 = this.document.styleSheets; |
| + | _results = []; |
| + | for (_l = 0, _len3 = _ref3.length; _l < _len3; _l++) { |
| + | styleSheet = _ref3[_l]; |
| + | _results.push(this.reloadStylesheetImages(styleSheet, path, expando)); |
| + | } |
| + | return _results; |
| + | } |
| + | }; |
| + | |
| + | Reloader.prototype.reloadStylesheetImages = function(styleSheet, path, expando) { |
| + | var rule, rules, styleNames, _i, _j, _len, _len1; |
| + | try { |
| + | rules = styleSheet != null ? styleSheet.cssRules : void 0; |
| + | } catch (e) { |
| + | |
| + | } |
| + | if (!rules) { |
| + | return; |
| + | } |
| + | for (_i = 0, _len = rules.length; _i < _len; _i++) { |
| + | rule = rules[_i]; |
| + | switch (rule.type) { |
| + | case CSSRule.IMPORT_RULE: |
| + | this.reloadStylesheetImages(rule.styleSheet, path, expando); |
| + | break; |
| + | case CSSRule.STYLE_RULE: |
| + | for (_j = 0, _len1 = IMAGE_STYLES.length; _j < _len1; _j++) { |
| + | styleNames = IMAGE_STYLES[_j].styleNames; |
| + | this.reloadStyleImages(rule.style, styleNames, path, expando); |
| + | } |
| + | break; |
| + | case CSSRule.MEDIA_RULE: |
| + | this.reloadStylesheetImages(rule, path, expando); |
| + | } |
| + | } |
| + | }; |
| + | |
| + | Reloader.prototype.reloadStyleImages = function(style, styleNames, path, expando) { |
| + | var newValue, styleName, value, _i, _len, |
| + | _this = this; |
| + | for (_i = 0, _len = styleNames.length; _i < _len; _i++) { |
| + | styleName = styleNames[_i]; |
| + | value = style[styleName]; |
| + | if (typeof value === 'string') { |
| + | newValue = value.replace(/\burl\s*\(([^)]*)\)/, function(match, src) { |
| + | if (pathsMatch(path, pathFromUrl(src))) { |
| + | return "url(" + (_this.generateCacheBustUrl(src, expando)) + ")"; |
| + | } else { |
| + | return match; |
| + | } |
| + | }); |
| + | if (newValue !== value) { |
| + | style[styleName] = newValue; |
| + | } |
| + | } |
| + | } |
| + | }; |
| + | |
| + | Reloader.prototype.reloadStylesheet = function(path) { |
| + | var imported, link, links, match, style, _i, _j, _k, _l, _len, _len1, _len2, _len3, _ref, _ref1, |
| + | _this = this; |
| + | links = (function() { |
| + | var _i, _len, _ref, _results; |
| + | _ref = this.document.getElementsByTagName('link'); |
| + | _results = []; |
| + | for (_i = 0, _len = _ref.length; _i < _len; _i++) { |
| + | link = _ref[_i]; |
| + | if (link.rel === 'stylesheet' && !link.__LiveReload_pendingRemoval) { |
| + | _results.push(link); |
| + | } |
| + | } |
| + | return _results; |
| + | }).call(this); |
| + | imported = []; |
| + | _ref = this.document.getElementsByTagName('style'); |
| + | for (_i = 0, _len = _ref.length; _i < _len; _i++) { |
| + | style = _ref[_i]; |
| + | if (style.sheet) { |
| + | this.collectImportedStylesheets(style, style.sheet, imported); |
| + | } |
| + | } |
| + | for (_j = 0, _len1 = links.length; _j < _len1; _j++) { |
| + | link = links[_j]; |
| + | this.collectImportedStylesheets(link, link.sheet, imported); |
| + | } |
| + | if (this.window.StyleFix && this.document.querySelectorAll) { |
| + | _ref1 = this.document.querySelectorAll('style[data-href]'); |
| + | for (_k = 0, _len2 = _ref1.length; _k < _len2; _k++) { |
| + | style = _ref1[_k]; |
| + | links.push(style); |
| + | } |
| + | } |
| + | this.console.log("LiveReload found " + links.length + " LINKed stylesheets, " + imported.length + " @imported stylesheets"); |
| + | match = pickBestMatch(path, links.concat(imported), function(l) { |
| + | return pathFromUrl(_this.linkHref(l)); |
| + | }); |
| + | if (match) { |
| + | if (match.object.rule) { |
| + | this.console.log("LiveReload is reloading imported stylesheet: " + match.object.href); |
| + | this.reattachImportedRule(match.object); |
| + | } else { |
| + | this.console.log("LiveReload is reloading stylesheet: " + (this.linkHref(match.object))); |
| + | this.reattachStylesheetLink(match.object); |
| + | } |
| + | } else { |
| + | this.console.log("LiveReload will reload all stylesheets because path '" + path + "' did not match any specific one"); |
| + | for (_l = 0, _len3 = links.length; _l < _len3; _l++) { |
| + | link = links[_l]; |
| + | this.reattachStylesheetLink(link); |
| + | } |
| + | } |
| + | return true; |
| + | }; |
| + | |
| + | Reloader.prototype.collectImportedStylesheets = function(link, styleSheet, result) { |
| + | var index, rule, rules, _i, _len; |
| + | try { |
| + | rules = styleSheet != null ? styleSheet.cssRules : void 0; |
| + | } catch (e) { |
| + | |
| + | } |
| + | if (rules && rules.length) { |
| + | for (index = _i = 0, _len = rules.length; _i < _len; index = ++_i) { |
| + | rule = rules[index]; |
| + | switch (rule.type) { |
| + | case CSSRule.CHARSET_RULE: |
| + | continue; |
| + | case CSSRule.IMPORT_RULE: |
| + | result.push({ |
| + | link: link, |
| + | rule: rule, |
| + | index: index, |
| + | href: rule.href |
| + | }); |
| + | this.collectImportedStylesheets(link, rule.styleSheet, result); |
| + | break; |
| + | default: |
| + | break; |
| + | } |
| + | } |
| + | } |
| + | }; |
| + | |
| + | Reloader.prototype.waitUntilCssLoads = function(clone, func) { |
| + | var callbackExecuted, executeCallback, poll, |
| + | _this = this; |
| + | callbackExecuted = false; |
| + | executeCallback = function() { |
| + | if (callbackExecuted) { |
| + | return; |
| + | } |
| + | callbackExecuted = true; |
| + | return func(); |
| + | }; |
| + | clone.onload = function() { |
| + | console.log("onload!"); |
| + | _this.knownToSupportCssOnLoad = true; |
| + | return executeCallback(); |
| + | }; |
| + | if (!this.knownToSupportCssOnLoad) { |
| + | (poll = function() { |
| + | if (clone.sheet) { |
| + | console.log("polling!"); |
| + | return executeCallback(); |
| + | } else { |
| + | return _this.Timer.start(50, poll); |
| + | } |
| + | })(); |
| + | } |
| + | return this.Timer.start(this.options.stylesheetReloadTimeout, executeCallback); |
| + | }; |
| + | |
| + | Reloader.prototype.linkHref = function(link) { |
| + | return link.href || link.getAttribute('data-href'); |
| + | }; |
| + | |
| + | Reloader.prototype.reattachStylesheetLink = function(link) { |
| + | var clone, parent, |
| + | _this = this; |
| + | if (link.__LiveReload_pendingRemoval) { |
| + | return; |
| + | } |
| + | link.__LiveReload_pendingRemoval = true; |
| + | if (link.tagName === 'STYLE') { |
| + | clone = this.document.createElement('link'); |
| + | clone.rel = 'stylesheet'; |
| + | clone.media = link.media; |
| + | clone.disabled = link.disabled; |
| + | } else { |
| + | clone = link.cloneNode(false); |
| + | } |
| + | clone.href = this.generateCacheBustUrl(this.linkHref(link)); |
| + | parent = link.parentNode; |
| + | if (parent.lastChild === link) { |
| + | parent.appendChild(clone); |
| + | } else { |
| + | parent.insertBefore(clone, link.nextSibling); |
| + | } |
| + | return this.waitUntilCssLoads(clone, function() { |
| + | var additionalWaitingTime; |
| + | if (/AppleWebKit/.test(navigator.userAgent)) { |
| + | additionalWaitingTime = 5; |
| + | } else { |
| + | additionalWaitingTime = 200; |
| + | } |
| + | return _this.Timer.start(additionalWaitingTime, function() { |
| + | var _ref; |
| + | if (!link.parentNode) { |
| + | return; |
| + | } |
| + | link.parentNode.removeChild(link); |
| + | clone.onreadystatechange = null; |
| + | return (_ref = _this.window.StyleFix) != null ? _ref.link(clone) : void 0; |
| + | }); |
| + | }); |
| + | }; |
| + | |
| + | Reloader.prototype.reattachImportedRule = function(_arg) { |
| + | var href, index, link, media, newRule, parent, rule, tempLink, |
| + | _this = this; |
| + | rule = _arg.rule, index = _arg.index, link = _arg.link; |
| + | parent = rule.parentStyleSheet; |
| + | href = this.generateCacheBustUrl(rule.href); |
| + | media = rule.media.length ? [].join.call(rule.media, ', ') : ''; |
| + | newRule = "@import url(\"" + href + "\") " + media + ";"; |
| + | rule.__LiveReload_newHref = href; |
| + | tempLink = this.document.createElement("link"); |
| + | tempLink.rel = 'stylesheet'; |
| + | tempLink.href = href; |
| + | tempLink.__LiveReload_pendingRemoval = true; |
| + | if (link.parentNode) { |
| + | link.parentNode.insertBefore(tempLink, link); |
| + | } |
| + | return this.Timer.start(this.importCacheWaitPeriod, function() { |
| + | if (tempLink.parentNode) { |
| + | tempLink.parentNode.removeChild(tempLink); |
| + | } |
| + | if (rule.__LiveReload_newHref !== href) { |
| + | return; |
| + | } |
| + | parent.insertRule(newRule, index); |
| + | parent.deleteRule(index + 1); |
| + | rule = parent.cssRules[index]; |
| + | rule.__LiveReload_newHref = href; |
| + | return _this.Timer.start(_this.importCacheWaitPeriod, function() { |
| + | if (rule.__LiveReload_newHref !== href) { |
| + | return; |
| + | } |
| + | parent.insertRule(newRule, index); |
| + | return parent.deleteRule(index + 1); |
| + | }); |
| + | }); |
| + | }; |
| + | |
| + | Reloader.prototype.generateUniqueString = function() { |
| + | return 'livereload=' + Date.now(); |
| + | }; |
| + | |
| + | Reloader.prototype.generateCacheBustUrl = function(url, expando) { |
| + | var hash, oldParams, params, _ref; |
| + | if (expando == null) { |
| + | expando = this.generateUniqueString(); |
| + | } |
| + | _ref = splitUrl(url), url = _ref.url, hash = _ref.hash, oldParams = _ref.params; |
| + | if (this.options.overrideURL) { |
| + | if (url.indexOf(this.options.serverURL) < 0) { |
| + | url = this.options.serverURL + this.options.overrideURL + "?url=" + encodeURIComponent(url); |
| + | } |
| + | } |
| + | params = oldParams.replace(/(\?|&)livereload=(\d+)/, function(match, sep) { |
| + | return "" + sep + expando; |
| + | }); |
| + | if (params === oldParams) { |
| + | if (oldParams.length === 0) { |
| + | params = "?" + expando; |
| + | } else { |
| + | params = "" + oldParams + "&" + expando; |
| + | } |
| + | } |
| + | return url + params + hash; |
| + | }; |
| + | |
| + | return Reloader; |
| + | |
| + | })(); |
| + | |
| + | }).call(this); |
| + | |
| + | // livereload |
| + | var Connector, LiveReload, Options, Reloader, Timer; |
| + | |
| + | Connector = __connector.Connector; |
| + | |
| + | Timer = __timer.Timer; |
| + | |
| + | Options = __options.Options; |
| + | |
| + | Reloader = __reloader.Reloader; |
| + | |
| + | __livereload.LiveReload = LiveReload = (function() { |
| + | |
| + | function LiveReload(window) { |
| + | var _this = this; |
| + | this.window = window; |
| + | this.listeners = {}; |
| + | this.plugins = []; |
| + | this.pluginIdentifiers = {}; |
| + | this.console = this.window.location.href.match(/LR-verbose/) && this.window.console && this.window.console.log && this.window.console.error ? this.window.console : { |
| + | log: function() {}, |
| + | error: function() {} |
| + | }; |
| + | if (!(this.WebSocket = this.window.WebSocket || this.window.MozWebSocket)) { |
| + | console.error("LiveReload disabled because the browser does not seem to support web sockets"); |
| + | return; |
| + | } |
| + | if (!(this.options = Options.extract(this.window.document))) { |
| + | console.error("LiveReload disabled because it could not find its own <SCRIPT> tag"); |
| + | return; |
| + | } |
| + | this.reloader = new Reloader(this.window, this.console, Timer); |
| + | this.connector = new Connector(this.options, this.WebSocket, Timer, { |
| + | connecting: function() {}, |
| + | socketConnected: function() {}, |
| + | connected: function(protocol) { |
| + | var _base; |
| + | if (typeof (_base = _this.listeners).connect === "function") { |
| + | _base.connect(); |
| + | } |
| + | _this.log("LiveReload is connected to " + _this.options.host + ":" + _this.options.port + " (protocol v" + protocol + ")."); |
| + | return _this.analyze(); |
| + | }, |
| + | error: function(e) { |
| + | if (e instanceof ProtocolError) { |
| + | return console.log("" + e.message + "."); |
| + | } else { |
| + | return console.log("LiveReload internal error: " + e.message); |
| + | } |
| + | }, |
| + | disconnected: function(reason, nextDelay) { |
| + | var _base; |
| + | if (typeof (_base = _this.listeners).disconnect === "function") { |
| + | _base.disconnect(); |
| + | } |
| + | switch (reason) { |
| + | case 'cannot-connect': |
| + | return _this.log("LiveReload cannot connect to " + _this.options.host + ":" + _this.options.port + ", will retry in " + nextDelay + " sec."); |
| + | case 'broken': |
| + | return _this.log("LiveReload disconnected from " + _this.options.host + ":" + _this.options.port + ", reconnecting in " + nextDelay + " sec."); |
| + | case 'handshake-timeout': |
| + | return _this.log("LiveReload cannot connect to " + _this.options.host + ":" + _this.options.port + " (handshake timeout), will retry in " + nextDelay + " sec."); |
| + | case 'handshake-failed': |
| + | return _this.log("LiveReload cannot connect to " + _this.options.host + ":" + _this.options.port + " (handshake failed), will retry in " + nextDelay + " sec."); |
| + | case 'manual': |
| + | break; |
| + | case 'error': |
| + | break; |
| + | default: |
| + | return _this.log("LiveReload disconnected from " + _this.options.host + ":" + _this.options.port + " (" + reason + "), reconnecting in " + nextDelay + " sec."); |
| + | } |
| + | }, |
| + | message: function(message) { |
| + | switch (message.command) { |
| + | case 'reload': |
| + | return _this.performReload(message); |
| + | case 'alert': |
| + | return _this.performAlert(message); |
| + | } |
| + | } |
| + | }); |
| + | } |
| + | |
| + | LiveReload.prototype.on = function(eventName, handler) { |
| + | return this.listeners[eventName] = handler; |
| + | }; |
| + | |
| + | LiveReload.prototype.log = function(message) { |
| + | return this.console.log("" + message); |
| + | }; |
| + | |
| + | LiveReload.prototype.performReload = function(message) { |
| + | var _ref, _ref2; |
| + | this.log("LiveReload received reload request for " + message.path + "."); |
| + | return this.reloader.reload(message.path, { |
| + | liveCSS: (_ref = message.liveCSS) != null ? _ref : true, |
| + | liveImg: (_ref2 = message.liveImg) != null ? _ref2 : true, |
| + | originalPath: message.originalPath || '', |
| + | overrideURL: message.overrideURL || '', |
| + | serverURL: "http://" + this.options.host + ":" + this.options.port |
| + | }); |
| + | }; |
| + | |
| + | LiveReload.prototype.performAlert = function(message) { |
| + | return alert(message.message); |
| + | }; |
| + | |
| + | LiveReload.prototype.shutDown = function() { |
| + | var _base; |
| + | this.connector.disconnect(); |
| + | this.log("LiveReload disconnected."); |
| + | return typeof (_base = this.listeners).shutdown === "function" ? _base.shutdown() : void 0; |
| + | }; |
| + | |
| + | LiveReload.prototype.hasPlugin = function(identifier) { |
| + | return !!this.pluginIdentifiers[identifier]; |
| + | }; |
| + | |
| + | LiveReload.prototype.addPlugin = function(pluginClass) { |
| + | var plugin; |
| + | var _this = this; |
| + | if (this.hasPlugin(pluginClass.identifier)) return; |
| + | this.pluginIdentifiers[pluginClass.identifier] = true; |
| + | plugin = new pluginClass(this.window, { |
| + | _livereload: this, |
| + | _reloader: this.reloader, |
| + | _connector: this.connector, |
| + | console: this.console, |
| + | Timer: Timer, |
| + | generateCacheBustUrl: function(url) { |
| + | return _this.reloader.generateCacheBustUrl(url); |
| + | } |
| + | }); |
| + | this.plugins.push(plugin); |
| + | this.reloader.addPlugin(plugin); |
| + | }; |
| + | |
| + | LiveReload.prototype.analyze = function() { |
| + | var plugin, pluginData, pluginsData, _i, _len, _ref; |
| + | if (!(this.connector.protocol >= 7)) return; |
| + | pluginsData = {}; |
| + | _ref = this.plugins; |
| + | for (_i = 0, _len = _ref.length; _i < _len; _i++) { |
| + | plugin = _ref[_i]; |
| + | pluginsData[plugin.constructor.identifier] = pluginData = (typeof plugin.analyze === "function" ? plugin.analyze() : void 0) || {}; |
| + | pluginData.version = plugin.constructor.version; |
| + | } |
| + | this.connector.sendCommand({ |
| + | command: 'info', |
| + | plugins: pluginsData, |
| + | url: this.window.location.href |
| + | }); |
| + | }; |
| + | |
| + | return LiveReload; |
| + | |
| + | })(); |
| + | |
| + | // less |
| + | var LessPlugin; |
| + | __less = LessPlugin = (function() { |
| + | LessPlugin.identifier = 'less'; |
| + | LessPlugin.version = '1.0'; |
| + | function LessPlugin(window, host) { |
| + | this.window = window; |
| + | this.host = host; |
| + | } |
| + | LessPlugin.prototype.reload = function(path, options) { |
| + | if (this.window.less && this.window.less.refresh) { |
| + | if (path.match(/\.less$/i)) { |
| + | return this.reloadLess(path); |
| + | } |
| + | if (options.originalPath.match(/\.less$/i)) { |
| + | return this.reloadLess(options.originalPath); |
| + | } |
| + | } |
| + | return false; |
| + | }; |
| + | LessPlugin.prototype.reloadLess = function(path) { |
| + | var link, links, _i, _len; |
| + | links = (function() { |
| + | var _i, _len, _ref, _results; |
| + | _ref = document.getElementsByTagName('link'); |
| + | _results = []; |
| + | for (_i = 0, _len = _ref.length; _i < _len; _i++) { |
| + | link = _ref[_i]; |
| + | if (link.href && link.rel === 'stylesheet/less' || (link.rel.match(/stylesheet/) && link.type.match(/^text\/(x-)?less$/))) { |
| + | _results.push(link); |
| + | } |
| + | } |
| + | return _results; |
| + | })(); |
| + | if (links.length === 0) { |
| + | return false; |
| + | } |
| + | for (_i = 0, _len = links.length; _i < _len; _i++) { |
| + | link = links[_i]; |
| + | link.href = this.host.generateCacheBustUrl(link.href); |
| + | } |
| + | this.host.console.log("LiveReload is asking LESS to recompile all stylesheets"); |
| + | this.window.less.refresh(true); |
| + | return true; |
| + | }; |
| + | LessPlugin.prototype.analyze = function() { |
| + | return { |
| + | disable: !!(this.window.less && this.window.less.refresh) |
| + | }; |
| + | }; |
| + | return LessPlugin; |
| + | })(); |
| + | |
| + | // startup |
| + | var CustomEvents, LiveReload, k; |
| + | CustomEvents = __customevents; |
| + | LiveReload = window.LiveReload = new (__livereload.LiveReload)(window); |
| + | for (k in window) { |
| + | if (k.match(/^LiveReloadPlugin/)) { |
| + | LiveReload.addPlugin(window[k]); |
| + | } |
| + | } |
| + | LiveReload.addPlugin(__less); |
| + | LiveReload.on('shutdown', function() { |
| + | return delete window.LiveReload; |
| + | }); |
| + | LiveReload.on('connect', function() { |
| + | return CustomEvents.fire(document, 'LiveReloadConnect'); |
| + | }); |
| + | LiveReload.on('disconnect', function() { |
| + | return CustomEvents.fire(document, 'LiveReloadDisconnect'); |
| + | }); |
| + | CustomEvents.bind(document, 'LiveReloadShutDown', function() { |
| + | return LiveReload.shutDown(); |
| + | }); |
| + | })(); |
| \ No newline at end of file | |
vendor/guard/livereload/reactor.rb
+80
-0
| @@ | @@ -0,0 +1,80 @@ |
| + | require 'multi_json' |
| + | |
| + | module Guard |
| + | class LiveReload |
| + | class Reactor |
| + | attr_reader :web_sockets, :thread, :options, :connections_count |
| + | |
| + | def initialize(options) |
| + | @web_sockets = [] |
| + | @options = options |
| + | @thread = Thread.new { _start_reactor } |
| + | @connections_count = 0 |
| + | end |
| + | |
| + | def stop |
| + | thread.kill |
| + | end |
| + | |
| + | def reload_browser(paths = []) |
| + | UI.info "Reloading browser: #{paths.join(' ')}" |
| + | paths.each do |path| |
| + | data = _data(path) |
| + | UI.debug(data) |
| + | web_sockets.each { |ws| ws.send(MultiJson.encode(data)) } |
| + | end |
| + | end |
| + | |
| + | private |
| + | |
| + | def _data(path) |
| + | data = { |
| + | command: 'reload', |
| + | path: "#{Dir.pwd}/#{path}", |
| + | liveCSS: options[:apply_css_live] |
| + | } |
| + | if options[:override_url] && File.exist?(path) |
| + | data[:overrideURL] = '/' + path |
| + | end |
| + | data |
| + | end |
| + | |
| + | def _start_reactor |
| + | EventMachine.epoll |
| + | EventMachine.run do |
| + | EventMachine.start_server(options[:host], options[:port], WebSocket, {}) do |ws| |
| + | ws.onopen { _connect(ws) } |
| + | ws.onclose { _disconnect(ws) } |
| + | ws.onmessage { |msg| _print_message(msg) } |
| + | end |
| + | UI.info "LiveReload is waiting for a browser to connect." |
| + | end |
| + | end |
| + | |
| + | def _connect(ws) |
| + | @connections_count += 1 |
| + | UI.info "Browser connected." if connections_count == 1 |
| + | |
| + | ws.send MultiJson.encode( |
| + | command: 'hello', |
| + | protocols: ['http://livereload.com/protocols/official-7'], |
| + | serverName: 'guard-livereload' |
| + | ) |
| + | @web_sockets << ws |
| + | rescue |
| + | UI.error $! |
| + | UI.error $!.backtrace |
| + | end |
| + | |
| + | def _disconnect(ws) |
| + | @web_sockets.delete(ws) |
| + | end |
| + | |
| + | def _print_message(message) |
| + | message = MultiJson.decode(message) |
| + | UI.info "Browser URL: #{message['url']}" if message['command'] == 'url' |
| + | end |
| + | |
| + | end |
| + | end |
| + | end |
vendor/guard/livereload/templates/Guardfile
+8
-0
| @@ | @@ -0,0 +1,8 @@ |
| + | guard 'livereload' do |
| + | watch(%r{app/views/.+\.(erb|haml|slim)$}) |
| + | watch(%r{app/helpers/.+\.rb}) |
| + | watch(%r{public/.+\.(css|js|html)}) |
| + | watch(%r{config/locales/.+\.yml}) |
| + | # Rails Assets Pipeline |
| + | watch(%r{(app|vendor)(/assets/\w+/(.+\.(css|js|html|png|jpg))).*}) { |m| "/assets/#{m[3]}" } |
| + | end |
vendor/guard/livereload/version.rb
+5
-0
| @@ | @@ -0,0 +1,5 @@ |
| + | module Guard |
| + | module LiveReloadVersion |
| + | VERSION = '2.2.0' |
| + | end |
| + | end |
vendor/guard/livereload/websocket.rb
+54
-0
| @@ | @@ -0,0 +1,54 @@ |
| + | require 'eventmachine' |
| + | require 'em-websocket' |
| + | require 'http/parser' |
| + | require 'uri' |
| + | |
| + | module Guard |
| + | class LiveReload |
| + | class WebSocket < EventMachine::WebSocket::Connection |
| + | |
| + | def dispatch(data) |
| + | parser = Http::Parser.new |
| + | parser << data |
| + | # prepend with '.' to make request url usable as a file path |
| + | request_path = '.' + URI.parse(parser.request_url).path |
| + | request_path += '/index.html' if File.directory? request_path |
| + | if parser.http_method != 'GET' || parser.upgrade? |
| + | super #pass the request to websocket |
| + | elsif request_path == './livereload.js' |
| + | _serve_file(_livereload_js_file) |
| + | elsif File.readable?(request_path) && !File.directory?(request_path) |
| + | _serve_file(request_path) |
| + | else |
| + | send_data("HTTP/1.1 404 Not Found\r\nContent-Type: text/plain\r\nContent-Length: 13\r\n\r\n404 Not Found") |
| + | close_connection_after_writing |
| + | end |
| + | end |
| + | |
| + | private |
| + | |
| + | def _serve_file(path) |
| + | UI.debug "Serving file #{path}" |
| + | send_data "HTTP/1.1 200 OK\r\nContent-Type: #{_content_type(path)}\r\nContent-Length: #{File.size path}\r\n\r\n" |
| + | stream_file_data(path).callback { close_connection_after_writing } |
| + | end |
| + | |
| + | def _content_type(path) |
| + | case File.extname(path).downcase |
| + | when '.html', '.htm' then 'text/html' |
| + | when '.css' then 'text/css' |
| + | when '.js' then 'application/ecmascript' |
| + | when '.gif' then 'image/gif' |
| + | when '.jpeg', '.jpg' then 'image/jpeg' |
| + | when '.png' then 'image/png' |
| + | else; 'text/plain' |
| + | end |
| + | end |
| + | |
| + | def _livereload_js_file |
| + | File.expand_path("../js/livereload.js", __FILE__) |
| + | end |
| + | |
| + | end |
| + | end |
| + | end |