diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 6fa090a9..e1b3416c 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -1089,6 +1089,12 @@ "message": "Exposes the top site domain in each iframe.\nEnables writing iframe-specific CSS like this:\nhtml[stylus-iframe$$=\"twitter.com\"] h1 { display:none }", "description": "Add attribute to iframe; make sure to include the double $$ in the css example, or the `$=` will be omitted in the displayed text." }, + "optionsAdvancedExposeStyleName": { + "message": "Expose style name" + }, + "optionsAdvancedExposeStyleNameNote": { + "message": "Exposes the style name in the page to facilitate debugging of styles in devtools. Please reload the tab(s) to apply the new setting." + }, "optionsAdvancedNewStyleAsUsercss": { "message": "Write new style as usercss" }, diff --git a/background/style-manager.js b/background/style-manager.js index 8a147f9f..fed12aee 100644 --- a/background/style-manager.js +++ b/background/style-manager.js @@ -184,6 +184,8 @@ const styleMan = (() => { }, }; } + // TODO: enable in FF when it supports sourceURL comment in style elements (also options.html) + const {exposeStyleName} = CHROME && prefs.__values; const sender = CHROME && this && this.sender || {}; if (sender.frameId === 0) { /* Chrome hides text frament from location.href of the page e.g. #:~:text=foo @@ -202,9 +204,9 @@ const styleMan = (() => { } else if (cache.maybeMatch.size) { buildCache(cache, url, Array.from(cache.maybeMatch, id2data).filter(Boolean)); } - return id - ? cache.sections[id] ? {[id]: cache.sections[id]} : {} - : Object.assign({cfg: {order}}, cache.sections); + return Object.assign({cfg: {exposeStyleName, order}}, + id ? mapObj(cache.sections, null, [id]) + : cache.sections); }, /** @returns {Promise} */ @@ -429,7 +431,7 @@ const styleMan = (() => { const code = getAppliedCode(createMatchQuery(url), style); if (code) { updated.add(url); - cache.sections[id] = {id, code}; + buildCacheEntry(cache, style, code); } else { excluded.add(url); delete cache.sections[id]; @@ -703,13 +705,20 @@ const styleMan = (() => { for (const {style, appliesTo, preview} of styleList) { const code = getAppliedCode(query, preview || style); if (code) { - const id = style.id; - cache.sections[id] = {id, code}; + buildCacheEntry(cache, style, code); appliesTo.add(url); } } } + function buildCacheEntry(cache, style, code = style.code) { + cache.sections[style.id] = { + code, + id: style.id, + name: style.customName || style.name, + }; + } + /** @returns {StyleObj[]} */ function getAllAsArray() { return Array.from(dataMap.values(), v => v.style); diff --git a/background/style-via-webrequest.js b/background/style-via-webrequest.js index 4913fd28..1da554ca 100644 --- a/background/style-via-webrequest.js +++ b/background/style-via-webrequest.js @@ -1,5 +1,5 @@ /* global API */// msg.js -/* global CHROME ignoreChromeError */// toolbox.js +/* global CHROME URLS ignoreChromeError */// toolbox.js /* global prefs */ 'use strict'; @@ -49,6 +49,12 @@ if (CHROME && !off) { chrome.webNavigation.onCommitted.addListener(injectData, {url: [{urlPrefix: 'http'}]}); } + if (CHROME) { + chrome.webRequest.onBeforeRequest.addListener(openNamedStyle, { + urls: [URLS.ownOrigin + 'virtual/*.css'], + types: ['main_frame'], + }, ['blocking']); + } state.csp = csp; state.off = off; state.xhr = xhr; @@ -146,6 +152,12 @@ } } + /** @param {chrome.webRequest.WebRequestBodyDetails} req */ + function openNamedStyle(req) { + chrome.tabs.update(req.tabId, {url: 'edit.html?id=' + req.url.split('#')[1]}); + return {cancel: true}; + } + function req2key(req) { return req.tabId + ':' + req.frameId; } diff --git a/content/apply.js b/content/apply.js index 824de819..5d490ff4 100644 --- a/content/apply.js +++ b/content/apply.js @@ -105,7 +105,6 @@ if (styles.cfg) { isDisabled = styles.cfg.disableAll; Object.assign(order, styles.cfg.order); - delete styles.cfg; } hasStyles = !isDisabled; if (hasStyles) { @@ -181,7 +180,6 @@ if (!hasStyles && isDisabled || matchUrl === request.url) break; matchUrl = request.url; API.styles.getSectionsByUrl(matchUrl).then(sections => { - delete sections.cfg; hasStyles = true; styleInjector.replace(sections); }); diff --git a/content/style-injector.js b/content/style-injector.js index dc434b38..037f8312 100644 --- a/content/style-injector.js +++ b/content/style-injector.js @@ -11,10 +11,12 @@ window.StyleInjector = window.INJECTED === 1 ? window.StyleInjector : ({ const ORDERED_TAGS = new Set(['head', 'body', 'frameset', 'style', 'link']); const docRewriteObserver = RewriteObserver(_sort); const docRootObserver = RootObserver(_sortIfNeeded); + const toSafeChar = c => String.fromCharCode(0xFF00 + c.charCodeAt(0) - 0x20); const list = []; const table = new Map(); let isEnabled = true; let isTransitionPatched; + let exposeStyleName; // will store the original method refs because the page can override them let creationDoc, createElement, createElementNS; @@ -83,7 +85,7 @@ window.StyleInjector = window.INJECTED === 1 ? window.StyleInjector : ({ }; function _add(style) { - const el = style.el = _createStyle(style.id, style.code); + const el = style.el = _createStyle(style); const i = list.findIndex(item => compare(item, style) > 0); table.set(style.id, style); if (isEnabled) { @@ -116,18 +118,18 @@ window.StyleInjector = window.INJECTED === 1 ? window.StyleInjector : ({ !styles.some(s => s.code.includes('transition'))) { return; } - const el = _createStyle(PATCH_ID, ` + const el = _createStyle({id: PATCH_ID, code: ` :root:not(#\\0):not(#\\0) * { transition: none !important; } - `); + `}); document.documentElement.appendChild(el); // wait for the next paint to complete // note: requestAnimationFrame won't fire in inactive tabs requestAnimationFrame(() => setTimeout(() => el.remove())); } - function _createStyle(id, code = '') { + function _createStyle({id, code = '', name} = {}) { if (!creationDoc) _initCreationDoc(); let el; if (document.documentElement instanceof SVGSVGElement) { @@ -145,6 +147,11 @@ window.StyleInjector = window.INJECTED === 1 ? window.StyleInjector : ({ const oldEl = document.getElementById(el.id); if (oldEl) oldEl.id += '-superseded-by-Stylus'; } + if (exposeStyleName && name) { + el.dataset.name = name; + name = encodeURIComponent(name.replace(/[#%/@:']/g, toSafeChar)); + code += `\n/*# sourceURL=${chrome.runtime.getURL('')}virtual/${name}.css#${id} */`; + } el.type = 'text/css'; // SVG className is not a string, but an instance of SVGAnimatedString el.classList.add('stylus'); @@ -224,9 +231,12 @@ window.StyleInjector = window.INJECTED === 1 ? window.StyleInjector : ({ } function _styleMapToArray(styleMap) { - return Object.values(styleMap).map(s => ({ - id: s.id, - code: s.code.join(''), + ({exposeStyleName} = styleMap.cfg || {}); + delete styleMap.cfg; + return Object.values(styleMap).map(({id, code, name}) => ({ + id, + name, + code: code.join(''), })); } diff --git a/js/prefs.js b/js/prefs.js index 364308fb..0511f912 100644 --- a/js/prefs.js +++ b/js/prefs.js @@ -24,6 +24,7 @@ 'show-badge': true, // display text on popup menu icon 'disableAll': false, // boss key 'exposeIframes': false, // Add 'stylus-iframe' attribute to HTML element in all iframes + 'exposeStyleName': false, // Add style name to the style for better devtools experience 'newStyleAsUsercss': false, // create new style in usercss format 'styleViaXhr': false, // early style injection to avoid FOUC 'patchCsp': false, // add data: and popular image hosting sites to strict CSP diff --git a/options.html b/options.html index e5342f3b..cf7445c9 100644 --- a/options.html +++ b/options.html @@ -269,6 +269,18 @@ +