├── .gitignore ├── Firefox ├── chrome.manifest ├── content │ ├── background-firefox.js │ ├── browser.xul │ └── options.xul ├── defaults │ └── preferences │ │ └── defaults.js ├── install.rdf └── skin │ ├── icon_16-off.png │ ├── icon_16-on.png │ └── icon_32.png ├── LiveReload-update.plist ├── LiveReload.chromeextension ├── background.html ├── icon128.png ├── icon19-on.png ├── icon19.png ├── icon48.png ├── manifest.json └── options.html ├── LiveReload.safariextension ├── Icon-32.png ├── Icon-48.png ├── Info.plist ├── LiveReload.html └── Settings.plist ├── README-old.md ├── README.md ├── Rakefile ├── artwork ├── icon100.png ├── icon100.psd ├── icon128.psd ├── icon16.psd ├── icon19.psd ├── icon32.psd ├── icon48.psd ├── icon512.psd ├── screenshot.png └── screenshot.psd ├── docs ├── VersionNumbers.md ├── WebSocketProtocol.md └── images │ ├── chrome-button.png │ ├── chrome-install-prompt.png │ ├── livereload-server-running.png │ ├── safari-context-menu.png │ └── safari-install-prompt.png ├── example ├── xbrowser.css └── xbrowser.html ├── server ├── bin │ └── livereload ├── lib │ └── livereload.rb └── livereload.gemspec ├── src ├── background.js ├── chrome │ ├── background.js │ └── content.js ├── content.js ├── safari │ ├── global.js │ └── injected.js └── xbrowser │ └── livereload.js └── test ├── content.equals.js ├── content.fileExtension.js ├── content.fileName.js ├── content.generateNextUrl.js ├── content.handleCSS.js ├── fixtures ├── colors.css ├── simple.html ├── typography.css ├── widget-a │ └── colors.css ├── widget-b │ └── colors.css └── widgets.html ├── index.html └── qunit ├── qunit.css └── qunit.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /dist/*.safariextz 3 | /server/*.gem 4 | .livereload 5 | -------------------------------------------------------------------------------- /Firefox/chrome.manifest: -------------------------------------------------------------------------------- 1 | content livereload content/ 2 | skin livereload classic/1.0 skin/ 3 | 4 | overlay chrome://browser/content/browser.xul chrome://livereload/content/browser.xul 5 | -------------------------------------------------------------------------------- /Firefox/content/background-firefox.js: -------------------------------------------------------------------------------- 1 | function LivereloadBackgroundFirefox() {} 2 | LivereloadBackgroundFirefox.prototype = new LivereloadBackground(function reloadPage(tab, data) { 3 | var client = new LivereloadContent(tab.linkedBrowser.contentDocument); 4 | client.reload(data); 5 | }); 6 | 7 | LivereloadBackgroundFirefox.prototype.connect = function () { 8 | var self = this; 9 | 10 | this.prefs = Components.classes["@mozilla.org/preferences-service;1"] 11 | .getService(Components.interfaces.nsIPrefService) 12 | .getBranch("livereload."); 13 | this.prefs.QueryInterface(Components.interfaces.nsIPrefBranch2); 14 | this.prefs.addObserver("", {observe: function () { 15 | self.disconnect(); 16 | }}, false); 17 | 18 | this.host = this.prefs.getCharPref("host"); 19 | 20 | this.port = this.prefs.getIntPref("port"); 21 | 22 | this.__proto__.__proto__.connect.call(this); 23 | }; 24 | 25 | LivereloadBackgroundFirefox.prototype.sendPageUrl = function() { 26 | var activeTab = this.lastPage; 27 | if (activeTab == null) { 28 | throw 'No active tab'; 29 | } 30 | var socket = this.socket; 31 | this.socket.send(activeTab.linkedBrowser.contentDocument.location.href); 32 | }; 33 | 34 | LivereloadBackgroundFirefox.prototype.onEnablePage = function(tabId) { 35 | this.icon.image = 'chrome://livereload/skin/icon_16-on.png'; 36 | this.icon.setAttribute('tooltiptext', 'Disable LiveReload'); 37 | }; 38 | 39 | LivereloadBackgroundFirefox.prototype.onDisablePage = function(tabId) { 40 | this.icon.image = 'chrome://livereload/skin/icon_16-off.png'; 41 | this.icon.setAttribute('tooltiptext', 'Enable LiveReload'); 42 | }; 43 | 44 | 45 | window.addEventListener('load', function() { 46 | 47 | var livereloadBackground = new LivereloadBackgroundFirefox; 48 | //@debug window.livereloadBackground = livereloadBackground; 49 | 50 | var icon = livereloadBackground.icon = document.getElementById('livereload-button'); 51 | 52 | icon.addEventListener('command', function(event) { 53 | livereloadBackground.togglePage(event.view.gBrowser.selectedTab); 54 | }, false); 55 | 56 | gBrowser.tabContainer.addEventListener('TabSelect', function(event) { 57 | var tab = event.target; 58 | var index = livereloadBackground.pages.indexOf(tab); 59 | if (index == -1) { 60 | livereloadBackground.onDisablePage(tab); 61 | } else { 62 | livereloadBackground.onEnablePage(tab); 63 | } 64 | }, false); 65 | 66 | gBrowser.tabContainer.addEventListener('TabClose', function(event) { 67 | var tab = event.target; 68 | if (tab) { 69 | livereloadBackground.disablePage(tab); 70 | } else { 71 | throw 'no contentWindow'; 72 | } 73 | }, false); 74 | 75 | }, false); 76 | -------------------------------------------------------------------------------- /Firefox/content/browser.xul: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /LiveReload.chromeextension/icon128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mockko/livereload/84dcf0ff3d23809ae2b46161dd02869a20f9064e/LiveReload.chromeextension/icon128.png -------------------------------------------------------------------------------- /LiveReload.chromeextension/icon19-on.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mockko/livereload/84dcf0ff3d23809ae2b46161dd02869a20f9064e/LiveReload.chromeextension/icon19-on.png -------------------------------------------------------------------------------- /LiveReload.chromeextension/icon19.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mockko/livereload/84dcf0ff3d23809ae2b46161dd02869a20f9064e/LiveReload.chromeextension/icon19.png -------------------------------------------------------------------------------- /LiveReload.chromeextension/icon48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mockko/livereload/84dcf0ff3d23809ae2b46161dd02869a20f9064e/LiveReload.chromeextension/icon48.png -------------------------------------------------------------------------------- /LiveReload.chromeextension/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "LiveReload", 3 | "version": "1.7", 4 | "background_page": "background.html", 5 | "content_scripts": [ 6 | { 7 | "matches": [""], 8 | "js": ["LiveReload-content.js"] 9 | } 10 | ], 11 | "permissions": [ 12 | "tabs", 13 | "http://*/*" 14 | ], 15 | "icons": { "48": "icon48.png", 16 | "128": "icon128.png" }, 17 | "browser_action": { 18 | "default_title": "Enable LiveReload", 19 | "default_icon": "icon19.png" 20 | }, 21 | "options_page": "options.html" 22 | } 23 | -------------------------------------------------------------------------------- /LiveReload.chromeextension/options.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | LiveReload Options 4 | 71 | 72 | 105 | 106 | 107 |

LiveReload Options

