Skip to content

Commit

Permalink
Refactor: rewrite style injector (#664)
Browse files Browse the repository at this point in the history
* Refactor: style injector/docRootObserver/docRewriteObserver

* Fix: minor

* Fix: disabled state

* Fix: use evade

* Fix: apply.js is broken in our pages

* Fix: transition patch is broken

* Fix: also check elements after the last userstyle

* Fix: remove outdated FIXME. styleInjector.toggle now toggle all styles

* Fix: call Object.keys twice

* Add a fixme

* Fix: typo

* Add a fixme

* Fix: don't argue for mutations generated by other extensions
  • Loading branch information
eight04 authored and Mottie committed Mar 10, 2019
1 parent 744bf01 commit b40849a
Show file tree
Hide file tree
Showing 8 changed files with 339 additions and 343 deletions.
481 changes: 139 additions & 342 deletions content/apply.js

Large diffs are not rendered by default.

187 changes: 187 additions & 0 deletions content/style-injector.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
/* exported createStyleInjector */
'use strict';

function createStyleInjector({compare, setStyleContent, onUpdate}) {
const CHROME = chrome.app ? parseInt(navigator.userAgent.match(/Chrom\w+\/(?:\d+\.){2}(\d+)|$/)[1]) : NaN;
const PREFIX = 'stylus-';
// styles are out of order if any of these elements is injected between them
const ORDERED_TAGS = new Set(['head', 'body', 'frameset', 'style', 'link']);
const list = [];
const table = new Map();
let enabled = true;
return {
// manipulation
add,
addMany,
remove,
update,
clear,
replaceAll,

// method
toggle,
sort,

// state
outOfOrder,
list,

// static util
createStyle
};

function outOfOrder() {
if (!list.length) {
return false;
}
let el = list[0].el;
if (el.parentNode !== document.documentElement) {
return true;
}
let i = 0;
while (el) {
if (i < list.length && el === list[i].el) {
i++;
} else if (ORDERED_TAGS.has(el.localName)) {
return true;
}
el = el.nextSibling;
}
// some styles are not injected to the document
return i < list.length;
}

function addMany(styles) {
const pending = Promise.all(styles.map(_add));
emitUpdate();
return pending;
}

function add(style) {
const pending = _add(style);
emitUpdate();
return pending;
}

function _add(style) {
if (table.has(style.id)) {
return update(style);
}
style.el = createStyle(style.id);
const pending = setStyleContent(style.el, style.code);
table.set(style.id, style);
const nextIndex = list.findIndex(i => compare(i, style) > 0);
if (nextIndex < 0) {
document.documentElement.appendChild(style.el);
list.push(style);
} else {
document.documentElement.insertBefore(style.el, list[nextIndex].el);
list.splice(nextIndex, 0, style);
}
// disabled flag is read-only when not attached to a document
style.el.disabled = !enabled;
return pending;
}

function remove(id) {
_remove(id);
emitUpdate();
}

function _remove(id) {
const style = table.get(id);
if (!style) return;
table.delete(id);
list.splice(list.indexOf(style), 1);
style.el.remove();
}

function update({id, code}) {
const style = table.get(id);
if (style.code === code) return;
style.code = code;
// workaround for Chrome devtools bug fixed in v65
// https://github.com/openstyles/stylus/commit/0fa391732ba8e35fa68f326a560fc04c04b8608b
let oldEl;
if (CHROME < 3321) {
oldEl = style.el;
oldEl.id = '';
style.el = createStyle(id);
oldEl.parentNode.insertBefore(style.el, oldEl.nextSibling);
style.el.disabled = !enabled;
}
return setStyleContent(style.el, code)
.then(() => oldEl && oldEl.remove());
}

function createStyle(id) {
let el;
if (document.documentElement instanceof SVGSVGElement) {
// SVG document style
el = document.createElementNS('http://www.w3.org/2000/svg', 'style');
} else if (document instanceof XMLDocument) {
// XML document style
el = document.createElementNS('http://www.w3.org/1999/xhtml', 'style');
} else {
// HTML document style; also works on HTML-embedded SVG
el = document.createElement('style');
}
el.id = `${PREFIX}${id}`;
el.type = 'text/css';
// SVG className is not a string, but an instance of SVGAnimatedString
el.classList.add('stylus');
return el;
}

function clear() {
for (const style of list) {
style.el.remove();
}
list.length = 0;
table.clear();
emitUpdate();
}

function toggle(_enabled) {
if (enabled === _enabled) return;
enabled = _enabled;
for (const style of list) {
style.el.disabled = !enabled;
}
}

function sort() {
list.sort(compare);
for (const style of list) {
// moving an element resets its 'disabled' state
const disabled = style.el.disabled;
// FIXME: do we need this?
// const copy = document.importNode(el, true);
// el.textContent += ' '; // invalidate CSSOM cache
document.documentElement.appendChild(style.el);
style.el.disabled = disabled;
}
}

function emitUpdate() {
if (onUpdate) {
onUpdate();
}
}

function replaceAll(styles) {
const added = new Set(styles.map(s => s.id));
const removed = [];
for (const style of list) {
if (!added.has(style.id)) {
removed.push(style.id);
}
}
// FIXME: is it possible that `docRootObserver` breaks the process?
return Promise.all(styles.map(_add))
.then(() => {
removed.forEach(_remove);
emitUpdate();
});
}
}
1 change: 1 addition & 0 deletions edit.html
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@
<script src="js/msg.js"></script>
<script src="js/worker-util.js"></script>

<script src="content/style-injector.js"></script>
<script src="content/apply.js"></script>

<link href="edit/global-search.css" rel="stylesheet">
Expand Down
1 change: 1 addition & 0 deletions install-usercss.html
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
<script src="js/localization.js"></script>
<script src="js/script-loader.js"></script>
<script src="js/storage-util.js"></script>
<script src="content/style-injector.js"></script>
<script src="content/apply.js"></script>
<script src="vendor/semver-bundle/semver.js"></script>

Expand Down
1 change: 1 addition & 0 deletions manage.html
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ <h2 class="style-name">
<script src="js/messaging.js"></script>
<script src="js/prefs.js"></script>
<script src="js/msg.js"></script>
<script src="content/style-injector.js"></script>
<script src="content/apply.js"></script>
<script src="js/localization.js"></script>
<script src="manage/filters.js"></script>
Expand Down
9 changes: 8 additions & 1 deletion manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,14 @@
"run_at": "document_start",
"all_frames": true,
"match_about_blank": true,
"js": ["js/polyfill.js", "js/promisify.js", "js/msg.js", "js/prefs.js", "content/apply.js"]
"js": [
"js/polyfill.js",
"js/promisify.js",
"js/msg.js",
"js/prefs.js",
"content/style-injector.js",
"content/apply.js"
]
},
{
"matches": ["http://userstyles.org/*", "https://userstyles.org/*"],
Expand Down
1 change: 1 addition & 0 deletions options.html
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
<script src="js/prefs.js"></script>
<script src="js/storage-util.js" async></script>
<script src="msgbox/msgbox.js" async></script>
<script src="content/style-injector.js"></script>
<script src="content/apply.js"></script>
</head>

Expand Down
1 change: 1 addition & 0 deletions popup.html
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,7 @@
<script src="js/localization.js"></script>
<script src="js/prefs.js"></script>
<script src="js/msg.js"></script>
<script src="content/style-injector.js"></script>
<script src="content/apply.js"></script>

<link rel="stylesheet" href="popup/popup.css">
Expand Down

0 comments on commit b40849a

Please sign in to comment.