stylus/background/style-manager.js

417 lines
10 KiB
JavaScript
Raw Normal View History

2018-10-05 10:47:52 +00:00
/* eslint no-eq-null: 0, eqeqeq: [2, "smart"] */
2018-10-06 05:02:45 +00:00
/*
global createCache db calcStyleDigest normalizeStyleSections db promisify
2018-10-06 07:33:18 +00:00
getStyleWithNoCode msg
2018-10-06 05:02:45 +00:00
*/
2018-10-04 04:46:19 +00:00
'use strict';
2018-10-03 19:35:07 +00:00
const styleManager = (() => {
const preparing = prepare();
2018-10-04 04:46:19 +00:00
const styles = new Map();
2018-10-03 19:35:07 +00:00
const cachedStyleForUrl = createCache();
const compiledRe = createCache();
const compiledExclusion = createCache();
const BAD_MATCHER = {test: () => false};
// FIXME: do we have to prepare `styles` map for all methods?
return ensurePrepared({
2018-10-05 10:47:52 +00:00
// styles,
// cachedStyleForUrl,
2018-10-04 17:03:40 +00:00
getStylesInfo,
getSectionsByUrl,
2018-10-03 19:35:07 +00:00
installStyle,
deleteStyle,
setStyleExclusions,
2018-10-04 17:03:40 +00:00
editSave,
2018-10-05 10:47:52 +00:00
toggleStyle,
getAllStyles, // used by import-export
2018-10-06 05:45:37 +00:00
getStylesInfoByUrl, // used by popup
2018-10-05 10:47:52 +00:00
countStyles,
2018-10-03 19:35:07 +00:00
// TODO: get all styles API?
// TODO: get style by ID?
});
2018-10-05 10:47:52 +00:00
function getAllStyles() {
return [...styles.values()].map(s => s.data);
}
2018-10-04 17:03:40 +00:00
function toggleStyle(id, enabled) {
const style = styles.get(id);
2018-10-05 10:47:52 +00:00
const newData = Object.assign({}, style.data, {enabled});
return saveStyle(newData)
.then(newData => {
style.data = newData;
2018-10-06 05:02:45 +00:00
const message = {
2018-10-05 10:47:52 +00:00
method: 'styleUpdated',
codeIsUpdated: false,
style: {id, enabled}
2018-10-06 05:02:45 +00:00
};
if ([...style.appliesTo].every(isExtensionUrl)) {
return msg.broadcastExtension(message);
}
2018-10-06 07:11:01 +00:00
return msg.broadcast(message, tab => style.appliesTo.has(tab.url));
2018-10-05 10:47:52 +00:00
})
.then(() => id);
2018-10-04 17:03:40 +00:00
}
2018-10-05 10:47:52 +00:00
function isExtensionUrl(url) {
return /^\w+?-extension:\/\//.test(url);
}
function getStylesInfo(filter) {
if (filter && filter.id) {
return [getStyleWithNoCode(styles.get(filter.id).data)];
}
return [...styles.values()]
.filter(s => !filter || filterMatchStyle(filter, s.data))
.map(s => getStyleWithNoCode(s.data));
}
2018-10-05 13:28:19 +00:00
function countStyles(filter) {
if (!filter) {
return styles.size;
}
if (filter.id) {
return styles.has(filter.id) ? 1 : 0;
}
return [...styles.values()]
.filter(s => filterMatchStyle(filter, s.data))
.length;
}
2018-10-05 10:47:52 +00:00
function filterMatchStyle(filter, style) {
for (const key of Object.keys(filter)) {
if (filter[key] !== style[key]) {
return false;
}
}
return true;
2018-10-04 17:03:40 +00:00
}
2018-10-06 05:45:37 +00:00
function editSave(data) {
data = Object.assign({}, styles.get(data.id).data, data);
return saveStyle(data)
.then(newData =>
broadcastStyleUpdated(newData)
.then(() => newData)
);
2018-10-05 13:28:19 +00:00
}
2018-10-03 19:35:07 +00:00
2018-10-06 05:45:37 +00:00
function setStyleExclusions(id, exclusions) {
const data = Object.assign({}, styles.get(id), {exclusions});
return saveStyle(data)
.then(newData =>
broadcastStyleUpdated(newData)
.then(() => newData)
);
}
2018-10-03 19:35:07 +00:00
function ensurePrepared(methods) {
for (const [name, fn] in Object.entries(methods)) {
methods[name] = (...args) =>
preparing.then(() => fn(...args));
}
return methods;
}
function deleteStyle(id) {
2018-10-05 10:47:52 +00:00
const style = styles.get(id);
2018-10-03 19:35:07 +00:00
return db.exec('delete', id)
.then(() => {
2018-10-05 10:47:52 +00:00
for (const url of style.appliesTo) {
const cache = cachedStyleForUrl.get(url);
2018-10-06 07:11:01 +00:00
if (cache) {
delete cache[id];
}
2018-10-05 10:47:52 +00:00
}
styles.delete(id);
2018-10-06 05:02:45 +00:00
return msg.broadcast({
2018-10-05 10:47:52 +00:00
method: 'styleDeleted',
2018-10-06 05:02:45 +00:00
style: {id}
2018-10-06 07:11:01 +00:00
}, tab => style.appliesTo.has(tab.url));
2018-10-05 10:47:52 +00:00
})
.then(() => id);
2018-10-03 19:35:07 +00:00
}
2018-10-05 13:28:19 +00:00
function createNewStyle() {
return {
enabled: true,
updateUrl: null,
md5Url: null,
url: null,
originalMd5: null,
installDate: Date.now()
};
}
function installStyle(data) {
2018-10-06 05:27:58 +00:00
const style = styles.get(data.id);
2018-10-05 13:28:19 +00:00
if (!style) {
data = Object.assign(createNewStyle(), data);
} else {
data = Object.assign({}, style.data, data);
}
// FIXME: update installDate?
return calcStyleDigest(data)
2018-10-03 19:35:07 +00:00
.then(digest => {
2018-10-05 13:28:19 +00:00
data.originalDigest = digest;
return saveStyle(data);
2018-10-03 19:35:07 +00:00
})
2018-10-06 05:27:58 +00:00
.then(newData =>
broadcastStyleUpdated(newData)
.then(() => newData)
);
}
function broadcastStyleUpdated(newData) {
const style = styles.get(newData.id);
if (!style) {
// new style
const appliesTo = new Set();
styles.set(newData.id, {
appliesTo,
data: newData
});
return Promise.all([
msg.broadcastExtension({method: 'styleAdded', style: getStyleWithNoCode(newData)}),
msg.broadcastTab(tab => getStyleAddedMessage(tab, newData, appliesTo))
]);
2018-10-06 05:45:37 +00:00
}
const excluded = new Set();
const updated = new Map();
for (const url of style.appliesTo) {
const code = getAppliedCode(url, newData);
const cache = cachedStyleForUrl.get(url);
if (!code) {
excluded.add(url);
if (cache) {
delete cache[newData.id];
2018-10-05 13:28:19 +00:00
}
2018-10-06 05:45:37 +00:00
} else {
updated.set(url, code);
2018-10-06 07:11:01 +00:00
if (cache) {
cache[newData.id] = {
id: newData.id,
enabled: newData.enabled,
code
};
}
2018-10-06 05:27:58 +00:00
}
}
2018-10-06 05:45:37 +00:00
style.appliesTo = new Set(updated.keys());
return Promise.all([
msg.broadcastExtension({method: 'styleUpdated', style: getStyleWithNoCode(newData)}),
msg.broadcastTab(tab => {
if (excluded.has(tab.url)) {
return {
method: 'styleDeleted',
style: {id: newData.id}
};
}
if (updated.has(tab.url)) {
return {
method: 'styleUpdated',
style: {id: newData.id, sections: updated.get(tab.url)}
};
}
return getStyleAddedMessage(tab, newData, style.appliesTo);
})
]);
2018-10-03 19:35:07 +00:00
}
2018-10-06 05:27:58 +00:00
function getStyleAddedMessage(tab, data, appliesTo) {
2018-10-06 05:02:45 +00:00
const code = getAppliedCode(tab.url, data);
if (!code) {
return;
}
const cache = cachedStyleForUrl.get(tab.url);
if (cache) {
2018-10-06 07:11:01 +00:00
cache[data.id] = {
id: data.id,
enabled: data.enabled,
code
};
2018-10-06 05:02:45 +00:00
}
appliesTo.add(tab.url);
return {
method: 'styleAdded',
style: {
id: data.id,
enabled: data.enabled,
sections: code
}
};
}
2018-10-03 19:35:07 +00:00
function importStyle(style) {
// FIXME: move this to importer
// 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 saveStyle(style) {
2018-10-06 05:02:45 +00:00
if (!style.name) {
throw new Error('style name is empty');
}
2018-10-06 05:27:58 +00:00
return db.exec('put', style)
2018-10-03 19:35:07 +00:00
.then(event => {
if (style.id == null) {
style.id = event.target.result;
}
return style;
});
}
2018-10-06 05:45:37 +00:00
function getStylesInfoByUrl(url) {
const sections = getSectionsByUrl(url);
return Object.keys(sections)
.map(k => getStyleWithNoCode(styles.get(Number(k)).data));
2018-10-05 10:47:52 +00:00
}
function getSectionsByUrl(url, filterId) {
2018-10-06 07:11:01 +00:00
let cache = cachedStyleForUrl.get(url);
if (!cache) {
cache = {};
2018-10-05 10:47:52 +00:00
for (const {appliesTo, data} of styles.values()) {
2018-10-05 13:28:19 +00:00
const code = getAppliedCode(url, data);
2018-10-05 10:47:52 +00:00
if (code) {
2018-10-06 07:11:01 +00:00
cache[data.id] = {
id: data.id,
enabled: data.enabled,
2018-10-06 07:22:04 +00:00
code
2018-10-06 07:11:01 +00:00
};
2018-10-05 10:47:52 +00:00
appliesTo.add(url);
2018-10-03 19:35:07 +00:00
}
}
2018-10-06 07:11:01 +00:00
cachedStyleForUrl.set(url, cache);
2018-10-05 10:47:52 +00:00
}
if (filterId) {
2018-10-06 07:11:01 +00:00
return {[filterId]: cache[filterId]};
2018-10-03 19:35:07 +00:00
}
2018-10-06 07:11:01 +00:00
return cache;
2018-10-03 19:35:07 +00:00
}
2018-10-05 13:28:19 +00:00
function getAppliedCode(url, data) {
if (!urlMatchStyle(url, data)) {
return;
}
let code = '';
for (const section of data.sections) {
if (urlMatchSection(url, section)) {
code += section.code;
}
}
2018-10-06 07:33:18 +00:00
return isCodeEmpty(code) ? null : code;
}
function isCodeEmpty(code) {
const rx = /\s+|\/\*[\s\S]*?\*\/|@namespace[^;]+;|@charset[^;]+;/giy;
while (rx.exec(code)) {
if (rx.lastIndex === code.length) {
return true;
}
}
return false;
2018-10-05 13:28:19 +00:00
}
2018-10-03 19:35:07 +00:00
function prepare() {
return db.exec('getAll').then(event => {
2018-10-05 10:47:52 +00:00
const styleList = event.target.result;
if (!styleList) {
return;
}
2018-10-03 19:35:07 +00:00
for (const style of styleList) {
2018-10-05 10:47:52 +00:00
styles.set(style.id, {
appliesTo: new Set(),
data: style
});
2018-10-03 19:35:07 +00:00
if (!style.name) {
style.name = 'ID: ' + style.id;
}
}
});
}
function urlMatchStyle(url, style) {
2018-10-04 04:46:19 +00:00
if (style.exclusions && style.exclusions.some(e => compileExclusion(e).test(url))) {
2018-10-03 19:35:07 +00:00
return false;
}
return true;
}
function urlMatchSection(url, section) {
// FIXME: match sub domains?
if (section.domains && section.domains.includes(getDomain(url))) {
return true;
}
if (section.urlPrefixes && section.urlPrefixes.some(p => url.startsWith(p))) {
return true;
}
if (section.urls && section.urls.includes(getUrlNoHash(url))) {
return true;
}
if (section.regexps && section.regexps.some(r => compileRe(r).test(url))) {
return true;
}
2018-10-06 05:48:46 +00:00
if (
(!section.regexps || !section.regexps.length) &&
(!section.urlPrefixes || !section.urlPrefixes.length) &&
(!section.urls || !section.urls.length) &&
(!section.domains || !section.domains.length)
2018-10-06 07:11:01 +00:00
) {
return true;
}
2018-10-03 19:35:07 +00:00
return false;
}
function compileRe(text) {
let re = compiledRe.get(text);
if (!re) {
// FIXME: it should be `$({text})$` but we don't use the standard for compatibility
re = tryRegExp(`^${text}$`);
if (!re) {
re = BAD_MATCHER;
}
compiledRe.set(text, re);
}
return re;
}
function compileExclusion(text) {
let re = compiledExclusion.get(text);
if (!re) {
re = tryRegExp(buildGlob(text));
if (!re) {
re = BAD_MATCHER;
}
compiledExclusion.set(text, re);
}
return re;
}
function buildGlob(text) {
const prefix = text[0] === '^' ? '' : '\\b';
const suffix = text[text.length - 1] === '$' ? '' : '\\b';
return `${prefix}${escape(text)}${suffix}`;
function escape(text) {
// FIXME: using .* everywhere is slow
return text.replace(/[.*]/g, m => m === '.' ? '\\.' : '.*');
}
}
function getDomain(url) {
// FIXME: use a naive regexp
2018-10-06 07:11:01 +00:00
return url.match(/^[\w-]+:\/\/(?:[\w:-]+@)?([^:/#]+)/)[1];
2018-10-03 19:35:07 +00:00
}
function getUrlNoHash(url) {
return url.split('#')[0];
}
})();
2018-10-05 10:47:52 +00:00
function notifyAllTabs() {}