Remove code:false mode; show sloppy regexps in popup

* Now that our own pages retrieve the styles directly via getStylesSafe the only 0.001% of cases where code:false would be needed (the browser is starting up with some of the tabs showing our built-in pages like editor or manage) is not worth optimizing for.

* According to CSS4 @document specification the entire URL must match. Stylish-for-Chrome implemented it incorrectly since the very beginning. We 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.
This commit is contained in:
tophf 2017-03-31 02:18:41 +03:00
parent 4bc7b55b91
commit f8d13d8dec
9 changed files with 487 additions and 218 deletions

View File

@ -12,6 +12,7 @@ globals:
# messaging.js
OWN_ORIGIN: false
KEEP_CHANNEL_OPEN: false
RX_SUPPORTED_URLS: false
configureCommands: false
notifyAllTabs: false
refreshAllTabs: false

View File

@ -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 <a href='https://developer.mozilla.org/docs/Web/CSS/@document'>CSS4 @document specification</a> 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"

View File

@ -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) {

View File

@ -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)

View File

@ -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) {

304
popup.css
View File

@ -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;
}
}

View File

@ -37,7 +37,19 @@
<div id="no-styles" class="entry" i18n-text="noStylesForSite"></div>
</template>
<script src="localization.js"></script>
<template data-id="regexpProblemIndicator">
<div class="regexp-problem-indicator" i18n-title="styleRegexpProblemTooltip"></div>
</template>
<template data-id="regexpProblemExplanation">
<div id="regexp-explanation">
<div id="regexp-partial" i18n-html="styleRegexpPartialExplanation"></div>
<div id="regexp-invalid" i18n-text="styleRegexpInvalidExplanation"></div>
<button i18n-text="confirmOK"></button>
</div>
</template>
<script src="localization.js"></script>
<script src="health.js"></script>
<script src="storage.js"></script>
<script src="messaging.js"></script>
@ -81,7 +93,7 @@
<a id="find-styles-link" href="#" i18n-text="findStylesForSite"></a>
</div>
<div id="write-style">
<span id="write-style-for" i18n-text="writeStyleFor"><br></span>
<span id="write-style-for" i18n-text="writeStyleFor"></span>
</div>
</div>
<!-- Actions -->

149
popup.js
View File

@ -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);
}
}

View File

@ -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;
}
}
}
}