Skip to content

Commit

Permalink
- use about:restartrequired when prompting for restarts (#17)
Browse files Browse the repository at this point in the history
- make restart notifications optional (#18)
- make style updates slightly more efficient
- handle subframes differently
  • Loading branch information
NiklasGollenstede committed Oct 22, 2018
1 parent ab3bf72 commit 6f50016
Show file tree
Hide file tree
Showing 16 changed files with 218 additions and 90 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ The add-on contains instructions on how to do that easily.
- "Exchange messages with programs other than Firefox": Use NativeExt if installed. Useless otherwise.
- "Display notifications to you": Success messages after user actions, error messages. <!-- Optional for --> Status changes.
- "Access recently closed tabs": Under some rare conditions, reStyle needs to open temporary popups. This is used to remove them from the history after they are closed.
<!-- - "bookmarks": create a bookmark to <code>about:restartrequired</code> if requested. -->


<b>Implementation status</b>
Expand Down
14 changes: 12 additions & 2 deletions background/badge.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
(function(global) { 'use strict'; define(async ({ // This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
'node_modules/web-ext-utils/browser/': { browserAction, Tabs, rootUrl, },
'node_modules/web-ext-utils/browser/storage': { local: Storage, },
'node_modules/web-ext-utils/browser/version': { fennec, },
'node_modules/web-ext-utils/loader/views': { getUrl, openView, },
'node_modules/web-ext-utils/utils/notify': notify,
'node_modules/es6lib/functional': { debounce, },
'common/options': options,
'views/': _, // put views in tabview
'./chrome/': ChromeStyle,
'./util': { isSubDomain, },
Expand All @@ -17,6 +19,9 @@

//// start implementation

let notifyChange = true; options.chrome.children.notifyChange.whenChange(([ value, ]) => (notifyChange = value));
const wantedRestart = Storage.get('wantedRestart'); Storage.set('wantedRestart', false);

// browser_action (can not be set in manifest due to fennec incompatibility)
browserAction.setPopup({ popup: getUrl({ name: 'panel', }).slice(rootUrl.length - 1).replace('#', '?w=350&h=250#'), });
fennec && browserAction.onClicked.addListener(() => openView('panel'));
Expand All @@ -36,7 +41,7 @@ Tabs.onActivated.addListener(({ tabId, }) => setBage(tabId));
Tabs.onUpdated.addListener((tabId, change, { active, }) => active && ('url' in change) && setBage(tabId, change.url));
async function setBage(tabId, url) {
if (arguments.length === 0) { // update all visible
return void (await Tabs.query({ active: true, })).forEach(({ id, url, }) => setBage(id, url));
(await Promise.all((await Tabs.query({ active: true, })).map(({ id, url, }) => setBage(id, url)))); return;
}
url = new global.URL(url || (await Tabs.get(tabId)).url);
let matching = 0, extra = 0; for (const [ , style, ] of Style) {
Expand All @@ -56,14 +61,19 @@ const colors = { normal: [ 0x00, 0x7f, 0x00, 0x60, ], restart: [ 0xa5, 0x50, 0x0
browserAction.setBadgeBackgroundColor({ color: colors.normal, });
let wasChanged = false; ChromeStyle.onWritten(changed => {
browserAction.setBadgeBackgroundColor({ color: changed ? colors.restart : colors.normal, });
(changed || wasChanged) && (changed ? notify.warn : notify.info)( // info or warn would be more appropriate than error ...
notifyChange && (changed || wasChanged) && (changed ? notify.warn : notify.info)( // info or warn would be more appropriate than error ...
`The UI styles were written`, changed
? `and have changed. The browser must be restarted to apply the changes!`
: `and changed back. No need to restart the browser anymore!`
);
wasChanged = changed; setBage();
Storage.set('wantedRestart', changed);
});

// i.e. clicked the `Restart` button on `about:restartrequired` on behalf of reStyle
wantedRestart && (await Tabs.query({ active: true, currentWindow: true, }))[0].url === 'about:restartrequired' // (can't filter by { url: 'about:restartrequired', })
&& Tabs.update({ loadReplace: true, url: getUrl({ name: 'restarted', }), });

return { update: setBage, };

}); })(this);
19 changes: 14 additions & 5 deletions background/chrome/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,17 @@ class ChromeStyle {
constructor(path, chrome, content) {
styles.add(this);
this.path = path;
// the sheets are loaded with origin 'user', which means their priority is below 'author' sheets unless they are !important, seee: https://developer.mozilla.org/en-US/docs/Web/CSS/Cascade#Cascading_order
// that means they are pretty useless unless they are !important ==> add that to all rules
this.chrome = (chrome || '').toString({ minify: false, important: true, namespace: false, }).trim().replace(/\r\n?/g, '\n');
this.content = (content || '').toString({ minify: false, important: true, namespace: false, }).trim().replace(/\r\n?/g, '\n');
this.sheets = { chrome, content, };
this.chrome = stringify(chrome);
this.content = stringify(content);
applyStyles();
}

update(chrome, content) {
chrome = stringify(chrome); content = stringify(content);
if (this.chrome === chrome && this.content === content) { return; }
this.chrome = chrome; this.content = content; applyStyles();
}

static get changed() { return changed; }

destroy() {
Expand Down Expand Up @@ -66,6 +69,12 @@ const fireWritten = setEvent(ChromeStyle, 'onWritten', { lazy: false, async: tru

//// start implementation

function stringify(sheet) {
// the sheets are loaded with origin 'user', which means their priority is below 'author' sheets unless they are !important, seee: https://developer.mozilla.org/en-US/docs/Web/CSS/Cascade#Cascading_order
// that means they are pretty useless unless they are !important ==> add that to all rules
return (sheet || '').toString({ minify: false, important: true, namespace: false, }).trim();
}

// This is unique for each (firefox) extension installation and makes sure that prefix, infix and suffix are unpredictable for the style authors
// and thus won't occur in the file on accident (or intentionally) where they don't belong.
const uuid = rootUrl.slice('moz-extension://'.length, -1);
Expand Down
2 changes: 1 addition & 1 deletion background/local/native.js
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ async function readAndWatch(include, dir, onChange, create) {
const path = Path.join(dir, name);
if (!name || (/^\.|\/\./).test(name) || !include.test(path)) { return; }
let stat; try { stat = (await get(FS.stat, path)); } catch (_) { }
if (!stat) { return void onChange(path, null); } // deleted
if (!stat) { onChange(path, null); return; } // deleted
const file = (await get(FS.readFile, path, 'utf8'));
if (file === data[path]) { return /*void console.log('no change', name)*/; } // ???
onChange(path, (data[path] = file));
Expand Down
38 changes: 27 additions & 11 deletions background/parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ class Sheet {
*/
static fromCode(code, options) { return Sheet_fromCode(code, options); }

// static extractMeta(code, options) { return Sheet_extractMeta(code, options); }

/**
* Transforms `userstyles.org`s JSON sheets into plain CSS code. Normalizes newlines.
* @param {string} json JSON response from `userstyles.orgs` API.
Expand Down Expand Up @@ -79,26 +81,35 @@ class Sheet {
* Parsed `@document` block within a `Sheet`. Exported as `Sheet.Section`.
* All include rules are set as interpreted strings, e.g.
* a literal `@regexp("a\\b\/c\.d")` would result in a `a\b/c.d` entry in `.regexp`.
* Except for `dynamic` all properties should be treated as recursively read-only.
* @property {[{value,as,}]} urls `url()` document include rules.
* @property {[{value,as,}]} urlPrefixes `url-prefixe()` document include rules.
* @property {[{value,as,}]} domains `domain()` document include rules.
* @property {[{value,as,}]} regexps `regexp()` document include rules.
* @property {[{value,as,}]} dynamic Dynamically configurable document include rules, like regexps.
* @property {[int,int]?} location For `Section`s directly parsed by `Section.fromCode` this
* is their location within the original source code string.
* @property {[string]} tokens Array of code tokens. For non-global sections everything between the curly brackets,
* for global sections everything between the closing bracket and the `@` of the next block.
* @property {[{value,as,}]} dynamic Dynamically configurable document include rules, like `regexps`.
* @property {[int,int]?} location For `Section`s directly parsed by `Section.fromCode` this is the string index
* of the first and past the last char of `.tokens` in the original source.
* @property {boolean} global Whether this is a section of global rules outside a `@document` block.
*/
class Section {

/// Constructs a `Sheet` from its components.
constructor(urls = [ ], urlPrefixes = [ ], domains = [ ], regexps = [ ], tokens = [ ], location = null, dynamic = [ ]) {
constructor(urls = [ ], urlPrefixes = [ ], domains = [ ], regexps = [ ], tokens = [ ], location = null, dynamic = [ ], global = false, _empty = undefined) {
this.urls = urls; this.urlPrefixes = urlPrefixes; this.domains = domains; this.regexps = regexps;
this.tokens = tokens; this.location = location; this.dynamic = dynamic;
this.tokens = tokens; this.location = location; this.dynamic = dynamic; this.global = global; this._empty = _empty;
}

cloneWithoutIncludes() { return new Section([ ], [ ], [ ], [ ], this.tokens, this.location, [ ]); }
cloneWithoutIncludes() { const self = Section.fromJSON(this); {
self.urls = [ ]; self.urlPrefixes = [ ]; self.domains = [ ]; self.regexps = [ ]; self.dynamic = [ ];
} return self; }

/// Returns `true` iff the `Section`s code consists of only comments and whitespaces.
isEmpty() { return !this.tokens.some(_=>!(/^\s*$|^\/\*/).test(_)); }
isEmpty() {
if (this._empty !== undefined) { return this._empty; }
return (this._empty = !this.tokens.some(_=>!(/^\s*$|^\/\*(?!!?\[\[)/).test(_)));
}

/// Copies the current values of all options occurring in this `Section`s `.tokens` to `get`
/// and replaces them by the values in `set`. Works with { [name]: value, } objects.
Expand All @@ -125,8 +136,8 @@ class Section {

toJSON() { return Object.assign({ }, this); }

static fromJSON({ urls, urlPrefixes, domains, regexps, tokens, dynamic, }) {
return new Section(urls, urlPrefixes, domains, regexps, tokens, dynamic);
static fromJSON({ urls, urlPrefixes, domains, regexps, tokens, dynamic, global, _empty, }) {
return new Section(urls, urlPrefixes, domains, regexps, tokens, dynamic, global, _empty);
}
}
Sheet.Section = Section;
Expand Down Expand Up @@ -209,7 +220,7 @@ function Sheet_fromCode(css, { onerror, } = { }) {
case 'description': value = value.replace(/^\s*\n/, ' |\n'); break;
case 'var': case 'advanced': (meta.vars || (meta.vars = [ ])).push(tokenize(value)); continue;
}
if (!parse(key +':'+ value) && !meta.hasOwnProperty(key)) { meta[key] = value.trim(); }
if (!parse(key +':'+ value) && !Object.hasOwnProperty.call(meta, key)) { meta[key] = value.trim(); }
}
function parse(string) { try { Object.assign(meta, YAML.parse(string)); return true; } catch (error) { onerror(error); return false; } }
} else {
Expand All @@ -224,6 +235,10 @@ function Sheet_fromCode(css, { onerror, } = { }) {
return self;
}

// function Sheet_extractMeta(css, { onerror, } = { }) {
// ...
// }

const rsFuzzyTitle = [
RegExpX('mi')`^ # in any line
[\ \t]*?\*[\ \t]* (?: # indention
Expand Down Expand Up @@ -300,7 +315,8 @@ function Section_swapOptions(get, set) {
if (set && (name in set)) {
const now = tokenize(set[name]);
const old = tokens.splice(start + 1, i - start - 1, ...now);
i += now.length - old.length;
const diff = now.length - old.length;
i += diff; l += diff;
}
start = -1; name = null;
} else { const name = match[2]; if (set && (name in set)) {
Expand Down
51 changes: 26 additions & 25 deletions background/style.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ class Style {
* Forcefully re-applies the current Sheet,
* e.g. to replace old cached `.chrome` and `.web` properties.
*/
reload() { const self = Self.get(this); !self.disabled && self.update(); }
reload() { const self = Self.get(this); !self.disabled && self.apply(); }

/// The `url` constructor parameter.
get url() { return Self.get(this).url; }
Expand Down Expand Up @@ -190,11 +190,11 @@ class _Style {
if (code === this.code) { return false; }
this.code = code; this._sheet = null;

this.update(); return true;
this.apply(); return true;
}

/// applies the `._sheet?` to the UI and browser
update() {
apply() {
if (this.disabled) { this.parseIncludes(); this._fireChanged(); return; }
const sheet = this._sheet = this._sheet || Meta.normalize(Sheet.fromCode(this.code), this.url);
this.updateName(); this.meta = deepFreeze(sheet.meta);
Expand Down Expand Up @@ -231,7 +231,7 @@ class _Style {
/// refreshes `._chrome`, `._content` and `._web` from `._sheet?`
parseSections() {
const { sections, namespace, } = this._sheet = this._sheet || Meta.normalize(Sheet.fromCode(this.code), this.url);
const userChrome = this._chrome = [ ]; const userContent = this._content = [ ]; const webContent = this._web = [ ];
const userChrome = [ ], userContent = [ ], webContent = [ ];
sections.forEach(section => {
const { urls, urlPrefixes, domains, regexps, } = section;

Expand Down Expand Up @@ -272,6 +272,9 @@ class _Style {
content.urls.length + content.urlPrefixes.length + content.domains.length + content.regexps.length > 0 && userContent.push(content);
web.urls.length + web.urlPrefixes.length + web.domains.length + web.regexps.length > 0 && webContent.push(web);
});
this._chrome = this._sheet.cloneWithSections(userChrome);
this._content = this._sheet.cloneWithSections(userContent);
this._web = this._sheet.cloneWithSections(webContent);
}

/// (re-)builds `.options.children[includes|options].children` settings branches from `this.meta`
Expand All @@ -286,10 +289,10 @@ class _Style {
checks: { balancedBrackets: Sheet.checkBalancedBrackets, },
});
const option = root.children[0]; parent.set(option, root);
option.onChange(() => {
option.onChange((_, __, option) => {
this.disabled = false;
!this._sheet && this.parseSections(); // restored from JSON
this[onChange](); this.applyStyle();
this[onChange](option); this.applyStyle();
});
return root.children[0];
}))
Expand All @@ -302,13 +305,13 @@ class _Style {
return { dynamicIncludes, dynamicOptions, };
}

/// applies the `.options.children.include.children` settings branch to `._sheet.sections` and `._web`
/// applies the `.options.children.include.children` settings branch to `._sheet.sections`, `._content` and `._web`
applyIncludes() {
const { _sheet: { sections, }, _web: webContent, _content: userContent, } = this;
const dynamicIncludes = this.options.children.include.children;
sections.forEach(section => Object.entries({
web: webContent, content: userContent,
}).forEach(([ type, targets, ]) => {
}).forEach(([ type, { sections: targets, }, ]) => {
const target = targets.find(_=>_.tokens === section.tokens) || section.cloneWithoutIncludes();
const hadDynamic = target.dynamic.splice(0, Infinity).length;
section.regexps.forEach(({ value: source, as, }) => {
Expand All @@ -328,34 +331,32 @@ class _Style {
}

/// applies the `.options.children.options.children` settings branch to `._sheet.sections`
applyOptions() {
const { _sheet: { sections, }, } = this;
const dynamicOptions = this.options.children.options.children;
applyOptions(changedOption) {
const { _sheet: sheet, } = this;
const dynamicOptions = changedOption ? [ changedOption, ] : this.options.children.options.children;
const prefs = { }; dynamicOptions.forEach(
({ name, value, model: { unit, }, }) => (prefs[name] = value + (unit || ''))
);
sections.forEach(_=>_.setOptions(prefs));
); sheet.swapOptions(null, prefs);
}

/// applies `._sheet`, `._chrome`, `._content` and `._web` to the UI and browser
/// (re-)applies `._chrome`, `._content` and `._web` to the UI and browser
applyStyle() {
const { _sheet: sheet, _chrome: userChrome, _content: userContent, _web: webContent, } = this;
const chrome = !userChrome.length && !userContent.length ? null
: new ChromeStyle(this.url,
userChrome.length ? sheet.cloneWithSections(userChrome) : null,
userContent.length ? sheet.cloneWithSections(userContent) : null,
); this.chrome && this.chrome.destroy(); this.chrome = chrome;

const web = !webContent.length ? null
: new WebStyle(this.url, sheet.cloneWithSections(webContent));
this.web && this.web.destroy(); this.web = web;
const { _chrome: userChrome, _content: userContent, _web: webContent, } = this;
[
[ 'chrome', ChromeStyle, userChrome.sections.length || userContent.sections.length ? [ userChrome, userContent, ] : null, ],
[ 'web', WebStyle, webContent.sections.length ? [ webContent, ] : null, ],
].forEach(([ key, Type, args, ]) => {
if (this[key] && args) { this[key].update(...args); }
else if (args) { this[key] = new Type(this.url, ...args); }
else if (this[key]) { this[key].destroy(); }
});

this._fireChanged();
}

enable() {
if (this.disabled === false) { return; } this.disabled = false;
this.update();
this.apply();
}
disable(supress) {
if (this.disabled === true) { return; } this.disabled = true;
Expand Down
1 change: 0 additions & 1 deletion background/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ const oEsc = { '&': '&amp;', '<': '&lt;', '>': '&gt;', "'": '&#39;', '"': '&quot
const rEsc = new RegExp('['+ Object.keys(oEsc).join('') +']', 'g');

async function sha1(string) {
typeof string !== 'string' && (string = JSON.stringify(string)); // for the styles loaded from https://userstyles.org/styles/chrome/\d+.json
const hash = (await global.crypto.subtle.digest('SHA-1', new global.TextEncoder('utf-8').encode(string)));
return Array.from(new Uint8Array(hash)).map((b => b.toString(16).padStart(2, '0'))).join('');
}
Expand Down
Loading

0 comments on commit 6f50016

Please sign in to comment.