diff --git a/.eslintrc b/.eslintrc
index 85e21a98..d02a8b8c 100644
--- a/.eslintrc
+++ b/.eslintrc
@@ -12,6 +12,7 @@ globals:
# messaging.js
OWN_ORIGIN: false
KEEP_CHANNEL_OPEN: false
+ RX_SUPPORTED_URLS: false
configureCommands: false
notifyAllTabs: false
refreshAllTabs: false
diff --git a/_locales/en/messages.json b/_locales/en/messages.json
index 9b462ee4..46eb7ae4 100644
--- a/_locales/en/messages.json
+++ b/_locales/en/messages.json
@@ -402,6 +402,20 @@
"message": "Invalid regexps skipped",
"description": "RegExp test report: label for the invalid expressions"
},
+ "styleRegexpPartialExplanation": {
+ "message": "This style uses partially matching regexps in violation of CSS4 @document specification which requires a full URL match. The affected CSS sections were not applied to the page. This style was probably created in Stylish-for-Chrome which incorrectly checks 'regexp()' rules since the very first version (known bug)."
+ },
+ "styleRegexpInvalidExplanation": {
+ "message": "Some 'regexp()' rules that could not be compiled at all."
+ },
+ "styleNotAppliedRegexpProblemTooltip": {
+ "message": "Style was not applied due to its incorrect usage of 'regexp()'",
+ "description": "Tooltip in the popup for styles that were not applied at all"
+ },
+ "styleRegexpProblemTooltip": {
+ "message": "Number of sections not applied due to incorrect usage of 'regexp()'",
+ "description": "Tooltip in the popup for styles that were applied only partially"
+ },
"styleBeautify": {
"message": "Beautify",
"description": "Label for the CSS-beautifier button on the edit style page"
diff --git a/backup/fileSaveLoad.js b/backup/fileSaveLoad.js
index 3bf03510..114d8763 100644
--- a/backup/fileSaveLoad.js
+++ b/backup/fileSaveLoad.js
@@ -70,7 +70,7 @@ function importFromString(jsonString) {
continue;
}
item.name = item.name.trim();
- const byId = (cachedStyles.byId.get(item.id) || {}).style;
+ const byId = cachedStyles.byId.get(item.id);
const byName = oldStylesByName.get(item.name);
const oldStyle = byId && byId.name.trim() == item.name || !byName ? byId : byName;
if (oldStyle == byName && byName) {
diff --git a/manage.js b/manage.js
index a29855d0..d7094034 100644
--- a/manage.js
+++ b/manage.js
@@ -7,7 +7,7 @@ const TARGET_TYPES = ['domains', 'urls', 'urlPrefixes', 'regexps'];
const TARGET_LIMIT = 10;
-getStylesSafe({code: false})
+getStylesSafe()
.then(showStyles)
.then(initGlobalEvents);
@@ -235,7 +235,7 @@ class EntryOnClick {
static delete(event) {
const styleElement = getClickedStyleElement(event);
const id = styleElement.styleId;
- const name = ((cachedStyles.byId.get(id) || {}).style || {}).name;
+ const {name} = cachedStyles.byId.get(id) || {};
animateElement(styleElement, {className: 'highlight'});
messageBox({
title: t('deleteStyleConfirm'),
@@ -436,7 +436,7 @@ function searchStyles({immediately, container}) {
}
for (const element of (container || installed).children) {
- const {style} = cachedStyles.byId.get(element.styleId) || {};
+ const style = cachedStyles.byId.get(element.styleId) || {};
if (style) {
const isMatching = !query
|| isMatchingText(style.name)
diff --git a/messaging.js b/messaging.js
index fb221764..d2e8ecba 100644
--- a/messaging.js
+++ b/messaging.js
@@ -4,6 +4,7 @@
// keep message channel open for sendResponse in chrome.runtime.onMessage listener
const KEEP_CHANNEL_OPEN = true;
const OWN_ORIGIN = chrome.runtime.getURL('');
+const RX_SUPPORTED_URLS = new RegExp(`^(file|https?|ftps?):|^${OWN_ORIGIN}`);
function notifyAllTabs(request) {
diff --git a/popup.css b/popup.css
index bcd7c7d6..a92052de 100644
--- a/popup.css
+++ b/popup.css
@@ -1,29 +1,43 @@
body {
width: 252px;
font-size: 12px;
- font-family: Arial,"Helvetica Neue",Helvetica,sans-serif;
+ font-family: Arial, "Helvetica Neue", Helvetica, sans-serif;
+ margin: 0;
}
+
+body > div:not(#installed) {
+ margin-left: 0.75em;
+ margin-right: 0.75em;
+}
+
input[type=checkbox] {
outline: none;
}
+
#disable-all-wrapper {
padding: 0.3em 0 0.6em;
}
+
#no-styles {
font-style: italic;
}
+
#popup-shortcuts-button {
margin-left: 3px;
}
+
.checker {
display: inline;
}
+
.style-name {
cursor: default;
font-weight: bold;
display: block;
}
-a, a:visited {
+
+a,
+a:visited {
color: black;
text-decoration-skip: ink;
}
@@ -33,53 +47,151 @@ a, a:visited {
width: 16px;
vertical-align: top;
}
+
.left-gutter input {
margin-bottom: 1px;
margin-top: 0;
margin-left: 0;
}
+
.main-controls {
display: table-cell;
}
-.entry {
- padding: 0.5em 0;
-}
-.entry:first-child {
- padding-top: 0;
-}
-
#unavailable,
#installed {
border-bottom: 1px solid black;
padding-bottom: 2px;
}
+
body > DIV:last-of-type,
body.blocked > DIV {
border-bottom: none;
}
+
#installed {
padding-top: 2px;
max-height: 434px;
overflow-y: auto;
}
+
#installed.disabled .style-name {
text-decoration: line-through;
}
+
#installed .actions {
cursor: default;
}
+
#installed .actions a {
cursor: pointer;
text-decoration: none;
}
-#installed .style-edit-link {
+
+/* entry */
+
+.entry {
+ display: flex;
+ align-items: center;
+ padding: 5px 0.75em;
+}
+
+.entry:nth-child(even) {
+ background-color: rgba(0, 0, 0, 0.05);
+}
+
+.entry .style-edit-link {
margin-right: 2px;
}
-#installed .style-edit-link, #installed .delete {
+
+.entry .style-edit-link,
+.entry .delete {
display: inline-block;
padding: 0 1px 0;
}
+
+.entry .main-controls {
+ display: flex;
+ flex: 1;
+ width: calc(100% - 20px);
+ align-items: center;
+}
+
+.entry .main-controls label {
+ flex: 1;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ padding-right: 5px;
+}
+
+.not-applied .checker,
+.not-applied .style-name,
+.not-applied .actions > * {
+ opacity: .2;
+ transition: opacity .5s ease-in-out .25s, color .5s ease-in-out .25s;
+}
+
+.not-applied:hover .checker,
+.not-applied:hover .style-name,
+.not-applied:hover .actions > * {
+ opacity: 1;
+}
+
+.not-applied:hover .style-name {
+ color: darkorange;
+}
+
+.regexp-problem-indicator {
+ background-color: darkorange;
+ width: 15px;
+ height: 15px;
+ line-height: 16px;
+ border-radius: 8px;
+ margin-right: 4px;
+ margin-left: 4px;
+ text-align: center;
+ color: white;
+ font-weight: bold;
+ box-sizing: border-box;
+ cursor: pointer;
+}
+
+.regexp-partial .actions,
+.regexp-invalid .actions {
+ order: 2;
+}
+
+#regexp-explanation {
+ position: fixed;
+ background-color: white;
+ left: 0;
+ right: 0;
+ padding: .5rem;
+ font-size: 90%;
+ border-top: 2px solid black;
+ border-bottom: 2px solid black;
+ box-shadow: 0 0 100px black;
+ display: flex;
+ flex-direction: column;
+}
+
+#regexp-explanation > div {
+ display: none;
+ list-style-type: none;
+ padding: 0;
+ margin: 0;
+}
+
+.regexp-partial #regexp-partial,
+.regexp-invalid #regexp-invalid {
+ display: block;
+}
+
+#regexp-explanation > div:not(:last-child) {
+ margin-bottom: .5rem;
+}
+
.svg-icon {
pointer-events: none;
transition: fill .5s;
@@ -92,20 +204,27 @@ body > .actions {
margin-top: 0.5em;
}
-.actions > div:not(:last-child):not(#disable-all-wrapper), .actions > .main-controls > div:not(:last-child), #unavailable:not(:last-child), #unavailable + .actions {
+.actions > div:not(:last-child):not(#disable-all-wrapper),
+.actions > .main-controls > div:not(:last-child),
+#unavailable:not(:last-child),
+#unavailable + .actions {
margin-bottom: 0.75em;
}
-.actions input, .actions label {
+
+.actions input,
+.actions label {
vertical-align: middle;
}
#unavailable {
border: none;
- display: none; /* flex */
+ display: none;
+ margin-top: 0.75em;
align-items: center;
justify-content: center;
font-size: 14px;
}
+
body.blocked #installed,
body.blocked #find-styles,
body.blocked #write-style,
@@ -118,41 +237,84 @@ body.blocked #unavailable {
}
/* Never shown, but can be enabled with a style */
-.enable, .disable {
+
+.enable,
+.disable {
display: none;
}
/* 'New style' links */
-#write-style-for {margin-right: .6ex}
-.write-style-link {margin-left: .6ex}
-.write-style-link::before, .write-style-link::after {font-size: 12px}
-.write-style-link::before {content: "\00ad"} /* "soft" hyphen */
-#match {overflow-wrap: break-word;}
+
+#write-style-for {
+ margin-right: .6ex
+}
+
+.write-style-link {
+ margin-left: .6ex
+}
+
+.write-style-link::before,
+.write-style-link::after {
+ font-size: 12px
+}
+
+.write-style-link::before {
+ content: "\00ad"; /* "soft" hyphen */
+}
+
+#match {
+ overflow-wrap: break-word;
+ display: inline-block;
+}
/* "breadcrumbs" 'new style' links */
-.breadcrumbs > .write-style-link {margin-left: 0}
-.breadcrumbs:hover a {color: #bbb; text-decoration: none}
+.breadcrumbs > .write-style-link {
+ margin-left: 0
+}
+
+.breadcrumbs:hover a {
+ color: #bbb;
+ text-decoration: none
+}
+
+/* use just the subdomain name instead of the full domain name */
+.breadcrumbs > .write-style-link[subdomain]:not(:nth-last-child(2)) {
+ font-size: 0
+}
- /* use just the subdomain name instead of the full domain name */
-.breadcrumbs > .write-style-link[subdomain]:not(:nth-last-child(2)) {font-size: 0}
.breadcrumbs > .write-style-link[subdomain]:not(:nth-last-child(2))::before {
content: attr(subdomain);
}
- /* "dot" after each subdomain name */
-.breadcrumbs > .write-style-link[subdomain]::after {content: "."}
- /* no "dot" after top-level domain */
-.breadcrumbs > .write-style-link:nth-last-child(2)::after {content: none}
- /* "forward slash" before path ("this URL") */
-.breadcrumbs > .write-style-link:last-child::before {content: "\200b/"}
+/* "dot" after each subdomain name */
+.breadcrumbs > .write-style-link[subdomain]::after {
+ content: "."
+}
+
+/* no "dot" after top-level domain */
+.breadcrumbs > .write-style-link:nth-last-child(2)::after {
+ content: none
+}
+
+/* "forward slash" before path ("this URL") */
+.breadcrumbs > .write-style-link:last-child::before {
+ content: "\200b/"
+}
+
.breadcrumbs > .write-style-link:last-child:first-child::before,
-.breadcrumbs > .write-style-link[subdomain=""] + .write-style-link::before {content: none}
+.breadcrumbs > .write-style-link[subdomain=""] + .write-style-link::before {
+ content: none
+}
- /* suppress TLD-only link */
-.breadcrumbs > .write-style-link[subdomain=""] {display: none}
+/* suppress TLD-only link */
+.breadcrumbs > .write-style-link[subdomain=""] {
+ display: none
+}
- /* :hover style */
-.breadcrumbs.url\(\) > .write-style-link, /* :hover or :focus on "this URL" sets class="url()" */
+/* :hover style */
+.breadcrumbs.url\(\) > .write-style-link,
+
+/* :hover or :focus on "this URL" sets class="url()" */
.breadcrumbs > .write-style-link:hover,
.breadcrumbs > .write-style-link:focus,
.breadcrumbs > .write-style-link:hover ~ .write-style-link[subdomain],
@@ -162,13 +324,15 @@ body.blocked #unavailable {
text-decoration-skip: ink;
}
- /* action buttons */
+/* action buttons */
+
#popup-options {
display: flex;
flex-direction: row;
justify-content: space-around;
padding: 1.2em 0;
}
+
#popup-options button {
margin: 0 2px;
width: 33%;
@@ -177,72 +341,43 @@ body.blocked #unavailable {
text-overflow: ellipsis;
}
- /* margins */
-body {
- margin: 0;
-}
-body>div:not(#installed) {
- margin-left:0.75em;
- margin-right:0.75em;
-}
-#unavailable {
- margin-top: 0.75em;
-}
-#installed .entry {
-}
- /* entries */
-#installed .entry {
- display: flex;
- align-items: center;
- padding: 5px 0.75em;
-}
-#installed .entry:nth-child(even) {
- background-color: rgba(0, 0, 0, 0.05);
-}
-#installed .main-controls {
- display: flex;
- flex: 1;
- width: calc(100% - 20px);
- align-items: center;
-}
-#installed .main-controls label {
- flex: 1;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- padding-right: 5px;
-}
- /* confirm */
+/* confirm */
+
#confirm,
-#confirm>div>span {
+#confirm > div > span {
align-items: center;
justify-content: center;
}
+
#confirm {
z-index: 2147483647;
- display: none; /* flex */
+ display: none;
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
- margin: 0!important;
+ margin: 0 !important;
box-sizing: border-box;
background-color: rgba(0, 0, 0, 0.4);
animation: lights-off .5s cubic-bezier(.03, .67, .08, .94);
animation-fill-mode: both;
}
+
#confirm.lights-on {
animation: lights-on .25s ease-in-out;
animation-fill-mode: both;
}
-#confirm.lights-on > div{
+
+#confirm.lights-on > div {
display: none;
}
+
#confirm[data-display=true] {
display: flex;
}
-#confirm>div {
+
+#confirm > div {
width: 80%;
height: 100px;
max-height: 80%;
@@ -252,25 +387,30 @@ body>div:not(#installed) {
flex-direction: column;
border: solid 2px rgba(0, 0, 0, 0.5);
}
-#confirm>div>span {
+
+#confirm > div > span {
display: flex;
flex: 1;
padding: 0 10px;
}
-#confirm>div>b {
+
+#confirm > div > b {
padding: 10px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
-#confirm>div>div {
+
+#confirm > div > div {
padding: 10px;
text-align: center;
}
-.non-windows #confirm>div>div {
+
+.non-windows #confirm > div > div {
direction: rtl;
text-align: right;
}
+
@keyframes lights-off {
from {
background-color: transparent;
@@ -279,6 +419,7 @@ body>div:not(#installed) {
background-color: rgba(0, 0, 0, 0.4);
}
}
+
@keyframes lights-on {
from {
background-color: rgba(0, 0, 0, 0.4);
@@ -287,4 +428,3 @@ body>div:not(#installed) {
background-color: transparent;
}
}
-
diff --git a/popup.html b/popup.html
index 5fb3f9b6..0a36c1ad 100644
--- a/popup.html
+++ b/popup.html
@@ -37,7 +37,19 @@
-
+
+
+
+
+
+
+
+
+
@@ -81,7 +93,7 @@
-
+
diff --git a/popup.js b/popup.js
index 23f6a30f..553d3ab3 100644
--- a/popup.js
+++ b/popup.js
@@ -1,15 +1,19 @@
+/* global SLOPPY_REGEXP_PREFIX, compileStyleRegExps */
'use strict';
let installed;
+let tabURL;
getActiveTabRealURL().then(url => {
- const RX_SUPPORTED_URLS = new RegExp(`^(file|https?|ftps?):|^${OWN_ORIGIN}`);
- const isUrlSupported = RX_SUPPORTED_URLS.test(url);
+ tabURL = RX_SUPPORTED_URLS.test(url) ? url : '';
Promise.all([
- isUrlSupported ? getStylesSafe({matchUrl: url}) : null,
- onDOMready().then(() => initPopup(isUrlSupported ? url : '')),
- ])
- .then(([styles]) => styles && showStyles(styles));
+ tabURL && getStylesSafe({matchUrl: tabURL}),
+ onDOMready().then(() => {
+ initPopup(tabURL);
+ }),
+ ]).then(([styles]) => {
+ showStyles(styles);
+ });
});
@@ -116,30 +120,55 @@ function initPopup(url) {
function showStyles(styles) {
+ if (!styles) {
+ return;
+ }
if (!styles.length) {
installed.innerHTML = template.noStyles.outerHTML;
- } else {
- const enabledFirst = prefs.get('popup.enabledFirst');
- styles.sort((a, b) => (
- enabledFirst && a.enabled !== b.enabled
- ? !(a.enabled < b.enabled) ? -1 : 1
- : a.name.localeCompare(b.name)
- ));
- const fragment = document.createDocumentFragment();
- for (const style of styles) {
- fragment.appendChild(createStyleElement(style));
- }
- installed.appendChild(fragment);
+ return;
}
+
+ const enabledFirst = prefs.get('popup.enabledFirst');
+ styles.sort((a, b) => (
+ enabledFirst && a.enabled !== b.enabled
+ ? !(a.enabled < b.enabled) ? -1 : 1
+ : a.name.localeCompare(b.name)
+ ));
+
+ let postponeDetect = false;
+ const t0 = performance.now();
+ const container = document.createDocumentFragment();
+ for (const style of styles) {
+ createStyleElement({style, container, postponeDetect});
+ postponeDetect = postponeDetect || performance.now() - t0 > 100;
+ }
+ installed.appendChild(container);
+
+ getStylesSafe({matchUrl: tabURL, strictRegexp: false})
+ .then(unscreenedStyles => {
+ for (const unscreened of unscreenedStyles) {
+ if (!styles.includes(unscreened)) {
+ postponeDetect = postponeDetect || performance.now() - t0 > 100;
+ createStyleElement({
+ style: Object.assign({appliedSections: [], postponeDetect}, unscreened),
+ });
+ }
+ }
+ });
}
// silence the inapplicable warning for async code
/* eslint no-use-before-define: [2, {"functions": false, "classes": false}] */
-function createStyleElement(style) {
+function createStyleElement({
+ style,
+ container = installed,
+ postponeDetect,
+}) {
const entry = template.style.cloneNode(true);
entry.setAttribute('style-id', style.id);
Object.assign(entry, {
+ id: 'style-' + style.id,
styleId: style.id,
className: entry.className + ' ' + (style.enabled ? 'enabled' : 'disabled'),
onmousedown: openEditorOnMiddleclick,
@@ -171,7 +200,18 @@ function createStyleElement(style) {
$('.disable', entry).onclick = EntryOnClick.toggle;
$('.delete', entry).onclick = EntryOnClick.delete;
- return entry;
+ if (postponeDetect) {
+ setTimeout(detectSloppyRegexps, 0, {entry, style});
+ } else {
+ detectSloppyRegexps({entry, style});
+ }
+
+ const oldElement = $('#style-' + style.id);
+ if (oldElement) {
+ oldElement.parentNode.replaceChild(entry, oldElement);
+ } else {
+ container.appendChild(entry);
+ }
}
@@ -194,7 +234,7 @@ class EntryOnClick {
const box = $('#confirm');
box.dataset.display = true;
box.style.cssText = '';
- $('b', box).textContent = ((cachedStyles.byId.get(id) || {}).style || {}).name;
+ $('b', box).textContent = (cachedStyles.byId.get(id) || {}).name;
$('[data-cmd="ok"]', box).onclick = () => confirm(true);
$('[data-cmd="cancel"]', box).onclick = () => confirm(false);
window.onkeydown = event => {
@@ -219,6 +259,18 @@ class EntryOnClick {
}
}
+ static indicator(event) {
+ const entry = getClickedStyleElement(event);
+ const info = template.regexpProblemExplanation.cloneNode(true);
+ $$('#' + info.id).forEach(el => el.remove());
+ $$('a', info).forEach(el => (el.onclick = openURLandHide));
+ $$('button', info).forEach(el => (el.onclick = EntryOnClick.closeExplanation));
+ entry.appendChild(info);
+ }
+
+ static closeExplanation(event) {
+ $('#regexp-explanation').remove();
+ }
}
@@ -264,24 +316,51 @@ function openURLandHide(event) {
function handleUpdate(style) {
- const styleElement = $(`[style-id="${style.id}"]`, installed);
- if (styleElement) {
- installed.replaceChild(createStyleElement(style), styleElement);
- } else {
- getActiveTabRealURL().then(url => {
- if (getApplicableSections(style, url).length) {
- // a new style for the current url is installed
- $('#unavailable').style.display = 'none';
- installed.appendChild(createStyleElement(style));
- }
- });
+ if ($('#style-' + style.id)) {
+ createStyleElement({style});
+ return;
+ }
+ // Add an entry when a new style for the current url is installed
+ if (tabURL && getApplicableSections({style, matchUrl: tabURL, stopOnFirst: true}).length) {
+ $('#unavailable').style.display = 'none';
+ createStyleElement({style});
}
}
function handleDelete(id) {
- const styleElement = $(`[style-id="${id}"]`, installed);
- if (styleElement) {
- installed.removeChild(styleElement);
+ $$('#style-' + id).forEach(el => el.remove());
+}
+
+
+/*
+ According to CSS4 @document specification the entire URL must match.
+ Stylish-for-Chrome implemented it incorrectly since the very beginning.
+ We'll detect styles that abuse the bug by finding the sections that
+ would have been applied by Stylish but not by us as we follow the spec.
+ Additionally we'll check for invalid regexps.
+*/
+function detectSloppyRegexps({entry, style}) {
+ const {
+ appliedSections = getApplicableSections({style, matchUrl: tabURL}),
+ wannabeSections = getApplicableSections({style, matchUrl: tabURL, strictRegexp: false}),
+ } = style;
+
+ compileStyleRegExps({style, compileAll: true});
+ entry.hasInvalidRegexps = wannabeSections.some(section =>
+ section.regexps.some(rx => !cachedStyles.regexps.has(rx)));
+ entry.sectionsSkipped = wannabeSections.length - appliedSections.length;
+
+ if (!appliedSections.length) {
+ entry.classList.add('not-applied');
+ $('.style-name', entry).title = t('styleNotAppliedRegexpProblemTooltip');
+ }
+ if (entry.sectionsSkipped || entry.hasInvalidRegexps) {
+ entry.classList.toggle('regexp-partial', entry.sectionsSkipped);
+ entry.classList.toggle('regexp-invalid', entry.hasInvalidRegexps);
+ const indicator = template.regexpProblemIndicator.cloneNode(true);
+ indicator.appendChild(document.createTextNode(entry.sectionsSkipped || '!'));
+ indicator.onclick = EntryOnClick.indicator;
+ $('.main-controls', entry).appendChild(indicator);
}
}
diff --git a/storage.js b/storage.js
index 3f3c3e33..656fcbf0 100644
--- a/storage.js
+++ b/storage.js
@@ -28,7 +28,7 @@ const RX_NAMESPACE = new RegExp([/[\s\r\n]*/,
/(@namespace[\s\r\n]+(?:[^\s\r\n]+[\s\r\n]+)?url\(http:\/\/.*?\);)/,
/[\s\r\n]*/].map(rx => rx.source).join(''), 'g');
const RX_CSS_COMMENTS = /\/\*[\s\S]*?\*\//g;
-
+const SLOPPY_REGEXP_PREFIX = '\0';
// Let manage/popup/edit reuse background page variables
// Note, only 'var'-declared variables are visible from another extension page
@@ -39,11 +39,11 @@ var cachedStyles, prefs;
cachedStyles = bg && bg.cachedStyles || {
bg,
list: null,
- noCode: null,
byId: new Map(),
filters: new Map(),
regexps: new Map(),
urlDomains: new Map(),
+ emptyCode: new Map(), // entire code is comments/whitespace/@namespace
mutex: {
inProgress: false,
onDone: [],
@@ -89,15 +89,12 @@ function getStyles(options, callback) {
const os = tx.objectStore('styles');
os.getAll().onsuccess = event => {
cachedStyles.list = event.target.result || [];
- cachedStyles.noCode = [];
cachedStyles.byId.clear();
for (const style of cachedStyles.list) {
- const noCode = getStyleWithNoCode(style);
- cachedStyles.noCode.push(noCode);
- cachedStyles.byId.set(style.id, {style, noCode});
- compileStyleRegExps(style);
+ cachedStyles.byId.set(style.id, style);
+ compileStyleRegExps({style});
}
- //console.log('%s getStyles %s, invoking cached callbacks: %o', (performance.now() - t0).toFixed(1), JSON.stringify(options), cachedStyles.mutex.onDone.map(e => JSON.stringify(e.options)))
+ //console.debug('%s getStyles %s, invoking cached callbacks: %o', (performance.now() - t0).toFixed(1), JSON.stringify(options), cachedStyles.mutex.onDone.map(e => JSON.stringify(e.options))); // eslint-disable-line max-len
callback(filterStyles(options));
cachedStyles.mutex.inProgress = false;
@@ -134,19 +131,16 @@ function invalidateCache(andNotify, {added, updated, deletedId} = {}) {
if (updated) {
const cached = cachedStyles.byId.get(updated.id);
if (cached) {
- Object.assign(cached.style, updated);
- Object.assign(cached.noCode, getStyleWithNoCode(updated));
- //console.log('cache: updated', updated);
+ Object.assign(cached, updated);
+ //console.debug('cache: updated', updated);
}
cachedStyles.filters.clear();
return;
}
if (added) {
- const noCode = getStyleWithNoCode(added);
cachedStyles.list.push(added);
- cachedStyles.noCode.push(noCode);
- cachedStyles.byId.set(added.id, {style: added, noCode});
- //console.log('cache: added', added);
+ cachedStyles.byId.set(added.id, added);
+ //console.debug('cache: added', added);
cachedStyles.filters.clear();
return;
}
@@ -155,46 +149,47 @@ function invalidateCache(andNotify, {added, updated, deletedId} = {}) {
if (deletedStyle) {
const cachedIndex = cachedStyles.list.indexOf(deletedStyle);
cachedStyles.list.splice(cachedIndex, 1);
- cachedStyles.noCode.splice(cachedIndex, 1);
cachedStyles.byId.delete(deletedId);
- //console.log('cache: deleted', deletedStyle);
+ //console.debug('cache: deleted', deletedStyle);
cachedStyles.filters.clear();
return;
}
}
cachedStyles.list = null;
- cachedStyles.noCode = null;
- //console.log('cache cleared');
+ //console.debug('cache cleared');
cachedStyles.filters.clear();
}
-function filterStyles(options = {}) {
+function filterStyles({
+ enabled,
+ url = null,
+ id = null,
+ matchUrl = null,
+ asHash = null,
+ strictRegexp = true, // used by the popup to detect bad regexps
+} = {}) {
//const t0 = performance.now();
- const enabled = fixBoolean(options.enabled);
- const url = 'url' in options ? options.url : null;
- const id = 'id' in options ? Number(options.id) : null;
- const matchUrl = 'matchUrl' in options ? options.matchUrl : null;
- const code = 'code' in options ? options.code : true;
- const asHash = 'asHash' in options ? options.asHash : false;
+ enabled = fixBoolean(enabled);
+ id = id === null ? null : Number(id);
if (enabled === null
&& url === null
&& id === null
&& matchUrl === null
&& asHash != true) {
- //console.log('%c%s filterStyles SKIPPED LOOP %s', 'color:gray', (performance.now() - t0).toFixed(1), JSON.stringify(options))
- return code ? cachedStyles.list : cachedStyles.noCode;
+ //console.debug('%c%s filterStyles SKIPPED LOOP %s', 'color:gray', (performance.now() - t0).toFixed(1), enabled, id, asHash, strictRegexp, matchUrl); // eslint-disable-line max-len
+ return cachedStyles.list;
}
// silence the inapplicable warning for async code
// eslint-disable-next-line no-use-before-define
const disableAll = asHash && prefs.get('disableAll', false);
// add \t after url to prevent collisions (not sure it can actually happen though)
- const cacheKey = ' ' + enabled + url + '\t' + id + matchUrl + '\t' + code + asHash;
+ const cacheKey = ' ' + enabled + url + '\t' + id + matchUrl + '\t' + asHash + strictRegexp;
const cached = cachedStyles.filters.get(cacheKey);
if (cached) {
- //console.log('%c%s filterStyles REUSED RESPONSE %s', 'color:gray', (performance.now() - t0).toFixed(1), JSON.stringify(options))
+ //console.debug('%c%s filterStyles REUSED RESPONSE %s', 'color:gray', (performance.now() - t0).toFixed(1), enabled, id, asHash, strictRegexp, matchUrl); // eslint-disable-line max-len
cached.hits++;
cached.lastHit = Date.now();
@@ -212,19 +207,22 @@ function filterStyles(options = {}) {
}
const styles = id === null
- ? (code ? cachedStyles.list : cachedStyles.noCode)
- : [(cachedStyles.byId.get(id) || {})[code ? 'style' : 'noCode']];
+ ? cachedStyles.list
+ : [cachedStyles.byId.get(id)];
const filtered = asHash ? {} : [];
if (!styles) {
// may happen when users [accidentally] reopen an old URL
// of edit.html with a non-existent style id parameter
return filtered;
}
+ const needSections = asHash || matchUrl !== null;
+
for (let i = 0, style; (style = styles[i]); i++) {
if ((enabled === null || style.enabled == enabled)
&& (url === null || style.url == url)
&& (id === null || style.id == id)) {
- const sections = (asHash || matchUrl !== null) && getApplicableSections(style, matchUrl);
+ const sections = needSections &&
+ getApplicableSections({style, matchUrl, strictRegexp, stopOnFirst: !asHash});
if (asHash) {
if (sections.length) {
filtered[style.id] = sections;
@@ -234,7 +232,7 @@ function filterStyles(options = {}) {
}
}
}
- //console.log('%s filterStyles %s', (performance.now() - t0).toFixed(1), JSON.stringify(options))
+ //console.debug('%s filterStyles %s', (performance.now() - t0).toFixed(1), enabled, id, asHash, strictRegexp, matchUrl); // eslint-disable-line max-len
cachedStyles.filters.set(cacheKey, {
styles: filtered,
lastHit: Date.now(),
@@ -307,7 +305,7 @@ function saveStyle(style) {
os.put(style).onsuccess = eventPut => {
style.id = style.id || eventPut.target.result;
invalidateCache(notify, existed ? {updated: style} : {added: style});
- compileStyleRegExps(style);
+ compileStyleRegExps({style});
if (notify) {
notifyAllTabs({
method: existed ? 'styleUpdated' : 'styleAdded',
@@ -338,7 +336,7 @@ function saveStyle(style) {
// Give it the ID that was generated
style.id = event.target.result;
invalidateCache(notify, {added: style});
- compileStyleRegExps(style);
+ compileStyleRegExps({style});
if (notify) {
notifyAllTabs({method: 'styleAdded', style, reason});
}
@@ -434,68 +432,88 @@ function getType(o) {
}
-function getApplicableSections(style, url) {
+function getApplicableSections({style, matchUrl, strictRegexp = true, stopOnFirst}) {
+ //let t0 = 0;
const sections = [];
checkingSections:
for (const section of style.sections) {
- // only http, https, file, ftp, and chrome-extension://OWN_EXTENSION_ID allowed
- if (!url.startsWith('http')
- && !url.startsWith('ftp')
- && !url.startsWith('file')
- && !url.startsWith(OWN_ORIGIN)) {
- continue checkingSections;
- }
- if (section.urls.length == 0
- && section.domains.length == 0
- && section.urlPrefixes.length == 0
- && section.regexps.length == 0) {
- sections.push(section);
- continue checkingSections;
- }
- if (section.urls.indexOf(url) != -1) {
- sections.push(section);
- continue checkingSections;
- }
- for (const urlPrefix of section.urlPrefixes) {
- if (url.startsWith(urlPrefix)) {
- sections.push(section);
+ andCollect:
+ do {
+ // only http, https, file, ftp, and chrome-extension://OWN_EXTENSION_ID allowed
+ if (!matchUrl.startsWith('http')
+ && !matchUrl.startsWith('ftp')
+ && !matchUrl.startsWith('file')
+ && !matchUrl.startsWith(OWN_ORIGIN)) {
continue checkingSections;
}
- }
- const urlDomains = cachedStyles.urlDomains.get(url) || getDomains(url);
- for (const domain of urlDomains) {
- if (section.domains.indexOf(domain) != -1) {
- sections.push(section);
- continue checkingSections;
+ if (section.urls.length == 0
+ && section.domains.length == 0
+ && section.urlPrefixes.length == 0
+ && section.regexps.length == 0) {
+ break andCollect;
}
- }
- for (const regexp of section.regexps) {
- let rx = cachedStyles.regexps.get(regexp);
- if (rx == false) {
- // bad regexp
- continue;
+ if (section.urls.indexOf(matchUrl) != -1) {
+ break andCollect;
}
- if (!rx) {
- rx = tryRegExp('^(?:' + regexp + ')$');
- cachedStyles.regexps.set(regexp, rx || false);
- if (!rx) {
- // bad regexp
- continue;
+ for (const urlPrefix of section.urlPrefixes) {
+ if (matchUrl.startsWith(urlPrefix)) {
+ break andCollect;
}
}
- if (rx.test(url)) {
- sections.push(section);
- continue checkingSections;
+ if (section.domains.length) {
+ const urlDomains = cachedStyles.urlDomains.get(matchUrl) || getDomains(matchUrl);
+ for (const domain of urlDomains) {
+ if (section.domains.indexOf(domain) != -1) {
+ break andCollect;
+ }
+ }
+ }
+ for (const regexp of section.regexps) {
+ for (let pass = 1; pass <= (strictRegexp ? 1 : 2); pass++) {
+ const cacheKey = pass == 1 ? regexp : SLOPPY_REGEXP_PREFIX + regexp;
+ let rx = cachedStyles.regexps.get(cacheKey);
+ if (rx == false) {
+ // invalid regexp
+ break;
+ }
+ if (!rx) {
+ const anchored = pass == 1 ? '^(?:' + regexp + ')$' : '^' + regexp + '$';
+ rx = tryRegExp(anchored);
+ cachedStyles.regexps.set(cacheKey, rx || false);
+ if (!rx) {
+ // invalid regexp
+ break;
+ }
+ }
+ if (rx.test(matchUrl)) {
+ break andCollect;
+ }
+ }
+ }
+ continue checkingSections;
+ } while (0);
+ // Collect the section if not empty or namespace-only.
+ // We don't check long code as it's slow both for emptyCode declared as Object
+ // and as Map in case the string is not the same reference used to add the item
+ //const t0start = performance.now();
+ const code = section.code;
+ let isEmpty = code.length < 1000 && cachedStyles.emptyCode.get(code);
+ if (isEmpty === undefined) {
+ isEmpty = !code || !code.trim()
+ || code.indexOf('@namespace') >= 0
+ && code.replace(RX_CSS_COMMENTS, '').replace(RX_NAMESPACE, '').trim() == '';
+ cachedStyles.emptyCode.set(code, isEmpty);
+ }
+ //t0 += performance.now() - t0start;
+ if (!isEmpty) {
+ sections.push(section);
+ if (stopOnFirst) {
+ //t0 >= 0.1 && console.debug('%s emptyCode', t0.toFixed(1)); // eslint-disable-line no-unused-expressions
+ return sections;
}
}
}
- // ignore @namespace-only results
- if (sections.length == 1
- && sections[0].code
- && sections[0].code.indexOf('@namespace') >= 0
- && sections[0].code.replace(RX_CSS_COMMENTS, '').replace(RX_NAMESPACE, '').trim() == '') {
- return [];
- }
+ //t0 >= 0.1 && console.debug('%s emptyCode', t0.toFixed(1)); // eslint-disable-line no-unused-expressions
return sections;
}
@@ -888,18 +906,22 @@ function styleSectionsEqual(styleA, styleB) {
}
-function compileStyleRegExps(style) {
+function compileStyleRegExps({style, compileAll}) {
const t0 = performance.now();
for (const section of style.sections || []) {
for (const regexp of section.regexps) {
- // we want to match the full url, so add ^ and $ if not already present
- if (cachedStyles.regexps.has(regexp)) {
- continue;
- }
- const rx = tryRegExp('^(?:' + regexp + ')$');
- cachedStyles.regexps.set(regexp, rx || false);
- if (performance.now() - t0 > 100) {
- return;
+ for (let pass = 1; pass <= (compileAll ? 2 : 1); pass++) {
+ const cacheKey = pass == 1 ? regexp : SLOPPY_REGEXP_PREFIX + regexp;
+ if (cachedStyles.regexps.has(cacheKey)) {
+ continue;
+ }
+ // according to CSS4 @document specification the entire URL must match
+ const anchored = pass == 1 ? '^(?:' + regexp + ')$' : '^' + regexp + '$';
+ const rx = tryRegExp(anchored);
+ cachedStyles.regexps.set(cacheKey, rx || false);
+ if (!compileAll && performance.now() - t0 > 100) {
+ return;
+ }
}
}
}