613 lines
		
	
	
		
			17 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			613 lines
		
	
	
		
			17 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| function getDatabase(ready, error) {
 | |
| 	var dbOpenRequest = window.indexedDB.open("stylish", 2);
 | |
| 	dbOpenRequest.onsuccess = function(e) {
 | |
| 		ready(e.target.result);
 | |
| 	};
 | |
| 	dbOpenRequest.onerror = function(event) {
 | |
| 		console.log(event.target.errorCode);
 | |
| 		if (error) {
 | |
| 			error(event);
 | |
| 		}
 | |
| 	};
 | |
| 	dbOpenRequest.onupgradeneeded = function(event) {
 | |
| 		if (event.oldVersion == 0) {
 | |
| 			var os = event.target.result.createObjectStore("styles", {keyPath: 'id', autoIncrement: true});
 | |
| 			webSqlStorage.migrate();
 | |
| 		}
 | |
| 	}
 | |
| };
 | |
| 
 | |
| var cachedStyles = null;
 | |
| function getStyles(options, callback) {
 | |
| 	if (cachedStyles != null) {
 | |
| 		callback(filterStyles(cachedStyles, options));
 | |
| 		return;
 | |
| 	}
 | |
| 	getDatabase(function(db) {
 | |
| 		var tx = db.transaction(["styles"], "readonly");
 | |
| 		var os = tx.objectStore("styles");
 | |
| 		var all = [];
 | |
| 		os.openCursor().onsuccess = function(event) {
 | |
| 			var cursor = event.target.result;
 | |
| 			if (cursor) {
 | |
| 				var s = cursor.value;
 | |
| 				s.id = cursor.key;
 | |
| 				all.push(cursor.value);
 | |
| 				cursor.continue();
 | |
| 			} else {
 | |
| 				cachedStyles = all;
 | |
| 				try{
 | |
| 					callback(filterStyles(all, options));
 | |
| 				} catch(e){
 | |
| 					// no error in console, it works
 | |
| 				}
 | |
| 			}
 | |
| 		};
 | |
|   }, null);
 | |
| }
 | |
| 
 | |
| function invalidateCache(andNotify) {
 | |
| 	cachedStyles = null;
 | |
| 	if (andNotify) {
 | |
| 		chrome.runtime.sendMessage({method: "invalidateCache"});
 | |
| 	}
 | |
| }
 | |
| 
 | |
| function filterStyles(styles, options) {
 | |
| 	var enabled = fixBoolean(options.enabled);
 | |
| 	var url = "url" in options ? options.url : null;
 | |
| 	var id = "id" in options ? Number(options.id) : null;
 | |
| 	var matchUrl = "matchUrl" in options ? options.matchUrl : null;
 | |
| 
 | |
| 	if (enabled != null) {
 | |
| 		styles = styles.filter(function(style) {
 | |
| 			return style.enabled == enabled;
 | |
| 		});
 | |
| 	}
 | |
| 	if (url != null) {
 | |
| 		styles = styles.filter(function(style) {
 | |
| 			return style.url == url;
 | |
| 		});
 | |
| 	}
 | |
| 	if (id != null) {
 | |
| 		styles = styles.filter(function(style) {
 | |
| 			return style.id == id;
 | |
| 		});
 | |
| 	}
 | |
| 	if (matchUrl != null) {
 | |
| 		// Return as a hash from style to applicable sections? Can only be used with matchUrl.
 | |
| 		var asHash = "asHash" in options ? options.asHash : false;
 | |
| 		if (asHash) {
 | |
| 			var h = {disableAll: prefs.get("disableAll", false)};
 | |
| 			styles.forEach(function(style) {
 | |
| 				var applicableSections = getApplicableSections(style, matchUrl);
 | |
| 				if (applicableSections.length > 0) {
 | |
| 					h[style.id] = applicableSections;
 | |
| 				}
 | |
| 			});
 | |
| 			return h;
 | |
| 		}
 | |
| 		styles = styles.filter(function(style) {
 | |
| 			var applicableSections = getApplicableSections(style, matchUrl);
 | |
| 			return applicableSections.length > 0;
 | |
| 		});
 | |
| 	}
 | |
| 	return styles;
 | |
| }
 | |