108 |
109 | 110 | 111 | 112 | 113 | 114 | 115 |
116 | 117 |
118 |
119 | 120 | 121 | -------------------------------------------------------------------------------- /LiveReload.safariextension/Icon-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mockko/livereload/84dcf0ff3d23809ae2b46161dd02869a20f9064e/LiveReload.safariextension/Icon-32.png -------------------------------------------------------------------------------- /LiveReload.safariextension/Icon-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mockko/livereload/84dcf0ff3d23809ae2b46161dd02869a20f9064e/LiveReload.safariextension/Icon-48.png -------------------------------------------------------------------------------- /LiveReload.safariextension/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Author 6 | Mockko 7 | CFBundleDisplayName 8 | LiveReload 9 | CFBundleIdentifier 10 | com.mockko.livereload 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleShortVersionString 14 | 1.6.2 15 | CFBundleVersion 16 | 1.6.2 17 | Chrome 18 | 19 | Context Menu Items 20 | 21 | 22 | Command 23 | enable 24 | Identifier 25 | enable 26 | Title 27 | Enable LiveReload 28 | 29 | 30 | Global Page 31 | LiveReload.html 32 | 33 | Content 34 | 35 | Scripts 36 | 37 | End 38 | 39 | LiveReload-injected.js 40 | 41 | 42 | 43 | Description 44 | Applies CSS and JavaScript changes live, reloads page when HTML changes 45 | ExtensionInfoDictionaryVersion 46 | 1.0 47 | Permissions 48 | 49 | Website Access 50 | 51 | Include Secure Pages 52 | 53 | Level 54 | All 55 | 56 | 57 | Update Manifest URL 58 | https://github.com/mockko/livereload/raw/master/LiveReload-update.plist 59 | Website 60 | https://github.com/mockko/livereload 61 | 62 | 63 | -------------------------------------------------------------------------------- /LiveReload.safariextension/LiveReload.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /LiveReload.safariextension/Settings.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Key 7 | host 8 | Title 9 | Host 10 | Type 11 | TextField 12 | 13 | 14 | DefaultValue 15 | 35729 16 | Key 17 | port 18 | Title 19 | Port 20 | Type 21 | TextField 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /README-old.md: -------------------------------------------------------------------------------- 1 | # LiveReload 2 | 3 | ![LR](https://github.com/mockko/livereload/raw/master/artwork/screenshot.png) 4 | 5 | LiveReload is a Safari/Chrome extension + a command-line tool that: 6 | 7 | 1. Applies CSS and JavaScript file changes without reloading a page. 8 | 2. Automatically reloads a page when any other file changes (html, image, server-side script, etc). 9 | 10 | **[Screencast](http://blog.envylabs.com/2010/07/livereload-screencast/)** by Gregg Pollack at envylabs.com. 11 | 12 | **Email support:** support@livereload.com 13 | 14 | 15 | ## What's new? 16 | 17 | We are working on LiveReload 2.0 that is a native Mac app (Windows some time later too), doesn't require installing any Ruby gems and supports automatic compilation of CoffeeScript, HAML, SASS, LESS, Stylus and Jade. It will be a paid update and will be distributed via the Mac App Store. Alpha version is available at http://livereload.com (beware! it's really just an alpha!) 18 | 19 | Version 1.x will remain available and free. 20 | 21 | We are also now providing email support for both paid (2.0 alpha) and free (1.x) versions of LiveReload. Say hi by emailing support@livereload.com. 22 | 23 | 1.6: Configurable host & port, no-extension pure-html/js cross-browser version (see `example/xbrowser.html`, more docs coming soon), many small bug fixes. 24 | 25 | 1.5: Support for `file://` URLs in Chrome (does not seem possible in Safari, sorry). JS live reloading is now off by default. Minor UI improvements. 26 | 27 | 1.4: Works on Windows. Sane file system monitoring (had to write it from scratch, see em-dir-watcher gem). Port number changed to 35729 because of a conflict with Zend Server. Added grace period to combine the changes made in rapid succession. Works with Vim. 28 | 29 | 1.3: Configuration file (`.livereload`) — you can customize extensions, configure exclusions, disable no-reload refreshing. Monitoring of multiple folders. Some bugs fixed. 30 | 31 | 1.2.2: add .erb to the list of monitored extensions (this is a gem-only update, run `gem update livereload` to install). 32 | 33 | 1.2.1: added workaround for Chrome bug (unable to open WebSocket to localhost), fixed problem with command-line tool trying to use kqueue on Linux. 34 | 35 | 1.2: added Chrome extension, added icon artwork, added a check that the command-line tool version is compatible with the extension version, fixed a bug with multiple stylesheet updates happening too fast. 36 | 37 | 1.1: enabled autoupdating for the Safari extension. 38 | 39 | 1.0: original release -- Safari extension and a command-line tool in a Ruby gem. 40 | 41 | 42 | ## Installation 43 | 44 | LiveReload consists of command-line monitoring tool (livereload ruby gem) and browser extensions (for Google Chrome and Safari). 45 | 46 | 47 | ### Monitoring tool 48 | 49 | #### Windows 50 | 51 | 1. Install Ruby from [rubyinstaller.org/downloads](http://rubyinstaller.org/downloads/). LiveReload has been tested on Ruby 1.9.1 and 1.8.7. 52 | 53 | 2. Download Ruby Development Kit from the same page and follow [instructions](https://github.com/oneclick/rubyinstaller/wiki/Development-Kit). 54 | 55 | 3. `gem install eventmachine-win32 win32-changenotify win32-event livereload --platform=ruby` 56 | 57 | 58 | #### Mac OS X 59 | 60 | 1. Mac OS X ships with Ruby installed. 61 | 62 | 2. You need Xcode tools installed to compile eventmachine gem. Get it from [developer.apple.com](http://developer.apple.com/technologies/tools/xcode.html). 63 | 64 | 3. Install [RubyCocoa](http://sourceforge.net/projects/rubycocoa/). If you are using [rvm](http://rvm.beginrescueend.com/), you can try [these instructions](https://gist.github.com/289868). 65 | 66 | 4. `sudo gem install livereload` 67 | 68 | 69 | #### Linux 70 | 71 | `sudo gem install rb-inotify livereload` 72 | 73 | 74 | Another option is to use [Guard](https://github.com/guard/guard) with [guard-livereload](https://github.com/guard/guard-livereload). It does not require RubyCocoa on Mac OS X. 75 | 76 | 77 | ### [Google Chrome extension](https://chrome.google.com/extensions/detail/jnihajbhpnppcggbcgedagnkighmdlei) 78 | 79 | ![](https://github.com/mockko/livereload/raw/master/docs/images/chrome-install-prompt.png) 80 | 81 | Click “Install”. Actually, LiveReload does not access your browser history. The warning is misleading. 82 | 83 | ![](https://github.com/mockko/livereload/raw/master/docs/images/chrome-button.png) 84 | 85 | 86 | ### Safari extension 87 | 88 | Download [LiveReload 1.6.2 extension](https://github.com/downloads/mockko/livereload/LiveReload-1.6.2.safariextz). Double-click it and confirm installation: 89 | 90 | ![](https://github.com/mockko/livereload/raw/master/docs/images/safari-install-prompt.png) 91 | 92 | 93 | ### [Firefox 4 extension](https://addons.mozilla.org/firefox/addon/livereload/) 94 | 95 | ![](https://static.addons.mozilla.net/img/uploads/previews/full/53/53026.png) 96 | 97 | 98 | ## Usage 99 | 100 | Run the server in the directory you want to watch: 101 | 102 | % livereload 103 | 104 | You should see something like this: 105 | 106 | ![](https://github.com/mockko/livereload/raw/master/docs/images/livereload-server-running.png) 107 | 108 | Now, if you are using Safari, right-click the page you want to be livereload'ed and choose “Enable LiveReload”: 109 | 110 | ![](https://github.com/mockko/livereload/raw/master/docs/images/safari-context-menu.png) 111 | 112 | If you are using Chrome, just click the toolbar button (it will turn green to indicate that LiveReload is active). 113 | 114 | 115 | ### Advanced Usage 116 | 117 | If you want to monitor several directories, pass them on the command line: 118 | 119 | % livereload /some/dir /another/dir /one/more/dir 120 | 121 | (in this case it does not matter which directory you run `livereload` from) 122 | 123 | Run `livereload --help` for a list of command-line options (there's nothing interesting there, though). 124 | 125 | Looking to also process CoffeeScript, SASS, LessCSS or HAML? Here's a [Rakefile that does that live too](http://gist.github.com/472349). (Please read the comments if you're using HAML for templates in a Rails app.) 126 | 127 | 128 | ### Configuration 129 | 130 | To: 131 | 132 | * exclude some directories or files from monitoring 133 | 134 | * monitor additional extensions (like `.haml`, if you're serving HAML directly from Rails without generating `.html` on disk) 135 | 136 | * reload the whole page when `.js` changes instead of applying the changes live 137 | 138 | ...you need to edit `.livereload` file in the monitored folder. (This file is automatically created if it does not exist when you run `livereload`.) 139 | 140 | Syntax is like this: 141 | 142 | # Lines starting with pound sign (#) are ignored. 143 | 144 | # additional extensions to monitor 145 | config.exts << 'haml' 146 | 147 | # exclude files with NAMES matching this mask 148 | config.exclusions << '~*' 149 | # exclude files with PATHS matching this mask (if the mask contains a slash) 150 | config.exclusions << '/excluded_dir/*' 151 | # exclude files with PATHS matching this REGEXP 152 | config.exclusions << /somedir.*(ab){2,4}.(css|js)$/ 153 | 154 | # reload the whole page when .js changes 155 | config.apply_js_live = false 156 | # reload the whole page when .css changes 157 | config.apply_css_live = false 158 | 159 | # wait 50ms for more changes before reloading a page 160 | #config.grace_period = 0.05 161 | 162 | Configuration changes are applied live (it is called *Live* Reload after all, that has to mean something). 163 | 164 | A global config file (`~/.livereload`) is also supported if you happen to need one. It is merged with per-folder configurations. 165 | 166 | 167 | ## Limitations 168 | 169 | LiveReload does not work with local files in Safari. 170 | 171 | ## Spread the word 172 | 173 | [@livereload](http://twitter.com/livereload) on Twitter! 174 | 175 | ###What do our users say? 176 | 177 | “I think LiveReload is going to change the way I work...” [@mheerema](http://twitter.com/mheerema/status/18363670011) 178 | 179 | “spent a day using livereload. really impressed, very nice to watch pages update as I add / change code.” [@pollingj](http://twitter.com/pollingj/status/18366550224) 180 | 181 | “Gem of the month (quarter?): LiveReload” [@grimen](http://twitter.com/grimen/status/18369684099) 182 | 183 | Feel like chatting? Join us at livereload@jaconda.im — just add this contact to your Jabber / Google Talk. 184 | 185 | 186 | ## License 187 | 188 | This software is distributed under the MIT license. 189 | 190 | 191 | ## Thanks 192 | 193 | LiveReload has been greatly inspired by (and actually borrows a few lines of code from) [XRefresh](http://xrefresh.binaryage.com/), a similar tool for Firefox. 194 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LiveReload 1.x Is Deprecated 2 | 3 | Please switch to either: 4 | 5 | [LiveReload 2](http://livereload.com/) — a native graphical app for Mac and Windows 6 | 7 | or: 8 | 9 | [guard-livereload](https://github.com/guard/guard-livereload) — another command-line tool using our browser extensions. 10 | 11 | If you really want it, though, [old instructions on settings up LiveReload 1.x are here](https://github.com/mockko/livereload/blob/master/README-old.md). 12 | 13 | See you! 14 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rake/clean' 2 | 3 | LIVERELOAD_VERSION = '1.6.1' 4 | GEM_SRC = FileList["server/lib/*.rb", "server/*.gemspec", "server/bin/*"] 5 | GEM_DIST = "server/livereload-#{LIVERELOAD_VERSION}.gem" 6 | 7 | 8 | class FileGroup 9 | 10 | attr_reader :sources, :mapping 11 | 12 | def initialize sources, &mapping 13 | @sources = sources 14 | @mapping = mapping 15 | end 16 | 17 | def targets 18 | @targets ||= @sources.map { |source| @mapping.call(source) } 19 | end 20 | 21 | def each 22 | @sources.each do |source| 23 | yield source, @mapping.call(source) 24 | end 25 | end 26 | 27 | def rule 28 | each do |source, target| 29 | file target => [source] do |t| 30 | yield source, target 31 | end 32 | end 33 | end 34 | 35 | def self.[] *args 36 | self.new(*args) 37 | end 38 | 39 | end 40 | 41 | 42 | def copy_source dest, *sources 43 | File.open(dest, 'w') do |df| 44 | df.write sources.map { |fn| File.read(fn) }.join("\n") 45 | end 46 | end 47 | 48 | 49 | file 'LiveReload.chromeextension/LiveReload-content.js' => ['src/content.js', 'src/chrome/content.js'] do |t| 50 | copy_source t.name, *t.prerequisites 51 | end 52 | 53 | file 'LiveReload.chromeextension/LiveReload-background.js' => ['src/background.js', 'src/chrome/background.js'] do |t| 54 | copy_source t.name, *t.prerequisites 55 | end 56 | 57 | desc "Prepare the Chome extension" 58 | task :chrome => ['LiveReload.chromeextension/LiveReload-content.js', 'LiveReload.chromeextension/LiveReload-background.js'] 59 | 60 | 61 | file 'LiveReload.safariextension/LiveReload-injected.js' => ['src/content.js', 'src/safari/injected.js'] do |t| 62 | copy_source t.name, *t.prerequisites 63 | end 64 | 65 | file 'LiveReload.safariextension/LiveReload-global.js' => ['src/background.js', 'src/safari/global.js'] do |t| 66 | copy_source t.name, *t.prerequisites 67 | end 68 | 69 | desc "Prepare the Safari extension" 70 | task :safari => ['LiveReload.safariextension/LiveReload-injected.js', 'LiveReload.safariextension/LiveReload-global.js'] 71 | 72 | FIREFOX_INTERIM = FileGroup.new(%w[src/background.js src/content.js]) { |f| File.join('Firefox/content', File.basename(f)) } 73 | FIREFOX_BASIC = FileList['Firefox/**/*.{js,xul,manifest,rdf}'] - FIREFOX_INTERIM.targets 74 | FIREFOX_ALL = FIREFOX_BASIC + FIREFOX_INTERIM.targets 75 | 76 | FIREFOX_INTERIM.rule do |source, target| 77 | copy_source target, source 78 | end 79 | 80 | file 'LiveReload.xpi' => FIREFOX_ALL do |t| 81 | full_dst = File.expand_path(t.name) 82 | Dir.chdir 'Firefox' do 83 | sh 'zip', full_dst, *t.prerequisites.map { |f| f.sub(%r!^Firefox/!, '') } 84 | end 85 | end 86 | 87 | desc "Build the Firefox extension" 88 | task :firefox => 'LiveReload.xpi' 89 | 90 | 91 | file 'livereload-xbrowser.js' => %w(src/background.js src/content.js src/xbrowser/livereload.js) do |t| 92 | src = %w(src/background.js src/content.js src/xbrowser/livereload.js).collect { |f| File.read(f).strip }.join("\n") + "\n" 93 | src.gsub! "host: (navigator.appVersion.indexOf('Linux') >= 0 ? '0.0.0.0' : 'localhost'),", "host: (location.host || 'localhost').split(':')[0]," 94 | File.open(t.name, 'w') { |f| f.write(src) } 95 | end 96 | 97 | file '../LiveReload/livereload.js' => ['livereload-xbrowser.js'] do |t| 98 | File.open(t.name, 'w') { |f| f.write(File.read(t.prerequisites.first)) } 99 | end 100 | 101 | desc "Build the cross-browser version" 102 | task :xbrowser => 'livereload-xbrowser.js' 103 | 104 | 105 | desc "Update the file bundled with LiveReload 2" 106 | task :lr2 => ['../LiveReload/livereload.js'] 107 | 108 | 109 | desc "Process all browser extensions" 110 | task :all => [:safari, :chrome, :firefox, :xbrowser] 111 | 112 | task :default => :all 113 | 114 | namespace :gem do 115 | file GEM_DIST => GEM_SRC do 116 | cd 'server' do 117 | sh 'gem', 'build', 'livereload.gemspec' 118 | end 119 | end 120 | 121 | desc "Build the livereload gem" 122 | task :build => GEM_DIST 123 | 124 | desc "Install the livereload gem/command" 125 | task :install => :build do 126 | sh 'sudo', 'gem', 'install', GEM_DIST 127 | end 128 | 129 | desc "Uninstall the livereload gem/command" 130 | task :uninstall do 131 | sh 'sudo', 'gem', 'uninstall', 'livereload' 132 | end 133 | 134 | desc "Publish the gem on gemcutter" 135 | task :publish => :build do 136 | sh 'gem', 'push', GEM_DIST 137 | end 138 | end 139 | 140 | 141 | task :test do 142 | #`python -m SimpleHTTPServer` 143 | 144 | require 'webrick' 145 | # http://tobyho.com/HTTP_Server_in_5_Lines_With_Webrick 146 | class NonCachingFileHandler < WEBrick::HTTPServlet::FileHandler 147 | def prevent_caching(res) 148 | res['ETag'] = nil 149 | res['Last-Modified'] = Time.now + 100**4 150 | res['Cache-Control'] = 'no-store, no-cache, must-revalidate, post-check=0, pre-check=0' 151 | end 152 | 153 | def do_GET(req, res) 154 | super 155 | prevent_caching(res) 156 | end 157 | 158 | end 159 | 160 | serv = Thread.new() { 161 | server = WEBrick::HTTPServer.new :Port => 8000 162 | server.mount('/', NonCachingFileHandler, './') 163 | puts 'http://0.0.0.0:8000 started' 164 | server.start 165 | } 166 | 167 | serv2 = Thread.new() { 168 | server = WEBrick::HTTPServer.new :Port => 8001 169 | server.mount('/', NonCachingFileHandler, './') 170 | puts 'http://0.0.0.0:8001 started' 171 | server.start 172 | } 173 | 174 | `open http://0.0.0.0:8000/test` unless `which open`.empty? 175 | serv.join 176 | serv2.join 177 | end 178 | 179 | 180 | CLEAN.include('LiveReload.xpi') 181 | CLOBBER.include(%w( 182 | LiveReload.chromeextension/LiveReload-content.js 183 | LiveReload.chromeextension/LiveReload-background.js 184 | LiveReload.safariextension/LiveReload-injected.js 185 | LiveReload.safariextension/LiveReload-global.js 186 | Firefox/content/background.js 187 | Firefox/content/content.js 188 | )) 189 | -------------------------------------------------------------------------------- /artwork/icon100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mockko/livereload/84dcf0ff3d23809ae2b46161dd02869a20f9064e/artwork/icon100.png -------------------------------------------------------------------------------- /artwork/icon100.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mockko/livereload/84dcf0ff3d23809ae2b46161dd02869a20f9064e/artwork/icon100.psd -------------------------------------------------------------------------------- /artwork/icon128.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mockko/livereload/84dcf0ff3d23809ae2b46161dd02869a20f9064e/artwork/icon128.psd -------------------------------------------------------------------------------- /artwork/icon16.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mockko/livereload/84dcf0ff3d23809ae2b46161dd02869a20f9064e/artwork/icon16.psd -------------------------------------------------------------------------------- /artwork/icon19.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mockko/livereload/84dcf0ff3d23809ae2b46161dd02869a20f9064e/artwork/icon19.psd -------------------------------------------------------------------------------- /artwork/icon32.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mockko/livereload/84dcf0ff3d23809ae2b46161dd02869a20f9064e/artwork/icon32.psd -------------------------------------------------------------------------------- /artwork/icon48.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mockko/livereload/84dcf0ff3d23809ae2b46161dd02869a20f9064e/artwork/icon48.psd -------------------------------------------------------------------------------- /artwork/icon512.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mockko/livereload/84dcf0ff3d23809ae2b46161dd02869a20f9064e/artwork/icon512.psd -------------------------------------------------------------------------------- /artwork/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mockko/livereload/84dcf0ff3d23809ae2b46161dd02869a20f9064e/artwork/screenshot.png -------------------------------------------------------------------------------- /artwork/screenshot.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mockko/livereload/84dcf0ff3d23809ae2b46161dd02869a20f9064e/artwork/screenshot.psd -------------------------------------------------------------------------------- /docs/VersionNumbers.md: -------------------------------------------------------------------------------- 1 | Version Numbers 2 | =============== 3 | 4 | 5 | **Safari extension version:** 6 | 7 | * `LiveReload.safariextension/Info.plist`: 8 | 9 | CFBundleShortVersionString 10 | 1.2 11 | CFBundleVersion 12 | 1.2 13 | 14 | * `LiveReload-update.plist` (autoupdate manifest used by Safari to check for updates — loaded right from master branch on GitHub): 15 | 16 | CFBundleVersion 17 | 1.2 18 | CFBundleShortVersionString 19 | 1.2 20 | URL 21 | http://github.com/downloads/mockko/livereload/LiveReload-1.2.safariextz 22 | 23 | (3 occurrences — note the last one in URL) 24 | 25 | 26 | **Chrome extension version:** 27 | 28 | * `LiveReload.chromeextension/manifest.json`: 29 | 30 | "version": "1.2.1", 31 | 32 | 33 | **Gem version** (used by gem command to check for updates): 34 | 35 | * `server/livereload.gemspec`: 36 | 37 | `s.version = "1.2.1"` 38 | 39 | * `Rakefile`: 40 | 41 | `LIVERELOAD_VERSION = '1.2.1'` 42 | 43 | 44 | **API version** (shared by the Gem and extensions): 45 | 46 | * `LiveReload.chromeextension/LiveReload-background.js`: 47 | 48 | var version = "1.2"; 49 | 50 | * `LiveReload.safariextension/LiveReload-global.js`: 51 | 52 | var version = "1.2"; 53 | 54 | * `server/lib/livereload.rb`: 55 | 56 | VERSION = "1.2" 57 | -------------------------------------------------------------------------------- /docs/WebSocketProtocol.md: -------------------------------------------------------------------------------- 1 | WebSocket Protocol Details 2 | ========================== 3 | 4 | 5 | API versions 1.3–1.6 use JSON in the server-to-browser direction. 6 | 7 | API version 1.2 was an extremely simple one. 8 | 9 | 10 | Handshake 11 | --------- 12 | 13 | After the connection is initiated, the server immediately sends API version info to the browser: 14 | 15 | !!ver:1.6 16 | 17 | If the browser is okay to speak this API version, it does nothing. If the browser does not support this version, it closes the connection. 18 | 19 | 20 | JSON command format 21 | ------------------- 22 | 23 | General format of a JSON command is: 24 | 25 | ["command_name", args] 26 | 27 | 28 | File Modified 29 | ------------- 30 | 31 | When a file is modified, the full path is sent to the browser as a “refresh” command: 32 | 33 | ["refresh", { "path": "/some/path/myfile.css", "apply_js_live": true, "apply_css_live": true }] 34 | 35 | `path` is required; `apply_js_live` and `apply_css_live` are optional. 36 | 37 | 38 | URL Change 39 | ---------- 40 | 41 | The browser sometimes sends the URL of the current page back to the server. It is printed on the console and serves purely informational purposes (to give the user some confidence that things are working the way (s)he expects). 42 | 43 | http://example.com/example/path/ 44 | -------------------------------------------------------------------------------- /docs/images/chrome-button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mockko/livereload/84dcf0ff3d23809ae2b46161dd02869a20f9064e/docs/images/chrome-button.png -------------------------------------------------------------------------------- /docs/images/chrome-install-prompt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mockko/livereload/84dcf0ff3d23809ae2b46161dd02869a20f9064e/docs/images/chrome-install-prompt.png -------------------------------------------------------------------------------- /docs/images/livereload-server-running.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mockko/livereload/84dcf0ff3d23809ae2b46161dd02869a20f9064e/docs/images/livereload-server-running.png -------------------------------------------------------------------------------- /docs/images/safari-context-menu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mockko/livereload/84dcf0ff3d23809ae2b46161dd02869a20f9064e/docs/images/safari-context-menu.png -------------------------------------------------------------------------------- /docs/images/safari-install-prompt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mockko/livereload/84dcf0ff3d23809ae2b46161dd02869a20f9064e/docs/images/safari-install-prompt.png -------------------------------------------------------------------------------- /example/xbrowser.css: -------------------------------------------------------------------------------- 1 | html { 2 | font-family: sans-serif; 3 | background-color: beige; 4 | } 5 | 6 | body { 7 | max-width: 300px; 8 | margin: 0 auto; 9 | } 10 | h1 { 11 | font: 150% futura, sans-serif; 12 | } 13 | ol, li { 14 | margin: .4em 0; 15 | padding-left: 0; 16 | } 17 | -------------------------------------------------------------------------------- /example/xbrowser.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | LiveReload example 5 | 6 | 7 | 8 | 9 | 14 | 15 | 16 |

Example of Cross-browser version of LiveReload

17 |
    18 |
  1. Run the LiveReload server in the directory that contains the file you are reading;
  2. 19 |
  3. Open this page in the browser;
  4. 20 |
  5. Edit;
  6. 21 |
  7. Enjoy!
  8. 22 |
