Add: install styles from *.user.css file
Fix: handle dup name+namespace Fix: eslint eqeqeq Fix: trim @name's spaces Add: check update for userstyle Add: build CSS variable Fix: only check dup when id is not provided Refactor: userStyle2json -> userstyle.json Add: style for input Add: config dialog Fix: preserve config during update Fix: onchange doesn't fire on keyboard enter event Fix: remove empty file Add: validator. Metas must stay in the same line Add: warn the user if installation failed Fix: add some delay before starting installation Add: open the editor after first installation Fix: add openEditor to globals Fix: i18n Add: preprocessor. Move userstyle.build to background page. Fix: remove unused global Fix: preserved unknown prop in saveStyleSource() like saveStyle() Add: edit userstyle source Fix: load preprocessor dynamically Fix: load content script dynamically Fix: buildCode is async function Fix: drop Object.entries Fix: style.sections is undefined Fix: don't hide the name input but disable it Fix: query the style before installation Revert: changes to editor, editor.html Refactor: use term `usercss` instead of `userstyle` Fix: don't show homepage action for usercss Refactor: move script-loader to js/ Refactor: pull out mozParser Fix: code style Fix: we don't need to build meta anymore Fix: use saveUsercss instead of saveStyle to get responsed error Fix: last is undefined, load script error Fix: switch to moz-format Fix: drop injectContentScript. Move usercss check into install-user-css Fix: response -> respond Fix: globals -> global Fix: queryUsercss -> filterUsercss Fix: add processUsercss function Fix: only open editor for usercss Fix: remove findupUsercss fixme Fix: globals -> global Fix: globals -> global Fix: global pollution Revert: update.js Refactor: checkStyle Add: support usercss Fix: no need to getURL in background page Fix: merget semver.js into usercss.js Fix: drop all_urls in match pattern Fix: drop respondWithError Move stylus -> stylus-lang Add stylus-lang/readme Fix: use include_globs Fix: global pollution
This commit is contained in:
parent
6f0ab8113e
commit
dece4b57f3
|
@ -83,6 +83,10 @@
|
||||||
"updateCheckHistory": {
|
"updateCheckHistory": {
|
||||||
"message": "History of update checks"
|
"message": "History of update checks"
|
||||||
},
|
},
|
||||||
|
"configureStyle": {
|
||||||
|
"message": "Configure",
|
||||||
|
"description": "Label for the button to configure userstyle"
|
||||||
|
},
|
||||||
"checkForUpdate": {
|
"checkForUpdate": {
|
||||||
"message": "Check for update",
|
"message": "Check for update",
|
||||||
"description": "Label for the button to check a single style for an update"
|
"description": "Label for the button to check a single style for an update"
|
||||||
|
@ -175,6 +179,10 @@
|
||||||
"message": "Yes",
|
"message": "Yes",
|
||||||
"description": "'Yes' button in a confirm dialog"
|
"description": "'Yes' button in a confirm dialog"
|
||||||
},
|
},
|
||||||
|
"confirmClose": {
|
||||||
|
"message": "Close",
|
||||||
|
"description": "'Close' button in a confirm dialog"
|
||||||
|
},
|
||||||
"dbError": {
|
"dbError": {
|
||||||
"message": "An error has occurred using the Stylus database. Would you like to visit a web page with possible solutions?",
|
"message": "An error has occurred using the Stylus database. Would you like to visit a web page with possible solutions?",
|
||||||
"description": "Prompt when a DB error is encountered"
|
"description": "Prompt when a DB error is encountered"
|
||||||
|
@ -629,6 +637,43 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"styleInstallOverwrite" : {
|
||||||
|
"message": "'$stylename$' is already installed. Overwrite?\nVersion: $oldVersion$ -> $newVersion$",
|
||||||
|
"description": "Confirmation when re-installing a style",
|
||||||
|
"placeholders": {
|
||||||
|
"stylename": {
|
||||||
|
"content": "$1"
|
||||||
|
},
|
||||||
|
"oldVersion": {
|
||||||
|
"content": "$2"
|
||||||
|
},
|
||||||
|
"newVersion": {
|
||||||
|
"content": "$3"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"styleInstallNoName": {
|
||||||
|
"message": "Install this style into stylus?",
|
||||||
|
"description": "Confirmation when installing a style"
|
||||||
|
},
|
||||||
|
"styleInstallFailed": {
|
||||||
|
"message": "Failed to install userstyle!\n$ERROR$",
|
||||||
|
"description": "Warning when installation failed",
|
||||||
|
"placeholders": {
|
||||||
|
"error": {
|
||||||
|
"content": "$1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"styleMissingMeta": {
|
||||||
|
"message": "Missing medata @$KEY$",
|
||||||
|
"description": "Error displayed when a mandatory metadata is missing",
|
||||||
|
"placeholders": {
|
||||||
|
"key": {
|
||||||
|
"content": "$1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"styleMissingName": {
|
"styleMissingName": {
|
||||||
"message": "Enter a name.",
|
"message": "Enter a name.",
|
||||||
"description": "Error displayed when user saves without providing a name"
|
"description": "Error displayed when user saves without providing a name"
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
/* global dbExec, getStyles, saveStyle */
|
/* global dbExec, getStyles, saveStyle, filterUsercss, saveUsercss */
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
// eslint-disable-next-line no-var
|
// eslint-disable-next-line no-var
|
||||||
|
@ -322,6 +322,14 @@ function onRuntimeMessage(request, sender, sendResponse) {
|
||||||
saveStyle(request).then(sendResponse);
|
saveStyle(request).then(sendResponse);
|
||||||
return KEEP_CHANNEL_OPEN;
|
return KEEP_CHANNEL_OPEN;
|
||||||
|
|
||||||
|
case 'saveUsercss':
|
||||||
|
saveUsercss(request).then(sendResponse);
|
||||||
|
return KEEP_CHANNEL_OPEN;
|
||||||
|
|
||||||
|
case 'filterUsercss':
|
||||||
|
filterUsercss(request).then(sendResponse);
|
||||||
|
return KEEP_CHANNEL_OPEN;
|
||||||
|
|
||||||
case 'healthCheck':
|
case 'healthCheck':
|
||||||
dbExec()
|
dbExec()
|
||||||
.then(() => sendResponse(true))
|
.then(() => sendResponse(true))
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
/* global LZString */
|
/* global LZString */
|
||||||
|
/* global usercss, openEditor */
|
||||||
|
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const RX_NAMESPACE = new RegExp([/[\s\r\n]*/,
|
const RX_NAMESPACE = new RegExp([/[\s\r\n]*/,
|
||||||
|
@ -259,8 +261,43 @@ function filterStylesInternal({
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Parse the source and find the duplication
|
||||||
|
// {id: int, style: object, source: string, checkDup: boolean}
|
||||||
|
function filterUsercss(req) {
|
||||||
|
return Promise.resolve().then(() => {
|
||||||
|
let style;
|
||||||
|
if (req.source) {
|
||||||
|
style = usercss.buildMeta(req.source);
|
||||||
|
} else {
|
||||||
|
style = req.style;
|
||||||
|
}
|
||||||
|
if (!style.id && req.id) {
|
||||||
|
style.id = req.id;
|
||||||
|
}
|
||||||
|
if (!style.id && req.checkDup) {
|
||||||
|
return findDupUsercss(style)
|
||||||
|
.then(dup => ({status: 'success', style, dup}));
|
||||||
|
}
|
||||||
|
return {status: 'success', style};
|
||||||
|
}).catch(err => ({status: 'error', error: String(err)}));
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveUsercss(style) {
|
||||||
|
// This function use `saveStyle`, however the response is different.
|
||||||
|
return saveStyle(style)
|
||||||
|
.then(result => ({
|
||||||
|
status: 'success',
|
||||||
|
style: result
|
||||||
|
}))
|
||||||
|
.catch(err => ({
|
||||||
|
status: 'error',
|
||||||
|
error: String(err)
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function saveStyle(style) {
|
function saveStyle(style) {
|
||||||
const id = Number(style.id) || null;
|
let id = Number(style.id) || null;
|
||||||
const reason = style.reason;
|
const reason = style.reason;
|
||||||
const notify = style.notify !== false;
|
const notify = style.notify !== false;
|
||||||
delete style.method;
|
delete style.method;
|
||||||
|
@ -271,6 +308,11 @@ function saveStyle(style) {
|
||||||
}
|
}
|
||||||
let existed;
|
let existed;
|
||||||
let codeIsUpdated;
|
let codeIsUpdated;
|
||||||
|
|
||||||
|
if (style.usercss) {
|
||||||
|
return processUsercss(style).then(decide);
|
||||||
|
}
|
||||||
|
|
||||||
if (reason === 'update' || reason === 'update-digest') {
|
if (reason === 'update' || reason === 'update-digest') {
|
||||||
return calcStyleDigest(style).then(digest => {
|
return calcStyleDigest(style).then(digest => {
|
||||||
style.originalDigest = digest;
|
style.originalDigest = digest;
|
||||||
|
@ -286,6 +328,27 @@ function saveStyle(style) {
|
||||||
}
|
}
|
||||||
return decide();
|
return decide();
|
||||||
|
|
||||||
|
function processUsercss(style) {
|
||||||
|
return findDupUsercss(style).then(dup => {
|
||||||
|
if (!dup) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!id) {
|
||||||
|
id = dup.id;
|
||||||
|
}
|
||||||
|
if (reason === 'config') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// preserve style.vars during update
|
||||||
|
for (const key of Object.keys(style.vars)) {
|
||||||
|
if (key in dup.vars) {
|
||||||
|
style.vars[key].value = dup.vars[key].value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(() => usercss.buildCode(style));
|
||||||
|
}
|
||||||
|
|
||||||
function decide() {
|
function decide() {
|
||||||
if (id !== null) {
|
if (id !== null) {
|
||||||
// Update or create
|
// Update or create
|
||||||
|
@ -338,6 +401,10 @@ function saveStyle(style) {
|
||||||
style, codeIsUpdated, reason,
|
style, codeIsUpdated, reason,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if (style.usercss && !existed && reason === 'install') {
|
||||||
|
// open the editor for usercss with the first install?
|
||||||
|
openEditor(style.id);
|
||||||
|
}
|
||||||
return style;
|
return style;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -354,6 +421,17 @@ function deleteStyle({id, notify = true}) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function findDupUsercss(style) {
|
||||||
|
if (style.id) {
|
||||||
|
return getStyles({id: style.id}).then(s => s[0]);
|
||||||
|
}
|
||||||
|
return getStyles().then(styles =>
|
||||||
|
styles.find(
|
||||||
|
s => s.name === style.name && s.namespace === style.namespace
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function getApplicableSections({
|
function getApplicableSections({
|
||||||
style,
|
style,
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/* global getStyles, saveStyle, styleSectionsEqual, chromeLocal */
|
/* global getStyles, saveStyle, styleSectionsEqual, chromeLocal */
|
||||||
/* global calcStyleDigest */
|
/* global calcStyleDigest, usercss */
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
// eslint-disable-next-line no-var
|
// eslint-disable-next-line no-var
|
||||||
|
@ -15,8 +15,10 @@ var updater = {
|
||||||
MAYBE_EDITED: 'may be locally edited',
|
MAYBE_EDITED: 'may be locally edited',
|
||||||
SAME_MD5: 'up-to-date: MD5 is unchanged',
|
SAME_MD5: 'up-to-date: MD5 is unchanged',
|
||||||
SAME_CODE: 'up-to-date: code sections are unchanged',
|
SAME_CODE: 'up-to-date: code sections are unchanged',
|
||||||
|
SAME_VERSION: 'up-to-date: version is unchanged',
|
||||||
ERROR_MD5: 'error: MD5 is invalid',
|
ERROR_MD5: 'error: MD5 is invalid',
|
||||||
ERROR_JSON: 'error: JSON is invalid',
|
ERROR_JSON: 'error: JSON is invalid',
|
||||||
|
ERROR_VERSION: 'error: version is invalid',
|
||||||
|
|
||||||
lastUpdateTime: parseInt(localStorage.lastUpdateTime) || Date.now(),
|
lastUpdateTime: parseInt(localStorage.lastUpdateTime) || Date.now(),
|
||||||
|
|
||||||
|
@ -53,9 +55,10 @@ var updater = {
|
||||||
|
|
||||||
'ignoreDigest' option is set on the second manual individual update check on the manage page.
|
'ignoreDigest' option is set on the second manual individual update check on the manage page.
|
||||||
*/
|
*/
|
||||||
|
const maybeUpdate = style.usercss ? maybeUpdateUsercss : maybeUpdateUSO;
|
||||||
return (ignoreDigest ? Promise.resolve() : calcStyleDigest(style))
|
return (ignoreDigest ? Promise.resolve() : calcStyleDigest(style))
|
||||||
.then(maybeFetchMd5)
|
.then(checkIfEdited)
|
||||||
.then(maybeFetchCode)
|
.then(maybeUpdate)
|
||||||
.then(maybeSave)
|
.then(maybeSave)
|
||||||
.then(saved => {
|
.then(saved => {
|
||||||
observer(updater.UPDATED, saved);
|
observer(updater.UPDATED, saved);
|
||||||
|
@ -67,25 +70,49 @@ var updater = {
|
||||||
updater.log(updater.SKIPPED + ` (${err}) #${style.id} ${style.name}`);
|
updater.log(updater.SKIPPED + ` (${err}) #${style.id} ${style.name}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
function maybeFetchMd5(digest) {
|
function checkIfEdited(digest) {
|
||||||
|
if (style.usercss) {
|
||||||
|
// FIXME: remove this after we can calculate digest from style.source
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!ignoreDigest && style.originalDigest && style.originalDigest !== digest) {
|
if (!ignoreDigest && style.originalDigest && style.originalDigest !== digest) {
|
||||||
return Promise.reject(updater.EDITED);
|
return Promise.reject(updater.EDITED);
|
||||||
}
|
}
|
||||||
return download(style.md5Url);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function maybeFetchCode(md5) {
|
function maybeUpdateUSO() {
|
||||||
if (!md5 || md5.length !== 32) {
|
return download(style.md5Url).then(md5 => {
|
||||||
return Promise.reject(updater.ERROR_MD5);
|
if (!md5 || md5.length !== 32) {
|
||||||
}
|
return Promise.reject(updater.ERROR_MD5);
|
||||||
if (md5 === style.originalMd5 && style.originalDigest && !ignoreDigest) {
|
}
|
||||||
return Promise.reject(updater.SAME_MD5);
|
if (md5 === style.originalMd5 && style.originalDigest && !ignoreDigest) {
|
||||||
}
|
return Promise.reject(updater.SAME_MD5);
|
||||||
return download(style.updateUrl);
|
}
|
||||||
|
return download(style.updateUrl)
|
||||||
|
.then(text => tryJSONparse(text));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function maybeSave(text) {
|
function maybeUpdateUsercss() {
|
||||||
const json = tryJSONparse(text);
|
return download(style.updateUrl).then(text => {
|
||||||
|
const json = usercss.buildMeta(text);
|
||||||
|
if (!json.version) {
|
||||||
|
return Promise.reject(updater.ERROR_VERSION);
|
||||||
|
}
|
||||||
|
if (style.version) {
|
||||||
|
if (usercss.semverTest(style.version, json.version) === 0) {
|
||||||
|
return Promise.reject(updater.SAME_VERSION);
|
||||||
|
}
|
||||||
|
if (usercss.semverTest(style.version, json.version) > 0) {
|
||||||
|
return Promise.reject(updater.ERROR_VERSION);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
json.id = style.id;
|
||||||
|
return json;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function maybeSave(json) {
|
||||||
if (!styleJSONseemsValid(json)) {
|
if (!styleJSONseemsValid(json)) {
|
||||||
return Promise.reject(updater.ERROR_JSON);
|
return Promise.reject(updater.ERROR_JSON);
|
||||||
}
|
}
|
||||||
|
|
72
content/install-user-css.js
Normal file
72
content/install-user-css.js
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
function fetchText(url) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
// you can't use fetch in Chrome under 'file:' protocol
|
||||||
|
const xhr = new XMLHttpRequest();
|
||||||
|
xhr.open('GET', url);
|
||||||
|
xhr.addEventListener('load', () => resolve(xhr.responseText));
|
||||||
|
xhr.addEventListener('error', () => reject(xhr));
|
||||||
|
xhr.send();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function install(style) {
|
||||||
|
const request = Object.assign(style, {
|
||||||
|
method: 'saveUsercss',
|
||||||
|
reason: 'install',
|
||||||
|
url: location.href,
|
||||||
|
updateUrl: location.href
|
||||||
|
});
|
||||||
|
return communicate(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
function communicate(request) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
chrome.runtime.sendMessage(request, result => {
|
||||||
|
if (result.status === 'error') {
|
||||||
|
reject(result.error);
|
||||||
|
} else {
|
||||||
|
resolve(result);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function initUsercssInstall() {
|
||||||
|
fetchText(location.href).then(source =>
|
||||||
|
communicate({
|
||||||
|
method: 'filterUsercss',
|
||||||
|
source: source,
|
||||||
|
checkDup: true
|
||||||
|
})
|
||||||
|
).then(({style, dup}) => {
|
||||||
|
if (dup) {
|
||||||
|
if (confirm(chrome.i18n.getMessage('styleInstallOverwrite', [style.name, dup.version, style.version]))) {
|
||||||
|
return install(style);
|
||||||
|
}
|
||||||
|
} else if (confirm(chrome.i18n.getMessage('styleInstall', [style.name]))) {
|
||||||
|
return install(style);
|
||||||
|
}
|
||||||
|
}).catch(err => {
|
||||||
|
alert(chrome.i18n.getMessage('styleInstallFailed', String(err)));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function isUsercss() {
|
||||||
|
if (!/\.user\.(css|styl|less|scss|sass)$/i.test(location.pathname)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!/text\/(css|plain)/.test(document.contentType)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!/==userstyle==/i.test(document.body.textContent)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isUsercss()) {
|
||||||
|
// It seems that we need to wait some time to redraw the page.
|
||||||
|
setTimeout(initUsercssInstall, 500);
|
||||||
|
}
|
|
@ -6,6 +6,8 @@
|
||||||
<script src="js/messaging.js"></script>
|
<script src="js/messaging.js"></script>
|
||||||
<script src="js/prefs.js"></script>
|
<script src="js/prefs.js"></script>
|
||||||
<script src="js/localization.js"></script>
|
<script src="js/localization.js"></script>
|
||||||
|
<script src="js/script-loader.js"></script>
|
||||||
|
<script src="js/moz-parser.js"></script>
|
||||||
<script src="content/apply.js"></script>
|
<script src="content/apply.js"></script>
|
||||||
<link rel="stylesheet" href="edit/edit.css">
|
<link rel="stylesheet" href="edit/edit.css">
|
||||||
<script src="edit/lint.js"></script>
|
<script src="edit/lint.js"></script>
|
||||||
|
|
158
edit/edit.js
158
edit/edit.js
|
@ -3,6 +3,8 @@
|
||||||
/* global onDOMscripted */
|
/* global onDOMscripted */
|
||||||
/* global css_beautify */
|
/* global css_beautify */
|
||||||
/* global CSSLint initLint linterConfig updateLintReport renderLintReport updateLinter */
|
/* global CSSLint initLint linterConfig updateLintReport renderLintReport updateLinter */
|
||||||
|
/* global mozParser */
|
||||||
|
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
let styleId = null;
|
let styleId = null;
|
||||||
|
@ -1498,17 +1500,7 @@ function showMozillaFormat() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function toMozillaFormat() {
|
function toMozillaFormat() {
|
||||||
return getSectionsHashes().map(section => {
|
return mozParser.format({sections: getSectionsHashes()});
|
||||||
let cssMds = [];
|
|
||||||
for (const i in propertyToCss) {
|
|
||||||
if (section[i]) {
|
|
||||||
cssMds = cssMds.concat(section[i].map(v =>
|
|
||||||
propertyToCss[i] + '("' + v.replace(/\\/g, '\\\\') + '")'
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return cssMds.length ? '@-moz-document ' + cssMds.join(', ') + ' {\n' + section.code + '\n}' : section.code;
|
|
||||||
}).join('\n\n');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function fromMozillaFormat() {
|
function fromMozillaFormat() {
|
||||||
|
@ -1542,121 +1534,8 @@ function fromMozillaFormat() {
|
||||||
const replaceOldStyle = target.name === 'import-replace';
|
const replaceOldStyle = target.name === 'import-replace';
|
||||||
$('.dismiss', popup).onclick();
|
$('.dismiss', popup).onclick();
|
||||||
const mozStyle = trimNewLines(popup.codebox.getValue());
|
const mozStyle = trimNewLines(popup.codebox.getValue());
|
||||||
const parser = new parserlib.css.Parser();
|
|
||||||
const lines = mozStyle.split('\n');
|
|
||||||
const sectionStack = [{code: '', start: {line: 1, col: 1}}];
|
|
||||||
const errors = [];
|
|
||||||
// let oldSectionCount = editors.length;
|
|
||||||
let firstAddedCM;
|
|
||||||
|
|
||||||
parser.addListener('startdocument', function (e) {
|
mozParser.parse(mozStyle).then(sections => {
|
||||||
let outerText = getRange(sectionStack.last.start, (--e.col, e));
|
|
||||||
const gapComment = outerText.match(/(\/\*[\s\S]*?\*\/)[\s\n]*$/);
|
|
||||||
const section = {code: '', start: backtrackTo(this, parserlib.css.Tokens.LBRACE, 'end')};
|
|
||||||
// move last comment before @-moz-document inside the section
|
|
||||||
if (gapComment && !gapComment[1].match(/\/\*\s*AGENT_SHEET\s*\*\//)) {
|
|
||||||
section.code = gapComment[1] + '\n';
|
|
||||||
outerText = trimNewLines(outerText.substring(0, gapComment.index));
|
|
||||||
}
|
|
||||||
if (outerText.trim()) {
|
|
||||||
sectionStack.last.code = outerText;
|
|
||||||
doAddSection(sectionStack.last);
|
|
||||||
sectionStack.last.code = '';
|
|
||||||
}
|
|
||||||
for (const f of e.functions) {
|
|
||||||
const m = f && f.match(/^([\w-]*)\((['"]?)(.+?)\2?\)$/);
|
|
||||||
if (!m || !/^(url|url-prefix|domain|regexp)$/.test(m[1])) {
|
|
||||||
errors.push(`${e.line}:${e.col + 1} invalid function "${m ? m[1] : f || ''}"`);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const aType = CssToProperty[m[1]];
|
|
||||||
const aValue = aType !== 'regexps' ? m[3] : m[3].replace(/\\\\/g, '\\');
|
|
||||||
(section[aType] = section[aType] || []).push(aValue);
|
|
||||||
}
|
|
||||||
sectionStack.push(section);
|
|
||||||
});
|
|
||||||
|
|
||||||
parser.addListener('enddocument', function () {
|
|
||||||
const end = backtrackTo(this, parserlib.css.Tokens.RBRACE, 'start');
|
|
||||||
const section = sectionStack.pop();
|
|
||||||
section.code += getRange(section.start, end);
|
|
||||||
sectionStack.last.start = (++end.col, end);
|
|
||||||
doAddSection(section);
|
|
||||||
});
|
|
||||||
|
|
||||||
parser.addListener('endstylesheet', () => {
|
|
||||||
// add nonclosed outer sections (either broken or the last global one)
|
|
||||||
const endOfText = {line: lines.length, col: lines.last.length + 1};
|
|
||||||
sectionStack.last.code += getRange(sectionStack.last.start, endOfText);
|
|
||||||
sectionStack.forEach(doAddSection);
|
|
||||||
|
|
||||||
delete maximizeCodeHeight.stats;
|
|
||||||
editors.forEach(cm => {
|
|
||||||
maximizeCodeHeight(cm.getSection(), cm === editors.last);
|
|
||||||
});
|
|
||||||
|
|
||||||
makeSectionVisible(firstAddedCM);
|
|
||||||
firstAddedCM.focus();
|
|
||||||
|
|
||||||
if (errors.length) {
|
|
||||||
showHelp(t('linterIssues'), $element({
|
|
||||||
tag: 'pre',
|
|
||||||
textContent: errors.join('\n'),
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
parser.addListener('error', e => {
|
|
||||||
errors.push(e.line + ':' + e.col + ' ' +
|
|
||||||
e.message.replace(/ at line \d.+$/, ''));
|
|
||||||
});
|
|
||||||
|
|
||||||
parser.parse(mozStyle);
|
|
||||||
|
|
||||||
function getRange(start, end) {
|
|
||||||
const L1 = start.line - 1;
|
|
||||||
const C1 = start.col - 1;
|
|
||||||
const L2 = end.line - 1;
|
|
||||||
const C2 = end.col - 1;
|
|
||||||
if (L1 === L2) {
|
|
||||||
return lines[L1].substr(C1, C2 - C1 + 1);
|
|
||||||
} else {
|
|
||||||
const middle = lines.slice(L1 + 1, L2).join('\n');
|
|
||||||
return lines[L1].substr(C1) + '\n' + middle +
|
|
||||||
(L2 >= lines.length ? '' : ((middle ? '\n' : '') + lines[L2].substring(0, C2)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function doAddSection(section) {
|
|
||||||
section.code = section.code.trim();
|
|
||||||
// don't add empty sections
|
|
||||||
if (
|
|
||||||
!section.code &&
|
|
||||||
!section.urls &&
|
|
||||||
!section.urlPrefixes &&
|
|
||||||
!section.domains &&
|
|
||||||
!section.regexps
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!firstAddedCM) {
|
|
||||||
if (!initFirstSection(section)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
setCleanItem(addSection(null, section), false);
|
|
||||||
firstAddedCM = firstAddedCM || editors.last;
|
|
||||||
}
|
|
||||||
// do onetime housekeeping as the imported text is confirmed to be a valid style
|
|
||||||
function initFirstSection(section) {
|
|
||||||
// skip adding the first global section when there's no code/comments
|
|
||||||
if (
|
|
||||||
/* ignore boilerplate NS */
|
|
||||||
!section.code.replace('@namespace url(http://www.w3.org/1999/xhtml);', '')
|
|
||||||
/* ignore all whitespace including new lines */
|
|
||||||
.replace(/[\s\n]/g, '')
|
|
||||||
) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (replaceOldStyle) {
|
if (replaceOldStyle) {
|
||||||
editors.slice(0).reverse().forEach(cm => {
|
editors.slice(0).reverse().forEach(cm => {
|
||||||
removeSection({target: cm.getSection().firstElementChild});
|
removeSection({target: cm.getSection().firstElementChild});
|
||||||
|
@ -1667,16 +1546,27 @@ function fromMozillaFormat() {
|
||||||
removeSection({target: editors.last.getSection()});
|
removeSection({target: editors.last.getSection()});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true;
|
|
||||||
}
|
const firstSection = sections[0];
|
||||||
}
|
setCleanItem(addSection(null, firstSection), false);
|
||||||
function backtrackTo(parser, tokenType, startEnd) {
|
const firstAddedCM = editors.last;
|
||||||
const tokens = parser._tokenStream._lt;
|
for (const section of sections.slice(1)) {
|
||||||
for (let i = parser._tokenStream._ltIndex - 1; i >= 0; --i) {
|
setCleanItem(addSection(null, section), false);
|
||||||
if (tokens[i].type === tokenType) {
|
|
||||||
return {line: tokens[i][startEnd + 'Line'], col: tokens[i][startEnd + 'Col']};
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
delete maximizeCodeHeight.stats;
|
||||||
|
editors.forEach(cm => {
|
||||||
|
maximizeCodeHeight(cm.getSection(), cm === editors.last);
|
||||||
|
});
|
||||||
|
|
||||||
|
makeSectionVisible(firstAddedCM);
|
||||||
|
firstAddedCM.focus();
|
||||||
|
}, errors => {
|
||||||
|
showHelp(t('issues'), $element({
|
||||||
|
tag: 'pre',
|
||||||
|
textContent: errors.join('\n'),
|
||||||
|
}));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
function trimNewLines(s) {
|
function trimNewLines(s) {
|
||||||
return s.replace(/^[\s\n]+/, '').replace(/[\s\n]+$/, '');
|
return s.replace(/^[\s\n]+/, '').replace(/[\s\n]+$/, '');
|
||||||
|
|
|
@ -393,3 +393,16 @@ function invokeOrPostpone(isInvoke, fn, ...args) {
|
||||||
? fn(...args)
|
? fn(...args)
|
||||||
: setTimeout(invokeOrPostpone, 0, true, fn, ...args);
|
: setTimeout(invokeOrPostpone, 0, true, fn, ...args);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function openEditor(id) {
|
||||||
|
let url = '/edit.html';
|
||||||
|
if (id) {
|
||||||
|
url += `?id=${id}`;
|
||||||
|
}
|
||||||
|
if (prefs.get('openEditInWindow')) {
|
||||||
|
chrome.windows.create(Object.assign({url}, prefs.get('windowPosition')));
|
||||||
|
} else {
|
||||||
|
openURL({url});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
149
js/moz-parser.js
Normal file
149
js/moz-parser.js
Normal file
|
@ -0,0 +1,149 @@
|
||||||
|
/* global parserlib, loadScript */
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-var
|
||||||
|
var mozParser = (function () {
|
||||||
|
// direct & reverse mapping of @-moz-document keywords and internal property names
|
||||||
|
const propertyToCss = {urls: 'url', urlPrefixes: 'url-prefix', domains: 'domain', regexps: 'regexp'};
|
||||||
|
const CssToProperty = {'url': 'urls', 'url-prefix': 'urlPrefixes', 'domain': 'domains', 'regexp': 'regexps'};
|
||||||
|
|
||||||
|
function backtrackTo(parser, tokenType, startEnd) {
|
||||||
|
const tokens = parser._tokenStream._lt;
|
||||||
|
for (let i = parser._tokenStream._ltIndex - 1; i >= 0; --i) {
|
||||||
|
if (tokens[i].type === tokenType) {
|
||||||
|
return {line: tokens[i][startEnd + 'Line'], col: tokens[i][startEnd + 'Col']};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function trimNewLines(s) {
|
||||||
|
return s.replace(/^[\s\n]+/, '').replace(/[\s\n]+$/, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseMozFormat(mozStyle) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const parser = new parserlib.css.Parser();
|
||||||
|
const lines = mozStyle.split('\n');
|
||||||
|
const sectionStack = [{code: '', start: {line: 1, col: 1}}];
|
||||||
|
const errors = [];
|
||||||
|
const sections = [];
|
||||||
|
|
||||||
|
parser.addListener('startdocument', function (e) {
|
||||||
|
const lastSection = sectionStack[sectionStack.length - 1];
|
||||||
|
let outerText = getRange(lastSection.start, (--e.col, e));
|
||||||
|
const gapComment = outerText.match(/(\/\*[\s\S]*?\*\/)[\s\n]*$/);
|
||||||
|
const section = {code: '', start: backtrackTo(this, parserlib.css.Tokens.LBRACE, 'end')};
|
||||||
|
// move last comment before @-moz-document inside the section
|
||||||
|
if (gapComment && !gapComment[1].match(/\/\*\s*AGENT_SHEET\s*\*\//)) {
|
||||||
|
section.code = gapComment[1] + '\n';
|
||||||
|
outerText = trimNewLines(outerText.substring(0, gapComment.index));
|
||||||
|
}
|
||||||
|
if (outerText.trim()) {
|
||||||
|
lastSection.code = outerText;
|
||||||
|
doAddSection(lastSection);
|
||||||
|
lastSection.code = '';
|
||||||
|
}
|
||||||
|
for (const f of e.functions) {
|
||||||
|
const m = f && f.match(/^([\w-]*)\((['"]?)(.+?)\2?\)$/);
|
||||||
|
if (!m || !/^(url|url-prefix|domain|regexp)$/.test(m[1])) {
|
||||||
|
errors.push(`${e.line}:${e.col + 1} invalid function "${m ? m[1] : f || ''}"`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const aType = CssToProperty[m[1]];
|
||||||
|
const aValue = aType !== 'regexps' ? m[3] : m[3].replace(/\\\\/g, '\\');
|
||||||
|
(section[aType] = section[aType] || []).push(aValue);
|
||||||
|
}
|
||||||
|
sectionStack.push(section);
|
||||||
|
});
|
||||||
|
|
||||||
|
parser.addListener('enddocument', function () {
|
||||||
|
const end = backtrackTo(this, parserlib.css.Tokens.RBRACE, 'start');
|
||||||
|
const section = sectionStack.pop();
|
||||||
|
const lastSection = sectionStack[sectionStack.length - 1];
|
||||||
|
section.code += getRange(section.start, end);
|
||||||
|
lastSection.start = (++end.col, end);
|
||||||
|
doAddSection(section);
|
||||||
|
});
|
||||||
|
|
||||||
|
parser.addListener('endstylesheet', () => {
|
||||||
|
// add nonclosed outer sections (either broken or the last global one)
|
||||||
|
const lastLine = lines[lines.length - 1];
|
||||||
|
const endOfText = {line: lines.length, col: lastLine.length + 1};
|
||||||
|
const lastSection = sectionStack[sectionStack.length - 1];
|
||||||
|
lastSection.code += getRange(lastSection.start, endOfText);
|
||||||
|
sectionStack.forEach(doAddSection);
|
||||||
|
|
||||||
|
if (errors.length) {
|
||||||
|
reject(errors);
|
||||||
|
} else {
|
||||||
|
resolve(sections);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
parser.addListener('error', e => {
|
||||||
|
errors.push(e.line + ':' + e.col + ' ' +
|
||||||
|
e.message.replace(/ at line \d.+$/, ''));
|
||||||
|
});
|
||||||
|
|
||||||
|
parser.parse(mozStyle);
|
||||||
|
|
||||||
|
function getRange(start, end) {
|
||||||
|
const L1 = start.line - 1;
|
||||||
|
const C1 = start.col - 1;
|
||||||
|
const L2 = end.line - 1;
|
||||||
|
const C2 = end.col - 1;
|
||||||
|
if (L1 === L2) {
|
||||||
|
return lines[L1].substr(C1, C2 - C1 + 1);
|
||||||
|
} else {
|
||||||
|
const middle = lines.slice(L1 + 1, L2).join('\n');
|
||||||
|
return lines[L1].substr(C1) + '\n' + middle +
|
||||||
|
(L2 >= lines.length ? '' : ((middle ? '\n' : '') + lines[L2].substring(0, C2)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function doAddSection(section) {
|
||||||
|
section.code = section.code.trim();
|
||||||
|
// don't add empty sections
|
||||||
|
if (
|
||||||
|
!section.code &&
|
||||||
|
!section.urls &&
|
||||||
|
!section.urlPrefixes &&
|
||||||
|
!section.domains &&
|
||||||
|
!section.regexps
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
/* ignore boilerplate NS */
|
||||||
|
if (section.code === '@namespace url(http://www.w3.org/1999/xhtml);') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
sections.push(Object.assign({}, section));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
// Parse mozilla-format userstyle into sections
|
||||||
|
parse(text) {
|
||||||
|
if (typeof parserlib === 'undefined') {
|
||||||
|
return loadScript('vendor/csslint/csslint-worker.js')
|
||||||
|
.then(() => parseMozFormat(text));
|
||||||
|
}
|
||||||
|
return parseMozFormat(text);
|
||||||
|
},
|
||||||
|
format(style) {
|
||||||
|
return style.sections.map(section => {
|
||||||
|
let cssMds = [];
|
||||||
|
for (const i in propertyToCss) {
|
||||||
|
if (section[i]) {
|
||||||
|
cssMds = cssMds.concat(section[i].map(v =>
|
||||||
|
propertyToCss[i] + '("' + v.replace(/\\/g, '\\\\') + '")'
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cssMds.length ? '@-moz-document ' + cssMds.join(', ') + ' {\n' + section.code + '\n}' : section.code;
|
||||||
|
}).join('\n\n');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
})();
|
34
js/script-loader.js
Normal file
34
js/script-loader.js
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-var
|
||||||
|
var loadScript = (function () {
|
||||||
|
const cache = new Map();
|
||||||
|
|
||||||
|
return function (path) {
|
||||||
|
if (!path.includes('://')) {
|
||||||
|
path = chrome.runtime.getURL(path);
|
||||||
|
}
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (cache.has(path)) {
|
||||||
|
resolve(cache.get(path));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const script = document.createElement('script');
|
||||||
|
script.src = path;
|
||||||
|
script.onload = () => {
|
||||||
|
resolve(script);
|
||||||
|
script.onload = null;
|
||||||
|
script.onerror = null;
|
||||||
|
|
||||||
|
cache.set(path, script);
|
||||||
|
};
|
||||||
|
script.onerror = event => {
|
||||||
|
reject(event);
|
||||||
|
script.onload = null;
|
||||||
|
script.onerror = null;
|
||||||
|
script.parentNode.removeChild(script);
|
||||||
|
};
|
||||||
|
document.head.appendChild(script);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
})();
|
202
js/usercss.js
Normal file
202
js/usercss.js
Normal file
|
@ -0,0 +1,202 @@
|
||||||
|
/* global loadScript mozParser */
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-var
|
||||||
|
var usercss = (function () {
|
||||||
|
function semverTest(a, b) {
|
||||||
|
a = a.split('.').map(Number);
|
||||||
|
b = b.split('.').map(Number);
|
||||||
|
|
||||||
|
for (let i = 0; i < a.length; i++) {
|
||||||
|
if (!(i in b)) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (a[i] < b[i]) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (a[i] > b[i]) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (a.length < b.length) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function guessType(value) {
|
||||||
|
if (/^url\(.+\)$/i.test(value)) {
|
||||||
|
return 'image';
|
||||||
|
}
|
||||||
|
if (/^#[0-9a-f]{3,8}$/i.test(value)) {
|
||||||
|
return 'color';
|
||||||
|
}
|
||||||
|
if (/^hsla?\(.+\)$/i.test(value)) {
|
||||||
|
return 'color';
|
||||||
|
}
|
||||||
|
if (/^rgba?\(.+\)$/i.test(value)) {
|
||||||
|
return 'color';
|
||||||
|
}
|
||||||
|
// should we use a color-name table to guess type?
|
||||||
|
return 'text';
|
||||||
|
}
|
||||||
|
|
||||||
|
const BUILDER = {
|
||||||
|
default: {
|
||||||
|
postprocess(sections, vars) {
|
||||||
|
let varDef = ':root {\n';
|
||||||
|
for (const key of Object.keys(vars)) {
|
||||||
|
varDef += ` --${key}: ${vars[key].value};\n`;
|
||||||
|
}
|
||||||
|
varDef += '}\n';
|
||||||
|
|
||||||
|
for (const section of sections) {
|
||||||
|
section.code = varDef + section.code;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
stylus: {
|
||||||
|
preprocess(source, vars) {
|
||||||
|
return loadScript('vendor/stylus-lang/stylus.min.js').then(() => (
|
||||||
|
new Promise((resolve, reject) => {
|
||||||
|
let varDef = '';
|
||||||
|
for (const key of Object.keys(vars)) {
|
||||||
|
varDef += `${key} = ${vars[key].value};\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-undef
|
||||||
|
stylus(varDef + source).render((err, output) => {
|
||||||
|
if (err) {
|
||||||
|
reject(err);
|
||||||
|
} else {
|
||||||
|
resolve(output);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function getMetaSource(source) {
|
||||||
|
const commentRe = /\/\*[\s\S]*?\*\//g;
|
||||||
|
const metaRe = /==userstyle==[\s\S]*?==\/userstyle==/i;
|
||||||
|
|
||||||
|
let m;
|
||||||
|
// iterate through each comment
|
||||||
|
while ((m = commentRe.exec(source))) {
|
||||||
|
const commentSource = source.slice(m.index, m.index + m[0].length);
|
||||||
|
const n = commentSource.match(metaRe);
|
||||||
|
if (n) {
|
||||||
|
return n[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildMeta(source) {
|
||||||
|
const style = _buildMeta(source);
|
||||||
|
validate(style);
|
||||||
|
return style;
|
||||||
|
}
|
||||||
|
|
||||||
|
function _buildMeta(source) {
|
||||||
|
const style = {
|
||||||
|
name: null,
|
||||||
|
usercss: true,
|
||||||
|
version: null,
|
||||||
|
source: source,
|
||||||
|
enabled: true,
|
||||||
|
sections: [],
|
||||||
|
vars: {},
|
||||||
|
preprocessor: null
|
||||||
|
};
|
||||||
|
|
||||||
|
const metaSource = getMetaSource(source);
|
||||||
|
|
||||||
|
const match = (re, callback) => {
|
||||||
|
let m;
|
||||||
|
if (!re.global) {
|
||||||
|
if ((m = metaSource.match(re))) {
|
||||||
|
if (m.length === 1) {
|
||||||
|
callback(m[0]);
|
||||||
|
} else {
|
||||||
|
callback(...m.slice(1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const result = [];
|
||||||
|
while ((m = re.exec(metaSource))) {
|
||||||
|
if (m.length <= 2) {
|
||||||
|
result.push(m[m.length - 1]);
|
||||||
|
} else {
|
||||||
|
result.push(m.slice(1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (result.length) {
|
||||||
|
callback(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// FIXME: finish all metas
|
||||||
|
match(/@name[^\S\r\n]+(.+?)[^\S\r\n]*$/m, m => (style.name = m));
|
||||||
|
match(/@namespace[^\S\r\n]+(\S+)/, m => (style.namespace = m));
|
||||||
|
match(/@preprocessor[^\S\r\n]+(\S+)/, m => (style.preprocessor = m));
|
||||||
|
match(/@version[^\S\r\n]+(\S+)/, m => (style.version = m));
|
||||||
|
match(
|
||||||
|
/@var[^\S\r\n]+(\S+)[^\S\r\n]+(?:(['"])((?:\\\2|.)*?)\2|(\S+))[^\S\r\n]+(.+?)[^\S\r\n]*$/gm,
|
||||||
|
ms => ms.forEach(([key,, label1, label2, value]) => (
|
||||||
|
style.vars[key] = {
|
||||||
|
type: guessType(value),
|
||||||
|
label: label1 || label2,
|
||||||
|
value: value
|
||||||
|
}
|
||||||
|
))
|
||||||
|
);
|
||||||
|
|
||||||
|
return style;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildCode(style) {
|
||||||
|
let builder;
|
||||||
|
if (style.preprocessor && style.preprocessor in BUILDER) {
|
||||||
|
builder = BUILDER[style.preprocessor];
|
||||||
|
} else {
|
||||||
|
builder = BUILDER.default;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.resolve().then(() => {
|
||||||
|
// preprocess
|
||||||
|
if (builder.preprocess) {
|
||||||
|
return builder.preprocess(style.source, style.vars);
|
||||||
|
}
|
||||||
|
return style.source;
|
||||||
|
}).then(mozStyle =>
|
||||||
|
// moz-parser
|
||||||
|
loadScript('/js/moz-parser.js').then(() =>
|
||||||
|
mozParser.parse(mozStyle).then(sections => {
|
||||||
|
style.sections = sections;
|
||||||
|
})
|
||||||
|
)
|
||||||
|
).then(() => {
|
||||||
|
// postprocess
|
||||||
|
if (builder.postprocess) {
|
||||||
|
return builder.postprocess(style.sections, style.vars);
|
||||||
|
}
|
||||||
|
}).then(() => style);
|
||||||
|
}
|
||||||
|
|
||||||
|
function validate(style) {
|
||||||
|
// mandatory fields
|
||||||
|
for (const prop of ['name', 'namespace', 'version']) {
|
||||||
|
if (!style[prop]) {
|
||||||
|
throw new Error(chrome.i18n.getMessage('styleMissingMeta', prop));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {buildMeta, buildCode, semverTest};
|
||||||
|
})();
|
|
@ -73,6 +73,15 @@
|
||||||
</svg>
|
</svg>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<template data-id="configureIcon">
|
||||||
|
<!-- FIXME: icon -->
|
||||||
|
<span class="configure-usercss" i18n-title="configureStyle">
|
||||||
|
<svg class="svg-icon" viewBox="0 0 20 20">
|
||||||
|
<path d="M4,4h5v2H6v8h8v-3h2v5H4V4z M11,3h6v6l-2-2l-4,4L9,9l4-4L11,3z"/>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
<template data-id="updaterIcons">
|
<template data-id="updaterIcons">
|
||||||
<span class="updater-icons">
|
<span class="updater-icons">
|
||||||
<span class="check-update" i18n-title="checkForUpdate">
|
<span class="check-update" i18n-title="checkForUpdate">
|
||||||
|
|
|
@ -186,12 +186,15 @@ function createStyleElement({style, name}) {
|
||||||
(style.enabled ? 'enabled' : 'disabled') +
|
(style.enabled ? 'enabled' : 'disabled') +
|
||||||
(style.updateUrl ? ' updatable' : '');
|
(style.updateUrl ? ' updatable' : '');
|
||||||
|
|
||||||
if (style.url) {
|
if (style.url && !style.usercss) {
|
||||||
$('.homepage', entry).appendChild(parts.homepageIcon.cloneNode(true));
|
$('.homepage', entry).appendChild(parts.homepageIcon.cloneNode(true));
|
||||||
}
|
}
|
||||||
if (style.updateUrl && newUI.enabled) {
|
if (style.updateUrl && newUI.enabled) {
|
||||||
$('.actions', entry).appendChild(template.updaterIcons.cloneNode(true));
|
$('.actions', entry).appendChild(template.updaterIcons.cloneNode(true));
|
||||||
}
|
}
|
||||||
|
if (style.vars && Object.keys(style.vars).length && newUI.enabled) {
|
||||||
|
$('.actions', entry).appendChild(template.configureIcon.cloneNode(true));
|
||||||
|
}
|
||||||
|
|
||||||
// name being supplied signifies we're invoked by showStyles()
|
// name being supplied signifies we're invoked by showStyles()
|
||||||
// which debounces its main loop thus loading the postponed favicons
|
// which debounces its main loop thus loading the postponed favicons
|
||||||
|
@ -275,6 +278,39 @@ Object.assign(handleEvent, {
|
||||||
'.update': 'update',
|
'.update': 'update',
|
||||||
'.delete': 'delete',
|
'.delete': 'delete',
|
||||||
'.applies-to .expander': 'expandTargets',
|
'.applies-to .expander': 'expandTargets',
|
||||||
|
'.configure-usercss': 'config'
|
||||||
|
},
|
||||||
|
|
||||||
|
config(event, {styleMeta: style}) {
|
||||||
|
let isChanged = false;
|
||||||
|
|
||||||
|
messageBox({
|
||||||
|
title: `Configure ${style.name}`,
|
||||||
|
className: 'regular-form',
|
||||||
|
contents: buildConfigForm(),
|
||||||
|
buttons: [t('confirmClose')]
|
||||||
|
}).then(() => {
|
||||||
|
if (!isChanged) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
style.reason = 'config';
|
||||||
|
saveStyleSafe(style);
|
||||||
|
});
|
||||||
|
|
||||||
|
function buildConfigForm() {
|
||||||
|
const labels = [];
|
||||||
|
for (const va of Object.values(style.vars)) {
|
||||||
|
const input = $element({tag: 'input', type: 'text', value: va.value});
|
||||||
|
input.oninput = () => {
|
||||||
|
isChanged = true;
|
||||||
|
va.value = input.value;
|
||||||
|
animateElement(input, {className: 'value-update'});
|
||||||
|
};
|
||||||
|
const label = $element({tag: 'label', appendChild: [va.label, input]});
|
||||||
|
labels.push(label);
|
||||||
|
}
|
||||||
|
return labels;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
entryClicked(event) {
|
entryClicked(event) {
|
||||||
|
|
|
@ -22,8 +22,10 @@
|
||||||
"scripts": [
|
"scripts": [
|
||||||
"js/messaging.js",
|
"js/messaging.js",
|
||||||
"vendor-overwrites/lz-string/LZString-2xspeedup.js",
|
"vendor-overwrites/lz-string/LZString-2xspeedup.js",
|
||||||
|
"js/usercss.js",
|
||||||
"background/storage.js",
|
"background/storage.js",
|
||||||
"js/prefs.js",
|
"js/prefs.js",
|
||||||
|
"js/script-loader.js",
|
||||||
"background/background.js",
|
"background/background.js",
|
||||||
"background/update.js"
|
"background/update.js"
|
||||||
]
|
]
|
||||||
|
@ -49,6 +51,13 @@
|
||||||
"run_at": "document_start",
|
"run_at": "document_start",
|
||||||
"all_frames": false,
|
"all_frames": false,
|
||||||
"js": ["content/install.js"]
|
"js": ["content/install.js"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"matches": ["<all_urls>"],
|
||||||
|
"include_globs": ["*.user.css", "*.user.styl"],
|
||||||
|
"run_at": "document_idle",
|
||||||
|
"all_frames": false,
|
||||||
|
"js": ["content/install-user-css.js"]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"browser_action": {
|
"browser_action": {
|
||||||
|
|
|
@ -137,3 +137,25 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#message-box.regular-form input[type=text] {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
margin: .4rem 0 .6rem;
|
||||||
|
padding-left: .25rem;
|
||||||
|
border-radius: .25rem;
|
||||||
|
border-width: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#message-box.regular-form input[type=text].value-update {
|
||||||
|
animation-name: input-fadeout;
|
||||||
|
animation-duration: .4s;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes input-fadeout {
|
||||||
|
from {
|
||||||
|
background: palegreen;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
background: white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
1
vendor/stylus-lang/README.md
vendored
Normal file
1
vendor/stylus-lang/README.md
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
The content of this folder belongs to [stylus preprocessor](https://github.com/stylus/stylus/).
|
6
vendor/stylus-lang/stylus.min.js
vendored
Normal file
6
vendor/stylus-lang/stylus.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue
Block a user