diff --git a/src/clients/Wyam/Commands/BuildCommand.cs b/src/clients/Wyam/Commands/BuildCommand.cs index 18b43dc48..7580bccc1 100644 --- a/src/clients/Wyam/Commands/BuildCommand.cs +++ b/src/clients/Wyam/Commands/BuildCommand.cs @@ -2,12 +2,13 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.CommandLine; -using System.IO; using System.Linq; using System.Threading; + using Wyam.Common.IO; +using Wyam.Common.Tracing; using Wyam.Configuration.Preprocessing; -using Trace = Wyam.Common.Tracing.Trace; +using Wyam.LiveReload; namespace Wyam.Commands { @@ -18,7 +19,7 @@ internal class BuildCommand : Command private readonly InterlockedBool _exit = new InterlockedBool(false); private readonly InterlockedBool _newEngine = new InterlockedBool(false); private readonly ConfigOptions _configOptions = new ConfigOptions(); - + private bool _preview = false; private int _previewPort = 5080; private DirectoryPath _previewVirtualDirectory = null; @@ -27,7 +28,7 @@ internal class BuildCommand : Command private bool _verifyConfig = false; private DirectoryPath _previewRoot = null; private bool _watch = false; - + public override string Description => "Runs the build process (this is the default command)."; public override string[] SupportedDirectives => new[] @@ -106,7 +107,7 @@ private static void AddSettings(IDictionary settings, IReadOnlyL { foreach (KeyValuePair kvp in MetadataParser.Parse(value)) { - settings[kvp.Key] = kvp.Value; + settings[kvp.Key] = kvp.Value; } } @@ -119,7 +120,7 @@ protected override ExitCode RunCommand(Preprocessor preprocessor) { // Get the standard input stream _configOptions.Stdin = StandardInputReader.Read(); - + // Fix the root folder and other files DirectoryPath currentDirectory = Environment.CurrentDirectory; _configOptions.RootPath = _configOptions.RootPath == null ? currentDirectory : currentDirectory.Combine(_configOptions.RootPath); @@ -162,6 +163,15 @@ protected override ExitCode RunCommand(Preprocessor preprocessor) bool messagePump = false; + // Start the LiveReload server. + bool runLiveReloadServer = _watch; + LiveReloadServer liveReloadServer = null; + if (runLiveReloadServer) + { + liveReloadServer = new LiveReloadServer(); + liveReloadServer.StartStandaloneHost(); + } + // Start the preview server IDisposable previewServer = null; if (_preview) @@ -170,7 +180,7 @@ protected override ExitCode RunCommand(Preprocessor preprocessor) DirectoryPath previewPath = _previewRoot == null ? engineManager.Engine.FileSystem.GetOutputDirectory().Path : engineManager.Engine.FileSystem.GetOutputDirectory(_previewRoot).Path; - previewServer = PreviewServer.Start(previewPath, _previewPort, _previewForceExtension, _previewVirtualDirectory); + previewServer = PreviewServer.Start(previewPath, _previewPort, _previewForceExtension, _previewVirtualDirectory, liveReloadServer); } // Start the watchers @@ -192,7 +202,7 @@ protected override ExitCode RunCommand(Preprocessor preprocessor) { Trace.Information("Watching configuration file {0}", _configOptions.ConfigFilePath); configFileWatcher = new ActionFileSystemWatcher(engineManager.Engine.FileSystem.GetOutputDirectory().Path, - new[] { _configOptions.ConfigFilePath.Directory }, false, _configOptions.ConfigFilePath.FileName.FullPath, path => + new[] {_configOptions.ConfigFilePath.Directory}, false, _configOptions.ConfigFilePath.FileName.FullPath, path => { FilePath filePath = new FilePath(path); if (_configOptions.ConfigFilePath.Equals(filePath)) @@ -228,7 +238,7 @@ protected override ExitCode RunCommand(Preprocessor preprocessor) // Wait for activity while (true) { - _messageEvent.WaitOne(); // Blocks the current thread until a signal + _messageEvent.WaitOne(); // Blocks the current thread until a signal if (_exit) { break; @@ -283,6 +293,8 @@ protected override ExitCode RunCommand(Preprocessor preprocessor) { exitCode = ExitCode.ExecutionError; } + + liveReloadServer?.RebuildCompleted(changedFiles); } } @@ -301,6 +313,7 @@ protected override ExitCode RunCommand(Preprocessor preprocessor) inputFolderWatcher?.Dispose(); configFileWatcher?.Dispose(); previewServer?.Dispose(); + liveReloadServer?.Dispose(); } return exitCode; diff --git a/src/clients/Wyam/Commands/PreviewCommand.cs b/src/clients/Wyam/Commands/PreviewCommand.cs index c1df39c49..c88510c9a 100644 --- a/src/clients/Wyam/Commands/PreviewCommand.cs +++ b/src/clients/Wyam/Commands/PreviewCommand.cs @@ -30,7 +30,7 @@ protected override void ParseParameters(ArgumentSyntax syntax) protected override ExitCode RunCommand(Preprocessor preprocessor) { _path = new DirectoryPath(Environment.CurrentDirectory).Combine(_path ?? "output"); - using (PreviewServer.Start(_path, _port, _forceExtension, _virtualDirectory)) + using (PreviewServer.Start(_path, _port, _forceExtension, _virtualDirectory, null)) { Trace.Information("Hit any key to exit"); Console.ReadKey(); diff --git a/src/clients/Wyam/LiveReload/LiveReloadScriptInjectionMiddleware.cs b/src/clients/Wyam/LiveReload/LiveReloadScriptInjectionMiddleware.cs new file mode 100644 index 000000000..d71886189 --- /dev/null +++ b/src/clients/Wyam/LiveReload/LiveReloadScriptInjectionMiddleware.cs @@ -0,0 +1,71 @@ +using System; +using System.IO; +using System.Threading.Tasks; + +using AngleSharp; +using AngleSharp.Dom; +using AngleSharp.Dom.Html; +using AngleSharp.Parser.Html; + +using Microsoft.Owin; + +namespace Wyam.LiveReload +{ + internal class LiveReloadScriptInjectionMiddleware : OwinMiddleware + { + private readonly string _scriptPath; + + internal HtmlParser HtmlParser { get; set; } = new HtmlParser(); + + public LiveReloadScriptInjectionMiddleware(OwinMiddleware next, string scriptPath) : base(next) + { + _scriptPath = scriptPath; + } + + public override async Task Invoke(IOwinContext context) + { + Stream originalBody = context.Response.Body; + MemoryStream interceptedBody = new MemoryStream(); + context.Response.Body = interceptedBody; + + await Next.Invoke(context); + + if (IsHtmlDocument(context)) + { + interceptedBody.Position = 0; + IHtmlDocument document = HtmlParser.Parse(interceptedBody); + + IElement script = document.CreateElement("script"); + script.SetAttribute("type", "text/javascript"); + script.SetAttribute("src", _scriptPath); + document.Body.Append(script); + + MemoryStream newContentBuffer = new MemoryStream(); + StreamWriter writer = new StreamWriter(newContentBuffer); + + document.ToHtml(writer, new AutoSelectedMarkupFormatter()); + writer.Flush(); + + context.Response.ContentLength = newContentBuffer.Length; + newContentBuffer.Position = 0; + newContentBuffer.CopyTo(originalBody); + + context.Response.Body = originalBody; + } + else + { + interceptedBody.Position = 0; + interceptedBody.CopyTo(originalBody); + + context.Response.Body = originalBody; + } + } + + private bool IsHtmlDocument(IOwinContext context) + { + const string rfc2854Type = "text/html"; + string contentType = context.Response.ContentType; + return string.Equals(contentType, rfc2854Type, StringComparison.OrdinalIgnoreCase); + } + } +} \ No newline at end of file diff --git a/src/clients/Wyam/LiveReload/LiveReloadScriptInjectionMiddlewareExtensions.cs b/src/clients/Wyam/LiveReload/LiveReloadScriptInjectionMiddlewareExtensions.cs new file mode 100644 index 000000000..c00f7ff2b --- /dev/null +++ b/src/clients/Wyam/LiveReload/LiveReloadScriptInjectionMiddlewareExtensions.cs @@ -0,0 +1,18 @@ +using System; + +using Owin; + +namespace Wyam.LiveReload +{ + public static class LiveReloadScriptInjectionMiddlewareExtensions + { + public static IAppBuilder UseLiveReloadScriptInjections(this IAppBuilder builder, string scriptPath) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + return builder.Use(scriptPath); + } + } +} \ No newline at end of file diff --git a/src/clients/Wyam/LiveReload/LiveReloadServer.cs b/src/clients/Wyam/LiveReload/LiveReloadServer.cs new file mode 100644 index 000000000..94bee4727 --- /dev/null +++ b/src/clients/Wyam/LiveReload/LiveReloadServer.cs @@ -0,0 +1,84 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; + +using Microsoft.Owin; +using Microsoft.Owin.FileSystems; +using Microsoft.Owin.StaticFiles; + +using Owin; +using Owin.WebSocket.Extensions; + +using Wyam.Common.Tracing; +using Wyam.Server; + +namespace Wyam.LiveReload +{ + internal class LiveReloadServer : IDisposable + { + private readonly ConcurrentBag _clients = new ConcurrentBag(); + private HttpServer _server; + + public virtual IEnumerable ReloadClients => _clients.ToArray(); + + public void StartStandaloneHost(int port = 35729, bool throwExceptions = false) + { + try + { + _server = new HttpServer(); + _server.StartServer(port, AddHostMiddleware); + } + catch (Exception ex) + { + Trace.Warning($"Error while running the LiveReload server: {ex.Message}"); + if (throwExceptions) + { + throw; + } + } + + Trace.Verbose($"LiveReload server listening on port {port}."); + } + + public void AddInjectionMiddleware(IAppBuilder app) + { + // Inject LR script. + app.UseLiveReloadScriptInjections("/livereload.js"); + } + + public void AddHostMiddleware(IAppBuilder app) + { + // Host livereload.js + Assembly liveReloadAssembly = typeof(LiveReloadServer).Assembly; + string rootNamespace = typeof(LiveReloadServer).Namespace; + IFileSystem reloadFilesystem = new EmbeddedResourceFileSystem(liveReloadAssembly, $"{rootNamespace}"); + app.UseStaticFiles(new StaticFileOptions + { + RequestPath = PathString.Empty, + FileSystem = reloadFilesystem, + ServeUnknownFileTypes = true + }); + + // Host ws:// + app.MapFleckRoute("/livereload", connection => _clients.Add((ReloadClient) connection)); + } + + public void RebuildCompleted(ICollection filesChanged) + { + foreach (IReloadClient client in ReloadClients.Where(x => x.IsConnected)) + { + foreach (string modifiedFile in filesChanged) + { + client.NotifyOfChanges(modifiedFile); + } + } + } + + public void Dispose() + { + _server?.Dispose(); + } + } +} \ No newline at end of file diff --git a/src/clients/Wyam/LiveReload/Messages/BasicMessage.cs b/src/clients/Wyam/LiveReload/Messages/BasicMessage.cs new file mode 100644 index 000000000..695b8e870 --- /dev/null +++ b/src/clients/Wyam/LiveReload/Messages/BasicMessage.cs @@ -0,0 +1,7 @@ +namespace Wyam.LiveReload.Messages +{ + internal class BasicMessage : ILiveReloadMessage + { + public string Command { get; set; } + } +} \ No newline at end of file diff --git a/src/clients/Wyam/LiveReload/Messages/HelloMessage.cs b/src/clients/Wyam/LiveReload/Messages/HelloMessage.cs new file mode 100644 index 000000000..f4e4142ac --- /dev/null +++ b/src/clients/Wyam/LiveReload/Messages/HelloMessage.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; + +namespace Wyam.LiveReload.Messages +{ + internal class HelloMessage : ILiveReloadMessage + { + public ICollection Protocols { get; set; } + + public string ServerName { get; set; } = "Wyam"; + + public string Command { get; set; } = "hello"; + } +} \ No newline at end of file diff --git a/src/clients/Wyam/LiveReload/Messages/ILiveReloadMessage.cs b/src/clients/Wyam/LiveReload/Messages/ILiveReloadMessage.cs new file mode 100644 index 000000000..0ebdd0a34 --- /dev/null +++ b/src/clients/Wyam/LiveReload/Messages/ILiveReloadMessage.cs @@ -0,0 +1,7 @@ +namespace Wyam.LiveReload.Messages +{ + internal interface ILiveReloadMessage + { + string Command { get; set; } + } +} \ No newline at end of file diff --git a/src/clients/Wyam/LiveReload/Messages/InfoMessage.cs b/src/clients/Wyam/LiveReload/Messages/InfoMessage.cs new file mode 100644 index 000000000..82d4ba95c --- /dev/null +++ b/src/clients/Wyam/LiveReload/Messages/InfoMessage.cs @@ -0,0 +1,9 @@ +namespace Wyam.LiveReload.Messages +{ + internal class InfoMessage : ILiveReloadMessage + { + public string Command { get; set; } = "info"; + + public string Url { get; set; } + } +} \ No newline at end of file diff --git a/src/clients/Wyam/LiveReload/Messages/ReloadMessage.cs b/src/clients/Wyam/LiveReload/Messages/ReloadMessage.cs new file mode 100644 index 000000000..1f414334e --- /dev/null +++ b/src/clients/Wyam/LiveReload/Messages/ReloadMessage.cs @@ -0,0 +1,11 @@ +namespace Wyam.LiveReload.Messages +{ + internal class ReloadMessage : ILiveReloadMessage + { + public string Path { get; set; } + + public bool LiveCss { get; set; } + + public string Command { get; set; } = "reload"; + } +} \ No newline at end of file diff --git a/src/clients/Wyam/LiveReload/ReloadClient.cs b/src/clients/Wyam/LiveReload/ReloadClient.cs new file mode 100644 index 000000000..d1a012db1 --- /dev/null +++ b/src/clients/Wyam/LiveReload/ReloadClient.cs @@ -0,0 +1,143 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.WebSockets; +using System.Text; +using System.Threading.Tasks; + +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; + +using Owin.WebSocket; + +using Wyam.Common.Tracing; +using Wyam.LiveReload.Messages; + +namespace Wyam.LiveReload +{ + public interface IReloadClient + { + bool IsConnected { get; } + void NotifyOfChanges(string modifiedFile, bool supportCssReload = true); + } + + public class ReloadClient : FleckWebSocketConnection, IReloadClient + { + // Attempt to support the Livereload protocol v7. + // http://feedback.livereload.com/knowledgebase/articles/86174-livereload-protocol + + private readonly JsonSerializerSettings _defaultSettings = new JsonSerializerSettings + { + ContractResolver = new CamelCasePropertyNamesContractResolver(), + Formatting = Formatting.Indented + }; + + private readonly HashSet _supportedVersion = new HashSet + { + "http://livereload.com/protocols/official-7" // Only supporting v7 right now + }; + + private readonly Guid _clientId = Guid.NewGuid(); + + public bool IsConnected { get; private set; } + + public override Task OnMessageReceived(ArraySegment message, WebSocketMessageType type) + { + string json = Encoding.UTF8.GetString(message.Array, message.Offset, message.Count); + HandleClientMessage(json); + + return Task.CompletedTask; + } + + public override void OnOpen() + { + SayHello(); + base.OnOpen(); + } + + public override void OnClose(WebSocketCloseStatus? closeStatus, string closeStatusDescription) + { + IsConnected = false; + LogVerbose($"Lost connection with LiveReload client. Status=({closeStatus}) Description=({closeStatusDescription})"); + base.OnClose(closeStatus, closeStatusDescription); + } + + public void NotifyOfChanges(string modifiedFile, bool supportCssReload = true) + { + // Asume changes have been rebuilt by this time + ReloadMessage reloadMessage = new ReloadMessage + { + Path = modifiedFile, + LiveCss = supportCssReload + }; + + LogVerbose($"Sending reload for {modifiedFile}."); + SendObject(reloadMessage); + } + + private ILiveReloadMessage HandleClientMessage(string json) + { + BasicMessage parsedMessage = JsonConvert.DeserializeObject(json, _defaultSettings); + switch (parsedMessage.Command) + { + case "info": + InfoMessage info = JsonConvert.DeserializeObject(json, _defaultSettings); + LogVerbose($"LiveReload client sent info ({info.Url})."); + break; + case "hello": + HelloMessage hello = JsonConvert.DeserializeObject(json, _defaultSettings); + HandleHello(hello); + break; + default: + LogVerbose($"Unknown command recieved from LiveReload client = {parsedMessage.Command}."); + break; + } + + return parsedMessage; + } + + private void HandleHello(HelloMessage message) + { + string negotiatedVersion = message.Protocols + .Intersect(_supportedVersion) + .OrderByDescending(x => x) + .FirstOrDefault(); + + if (negotiatedVersion == null) + { + string incompatibleMessage = "LiveReload client is not compatible with this server, aborting connection. " + + $"Client=({string.Join(",", message.Protocols)}) " + + $"Server=({string.Join(",", _supportedVersion)})"; + LogVerbose(incompatibleMessage); + Abort(); + } + else + { + LogVerbose($"LiveReload client hello. Negotiated=({negotiatedVersion})"); + IsConnected = true; + } + } + + private void SayHello() + { + HelloMessage helloMessage = new HelloMessage + { + Protocols = _supportedVersion + }; + + SendObject(helloMessage); + } + + private void SendObject(T obj) + { + string json = JsonConvert.SerializeObject(obj, _defaultSettings); + byte[] bytes = Encoding.UTF8.GetBytes(json); // UTF-8 by spec + SendText(bytes, true); + } + + private void LogVerbose(string message) + { + Trace.Verbose($"{message} Client=({_clientId})"); + } + } +} \ No newline at end of file diff --git a/src/clients/Wyam/LiveReload/livereload.js b/src/clients/Wyam/LiveReload/livereload.js new file mode 100644 index 000000000..6bb479c1e --- /dev/null +++ b/src/clients/Wyam/LiveReload/livereload.js @@ -0,0 +1,1205 @@ +// Copyright (c) 2010-2012 Andrey Tarantsov + +// 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. + +(function e(t, n, r) { function s(o, u) { if (!n[o]) { if (!t[o]) { var a = typeof require == "function" && require; if (!u && a) return a(o, !0); if (i) return i(o, !0); var f = new Error("Cannot find module '" + o + "'"); throw f.code = "MODULE_NOT_FOUND", f } var l = n[o] = { exports: {} }; t[o][0].call(l.exports, function (e) { var n = t[o][1][e]; return s(n ? n : e) }, l, l.exports, e, t, n, r) } return n[o].exports } var i = typeof require == "function" && require; for (var o = 0; o < r.length; o++)s(r[o]); return s })({ + 1: [function (require, module, exports) { +(function() { + var Connector, PROTOCOL_6, PROTOCOL_7, Parser, Version, _ref; + + _ref = require('./protocol'), Parser = _ref.Parser, PROTOCOL_6 = _ref.PROTOCOL_6, PROTOCOL_7 = _ref.PROTOCOL_7; + + Version = '2.2.2'; + + exports.Connector = Connector = (function() { + function Connector(options, WebSocket, Timer, handlers) { + this.options = options; + this.WebSocket = WebSocket; + this.Timer = Timer; + this.handlers = handlers; + this._uri = "ws" + (this.options.https ? "s" : "") + "://" + this.options.host + ":" + this.options.port + "/livereload"; + this._nextDelay = this.options.mindelay; + this._connectionDesired = false; + this.protocol = 0; + this.protocolParser = new Parser({ + connected: (function(_this) { + return function(protocol) { + _this.protocol = protocol; + _this._handshakeTimeout.stop(); + _this._nextDelay = _this.options.mindelay; + _this._disconnectionReason = 'broken'; + return _this.handlers.connected(protocol); + }; + })(this), + error: (function(_this) { + return function(e) { + _this.handlers.error(e); + return _this._closeOnError(); + }; + })(this), + message: (function(_this) { + return function(message) { + return _this.handlers.message(message); + }; + })(this) + }); + this._handshakeTimeout = new Timer((function(_this) { + return function() { + if (!_this._isSocketConnected()) { + return; + } + _this._disconnectionReason = 'handshake-timeout'; + return _this.socket.close(); + }; + })(this)); + this._reconnectTimer = new Timer((function(_this) { + return function() { + if (!_this._connectionDesired) { + return; + } + return _this.connect(); + }; + })(this)); + this.connect(); + } + + Connector.prototype._isSocketConnected = function() { + return this.socket && this.socket.readyState === this.WebSocket.OPEN; + }; + + Connector.prototype.connect = function() { + 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(_this) { + return function(e) { + return _this._onopen(e); + }; + })(this); + this.socket.onclose = (function(_this) { + return function(e) { + return _this._onclose(e); + }; + })(this); + this.socket.onmessage = (function(_this) { + return function(e) { + return _this._onmessage(e); + }; + })(this); + return this.socket.onerror = (function(_this) { + return function(e) { + return _this._onerror(e); + }; + })(this); + }; + + 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; + + })(); + +}).call(this); + +},{"./protocol":6}],2:[function(require,module,exports){ +(function() { + 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"); + } + } + }; + + exports.bind = CustomEvents.bind; + + exports.fire = CustomEvents.fire; + +}).call(this); + +},{}],3:[function(require,module,exports){ +(function() { + var LessPlugin; + + module.exports = 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.match(/^stylesheet\/less$/i) || (link.rel.match(/stylesheet/i) && link.type.match(/^text\/(x-)?less$/i))) { + _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; + + })(); + +}).call(this); + +},{}],4:[function(require,module,exports){ +(function() { + var Connector, LiveReload, Options, Reloader, Timer, + __hasProp = {}.hasOwnProperty; + + Connector = require('./connector').Connector; + + Timer = require('./timer').Timer; + + Options = require('./options').Options; + + Reloader = require('./reloader').Reloader; + + exports.LiveReload = LiveReload = (function() { + function LiveReload(window) { + var k, v, _ref; + this.window = window; + this.listeners = {}; + this.plugins = []; + this.pluginIdentifiers = {}; + this.console = this.window.console && this.window.console.log && this.window.console.error ? this.window.location.href.match(/LR-verbose/) ? this.window.console : { + log: function() {}, + error: this.window.console.error.bind(this.window.console) + } : { + log: function() {}, + error: function() {} + }; + if (!(this.WebSocket = this.window.WebSocket || this.window.MozWebSocket)) { + this.console.error("LiveReload disabled because the browser does not seem to support web sockets"); + return; + } + if ('LiveReloadOptions' in window) { + this.options = new Options(); + _ref = window['LiveReloadOptions']; + for (k in _ref) { + if (!__hasProp.call(_ref, k)) continue; + v = _ref[k]; + this.options.set(k, v); + } + } else { + this.options = Options.extract(this.window.document); + if (!this.options) { + this.console.error("LiveReload disabled because it could not find its own