23 | 24 | 25 | -------------------------------------------------------------------------------- /server/bin/livereload: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env ruby 2 | require 'rubygems' 3 | require File.join(File.dirname(__FILE__), '..', 'lib', 'livereload') 4 | require 'optparse' 5 | 6 | def parse_command_line_config(args) 7 | LiveReload::Config.new do |config| 8 | 9 | opts = OptionParser.new do |opts| 10 | opts.banner = "Usage: livereload [options] [directory1 directory2 ...]" 11 | 12 | opts.separator "" 13 | opts.separator "Browser communication options:" 14 | 15 | opts.on("--address [ADDRESS]", "Network interface to listen on (default is 0.0.0.0)") do |v| 16 | config.host = v 17 | end 18 | 19 | opts.on("--port [PORT]", Integer, "TCP port to listen on (default is 35729)") do |v| 20 | config.port = v.to_i 21 | end 22 | 23 | opts.separator "" 24 | opts.separator "Debugging options:" 25 | 26 | opts.on("-D", "--[no-]debug", "Print debugging info") do |v| 27 | config.debug = v 28 | end 29 | 30 | opts.separator "" 31 | opts.separator "Common options:" 32 | 33 | opts.on_tail("-h", "--help", "Show this message") do 34 | puts opts 35 | exit 36 | end 37 | 38 | opts.on_tail("--version", "Show version") do 39 | puts LiveReload::GEM_VERSION 40 | exit 41 | end 42 | end 43 | 44 | opts.parse!(args) 45 | end 46 | end 47 | 48 | explicit_config = parse_command_line_config(ARGV) 49 | 50 | directories = if ARGV.size == 0 then ['.'] else ARGV end 51 | LiveReload.run directories, explicit_config 52 | -------------------------------------------------------------------------------- /server/lib/livereload.rb: -------------------------------------------------------------------------------- 1 | require 'em-websocket' 2 | require 'em-dir-watcher' 3 | require 'json' 4 | require 'stringio' 5 | 6 | # Chrome sometimes sends HTTP/1.0 requests in violation of WebSockets spec 7 | # hide the warning about redifinition of a constant 8 | saved_stderr = $stderr 9 | $stderr = StringIO.new 10 | EventMachine::WebSocket::HandlerFactory::PATH = /^(\w+) (\/[^\s]*) HTTP\/1\.[01]$/ 11 | $stderr = saved_stderr 12 | 13 | class Object 14 | def method_missing_with_livereload id, *args, &block 15 | if id == :config 16 | Object.send(:instance_variable_get, '@livereload_config') 17 | else 18 | method_missing_without_livereload id, *args, &block 19 | end 20 | end 21 | end 22 | 23 | module LiveReload 24 | GEM_VERSION = "1.6" 25 | API_VERSION = "1.6" 26 | 27 | PROJECT_CONFIG_FILE_TEMPLATE = <<-END.strip.split("\n").collect { |line| line.strip + "\n" }.join("") 28 | # Lines starting with pound sign (#) are ignored. 29 | 30 | # additional extensions to monitor 31 | #config.exts << 'haml' 32 | 33 | # exclude files with NAMES matching this mask 34 | #config.exclusions << '~*' 35 | # exclude files with PATHS matching this mask (if the mask contains a slash) 36 | #config.exclusions << '/excluded_dir/*' 37 | # exclude files with PATHS matching this REGEXP 38 | #config.exclusions << /somedir.*(ab){2,4}\.(css|js)$/ 39 | 40 | # reload the whole page when .js changes 41 | #config.apply_js_live = false 42 | # reload the whole page when .css changes 43 | #config.apply_css_live = false 44 | # reload the whole page when images (png, jpg, gif) change 45 | #config.apply_images_live = false 46 | 47 | # wait 100ms for more changes before reloading a page 48 | #config.grace_period = 0.1 49 | END 50 | 51 | # note that host and port options do not make sense in per-project config files 52 | class Config 53 | attr_accessor :host, :port, :exts, :exts_overwrite, :exclusions, :debug, :apply_js_live, :apply_css_live, :apply_images_live, :grace_period 54 | 55 | def initialize &block 56 | @host = nil 57 | @port = nil 58 | @debug = nil 59 | @exts = [] 60 | @exclusions = [] 61 | @apply_js_live = nil 62 | @apply_css_live = nil 63 | @apply_images_live = nil 64 | @grace_period = nil 65 | 66 | update!(&block) if block 67 | end 68 | 69 | def update! 70 | @exts = [nil] + @exts # nil is used as a marker to detect if the array has been overwritten 71 | 72 | yield self 73 | 74 | @exts_overwrite = @exts.empty? || ! @exts.first.nil? 75 | @exts = @exts.compact 76 | 77 | # remove leading dots 78 | @exts = @exts.collect { |e| e.sub(/^\./, '') } 79 | end 80 | 81 | def merge! other 82 | @host = other.host if other.host 83 | @port = other.port if other.port 84 | if other.exts_overwrite 85 | @exts = other.exts 86 | else 87 | @exts += other.exts.compact 88 | end 89 | @exclusions = other.exclusions + @exclusions 90 | @debug = other.debug if other.debug != nil 91 | @apply_js_live = other.apply_js_live if other.apply_js_live != nil 92 | @apply_css_live = other.apply_css_live if other.apply_css_live != nil 93 | @apply_images_live = other.apply_images_live if other.apply_images_live != nil 94 | @grace_period = other.grace_period if other.grace_period != nil 95 | 96 | self 97 | end 98 | 99 | class << self 100 | def load_from file 101 | Config.new do |config| 102 | if File.file? file 103 | Object.send(:instance_variable_set, '@livereload_config', config) 104 | Object.send(:alias_method, :method_missing_without_livereload, :method_missing) 105 | Object.send(:alias_method, :method_missing, :method_missing_with_livereload) 106 | load file, true 107 | Object.send(:alias_method, :method_missing, :method_missing_without_livereload) 108 | Object.send(:instance_variable_set, '@livereload_config', nil) 109 | end 110 | end 111 | end 112 | 113 | def merge *configs 114 | configs.inject(Config.new) { |merged, config| config && merged.merge!(config) || merged } 115 | end 116 | end 117 | end 118 | 119 | DEFAULT_CONFIG = Config.new do |config| 120 | config.debug = false 121 | config.host = '0.0.0.0' 122 | config.port = 35729 123 | config.exts = %w/html css js png gif jpg php php5 py rb erb/ 124 | config.exclusions = %w!*/.git/* */.svn/* */.hg/*! 125 | config.apply_js_live = false 126 | config.apply_css_live = true 127 | config.apply_images_live = true 128 | config.grace_period = 0.05 129 | end 130 | 131 | USER_CONFIG_FILE = File.expand_path("~/.livereload") 132 | USER_CONFIG = Config.load_from(USER_CONFIG_FILE) 133 | 134 | class Project 135 | attr_reader :config 136 | 137 | def initialize directory, explicit_config=nil 138 | @directory = directory 139 | @explicit_config = explicit_config 140 | read_config 141 | end 142 | 143 | def read_config 144 | project_config_file = File.join(@directory, '.livereload') 145 | unless File.file? project_config_file 146 | File.open(project_config_file, 'w') do |file| 147 | file.write PROJECT_CONFIG_FILE_TEMPLATE 148 | end 149 | end 150 | project_config = Config.load_from project_config_file 151 | @config = Config.merge(DEFAULT_CONFIG, USER_CONFIG, project_config, @explicit_config) 152 | end 153 | 154 | def print_config 155 | puts "Watching: #{@directory}" 156 | puts " - extensions: " + @config.exts.collect {|e| ".#{e}"}.join(" ") 157 | if !@config.apply_js_live && !@config.apply_css_live 158 | puts " - live refreshing disabled for .css & .js: will reload the whole page on every change" 159 | elsif !@config.apply_js_live 160 | puts " - live refreshing disabled for .js: will reload the whole page when .js is changed" 161 | elsif !@config.apply_css_live 162 | puts " - live refreshing disabled for .css: will reload the whole page when .css is changed" 163 | end 164 | if @config.exclusions.size > 0 165 | puts " - excluding changes in: " + @config.exclusions.join(" ") 166 | end 167 | if @config.grace_period > 0 168 | puts " - with a grace period of #{sprintf('%0.2f', @config.grace_period)} sec after each change" 169 | end 170 | end 171 | 172 | def when_changes_detected &block 173 | @when_changes_detected = block 174 | end 175 | 176 | def restart_watching 177 | if @dw 178 | @dw.stop 179 | end 180 | @dw = EMDirWatcher.watch @directory, 181 | :include_only => (['/.livereload'] + @config.exts.collect { |ext| ["*.#{ext}", ".*.#{ext}"]}).flatten, 182 | :exclude => @config.exclusions, 183 | :grace_period => @config.grace_period do |paths| 184 | begin 185 | paths.each do |path| 186 | if File.basename(path) == '.livereload' 187 | @when_changes_detected.call [:config_changed, path] 188 | else 189 | @when_changes_detected.call [:modified, File.join(@directory, path)] 190 | end 191 | end 192 | rescue 193 | puts $! 194 | puts $!.backtrace 195 | end 196 | end 197 | end 198 | end 199 | 200 | def self.configure 201 | yield Config 202 | end 203 | 204 | def self.run(directories, explicit_config) 205 | # EventMachine needs to run kqueue for the watching API to work 206 | EM.kqueue = true if EM.kqueue? 207 | 208 | web_sockets = [] 209 | 210 | # for host and port 211 | global_config = Config.merge(DEFAULT_CONFIG, USER_CONFIG, explicit_config) 212 | directories = directories.collect { |directory| File.expand_path(directory) } 213 | projects = directories.collect { |directory| Project.new(directory, explicit_config) } 214 | 215 | puts 216 | puts "Version: #{GEM_VERSION} (compatible with browser extension versions #{API_VERSION}.x)" 217 | puts "Port: #{global_config.port}" 218 | projects.each { |project| project.print_config } 219 | 220 | EventMachine.run do 221 | projects.each do |project| 222 | project.when_changes_detected do |event, modified_file| 223 | case event 224 | when :config_changed 225 | puts 226 | puts ">> Configuration change: " + modified_file 227 | puts 228 | EventMachine.next_tick do 229 | projects.each { |project| project.read_config; project.print_config } 230 | puts 231 | projects.each { |project| project.restart_watching } 232 | end 233 | when :excluded 234 | puts "Excluded: #{File.basename(modified_file)}" 235 | when :modified 236 | puts "Modified: #{File.basename(modified_file)}" 237 | data = ['refresh', { :path => modified_file, 238 | :apply_js_live => project.config.apply_js_live, 239 | :apply_css_live => project.config.apply_css_live, 240 | :apply_images_live => project.config.apply_images_live }].to_json 241 | puts data if global_config.debug 242 | web_sockets.each do |ws| 243 | ws.send data 244 | end 245 | end 246 | end 247 | end 248 | 249 | projects.each { |project| project.restart_watching } 250 | 251 | puts 252 | puts "LiveReload is waiting for browser to connect." 253 | EventMachine::WebSocket.start(:host => global_config.host, :port => global_config.port, :debug => global_config.debug) do |ws| 254 | ws.onopen do 255 | begin 256 | puts "Browser connected."; ws.send "!!ver:#{API_VERSION}"; web_sockets << ws 257 | rescue 258 | puts $! 259 | puts $!.backtrace 260 | end 261 | end 262 | ws.onmessage do |msg| 263 | puts "Browser URL: #{msg}" 264 | end 265 | ws.onclose do 266 | web_sockets.delete ws 267 | puts "Browser disconnected." 268 | end 269 | end 270 | end 271 | end 272 | end 273 | -------------------------------------------------------------------------------- /server/livereload.gemspec: -------------------------------------------------------------------------------- 1 | Gem::Specification.new do |s| 2 | s.name = 'livereload' 3 | s.version = "1.6.1" 4 | 5 | s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= 6 | s.authors = ["Andrey Tarantsov"] 7 | s.date = %q{2011-11-26} 8 | s.description = <<-END 9 | LiveReload is a Safari/Chrome extension + a command-line tool that: 10 | 1. Applies CSS and JavaScript file changes without reloading a page. 11 | 2. Automatically reloads a page when any other file changes (html, image, server-side script, etc). 12 | END 13 | s.email = %q{andreyvit@gmail.com} 14 | s.extra_rdoc_files = [] 15 | s.files = ["bin/livereload", "lib/livereload.rb"] 16 | s.homepage = %q{http://github.com/mockko/livereload/} 17 | s.rdoc_options = ["--charset=UTF-8"] 18 | s.rubygems_version = %q{1.3.6} 19 | s.summary = %q{A command-line server for the LiveReload Safari/Chrome extension} 20 | s.test_files = [] 21 | s.executables = ['livereload'] 22 | s.requirements << 'LiveReload Safari extension' 23 | s.add_dependency('em-websocket', '>= 0.3.5') 24 | s.add_dependency('em-dir-watcher', '>= 0.1') 25 | s.add_dependency('json', '>= 1.5.3') 26 | end 27 | -------------------------------------------------------------------------------- /src/background.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @param {Function} reloadPage function(page, data){} 5 | */ 6 | function LivereloadBackground(reloadPage) { 7 | this.reloadPage = reloadPage; 8 | this.pages = []; 9 | this.socket = null; 10 | this.disconnectionReason = 'unexpected'; 11 | this.versionInfoReceived = false; 12 | } 13 | 14 | LivereloadBackground.prototype = { 15 | 16 | apiVersion: '1.6', 17 | 18 | // localhost does not work on Linux b/c of http://code.google.com/p/chromium/issues/detail?id=36652, 19 | // 0.0.0.0 does not work on Windows 20 | host: (navigator.appVersion.indexOf('Linux') >= 0 ? '0.0.0.0' : 'localhost'), 21 | 22 | port: 35729, 23 | 24 | get uri(){ 25 | return 'ws://' + this.host + ':' + this.port + '/websocket'; 26 | }, 27 | 28 | addPage: function(page) { 29 | var index = this.pages.indexOf(page); 30 | if (index == -1) { 31 | this.pages.push(page); 32 | } 33 | }, 34 | 35 | get lastPage() { 36 | var length = this.pages.length; 37 | return length ? this.pages[length - 1] : null; 38 | }, 39 | 40 | alert: function(message) { 41 | alert(message); 42 | }, 43 | 44 | log: function(message) { 45 | if (window.console && console.log) { 46 | console.log('LiveReload ' + message); 47 | } 48 | }, 49 | 50 | versionAsFloat: function(version) { 51 | var triple = version.split('.').map(function(n){ 52 | return parseInt(n); 53 | }); 54 | return parseFloat(triple[0] + '.' + triple[1]); 55 | }, 56 | 57 | /** 58 | * @param {string} data 59 | * @return {boolean} true on succes, false on error 60 | */ 61 | checkVersion: function(data) { 62 | var m = data.match(/!!ver:([\d.]+)/); 63 | if (m) { 64 | var server = this.versionAsFloat(m[1]); 65 | var client = this.versionAsFloat(this.apiVersion); 66 | // Compare only major and minor versions. Do not compare patch version. 67 | if (server == client) { 68 | return true; 69 | } else { 70 | if (server < client) { 71 | this.alert('You need to update the command-line tool to continue using LiveReload.\n\n' 72 | + 'Extension version: ' + this.apiVersion + '\n' 73 | + 'Command-line tool version: ' + m[1] + '\n\n' 74 | + 'Please run the following command to update your command-line tool:\n' 75 | + ' gem update livereload'); 76 | } else { 77 | this.alert('You need to update the browser extension to continue using LiveReload.\n\n' 78 | + 'Extension version: ' + this.apiVersion + '\n' 79 | + 'Command-line tool version: ' + m[1] + '\n\n' 80 | + 'Please go to the extensions manager and check for updates.'); 81 | } 82 | } 83 | } else { 84 | this.alert('You are using an old incompatible version of the command-line tool.\n\n' 85 | + 'Please run the following command to update your command-line tool:\n' 86 | + ' gem update livereload'); 87 | } 88 | return false; 89 | }, 90 | 91 | reloadPages: function(data) { 92 | for (var i = this.pages.length; i--;) { 93 | this.reloadPage(this.pages[i], data); 94 | } 95 | }, 96 | 97 | _onmessage: function(event) { 98 | if (this.pages.length == 0) { 99 | throw 'No pages'; 100 | } 101 | var data = event.data; 102 | this.log('received: ' + data); 103 | if (!this.versionInfoReceived) { 104 | if (this.checkVersion(data)) { 105 | this.versionInfoReceived = true; 106 | } else { 107 | this.disconnectionReason = 'version-mismatch'; 108 | event.target.close(); 109 | } 110 | } else { 111 | this.reloadPages(data); 112 | } 113 | }, 114 | 115 | _onclose: function(e) { 116 | this.log('disconnected from ' + (e.target.URL || e.target.url)); 117 | if (this.disconnectionReason == 'cannot-connect') { 118 | this.alert('Cannot connect to LiveReload server:\n' + this.uri); 119 | } 120 | this.onDisconnect(); 121 | }, 122 | 123 | _onopen: function(e) { 124 | this.log('connected to ' + (e.target.URL || e.target.url)); 125 | this.disconnectionReason = 'broken'; 126 | this.sendPageUrl(); 127 | }, 128 | 129 | _onerror: function(event) { 130 | console.warn('error: ', event); 131 | }, 132 | 133 | connect: function() { 134 | var Socket = window.MozWebSocket || window.WebSocket; 135 | if (!Socket) { 136 | if (window.opera) { 137 | throw 'WebSocket is disabled. To turn it on, open \nopera:config#UserPrefs|EnableWebSockets and check in the checkbox.'; 138 | } else if (navigator.userAgent.indexOf('Firefox/') != -1) { 139 | throw 'WebSocket is disabled.\nTo turn it on, open about:config and set network.websocket.override-security-block to true.\nhttps://developer.mozilla.org/en/WebSockets'; 140 | } 141 | } 142 | 143 | if (this.socket) { 144 | throw 'WebSocket already opened'; 145 | } 146 | 147 | var socket = this.socket = new Socket(this.uri); 148 | 149 | this.disconnectionReason = 'cannot-connect'; 150 | this.versionInfoReceived = false; 151 | 152 | var self = this; 153 | socket.onopen = function(e) { return self._onopen(e); }; 154 | socket.onmessage = function(e) { return self._onmessage(e); }; 155 | socket.onclose = function(e) { return self._onclose(e); }; 156 | socket.onerror = function(e) { return self._onerror(e); }; 157 | }, 158 | 159 | disconnect: function() { 160 | this.disconnectionReason = 'manual'; 161 | if (this.socket) { 162 | this.socket.close(); 163 | } 164 | }, 165 | 166 | onDisconnect: function() { 167 | this.socket = null; 168 | this.versionInfoReceived = false; 169 | this.disableAllPages(); 170 | }, 171 | 172 | sendPageUrl: function() { 173 | var activePage = this.lastPage; 174 | if (activePage == null) { 175 | throw 'No active page'; 176 | } 177 | this.socket && this.socket.send(activePage.location.href); 178 | }, 179 | 180 | enablePage: function(page) { 181 | if (this.pages.indexOf(page) > -1) { 182 | throw 'Page alredy enabled'; 183 | } 184 | this.pages.push(page); 185 | if (this.socket && this.socket.readyState == 1) { 186 | this.sendPageUrl(); 187 | } else { 188 | try { 189 | this.connect(); 190 | } catch(e) { 191 | this.alert('LiveReload failed to establish connection: ' + (e && e.message || e)); 192 | return; 193 | } 194 | } 195 | this.onEnablePage(page); 196 | }, 197 | 198 | onEnablePage: function(page) {}, 199 | 200 | disablePage: function(page) { 201 | var index = this.pages.indexOf(page); 202 | if (index > -1) { 203 | //TODO: log on the server about disconected pages 204 | if (this.pages.length == 1) { 205 | this.disconnect(); 206 | } else { 207 | this.pages.splice(index, 1); 208 | this.onDisablePage(page); 209 | } 210 | } 211 | }, 212 | 213 | onDisablePage: function(page) {}, 214 | 215 | disableAllPages: function() { 216 | for (var i = this.pages.length; i--;) { 217 | this.onDisablePage(this.pages[i]); 218 | } 219 | this.pages.length = 0; 220 | }, 221 | 222 | togglePage: function(page) { 223 | var index = this.pages.indexOf(page); 224 | if (index == -1) { 225 | this.enablePage(page); 226 | } else { 227 | this.disablePage(page); 228 | } 229 | }, 230 | 231 | constructor: LivereloadBackground 232 | 233 | }; 234 | 235 | -------------------------------------------------------------------------------- /src/chrome/background.js: -------------------------------------------------------------------------------- 1 | function LivereloadBackgroundChrome() {} 2 | LivereloadBackgroundChrome.prototype = new LivereloadBackground(function reloadPage(tabId, data) { 3 | chrome.tabs.sendRequest(tabId, data); 4 | }); 5 | 6 | LivereloadBackgroundChrome.prototype.updateSettings = function () { 7 | this.disconnect(); 8 | }; 9 | 10 | LivereloadBackgroundChrome.prototype.connect = function () { 11 | if (localStorage['host']) this.host = localStorage['host']; 12 | else this.host = this.__proto__.__proto__.host; 13 | 14 | if (localStorage['port']) this.port = localStorage['port']; 15 | else this.port = this.__proto__.__proto__.port; 16 | 17 | this.__proto__.__proto__.connect.call(this); 18 | }; 19 | 20 | LivereloadBackgroundChrome.prototype.sendPageUrl = function() { 21 | var activeTab = this.lastPage; 22 | if (activeTab == null) { 23 | throw 'No active tab'; 24 | } 25 | var socket = this.socket; 26 | chrome.tabs.get(activeTab, function(tab) { 27 | socket.send(tab.url); 28 | }); 29 | }; 30 | 31 | LivereloadBackgroundChrome.prototype.onEnablePage = function(tabId) { 32 | chrome.browserAction.setTitle({title: 'Disable LiveReload'}); 33 | chrome.browserAction.setIcon({path: 'icon19-on.png'}); 34 | }; 35 | 36 | LivereloadBackgroundChrome.prototype.onDisablePage = function(tabId) { 37 | chrome.browserAction.setTitle({title: 'Enable LiveReload'}); 38 | chrome.browserAction.setIcon({path: 'icon19.png'}); 39 | }; 40 | 41 | 42 | var livereload = new LivereloadBackgroundChrome; 43 | 44 | chrome.browserAction.onClicked.addListener(function(tab) { 45 | livereload.togglePage(tab.id); 46 | }); 47 | 48 | chrome.tabs.onSelectionChanged.addListener(function(tabId, selectInfo) { 49 | if (livereload.pages.indexOf(tabId) > -1) { 50 | livereload.onEnablePage(tabId); 51 | } else { 52 | livereload.onDisablePage(tabId); 53 | } 54 | }, false); 55 | 56 | chrome.tabs.onRemoved.addListener(function(tabId) { 57 | livereload.disablePage(tabId); 58 | }); 59 | -------------------------------------------------------------------------------- /src/chrome/content.js: -------------------------------------------------------------------------------- 1 | var livereload = new LivereloadContent(document); 2 | 3 | chrome.extension.onRequest.addListener(function(request, sender, sendResponse) { 4 | livereload.log('LiveReload: ' + request); 5 | livereload.reload(request); 6 | }); 7 | -------------------------------------------------------------------------------- /src/content.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function LivereloadContent(document) { 4 | this.document = document; 5 | this.strictMode = this.strictModePossible(document); 6 | } 7 | 8 | LivereloadContent.prototype = { 9 | 10 | apply_js_live: false, 11 | apply_css_live: true, 12 | apply_images_live: true, 13 | 14 | /** 15 | * Very unstable! 16 | * '' Never 17 | * 'match' On exact match 18 | * 'restricted' On exact match and restricted stylesheet (e.g. from another domain) 19 | * 'always' Reload all imported stylesheets every time 20 | */ 21 | apply_imported_css: '', 22 | 23 | strictMode: false, 24 | 25 | strictModePossible: function(document) { 26 | return document.location.protocol == 'file:'; 27 | }, 28 | 29 | /** 30 | * @param {string} path 31 | * @nosideeffects 32 | * @return {string} 33 | */ 34 | fileName: function(path) { 35 | return path 36 | .replace(/\?.*$/, '') // strip query string if any 37 | .replace(/^.*\//, ''); 38 | }, 39 | 40 | /** 41 | * @param {string} url 42 | * @param {string} path 43 | * @nosideeffects 44 | * @return {boolean} paths the same 45 | */ 46 | equals: function(url, path) { 47 | var paramsIndex = url.indexOf('?'); 48 | if (paramsIndex != -1) { 49 | url = url.slice(0, paramsIndex); 50 | } else { 51 | var hashIndex = url.indexOf('#'); 52 | if (hashIndex != -1) { 53 | url = url.slice(0, hashIndex); 54 | } 55 | } 56 | if (url.indexOf('file://') == 0) { 57 | url = url.replace(/file:[/][/](localhost)?/, ''); 58 | } else { 59 | url = this.fileName(url); 60 | } 61 | return decodeURIComponent(url) == path; 62 | }, 63 | 64 | /** 65 | * @param {string} name 66 | * @nosideeffects 67 | * @return {string} 68 | */ 69 | fileExtension: function(name) { 70 | var base = this.fileName(name); 71 | var matched = base.match(/\.[^.]+$/); 72 | return matched ? matched[0] : ''; 73 | }, 74 | 75 | /** 76 | * @nosideeffects 77 | * @return {string} 78 | */ 79 | generateExpando: function() { 80 | return 'livereload=' + Date.now(); 81 | }, 82 | 83 | /** 84 | * @param {string} msg 85 | */ 86 | log: function(msg) { 87 | if (window.console && console.log) { 88 | console.log(msg); 89 | } 90 | }, 91 | 92 | /** 93 | * @param {string} msg 94 | */ 95 | warn: function(msg) { 96 | if (window.console && console.warn) { 97 | console.warn(msg); 98 | } 99 | }, 100 | 101 | /** 102 | * @param {string} url 103 | * @param {string} [expando] 104 | * @nosideeffects 105 | * @return {string} 106 | */ 107 | generateNextUrl: function(url, expando) { 108 | expando = expando || this.generateExpando(); 109 | 110 | var hashIndex = url.indexOf('#'); 111 | var hash = ''; 112 | if (hashIndex != -1) { 113 | hash = url.slice(hashIndex); 114 | url = url.slice(0, hashIndex); 115 | } 116 | 117 | var paramsIndex = url.indexOf('?'); 118 | var params = ''; 119 | if (paramsIndex != -1) { 120 | params = url.slice(paramsIndex); 121 | var re = /(\?|&)livereload=(\d+)/; 122 | if (re.test(params)) { 123 | params = params.replace(re, function(match, separator) { 124 | return separator + expando; 125 | }); 126 | } else { 127 | params += '&' + expando; 128 | } 129 | url = url.slice(0, paramsIndex); 130 | } else { 131 | params += '?' + expando; 132 | } 133 | 134 | return url + params + hash; 135 | }, 136 | 137 | /** 138 | * @deprecated 139 | * @param {Element} script 140 | */ 141 | reloadScript: function(script) { 142 | this.log('Reloading script: ' + script.src); 143 | var clone = script.cloneNode(false); 144 | clone.src = this.generateNextUrl(script.src); 145 | script.parentNode.replaceChild(clone, script); 146 | }, 147 | 148 | /** 149 | * 150 | * @param {CSSStyleSheet} stylesheet 151 | */ 152 | reloadStylesheet: function(stylesheet) { 153 | if (stylesheet.ownerNode) { 154 | this.reattachStylesheetLink(stylesheet.ownerNode) 155 | } else if (stylesheet.ownerRule) { 156 | this.reattachImportedRule(stylesheet.ownerRule); 157 | } 158 | }, 159 | 160 | reattachStylesheetLink: function(link) { 161 | this.log('Reloading stylesheet: ' + link.href); 162 | 163 | if (link.__pendingRemoval) { 164 | this.warn('Attempt to reload a stylesheet that pending for removal.'); 165 | return 0; 166 | } 167 | link.__pendingRemoval = true; 168 | 169 | var clone = link.cloneNode(false); 170 | clone.href = this.generateNextUrl(link.href); 171 | 172 | var parent = link.parentNode; 173 | if (parent.lastChild == link) { 174 | parent.appendChild(clone); 175 | } else { 176 | parent.insertBefore(clone, link.nextSibling); 177 | } 178 | 179 | if ('sheet' in clone) { 180 | var intervalId = setInterval(function() { 181 | if (clone.sheet) { 182 | removeOld(); 183 | } 184 | }, 20); 185 | } 186 | 187 | var timeoutId = setTimeout(removeOld, 1000); 188 | 189 | function removeOld() { 190 | intervalId && clearInterval(intervalId); 191 | timeoutId && clearTimeout(timeoutId); 192 | link.parentNode && link.parentNode.removeChild(link); 193 | } 194 | 195 | return 1; 196 | }, 197 | 198 | /** 199 | * Recursevly reload all stylesheets that matches nameToReload 200 | * @param {CSSStyleSheet} stylesheet 201 | * @param {string} [nameToReload] reload all stylesheets when omitted 202 | * @return {number} found items count 203 | */ 204 | reloadImportedStylesheet: function(stylesheet, nameToReload) { 205 | 206 | try { 207 | var rules = stylesheet.cssRules; 208 | } catch(error) { 209 | this.warn(error.message); 210 | } 211 | if (!rules) { 212 | this.warn('Cannot access ' + stylesheet.href); 213 | this.reloadStylesheet(stylesheet); 214 | return 1; 215 | } 216 | 217 | var found = 0; 218 | for (var i = 0; i < rules.length; i++) { 219 | var rule = rules[i]; 220 | switch (rule.type) { 221 | case CSSRule.CHARSET_RULE: 222 | // Only charset rules can precede import rules 223 | continue; 224 | case CSSRule.IMPORT_RULE: 225 | var href = rule.href; 226 | if (!nameToReload || this.equals(href, nameToReload)) { 227 | this.reattachImportedRule(rule, i); 228 | found = 1; 229 | } else { 230 | found = this.reloadImportedStylesheet(rule.styleSheet, nameToReload) || found; 231 | } 232 | break; 233 | default: 234 | return found; 235 | } 236 | } 237 | return found; 238 | }, 239 | 240 | reattachImportedRule: function(rule, index) { 241 | var parent = rule.parentStyleSheet; 242 | if (index === undefined) { 243 | if (rule.parentRule) { 244 | throw '@import inside CSS rule? Impossible!'; 245 | } 246 | index = [].indexOf.call(parent.cssRules, rule); 247 | } 248 | var href = rule.href; 249 | this.log('Reloading imported stylesheet: ' + href); 250 | var media = rule.media.length ? [].join.call(rule.media, ', ') : ''; 251 | parent.insertRule('@import url("' + this.generateNextUrl(href) + '") ' + media + ';', index); 252 | parent.deleteRule(index + 1); 253 | }, 254 | 255 | /** 256 | * Recursevly reload all background-image and border-image properties 257 | * @param {CSSStyleSheet|CSSMediaRule} stylesheet 258 | * @param {string} nameToReload 259 | * @param {string} expando 260 | * @return {Array} reloaded rules 261 | */ 262 | reloadStylesheetImages: function(stylesheet, nameToReload, expando) { 263 | 264 | var result = []; 265 | 266 | var rules = stylesheet.cssRules; 267 | if (!rules) { 268 | this.warn('Can\'t access stylesheet: ' + stylesheet.href); 269 | return false; 270 | } 271 | 272 | for (var i = 0; i < rules.length; i++) { 273 | var rule = rules[i]; 274 | switch (rule.type) { 275 | case CSSRule.IMPORT_RULE: 276 | if (rule.href) { 277 | result.push.apply(result, this.reloadStylesheetImages(rule.styleSheet, nameToReload, expando)); 278 | } 279 | break; 280 | case CSSRule.STYLE_RULE: 281 | var found = false; 282 | if (rule.style.backgroundImage) { 283 | var backgroundImage = this.extractURL(rule.style.backgroundImage); 284 | if (this.equals(backgroundImage, nameToReload)) { 285 | rule.style.backgroundImage = 'url(' + this.generateNextUrl(backgroundImage, expando) + ')'; 286 | found = true; 287 | } 288 | } 289 | if (rule.style.borderImage) { 290 | var borderImage = this.extractURL(rule.style.borderImage); 291 | if (this.equals(borderImage, nameToReload)) { 292 | rule.style.borderImage = 'url(' + this.generateNextUrl(borderImage, expando) + ')'; 293 | found = true; 294 | } 295 | } 296 | if (found) { 297 | result.push(rule); 298 | } 299 | break; 300 | case CSSRule.MEDIA_RULE: 301 | result.push.apply(result, this.reloadStylesheetImages(rule, nameToReload, expando)); 302 | break; 303 | } 304 | } 305 | 306 | return result; 307 | }, 308 | 309 | /** 310 | * extractURL('url(ferrets.jpg)') 311 | * -> 'ferrets.jpg' 312 | * 313 | * @param {string} url 314 | * @nosideeffects 315 | * @return {string} 316 | */ 317 | extractURL: function(url) { 318 | return url.slice(4).slice(0, -1); 319 | }, 320 | 321 | handleJS: function(path, options) { 322 | var reloaded = 0; 323 | 324 | options = options || {}; 325 | if (!options.apply_js_live) { 326 | this.handleDefault(path, options); 327 | return reloaded; 328 | } 329 | 330 | var scripts = [].slice.call(this.document.scripts, 0); 331 | for (var i = 0; i < scripts.length; i++) { 332 | var script = scripts[i]; 333 | if (script.src && this.equals(script.src, path)) { 334 | this.reloadScript(script); 335 | reloaded++; 336 | } 337 | } 338 | 339 | return reloaded; 340 | }, 341 | 342 | handleCSS: function(path, options) { 343 | var reloaded = 0; 344 | 345 | if (options && !options.apply_css_live) { 346 | this.handleDefault(path, options); 347 | return reloaded; 348 | } 349 | 350 | var links = this.document.querySelectorAll('link[rel="stylesheet"]'); 351 | // Clone it to avoid changes. 352 | links = [].slice.call(links, 0); 353 | for (var i = 0; i < links.length; i++) { 354 | var link = links[i]; 355 | if (this.equals(link.href, path)) { 356 | reloaded += this.reattachStylesheetLink(link); 357 | } else if (this.apply_imported_css) { 358 | try { 359 | // cssRules might be null in WebKit 360 | var stylesheet = link.sheet; 361 | var canAccess = !!stylesheet.cssRules; 362 | } catch (error) { 363 | // Firefox and Opera might throw an error 364 | this.warn(error.message); 365 | } 366 | if (canAccess && this.apply_imported_css >= 2) { 367 | reloaded += this.reloadImportedStylesheet(stylesheet, (this.apply_imported_css >= 3 ? '' : path)); 368 | } else { 369 | reloaded += this.reattachStylesheetLink(link); 370 | } 371 | } 372 | } 373 | 374 | if (this.apply_imported_css) { 375 | var styles = this.document.getElementsByTagName('style'); 376 | for (var j = 0; j < styles.length; j++) { 377 | var sheet = styles[j].sheet; 378 | if (!sheet) { 379 | continue; 380 | } 381 | reloaded += this.reloadImportedStylesheet(sheet, path); 382 | } 383 | } 384 | 385 | if (!reloaded) { 386 | if (this.strictMode) { 387 | this.log('LiveReload: "' + path + '" does not correspond to any stylesheet. Do nothing.'); 388 | } else { 389 | this.log('LiveReload: "' + path + '" does not correspond to any stylesheet. Reloading all of them.'); 390 | for (i = 0; i < links.length; i++) { 391 | reloaded += this.reattachStylesheetLink(links[i]); 392 | } 393 | if (this.apply_imported_css) { 394 | for (j = 0; j < styles.length; j++) { 395 | reloaded += this.reloadImportedStylesheet(styles[i]); 396 | } 397 | } 398 | } 399 | } 400 | 401 | return reloaded; 402 | }, 403 | 404 | handleImages: function(path, options) { 405 | var reloaded = 0; 406 | 407 | if (options && !options.apply_images_live) { 408 | this.handleDefault(path); 409 | return reloaded; 410 | } 411 | 412 | var stylesheets = this.document.styleSheets; 413 | var imgs = this.document.images; 414 | var expando = this.generateExpando(); 415 | for (var i = 0; i < imgs.length; i++) { 416 | var img = imgs[i]; 417 | if (this.equals(img.src, path)) { 418 | img.src = this.generateNextUrl(img.src, expando); 419 | reloaded++; 420 | } 421 | } 422 | var src; 423 | imgs = this.document.querySelectorAll('[style*=background]'); 424 | for (i = 0; i < imgs.length; i++) { 425 | img = imgs[i]; 426 | if (!img.style.backgroundImage) { 427 | continue; 428 | } 429 | src = this.extractURL(img.style.backgroundImage); 430 | if (src && this.equals(src, path)) { 431 | img.style.backgroundImage = 'url(' + this.generateNextUrl(src, expando) + ')'; 432 | reloaded++; 433 | } 434 | } 435 | 436 | imgs = this.document.querySelectorAll('[style*=border]'); 437 | for (i = 0; i < imgs.length; i++) { 438 | img = imgs[i]; 439 | if (!img.style.borderImage) { 440 | continue; 441 | } 442 | src = this.extractURL(img.style.borderImage); 443 | if (src && this.equals(src, path)) { 444 | img.style.borderImage = 'url(' + this.generateNextUrl(src, expando) + ')'; 445 | reloaded++; 446 | } 447 | } 448 | 449 | for (i = 0; i < stylesheets.length; i++) { 450 | reloaded += this.reloadStylesheetImages(stylesheets[i], path, expando).length; 451 | } 452 | 453 | return reloaded; 454 | }, 455 | 456 | handleHTML: function(path) { 457 | if (this.document.location.protocol == 'file:') { 458 | if (this.equals(document.location.href, path)) { 459 | this.document.location.reload(); 460 | } else { 461 | this.log(path + ' does not match any file. Do nothing.'); 462 | } 463 | } else { 464 | this.document.location.reload(); 465 | } 466 | }, 467 | 468 | handleDefault: function(path) { 469 | this.document.location.reload(); 470 | }, 471 | 472 | /** 473 | * @param {Object} options 474 | * @nosideeffects 475 | * @return {Object} 476 | */ 477 | mergeConfig: function(options) { 478 | if (!this.strictMode) { 479 | options.path = this.fileName(options.path); 480 | } 481 | if (options.apply_js_live === undefined) { 482 | options.apply_js_live = this.apply_js_live; 483 | } 484 | if (options.apply_css_live === undefined) { 485 | options.apply_css_live = this.apply_css_live; 486 | } 487 | if (options.apply_images_live === undefined) { 488 | options.apply_images_live = this.apply_images_live; 489 | } 490 | return options; 491 | }, 492 | 493 | /** 494 | * @param {string} data is a JSON such as '["refresh", {"path": "/tmp/index.html"}' 495 | */ 496 | reload: function(data) { 497 | var parsed = JSON.parse(data); 498 | if (parsed[0] != 'refresh') { 499 | throw 'Unknown command: ' + parsed[0]; 500 | } 501 | var options = this.mergeConfig(parsed[1]); 502 | var path = options.path || ''; 503 | var extension = this.fileExtension(path); 504 | 505 | if (typeof this[extension] == 'function') { 506 | this[extension](path, options); 507 | } else { 508 | this.handleDefault(path, options); 509 | } 510 | }, 511 | 512 | constructor: LivereloadContent 513 | }; 514 | 515 | LivereloadContent.prototype['.js'] = LivereloadContent.prototype.handleJS; 516 | LivereloadContent.prototype['.css'] = LivereloadContent.prototype.handleCSS; 517 | LivereloadContent.prototype['.sass'] = LivereloadContent.prototype.handleCSS; 518 | LivereloadContent.prototype['.scss'] = LivereloadContent.prototype.handleCSS; 519 | LivereloadContent.prototype['.jpg'] = 520 | LivereloadContent.prototype['.jpeg'] = 521 | LivereloadContent.prototype['.png'] = 522 | LivereloadContent.prototype['.gif'] = LivereloadContent.prototype.handleImages; 523 | LivereloadContent.prototype['.html'] = 524 | LivereloadContent.prototype['.htm'] = 525 | LivereloadContent.prototype['.xhtml'] = 526 | LivereloadContent.prototype['.xml'] = LivereloadContent.prototype.handleHTML; 527 | 528 | -------------------------------------------------------------------------------- /src/safari/global.js: -------------------------------------------------------------------------------- 1 | function SafariLivereloadGlobal() {} 2 | SafariLivereloadGlobal.prototype = new LivereloadBackground(function reloadTab(tab, data) { 3 | tab.page.dispatchMessage('LiveReload', data); 4 | }); 5 | 6 | 7 | SafariLivereloadGlobal.prototype.connect = function () { 8 | if (safari.extension.settings.host) this.host = safari.extension.settings.host; 9 | else this.host = this.__proto__.__proto__.host; 10 | 11 | if (safari.extension.settings.port) this.port = safari.extension.settings.port; 12 | else this.port = this.__proto__.__proto__.port; 13 | 14 | this.__proto__.__proto__.connect.call(this); 15 | }; 16 | 17 | SafariLivereloadGlobal.prototype.sendPageUrl = function() { 18 | var activePage = this.lastPage; 19 | if (activePage == null) { 20 | throw 'No active page'; 21 | } 22 | this.socket && this.socket.send(activePage.url); 23 | }; 24 | 25 | SafariLivereloadGlobal.prototype.alert = function(message) { 26 | console.error(message); 27 | }; 28 | 29 | // http://stackoverflow.com/questions/4587500/catching-close-tab-event-in-a-safari-extension 30 | SafariLivereloadGlobal.prototype.killZombies = function() { 31 | for (var i = this.pages.length; i--;) { 32 | if (!this.pages[i].url) { 33 | this.pages.splice(i, 1); 34 | } 35 | } 36 | }; 37 | 38 | SafariLivereloadGlobal.prototype.togglePage = function(tab) { 39 | this.killZombies(); 40 | this.__proto__.__proto__.togglePage.call(this, tab); 41 | }; 42 | 43 | SafariLivereloadGlobal.prototype._onmessage = function(event) { 44 | this.killZombies(); 45 | this.__proto__.__proto__._onmessage.call(this, event); 46 | }; 47 | 48 | SafariLivereloadGlobal.prototype.constructor = SafariLivereloadGlobal; 49 | 50 | 51 | var livereload = new SafariLivereloadGlobal; 52 | 53 | safari.application.addEventListener('command', function(event) { 54 | if (event.command == 'enable') { 55 | var tab = safari.application.activeBrowserWindow.activeTab; 56 | livereload.togglePage(tab); 57 | } 58 | }, false); 59 | 60 | safari.application.addEventListener('validate', function(event) { 61 | var tab = safari.application.activeBrowserWindow.activeTab; 62 | if (livereload.pages.indexOf(tab) == -1) { 63 | event.target.title = 'Enable LiveReload'; 64 | } else { 65 | event.target.title = 'Disable LiveReload'; 66 | } 67 | }, false); 68 | 69 | safari.extension.settings.addEventListener("change", function(event) { 70 | livereload.disconnect(); 71 | }, false); 72 | -------------------------------------------------------------------------------- /src/safari/injected.js: -------------------------------------------------------------------------------- 1 | var livereload = new LivereloadContent(document); 2 | safari.self.addEventListener('message', function(event) { 3 | if (event.name == 'LiveReload') { 4 | livereload.reload(event.message); 5 | } 6 | }, false); 7 | -------------------------------------------------------------------------------- /src/xbrowser/livereload.js: -------------------------------------------------------------------------------- 1 | var livereload = {}; 2 | 3 | livereload.client = new LivereloadContent(document); 4 | 5 | livereload.background = new LivereloadBackground(function reloadDocument(doc, data) { 6 | livereload.client.reload(data); 7 | }); 8 | 9 | livereload.run = function() { 10 | livereload.background.enablePage(document); 11 | }; 12 | 13 | livereload.run(); 14 | -------------------------------------------------------------------------------- /test/content.equals.js: -------------------------------------------------------------------------------- 1 | module('Content'); 2 | 3 | function LR_equals(url, path) { 4 | var lr = new LivereloadContent(document); 5 | QUnit.push(lr.equals(url, path), url, path, 'Equal paths'); 6 | } 7 | 8 | function LR_notEquals(url, path) { 9 | var lr = new LivereloadContent(document); 10 | QUnit.push(!lr.equals(url, path), url, path, 'Not equal paths'); 11 | } 12 | 13 | test('equals', function(){ 14 | LR_equals('file:///tmp/42.html', '/tmp/42.html'); 15 | LR_equals('file://localhost/tmp/42.html', '/tmp/42.html'); 16 | LR_notEquals('file:///tmp/42.htm', '42.htm'); 17 | 18 | LR_equals('http://livereload.local/x+1%5Cy=12%3F.html', 'x+1\\y=12?.html'); 19 | LR_notEquals('http://livereload.local/упячка?.html', 'упячка?.html'); 20 | 21 | LR_equals('http://cssparser.com/docs/parse.html?q=foo&bar=%2B', 'parse.html'); 22 | LR_equals('http://cssparser.com/docs/parse.html#%D1%8B', 'parse.html'); 23 | LR_equals('http://cssparser.com/docs/parse.html?q=foo&bar=%2B#%D1%8B', 'parse.html'); 24 | }); 25 | -------------------------------------------------------------------------------- /test/content.fileExtension.js: -------------------------------------------------------------------------------- 1 | module('Content'); 2 | 3 | test('fileExtension', function() { 4 | equal(LivereloadContent.prototype.fileExtension('http://elv1s.ru/main.css'), '.css'); 5 | equal(LivereloadContent.prototype.fileExtension('file:///Users/nv/Code/livereload/test/index.html'), '.html'); 6 | equal(LivereloadContent.prototype.fileExtension('/tmp/livereload/.livereload'), '.livereload'); 7 | equal(LivereloadContent.prototype.fileExtension('foo\\bar/Guardfile'), ''); 8 | }); 9 | -------------------------------------------------------------------------------- /test/content.fileName.js: -------------------------------------------------------------------------------- 1 | module('Content'); 2 | 3 | test('fileName', function() { 4 | equal(LivereloadContent.prototype.fileName('http://elv1s.ru/index.html'), 'index.html'); 5 | equal(LivereloadContent.prototype.fileName('/tmp/livereload/.livereload'), '.livereload'); 6 | equal(LivereloadContent.prototype.fileName('~/Documents/3\\4.html'), '3\\4.html'); 7 | }); 8 | -------------------------------------------------------------------------------- /test/content.generateNextUrl.js: -------------------------------------------------------------------------------- 1 | module('Content'); 2 | 3 | /** 4 | * Return substring after the first occurrence of _str_. 5 | * @param {string} str 6 | * @see https://github.com/visionmedia/ext.js/blob/master/lib/ext/core_ext/string/extensions.js 7 | * @return {string} 8 | */ 9 | String.prototype.after = function(str) { 10 | var i = this.indexOf(str); 11 | return i === -1 ? '' : this.substring(i + str.length); 12 | }; 13 | 14 | /** 15 | * Return substring before the first occurrence of _str_. 16 | * @param {string} str 17 | * @return {string} 18 | */ 19 | String.prototype.before = function(str) { 20 | var i = this.indexOf(str); 21 | return i === -1 ? '' : this.substring(0, i); 22 | }; 23 | 24 | 25 | test('generateNextUrl', function() { 26 | var url = LivereloadContent.prototype.generateNextUrl('../icons/browsers.svg?size=100x100#chrome'); 27 | 28 | equal(url.after('#'), 'chrome'); 29 | 30 | var param = url.before('#').after('?'); 31 | ok(/size=100x100&livereload=\d+/.test(param), 'new livereload param must be appended'); 32 | 33 | equal(url.before('?'), '../icons/browsers.svg'); 34 | 35 | var styleUrl = 'http://example.com/style.css?livereload=1293305882505'; 36 | url = LivereloadContent.prototype.generateNextUrl(styleUrl); 37 | ok(url != styleUrl, 'livereload param must be updated'); 38 | ok(/http[:][/][/]example[.]com[/]style[.]css[?]livereload=\d+/.test(url)); 39 | 40 | }); 41 | -------------------------------------------------------------------------------- /test/content.handleCSS.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module('Content'); 4 | 5 | function setupFixture(path, onReady) { 6 | var iframe = document.createElement('iframe'); 7 | iframe.src = path; 8 | iframe.id = path; 9 | 10 | var iframe_fixtures = document.getElementById('iframe_fixtures'); 11 | 12 | iframe.addEventListener('load', function loaded(e) { 13 | this.removeEventListener('load', loaded, false); 14 | var doc = this.contentDocument; 15 | try { 16 | var canAccess = !!doc.location; 17 | } catch (error) {} 18 | if (canAccess) { 19 | onReady(this.contentDocument); 20 | } else { 21 | ok(false, 'Cannot access an iframe. ' + (navigator.userAgent.indexOf('Chrome') > -1 ? 'Run Google Chrome with --allow-file-access-from-files to fix it.' : '')); 22 | start(); 23 | } 24 | }, false); 25 | 26 | iframe_fixtures.appendChild(iframe); 27 | 28 | return iframe; 29 | } 30 | 31 | LivereloadContent.prototype.log = 32 | LivereloadContent.prototype.warn = function log(msg) { 33 | var p = this.document.createElement('p'); 34 | p.appendChild(this.document.createTextNode(msg)); 35 | this.document.body.appendChild(p); 36 | }; 37 | 38 | asyncTest('handleCSS: link', function(){ 39 | var iframe = setupFixture('fixtures/simple.html', function(document) { 40 | var lr = new LivereloadContent(document); 41 | if (lr.strictMode) { 42 | equal(lr.handleCSS('colors.css'), 0, "Names match, but paths don't"); 43 | equal(lr.handleCSS('typography.css'), 0, "Names match, but paths don't"); 44 | equal(lr.handleCSS('doesnt_exist.css'), 0, "Doesn't match, do nothing"); 45 | } else { 46 | equal(lr.handleCSS('colors.css'), 1, 'Reload only matched colors.css'); 47 | equal(lr.handleCSS('typography.css'), 1, 'Reload only matched typography.css'); 48 | equal(lr.handleCSS('doesnt_exist.css'), 2, 'Reload both stylesheets'); 49 | } 50 | start(); 51 | }); 52 | }); 53 | 54 | asyncTest('handleCSS: link: Same file-name, different paths', function(){ 55 | setupFixture('fixtures/widgets.html', function(document){ 56 | var lr = new LivereloadContent(document); 57 | if (lr.strictMode) { 58 | var path = location.pathname.slice(0, -(location.hash.length + location.search.length)); 59 | var dir = location.pathname.match(/(.+\/).*$/)[1]; 60 | equal(lr.handleCSS(dir + 'fixtures/colors.css'), 1); 61 | } else { 62 | equal(lr.handleCSS('colors.css'), 3); 63 | } 64 | start(); 65 | }); 66 | }); 67 | -------------------------------------------------------------------------------- /test/fixtures/colors.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: #EEE; 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/simple.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |

