From 124dce44e85aa481f9c54b4d30dc31c78ebd2a66 Mon Sep 17 00:00:00 2001 From: tophf Date: Mon, 12 Oct 2020 21:52:31 +0300 Subject: [PATCH] use adoptedStyleSheets --- _locales/en/messages.json | 3 + background/style-manager.js | 23 ++--- content/apply.js | 2 +- content/style-injector.js | 184 +++++++++++++++++++++++++++++------- js/prefs.js | 1 + options.html | 7 ++ options/options.js | 4 + 7 files changed, 176 insertions(+), 48 deletions(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index e5f505f3..ea1e5972 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -943,6 +943,9 @@ "optionsAdvanced": { "message": "Advanced" }, + "optionsAdvancedAdoptedStyleSheets": { + "message": "Apply styles directly via AdoptedStyleSheets" + }, "optionsAdvancedContextDelete": { "message": "Add 'Delete' in editor context menu" }, diff --git a/background/style-manager.js b/background/style-manager.js index 4ae2a5c2..60855fc8 100644 --- a/background/style-manager.js +++ b/background/style-manager.js @@ -1,6 +1,6 @@ /* eslint no-eq-null: 0, eqeqeq: [2, "smart"] */ /* global createCache db calcStyleDigest db tryRegExp styleCodeEmpty styleSectionGlobal - getStyleWithNoCode msg sync uuidv4 URLS */ + getStyleWithNoCode msg sync uuidv4 URLS prefs */ /* exported styleManager */ 'use strict'; @@ -332,8 +332,10 @@ const styleManager = (() => { function ensurePrepared(methods) { const prepared = {}; for (const [name, fn] of Object.entries(methods)) { - prepared[name] = (...args) => - preparing.then(() => fn(...args)); + prepared[name] = async function () { + await preparing; + return fn.apply(this, arguments); + }; } return prepared; } @@ -475,7 +477,7 @@ const styleManager = (() => { return result; } - function getSectionsByUrl(url, id) { + function getSectionsByUrl(url, id, checkASS) { let cache = cachedStyleForUrl.get(url); if (!cache) { cache = { @@ -491,13 +493,12 @@ const styleManager = (() => { .map(i => styles.get(i)) ); } - if (id) { - if (cache.sections[id]) { - return {[id]: cache.sections[id]}; - } - return {}; - } - return cache.sections; + const sections = id + ? cache.sections[id] && {[id]: cache.sections[id]} || {} + : cache.sections; + return checkASS + ? Object.assign({ASS: prefs.get('adoptedStyleSheets')}, sections) + : sections; function buildCache(styleList) { const query = createMatchQuery(url); diff --git a/content/apply.js b/content/apply.js index 555535a2..9f67b025 100644 --- a/content/apply.js +++ b/content/apply.js @@ -59,7 +59,7 @@ self.INJECTED !== 1 && (() => { function init() { return STYLE_VIA_API ? API.styleViaAPI({method: 'styleApply'}) : - API.getSectionsByUrl(getMatchUrl()).then(styleInjector.apply); + API.getSectionsByUrl(getMatchUrl(), null, true).then(styleInjector.apply); } function getMatchUrl() { diff --git a/content/style-injector.js b/content/style-injector.js index 8630bf05..dc170fff 100644 --- a/content/style-injector.js +++ b/content/style-injector.js @@ -1,3 +1,4 @@ +/* global prefs */ 'use strict'; self.createStyleInjector = self.INJECTED === 1 ? self.createStyleInjector : ({ @@ -16,6 +17,7 @@ self.createStyleInjector = self.INJECTED === 1 ? self.createStyleInjector : ({ const table = new Map(); let isEnabled = true; let isTransitionPatched; + let ASS; // will store the original method refs because the page can override them let creationDoc, createElement, createElementNS; @@ -24,6 +26,7 @@ self.createStyleInjector = self.INJECTED === 1 ? self.createStyleInjector : ({ list, apply(styleMap) { + if ('ASS' in styleMap) _init(styleMap); const styles = _styleMapToArray(styleMap); return ( !styles.length ? @@ -45,6 +48,7 @@ self.createStyleInjector = self.INJECTED === 1 ? self.createStyleInjector : ({ }, clearOrphans() { + if (ASS) ASS.list = ASS.wipedList; for (const el of document.querySelectorAll(`style[id^="${PREFIX}"].stylus`)) { const id = el.id.slice(PREFIX.length); if (/^\d+$/.test(id) || id === PATCH_ID) { @@ -82,24 +86,28 @@ self.createStyleInjector = self.INJECTED === 1 ? self.createStyleInjector : ({ }; function _add(style) { - const el = style.el = _createStyle(style.id, style.code); + _domCreate(style); const i = list.findIndex(item => compare(item, style) > 0); table.set(style.id, style); - if (isEnabled) { - document.documentElement.insertBefore(el, i < 0 ? null : list[i].el); - } + if (isEnabled) _domInsert(style, (list[i] || {}).el); list.splice(i < 0 ? list.length : i, 0, style); - return el; + return style.el; } function _addRemoveElements(add) { - for (const {el} of list) { - if (add) { - document.documentElement.appendChild(el); + const asses = []; + for (const style of list) { + if (style.ass) { + asses.push(style.el); + } else if (add) { + _domInsert(style); } else { - el.remove(); + _domDelete(style); } } + if (ASS) { + ASS.list = ASS.wipedList.concat(add ? asses : []); + } } function _addUpdate(style) { @@ -115,20 +123,35 @@ self.createStyleInjector = self.INJECTED === 1 ? self.createStyleInjector : ({ !styles.some(s => s.code.includes('transition'))) { return; } - const el = _createStyle(PATCH_ID, ` - :root:not(#\\0):not(#\\0) * { - transition: none !important; - } - `); - document.documentElement.appendChild(el); + const style = { + id: PATCH_ID, + code: ` + :root:not(#\\0):not(#\\0) * { + transition: none !important; + } + `, + }; + _domCreate(style); + _domInsert(style); // wait for the next paint to complete // note: requestAnimationFrame won't fire in inactive tabs - requestAnimationFrame(() => setTimeout(() => el.remove())); + requestAnimationFrame(() => setTimeout(_domDelete, 0, style)); } - function _createStyle(id, code = '') { - if (!creationDoc) _initCreationDoc(); + /** + * @returns {Element|CSSStyleSheet} + * @mutates style + */ + function _domCreate(style = {}) { + const {id, code = ''} = style; let el; + if (ASS) { + el = style.el = new CSSStyleSheet(id ? {media: PREFIX + id} : {}); + if (_setASSCode(style, code)) { + return el; + } + } + if (!creationDoc) _initCreationDoc(); if (document.documentElement instanceof SVGSVGElement) { // SVG document style el = createElementNS.call(creationDoc, 'http://www.w3.org/2000/svg', 'style'); @@ -148,9 +171,45 @@ self.createStyleInjector = self.INJECTED === 1 ? self.createStyleInjector : ({ // SVG className is not a string, but an instance of SVGAnimatedString el.classList.add('stylus'); el.textContent = code; + style.el = el; + style.ass = false; return el; } + function _domInsert({ass, el}, beforeEl) { + if (ass) { + const list = ASS.omit({el}); + const i = list.indexOf(beforeEl); + list.splice(i >= 0 ? i : list.length, 0, el); + ASS.list = list; + } else { + document.documentElement.insertBefore(el, beforeEl); + } + } + + function _domDelete({ass, el}) { + if (ass) { + ASS.omit({el, commit: true}); + } else { + el.remove(); + } + } + + function _setASSCode(style, code) { + try { + style.el.replaceSync(code); // throws on @import per spec + style.ass = true; + } catch (err) { + style.ass = false; + } + const isTight = style.ass && list.every(s => s.ass); + if (ASS.isTight !== isTight) { + ASS.isTight = isTight; + docRootObserver[isTight ? 'stop' : 'start'](); + } + return style.ass; + } + function _toggleObservers(shouldStart) { const onOff = shouldStart && isEnabled ? 'start' : 'stop'; docRewriteObserver[onOff](); @@ -163,6 +222,51 @@ self.createStyleInjector = self.INJECTED === 1 ? self.createStyleInjector : ({ return value; } + + function _init(styleMap) { + if (ASS == null && Array.isArray(document.adoptedStyleSheets)) { + _initASS(styleMap.ASS); + prefs.subscribe(['adoptedStyleSheets'], (key, value) => { + _toggleObservers(false); + _addRemoveElements(false); + _initASS(value); + list.forEach(_domCreate); + if (isEnabled) _addRemoveElements(true); + _toggleObservers(true); + }); + } + delete styleMap.ASS; + } + + function _initASS(enable) { + if (ASS && !enable) { + ASS = false; + } else if (!ASS && enable) { + ASS = { + isTight: true, + /** @param {CSSStyleSheet[]} sheets */ + set list(sheets) { + document.adoptedStyleSheets = sheets; + }, + /** @returns {CSSStyleSheet[]} */ + get list() { + return [...document.adoptedStyleSheets]; + }, + /** @returns {CSSStyleSheet[]} without our elements */ + get wipedList() { + return ASS.list.filter(({media: {0: id = ''}}) => + !(id.startsWith(PREFIX) && Number(id.slice(PREFIX.length)) || id.endsWith(PATCH_ID))); + }, + omit({el, list = ASS.list, commit}) { + const i = list.indexOf(el); + if (i >= 0) list.splice(i, 1); + if (commit) ASS.list = list; + return list; + }, + }; + } + } + /* FF59+ workaround: allow the page to read our sheets, https://github.com/openstyles/stylus/issues/461 First we're trying the page context document where inline styles may be forbidden by CSP @@ -174,7 +278,7 @@ self.createStyleInjector = self.INJECTED === 1 ? self.createStyleInjector : ({ creationDoc = !Event.prototype.getPreventDefault && document.wrappedJSObject; if (creationDoc) { ({createElement, createElementNS} = creationDoc); - const el = document.documentElement.appendChild(_createStyle()); + const el = document.documentElement.appendChild(_domCreate()); const isApplied = el.sheet; el.remove(); if (isApplied) return; @@ -188,7 +292,7 @@ self.createStyleInjector = self.INJECTED === 1 ? self.createStyleInjector : ({ if (!style) return; table.delete(id); list.splice(list.indexOf(style), 1); - style.el.remove(); + _domDelete(style); } function _sort() { @@ -237,13 +341,16 @@ self.createStyleInjector = self.INJECTED === 1 ? self.createStyleInjector : ({ // workaround for Chrome devtools bug fixed in v65 if (isChromePre65) { const oldEl = style.el; - style.el = _createStyle(id, code); + style.el = _domCreate({id, code}); if (isEnabled) { oldEl.parentNode.insertBefore(style.el, oldEl.nextSibling); oldEl.remove(); } - } else { + } else if (!style.ass) { style.el.textContent = code; + } else if (!_setASSCode(style, code)) { + _remove(id); + _add(style); } } @@ -283,21 +390,25 @@ self.createStyleInjector = self.INJECTED === 1 ? self.createStyleInjector : ({ let digest = 0; let lastCalledTime = NaN; let observing = false; - const observer = new MutationObserver(() => { - if (digest) { - if (performance.now() - lastCalledTime > 1000) { - digest = 0; - } else if (digest > 5) { - throw new Error('The page keeps generating mutations. Skip the event.'); - } - } - if (onChange()) { - digest++; - lastCalledTime = performance.now(); - } - }); + let observer; return {evade, start, stop}; + function create() { + observer = new MutationObserver(() => { + if (digest) { + if (performance.now() - lastCalledTime > 1000) { + digest = 0; + } else if (digest > 5) { + throw new Error('The page keeps generating mutations. Skip the event.'); + } + } + if (onChange()) { + digest++; + lastCalledTime = performance.now(); + } + }); + } + function evade(fn) { const restore = observing && start; stop(); @@ -306,7 +417,8 @@ self.createStyleInjector = self.INJECTED === 1 ? self.createStyleInjector : ({ } function start() { - if (observing) return; + if (observing || ASS && ASS.isTight) return; + if (!observer) create(); observer.observe(document.documentElement, {childList: true}); observing = true; } diff --git a/js/prefs.js b/js/prefs.js index 3b39cf85..f4a365ea 100644 --- a/js/prefs.js +++ b/js/prefs.js @@ -9,6 +9,7 @@ self.prefs = self.INJECTED === 1 ? self.prefs : (() => { 'disableAll': false, // boss key 'exposeIframes': false, // Add 'stylus-iframe' attribute to HTML element in all iframes 'newStyleAsUsercss': false, // create new style in usercss format + 'adoptedStyleSheets': false, // try to apply styles via document.adoptedStyleSheets // checkbox in style config dialog 'config.autosave': true, diff --git a/options.html b/options.html index 718a8d31..0a52c4ec 100644 --- a/options.html +++ b/options.html @@ -232,6 +232,13 @@
+