├── test
├── html
│ ├── import.css
│ ├── scss
│ │ ├── style.css
│ │ ├── style.scss
│ │ └── index.html
│ ├── file.css
│ ├── sample.html
│ ├── random-stylesheet.js
│ └── select-box.html
├── compact-paths.js
├── deferred.js
├── client-expect.js
└── model.js
├── .gitignore
├── deprecated.png
├── icon
├── icon16.png
├── icon48.png
├── ba-active.png
├── ba-error1.png
├── ba-error2.png
├── icon128.png
├── ba-disabled.png
└── ba-warning.png
├── .travis.yml
├── styles
├── assets
│ ├── fontawesome.woff
│ ├── ptsans-bold.woff
│ ├── ptsans-regular.woff
│ ├── fonts.css
│ ├── button.css
│ ├── global-message.css
│ ├── select-box.css
│ ├── toggler.css
│ └── remote-view.css
└── popup.css
├── scripts
├── worker.js
├── deprecated.js
├── lib
│ ├── tracker.js
│ ├── port-expect.js
│ ├── livestyle-model.js
│ ├── deferred.js
│ ├── crc32.js
│ ├── client-expect.js
│ ├── associations.js
│ ├── browser-action-icon.js
│ ├── event-emitter.js
│ ├── utils.js
│ ├── tween.js
│ └── model.js
├── helpers
│ ├── compact-paths.js
│ ├── get-stylesheet-content.js
│ ├── origin.js
│ ├── user-stylesheets.js
│ └── shadow-css.js
├── devtools.js
├── error-log.js
├── controllers
│ ├── error-logger.js
│ ├── editor.js
│ ├── browser-action-icon.js
│ ├── error-tracker.js
│ ├── devtools.js
│ ├── remote-view.js
│ └── model.js
├── ui
│ ├── select-box.js
│ └── remote-view.js
├── content-script.js
├── popup.js
├── devtools
│ └── resources.js
└── background.js
├── devtools.html
├── error-log.html
├── deprecated.html
├── package.json
├── manifest.json
├── gulpfile.js
├── popup.html
└── third-party
└── advisor-media.js
/test/html/import.css:
--------------------------------------------------------------------------------
1 | body {
2 | background: red;
3 | }
--------------------------------------------------------------------------------
/test/html/scss/style.css:
--------------------------------------------------------------------------------
1 | div {
2 | margin-top: 100px;
3 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | out
3 | livestyle-alpha.crx
4 | .DS_Store
5 |
--------------------------------------------------------------------------------
/test/html/scss/style.scss:
--------------------------------------------------------------------------------
1 | $a: 100px;
2 | div {
3 | margin-top: $a;
4 | }
--------------------------------------------------------------------------------
/deprecated.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/livestyle/chrome/HEAD/deprecated.png
--------------------------------------------------------------------------------
/icon/icon16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/livestyle/chrome/HEAD/icon/icon16.png
--------------------------------------------------------------------------------
/icon/icon48.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/livestyle/chrome/HEAD/icon/icon48.png
--------------------------------------------------------------------------------
/icon/ba-active.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/livestyle/chrome/HEAD/icon/ba-active.png
--------------------------------------------------------------------------------
/icon/ba-error1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/livestyle/chrome/HEAD/icon/ba-error1.png
--------------------------------------------------------------------------------
/icon/ba-error2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/livestyle/chrome/HEAD/icon/ba-error2.png
--------------------------------------------------------------------------------
/icon/icon128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/livestyle/chrome/HEAD/icon/icon128.png
--------------------------------------------------------------------------------
/icon/ba-disabled.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/livestyle/chrome/HEAD/icon/ba-disabled.png
--------------------------------------------------------------------------------
/icon/ba-warning.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/livestyle/chrome/HEAD/icon/ba-warning.png
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | sudo: false
3 | node_js:
4 | - "4"
5 | - "5"
6 | - "6"
7 |
--------------------------------------------------------------------------------
/styles/assets/fontawesome.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/livestyle/chrome/HEAD/styles/assets/fontawesome.woff
--------------------------------------------------------------------------------
/styles/assets/ptsans-bold.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/livestyle/chrome/HEAD/styles/assets/ptsans-bold.woff
--------------------------------------------------------------------------------
/scripts/worker.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | import worker from 'livestyle-patcher/lib/worker';
4 | export default worker;
--------------------------------------------------------------------------------
/styles/assets/ptsans-regular.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/livestyle/chrome/HEAD/styles/assets/ptsans-regular.woff
--------------------------------------------------------------------------------
/scripts/deprecated.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | import trackEvent from './lib/tracker';
4 | trackEvent('DevTools', 'open');
--------------------------------------------------------------------------------
/test/html/file.css:
--------------------------------------------------------------------------------
1 | @charset 'utf-8';
2 | @import url(import.css);
3 |
4 | div {
5 | background: yellow;
6 | }
7 |
8 | .foo {
9 | color: red;
10 | }
11 |
12 | .bar {
13 | color: blue;
14 | }
--------------------------------------------------------------------------------
/devtools.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | LiveStyle devtools page
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/test/html/scss/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | SCSS sample
6 |
7 |
8 |
9 | A div
10 |
11 |
--------------------------------------------------------------------------------
/test/html/sample.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Sample page
6 |
7 |
8 |
9 |
10 | A div
11 |
12 | foo
13 | bar
14 | baz
15 |
16 |
17 |
--------------------------------------------------------------------------------
/styles/assets/fonts.css:
--------------------------------------------------------------------------------
1 | @font-face {
2 | font-family: 'PT Sans';
3 | src: url('ptsans-regular.woff') format('woff');
4 | font-weight: normal;
5 | font-style: normal;
6 | }
7 |
8 | @font-face {
9 | font-family: 'PT Sans';
10 | src: url('ptsans-bold.woff') format('woff');
11 | font-weight: bold;
12 | font-style: normal;
13 | }
14 |
15 | @font-face {
16 | font-family: 'Font Awesome';
17 | src: url('fontawesome.woff') format('woff');
18 | font-weight: normal;
19 | font-style: normal;
20 | }
--------------------------------------------------------------------------------
/scripts/lib/tracker.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | window._gaq = window._gaq || [];
4 | window._gaq.push(['_setAccount', 'UA-4523560-11']);
5 | loadTracker();
6 |
7 | export default function(category, action, label) {
8 | window._gaq.push(['_trackEvent', category, action, label]);
9 | }
10 |
11 | function loadTracker() {
12 | var ga = document.createElement('script');
13 | ga.async = true;
14 | ga.src = 'https://ssl.google-analytics.com/ga.js';
15 | var s = document.getElementsByTagName('script')[0];
16 | s.parentNode.insertBefore(ga, s);
17 | }
--------------------------------------------------------------------------------
/styles/assets/button.css:
--------------------------------------------------------------------------------
1 | button {
2 | border: none;
3 | font-size: 13px;
4 | font-weight: normal;
5 | line-height: 1.2;
6 | border-radius: 4px;
7 | padding: 7px 10px;
8 | background: #344a5d;
9 | color: #fff;
10 | cursor: pointer;
11 | outline: none;
12 |
13 | -webkit-transition: background-color 0.25s linear;
14 | -moz-transition: background-color 0.25s linear;
15 | -ms-transition: background-color 0.25s linear;
16 | -o-transition: background-color 0.25s linear;
17 | transition: background-color 0.25s linear;
18 |
19 | -webkit-appearance: none;
20 | -moz-appearance: none;
21 | appearance: none;
22 | }
23 |
24 | button:hover {
25 | background-color: #415c75;
26 | }
27 |
28 | button:active {
29 | background-color: #2c3e50;
30 | }
--------------------------------------------------------------------------------
/styles/assets/global-message.css:
--------------------------------------------------------------------------------
1 | .global-message {
2 | position: absolute;
3 | z-index: 30;
4 | top: 0;
5 | right: 0;
6 | bottom: 0;
7 | left: 0;
8 | background: #fff;
9 | padding: 30px;
10 | display: none;
11 | }
12 |
13 | .global-message__title {
14 | font-weight: normal;
15 | font-size: 2.5em;
16 | text-align: center;
17 | margin-top: 0;
18 | }
19 |
20 | .global-message__comment {
21 | font-size: 0.8em;
22 | color: #95a5a6;
23 | line-height: 1.7;
24 | }
25 |
26 | .global-message__comment a {
27 | color: #2a82b7;
28 | }
29 |
30 | .status__no-editor .global-message_no-editor,
31 | .status__needs-refresh .global-message_extension-update {
32 | display: block;
33 | }
34 |
35 | .status__is-chrome.status__no-devtools .global-message_chrome-protocol {
36 | display: block;
37 | }
38 |
39 | .status__is-chrome .add-file {
40 | display: none;
41 | }
--------------------------------------------------------------------------------
/test/html/random-stylesheet.js:
--------------------------------------------------------------------------------
1 | /**
2 | * A small Node.js web-server for serving static files.
3 | * Outputs HTML with random stylesteet URL
4 | */
5 | var fs = require('fs');
6 | var path = require('path');
7 | var http = require('http');
8 | var connect = require('connect');
9 | var serveStatic = require('serve-static');
10 |
11 | var app = connect();
12 | app.use(function(req, res, next) {
13 | req.url = req.url.replace(/^\/\-\/\w+\//, '/');
14 | if (!/\.html?$/.test(req.url)) {
15 | return next();
16 | }
17 |
18 | var contents = fs.readFileSync(path.join(__dirname, req.url), 'utf8');
19 | res.end(contents.replace(/("|')([\w\/]+.css)\1/g, function(str, quote, url) {
20 | return quote + path.join('/-/' + (Math.random() * 1000 | 0), url) + quote;
21 | }));
22 | });
23 | app.use(serveStatic('./'));
24 |
25 | console.log('Starting local web-server on http://localhost:3000');
26 | http.createServer(app).listen(3000);
--------------------------------------------------------------------------------
/scripts/lib/port-expect.js:
--------------------------------------------------------------------------------
1 | /**
2 | * A wrapper for Chrome Port messaging able to send given message and wait for
3 | * response with expected message name
4 | */
5 | 'use strict';
6 |
7 | export default function(port, name, data, expectResponse) {
8 | if (typeof data === 'string' && expectResponse == null) {
9 | expectResponse = data;
10 | data = null;
11 | }
12 |
13 | return new Promise(function(resolve, reject) {
14 | var isResponded = false;
15 | var handleResponse = function(message) {
16 | if (message && message.name === expectResponse) {
17 | resolve(message.data);
18 | port.onMessage.removeListener(handleResponse);
19 | }
20 | };
21 |
22 | // in case of any error in DevTools page, respond after some time
23 | setTimeout(() => {
24 | var err = new Error(`Expectation timeout: did not received "${expectResponse}" response`);
25 | }, 3000);
26 |
27 | port.onMessage.addListener(handleResponse);
28 | port.postMessage({name, data});
29 | });
30 | };
--------------------------------------------------------------------------------
/error-log.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | LiveStyle log
6 |
49 |
50 |
51 | LiveStyle log
52 |
53 |
54 |
55 |
--------------------------------------------------------------------------------
/deprecated.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | LiveStyle DevTools panel (deprecated)
6 |
28 |
29 |
30 |
31 |
32 | Welcome to all-new Emmet LiveStyle!
33 | LiveStyle is finally reached v1.0. It no longer requires opened DevTools to work and its UI is moved to browser toolbar.
34 | Check out all exiting features, including LESS and SCSS support, at http://livestyle.io.
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "livestyle-chrome",
3 | "version": "1.0.8",
4 | "description": "A Google Chrome extension for LiveStyle",
5 | "main": "index.js",
6 | "directories": {
7 | "test": "test"
8 | },
9 | "scripts": {
10 | "test": "mocha",
11 | "pack": "gulp pack --production"
12 | },
13 | "repository": {
14 | "type": "git",
15 | "url": "git://github.com/livestyle/chrome.git"
16 | },
17 | "author": "Sergey Chikuyonok ",
18 | "license": "MIT",
19 | "bugs": {
20 | "url": "https://github.com/livestyle/chrome/issues"
21 | },
22 | "homepage": "https://github.com/livestyle/chrome",
23 | "devDependencies": {
24 | "babel": "^4.7.16",
25 | "connect": "^3.3.5",
26 | "gulp": "^3.9.0",
27 | "gulp-zip": "^3.0.2",
28 | "js-bundler": "github:sergeche/js-bundler.git#v1.1.0",
29 | "mocha": "^3.0.0",
30 | "node-notifier": "^4.1.2",
31 | "serve-static": "^1.9.2",
32 | "through2": "^2.0.0"
33 | },
34 | "dependencies": {
35 | "livestyle-client": "livestyle/client",
36 | "livestyle-cssom-patcher": "livestyle/cssom-patcher",
37 | "livestyle-patcher": "livestyle/patcher#v1.0.1"
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/test/compact-paths.js:
--------------------------------------------------------------------------------
1 | var assert = require('assert');
2 | require('babel/register');
3 | var compactPaths = require('../scripts/helpers/compact-paths');
4 |
5 | describe('Compact paths', function() {
6 | function pluck(items, key) {
7 | return items.map(function(item) {
8 | return item[key];
9 | });
10 | }
11 |
12 | function process(paths) {
13 | return pluck(compactPaths(paths), 'label');
14 | }
15 |
16 | it('keep names', function() {
17 | assert.deepEqual(
18 | process(['/path/to/file1.css', 'path/to/file2.css', 'file3.css']),
19 | ['file1.css', 'file2.css', 'file3.css']
20 | );
21 | });
22 |
23 | it('keep partial names', function() {
24 | assert.deepEqual(
25 | process(['/path/to1/file.css', 'path/to2/file.css', 'file3.css']),
26 | ['to1/file.css', 'to2/file.css', 'file3.css']
27 | );
28 | });
29 |
30 | it('keep full names', function() {
31 | assert.deepEqual(
32 | process(['/path1/to/file.css', 'path2/to/file.css', 'file3.css']),
33 | ['/path1/to/file.css', 'path2/to/file.css', 'file3.css']
34 | );
35 | });
36 |
37 | it('Windows path separator', function() {
38 | assert.deepEqual(
39 | process(['C:\\path\\to\\file1.css', 'path/to/file2.css', 'file3.css']),
40 | ['file1.css', 'file2.css', 'file3.css']
41 | );
42 | });
43 | });
--------------------------------------------------------------------------------
/scripts/helpers/compact-paths.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Compacts given list of paths: keeps smallest right-hand
3 | * difference between paths
4 | */
5 | 'use strict';
6 |
7 | import {unique} from '../lib/utils';
8 |
9 | export default function(list) {
10 | var data = unique(list).map(function(path) {
11 | return {
12 | parts: path.split(/\/|\\/).filter(Boolean),
13 | rightParts: [],
14 | path: path
15 | };
16 | });
17 |
18 | var lookup = {};
19 | var hasCollision = true, hasNext = true;
20 | var process = function(item) {
21 | if (item.parts.length) {
22 | item.rightParts.unshift(item.parts.pop());
23 | var lookupKey = item.rightParts.join('/');
24 | if (!lookup[lookupKey]) {
25 | lookup[lookupKey] = true;
26 | } else {
27 | hasCollision = true;
28 | }
29 | }
30 | return !!item.parts.length;
31 | };
32 |
33 | while (hasNext) {
34 | hasNext = false;
35 | hasCollision = false;
36 | lookup = {};
37 | for (var i = 0, il = data.length; i < il; i++) {
38 | hasNext = process(data[i]) || hasNext;
39 | }
40 |
41 | if (!hasCollision) {
42 | break;
43 | }
44 | }
45 |
46 | return data.map(function(item) {
47 | return {
48 | label: item.parts.length ? item.rightParts.join('/') : item.path,
49 | value: item.path
50 | };
51 | });
52 | };
--------------------------------------------------------------------------------
/test/html/select-box.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Select box test
6 |
7 |
8 |
22 |
23 |
24 |
25 | Regular box
26 |
32 |
33 |
34 |
35 | Offscreen dropdown box
36 |
42 |
43 |
44 |
47 |
48 |
--------------------------------------------------------------------------------
/scripts/helpers/get-stylesheet-content.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Fetches content of given stylesheet URL by any possible way: either from
3 | * DevTools resource (faster, contains most recent version) or via XHR
4 | */
5 | 'use strict';
6 | import * as devtools from '../controllers/devtools';
7 |
8 | export default function(url, tabId, callback) {
9 | if (typeof tabId === 'function') {
10 | callback = tabId;
11 | tabId = null;
12 | }
13 |
14 | var p;
15 | if (tabId && devtools.isOpenedForTab(tabId)) {
16 | p = devtools.stylesheetContent(tabId, url);
17 | } else {
18 | p = load(url);
19 | }
20 |
21 | p.then(callback, err => {
22 | console.error('Error fetching %s stylesheet content', url, err);
23 | callback(null);
24 | });
25 | };
26 |
27 | function load(url) {
28 | // no `fetch` here since it doesn’t support 'file:' protocol
29 | return new Promise(function(resolve, reject) {
30 | var xhr = new XMLHttpRequest();
31 | xhr.onreadystatechange = function() {
32 | if (xhr.readyState === 4) {
33 | if (xhr.status < 300) {
34 | resolve(xhr.responseText);
35 | } else {
36 | var err = new Error(`Unable to fetch ${url}: received ${xhr.status} code`);
37 | err.code = xhr.status;
38 | reject(new Error(err));
39 | }
40 | }
41 | };
42 | xhr.open('GET', url, true);
43 | xhr.send();
44 | });
45 | }
46 |
47 |
--------------------------------------------------------------------------------
/scripts/helpers/origin.js:
--------------------------------------------------------------------------------
1 | /**
2 | * A content script for extracting page’s URL origin. Mostly used for getting
3 | * origin of documents with `file:` protocol for Remote View.
4 | * By default it’s a filesystem root, so if RV will open a public HTTP server
5 | * pointing filesystem root, it’s gonna be a huge security breach. This module
6 | * will try to find a largest common dir prefix for resources from current
7 | * page.
8 | */
9 | 'use strict';
10 |
11 | const reIsFile = /^file:/;
12 |
13 | export default function() {
14 | var origin = location.origin;
15 | if (/^https?:/.test(origin)) {
16 | return origin;
17 | }
18 |
19 | if (reIsFile.test(origin)) {
20 | return findFileOrigin();
21 | }
22 |
23 | return null;
24 | };
25 |
26 | function $$(sel, context) {
27 | var items = (context || document).querySelectorAll(sel);
28 | return Array.prototype.slice.call(items, 0);
29 | }
30 |
31 | function findFileOrigin() {
32 | return $$('link, img, a, video, audio, script, iframe').concat([location])
33 | .map(elem => elem.currentSrc || elem.src || elem.href)
34 | .filter(url => url && reIsFile.test(url))
35 | .map(url => {
36 | // remove file from url and normalize it
37 | var parts = url.replace(/^file:\/\//, '').split('/');
38 | if (/\.[\w-]+$/.test(parts[parts.length - 1] || '')) {
39 | parts.pop();
40 | }
41 | return 'file://' + parts.join('/').replace(/\/+$/, '');
42 | })
43 | .reduce((prev, cur) => cur.length < prev.length ? cur : prev);
44 | }
--------------------------------------------------------------------------------
/scripts/devtools.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | import * as resources from './devtools/resources';
4 |
5 | var port = chrome.runtime.connect({
6 | name: 'devtools-page:' + chrome.devtools.inspectedWindow.tabId
7 | });
8 |
9 | function send(name, data) {
10 | port.postMessage({
11 | name: name,
12 | data: data
13 | });
14 | }
15 |
16 | function log() {
17 | send('log', Array.prototype.slice.call(arguments, 0));
18 | }
19 |
20 | port.onMessage.addListener(function(message) {
21 | log('Received message', message);
22 | switch (message.name) {
23 | case 'diff':
24 | resources.get(message.data.uri, function(res) {
25 | res && res.patch(message.data.patches);
26 | });
27 | break;
28 | case 'pending-patches':
29 | resources.applyPendingPatches(message.data);
30 | break;
31 | case 'get-stylesheets':
32 | resources.list(function(urls) {
33 | send('stylesheets', urls.filter(Boolean));
34 | });
35 | break;
36 | case 'get-stylesheet-content':
37 | resources.get(message.data.url, function(res) {
38 | send('stylesheet-content', {
39 | content: res ? res.content : null
40 | });
41 | });
42 | break;
43 | case 'reset':
44 | resources.reset();
45 | break;
46 | }
47 | });
48 |
49 | resources
50 | .on('log', strings => log(...strings))
51 | .on('update', (url, content) => send('resource-updated', {url, content}));
52 |
53 | chrome.devtools.panels.create('LiveStyle', 'icon/icon48.png', 'deprecated.html');
54 |
55 | log('Connected');
--------------------------------------------------------------------------------
/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Emmet LiveStyle",
3 | "description": "LiveStyle — the first bi-directional real-time edit tool for CSS, LESS and SCSS.",
4 | "short_name": "LiveStyle",
5 | "version": "1.0.0",
6 | "permissions": [
7 | "tabs",
8 | "storage",
9 | "identity",
10 | "webRequest",
11 | "http://*/*",
12 | "https://*/*"
13 | ],
14 | "icons": {
15 | "16": "icon/icon16.png",
16 | "48": "icon/icon48.png",
17 | "128": "icon/icon128.png"
18 | },
19 | "background": {
20 | "scripts": ["scripts/background.js", "third-party/advisor-media.js"]
21 | },
22 | "content_scripts": [{
23 | "matches": ["http://*/*", "https://*/*", "file:///*"],
24 | "js": ["scripts/content-script.js"],
25 | "run_at": "document_start"
26 | }],
27 | "content_security_policy": "script-src 'self' 'unsafe-eval' https://ssl.google-analytics.com https://www.google.com; object-src 'self'",
28 | "devtools_page": "./devtools.html",
29 | "browser_action": {
30 | "default_title": "LiveStyle Control Panel",
31 | "default_popup": "popup.html",
32 | "default_icon": "icon/ba-disabled.png"
33 | },
34 | "key": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDcrTJSUd51qTt4JPhpve5/bMwmJ5SFhUU2blEF9tUdt7TNCS/Y2oFz5yGWWXgLnLQ5822/+PkiSOwoG1QoLjaRz+TBIup+vctPuEsYk3+H1bGhdULjxhrsczKjYA2KPMX0ll/ncR1C3lalRx8PMwYW68bkXH/Z9USp5ITlBjoJ6QIDAQAB",
35 | "oauth2": {
36 | "client_id": "429076250235-ik9plskehth6ihmslgnl5a83do6e9tm9.apps.googleusercontent.com",
37 | "scopes": ["email"]
38 | },
39 | "manifest_version": 2
40 | }
41 |
--------------------------------------------------------------------------------
/scripts/lib/livestyle-model.js:
--------------------------------------------------------------------------------
1 | /**
2 | * LiveStyle model: responsible for storing info about
3 | * LiveStyle state for context page
4 | */
5 | 'use strict';
6 |
7 | import Model from './model';
8 | import EventEmitter from './event-emitter';
9 | import associations from './associations';
10 |
11 | var emitter = new EventEmitter();
12 |
13 | export default class LiveStyleModel extends Model {
14 | constructor(id) {
15 | super();
16 | this.id = id;
17 | this.lastUpdate = Date.now();
18 | this
19 | .on('change:browserFiles change:editorFiles change:assocs change:userStylesheets', function() {
20 | this.emit('update');
21 | })
22 | .on('all', function() {
23 | // pass all inner model events to the global dispatcher
24 | LiveStyleModel.emit.apply(LiveStyleModel, arguments);
25 | });
26 | }
27 |
28 | /**
29 | * Returns virtual file associations. Unlike “real“ associations,
30 | * where user explicitly pick files, virtual ones contains guessed
31 | * associations for files user didn’t picked yet
32 | * @return {Object}
33 | */
34 | associations() {
35 | var browserFiles = this.get('browserFiles') || [];
36 | var userStylesheets = Object.keys(this.get('userStylesheets') || {});
37 | return associations(
38 | browserFiles.concat(userStylesheets),
39 | this.get('editorFiles'),
40 | this.get('assocs')
41 | );
42 | }
43 | }
44 |
45 | LiveStyleModel.on = emitter.on.bind(emitter);
46 | LiveStyleModel.off = emitter.off.bind(emitter);
47 | LiveStyleModel.emit = emitter.emit.bind(emitter);
--------------------------------------------------------------------------------
/scripts/lib/deferred.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var STATE_PENDING = 'pending';
4 | var STATE_FULFILLED = 'fulfilled';
5 | var STATE_REJECTED = 'rejected';
6 |
7 | import {toArray} from './utils';
8 |
9 | function fulfill(listeners, args) {
10 | listeners.forEach(function(fn) {
11 | fn.apply(null, args);
12 | });
13 | }
14 |
15 | function isFn(obj) {
16 | return typeof obj === 'function';
17 | }
18 |
19 | export default function Deferred(fn) {
20 | if (!(this instanceof Deferred)) {
21 | return new Deferred(fn);
22 | }
23 |
24 | var state = STATE_PENDING;
25 | var value = void 0;
26 | var fulfilled = [];
27 | var rejected = [];
28 | var self = this;
29 |
30 | var respond = function(callbacks) {
31 | fulfill(callbacks, value);
32 | fulfilled.length = rejected.length = 0;
33 | };
34 |
35 | var changeState = function(newState, callbacks) {
36 | return function() {
37 | if (state === STATE_PENDING) {
38 | state = newState;
39 | value = toArray(arguments);
40 | respond(callbacks);
41 | }
42 | return self;
43 | };
44 | };
45 |
46 | this.resolve = changeState(STATE_FULFILLED, fulfilled);
47 | this.reject = changeState(STATE_REJECTED, rejected);
48 | this.then = function(onFulfilled, onRejected) {
49 | isFn(onFulfilled) && fulfilled.push(onFulfilled);
50 | isFn(onRejected) && rejected.push(onRejected);
51 | if (state === STATE_FULFILLED) {
52 | respond(fulfilled);
53 | } else if (state === STATE_REJECTED) {
54 | respond(rejected);
55 | }
56 |
57 | return this;
58 | };
59 |
60 | Object.defineProperty(this, 'state', {
61 | enumerable: true,
62 | get: function() {
63 | return state;
64 | }
65 | });
66 |
67 | if (isFn(fn)) {
68 | fn.call(this);
69 | }
70 | }
--------------------------------------------------------------------------------
/test/deferred.js:
--------------------------------------------------------------------------------
1 | var assert = require('assert');
2 | require('babel/register');
3 | var deferred = require('../scripts/lib/deferred');
4 |
5 | describe('Deferred', function() {
6 | it('resolve', function() {
7 | var resolved = 0, rejected = 0;
8 | var d = deferred()
9 | .then(function() {resolved++;}, function() {rejected++;})
10 | .resolve()
11 | .then(function() {resolved++;});
12 |
13 | assert.equal(resolved, 2);
14 | assert.equal(rejected, 0);
15 | assert.equal(d.state, 'fulfilled');
16 | });
17 |
18 | it('reject', function() {
19 | var resolved = 0, rejected = 0;
20 | var d = deferred()
21 | .then(function() {resolved++;}, function() {rejected++;})
22 | .reject()
23 | .then(null, function() {rejected++;});
24 |
25 | assert.equal(resolved, 0);
26 | assert.equal(rejected, 2);
27 | assert.equal(d.state, 'rejected');
28 | });
29 |
30 | it('preserve state', function() {
31 | var resolved = 0, rejected = 0;
32 | var d = deferred()
33 | .resolve()
34 | .reject()
35 | .then(function() {resolved++;}, function() {rejected++;});
36 |
37 | assert.equal(resolved, 1);
38 | assert.equal(rejected, 0);
39 | assert.equal(d.state, 'fulfilled');
40 | });
41 |
42 | it('default handler', function() {
43 | var d = deferred(function() {
44 | this.resolve();
45 | });
46 | assert.equal(d.state, 'fulfilled');
47 | });
48 |
49 | it('arguments passing', function() {
50 | var result = '';
51 | var d = deferred()
52 | .then(function(a, b) {result = a + ':' + b;}, function(a, b) {result = b + ':' + a;})
53 | .resolve('foo', 'bar');
54 |
55 | assert.equal(result, 'foo:bar');
56 | });
57 |
58 | it('async', function(done) {
59 | var d = deferred().then(function() {
60 | assert('ok');
61 | done();
62 | });
63 |
64 | setTimeout(d.resolve, 10);
65 | });
66 | });
--------------------------------------------------------------------------------
/scripts/lib/crc32.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Fast CRC32 algorithm for strings.
3 | * Original source: https://github.com/SheetJS/js-crc32
4 | * © 2014 SheetJS — http://sheetjs.com
5 | */
6 | 'use strict';
7 |
8 | var table = new Array(256);
9 | for (var n = 0, c; n != 256; ++n) {
10 | c = n;
11 | c = ((c&1) ? (-306674912 ^ (c >>> 1)) : (c >>> 1));
12 | c = ((c&1) ? (-306674912 ^ (c >>> 1)) : (c >>> 1));
13 | c = ((c&1) ? (-306674912 ^ (c >>> 1)) : (c >>> 1));
14 | c = ((c&1) ? (-306674912 ^ (c >>> 1)) : (c >>> 1));
15 | c = ((c&1) ? (-306674912 ^ (c >>> 1)) : (c >>> 1));
16 | c = ((c&1) ? (-306674912 ^ (c >>> 1)) : (c >>> 1));
17 | c = ((c&1) ? (-306674912 ^ (c >>> 1)) : (c >>> 1));
18 | c = ((c&1) ? (-306674912 ^ (c >>> 1)) : (c >>> 1));
19 | table[n] = c;
20 | }
21 |
22 | if (typeof Int32Array !== 'undefined') {
23 | table = new Int32Array(table);
24 | }
25 |
26 | export default function(str) {
27 | for (var crc = -1, i = 0, L=str.length, c, d; i < L;) {
28 | c = str.charCodeAt(i++);
29 | if (c < 0x80) {
30 | crc = (crc >>> 8) ^ table[(crc ^ c) & 0xFF];
31 | } else if (c < 0x800) {
32 | crc = (crc >>> 8) ^ table[(crc ^ (192|((c>>6)&31))) & 0xFF];
33 | crc = (crc >>> 8) ^ table[(crc ^ (128|(c&63))) & 0xFF];
34 | } else if (c >= 0xD800 && c < 0xE000) {
35 | c = (c&1023)+64; d = str.charCodeAt(i++) & 1023;
36 | crc = (crc >>> 8) ^ table[(crc ^ (240|((c>>8)&7))) & 0xFF];
37 | crc = (crc >>> 8) ^ table[(crc ^ (128|((c>>2)&63))) & 0xFF];
38 | crc = (crc >>> 8) ^ table[(crc ^ (128|((d>>6)&15)|(c&3))) & 0xFF];
39 | crc = (crc >>> 8) ^ table[(crc ^ (128|(d&63))) & 0xFF];
40 | } else {
41 | crc = (crc >>> 8) ^ table[(crc ^ (224|((c>>12)&15))) & 0xFF];
42 | crc = (crc >>> 8) ^ table[(crc ^ (128|((c>>6)&63))) & 0xFF];
43 | crc = (crc >>> 8) ^ table[(crc ^ (128|(c&63))) & 0xFF];
44 | }
45 | }
46 | return crc ^ -1;
47 | };
--------------------------------------------------------------------------------
/scripts/lib/client-expect.js:
--------------------------------------------------------------------------------
1 | /**
2 | * A wrapper around LiveStyle client able to send messages and wait for expected
3 | * response
4 | */
5 | 'use strict';
6 |
7 | import client from 'livestyle-client';
8 |
9 | export function send(name, data) {
10 | var messageSent = false;
11 | setTimeout(function() {
12 | client.send(name, data);
13 | messageSent = true;
14 | }, 0);
15 |
16 | return {
17 | expect(expectedMessageName, validate, timeout=1000) {
18 | if (messageSent) {
19 | var err = new Error(`Message "${expectedMessageName}" already sent`);
20 | err.code = 'EMESSAGESENT';
21 | return Promise.reject(err);
22 | }
23 |
24 | if (typeof validate === 'number') {
25 | timeout = validate;
26 | validate = null;
27 | }
28 |
29 | return new Promise(function(resolve, reject) {
30 | var cancelId = setTimeout(function() {
31 | client.off('message-receive', callback);
32 | var err = new Error(`Expected message "${expectedMessageName}" timed out`);
33 | err.code = 'EEXPECTTIMEOUT';
34 | err.messageName = expectedMessageName;
35 | reject(err);
36 | }, timeout);
37 |
38 | var callback = function(name, data) {
39 | if (name === expectedMessageName) {
40 | var isValid = true;
41 | if (validate) {
42 | try {
43 | isValid = validate(data);
44 | } catch (e) {
45 | isValid = false;
46 | }
47 | }
48 |
49 | if (isValid) {
50 | client.off('message-receive', callback);
51 | clearTimeout(cancelId);
52 | resolve(data);
53 | }
54 | }
55 | };
56 |
57 | client.on('message-receive', callback);
58 | });
59 | }
60 | };
61 | }
62 |
63 | export function on() {
64 | client.on.apply(client, arguments);
65 | }
66 |
67 | export function off() {
68 | client.off.apply(client, arguments);
69 | }
70 |
71 | export function emit() {
72 | client.emit.apply(client, arguments);
73 | }
--------------------------------------------------------------------------------
/scripts/error-log.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | function padNum(num) {
4 | return (num < 10 ? '0' : '') + num;
5 | }
6 |
7 | function toDOM(html) {
8 | var div = document.createElement('div');
9 | div.innerHTML = html;
10 | var df = document.createDocumentFragment();
11 | while (div.firstChild) {
12 | df.appendChild(div.firstChild);
13 | }
14 | return df;
15 | }
16 |
17 | function renderLogItem(item) {
18 | var date = new Date(item.date);
19 | var time = padNum(date.getHours()) + ':' + padNum(date.getMinutes());
20 |
21 | return toDOM(''
22 | + '[' + time + '] '
23 | + item.message.replace(/\t/g, ' ')
24 | + ''
25 | );
26 | }
27 |
28 | function updateLog(items) {
29 | // show log items in reverse order, e.g. newer on top
30 | items = items.reverse();
31 |
32 | var container = document.querySelector('.log');
33 | var currentItems = container.querySelectorAll('.log__item');
34 | var lookup = {};
35 | for (var i = 0, il = currentItems.length; i < il; i++) {
36 | lookup[currentItems[i].getAttribute('id')] = currentItems[i];
37 | }
38 |
39 | var df = document.createDocumentFragment();
40 | items.forEach(function(item) {
41 | var itemId = item.messageId + '';
42 | if (lookup[itemId]) {
43 | df.appendChild(lookup[itemId]);
44 | delete lookup[itemId];
45 | } else {
46 | df.appendChild(renderLogItem(item));
47 | }
48 | });
49 |
50 | // Remove old messages
51 | Object.keys(lookup).forEach(function(id) {
52 | container.removeChild(lookup[id]);
53 | });
54 |
55 | // Insert current messages
56 | container.appendChild(df);
57 | }
58 |
59 | // Listen to log updates
60 | chrome.runtime.onMessage.addListener(function(message) {
61 | if (message.name === 'log-updated') {
62 | updateLog(message.data);
63 | }
64 | });
65 |
66 | // Request current log
67 | chrome.runtime.sendMessage({name: 'get-log'}, updateLog);
--------------------------------------------------------------------------------
/scripts/controllers/error-logger.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Logs all errors in LiveStyle worker. Unlike error tracker, which
3 | * simply notifies user about possible errors, this method actually
4 | * logs error messages and displays them upon request
5 | */
6 | 'use strict';
7 |
8 | var logItems = [];
9 | var maxLogItems = 50;
10 | var messageId = 0;
11 |
12 | /**
13 | * Watches for errors on given LiveStyle patcher instance
14 | * @param {CommandQueue} patcher LiveStyle patcher
15 | */
16 | export function watch(patcher) {
17 | patcher.worker.addEventListener('message', handleWorkerEvent);
18 | }
19 |
20 | /**
21 | * Stops watching for errors on given LiveStyle patcher instance
22 | * @param {CommandQueue} patcher LiveStyle patcher
23 | */
24 | export function unwatch(patcher) {
25 | patcher.worker.removeEventListener('message', handleWorkerEvent);
26 | }
27 |
28 | /**
29 | * Returns currently logged items
30 | * @return {Array} Array of log items
31 | */
32 | export function getLog() {
33 | return logItems;
34 | }
35 |
36 | function logMessage(message, type) {
37 | // Remove items with the same message
38 | for (var i = logItems.length - 1; i >= 0; i--) {
39 | if (logItems[i].message == message) {
40 | logItems.splice(i, 1);
41 | }
42 | }
43 |
44 | logItems.push({
45 | messageId: messageId++,
46 | date: Date.now(),
47 | message: message,
48 | type: type
49 | });
50 |
51 | messageId %= 10000;
52 |
53 | while (logItems.length > maxLogItems) {
54 | logItems.shift();
55 | }
56 |
57 | chrome.runtime.sendMessage({
58 | name: 'log-updated',
59 | data: logItems
60 | });
61 | }
62 |
63 | function handleWorkerEvent(message) {
64 | var payload = message.data;
65 | if (payload.status === 'error') {
66 | logMessage(payload.data, 'error');
67 | }
68 | }
69 |
70 | // handle internal extension communication
71 | chrome.runtime.onMessage.addListener(function(message, sender, callback) {
72 | if (message.name === 'get-log') {
73 | callback(logItems);
74 | return true;
75 | }
76 | });
--------------------------------------------------------------------------------
/scripts/helpers/user-stylesheets.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Manages user stylesheets life cycle.
3 | * A user stylesheet is a stylesheet created by LiveStyle
4 | * on current page specifically for live updates:
5 | * it is added below page stylesheets (hence has higher
6 | * priority), it’s small and fast: a good alternative
7 | * for very large page stylesheets where each update
8 | * could take some time
9 | */
10 | 'use strict';
11 |
12 | var reUser = /^livestyle:([0-9]+)$/;
13 |
14 | /**
15 | * Creates user stylsheets for given IDs and
16 | * returns hash where key is given ID and value
17 | * is generated Blob URL
18 | * @param {Array} urls Array of interlat LiveStyle IDs
19 | * @param {Function} callback Invoked with hash result
20 | */
21 | export function create(tabId, url, callback) {
22 | if (!url || !url.length) {
23 | return callback({});
24 | }
25 |
26 | chrome.tabs.sendMessage(tabId, {
27 | name: 'create-user-stylesheet',
28 | data: {url: url}
29 | }, callback);
30 | }
31 |
32 | /**
33 | * Removes stylesheet with given URL (blob or internal LiveStyle ID)
34 | * @param {String} url Stylesteet URL
35 | */
36 | export function remove(tabId, url) {
37 | chrome.tabs.sendMessage(tabId, {
38 | name: 'remove-user-stylesheet',
39 | data: {url: url}
40 | });
41 | }
42 |
43 | /**
44 | * Validates given list of interla URLs: creates missing
45 | * and removes redundant stylesheets
46 | * @param {String} url Internal URL or array of URLs
47 | * @param {Function} callback Callback function receives hash
48 | * where key is given URL and value is generated blob URL
49 | */
50 | export function validate(tabId, url, callback) {
51 | if (!url || !url.length) {
52 | return callback({});
53 | }
54 |
55 | chrome.tabs.sendMessage(tabId, {
56 | name: 'validate-user-stylesheet',
57 | data: {url: url}
58 | }, callback);
59 | }
60 |
61 | /**
62 | * Check if given URL is user stylesheet file
63 | * @param {String} url
64 | * @return {Boolean}
65 | */
66 | export function is(url) {
67 | var m = url.match(reUser);
68 | return m && m[1];
69 | }
70 |
--------------------------------------------------------------------------------
/styles/assets/select-box.css:
--------------------------------------------------------------------------------
1 | .select-box {
2 | position: relative;
3 | display: block;
4 | font-size: 14px;
5 | font-weight: normal;
6 | line-height: 1.4;
7 | cursor: pointer;
8 | background: #26bb9d;
9 | border-radius: 3px;
10 |
11 | -webkit-transition: background-color 0.5s;
12 | -moz-transition: background-color 0.5s;
13 | -ms-transition: background-color 0.5s;
14 | -o-transition: background-color 0.5s;
15 | transition: background-color 0.5s;
16 | }
17 |
18 | .select-box:hover {
19 | background-color: #48c9b0;
20 | }
21 |
22 | .select-box__label {
23 | padding: 7px 30px 7px 10px;
24 | color: #fff;
25 | display: block;
26 | }
27 |
28 | .select-box:after {
29 | position: absolute;
30 | top: 42%;
31 | right: 13px;
32 | display: inline-block;
33 | border-color: transparent;
34 | border-top-color: #fff;
35 | border-style: solid;
36 | border-width: 6px 4px;
37 | pointer-events: none;
38 | content: '';
39 | }
40 |
41 | .select-box_active > .select-box__picker {
42 | display: block;
43 | }
44 |
45 | .select-box__picker {
46 | display: none;
47 | position: absolute;
48 | list-style-type: none;
49 | z-index: 10;
50 | min-width: 220px;
51 | max-height: 165px;
52 | min-height: 37px;
53 | overflow: auto;
54 | width: 100%;
55 | left: 0;
56 | top: 100%;
57 | padding: 0;
58 | margin-top: 9px;
59 | font-size: 14px;
60 | background-color: #f3f4f5;
61 | color: #34495e;
62 | border-radius: 4px;
63 | }
64 |
65 | .select-box__picker_attop {
66 | margin-top: 0;
67 | margin-bottom: 9px;
68 | top: auto;
69 | bottom: 100%;
70 | }
71 |
72 | .select-box__picker-item {
73 | padding: 8px 10px;
74 | line-height: 1.5;
75 | }
76 |
77 | .select-box__picker-item:first-child {
78 | border-top-left-radius: 4px;
79 | border-top-right-radius: 4px;
80 | }
81 |
82 | .select-box__picker-item:last-child {
83 | border-bottom-left-radius: 4px;
84 | border-bottom-right-radius: 4px;
85 | }
86 |
87 | .select-box__picker-item:hover {
88 | background: #e1e4e7;
89 | }
90 |
91 | .select-box__picker-item_selected,
92 | .select-box__picker-item_selected:hover {
93 | background: #95a5a6;
94 | color: #fff;
95 | }
--------------------------------------------------------------------------------
/gulpfile.js:
--------------------------------------------------------------------------------
1 | var path = require('path');
2 | var gulp = require('gulp');
3 | var zip = require('gulp-zip');
4 | var js = require('js-bundler');
5 | var notifier = require('node-notifier');
6 | var through = require('through2');
7 | var pkg = require('./package.json');
8 |
9 | var production = process.argv.indexOf('--production') !== -1;
10 | var dest = './out';
11 | var src = {
12 | js: './scripts/*.js',
13 | assets: ['./{icon,styles}/**', './*.{html,png}', './manifest.json', './third-party/advisor-media.js'],
14 | options: {base: './'}
15 | };
16 |
17 | function cleanup() {
18 | return through.obj(function(file, enc, next) {
19 | var str = file.contents.toString();
20 | if (str.indexOf(__dirname)) {
21 | file.contents = new Buffer(str.replace(__dirname, ''));
22 | }
23 | next(null, file);
24 | });
25 | }
26 |
27 | function np(lib) {
28 | return path.join(__dirname, 'node_modules', lib);
29 | }
30 |
31 | gulp.task('js', function() {
32 | return gulp.src(src.js, src.options)
33 | .pipe(js({
34 | standalone: true,
35 | sourceMap: !production,
36 | noParse: [np('livestyle-cssom-patcher/out/livestyle-cssom.js')],
37 | detectGlobals: false
38 | }))
39 | .pipe(cleanup())
40 | .pipe(gulp.dest(dest))
41 | });
42 |
43 | gulp.task('assets', function() {
44 | return gulp.src(src.assets, src.options)
45 | .pipe(through.obj(function(file, enc, next) {
46 | if (path.basename(file.path) === 'manifest.json') {
47 | var data = JSON.parse(file.contents.toString());
48 | data.version = pkg.version;
49 | file.contents = new Buffer(JSON.stringify(data), null, '\t');
50 | }
51 | next(null, file);
52 | }))
53 | .pipe(gulp.dest(dest));
54 | });
55 |
56 | gulp.task('pack', ['build'], function() {
57 | return gulp.src(['**', '!*.zip'], {cwd: dest})
58 | .pipe(zip('livestyle.zip'))
59 | .pipe(gulp.dest(dest));
60 | });
61 |
62 | gulp.task('watch', ['build'], function() {
63 | js.watch({sourceMap: true, uglify: false});
64 | gulp.watch('./scripts/**/*.js', ['js']);
65 | gulp.watch(src.assets, ['assets']);
66 | });
67 |
68 | gulp.task('build', ['js', 'assets']);
69 | gulp.task('default', ['build']);
70 |
--------------------------------------------------------------------------------
/scripts/controllers/editor.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Editor files controller: provides model with
3 | * available files from all connected editors. This model
4 | * is updated whenever user connects new editor or opens/closes
5 | * stylesheet files
6 | */
7 | 'use strict';
8 |
9 | import Model from '../lib/model';
10 | import {unique} from '../lib/utils';
11 |
12 | // The `active` key tells if there are any connected editor
13 | var editorFiles = new Model();
14 | var connectedEditors = {};
15 |
16 | export default editorFiles;
17 |
18 | /**
19 | * Sync all connect editor files with underlying
20 | * editor files model
21 | */
22 | function sync() {
23 | var allFiles = [];
24 | var ids = Object.keys(connectedEditors);
25 | ids.forEach(function(id) {
26 | allFiles = allFiles.concat(connectedEditors[id] || []);
27 | });
28 |
29 | allFiles = unique(allFiles);
30 | editorFiles.set('files', allFiles);
31 | editorFiles.set('active', ids.length > 0);
32 | return allFiles;
33 | }
34 |
35 | function onFileListReceived(payload) {
36 | connectedEditors[payload.id] = payload.files || [];
37 | sync();
38 | }
39 |
40 | function onEditorDisconnect(payload) {
41 | if (payload.id in connectedEditors) {
42 | delete connectedEditors[payload.id];
43 | sync();
44 | }
45 | }
46 |
47 | function onConnectionClosed() {
48 | connectedEditors = {};
49 | sync();
50 | }
51 |
52 | /**
53 | * Connects model with given LiveStyle client:
54 | * model now tracks all editor file-related changes and
55 | * notifies all listener on update
56 | * @param {LiveStyleClient} client
57 | */
58 | editorFiles.connect = function(client) {
59 | client
60 | .on('editor-files', onFileListReceived)
61 | .on('editor-disconnect', onEditorDisconnect)
62 | .on('close', onConnectionClosed);
63 | };
64 |
65 | /**
66 | * Disconnects model from given client: it no longer
67 | * listens to editor files update
68 | * @param {LiveStyleClient} client
69 | */
70 | editorFiles.disconnect = function(client) {
71 | client
72 | .off('editor-files', onFileListReceived)
73 | .off('editor-disconnect', onEditorDisconnect)
74 | .off('close', onConnectionClosed);
75 | };
76 |
77 | sync();
--------------------------------------------------------------------------------
/scripts/controllers/browser-action-icon.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Controls browser action icon state depending on
3 | * user activity with tabs
4 | */
5 | 'use strict';
6 |
7 | import * as modelController from './model';
8 | import * as icon from '../lib/browser-action-icon';
9 | import LiveStyleModel from '../lib/livestyle-model';
10 |
11 | export function watchErrors(tracker) {
12 | tracker.on('change:error', function() {
13 | if (this.get('error')) {
14 | modelController.current((model, tab) => icon.state(tab.id, 'error'));
15 | } else {
16 | update();
17 | }
18 | });
19 | }
20 |
21 | export function update() {
22 | chrome.tabs.query({active: true, windowType: 'normal'}, function(tabs) {
23 | tabs.forEach(function(tab) {
24 | modelController.get(tab, model => updateIconState(tab, model));
25 | });
26 | });
27 | }
28 |
29 | function updateIconState(tab, model) {
30 | if (typeof tab === 'object') {
31 | tab = tab.id;
32 | }
33 |
34 | var state = model.get('enabled') ? 'active' : 'disabled';
35 | if (state === 'active' && model.get('needsRefresh')) {
36 | state = 'warning';
37 | }
38 | icon.state(tab, state);
39 | }
40 |
41 | /**
42 | * Returns list of active tabs that matches given module
43 | * @param {LiveStyleModel} model
44 | * @param {Function} callback
45 | */
46 | function activeTabsForModel(model, callback) {
47 | chrome.tabs.query({active: true, windowType: 'normal'}, function(tabs) {
48 | callback(tabs.filter(function(tab) {
49 | return modelController.id(tab) === model.id;
50 | }));
51 | });
52 | }
53 |
54 | // listen to changes on activity state of models and update
55 | // browser icons accordingly
56 | LiveStyleModel.on('change:enabled', function(model) {
57 | activeTabsForModel(model, function(tabs) {
58 | tabs.forEach(tab => updateIconState(tab, model));
59 | });
60 | });
61 |
62 | update();
63 | chrome.tabs.onActivated.addListener(update);
64 | chrome.tabs.onRemoved.addListener(icon.clearState);
65 | chrome.tabs.onUpdated.addListener(function(tabId, changeInfo, tab) {
66 | if (changeInfo.status === 'loading') {
67 | return icon.clearState(tabId);
68 | }
69 |
70 | if (changeInfo.status === 'complete' && tab.active) {
71 | update();
72 | }
73 | });
74 |
--------------------------------------------------------------------------------
/test/client-expect.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var assert = require('assert');
4 | var _client = require('livestyle-client');
5 | require('babel/register');
6 | var client = require('../scripts/lib/client-expect');
7 |
8 | describe('Client Expect', function() {
9 | var oldStatus = _client.status;
10 | before(function() {
11 | _client._setStatus('connected');
12 | });
13 | after(function() {
14 | _client._setStatus(oldStatus);
15 | });
16 |
17 | it('send', function(done) {
18 | client.on('message-send', function onMessage(name) {
19 | assert.equal(name, 'ping');
20 | client.off('message-send', onMessage);
21 | done();
22 | });
23 |
24 | client.send('ping');
25 | });
26 |
27 | it('send & expect (success)', function(done) {
28 | var onMessage = function(name) {
29 | if (name === 'ping') {
30 | client.emit('message-receive', 'pong');
31 | }
32 | };
33 |
34 | client.on('message-send', onMessage);
35 |
36 | client.send('ping').expect('pong').then(function() {
37 | client.off('message-send', onMessage);
38 | done();
39 | }, done);
40 | });
41 |
42 | it('send & expect (fail)', function(done) {
43 | client.send('ping').expect('pong', 100).then(function() {
44 | done(new Error('Should not resolve'));
45 | }, function(err) {
46 | assert(err);
47 | assert.equal(err.messageName, 'pong');
48 | done();
49 | });
50 | });
51 |
52 | it('expect after send', function(done) {
53 | var obj = client.send('ping');
54 | setTimeout(function() {
55 | obj.expect('pong').then(function() {
56 | done(new Error('Should not resolve'));
57 | }, function(err) {
58 | assert(err);
59 | assert(err.message.indexOf('already sent') !== -1);
60 | done();
61 | });
62 | }, 10);
63 | });
64 |
65 | it('validate', function(done) {
66 | var onMessage = function(name) {
67 | if (name === 'ping') {
68 | client.emit('message-receive', 'pong', {a: 1});
69 |
70 | setTimeout(function() {
71 | client.emit('message-receive', 'pong', {a: 2, foo: 'bar'});
72 | }, 100);
73 | }
74 | };
75 |
76 | client.on('message-send', onMessage);
77 | client.send('ping')
78 | .expect('pong', function(data) {
79 | return data.a === 2;
80 | })
81 | .then(function(data) {
82 | assert.equal(data.a, 2);
83 | assert.equal(data.foo, 'bar');
84 | client.off('message-send', onMessage);
85 | done();
86 | }, done);
87 | });
88 | });
--------------------------------------------------------------------------------
/scripts/lib/associations.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Returns virtual associations between browser and editor files.
3 | *
4 | * Unlike “real” associations where user manually picks files,
5 | * virtual associations may contain guessed matches that
6 | * may change if user opens another file in editor
7 | */
8 | 'use strict';
9 |
10 | export default function(browserFiles, editorFiles, assocs) {
11 | assocs = assocs || {};
12 | browserFiles = browserFiles || [];
13 | editorFiles = editorFiles || [];
14 |
15 | return browserFiles.reduce(function(result, browserFile) {
16 | var editorFile = assocs[browserFile];
17 | if (editorFile == null) {
18 | // user didn’t picked association yet: guess it
19 | // XXX compare with `null` and `undefined` because empty string
20 | // means user forcibly removed association (for example, from
21 | // guessed association)
22 | editorFile = ~editorFiles.indexOf(browserFile) ? browserFile : guessAssoc(editorFiles, browserFile);
23 | } else if (!~editorFiles.indexOf(editorFile)) {
24 | // we have association but user didn’t opened it yet:
25 | // assume there’s no association
26 | editorFile = null;
27 | }
28 | result[browserFile] = editorFile;
29 | return result;
30 | }, {});
31 | }
32 |
33 | function pathLookup(path) {
34 | return path.split('?')[0].split('/').filter(Boolean);
35 | }
36 |
37 | function guessAssoc(list, file) {
38 | var fileLookup = pathLookup(file).reverse();
39 | var candidates = list.map(function(path) {
40 | return {
41 | path: path,
42 | lookup: pathLookup(path)
43 | };
44 | });
45 |
46 | var chunk, prevCandidates;
47 | for (var i = 0, il = fileLookup.length; i < il; i++) {
48 | prevCandidates = candidates;
49 | candidates = candidates.filter(function(candidate) {
50 | var part = candidate.lookup.pop();
51 | if (fileLookup[i] === part) {
52 | return true;
53 | }
54 |
55 | if (i === 0) {
56 | // comparing file names: also try names without extension
57 | return cleanFileName(fileLookup[i]) === cleanFileName(part);
58 | }
59 | });
60 |
61 | if (candidates.length === 1) {
62 | break;
63 | } else if (!candidates.length) {
64 | // empty candidates list on first pass means we
65 | // didn’t found anything at all
66 | candidates = i ? prevCandidates : null;
67 | break;
68 | }
69 | }
70 |
71 | if (candidates && candidates.length) {
72 | return candidates[0].path;
73 | }
74 | }
75 |
76 | function cleanFileName(file) {
77 | return file.replace(/\.\w+$/, '');
78 | }
--------------------------------------------------------------------------------
/scripts/lib/browser-action-icon.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Displays browser action icon according to current activity state
3 | */
4 | 'use strict';
5 |
6 | import deferred from './deferred';
7 |
8 | var states = {};
9 | var loaded = deferred();
10 | var canvas = document.createElement('canvas');
11 | var ctx = canvas.getContext('2d');
12 | var images = {
13 | disabled: image('./icon/ba-disabled.png'),
14 | active: image('./icon/ba-active.png'),
15 | warning: image('./icon/ba-warning.png'),
16 | error1: image('./icon/ba-error1.png'),
17 | error2: image('./icon/ba-error2.png')
18 | };
19 | var PI2 = Math.PI * 2;
20 | var errState = {
21 | pos: 0,
22 | step: 0.05
23 | };
24 |
25 | canvas.width = canvas.height = 19;
26 |
27 | export function state(tabId, value) {
28 | if (typeof value !== 'undefined' && value !== states[tabId]) {
29 | states[tabId] = value;
30 | loaded.then(function() {
31 | renderState(tabId, value);
32 | });
33 | }
34 | return states[tabId];
35 | }
36 |
37 | export function reset() {
38 | Object.keys(states).forEach(function(tabId) {
39 | renderState(tabId, 'disabled');
40 | clearState(tabId)
41 | });
42 | }
43 |
44 | export function clearState(tabId) {
45 | delete states[tabId];
46 | }
47 |
48 | function image(src) {
49 | var img = new Image();
50 | img.onload = function() {
51 | if (!loaded.total) {
52 | loaded.total = 0;
53 | }
54 |
55 | img.onload = null;
56 | if (++loaded.total >= Object.keys(images).length) {
57 | loaded.resolve(images);
58 | }
59 | };
60 | img.src = src;
61 | return img;
62 | }
63 |
64 | function clear() {
65 | ctx.clearRect(0, 0, canvas.width, canvas.height);
66 | return ctx;
67 | }
68 |
69 | function draw(image) {
70 | ctx.drawImage(image, 0, 0, canvas.width, canvas.height);
71 | }
72 |
73 | function renderState(tabId, state) {
74 | if (states[tabId] === 'error') {
75 | renderErrorState();
76 | } else if (images[state]) {
77 | clear();
78 | draw(images[state]);
79 | paintIcon(tabId);
80 | } else {
81 | console.warn('Unknown icon state:', state);
82 | }
83 | }
84 |
85 | function renderErrorState() {
86 | var tabs = Object.keys(states);
87 | var errTabs = tabs.filter(function(tabId) {
88 | return states[tabId] === 'error';
89 | });
90 |
91 | if (!errTabs.length) {
92 | return tabs.forEach(function(tabId) {
93 | renderState(tabId, states[tabId]);
94 | });
95 | }
96 |
97 | errState.pos = (errState.pos + errState.step) % PI2;
98 | var alpha = Math.cos(errState.pos) * 0.5 + 0.5;
99 |
100 | clear();
101 | ctx.save();
102 | ctx.globalAlpha = alpha;
103 | draw(images.error1);
104 | ctx.globalAlpha = 1 - alpha;
105 | draw(images.error2);
106 | ctx.restore();
107 |
108 | errTabs.forEach(paintIcon);
109 | setTimeout(renderErrorState, 16);
110 | }
111 |
112 | function paintIcon(tabId) {
113 | chrome.browserAction.setIcon({
114 | imageData: ctx.getImageData(0, 0, canvas.width, canvas.height),
115 | tabId: +tabId
116 | });
117 | }
--------------------------------------------------------------------------------
/styles/assets/toggler.css:
--------------------------------------------------------------------------------
1 | .toggler {
2 | display: inline-block;
3 | position: relative;
4 | overflow: hidden;
5 | font-size: 12px;
6 | width: 60px;
7 | height: 23px;
8 | line-height: 23px;
9 | }
10 |
11 | .toggler > input[type="checkbox"] {
12 | position: absolute;
13 | display: inline-block;
14 | -webkit-transform-origin: 0 0;
15 | -moz-transform-origin: 0 0;
16 | -ms-transform-origin: 0 0;
17 | -o-transform-origin: 0 0;
18 | transform-origin: 0 0;
19 |
20 | -webkit-transform: scale(6, 2);
21 | -moz-transform: scale(6, 2);
22 | -ms-transform: scale(6, 2);
23 | -o-transform: scale(6, 2);
24 | transform: scale(6, 2);
25 | z-index: 1;
26 | opacity: 0;
27 | margin: 0;
28 | padding: 0;
29 | }
30 |
31 | .toggler__bg {
32 | position: relative;
33 | display: inline-block;
34 | width: 100%;
35 | height: 100%;
36 | border-radius: 2em;
37 | color: #fff;
38 | background-color: #bdc3c7;
39 | overflow: hidden;
40 |
41 | -webkit-transition-property: background-color, color;
42 | -moz-transition-property: background-color, color;
43 | -ms-transition-property: background-color, color;
44 | -o-transition-property: background-color, color;
45 | transition-property: background-color, color;
46 |
47 | -webkit-transition-duration: 0.3s;
48 | -moz-transition-duration: 0.3s;
49 | -ms-transition-duration: 0.3s;
50 | -o-transition-duration: 0.3s;
51 | transition-duration: 0.3s;
52 | }
53 |
54 | .toggler__knob {
55 | display: inline-block;
56 | position: absolute;
57 | width: 17px;
58 | height: 17px;
59 | border-radius: 17px;
60 | line-height: 17px;
61 | top: 3px;
62 | left: 3px;
63 | background: #7f8c9a;
64 | font-style: normal;
65 |
66 | -webkit-transition-property: -webkit-transform, background-color;
67 | -moz-transition-property: -moz-transform, background-color;
68 | -ms-transition-property: -ms-transform, background-color;
69 | -o-transition-property: -o-transform, background-color;
70 | transition-property: transform, background-color;
71 |
72 | -webkit-transition-duration: 0.3s;
73 | -moz-transition-duration: 0.3s;
74 | -ms-transition-duration: 0.3s;
75 | -o-transition-duration: 0.3s;
76 | transition-duration: 0.3s;
77 | }
78 |
79 | .toggler__knob:before,
80 | .toggler__knob:after {
81 | display: inline-block;
82 | width: 2.2em;
83 | text-align: center;
84 | position: absolute;
85 | top: 0;
86 | }
87 |
88 | .toggler__knob:before {
89 | content: 'ON';
90 | right: 100%;
91 | margin-right: 4px;
92 | }
93 |
94 | .toggler__knob:after {
95 | content: 'OFF';
96 | left: 100%;
97 | margin-left: 4px;
98 | }
99 |
100 | input[type="checkbox"]:checked + .toggler__bg {
101 | background: #344a5d;
102 | color: #26bb9d;
103 | }
104 |
105 | input[type="checkbox"]:checked + .toggler__bg .toggler__knob {
106 | -webkit-transform: translateX(37px);
107 | -moz-transform: translateX(37px);
108 | -ms-transform: translateX(37px);
109 | -o-transform: translateX(37px);
110 | transform: translateX(37px);
111 | background: #26bb9d;
112 | }
--------------------------------------------------------------------------------
/scripts/controllers/error-tracker.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Keeps track of errors occurred LiveStyle activity.
3 | * Provides model with `error` boolean attribute indicating
4 | * if there’s something that user should be aware of.
5 | *
6 | * This controller tries to detect intermediate error states:
7 | * for example, when user type something he may accidentally
8 | * put stylesheet in error state but fix it later. In this case,
9 | * we shouldn’t trigger error state.
10 | */
11 | 'use strict';
12 |
13 | import Model from '../lib/model';
14 | import {debounce} from '../lib/utils';
15 |
16 | // Worker commads that commands may generate errors we should track
17 | var trackCommands = ['calculate-diff', 'apply-patch', 'initial-content'];
18 | var commandState = {};
19 | var errorFiles = [];
20 | var model = new Model();
21 |
22 | export default model;
23 |
24 | /**
25 | * Listens to events on given LiveStyle worker command queue
26 | * @param {CommandQueue} commandQueue
27 | */
28 | model.watch = function(commandQueue) {
29 | commandQueue.on('command-create command-reply', handleWorkerEvent);
30 | return this;
31 | };
32 |
33 | /**
34 | * Stops listening events on given LiveStyle worker
35 | * @param {CommandQueue} commandQueue
36 | */
37 | model.unwatch = function(commandQueue) {
38 | commandQueue.off('command-create command-reply', handleWorkerEvent);
39 | return this;
40 | };
41 |
42 | model.set({
43 | error: false,
44 | warning: false
45 | });
46 |
47 | var setErrorState = debounce(function() {
48 | if (errorFiles.length) {
49 | model.set('error', true);
50 | resetErrorState();
51 | }
52 | }, 2000);
53 |
54 | var resetErrorState = debounce(function() {
55 | model.set('error', false);
56 | errorFiles.length = 0;
57 | }, 30000);
58 |
59 |
60 | function markError(uri) {
61 | if (!~errorFiles.indexOf(uri)) {
62 | errorFiles.push(uri);
63 | setErrorState();
64 | }
65 | }
66 |
67 | function unmarkError(uri) {
68 | var ix = errorFiles.indexOf(uri);
69 | if (~ix) {
70 | errorFiles.splice(ix, 1);
71 | }
72 | }
73 |
74 | function handleWorkerEvent(payload) {
75 | if (!payload.commandId) {
76 | return;
77 | }
78 |
79 | if ('name' in payload && ~trackCommands.indexOf(payload.name)) {
80 | // a command request sent to worker
81 | commandState[payload.commandId] = {
82 | created: Date.now(),
83 | uri: payload.data.uri
84 | };
85 | } else if ('status' in payload && payload.commandId in commandState) {
86 | // a reply from worker on previous command request
87 | var state = commandState[payload.commandId];
88 | if (payload.status === 'error') {
89 | markError(state.uri);
90 | } else {
91 | unmarkError(state.uri);
92 | }
93 | delete commandState[payload.commandId];
94 | }
95 | }
96 |
97 | // Watch for hung states: ones we didn’t received reply on
98 | setInterval(function() {
99 | var end = Date.now() + 10000;
100 | Object.keys(commandState).forEach(function(id) {
101 | if (commandState[id].created < end) {
102 | delete commandState[id];
103 | }
104 | });
105 | }, 5000);
--------------------------------------------------------------------------------
/test/model.js:
--------------------------------------------------------------------------------
1 | var assert = require('assert');
2 | require('babel/register');
3 | var Model = require('../scripts/lib/livestyle-model');
4 |
5 | describe('LiveStyle Model', function() {
6 | it('get & set attributes', function() {
7 | var model = new Model();
8 | model.set('enabled', true);
9 | model.set('browserFiles', ['a.css', 'b.css']);
10 |
11 | assert.equal(model.get('enabled'), true);
12 | assert.deepEqual(model.get('browserFiles'), ['a.css', 'b.css']);
13 | });
14 |
15 | it('event dispatching', function() {
16 | var model = new Model();
17 | var update = 0, enabled = 0, browserFiles = 0;
18 | model.on('change:enabled', function() {
19 | enabled++;
20 | })
21 | .on('change:browserFiles', function() {
22 | browserFiles++;
23 | })
24 | .on('update', function() {
25 | // cumulative event
26 | update++;
27 | });
28 |
29 | model
30 | .set('enabled', true)
31 | .set('enabled', true)
32 | .set('browserFiles', ['a', 'b'])
33 | .set('browserFiles', ['a', 'b']);
34 |
35 | assert.equal(update, 1);
36 | assert.equal(enabled, 1);
37 | assert.equal(browserFiles, 1);
38 | });
39 |
40 | // it('global event dispatching', function() {
41 | // var m1 = new Model('m1');
42 | // var m2 = new Model('m2');
43 |
44 | // var changes = {}
45 | // Model.on('change:editorFiles update', function(model) {
46 | // if (!changes[model.id]) {
47 | // changes[model.id] = 0;
48 | // }
49 |
50 | // changes[model.id]++;
51 | // });
52 |
53 | // m1.set('editorFiles', ['a', 'b']);
54 | // m2.set('editorFiles', ['c', 'd']);
55 |
56 | // assert.deepEqual(changes, {m1: 2, m2: 2});
57 | // });
58 |
59 | it('file assocs', function() {
60 | var model = new Model();
61 | model.set('browserFiles', [
62 | '/assets/css/file1.css',
63 | '/assets/css/file2.css',
64 | '/assets/css/file3.css',
65 | '/assets/css/file4.css',
66 | '/assets/css/file5.css',
67 | '/assets/css/file6.css?v=123'
68 | ]);
69 |
70 | model.set('editorFiles', [
71 | '/assets/css/file1.css',
72 |
73 | '/files/css2/file2.css',
74 | '/files/css3/file2.css',
75 | '/files/css3/file5.less',
76 | '/files/css3/file6.scss',
77 |
78 | '/assets/css/foo.css'
79 | ]);
80 |
81 | model.set('assocs', {'/assets/css/file4.css': '/assets/css/foo.css'});
82 |
83 | var assocs = model.associations();
84 |
85 | // strict guessing
86 | assert.equal(assocs['/assets/css/file1.css'], '/assets/css/file1.css');
87 |
88 | // semi-strict guessing (match by extension)
89 | assert.equal(assocs['/assets/css/file5.css'], '/files/css3/file5.less');
90 |
91 | // semi-strict guessing (remove query sting)
92 | assert.equal(assocs['/assets/css/file6.css?v=123'], '/files/css3/file6.scss');
93 |
94 | // fuzzy guessing
95 | assert.equal(assocs['/assets/css/file2.css'], '/files/css2/file2.css');
96 |
97 | // no match
98 | assert.equal(assocs['/assets/css/file3.css'], undefined);
99 |
100 | // explicit association
101 | assert.equal(assocs['/assets/css/file4.css'], '/assets/css/foo.css');
102 | });
103 | });
--------------------------------------------------------------------------------
/styles/assets/remote-view.css:
--------------------------------------------------------------------------------
1 | .rv {
2 | background-color: #e57c2c;
3 | color: #fff;
4 | flex-grow: 0;
5 | flex-shrink: 0;
6 | z-index: 3;
7 | position: relative;
8 | transition: background-color 0.25s;
9 | }
10 |
11 | .rv-header, .rv-description {
12 | position: relative;
13 | overflow: hidden;
14 | font-size: 13px;
15 | line-height: 1.4;
16 | }
17 |
18 | .rv-header {
19 | position: relative;
20 | }
21 |
22 | .rv-title {
23 | font-size: 18px;
24 | padding: 7px 20px 2px;
25 | line-height: 22px;
26 | box-sizing: border-box;
27 | overflow: hidden;
28 | white-space: nowrap;
29 | text-overflow: ellipsis;
30 | }
31 |
32 | .rv-title a {
33 | color: inherit;
34 | text-decoration: none;
35 | font-size: 0.9em;
36 | }
37 |
38 | .rv-title a:hover {
39 | text-decoration: underline;
40 | }
41 |
42 | .rv-comment {
43 | color: #f1c22a;
44 | position: relative;
45 | font-size: 12px;
46 | padding: 0 20px 7px;
47 | overflow: hidden;
48 | box-sizing: border-box;
49 | }
50 |
51 | .rv-comment a {
52 | color: #ecf0f1;
53 | }
54 |
55 | .rv-comment a:hover {
56 | color: #fff;
57 | }
58 |
59 | .rv-message {
60 | position: relative;
61 | }
62 |
63 | .rv-learn-more {
64 | cursor: pointer;
65 | border-bottom: 1px dotted currentColor;
66 | color: #ecf0f1;
67 | }
68 |
69 | .rv-learn-more:hover {
70 | color: #fff;
71 | }
72 |
73 | .rv .toggler {
74 | float: right;
75 | margin-right: 17px;
76 | margin-top: 7px;
77 | }
78 |
79 | .rv .toggler__bg {
80 | background-color: #f29a26;
81 | }
82 |
83 | .rv .toggler__knob {
84 | background-color: #e57c2c;
85 | }
86 |
87 | .rv input[type="checkbox"]:checked + .toggler__bg {
88 | background-color: #fff;
89 | color: #e57c2c;
90 | }
91 |
92 | .rv input[type="checkbox"]:checked + .toggler__bg .toggler__knob {
93 | background-color: #e57c2c;
94 | }
95 |
96 | .rv-description {
97 | position: absolute;
98 | left: 0;
99 | right: 0;
100 | height: 0;
101 | overflow: hidden;
102 | padding: 0 20px;
103 | background-color: inherit;
104 | margin-top: -1px;
105 | }
106 |
107 | .rv-description a {
108 | color: #fff;
109 | }
110 |
111 | .rv-description em {
112 | font-style: normal;
113 | color: #f1c22a;
114 | }
115 |
116 | /* Unavailable state */
117 | .rv__unavailable {
118 | background-color: #bdc3c7;
119 | }
120 |
121 | .rv__unavailable .toggler {
122 | display: none;
123 | }
124 |
125 | .rv__unavailable .rv-comment {
126 | color: #ecf0f1;
127 | }
128 |
129 | /* Spinner */
130 | .rv-spinner {
131 | display: inline-block;
132 | margin-left: 5px;
133 | }
134 |
135 | .rv-spinner__item {
136 | display: inline-block;
137 | width: 4px;
138 | height: 4px;
139 | background: #fff;
140 | border-radius: 2px;
141 | vertical-align: middle;
142 | animation: rv-spinner 0.7s infinite;
143 | }
144 |
145 | .rv-spinner__item:nth-of-type(2) {
146 | animation-delay: 0.1s;
147 | }
148 |
149 | .rv-spinner__item:nth-of-type(3) {
150 | animation-delay: 0.2s;
151 | }
152 |
153 | @keyframes rv-spinner {
154 | from {
155 | transform: scale(0);
156 | opacity: 1;
157 | }
158 |
159 | to {
160 | transform: scale(2);
161 | opacity: 0;
162 | }
163 | }
--------------------------------------------------------------------------------
/scripts/helpers/shadow-css.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Shadow CSS is a concept used to bypass Chrome security restrictions for CSSOM:
3 | * if a stylesheet is loaded from different origin or 'file:' protocol, you cannot
4 | * access its `cssRules`. But `insertRule()` and `deleteRule()` works fine though.
5 | *
6 | * Here’s how Shadow CSS works:
7 | * 1. Loads contents of given (security restricted) stylesheet either from
8 | * DevTools resource (faster, contains most recent changes) or via XHR
9 | * (extensions can bypass CORS restrictions).
10 | * 2. Creates inline style with this stylesheet contents in hidden iframe. This
11 | * stylesheets always allows access to `cssRules`.
12 | * 3. Use this inline stylesheet for CSSOM patching. The patching will return
13 | * an update plan: a set of `insterRule()` and `deleteRule()` instructions that
14 | * must be applied to origin stylesheet to get the same result.
15 | * 4. Blindly apply this update plan to original stylesheet and hope everything
16 | * works as expected.
17 | * 5. Automatically keep track of all DevTools Resources updates and keep
18 | * shadow CSS in sync.
19 | */
20 | 'use strict';
21 |
22 | var host = null;
23 | var shadowCSS = {};
24 |
25 | export default function(url) {
26 | return new Promise(function(resolve, reject) {
27 | if (url in shadowCSS) {
28 | return resolve(shadowCSS[url].sheet);
29 | }
30 |
31 | // reject promise if no answer for too long
32 | var _timer = setTimeout(() => {
33 | reject(makeError('ESHADOWSTNORESPONSE', `Unable to fetch ${url}: no response from background page`));
34 | }, 5000);
35 |
36 | // fetch stylesheet contents first
37 | chrome.runtime.sendMessage({name: 'get-stylesheet-content', data: {url}}, resp => {
38 | if (_timer) {
39 | clearTimeout(_timer);
40 | _timer = null;
41 | }
42 |
43 | // A stylesheet may be already created with another request
44 | if (!shadowCSS[url]) {
45 | if (resp == null) {
46 | // `null` or `undefined` means error while fetching CSS contents,
47 | // try again later
48 | return reject(makeError('ESHADOWSTEMPTY', `Content fetch request for ${url} returned null`));
49 | }
50 |
51 | shadowCSS[url] = createShadowStylesheet(resp);
52 | shadowCSS[url].dataset.href = url;
53 | }
54 |
55 | resolve(shadowCSS[url].sheet);
56 | });
57 | });
58 | };
59 |
60 | function getHost() {
61 | if (!host) {
62 | var iframe = document.createElement('iframe');
63 | iframe.style.cssText = 'width:1px;height:1px;border:0;position:absolute;display:none';
64 | iframe.id = 'livestyle-shadow-css';
65 | var content = new Blob([''], {type: 'text/html'});
66 | iframe.src = URL.createObjectURL(content);
67 | document.body.appendChild(iframe);
68 | host = iframe.contentDocument;
69 | }
70 | return host;
71 | }
72 |
73 | function createShadowStylesheet(content) {
74 | var style = getHost().createElement('style');
75 | getHost().head.appendChild(style);
76 | if (style.sheet) {
77 | style.sheet.disabled = true;
78 | }
79 | style.textContent = content || '';
80 | return style;
81 | }
82 |
83 | function makeError(code, message) {
84 | var err = new Error(message || code);
85 | err.code = code;
86 | return err;
87 | }
88 |
89 | // listen to DevTools Resource updates
90 | chrome.runtime.onMessage.addListener(function(message) {
91 | if (message && message.name === 'resource-updated' && shadowCSS[message.data.url]) {
92 | var data = message.data;
93 | shadowCSS[data.url].textContent = data.content;
94 | }
95 | });
--------------------------------------------------------------------------------
/scripts/lib/event-emitter.js:
--------------------------------------------------------------------------------
1 | /**
2 | * A simple event emitter, borrowed from Backbone.Event.
3 | */
4 | 'use strict';
5 |
6 | // Regular expression used to split event strings
7 | var eventSplitter = /\s+/;
8 |
9 | // Create a local reference to slice/splice.
10 | var slice = Array.prototype.slice;
11 |
12 | export default class EventEmitter {
13 | /**
14 | * Bind one or more space separated events, `events`, to a `callback`
15 | * function. Passing `"all"` will bind the callback to all events fired.
16 | * @param {String} events
17 | * @param {Function} callback
18 | * @param {Object} context
19 | * @memberOf eventDispatcher
20 | */
21 | on(events, callback, context) {
22 | var calls, event, node, tail, list;
23 | if (!callback)
24 | return this;
25 |
26 | events = events.split(eventSplitter);
27 | calls = this._callbacks || (this._callbacks = {});
28 |
29 | // Create an immutable callback list, allowing traversal during
30 | // modification. The tail is an empty object that will always be used
31 | // as the next node.
32 | while (event = events.shift()) {
33 | list = calls[event];
34 | node = list ? list.tail : {};
35 | node.next = tail = {};
36 | node.context = context;
37 | node.callback = callback;
38 | calls[event] = {
39 | tail : tail,
40 | next : list ? list.next : node
41 | };
42 | }
43 |
44 | return this;
45 | }
46 |
47 | /**
48 | * Remove one or many callbacks. If `context` is null, removes all
49 | * callbacks with that function. If `callback` is null, removes all
50 | * callbacks for the event. If `events` is null, removes all bound
51 | * callbacks for all events.
52 | * @param {String} events
53 | * @param {Function} callback
54 | * @param {Object} context
55 | */
56 | off(events, callback, context) {
57 | var event, calls, node, tail, cb, ctx;
58 |
59 | // No events, or removing *all* events.
60 | if (!(calls = this._callbacks))
61 | return;
62 | if (!(events || callback || context)) {
63 | delete this._callbacks;
64 | return this;
65 | }
66 |
67 | // Loop through the listed events and contexts, splicing them out of the
68 | // linked list of callbacks if appropriate.
69 | events = events ? events.split(eventSplitter) : _.keys(calls);
70 | while (event = events.shift()) {
71 | node = calls[event];
72 | delete calls[event];
73 | if (!node || !(callback || context))
74 | continue;
75 | // Create a new list, omitting the indicated callbacks.
76 | tail = node.tail;
77 | while ((node = node.next) !== tail) {
78 | cb = node.callback;
79 | ctx = node.context;
80 | if ((callback && cb !== callback) || (context && ctx !== context)) {
81 | this.on(event, cb, ctx);
82 | }
83 | }
84 | }
85 |
86 | return this;
87 | }
88 |
89 | /**
90 | * Trigger one or many events, firing all bound callbacks. Callbacks are
91 | * passed the same arguments as `emit` is, apart from the event name
92 | * (unless you're listening on `"all"`, which will cause your callback
93 | * to receive the true name of the event as the first argument).
94 | * @param {String} events
95 | */
96 | emit(events) {
97 | var event, node, calls, tail, args, all, rest;
98 | if (!(calls = this._callbacks)) {
99 | return this;
100 | }
101 | all = calls.all;
102 | events = events.split(eventSplitter);
103 | rest = slice.call(arguments, 1);
104 |
105 | // For each event, walk through the linked list of callbacks twice,
106 | // first to trigger the event, then to trigger any `"all"` callbacks.
107 | while (event = events.shift()) {
108 | if (node = calls[event]) {
109 | tail = node.tail;
110 | while ((node = node.next) !== tail) {
111 | node.callback.apply(node.context || this, rest);
112 | }
113 | }
114 | if (node = all) {
115 | tail = node.tail;
116 | args = [ event ].concat(rest);
117 | while ((node = node.next) !== tail) {
118 | node.callback.apply(node.context || this, args);
119 | }
120 | }
121 | }
122 |
123 | return this;
124 | }
125 | }
--------------------------------------------------------------------------------
/scripts/lib/utils.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | export function $(selector, context=document) {
4 | return context.querySelector(selector);
5 | }
6 |
7 | export function $$(selector, context=document) {
8 | return toArray(context.querySelectorAll(selector));
9 | }
10 |
11 | export function toArray(obj, ix=0) {
12 | return Array.prototype.slice.call(obj, 0);
13 | }
14 |
15 | export function toDom(html) {
16 | var div = document.createElement('div');
17 | div.innerHTML = html;
18 | var result = div.firstChild;
19 | div.removeChild(result);
20 | return result;
21 | }
22 |
23 | /**
24 | * Extend given object with properties from other objects
25 | * @param {Object} obj
26 | * @return {Object}
27 | */
28 | export function extend(obj, ...args) {
29 | args.forEach(arg => {
30 | if (arg) {
31 | for (var key in arg) if (arg.hasOwnProperty(key)) {
32 | obj[key] = arg[key];
33 | }
34 | }
35 | });
36 | return obj;
37 | }
38 |
39 | export function copy(...args) {
40 | return extend({}, ...args);
41 | }
42 |
43 | export function closest(elem, sel) {
44 | while (elem && elem !== document) {
45 | if (elem.matches && elem.matches(sel)) {
46 | return elem;
47 | }
48 | elem = elem.parentNode;
49 | }
50 | }
51 |
52 | /**
53 | * Returns copy of given array with unique values
54 | * @param {Array} arr
55 | * @return {Array}
56 | */
57 | export function unique(arr) {
58 | var lookup = [];
59 | return arr.filter(val => {
60 | if (lookup.indexOf(val) < 0) {
61 | lookup.push(val);
62 | return true;
63 | }
64 | });
65 | }
66 |
67 | /**
68 | * Returns a function, that, as long as it continues to be invoked, will not
69 | * be triggered. The function will be called after it stops being called for
70 | * N milliseconds. If `immediate` is passed, trigger the function on the
71 | * leading edge, instead of the trailing.
72 | *
73 | * @src underscore.js
74 | *
75 | * @param {Function} func
76 | * @param {Number} wait
77 | * @param {Boolean} immediate
78 | * @return {Function}
79 | */
80 | export function debounce(func, wait, immediate) {
81 | var timeout, args, context, timestamp, result;
82 |
83 | var later = function() {
84 | var last = Date.now() - timestamp;
85 |
86 | if (last < wait && last >= 0) {
87 | timeout = setTimeout(later, wait - last);
88 | } else {
89 | timeout = null;
90 | if (!immediate) {
91 | result = func.apply(context, args);
92 | if (!timeout) context = args = null;
93 | }
94 | }
95 | };
96 |
97 | return function() {
98 | context = this;
99 | args = arguments;
100 | timestamp = Date.now();
101 | var callNow = immediate && !timeout;
102 | if (!timeout) timeout = setTimeout(later, wait);
103 | if (callNow) {
104 | result = func.apply(context, args);
105 | context = args = null;
106 | }
107 |
108 | return result;
109 | };
110 | }
111 |
112 | /**
113 | * Returns string representation for given node path
114 | * @param {Array} nodePath
115 | * @type {String}
116 | */
117 | export function stringifyPath(nodePath) {
118 | return nodePath.map(c => c[0] + (c[1] > 1 ? '|' + c[1] : '')).join(' / ');
119 | }
120 |
121 | /**
122 | * Returns string representation of given patch JSON
123 | * @param {Object} patch
124 | * @type {String}
125 | */
126 | export function stringifyPatch(patch) {
127 | var str = this.stringifyPath(patch.path) + ' {\n' +
128 | patch.update.map(prop => ` ${prop.name}: ${prop.value};\n`).join('') +
129 | patch.remove.map(prop => ` /* ${prop.name}: ${prop.value}; */\n`).join('') +
130 | '}';
131 |
132 | if (patch.action === 'remove') {
133 | str = '/* remove: ' + this.stringifyPath(patch.path) + ' */';
134 | }
135 |
136 | if (patch.hints && patch.hints.length) {
137 | var hint = patch.hints[patch.hints.length - 1];
138 | var self = this;
139 |
140 | var before = (hint.before || []).map(function(p) {
141 | return self.stringifyPath([p]);
142 | }).join(' / ');
143 |
144 | var after = (hint.after || []).map(function(p) {
145 | return self.stringifyPath([p]);
146 | }).join(' / ');
147 |
148 | if (before) {
149 | str = `/** before: ${before} */\n${str}`;
150 | }
151 |
152 | if (after) {
153 | str += `\n/** after: ${after} */\n`;
154 | }
155 | }
156 |
157 | return str.trim();
158 | }
--------------------------------------------------------------------------------
/scripts/controllers/devtools.js:
--------------------------------------------------------------------------------
1 | /**
2 | * A DevTools controller for background page.
3 | *
4 | * For generic CSS patching extension uses CSSOM
5 | * which is very fast even on large sources. The problem is
6 | * that these changes in CSSOM are not reflected into original
7 | * source, e.g. in DevTools you’ll still see unchanges properties.
8 | * Moreover, any change in DevTools will reset all CSSOM changes.
9 | *
10 | * This module keeps track of all pending diffs for tabs and
11 | * when DevTools for tab became available, it flushes these
12 | * changes to DevTools page so it can apply diffs on page resources.
13 | */
14 | 'use strict';
15 |
16 | import portExpect from '../lib/port-expect';
17 |
18 | var openedDevtools = {};
19 | var pendingPatches = {};
20 |
21 | var devtoolsPort = /^devtools\-page:(\d+)$/;
22 |
23 | export function saveDiff(tabId, stylesheetUrl, patches) {
24 | if (isOpenedForTab(tabId)) {
25 | // we have opened DevTools for this tab,
26 | // send diff directly to it
27 | console.log('DevTools opened, send diff directly');
28 | return getPort(tabId).postMessage({
29 | name: 'diff',
30 | data: {
31 | uri: stylesheetUrl,
32 | syntax: 'css', // always CSS
33 | patches: patches
34 | }
35 | });
36 | }
37 |
38 | // no opened DevTools, accumulate changes
39 | if (!pendingPatches[tabId]) {
40 | pendingPatches[tabId] = {};
41 | }
42 |
43 | if (!pendingPatches[tabId][stylesheetUrl]) {
44 | pendingPatches[tabId][stylesheetUrl] = [];
45 | }
46 |
47 | console.log('Append patches for', stylesheetUrl);
48 | pendingPatches[tabId][stylesheetUrl] = pendingPatches[tabId][stylesheetUrl].concat(patches);
49 | }
50 |
51 | export function getPort(tabId) {
52 | if (typeof tabId === 'object') {
53 | tabId = tabId.id;
54 | }
55 |
56 | return openedDevtools[tabId];
57 | }
58 |
59 | export function isOpenedForTab(tabId) {
60 | return !!getPort(tabId);
61 | }
62 |
63 | /**
64 | * Resets current DevTools state for given tab id
65 | */
66 | export function reset(tabId) {
67 | var port = getPort(tabId);
68 | if (port) {
69 | port.postMessage({name: 'reset'});
70 | }
71 | }
72 |
73 | export function stylesheets(tabId, callback) {
74 | if (!this.isOpenedForTab(tabId)) {
75 | return callback([]);
76 | }
77 |
78 | return portExpect(getPort(tabId), 'get-stylesheets', 'stylesheets')
79 | .then(callback, err => callback([]));
80 | }
81 |
82 | export function stylesheetContent(tabId, url) {
83 | if (!this.isOpenedForTab(tabId)) {
84 | return callback([]);
85 | }
86 |
87 | return portExpect(getPort(tabId), 'get-stylesheet-content', {url}, 'stylesheet-content')
88 | .then(resp => resp.content);
89 | }
90 |
91 | function normalizeUrl(url) {
92 | return url.split('#')[0];
93 | }
94 |
95 | /**
96 | * Show log messages coming from DevTools
97 | * @param {Array} strings Array of string
98 | */
99 | function devtoolsLog(strings) {
100 | var args = ['%c[DevTools]', 'background-color:#344a5d;color:#fff'].concat(strings);
101 | console.log.apply(console, args);
102 | }
103 |
104 | /**
105 | * Handles incoming messages from DevTools connection port
106 | * @param {Object} message Incoming message
107 | */
108 | function devtoolsMessageHandler(tabId, message) {
109 | if (message.name === 'log') {
110 | devtoolsLog(message.data);
111 | } else if (message.name === 'resource-updated') {
112 | // notify tabs about updates resources
113 | chrome.tabs.sendMessage(tabId, message);
114 | }
115 | }
116 |
117 | function resetPatches(tabId) {
118 | if (tabId in pendingPatches) {
119 | delete pendingPatches[tabId];
120 | }
121 | }
122 |
123 | chrome.runtime.onConnect.addListener(function(port) {
124 | var m = port.name.match(devtoolsPort);
125 | if (m) {
126 | var tabId = +m[1];
127 | openedDevtools[tabId] = port;
128 | console.log('Opened devtools for', tabId);
129 |
130 | if (tabId in pendingPatches) {
131 | // flush pending patches
132 | port.postMessage({
133 | name: 'pending-patches',
134 | data: pendingPatches[tabId]
135 | });
136 | delete pendingPatches[tabId];
137 | }
138 |
139 | var messageHandler = message => {
140 | devtoolsMessageHandler(tabId, message);
141 | };
142 |
143 | port.onMessage.addListener(messageHandler);
144 |
145 | port.onDisconnect.addListener(function() {
146 | console.log('Closed devtools for', tabId);
147 | delete openedDevtools[tabId];
148 | port.onMessage.removeListener(messageHandler);
149 | });
150 | }
151 | });
152 |
153 | // cleanup patches when tab is closed or refreshed
154 | chrome.tabs.onRemoved.addListener(resetPatches);
155 | chrome.tabs.onUpdated.addListener(resetPatches);
--------------------------------------------------------------------------------
/scripts/ui/select-box.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | import {$$, closest} from '../lib/utils';
4 | var boxes = [];
5 |
6 | export function init(container) {
7 | $$('select', container).forEach(convert);
8 | }
9 |
10 | export function find(sel) {
11 | var matchedBox = null;
12 | boxes.some(function(box) {
13 | if (box._sel === sel) {
14 | return matchedBox = box;
15 | }
16 | });
17 |
18 | return matchedBox;
19 | };
20 |
21 | export function convert(sel) {
22 | if (!sel.getAttribute('data-select-box')) {
23 | return new SelectBox(sel);
24 | }
25 |
26 | return find(sel);
27 | };
28 |
29 | export function sync(sel) {
30 | var box = find(sel);
31 | if (box) {
32 | return box.sync();
33 | }
34 | }
35 |
36 | /**
37 | * Creates custom select box from given