typography.css, colors.css

10 | 11 | 12 | -------------------------------------------------------------------------------- /test/fixtures/typography.css: -------------------------------------------------------------------------------- 1 | html { 2 | font: 12px sans-serif; 3 | } 4 | 5 | h1 { 6 | font: 150% futura, sans-serif; 7 | margin: 0 0 .3em; 8 | padding: 0; 9 | } 10 | p { 11 | margin: 0 0 .4em; 12 | } 13 | -------------------------------------------------------------------------------- /test/fixtures/widget-a/colors.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: burlyWood; 3 | } 4 | .widget-a { 5 | color: #465b24; 6 | } 7 | -------------------------------------------------------------------------------- /test/fixtures/widget-b/colors.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: beige; 3 | } 4 | .widget-b { 5 | color: #465b24; 6 | } 7 | -------------------------------------------------------------------------------- /test/fixtures/widgets.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 14 | 15 | 16 |

17 | typography.css, 18 | colors.css, 19 | widget-a/colors.css, 20 | widget-b/colors.css 21 |

22 | 23 | 24 | -------------------------------------------------------------------------------- /test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | LiveReload tests 5 | 6 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 |

LiveReload tests

23 |

24 |
    25 |

    26 |
    27 |

    28 | 29 | 30 |

    31 |
    32 |
    33 | 34 | 35 | -------------------------------------------------------------------------------- /test/qunit/qunit.css: -------------------------------------------------------------------------------- 1 | /** Font Family and Sizes */ 2 | 3 | #qunit-wrapper { 4 | font-family: "Helvetica Neue", Helvetica, sans-serif; 5 | } 6 | 7 | #qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult, #qunit-tests li { font-size: small; } 8 | #qunit-tests { font-size: smaller; } 9 | 10 | 11 | /** Resets */ 12 | 13 | #qunit-wrapper, #qunit-tests, #qunit-tests ol, #qunit-header, #qunit-banner, #qunit-userAgent, #qunit-testresult { 14 | margin: 0; 15 | padding: 0; 16 | } 17 | 18 | 19 | /** Header */ 20 | 21 | #qunit-header { 22 | font-family: "Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Helvetica, sans-serif; 23 | padding: 0.5em 0 0.5em 1.3em; 24 | 25 | color: #8699a4; 26 | background-color: #0d3349; 27 | 28 | font-size: 1.5em; 29 | line-height: 1em; 30 | font-weight: normal; 31 | } 32 | 33 | #qunit-header a { 34 | text-decoration: none; 35 | color: #c2ccd1; 36 | } 37 | 38 | #qunit-header a:hover, 39 | #qunit-header a:focus { 40 | color: #fff; 41 | } 42 | 43 | #qunit-banner.qunit-pass { 44 | height: 3px; 45 | } 46 | #qunit-banner.qunit-fail { 47 | height: 5px; 48 | } 49 | 50 | #qunit-testrunner-toolbar { 51 | padding: 0 0 0.5em 2em; 52 | } 53 | 54 | #qunit-testrunner-toolbar label { 55 | margin-right: 1em; 56 | } 57 | 58 | #qunit-userAgent { 59 | padding: 0.5em 0 0.5em 2.5em; 60 | font-weight: normal; 61 | color: #666; 62 | } 63 | 64 | 65 | /** Tests: Pass/Fail */ 66 | 67 | #qunit-tests { 68 | list-style-type: none; 69 | background-color: #D2E0E6; 70 | } 71 | 72 | #qunit-tests li { 73 | padding: 0.4em 0.5em 0.4em 2.5em; 74 | } 75 | 76 | #qunit-tests li strong { 77 | font-weight: normal; 78 | cursor: pointer; 79 | } 80 | 81 | #qunit-tests ol { 82 | margin: 0.5em 0 1em; 83 | background-color: #fff; 84 | } 85 | 86 | #qunit-tests table { 87 | border-collapse: collapse; 88 | margin-top: .2em; 89 | } 90 | 91 | #qunit-tests th { 92 | text-align: right; 93 | vertical-align: top; 94 | padding: 0 .5em 0 0; 95 | } 96 | 97 | #qunit-tests td { 98 | vertical-align: top; 99 | } 100 | 101 | #qunit-tests pre { 102 | margin: 0; 103 | white-space: pre-wrap; 104 | word-wrap: break-word; 105 | } 106 | 107 | #qunit-tests del { 108 | background-color: #e0f2be; 109 | color: #374e0c; 110 | text-decoration: none; 111 | } 112 | 113 | #qunit-tests ins { 114 | background-color: #ffcaca; 115 | color: #500; 116 | text-decoration: none; 117 | } 118 | 119 | /*** Test Counts */ 120 | 121 | #qunit-tests b.passed { color: #5E740B; } 122 | #qunit-tests b.failed { 123 | color: #710909; 124 | } 125 | #qunit-tests li.fail .failed { 126 | color: #E48989; 127 | } 128 | #qunit-tests li.fail .passed { 129 | color: #E3C987; 130 | } 131 | 132 | #qunit-tests li li { 133 | margin-left: 2.5em; 134 | padding: 0.7em 0.5em 0.7em 0; 135 | background-color: #fff; 136 | border-bottom: none; 137 | } 138 | 139 | #qunit-tests b.counts { 140 | font-weight: normal; 141 | } 142 | 143 | /*** Passing Styles */ 144 | 145 | #qunit-tests li li.pass { 146 | color: #5E740B; 147 | background-color: #fff; 148 | } 149 | 150 | #qunit-tests .pass { color: #2f3424; background-color: #d9dec3; } 151 | #qunit-tests .pass .module-name { color: #636b51; } 152 | 153 | #qunit-tests .pass .test-actual, 154 | #qunit-tests .pass .test-expected { color: #999999; } 155 | 156 | #qunit-banner.qunit-pass { background-color: #C6E746; } 157 | 158 | /*** Failing Styles */ 159 | 160 | #qunit-tests li li.fail { 161 | color: #710909; 162 | background-color: #fff; 163 | } 164 | 165 | #qunit-tests .fail { color: #fff; background-color: #962323; } 166 | #qunit-tests .fail .module-name, 167 | #qunit-tests .fail .counts { color: #DEC1C1; } 168 | 169 | #qunit-tests .fail .test-actual { color: #B72F2F; } 170 | #qunit-tests .fail .test-expected { color: green; } 171 | 172 | #qunit-banner.qunit-fail, 173 | #qunit-testrunner-toolbar { color: #dec1c1; background-color: #962323; } 174 | 175 | 176 | /** Footer */ 177 | 178 | #qunit-testresult { 179 | padding: 0.5em 0.5em 0.5em 2.5em; 180 | color: #333; 181 | } 182 | 183 | /** Fixture */ 184 | 185 | #qunit-fixture { 186 | position: absolute; 187 | top: -10000px; 188 | left: -10000px; 189 | } 190 | -------------------------------------------------------------------------------- /test/qunit/qunit.js: -------------------------------------------------------------------------------- 1 | /* 2 | * QUnit - A JavaScript Unit Testing Framework 3 | * 4 | * http://docs.jquery.com/QUnit 5 | * 6 | * Copyright (c) 2011 John Resig, Jörn Zaefferer 7 | * Dual licensed under the MIT (MIT-LICENSE.txt) 8 | * or GPL (GPL-LICENSE.txt) licenses. 9 | */ 10 | 11 | (function(window) { 12 | 13 | var defined = { 14 | setTimeout: typeof window.setTimeout !== "undefined", 15 | sessionStorage: (function() { 16 | try { 17 | return !!sessionStorage.getItem; 18 | } catch(e){ 19 | return false; 20 | } 21 | })() 22 | } 23 | 24 | var testId = 0; 25 | 26 | var Test = function(name, testName, expected, testEnvironmentArg, async, callback) { 27 | this.name = name; 28 | this.testName = testName; 29 | this.expected = expected; 30 | this.testEnvironmentArg = testEnvironmentArg; 31 | this.async = async; 32 | this.callback = callback; 33 | this.assertions = []; 34 | }; 35 | Test.prototype = { 36 | init: function() { 37 | var tests = id("qunit-tests"); 38 | if (tests) { 39 | var b = document.createElement("strong"); 40 | b.innerHTML = "Running " + this.name; 41 | var li = document.createElement("li"); 42 | li.appendChild( b ); 43 | li.id = this.id = "test-output" + testId++; 44 | tests.appendChild( li ); 45 | } 46 | }, 47 | setup: function() { 48 | if (this.module != config.previousModule) { 49 | if ( config.previousModule ) { 50 | QUnit.moduleDone( { 51 | name: config.previousModule, 52 | failed: config.moduleStats.bad, 53 | passed: config.moduleStats.all - config.moduleStats.bad, 54 | total: config.moduleStats.all 55 | } ); 56 | } 57 | config.previousModule = this.module; 58 | config.moduleStats = { all: 0, bad: 0 }; 59 | QUnit.moduleStart( { 60 | name: this.module 61 | } ); 62 | } 63 | 64 | config.current = this; 65 | this.testEnvironment = extend({ 66 | setup: function() {}, 67 | teardown: function() {} 68 | }, this.moduleTestEnvironment); 69 | if (this.testEnvironmentArg) { 70 | extend(this.testEnvironment, this.testEnvironmentArg); 71 | } 72 | 73 | QUnit.testStart( { 74 | name: this.testName 75 | } ); 76 | 77 | // allow utility functions to access the current test environment 78 | // TODO why?? 79 | QUnit.current_testEnvironment = this.testEnvironment; 80 | 81 | try { 82 | if ( !config.pollution ) { 83 | saveGlobal(); 84 | } 85 | 86 | this.testEnvironment.setup.call(this.testEnvironment); 87 | } catch(e) { 88 | QUnit.ok( false, "Setup failed on " + this.testName + ": " + e.message ); 89 | } 90 | }, 91 | run: function() { 92 | if ( this.async ) { 93 | QUnit.stop(); 94 | } 95 | 96 | if ( config.notrycatch ) { 97 | this.callback.call(this.testEnvironment); 98 | return; 99 | } 100 | try { 101 | this.callback.call(this.testEnvironment); 102 | } catch(e) { 103 | fail("Test " + this.testName + " died, exception and test follows", e, this.callback); 104 | QUnit.ok( false, "Died on test #" + (this.assertions.length + 1) + ": " + e.message + " - " + QUnit.jsDump.parse(e) ); 105 | // else next test will carry the responsibility 106 | saveGlobal(); 107 | 108 | // Restart the tests if they're blocking 109 | if ( config.blocking ) { 110 | start(); 111 | } 112 | } 113 | }, 114 | teardown: function() { 115 | try { 116 | checkPollution(); 117 | this.testEnvironment.teardown.call(this.testEnvironment); 118 | } catch(e) { 119 | QUnit.ok( false, "Teardown failed on " + this.testName + ": " + e.message ); 120 | } 121 | }, 122 | finish: function() { 123 | if ( this.expected && this.expected != this.assertions.length ) { 124 | QUnit.ok( false, "Expected " + this.expected + " assertions, but " + this.assertions.length + " were run" ); 125 | } 126 | 127 | var good = 0, bad = 0, 128 | tests = id("qunit-tests"); 129 | 130 | config.stats.all += this.assertions.length; 131 | config.moduleStats.all += this.assertions.length; 132 | 133 | if ( tests ) { 134 | var ol = document.createElement("ol"); 135 | 136 | for ( var i = 0; i < this.assertions.length; i++ ) { 137 | var assertion = this.assertions[i]; 138 | 139 | var li = document.createElement("li"); 140 | li.className = assertion.result ? "pass" : "fail"; 141 | li.innerHTML = assertion.message || (assertion.result ? "okay" : "failed"); 142 | ol.appendChild( li ); 143 | 144 | if ( assertion.result ) { 145 | good++; 146 | } else { 147 | bad++; 148 | config.stats.bad++; 149 | config.moduleStats.bad++; 150 | } 151 | } 152 | 153 | // store result when possible 154 | defined.sessionStorage && sessionStorage.setItem("qunit-" + this.testName, bad); 155 | 156 | if (bad == 0) { 157 | ol.style.display = "none"; 158 | } 159 | 160 | var b = document.createElement("strong"); 161 | b.innerHTML = this.name + " (" + bad + ", " + good + ", " + this.assertions.length + ")"; 162 | 163 | addEvent(b, "click", function() { 164 | var next = b.nextSibling, display = next.style.display; 165 | next.style.display = display === "none" ? "block" : "none"; 166 | }); 167 | 168 | addEvent(b, "dblclick", function(e) { 169 | var target = e && e.target ? e.target : window.event.srcElement; 170 | if ( target.nodeName.toLowerCase() == "span" || target.nodeName.toLowerCase() == "b" ) { 171 | target = target.parentNode; 172 | } 173 | if ( window.location && target.nodeName.toLowerCase() === "strong" ) { 174 | window.location.search = "?" + encodeURIComponent(getText([target]).replace(/\(.+\)$/, "").replace(/(^\s*|\s*$)/g, "")); 175 | } 176 | }); 177 | 178 | var li = id(this.id); 179 | li.className = bad ? "fail" : "pass"; 180 | li.style.display = resultDisplayStyle(!bad); 181 | li.removeChild( li.firstChild ); 182 | li.appendChild( b ); 183 | li.appendChild( ol ); 184 | 185 | } else { 186 | for ( var i = 0; i < this.assertions.length; i++ ) { 187 | if ( !this.assertions[i].result ) { 188 | bad++; 189 | config.stats.bad++; 190 | config.moduleStats.bad++; 191 | } 192 | } 193 | } 194 | 195 | try { 196 | QUnit.reset(); 197 | } catch(e) { 198 | fail("reset() failed, following Test " + this.testName + ", exception and reset fn follows", e, QUnit.reset); 199 | } 200 | 201 | QUnit.testDone( { 202 | name: this.testName, 203 | failed: bad, 204 | passed: this.assertions.length - bad, 205 | total: this.assertions.length 206 | } ); 207 | }, 208 | 209 | queue: function() { 210 | var test = this; 211 | synchronize(function() { 212 | test.init(); 213 | }); 214 | function run() { 215 | // each of these can by async 216 | synchronize(function() { 217 | test.setup(); 218 | }); 219 | synchronize(function() { 220 | test.run(); 221 | }); 222 | synchronize(function() { 223 | test.teardown(); 224 | }); 225 | synchronize(function() { 226 | test.finish(); 227 | }); 228 | } 229 | // defer when previous test run passed, if storage is available 230 | var bad = defined.sessionStorage && +sessionStorage.getItem("qunit-" + this.testName); 231 | if (bad) { 232 | run(); 233 | } else { 234 | synchronize(run); 235 | }; 236 | } 237 | 238 | } 239 | 240 | var QUnit = { 241 | 242 | // call on start of module test to prepend name to all tests 243 | module: function(name, testEnvironment) { 244 | config.currentModule = name; 245 | config.currentModuleTestEnviroment = testEnvironment; 246 | }, 247 | 248 | asyncTest: function(testName, expected, callback) { 249 | if ( arguments.length === 2 ) { 250 | callback = expected; 251 | expected = 0; 252 | } 253 | 254 | QUnit.test(testName, expected, callback, true); 255 | }, 256 | 257 | test: function(testName, expected, callback, async) { 258 | var name = '' + testName + '', testEnvironmentArg; 259 | 260 | if ( arguments.length === 2 ) { 261 | callback = expected; 262 | expected = null; 263 | } 264 | // is 2nd argument a testEnvironment? 265 | if ( expected && typeof expected === 'object') { 266 | testEnvironmentArg = expected; 267 | expected = null; 268 | } 269 | 270 | if ( config.currentModule ) { 271 | name = '' + config.currentModule + ": " + name; 272 | } 273 | 274 | if ( !validTest(config.currentModule + ": " + testName) ) { 275 | return; 276 | } 277 | 278 | var test = new Test(name, testName, expected, testEnvironmentArg, async, callback); 279 | test.module = config.currentModule; 280 | test.moduleTestEnvironment = config.currentModuleTestEnviroment; 281 | test.queue(); 282 | }, 283 | 284 | /** 285 | * Specify the number of expected assertions to gurantee that failed test (no assertions are run at all) don't slip through. 286 | */ 287 | expect: function(asserts) { 288 | config.current.expected = asserts; 289 | }, 290 | 291 | /** 292 | * Asserts true. 293 | * @example ok( "asdfasdf".length > 5, "There must be at least 5 chars" ); 294 | */ 295 | ok: function(a, msg) { 296 | a = !!a; 297 | var details = { 298 | result: a, 299 | message: msg 300 | }; 301 | msg = escapeHtml(msg); 302 | QUnit.log(details); 303 | config.current.assertions.push({ 304 | result: a, 305 | message: msg 306 | }); 307 | }, 308 | 309 | /** 310 | * Checks that the first two arguments are equal, with an optional message. 311 | * Prints out both actual and expected values. 312 | * 313 | * Prefered to ok( actual == expected, message ) 314 | * 315 | * @example equal( format("Received {0} bytes.", 2), "Received 2 bytes." ); 316 | * 317 | * @param Object actual 318 | * @param Object expected 319 | * @param String message (optional) 320 | */ 321 | equal: function(actual, expected, message) { 322 | QUnit.push(expected == actual, actual, expected, message); 323 | }, 324 | 325 | notEqual: function(actual, expected, message) { 326 | QUnit.push(expected != actual, actual, expected, message); 327 | }, 328 | 329 | deepEqual: function(actual, expected, message) { 330 | QUnit.push(QUnit.equiv(actual, expected), actual, expected, message); 331 | }, 332 | 333 | notDeepEqual: function(actual, expected, message) { 334 | QUnit.push(!QUnit.equiv(actual, expected), actual, expected, message); 335 | }, 336 | 337 | strictEqual: function(actual, expected, message) { 338 | QUnit.push(expected === actual, actual, expected, message); 339 | }, 340 | 341 | notStrictEqual: function(actual, expected, message) { 342 | QUnit.push(expected !== actual, actual, expected, message); 343 | }, 344 | 345 | raises: function(block, expected, message) { 346 | var actual, ok = false; 347 | 348 | if (typeof expected === 'string') { 349 | message = expected; 350 | expected = null; 351 | } 352 | 353 | try { 354 | block(); 355 | } catch (e) { 356 | actual = e; 357 | } 358 | 359 | if (actual) { 360 | // we don't want to validate thrown error 361 | if (!expected) { 362 | ok = true; 363 | // expected is a regexp 364 | } else if (QUnit.objectType(expected) === "regexp") { 365 | ok = expected.test(actual); 366 | // expected is a constructor 367 | } else if (actual instanceof expected) { 368 | ok = true; 369 | // expected is a validation function which returns true is validation passed 370 | } else if (expected.call({}, actual) === true) { 371 | ok = true; 372 | } 373 | } 374 | 375 | QUnit.ok(ok, message); 376 | }, 377 | 378 | start: function() { 379 | config.semaphore--; 380 | if (config.semaphore > 0) { 381 | // don't start until equal number of stop-calls 382 | return; 383 | } 384 | if (config.semaphore < 0) { 385 | // ignore if start is called more often then stop 386 | config.semaphore = 0; 387 | } 388 | // A slight delay, to avoid any current callbacks 389 | if ( defined.setTimeout ) { 390 | window.setTimeout(function() { 391 | if ( config.timeout ) { 392 | clearTimeout(config.timeout); 393 | } 394 | 395 | config.blocking = false; 396 | process(); 397 | }, 13); 398 | } else { 399 | config.blocking = false; 400 | process(); 401 | } 402 | }, 403 | 404 | stop: function(timeout) { 405 | config.semaphore++; 406 | config.blocking = true; 407 | 408 | if ( timeout && defined.setTimeout ) { 409 | clearTimeout(config.timeout); 410 | config.timeout = window.setTimeout(function() { 411 | QUnit.ok( false, "Test timed out" ); 412 | QUnit.start(); 413 | }, timeout); 414 | } 415 | } 416 | 417 | }; 418 | 419 | // Backwards compatibility, deprecated 420 | QUnit.equals = QUnit.equal; 421 | QUnit.same = QUnit.deepEqual; 422 | 423 | // Maintain internal state 424 | var config = { 425 | // The queue of tests to run 426 | queue: [], 427 | 428 | // block until document ready 429 | blocking: true 430 | }; 431 | 432 | // Load paramaters 433 | (function() { 434 | var location = window.location || { search: "", protocol: "file:" }, 435 | GETParams = location.search.slice(1).split('&'); 436 | 437 | for ( var i = 0; i < GETParams.length; i++ ) { 438 | GETParams[i] = decodeURIComponent( GETParams[i] ); 439 | if ( GETParams[i] === "noglobals" ) { 440 | GETParams.splice( i, 1 ); 441 | i--; 442 | config.noglobals = true; 443 | } else if ( GETParams[i] === "notrycatch" ) { 444 | GETParams.splice( i, 1 ); 445 | i--; 446 | config.notrycatch = true; 447 | } else if ( GETParams[i].search('=') > -1 ) { 448 | GETParams.splice( i, 1 ); 449 | i--; 450 | } 451 | } 452 | 453 | // restrict modules/tests by get parameters 454 | config.filters = GETParams; 455 | 456 | // Figure out if we're running the tests from a server or not 457 | QUnit.isLocal = !!(location.protocol === 'file:'); 458 | })(); 459 | 460 | // Expose the API as global variables, unless an 'exports' 461 | // object exists, in that case we assume we're in CommonJS 462 | if ( typeof exports === "undefined" || typeof require === "undefined" ) { 463 | extend(window, QUnit); 464 | window.QUnit = QUnit; 465 | } else { 466 | extend(exports, QUnit); 467 | exports.QUnit = QUnit; 468 | } 469 | 470 | // define these after exposing globals to keep them in these QUnit namespace only 471 | extend(QUnit, { 472 | config: config, 473 | 474 | // Initialize the configuration options 475 | init: function() { 476 | extend(config, { 477 | stats: { all: 0, bad: 0 }, 478 | moduleStats: { all: 0, bad: 0 }, 479 | started: +new Date, 480 | updateRate: 1000, 481 | blocking: false, 482 | autostart: true, 483 | autorun: false, 484 | filters: [], 485 | queue: [], 486 | semaphore: 0 487 | }); 488 | 489 | var tests = id("qunit-tests"), 490 | banner = id("qunit-banner"), 491 | result = id("qunit-testresult"); 492 | 493 | if ( tests ) { 494 | tests.innerHTML = ""; 495 | } 496 | 497 | if ( banner ) { 498 | banner.className = ""; 499 | } 500 | 501 | if ( result ) { 502 | result.parentNode.removeChild( result ); 503 | } 504 | }, 505 | 506 | /** 507 | * Resets the test setup. Useful for tests that modify the DOM. 508 | * 509 | * If jQuery is available, uses jQuery's html(), otherwise just innerHTML. 510 | */ 511 | reset: function() { 512 | if ( window.jQuery ) { 513 | jQuery( "#main, #qunit-fixture" ).html( config.fixture ); 514 | } else { 515 | var main = id( 'main' ) || id( 'qunit-fixture' ); 516 | if ( main ) { 517 | main.innerHTML = config.fixture; 518 | } 519 | } 520 | }, 521 | 522 | /** 523 | * Trigger an event on an element. 524 | * 525 | * @example triggerEvent( document.body, "click" ); 526 | * 527 | * @param DOMElement elem 528 | * @param String type 529 | */ 530 | triggerEvent: function( elem, type, event ) { 531 | if ( document.createEvent ) { 532 | event = document.createEvent("MouseEvents"); 533 | event.initMouseEvent(type, true, true, elem.ownerDocument.defaultView, 534 | 0, 0, 0, 0, 0, false, false, false, false, 0, null); 535 | elem.dispatchEvent( event ); 536 | 537 | } else if ( elem.fireEvent ) { 538 | elem.fireEvent("on"+type); 539 | } 540 | }, 541 | 542 | // Safe object type checking 543 | is: function( type, obj ) { 544 | return QUnit.objectType( obj ) == type; 545 | }, 546 | 547 | objectType: function( obj ) { 548 | if (typeof obj === "undefined") { 549 | return "undefined"; 550 | 551 | // consider: typeof null === object 552 | } 553 | if (obj === null) { 554 | return "null"; 555 | } 556 | 557 | var type = Object.prototype.toString.call( obj ) 558 | .match(/^\[object\s(.*)\]$/)[1] || ''; 559 | 560 | switch (type) { 561 | case 'Number': 562 | if (isNaN(obj)) { 563 | return "nan"; 564 | } else { 565 | return "number"; 566 | } 567 | case 'String': 568 | case 'Boolean': 569 | case 'Array': 570 | case 'Date': 571 | case 'RegExp': 572 | case 'Function': 573 | return type.toLowerCase(); 574 | } 575 | if (typeof obj === "object") { 576 | return "object"; 577 | } 578 | return undefined; 579 | }, 580 | 581 | push: function(result, actual, expected, message) { 582 | var details = { 583 | result: result, 584 | message: message, 585 | actual: actual, 586 | expected: expected 587 | }; 588 | 589 | message = escapeHtml(message) || (result ? "okay" : "failed"); 590 | message = '' + message + ""; 591 | expected = escapeHtml(QUnit.jsDump.parse(expected)); 592 | actual = escapeHtml(QUnit.jsDump.parse(actual)); 593 | var output = message + ''; 594 | if (actual != expected) { 595 | output += ''; 596 | output += ''; 597 | } 598 | if (!result) { 599 | var source = sourceFromStacktrace(); 600 | if (source) { 601 | details.source = source; 602 | output += ''; 603 | } 604 | } 605 | output += "
    Expected:
    ' + expected + '
    Result:
    ' + actual + '
    Diff:
    ' + QUnit.diff(expected, actual) +'
    Source:
    ' + source +'
    "; 606 | 607 | QUnit.log(details); 608 | 609 | config.current.assertions.push({ 610 | result: !!result, 611 | message: output 612 | }); 613 | }, 614 | 615 | // Logging callbacks; all receive a single argument with the listed properties 616 | // run test/logs.html for any related changes 617 | begin: function() {}, 618 | // done: { failed, passed, total, runtime } 619 | done: function() {}, 620 | // log: { result, actual, expected, message } 621 | log: function() {}, 622 | // testStart: { name } 623 | testStart: function() {}, 624 | // testDone: { name, failed, passed, total } 625 | testDone: function() {}, 626 | // moduleStart: { name } 627 | moduleStart: function() {}, 628 | // moduleDone: { name, failed, passed, total } 629 | moduleDone: function() {} 630 | }); 631 | 632 | if ( typeof document === "undefined" || document.readyState === "complete" ) { 633 | config.autorun = true; 634 | } 635 | 636 | addEvent(window, "load", function() { 637 | QUnit.begin({}); 638 | 639 | // Initialize the config, saving the execution queue 640 | var oldconfig = extend({}, config); 641 | QUnit.init(); 642 | extend(config, oldconfig); 643 | 644 | config.blocking = false; 645 | 646 | var userAgent = id("qunit-userAgent"); 647 | if ( userAgent ) { 648 | userAgent.innerHTML = navigator.userAgent; 649 | } 650 | var banner = id("qunit-header"); 651 | if ( banner ) { 652 | var paramsIndex = location.href.lastIndexOf(location.search); 653 | if ( paramsIndex > -1 ) { 654 | var mainPageLocation = location.href.slice(0, paramsIndex); 655 | if ( mainPageLocation == location.href ) { 656 | banner.innerHTML = ' ' + banner.innerHTML + ' '; 657 | } else { 658 | var testName = decodeURIComponent(location.search.slice(1)); 659 | banner.innerHTML = '' + banner.innerHTML + '' + testName + ''; 660 | } 661 | } 662 | } 663 | 664 | var toolbar = id("qunit-testrunner-toolbar"); 665 | if ( toolbar ) { 666 | var filter = document.createElement("input"); 667 | filter.type = "checkbox"; 668 | filter.id = "qunit-filter-pass"; 669 | addEvent( filter, "click", function() { 670 | var li = document.getElementsByTagName("li"); 671 | for ( var i = 0; i < li.length; i++ ) { 672 | if ( li[i].className.indexOf("pass") > -1 ) { 673 | li[i].style.display = filter.checked ? "none" : ""; 674 | } 675 | } 676 | if ( defined.sessionStorage ) { 677 | sessionStorage.setItem("qunit-filter-passed-tests", filter.checked ? "true" : ""); 678 | } 679 | }); 680 | if ( defined.sessionStorage && sessionStorage.getItem("qunit-filter-passed-tests") ) { 681 | filter.checked = true; 682 | } 683 | toolbar.appendChild( filter ); 684 | 685 | var label = document.createElement("label"); 686 | label.setAttribute("for", "qunit-filter-pass"); 687 | label.innerHTML = "Hide passed tests"; 688 | toolbar.appendChild( label ); 689 | } 690 | 691 | var main = id('main') || id('qunit-fixture'); 692 | if ( main ) { 693 | config.fixture = main.innerHTML; 694 | } 695 | 696 | if (config.autostart) { 697 | QUnit.start(); 698 | } 699 | }); 700 | 701 | function done() { 702 | config.autorun = true; 703 | 704 | // Log the last module results 705 | if ( config.currentModule ) { 706 | QUnit.moduleDone( { 707 | name: config.currentModule, 708 | failed: config.moduleStats.bad, 709 | passed: config.moduleStats.all - config.moduleStats.bad, 710 | total: config.moduleStats.all 711 | } ); 712 | } 713 | 714 | var banner = id("qunit-banner"), 715 | tests = id("qunit-tests"), 716 | runtime = +new Date - config.started, 717 | passed = config.stats.all - config.stats.bad, 718 | html = [ 719 | 'Tests completed in ', 720 | runtime, 721 | ' milliseconds.
    ', 722 | '', 723 | passed, 724 | ' tests of ', 725 | config.stats.all, 726 | ' passed, ', 727 | config.stats.bad, 728 | ' failed.' 729 | ].join(''); 730 | 731 | if ( banner ) { 732 | banner.className = (config.stats.bad ? "qunit-fail" : "qunit-pass"); 733 | } 734 | 735 | if ( tests ) { 736 | var result = id("qunit-testresult"); 737 | 738 | if ( !result ) { 739 | result = document.createElement("p"); 740 | result.id = "qunit-testresult"; 741 | result.className = "result"; 742 | tests.parentNode.insertBefore( result, tests.nextSibling ); 743 | } 744 | 745 | result.innerHTML = html; 746 | } 747 | 748 | QUnit.done( { 749 | failed: config.stats.bad, 750 | passed: passed, 751 | total: config.stats.all, 752 | runtime: runtime 753 | } ); 754 | } 755 | 756 | function validTest( name ) { 757 | var i = config.filters.length, 758 | run = false; 759 | 760 | if ( !i ) { 761 | return true; 762 | } 763 | 764 | while ( i-- ) { 765 | var filter = config.filters[i], 766 | not = filter.charAt(0) == '!'; 767 | 768 | if ( not ) { 769 | filter = filter.slice(1); 770 | } 771 | 772 | if ( name.indexOf(filter) !== -1 ) { 773 | return !not; 774 | } 775 | 776 | if ( not ) { 777 | run = true; 778 | } 779 | } 780 | 781 | return run; 782 | } 783 | 784 | // so far supports only Firefox, Chrome and Opera (buggy) 785 | // could be extended in the future to use something like https://github.com/csnover/TraceKit 786 | function sourceFromStacktrace() { 787 | try { 788 | throw new Error(); 789 | } catch ( e ) { 790 | if (e.stacktrace) { 791 | // Opera 792 | return e.stacktrace.split("\n")[6]; 793 | } else if (e.stack) { 794 | // Firefox, Chrome 795 | return e.stack.split("\n")[4]; 796 | } 797 | } 798 | } 799 | 800 | function resultDisplayStyle(passed) { 801 | return passed && id("qunit-filter-pass") && id("qunit-filter-pass").checked ? 'none' : ''; 802 | } 803 | 804 | function escapeHtml(s) { 805 | if (!s) { 806 | return ""; 807 | } 808 | s = s + ""; 809 | return s.replace(/[\&"<>\\]/g, function(s) { 810 | switch(s) { 811 | case "&": return "&"; 812 | case "\\": return "\\\\"; 813 | case '"': return '\"'; 814 | case "<": return "<"; 815 | case ">": return ">"; 816 | default: return s; 817 | } 818 | }); 819 | } 820 | 821 | function synchronize( callback ) { 822 | config.queue.push( callback ); 823 | 824 | if ( config.autorun && !config.blocking ) { 825 | process(); 826 | } 827 | } 828 | 829 | function process() { 830 | var start = (new Date()).getTime(); 831 | 832 | while ( config.queue.length && !config.blocking ) { 833 | if ( config.updateRate <= 0 || (((new Date()).getTime() - start) < config.updateRate) ) { 834 | config.queue.shift()(); 835 | } else { 836 | window.setTimeout( process, 13 ); 837 | break; 838 | } 839 | } 840 | if (!config.blocking && !config.queue.length) { 841 | done(); 842 | } 843 | } 844 | 845 | function saveGlobal() { 846 | config.pollution = []; 847 | 848 | if ( config.noglobals ) { 849 | for ( var key in window ) { 850 | config.pollution.push( key ); 851 | } 852 | } 853 | } 854 | 855 | function checkPollution( name ) { 856 | var old = config.pollution; 857 | saveGlobal(); 858 | 859 | var newGlobals = diff( old, config.pollution ); 860 | if ( newGlobals.length > 0 ) { 861 | ok( false, "Introduced global variable(s): " + newGlobals.join(", ") ); 862 | config.current.expected++; 863 | } 864 | 865 | var deletedGlobals = diff( config.pollution, old ); 866 | if ( deletedGlobals.length > 0 ) { 867 | ok( false, "Deleted global variable(s): " + deletedGlobals.join(", ") ); 868 | config.current.expected++; 869 | } 870 | } 871 | 872 | // returns a new Array with the elements that are in a but not in b 873 | function diff( a, b ) { 874 | var result = a.slice(); 875 | for ( var i = 0; i < result.length; i++ ) { 876 | for ( var j = 0; j < b.length; j++ ) { 877 | if ( result[i] === b[j] ) { 878 | result.splice(i, 1); 879 | i--; 880 | break; 881 | } 882 | } 883 | } 884 | return result; 885 | } 886 | 887 | function fail(message, exception, callback) { 888 | if ( typeof console !== "undefined" && console.error && console.warn ) { 889 | console.error(message); 890 | console.error(exception); 891 | console.warn(callback.toString()); 892 | 893 | } else if ( window.opera && opera.postError ) { 894 | opera.postError(message, exception, callback.toString); 895 | } 896 | } 897 | 898 | function extend(a, b) { 899 | for ( var prop in b ) { 900 | a[prop] = b[prop]; 901 | } 902 | 903 | return a; 904 | } 905 | 906 | function addEvent(elem, type, fn) { 907 | if ( elem.addEventListener ) { 908 | elem.addEventListener( type, fn, false ); 909 | } else if ( elem.attachEvent ) { 910 | elem.attachEvent( "on" + type, fn ); 911 | } else { 912 | fn(); 913 | } 914 | } 915 | 916 | function id(name) { 917 | return !!(typeof document !== "undefined" && document && document.getElementById) && 918 | document.getElementById( name ); 919 | } 920 | 921 | // Test for equality any JavaScript type. 922 | // Discussions and reference: http://philrathe.com/articles/equiv 923 | // Test suites: http://philrathe.com/tests/equiv 924 | // Author: Philippe Rathé 925 | QUnit.equiv = function () { 926 | 927 | var innerEquiv; // the real equiv function 928 | var callers = []; // stack to decide between skip/abort functions 929 | var parents = []; // stack to avoiding loops from circular referencing 930 | 931 | // Call the o related callback with the given arguments. 932 | function bindCallbacks(o, callbacks, args) { 933 | var prop = QUnit.objectType(o); 934 | if (prop) { 935 | if (QUnit.objectType(callbacks[prop]) === "function") { 936 | return callbacks[prop].apply(callbacks, args); 937 | } else { 938 | return callbacks[prop]; // or undefined 939 | } 940 | } 941 | } 942 | 943 | var callbacks = function () { 944 | 945 | // for string, boolean, number and null 946 | function useStrictEquality(b, a) { 947 | if (b instanceof a.constructor || a instanceof b.constructor) { 948 | // to catch short annotaion VS 'new' annotation of a declaration 949 | // e.g. var i = 1; 950 | // var j = new Number(1); 951 | return a == b; 952 | } else { 953 | return a === b; 954 | } 955 | } 956 | 957 | return { 958 | "string": useStrictEquality, 959 | "boolean": useStrictEquality, 960 | "number": useStrictEquality, 961 | "null": useStrictEquality, 962 | "undefined": useStrictEquality, 963 | 964 | "nan": function (b) { 965 | return isNaN(b); 966 | }, 967 | 968 | "date": function (b, a) { 969 | return QUnit.objectType(b) === "date" && a.valueOf() === b.valueOf(); 970 | }, 971 | 972 | "regexp": function (b, a) { 973 | return QUnit.objectType(b) === "regexp" && 974 | a.source === b.source && // the regex itself 975 | a.global === b.global && // and its modifers (gmi) ... 976 | a.ignoreCase === b.ignoreCase && 977 | a.multiline === b.multiline; 978 | }, 979 | 980 | // - skip when the property is a method of an instance (OOP) 981 | // - abort otherwise, 982 | // initial === would have catch identical references anyway 983 | "function": function () { 984 | var caller = callers[callers.length - 1]; 985 | return caller !== Object && 986 | typeof caller !== "undefined"; 987 | }, 988 | 989 | "array": function (b, a) { 990 | var i, j, loop; 991 | var len; 992 | 993 | // b could be an object literal here 994 | if ( ! (QUnit.objectType(b) === "array")) { 995 | return false; 996 | } 997 | 998 | len = a.length; 999 | if (len !== b.length) { // safe and faster 1000 | return false; 1001 | } 1002 | 1003 | //track reference to avoid circular references 1004 | parents.push(a); 1005 | for (i = 0; i < len; i++) { 1006 | loop = false; 1007 | for(j=0;j= 0) { 1152 | type = "array"; 1153 | } else { 1154 | type = typeof obj; 1155 | } 1156 | return type; 1157 | }, 1158 | separator:function() { 1159 | return this.multiline ? this.HTML ? '
    ' : '\n' : this.HTML ? ' ' : ' '; 1160 | }, 1161 | indent:function( extra ) {// extra can be a number, shortcut for increasing-calling-decreasing 1162 | if ( !this.multiline ) 1163 | return ''; 1164 | var chr = this.indentChar; 1165 | if ( this.HTML ) 1166 | chr = chr.replace(/\t/g,' ').replace(/ /g,' '); 1167 | return Array( this._depth_ + (extra||0) ).join(chr); 1168 | }, 1169 | up:function( a ) { 1170 | this._depth_ += a || 1; 1171 | }, 1172 | down:function( a ) { 1173 | this._depth_ -= a || 1; 1174 | }, 1175 | setParser:function( name, parser ) { 1176 | this.parsers[name] = parser; 1177 | }, 1178 | // The next 3 are exposed so you can use them 1179 | quote:quote, 1180 | literal:literal, 1181 | join:join, 1182 | // 1183 | _depth_: 1, 1184 | // This is the list of parsers, to modify them, use jsDump.setParser 1185 | parsers:{ 1186 | window: '[Window]', 1187 | document: '[Document]', 1188 | error:'[ERROR]', //when no parser is found, shouldn't happen 1189 | unknown: '[Unknown]', 1190 | 'null':'null', 1191 | undefined:'undefined', 1192 | 'function':function( fn ) { 1193 | var ret = 'function', 1194 | name = 'name' in fn ? fn.name : (reName.exec(fn)||[])[1];//functions never have name in IE 1195 | if ( name ) 1196 | ret += ' ' + name; 1197 | ret += '('; 1198 | 1199 | ret = [ ret, QUnit.jsDump.parse( fn, 'functionArgs' ), '){'].join(''); 1200 | return join( ret, QUnit.jsDump.parse(fn,'functionCode'), '}' ); 1201 | }, 1202 | array: array, 1203 | nodelist: array, 1204 | arguments: array, 1205 | object:function( map ) { 1206 | var ret = [ ]; 1207 | QUnit.jsDump.up(); 1208 | for ( var key in map ) 1209 | ret.push( QUnit.jsDump.parse(key,'key') + ': ' + QUnit.jsDump.parse(map[key]) ); 1210 | QUnit.jsDump.down(); 1211 | return join( '{', ret, '}' ); 1212 | }, 1213 | node:function( node ) { 1214 | var open = QUnit.jsDump.HTML ? '<' : '<', 1215 | close = QUnit.jsDump.HTML ? '>' : '>'; 1216 | 1217 | var tag = node.nodeName.toLowerCase(), 1218 | ret = open + tag; 1219 | 1220 | for ( var a in QUnit.jsDump.DOMAttrs ) { 1221 | var val = node[QUnit.jsDump.DOMAttrs[a]]; 1222 | if ( val ) 1223 | ret += ' ' + a + '=' + QUnit.jsDump.parse( val, 'attribute' ); 1224 | } 1225 | return ret + close + open + '/' + tag + close; 1226 | }, 1227 | functionArgs:function( fn ) {//function calls it internally, it's the arguments part of the function 1228 | var l = fn.length; 1229 | if ( !l ) return ''; 1230 | 1231 | var args = Array(l); 1232 | while ( l-- ) 1233 | args[l] = String.fromCharCode(97+l);//97 is 'a' 1234 | return ' ' + args.join(', ') + ' '; 1235 | }, 1236 | key:quote, //object calls it internally, the key part of an item in a map 1237 | functionCode:'[code]', //function calls it internally, it's the content of the function 1238 | attribute:quote, //node calls it internally, it's an html attribute value 1239 | string:quote, 1240 | date:quote, 1241 | regexp:literal, //regex 1242 | number:literal, 1243 | 'boolean':literal 1244 | }, 1245 | DOMAttrs:{//attributes to dump from nodes, name=>realName 1246 | id:'id', 1247 | name:'name', 1248 | 'class':'className' 1249 | }, 1250 | HTML:false,//if true, entities are escaped ( <, >, \t, space and \n ) 1251 | indentChar:' ',//indentation unit 1252 | multiline:true //if true, items in a collection, are separated by a \n, else just a space. 1253 | }; 1254 | 1255 | return jsDump; 1256 | })(); 1257 | 1258 | // from Sizzle.js 1259 | function getText( elems ) { 1260 | var ret = "", elem; 1261 | 1262 | for ( var i = 0; elems[i]; i++ ) { 1263 | elem = elems[i]; 1264 | 1265 | // Get the text from text nodes and CDATA nodes 1266 | if ( elem.nodeType === 3 || elem.nodeType === 4 ) { 1267 | ret += elem.nodeValue; 1268 | 1269 | // Traverse everything else, except comment nodes 1270 | } else if ( elem.nodeType !== 8 ) { 1271 | ret += getText( elem.childNodes ); 1272 | } 1273 | } 1274 | 1275 | return ret; 1276 | }; 1277 | 1278 | /* 1279 | * Javascript Diff Algorithm 1280 | * By John Resig (http://ejohn.org/) 1281 | * Modified by Chu Alan "sprite" 1282 | * 1283 | * Released under the MIT license. 1284 | * 1285 | * More Info: 1286 | * http://ejohn.org/projects/javascript-diff-algorithm/ 1287 | * 1288 | * Usage: QUnit.diff(expected, actual) 1289 | * 1290 | * QUnit.diff("the quick brown fox jumped over", "the quick fox jumps over") == "the quick brown fox jumped jumps over" 1291 | */ 1292 | QUnit.diff = (function() { 1293 | function diff(o, n){ 1294 | var ns = new Object(); 1295 | var os = new Object(); 1296 | 1297 | for (var i = 0; i < n.length; i++) { 1298 | if (ns[n[i]] == null) 1299 | ns[n[i]] = { 1300 | rows: new Array(), 1301 | o: null 1302 | }; 1303 | ns[n[i]].rows.push(i); 1304 | } 1305 | 1306 | for (var i = 0; i < o.length; i++) { 1307 | if (os[o[i]] == null) 1308 | os[o[i]] = { 1309 | rows: new Array(), 1310 | n: null 1311 | }; 1312 | os[o[i]].rows.push(i); 1313 | } 1314 | 1315 | for (var i in ns) { 1316 | if (ns[i].rows.length == 1 && typeof(os[i]) != "undefined" && os[i].rows.length == 1) { 1317 | n[ns[i].rows[0]] = { 1318 | text: n[ns[i].rows[0]], 1319 | row: os[i].rows[0] 1320 | }; 1321 | o[os[i].rows[0]] = { 1322 | text: o[os[i].rows[0]], 1323 | row: ns[i].rows[0] 1324 | }; 1325 | } 1326 | } 1327 | 1328 | for (var i = 0; i < n.length - 1; i++) { 1329 | if (n[i].text != null && n[i + 1].text == null && n[i].row + 1 < o.length && o[n[i].row + 1].text == null && 1330 | n[i + 1] == o[n[i].row + 1]) { 1331 | n[i + 1] = { 1332 | text: n[i + 1], 1333 | row: n[i].row + 1 1334 | }; 1335 | o[n[i].row + 1] = { 1336 | text: o[n[i].row + 1], 1337 | row: i + 1 1338 | }; 1339 | } 1340 | } 1341 | 1342 | for (var i = n.length - 1; i > 0; i--) { 1343 | if (n[i].text != null && n[i - 1].text == null && n[i].row > 0 && o[n[i].row - 1].text == null && 1344 | n[i - 1] == o[n[i].row - 1]) { 1345 | n[i - 1] = { 1346 | text: n[i - 1], 1347 | row: n[i].row - 1 1348 | }; 1349 | o[n[i].row - 1] = { 1350 | text: o[n[i].row - 1], 1351 | row: i - 1 1352 | }; 1353 | } 1354 | } 1355 | 1356 | return { 1357 | o: o, 1358 | n: n 1359 | }; 1360 | } 1361 | 1362 | return function(o, n){ 1363 | o = o.replace(/\s+$/, ''); 1364 | n = n.replace(/\s+$/, ''); 1365 | var out = diff(o == "" ? [] : o.split(/\s+/), n == "" ? [] : n.split(/\s+/)); 1366 | 1367 | var str = ""; 1368 | 1369 | var oSpace = o.match(/\s+/g); 1370 | if (oSpace == null) { 1371 | oSpace = [" "]; 1372 | } 1373 | else { 1374 | oSpace.push(" "); 1375 | } 1376 | var nSpace = n.match(/\s+/g); 1377 | if (nSpace == null) { 1378 | nSpace = [" "]; 1379 | } 1380 | else { 1381 | nSpace.push(" "); 1382 | } 1383 | 1384 | if (out.n.length == 0) { 1385 | for (var i = 0; i < out.o.length; i++) { 1386 | str += '' + out.o[i] + oSpace[i] + ""; 1387 | } 1388 | } 1389 | else { 1390 | if (out.n[0].text == null) { 1391 | for (n = 0; n < out.o.length && out.o[n].text == null; n++) { 1392 | str += '' + out.o[n] + oSpace[n] + ""; 1393 | } 1394 | } 1395 | 1396 | for (var i = 0; i < out.n.length; i++) { 1397 | if (out.n[i].text == null) { 1398 | str += '' + out.n[i] + nSpace[i] + ""; 1399 | } 1400 | else { 1401 | var pre = ""; 1402 | 1403 | for (n = out.n[i].row + 1; n < out.o.length && out.o[n].text == null; n++) { 1404 | pre += '' + out.o[n] + oSpace[n] + ""; 1405 | } 1406 | str += " " + out.n[i].text + nSpace[i] + pre; 1407 | } 1408 | } 1409 | } 1410 | 1411 | return str; 1412 | }; 1413 | })(); 1414 | 1415 | })(this); 1416 | --------------------------------------------------------------------------------