| 
 | |
| function saveStyle(style, {notify = true} = {}) {
 | |
| 	return new Promise(resolve => {
 | |
| 		getDatabase(db => {
 | |
| 			const tx = db.transaction(['styles'], 'readwrite');
 | |
| 			const os = tx.objectStore('styles');
 | |
| 
 | |
| 			// Update
 | |
| 			if (style.id) {
 | |
| 				style.id = Number(style.id);
 | |
| 				os.get(style.id).onsuccess = eventGet => {
 | |
| 					const oldStyle = Object.assign({}, eventGet.target.result);
 | |
| 					const codeIsUpdated = !styleSectionsEqual(style, oldStyle);
 | |
| 					style = Object.assign(oldStyle, style);
 | |
| 					os.put(style).onsuccess = eventPut => {
 | |
| 						style.id = style.id || eventPut.target.result;
 | |
| 						if (notify) {
 | |
| 							notifyAllTabs({method: 'styleUpdated', style, codeIsUpdated});
 | |
| 						}
 | |
| 						invalidateCache(notify);
 | |
| 						resolve(style);
 | |
| 					};
 | |
| 				};
 | |
| 				return;
 | |
| 			}
 | |
| 
 | |
| 			// Create
 | |
| 			style = Object.assign({
 | |
| 				// Set optional things if they're undefined
 | |
| 				enabled: true,
 | |
| 				updateUrl: null,
 | |
| 				md5Url: null,
 | |
| 				url: null,
 | |
| 				originalMd5: null,
 | |
| 			}, style, {
 | |
| 				// Set other optional things to empty array if they're undefined
 | |
| 				sections: style.sections.map(section =>
 | |
| 					Object.assign({
 | |
| 						urls: [],
 | |
| 						urlPrefixes: [],
 | |
| 						domains: [],
 | |
| 						regexps: [],
 | |
| 					}, section)
 | |
| 				),
 | |
| 			})
 | |
| 			// Make sure it's not null - that makes indexeddb sad
 | |
| 			delete style.id;
 | |
| 			os.add(style).onsuccess = event => {
 | |
| 				invalidateCache(true);
 | |
| 				// Give it the ID that was generated
 | |
| 				style.id = event.target.result;
 | |
| 				notifyAllTabs({method: 'styleAdded', style});
 | |
| 				resolve(style);
 | |
| 			};
 | |
| 		});
 | |
| 	});
 | |
| }
 | |
| 
 | |
| function enableStyle(id, enabled) {
 | |
| 	saveStyle({id: id, enabled: enabled}).then(style => {
 | |
| 		handleUpdate(style);
 | |
| 		notifyAllTabs({method: "styleUpdated", style});
 | |
| 	});
 | |
| }
 | |
| 
 | |
| function deleteStyle(id, callback = function (){}) {
 | |
| 	getDatabase(function(db) {
 | |
| 		var tx = db.transaction(["styles"], "readwrite");
 | |
| 		var os = tx.objectStore("styles");
 | |
| 		var request = os.delete(Number(id));
 | |
| 		request.onsuccess = function(event) {
 | |
| 			handleDelete(id);
 | |
| 			invalidateCache(true);
 | |
| 			notifyAllTabs({method: "styleDeleted", id: id});
 | |
| 			callback();
 | |
| 		};
 | |
| 	});
 | |
| }
 | |
| 
 | |
| function reportError() {
 | |
| 	for (i in arguments) {
 | |
| 		if ("message" in arguments[i]) {
 | |
| 			//alert(arguments[i].message);
 | |
| 			console.log(arguments[i].message);
 | |
| 		}
 | |
| 	}
 | |
| }
 | |
| 
 | |
