9503acc2bf
Thus we account for the case of multiple sections matching the same URL because the order of rules is part of cascading
804 lines
23 KiB
JavaScript
804 lines
23 KiB
JavaScript
/* global LZString */
|
|
'use strict';
|
|
|
|
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;
|
|
// eslint-disable-next-line no-var
|
|
var SLOPPY_REGEXP_PREFIX = '\0';
|
|
|
|
// CSS transition bug workaround: since we insert styles asynchronously,
|
|
// the browsers, especially Firefox, may apply all transitions on page load
|
|
const CSS_TRANSITION_SUPPRESSOR = '* { transition: none !important; }';
|
|
const RX_CSS_TRANSITION_DETECTOR = /([\s\n;/{]|-webkit-|-moz-)transition[\s\n]*:[\s\n]*(?!none)/;
|
|
|
|
// Note, only 'var'-declared variables are visible from another extension page
|
|
// eslint-disable-next-line no-var
|
|
var cachedStyles = {
|
|
list: null, // array of all styles
|
|
byId: new Map(), // all styles indexed by id
|
|
filters: new Map(), // filterStyles() parameters mapped to the returned results, 10k max
|
|
regexps: new Map(), // compiled style regexps
|
|
urlDomains: new Map(), // getDomain() results for 100 last checked urls
|
|
needTransitionPatch: new Map(), // FF bug workaround
|
|
mutex: {
|
|
inProgress: true, // while getStyles() is reading IndexedDB all subsequent calls
|
|
// (initially 'true' to prevent rogue getStyles before dbExec.initialized)
|
|
onDone: [], // to getStyles() are queued and resolved when the first one finishes
|
|
},
|
|
};
|
|
|
|
// eslint-disable-next-line no-var
|
|
var chromeLocal = {
|
|
get(options) {
|
|
return new Promise(resolve => {
|
|
chrome.storage.local.get(options, data => resolve(data));
|
|
});
|
|
},
|
|
set(data) {
|
|
return new Promise(resolve => {
|
|
chrome.storage.local.set(data, () => resolve(data));
|
|
});
|
|
},
|
|
remove(keyOrKeys) {
|
|
return new Promise(resolve => {
|
|
chrome.storage.local.remove(keyOrKeys, resolve);
|
|
});
|
|
},
|
|
getValue(key) {
|
|
return chromeLocal.get(key).then(data => data[key]);
|
|
},
|
|
setValue(key, value) {
|
|
return chromeLocal.set({[key]: value});
|
|
},
|
|
};
|
|
|
|
// eslint-disable-next-line no-var
|
|
var chromeSync = {
|
|
get(options) {
|
|
return new Promise(resolve => {
|
|
chrome.storage.sync.get(options, resolve);
|
|
});
|
|
},
|
|
set(data) {
|
|
return new Promise(resolve => {
|
|
chrome.storage.sync.set(data, () => resolve(data));
|
|
});
|
|
},
|
|
getLZValue(key) {
|
|
return chromeSync.getLZValues([key]).then(data => data[key]);
|
|
},
|
|
getLZValues(keys) {
|
|
return chromeSync.get(keys).then((data = {}) => {
|
|
for (const key of keys) {
|
|
const value = data[key];
|
|
data[key] = value && tryJSONparse(LZString.decompressFromUTF16(value));
|
|
}
|
|
return data;
|
|
});
|
|
},
|
|
setLZValue(key, value) {
|
|
return chromeSync.set({[key]: LZString.compressToUTF16(JSON.stringify(value))});
|
|
}
|
|
};
|
|
|
|
// eslint-disable-next-line no-var
|
|
var dbExec = dbExecIndexedDB;
|
|
dbExec.initialized = false;
|
|
|
|
// we use chrome.storage.local fallback if IndexedDB doesn't save data,
|
|
// which, once detected on the first run, is remembered in chrome.storage.local
|
|
// for reliablility and in localStorage for fast synchronous access
|
|
// (FF may block localStorage depending on its privacy options)
|
|
do {
|
|
const done = () => {
|
|
cachedStyles.mutex.inProgress = false;
|
|
getStyles().then(() => {
|
|
dbExec.initialized = true;
|
|
window.dispatchEvent(new Event('storageReady'));
|
|
});
|
|
};
|
|
const fallback = () => {
|
|
dbExec = dbExecChromeStorage;
|
|
chromeLocal.set({dbInChromeStorage: true});
|
|
localStorage.dbInChromeStorage = 'true';
|
|
ignoreChromeError();
|
|
done();
|
|
};
|
|
const fallbackSet = localStorage.dbInChromeStorage;
|
|
if (fallbackSet === 'true' || !tryCatch(() => indexedDB)) {
|
|
fallback();
|
|
break;
|
|
} else if (fallbackSet === 'false') {
|
|
done();
|
|
break;
|
|
}
|
|
chromeLocal.get('dbInChromeStorage')
|
|
.then(data =>
|
|
data && data.dbInChromeStorage && Promise.reject())
|
|
.then(() =>
|
|
tryCatch(dbExecIndexedDB, 'getAllKeys', IDBKeyRange.lowerBound(1), 1) ||
|
|
Promise.reject())
|
|
.then(({target}) => (
|
|
(target.result || [])[0] ?
|
|
Promise.reject('ok') :
|
|
dbExecIndexedDB('put', {id: -1})))
|
|
.then(() =>
|
|
dbExecIndexedDB('get', -1))
|
|
.then(({target}) => (
|
|
(target.result || {}).id === -1 ?
|
|
dbExecIndexedDB('delete', -1) :
|
|
Promise.reject()))
|
|
.then(() =>
|
|
Promise.reject('ok'))
|
|
.catch(result => {
|
|
if (result === 'ok') {
|
|
chromeLocal.set({dbInChromeStorage: false});
|
|
localStorage.dbInChromeStorage = 'false';
|
|
done();
|
|
} else {
|
|
fallback();
|
|
}
|
|
});
|
|
} while (0);
|
|
|
|
|
|
function dbExecIndexedDB(method, ...args) {
|
|
return new Promise((resolve, reject) => {
|
|
Object.assign(indexedDB.open('stylish', 2), {
|
|
onsuccess(event) {
|
|
const database = event.target.result;
|
|
if (!method) {
|
|
resolve(database);
|
|
} else {
|
|
const transaction = database.transaction(['styles'], 'readwrite');
|
|
const store = transaction.objectStore('styles');
|
|
Object.assign(store[method](...args), {
|
|
onsuccess: event => resolve(event, store, transaction, database),
|
|
onerror: reject,
|
|
});
|
|
}
|
|
},
|
|
onerror(event) {
|
|
console.warn(event.target.error || event.target.errorCode);
|
|
reject(event);
|
|
},
|
|
onupgradeneeded(event) {
|
|
if (event.oldVersion === 0) {
|
|
event.target.result.createObjectStore('styles', {
|
|
keyPath: 'id',
|
|
autoIncrement: true,
|
|
});
|
|
}
|
|
},
|
|
});
|
|
});
|
|
}
|
|
|
|
|
|
function dbExecChromeStorage(method, data) {
|
|
const STYLE_KEY_PREFIX = 'style-';
|
|
switch (method) {
|
|
case 'get':
|
|
return chromeLocal.getValue(STYLE_KEY_PREFIX + data)
|
|
.then(result => ({target: {result}}));
|
|
|
|
case 'put':
|
|
if (!data.id) {
|
|
return getStyles().then(() => {
|
|
data.id = 1;
|
|
for (const style of cachedStyles.list) {
|
|
data.id = Math.max(data.id, style.id + 1);
|
|
}
|
|
return dbExecChromeStorage('put', data);
|
|
});
|
|
}
|
|
return chromeLocal.setValue(STYLE_KEY_PREFIX + data.id, data)
|
|
.then(() => (chrome.runtime.lastError ? Promise.reject() : data.id));
|
|
|
|
case 'delete':
|
|
return chromeLocal.remove(STYLE_KEY_PREFIX + data);
|
|
|
|
case 'getAll':
|
|
return chromeLocal.get(null).then(storage => {
|
|
const styles = [];
|
|
for (const key in storage) {
|
|
if (key.startsWith(STYLE_KEY_PREFIX) &&
|
|
Number(key.substr(STYLE_KEY_PREFIX.length))) {
|
|
styles.push(storage[key]);
|
|
}
|
|
}
|
|
return {target: {result: styles}};
|
|
});
|
|
}
|
|
return Promise.reject();
|
|
}
|
|
|
|
|
|
function getStyles(options) {
|
|
if (cachedStyles.list) {
|
|
return Promise.resolve(filterStyles(options));
|
|
}
|
|
if (cachedStyles.mutex.inProgress) {
|
|
return new Promise(resolve => {
|
|
cachedStyles.mutex.onDone.push({options, resolve});
|
|
});
|
|
}
|
|
cachedStyles.mutex.inProgress = true;
|
|
|
|
return dbExec('getAll').then(event => {
|
|
cachedStyles.list = event.target.result || [];
|
|
cachedStyles.byId.clear();
|
|
for (const style of cachedStyles.list) {
|
|
cachedStyles.byId.set(style.id, style);
|
|
}
|
|
|
|
cachedStyles.mutex.inProgress = false;
|
|
for (const {options, resolve} of cachedStyles.mutex.onDone) {
|
|
resolve(filterStyles(options));
|
|
}
|
|
cachedStyles.mutex.onDone = [];
|
|
return filterStyles(options);
|
|
});
|
|
}
|
|
|
|
|
|
function filterStyles({
|
|
enabled = null,
|
|
url = null,
|
|
id = null,
|
|
matchUrl = null,
|
|
asHash = null,
|
|
strictRegexp = true, // used by the popup to detect bad regexps
|
|
} = {}) {
|
|
enabled = enabled === null || typeof enabled === 'boolean' ? enabled :
|
|
typeof enabled === 'string' ? enabled === 'true' : null;
|
|
id = id === null ? null : Number(id);
|
|
|
|
if (
|
|
enabled === null &&
|
|
url === null &&
|
|
id === null &&
|
|
matchUrl === null &&
|
|
asHash !== true
|
|
) {
|
|
return cachedStyles.list;
|
|
}
|
|
|
|
if (matchUrl && !URLS.supported(matchUrl)) {
|
|
return asHash ? {} : [];
|
|
}
|
|
|
|
const blankHash = asHash && {
|
|
disableAll: prefs.get('disableAll'),
|
|
exposeIframes: prefs.get('exposeIframes'),
|
|
};
|
|
|
|
// add \t after url to prevent collisions (not sure it can actually happen though)
|
|
const cacheKey = ' ' + enabled + url + '\t' + id + matchUrl + '\t' + asHash + strictRegexp;
|
|
const cached = cachedStyles.filters.get(cacheKey);
|
|
if (cached) {
|
|
cached.hits++;
|
|
cached.lastHit = Date.now();
|
|
return asHash
|
|
? Object.assign(blankHash, cached.styles)
|
|
: cached.styles;
|
|
}
|
|
|
|
return filterStylesInternal({
|
|
enabled,
|
|
url,
|
|
id,
|
|
matchUrl,
|
|
asHash,
|
|
strictRegexp,
|
|
blankHash,
|
|
cacheKey,
|
|
});
|
|
}
|
|
|
|
|
|
function filterStylesInternal({
|
|
// js engines don't like big functions (V8 often deoptimized the original filterStyles)
|
|
// it also makes sense to extract the less frequently executed code
|
|
enabled,
|
|
url,
|
|
id,
|
|
matchUrl,
|
|
asHash,
|
|
strictRegexp,
|
|
blankHash,
|
|
cacheKey,
|
|
}) {
|
|
if (matchUrl && !cachedStyles.urlDomains.has(matchUrl)) {
|
|
cachedStyles.urlDomains.set(matchUrl, getDomains(matchUrl));
|
|
for (let i = cachedStyles.urlDomains.size - 100; i > 0; i--) {
|
|
const firstKey = cachedStyles.urlDomains.keys().next().value;
|
|
cachedStyles.urlDomains.delete(firstKey);
|
|
}
|
|
}
|
|
|
|
const styles = id === null
|
|
? cachedStyles.list
|
|
: [cachedStyles.byId.get(id)];
|
|
if (!styles[0]) {
|
|
// may happen when users [accidentally] reopen an old URL
|
|
// of edit.html with a non-existent style id parameter
|
|
return asHash ? blankHash : [];
|
|
}
|
|
const filtered = asHash ? {} : [];
|
|
const needSections = asHash || matchUrl !== null;
|
|
const matchUrlBase = matchUrl && matchUrl.includes('#') && matchUrl.split('#', 1)[0];
|
|
|
|
let style;
|
|
for (let i = 0; (style = styles[i]); i++) {
|
|
if ((enabled === null || style.enabled === enabled)
|
|
&& (url === null || style.url === url)
|
|
&& (id === null || style.id === id)) {
|
|
const sections = needSections &&
|
|
getApplicableSections({
|
|
style,
|
|
matchUrl,
|
|
strictRegexp,
|
|
stopOnFirst: !asHash,
|
|
skipUrlCheck: true,
|
|
matchUrlBase,
|
|
});
|
|
if (asHash) {
|
|
if (sections.length) {
|
|
filtered[style.id] = sections;
|
|
}
|
|
} else if (matchUrl === null || sections.length) {
|
|
filtered.push(style);
|
|
}
|
|
}
|
|
}
|
|
|
|
cachedStyles.filters.set(cacheKey, {
|
|
styles: filtered,
|
|
lastHit: Date.now(),
|
|
hits: 1,
|
|
});
|
|
if (cachedStyles.filters.size > 10000) {
|
|
cleanupCachedFilters();
|
|
}
|
|
|
|
// a shallow copy is needed because the cache doesn't store options like disableAll
|
|
return asHash
|
|
? Object.assign(blankHash, filtered)
|
|
: filtered;
|
|
}
|
|
|
|
|
|
function saveStyle(style) {
|
|
const id = Number(style.id) || null;
|
|
const reason = style.reason;
|
|
const notify = style.notify !== false;
|
|
delete style.method;
|
|
delete style.reason;
|
|
delete style.notify;
|
|
if (!style.name) {
|
|
delete style.name;
|
|
}
|
|
let existed;
|
|
let codeIsUpdated;
|
|
|
|
return maybeCalcDigest()
|
|
.then(maybeImportFix)
|
|
.then(decide);
|
|
|
|
function maybeCalcDigest() {
|
|
if (reason === 'update' || reason === 'update-digest') {
|
|
return calcStyleDigest(style).then(digest => {
|
|
style.originalDigest = digest;
|
|
});
|
|
}
|
|
return Promise.resolve();
|
|
}
|
|
|
|
function maybeImportFix() {
|
|
if (reason === 'import') {
|
|
style.originalDigest = style.originalDigest || style.styleDigest; // TODO: remove in the future
|
|
delete style.styleDigest; // TODO: remove in the future
|
|
if (typeof style.originalDigest !== 'string' || style.originalDigest.length !== 40) {
|
|
delete style.originalDigest;
|
|
}
|
|
}
|
|
}
|
|
|
|
function decide() {
|
|
if (id !== null) {
|
|
// Update or create
|
|
style.id = id;
|
|
return dbExec('get', id).then((event, store) => {
|
|
const oldStyle = event.target.result;
|
|
existed = Boolean(oldStyle);
|
|
if (reason === 'update-digest' && oldStyle.originalDigest === style.originalDigest) {
|
|
return style;
|
|
}
|
|
codeIsUpdated = !existed || 'sections' in style && !styleSectionsEqual(style, oldStyle);
|
|
style = Object.assign({}, oldStyle, style);
|
|
return write(style, store);
|
|
});
|
|
} else {
|
|
// Create
|
|
delete style.id;
|
|
style = Object.assign({
|
|
// Set optional things if they're undefined
|
|
enabled: true,
|
|
updateUrl: null,
|
|
md5Url: null,
|
|
url: null,
|
|
originalMd5: null,
|
|
}, style);
|
|
return write(style);
|
|
}
|
|
}
|
|
|
|
function write(style, store) {
|
|
style.sections = normalizeStyleSections(style);
|
|
if (store) {
|
|
return new Promise(resolve => {
|
|
store.put(style).onsuccess = event => resolve(done(event));
|
|
});
|
|
} else {
|
|
return dbExec('put', style).then(done);
|
|
}
|
|
}
|
|
|
|
function done(event) {
|
|
if (reason === 'update-digest') {
|
|
return style;
|
|
}
|
|
style.id = style.id || event.target.result;
|
|
invalidateCache(existed ? {updated: style} : {added: style});
|
|
if (notify) {
|
|
notifyAllTabs({
|
|
method: existed ? 'styleUpdated' : 'styleAdded',
|
|
style, codeIsUpdated, reason,
|
|
});
|
|
}
|
|
return style;
|
|
}
|
|
}
|
|
|
|
|
|
function deleteStyle({id, notify = true}) {
|
|
id = Number(id);
|
|
return dbExec('delete', id).then(() => {
|
|
invalidateCache({deletedId: id});
|
|
if (notify) {
|
|
notifyAllTabs({method: 'styleDeleted', id});
|
|
}
|
|
return id;
|
|
});
|
|
}
|
|
|
|
|
|
function getApplicableSections({
|
|
style,
|
|
matchUrl,
|
|
strictRegexp = true,
|
|
// filterStylesInternal() sets the following to avoid recalc on each style:
|
|
stopOnFirst,
|
|
skipUrlCheck,
|
|
matchUrlBase = matchUrl.includes('#') && matchUrl.split('#', 1)[0],
|
|
// as per spec the fragment portion is ignored in @-moz-document:
|
|
// https://www.w3.org/TR/2012/WD-css3-conditional-20120911/#url-of-doc
|
|
// but the spec is outdated and doesn't account for SPA sites
|
|
// so we only respect it in case of url("http://exact.url/without/hash")
|
|
}) {
|
|
if (!skipUrlCheck && !URLS.supported(matchUrl)) {
|
|
return [];
|
|
}
|
|
const sections = [];
|
|
for (const section of style.sections) {
|
|
const {urls, domains, urlPrefixes, regexps, code} = section;
|
|
const isGlobal = !urls.length && !urlPrefixes.length && !domains.length && !regexps.length;
|
|
const isMatching = !isGlobal && (
|
|
urls.length
|
|
&& (urls.includes(matchUrl) || matchUrlBase && urls.includes(matchUrlBase))
|
|
|| urlPrefixes.length
|
|
&& arraySomeIsPrefix(urlPrefixes, matchUrl)
|
|
|| domains.length
|
|
&& arraySomeIn(cachedStyles.urlDomains.get(matchUrl) || getDomains(matchUrl), domains)
|
|
|| regexps.length
|
|
&& arraySomeMatches(regexps, matchUrl, strictRegexp));
|
|
if (isGlobal && !styleCodeEmpty(code) || isMatching) {
|
|
sections.push(section);
|
|
if (stopOnFirst) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
return sections;
|
|
|
|
function arraySomeIsPrefix(array, string) {
|
|
for (const prefix of array) {
|
|
if (string.startsWith(prefix)) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function arraySomeIn(array, haystack) {
|
|
for (const el of array) {
|
|
if (haystack.indexOf(el) >= 0) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function arraySomeMatches(array, matchUrl, strictRegexp) {
|
|
for (const regexp of array) {
|
|
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)) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
}
|
|
|
|
|
|
function styleCodeEmpty(code) {
|
|
// Collect the global section if it's not empty, not comment-only, not namespace-only.
|
|
const cmtOpen = code && code.indexOf('/*');
|
|
if (cmtOpen >= 0) {
|
|
const cmtCloseLast = code.lastIndexOf('*/');
|
|
if (cmtCloseLast < 0) {
|
|
code = code.substr(0, cmtOpen);
|
|
} else {
|
|
code = code.substr(0, cmtOpen) +
|
|
code.substring(cmtOpen, cmtCloseLast + 2).replace(RX_CSS_COMMENTS, '') +
|
|
code.substr(cmtCloseLast + 2);
|
|
}
|
|
}
|
|
return !code
|
|
|| !code.trim()
|
|
|| code.includes('@namespace') && !code.replace(RX_NAMESPACE, '').trim();
|
|
}
|
|
|
|
|
|
function styleSectionsEqual({sections: a}, {sections: b}) {
|
|
if (!a || !b) {
|
|
return undefined;
|
|
}
|
|
if (a.length !== b.length) {
|
|
return false;
|
|
}
|
|
// order of sections should be identical to account for the case of multiple
|
|
// sections matching the same URL because the order of rules is part of cascading
|
|
return a.every((sectionA, index) => propertiesEqual(sectionA, b[index]));
|
|
|
|
function propertiesEqual(secA, secB) {
|
|
for (const name of ['urlPrefixes', 'urls', 'domains', 'regexps']) {
|
|
if (!equalOrEmpty(secA[name], secB[name], 'every', arrayMirrors)) {
|
|
return false;
|
|
}
|
|
}
|
|
return equalOrEmpty(secA.code, secB.code, 'substr', (a, b) => a === b);
|
|
}
|
|
|
|
function equalOrEmpty(a, b, telltale, comparator) {
|
|
const typeA = a && typeof a[telltale] === 'function';
|
|
const typeB = b && typeof b[telltale] === 'function';
|
|
return (
|
|
(a === null || a === undefined || (typeA && !a.length)) &&
|
|
(b === null || b === undefined || (typeB && !b.length))
|
|
) || typeA && typeB && a.length === b.length && comparator(a, b);
|
|
}
|
|
|
|
function arrayMirrors(array1, array2) {
|
|
return (
|
|
array1.every(el => array2.includes(el)) &&
|
|
array2.every(el => array1.includes(el))
|
|
);
|
|
}
|
|
}
|
|
|
|
|
|
function invalidateCache({added, updated, deletedId} = {}) {
|
|
if (!cachedStyles.list) {
|
|
return;
|
|
}
|
|
const id = added ? added.id : updated ? updated.id : deletedId;
|
|
const cached = cachedStyles.byId.get(id);
|
|
if (updated) {
|
|
if (cached) {
|
|
Object.assign(cached, updated);
|
|
cachedStyles.filters.clear();
|
|
cachedStyles.needTransitionPatch.delete(id);
|
|
return;
|
|
} else {
|
|
added = updated;
|
|
}
|
|
}
|
|
if (added) {
|
|
if (!cached) {
|
|
cachedStyles.list.push(added);
|
|
cachedStyles.byId.set(added.id, added);
|
|
cachedStyles.filters.clear();
|
|
cachedStyles.needTransitionPatch.delete(id);
|
|
}
|
|
return;
|
|
}
|
|
if (deletedId !== undefined) {
|
|
if (cached) {
|
|
const cachedIndex = cachedStyles.list.indexOf(cached);
|
|
cachedStyles.list.splice(cachedIndex, 1);
|
|
cachedStyles.byId.delete(deletedId);
|
|
cachedStyles.filters.clear();
|
|
cachedStyles.needTransitionPatch.delete(id);
|
|
return;
|
|
}
|
|
}
|
|
cachedStyles.list = null;
|
|
cachedStyles.filters.clear();
|
|
cachedStyles.needTransitionPatch.clear(id);
|
|
}
|
|
|
|
|
|
function cleanupCachedFilters({force = false} = {}) {
|
|
if (!force) {
|
|
debounce(cleanupCachedFilters, 1000, {force: true});
|
|
return;
|
|
}
|
|
const size = cachedStyles.filters.size;
|
|
const oldestHit = cachedStyles.filters.values().next().value.lastHit;
|
|
const now = Date.now();
|
|
const timeSpan = now - oldestHit;
|
|
const recencyWeight = 5 / size;
|
|
const hitWeight = 1 / 4; // we make ~4 hits per URL
|
|
const lastHitWeight = 10;
|
|
// delete the oldest 10%
|
|
[...cachedStyles.filters.entries()]
|
|
.map(([id, v], index) => ({
|
|
id,
|
|
weight:
|
|
index * recencyWeight +
|
|
v.hits * hitWeight +
|
|
(v.lastHit - oldestHit) / timeSpan * lastHitWeight,
|
|
}))
|
|
.sort((a, b) => a.weight - b.weight)
|
|
.slice(0, size / 10 + 1)
|
|
.forEach(({id}) => cachedStyles.filters.delete(id));
|
|
}
|
|
|
|
|
|
function getDomains(url) {
|
|
let d = /.*?:\/*([^/:]+)|$/.exec(url)[1];
|
|
if (!d || url.startsWith('file:')) {
|
|
return [];
|
|
}
|
|
const domains = [d];
|
|
while (d.indexOf('.') !== -1) {
|
|
d = d.substring(d.indexOf('.') + 1);
|
|
domains.push(d);
|
|
}
|
|
return domains;
|
|
}
|
|
|
|
|
|
function normalizeStyleSections({sections}) {
|
|
// retain known properties in an arbitrarily predefined order
|
|
return (sections || []).map(section => ({
|
|
code: section.code || '',
|
|
urls: section.urls || [],
|
|
urlPrefixes: section.urlPrefixes || [],
|
|
domains: section.domains || [],
|
|
regexps: section.regexps || [],
|
|
}));
|
|
}
|
|
|
|
|
|
function calcStyleDigest(style) {
|
|
const jsonString = style.usercssData ?
|
|
style.sourceCode : JSON.stringify(normalizeStyleSections(style));
|
|
const text = new TextEncoder('utf-8').encode(jsonString);
|
|
return crypto.subtle.digest('SHA-1', text).then(hex);
|
|
|
|
function hex(buffer) {
|
|
const parts = [];
|
|
const PAD8 = '00000000';
|
|
const view = new DataView(buffer);
|
|
for (let i = 0; i < view.byteLength; i += 4) {
|
|
parts.push((PAD8 + view.getUint32(i).toString(16)).slice(-8));
|
|
}
|
|
return parts.join('');
|
|
}
|
|
}
|
|
|
|
|
|
function handleCssTransitionBug({tabId, frameId, url, styles}) {
|
|
for (let id in styles) {
|
|
id |= 0;
|
|
if (!id) {
|
|
continue;
|
|
}
|
|
let need = cachedStyles.needTransitionPatch.get(id);
|
|
if (need === false) {
|
|
continue;
|
|
}
|
|
if (need !== true) {
|
|
need = styles[id].some(sectionContainsTransitions);
|
|
cachedStyles.needTransitionPatch.set(id, need);
|
|
if (!need) {
|
|
continue;
|
|
}
|
|
}
|
|
if (FIREFOX && !url.startsWith(URLS.ownOrigin)) {
|
|
patchFirefox();
|
|
} else {
|
|
styles.needTransitionPatch = true;
|
|
}
|
|
break;
|
|
}
|
|
|
|
function patchFirefox() {
|
|
const options = {
|
|
frameId,
|
|
code: CSS_TRANSITION_SUPPRESSOR,
|
|
matchAboutBlank: true,
|
|
};
|
|
if (FIREFOX >= 53) {
|
|
options.cssOrigin = 'user';
|
|
}
|
|
browser.tabs.insertCSS(tabId, Object.assign(options, {
|
|
runAt: 'document_start',
|
|
})).then(() => setTimeout(() => {
|
|
browser.tabs.removeCSS(tabId, options).catch(ignoreChromeError);
|
|
})).catch(ignoreChromeError);
|
|
}
|
|
|
|
function sectionContainsTransitions(section) {
|
|
let code = section.code;
|
|
const firstTransition = code.indexOf('transition');
|
|
if (firstTransition < 0) {
|
|
return false;
|
|
}
|
|
const firstCmt = code.indexOf('/*');
|
|
// check the part before the first comment
|
|
if (firstCmt < 0 || firstTransition < firstCmt) {
|
|
if (quickCheckAround(code, firstTransition)) {
|
|
return true;
|
|
} else if (firstCmt < 0) {
|
|
return false;
|
|
}
|
|
}
|
|
// check the rest
|
|
const lastCmt = code.lastIndexOf('*/');
|
|
if (lastCmt < firstCmt) {
|
|
// the comment is unclosed and we already checked the preceding part
|
|
return false;
|
|
}
|
|
let mid = code.slice(firstCmt, lastCmt + 2);
|
|
mid = mid.indexOf('*/') === mid.length - 2 ? '' : mid.replace(RX_CSS_COMMENTS, '');
|
|
code = mid + code.slice(lastCmt + 2);
|
|
return quickCheckAround(code) || RX_CSS_TRANSITION_DETECTOR.test(code);
|
|
}
|
|
|
|
function quickCheckAround(code, pos = code.indexOf('transition')) {
|
|
return RX_CSS_TRANSITION_DETECTOR.test(code.substr(Math.max(0, pos - 10), 50));
|
|
}
|
|
}
|