├── .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 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/Firefox/content/options.xul:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/Firefox/defaults/preferences/defaults.js:
--------------------------------------------------------------------------------
1 | pref("livereload.host", "localhost");
2 | pref("livereload.port", 35729);
3 |
--------------------------------------------------------------------------------
/Firefox/install.rdf:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | livereload@livereload.com
5 | 2
6 | LiveReload
7 | LiveReload refreshes a web page when files change.
8 | 0.6.4
9 | Nikita Vasilyev
10 | Andrey Tarantsov
11 | Stefan Thomas
12 | http://livereload.com/
13 | chrome://livereload/skin/icon_32.png
14 | chrome://livereload/content/options.xul
15 |
16 |
17 | {ec8030f7-c20a-464f-9b0e-13a3a9e97384}
18 | 4.0b6pre
19 | 9.0a2
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/Firefox/skin/icon_16-off.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mockko/livereload/84dcf0ff3d23809ae2b46161dd02869a20f9064e/Firefox/skin/icon_16-off.png
--------------------------------------------------------------------------------
/Firefox/skin/icon_16-on.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mockko/livereload/84dcf0ff3d23809ae2b46161dd02869a20f9064e/Firefox/skin/icon_16-on.png
--------------------------------------------------------------------------------
/Firefox/skin/icon_32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mockko/livereload/84dcf0ff3d23809ae2b46161dd02869a20f9064e/Firefox/skin/icon_32.png
--------------------------------------------------------------------------------
/LiveReload-update.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Extension Updates
6 |
7 |
8 | CFBundleIdentifier
9 | com.mockko.livereload
10 | Developer Identifier
11 | D963M2VVCH
12 | CFBundleVersion
13 | 1.6.2
14 | CFBundleShortVersionString
15 | 1.6.2
16 | URL
17 | http://github.com/downloads/mockko/livereload/LiveReload-1.6.2.safariextz
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/LiveReload.chromeextension/background.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
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 | 
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 | 
80 |
81 | Click “Install”. Actually, LiveReload does not access your browser history. The warning is misleading.
82 |
83 | 
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 | 
91 |
92 |
93 | ### [Firefox 4 extension](https://addons.mozilla.org/firefox/addon/livereload/)
94 |
95 | 
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 | 
107 |
108 | Now, if you are using Safari, right-click the page you want to be livereload'ed and choose “Enable LiveReload”:
109 |
110 | 
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 | - Run the LiveReload server in the directory that contains the file you are reading;
19 | - Open this page in the browser;
20 | - Edit;
21 | - Enjoy!
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 |
23 |
24 |
25 |
26 |
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 + 'Expected: | ' + expected + ' |
';
594 | if (actual != expected) {
595 | output += 'Result: | ' + actual + ' |
';
596 | output += 'Diff: | ' + QUnit.diff(expected, actual) +' |
';
597 | }
598 | if (!result) {
599 | var source = sourceFromStacktrace();
600 | if (source) {
601 | details.source = source;
602 | output += 'Source: | ' + source +' |
';
603 | }
604 | }
605 | output += "
";
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 |
--------------------------------------------------------------------------------