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:
parent
4bc7b55b91
commit
f8d13d8dec
|
@ -12,6 +12,7 @@ globals:
|
|||
# messaging.js
|
||||
OWN_ORIGIN: false
|
||||
KEEP_CHANNEL_OPEN: false
|
||||
RX_SUPPORTED_URLS: false
|
||||
configureCommands: false
|
||||
notifyAllTabs: false
|
||||
refreshAllTabs: false
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
304
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
16
popup.html
16
popup.html
|
@ -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
149
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);
|
||||
}
|
||||
}
|
||||
|
|
212
storage.js
212
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user