| function fixBoolean(b) {
 | |
| 	if (typeof b != "undefined") {
 | |
| 		return b != "false";
 | |
| 	}
 | |
| 	return null;
 | |
| }
 | |
| 
 | |
| function getDomains(url) {
 | |
| 	if (url.indexOf("file:") == 0) {
 | |
| 		return [];
 | |
| 	}
 | |
| 	var d = /.*?:\/*([^\/:]+)/.exec(url)[1];
 | |
| 	var domains = [d];
 | |
| 	while (d.indexOf(".") != -1) {
 | |
| 		d = d.substring(d.indexOf(".") + 1);
 | |
| 		domains.push(d);
 | |
| 	}
 | |
| 	return domains;
 | |
| }
 | |
| 
 | |
| function getType(o) {
 | |
| 	if (typeof o == "undefined" || typeof o == "string") {
 | |
| 		return typeof o;
 | |
| 	}
 | |
| 	if (o instanceof Array) {
 | |
| 		return "array";
 | |
| 	}
 | |
| 	throw "Not supported - " + o;
 | |
| }
 | |
| 
 | |
| var namespacePattern = /^\s*(@namespace[^;]+;\s*)+$/;
 | |
| function getApplicableSections(style, url) {
 | |
| 	var sections = style.sections.filter(function(section) {
 | |
| 		return sectionAppliesToUrl(section, url);
 | |
| 	});
 | |
| 	// ignore if it's just namespaces
 | |
| 	if (sections.length == 1 && namespacePattern.test(sections[0].code)) {
 | |
| 		return [];
 | |
| 	}
 | |
| 	return sections;
 | |
| }
 | |
| 
 | |
| function sectionAppliesToUrl(section, url) {
 | |
| 	// only http, https, file, and chrome-extension allowed
 | |
| 	if (url.indexOf("http") != 0 && url.indexOf("file") != 0 && url.indexOf("chrome-extension") != 0 && url.indexOf("ftp") != 0) {
 | |
| 		return false;
 | |
| 	}
 | |
| 	// other extensions can't be styled
 | |
| 	if (url.indexOf("chrome-extension") == 0 && url.indexOf(chrome.extension.getURL("")) != 0) {
 | |
| 		return false;
 | |
| 	}
 | |
| 	if (section.urls.length == 0 && section.domains.length == 0 && section.urlPrefixes.length == 0 && section.regexps.length == 0) {
 | |
| 		//console.log(section.id + " is global");
 | |
| 		return true;
 | |
| 	}
 | |
| 	if (section.urls.indexOf(url) != -1) {
 | |
| 		//console.log(section.id + " applies to " + url + " due to URL rules");
 | |
| 		return true;
 | |
| 	}
 | |
| 	if (section.urlPrefixes.some(function(prefix) {
 | |
| 		return url.indexOf(prefix) == 0;
 | |
| 	})) {
 | |
| 		//console.log(section.id + " applies to " + url + " due to URL prefix rules");
 | |
| 		return true;
 | |
| 	}
 | |
| 	if (section.domains.length > 0 && getDomains(url).some(function(domain) {
 | |
| 		return section.domains.indexOf(domain) != -1;
 | |
| 	})) {
 | |
| 		//console.log(section.id + " applies due to " + url + " due to domain rules");
 | |
| 		return true;
 | |
| 	}
 | |
| 	if (section.regexps.some(function(regexp) {
 | |
| 		// we want to match the full url, so add ^ and $ if not already present
 | |
| 		if (regexp[0] != "^") {
 | |
| 			regexp = "^" + regexp;
 | |
| 		}
 | |
| 		if (regexp[regexp.length - 1] != "$") {
 | |
| 			regexp += "$";
 | |
| 		}
 | |
| 		var re = runTryCatch(function() { return new RegExp(regexp) });
 | |
| 		if (re) {
 | |
| 			return (re).test(url);
 | |
| 		} else {
 | |
| 			console.log(section.id + "'s regexp '" + regexp + "' is not valid");
 | |
| 		}
 | |
| 	})) {
 | |
| 		//console.log(section.id + " applies to " + url + " due to regexp rules");
 | |
| 		return true;
 | |
| 	}
 | |
| 	//console.log(section.id + " does not apply due to " + url);
 | |
| 	return false;
 | |
| }
 | |
| 
 | |
| function isCheckbox(el) {
 | |
| 	return el.nodeName.toLowerCase() == "input" && "checkbox" == el.type.toLowerCase();
 | |
| }
 | |
| 
 | |
| // js engine can't optimize the entire function if it contains try-catch
 | |
| // so we should keep it isolated from normal code in a minimal wrapper
 | |
| function runTryCatch(func) {
 | |
| 	try { return func() }
 | |
| 	catch(e) {}
 | |
| }
 | |
| 
 | |
| // Accepts an array of pref names (values are fetched via prefs.get)
 | |
| // and establishes a two-way connection between the document elements and the actual prefs
 | |
| function setupLivePrefs(IDs) {
 | |
| 	var localIDs = {};
 | |
| 	IDs.forEach(function(id) {
 | |
| 		localIDs[id] = true;
 | |
| 		updateElement(id).addEventListener("change", function() {
 | |
| 			prefs.set(this.id, isCheckbox(this) ? this.checked : this.value);
 | |
| 		});
 | |
| 	});
 | |
| 	chrome.runtime.onMessage.addListener(function(request) {
 | |
| 		if (request.prefName in localIDs) {
 | |
| 			updateElement(request.prefName);
 | |
| 		}
 | |
| 	});
 | |
| 	function updateElement(id) {
 | |
| 		var el = document.getElementById(id);
 | |
| 		el[isCheckbox(el) ? "checked" : "value"] = prefs.get(id);
 | |
| 		el.dispatchEvent(new Event("change", {bubbles: true, cancelable: true}));
 | |
| 		return el;
 | |
| 	}
 | |
| }
 | |
| 
 | |
| var prefs = chrome.extension.getBackgroundPage().prefs || new function Prefs() {
 | |
| 	var me = this;
 | |
| 
 | |
| 	var defaults = {
 | |
| 		"openEditInWindow": false,      // new editor opens in a own browser window
 | |
| 		"windowPosition": {},           // detached window position
 | |
| 		"show-badge": true,             // display text on popup menu icon
 | |
| 		"disableAll": false,            // boss key
 | |
| 
 | |
| 		"popup.breadcrumbs": true,      // display "New style" links as URL breadcrumbs
 | |
| 		"popup.breadcrumbs.usePath": false, // use URL path for "this URL"
 | |
| 		"popup.enabledFirst": true,     // display enabled styles before disabled styles
 | |
| 		"popup.stylesFirst": true,      // display enabled styles before disabled styles
 | |
| 
 | |
| 		"manage.onlyEnabled": false,    // display only enabled styles
 | |
| 		"manage.onlyEdited": false,     // display only styles created locally
 | |
| 
 | |
| 		"editor.options": {},           // CodeMirror.defaults.*
 | |
| 		"editor.lineWrapping": true,    // word wrap
 | |
| 		"editor.smartIndent": true,     // "smart" indent
 | |
| 		"editor.indentWithTabs": false, // smart indent with tabs
 | |
| 		"editor.tabSize": 4,            // tab width, in spaces
 | |
| 		"editor.keyMap": navigator.appVersion.indexOf("Windows") > 0 ? "sublime" : "default",
 | |
| 		"editor.theme": "default",      // CSS theme
 | |
| 		"editor.beautify": {            // CSS beautifier
 | |
| 			selector_separator_newline: true,
 | |
| 			newline_before_open_brace: false,
 | |
| 			newline_after_open_brace: true,
 | |
| 			newline_between_properties: true,
 | |
| 			newline_before_close_brace: true,
 | |
| 			newline_between_rules: false,
 | |
| 			end_with_newline: false
 | |
| 		},
 | |
| 		"editor.lintDelay": 500,        // lint gutter marker update delay, ms
 | |
| 		"editor.lintReportDelay": 4500, // lint report update delay, ms
 | |
| 
 | |
| 		"badgeDisabled": "#8B0000",     // badge background color when disabled
 | |
| 		"badgeNormal": "#006666",       // badge background color
 | |
| 
 | |
| 		"popupWidth": 240,              // popup width in pixels
 | |
| 
 | |
| 		"updateInterval": 0             // user-style automatic update interval, hour
 | |
| 	};
 | |
| 	var values = deepCopy(defaults);
 | |
| 
 | |
| 	var syncTimeout; // see broadcast() function below
 | |
| 
 | |
| 	Object.defineProperty(this, "readOnlyValues", {value: {}});
 | |
| 
 | |
| 	Prefs.prototype.get = function(key, defaultValue) {
 | |
| 		if (key in values) {
 | |
| 			return values[key];
 | |
| 		}
 | |
| 		if (defaultValue !== undefined) {
 | |
| 			return defaultValue;
 | |
| 		}
 | |
| 		if (key in defaults) {
 | |
| 			return defaults[key];
 | |
| 		}
 | |
| 		console.warn("No default preference for '%s'", key);
 | |
| 	};
 | |
| 
 | |
| 	Prefs.prototype.getAll = function(key) {
 | |
| 		return deepCopy(values);
 | |
| 	};
 | |
| 
 | |
| 	Prefs.prototype.set = function(key, value, options) {
 | |
| 		var oldValue = deepCopy(values[key]);
 | |
| 		values[key] = value;
 | |
| 		defineReadonlyProperty(this.readOnlyValues, key, value);
 | |
| 		if ((!options || !options.noBroadcast) && !equal(value, oldValue)) {
 | |
| 			me.broadcast(key, value, options);
 | |
| 		}
 | |
| 	};
 | |
| 
 | |
| 	Prefs.prototype.remove = function(key) { me.set(key, undefined) };
 | |
| 
 | |
| 	Prefs.prototype.broadcast = function(key, value, options) {
 | |
| 		var message = {method: "prefChanged", prefName: key, value: value};
 | |
| 		notifyAllTabs(message);
 | |
| 		chrome.runtime.sendMessage(message);
 | |
| 		if (key == "disableAll") {
 | |
| 			notifyAllTabs({method: "styleDisableAll", disableAll: value});
 | |
| 		}
 | |
| 		if (!options || !options.noSync) {
 | |
| 			clearTimeout(syncTimeout);
 | |
| 			syncTimeout = setTimeout(function() {
 | |
| 				getSync().set({"settings": values});
 | |
| 			}, 0);
 | |
| 		}
 | |
| 	};
 | |
| 
 | |
| 	Object.keys(defaults).forEach(function(key) {
 | |
| 		me.set(key, defaults[key], {noBroadcast: true});
 | |
| 	});
 | |
| 
 | |
| 	getSync().get("settings", function(result) {
 | |
| 		var synced = result.settings;
 | |
| 		for (var key in defaults) {
 | |
| 			if (synced && (key in synced)) {
 | |
| 				me.set(key, synced[key], {noSync: true});
 | |
| 			} else {
 | |
| 				var value = tryMigrating(key);
 | |
| 				if (value !== undefined) {
 | |
| 					me.set(key, value);
 | |
| 				}
 | |
| 			}
 | |
| 		}
 | |
| 		// make sure right click context menu is in the right state when prefs are loaded
 | |
| 		chrome.contextMenus.update("disableAll", {checked: prefs.get("disableAll")});
 | |
| 	});
 | |
| 
 | |
| 	chrome.storage.onChanged.addListener(function(changes, area) {
 | |
| 		if (area == "sync" && "settings" in changes) {
 | |
| 			var synced = changes.settings.newValue;
 | |
| 			if (synced) {
 | |
| 				for (key in defaults) {
 | |
| 					if (key in synced) {
 | |
| 						me.set(key, synced[key], {noSync: true});
 | |
| 					}
 | |
| 				}
 | |
| 			} else {
 | |
| 				// user manually deleted our settings, we'll recreate them
 | |
| 				getSync().set({"settings": values});
 | |
| 			}
 | |
| 		}
 | |
| 	});
 | |
| 
 | |
| 	function tryMigrating(key) {
 | |
| 		if (!(key in localStorage)) {
 | |
| 			return undefined;
 | |
| 		}
 | |
| 		var value = localStorage[key];
 | |
| 		delete localStorage[key];
 | |
| 		localStorage["DEPRECATED: " + key] = value;
 | |
| 		switch (typeof defaults[key]) {
 | |
| 			case "boolean":
 | |
| 				return value.toLowerCase() === "true";
 | |
| 			case "number":
 | |
| 				return Number(value);
 | |
| 			case "object":
 | |
| 				try {
 | |
| 					return JSON.parse(value);
 | |
| 				} catch(e) {
 | |
| 					console.log("Cannot migrate from localStorage %s = '%s': %o", key, value, e);
 | |
| 					return undefined;
 | |
| 				}
 | |
| 		}
 | |
| 		return value;
 | |
| 	}
 | |
| };
 | |
| 
 | |
| function getCodeMirrorThemes(callback) {
 | |
| 	chrome.runtime.getPackageDirectoryEntry(function(rootDir) {
 | |
| 		rootDir.getDirectory("codemirror/theme", {create: false}, function(themeDir) {
 | |
| 			themeDir.createReader().readEntries(function(entries) {
 | |
| 				var themes = [chrome.i18n.getMessage("defaultTheme")];
 | |
| 				entries
 | |
| 					.filter(function(entry) { return entry.isFile })
 | |
| 					.sort(function(a, b) { return a.name < b.name ? -1 : 1 })
 | |
| 					.forEach(function(entry) {
 | |
| 						themes.push(entry.name.replace(/\.css$/, ""));
 | |
| 					});
 | |
| 				if (callback) {
 | |
| 					callback(themes);
 | |
| 				}
 | |
| 			});
 | |
| 		});
 | |
| 	});
 | |
| }
 | |
| 
 | |
| function sessionStorageHash(name) {
 | |
| 	var hash = {
 | |
| 		value: {},
 | |
| 		set: function(k, v) { this.value[k] = v; this.updateStorage(); },
 | |
| 		unset: function(k) { delete this.value[k]; this.updateStorage(); },
 | |
| 		updateStorage: function() {
 | |
| 			sessionStorage[this.name] = JSON.stringify(this.value);
 | |
| 		}
 | |
| 	};
 | |
| 	try { hash.value = JSON.parse(sessionStorage[name]); } catch(e) {}
 | |
| 	Object.defineProperty(hash, "name", {value: name});
 | |
| 	return hash;
 | |
| }
 | |
| 
 | |
| function deepCopy(obj) {
 | |
| 	if (!obj || typeof obj != "object") {
 | |
| 		return obj;
 | |
| 	} else {
 | |
| 		var emptyCopy = Object.create(Object.getPrototypeOf(obj));
 | |
| 		return deepMerge(emptyCopy, obj);
 | |
| 	}
 | |
| }
 | |
| 
 | |
| function deepMerge(target, obj1 /* plus any number of object arguments */) {
 | |
| 	for (var i = 1; i < arguments.length; i++) {
 | |
| 		var obj = arguments[i];
 | |
| 		for (var k in obj) {
 | |
| 			// hasOwnProperty checking is not needed for our non-OOP stuff
 | |
| 			var value = obj[k];
 | |
| 			if (!value || typeof value != "object") {
 | |
| 				target[k] = value;
 | |
| 			} else if (k in target) {
 | |
| 				deepMerge(target[k], value);
 | |
| 			} else {
 | |
| 				target[k] = deepCopy(value);
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 	return target;
 | |
| }
 | |
| 
 | |
| function shallowMerge(target, obj1 /* plus any number of object arguments */) {
 | |
| 	for (var i = 1; i < arguments.length; i++) {
 | |
| 		var obj = arguments[i];
 | |
| 		for (var k in obj) {
 | |
| 			target[k] = obj[k];
 | |
| 			// hasOwnProperty checking is not needed for our non-OOP stuff
 | |
| 		}
 | |
| 	}
 | |
| 	return target;
 | |
| }
 | |
| 
 | |
| function equal(a, b) {
 | |
| 	if (!a || !b || typeof a != "object" || typeof b != "object") {
 | |
| 		return a === b;
 | |
| 	}
 | |
| 	if (Object.keys(a).length != Object.keys(b).length) {
 | |
| 		return false;
 | |
| 	}
 | |
| 	for (var k in a) {
 | |
| 		if (a[k] !== b[k]) {
 | |
| 			return false;
 | |
| 		}
 | |
| 	}
 | |
| 	return true;
 | |
| }
 | |
| 
 | |
| function defineReadonlyProperty(obj, key, value) {
 | |
| 	var copy = deepCopy(value);
 | |
| 	// In ES6, freezing a literal is OK (it returns the same value), but in previous versions it's an exception.
 | |
| 	if (typeof copy == "object") {
 | |
| 		Object.freeze(copy);
 | |
| 	}
 | |
| 	Object.defineProperty(obj, key, {value: copy, configurable: true})
 | |
| }
 | |
| 
 | |
| // Polyfill, can be removed when Firefox gets this - https://bugzilla.mozilla.org/show_bug.cgi?id=1220494
 | |
| function getSync() {
 | |
| 	if ("sync" in chrome.storage) {
 | |
| 		return chrome.storage.sync;
 | |
| 	}
 | |
| 	crappyStorage = {};
 | |
| 	return {
 | |
| 		get: function(key, callback) {
 | |
| 			callback(crappyStorage[key] || {});
 | |
| 		},
 | |
| 		set: function(source, callback) {
 | |
| 			for (var property in source) {
 | |
| 					if (source.hasOwnProperty(property)) {
 | |
| 							crappyStorage[property] = source[property];
 | |
| 					}
 | |
| 			}
 | |
| 			callback();
 | |
| 		}
 | |
| 	}
 | |
| }
 | |
| 
 | |
| function styleSectionsEqual(styleA, styleB) {
 | |
| 	if (!styleA.sections || !styleB.sections) {
 | |
| 		return undefined;
 | |
| 	}
 | |
| 	if (styleA.sections.length != styleB.sections.length) {
 | |
| 		return false;
 | |
| 	}
 | |
| 	const properties = ['code', 'urlPrefixes', 'urls', 'domains', 'regexps'];
 | |
| 	return styleA.sections.every(sectionA =>
 | |
| 		styleB.sections.some(sectionB =>
 | |
| 			properties.every(property => sectionEquals(sectionA, sectionB, property))
 | |
| 		)
 | |
| 	);
 | |
| 
 | |
| 	function sectionEquals(a, b, property) {
 | |
| 		const aProp = a[property], typeA = getType(aProp);
 | |
| 		const bProp = b[property], typeB = getType(bProp);
 | |
| 		if (typeA != typeB) {
 | |
| 			// consider empty arrays equivalent to lack of property
 | |
| 			return ((typeA == 'undefined' || (typeA == 'array' && aProp.length == 0)) &&
 | |
| 				(typeB == 'undefined' || (typeB == 'array' && bProp.length == 0)));
 | |
| 		}
 | |
| 		if (typeA == 'undefined') {
 | |
| 			return true;
 | |
| 		}
 | |
| 		if (typeA == 'array') {
 | |
| 			return aProp.length == bProp.length && aProp.every(item => bProp.includes(item));
 | |
| 		}
 | |
| 		if (typeA == 'string') {
 | |
| 			return aProp == bProp;
 | |
| 		}
 | |
| 	}
 | |
| }
 |