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": {
|
||||
"message": "History of update checks"
|
||||
},
|
||||
"configureStyle": {
|
||||
"message": "Configure",
|
||||
"description": "Label for the button to configure userstyle"
|
||||
},
|
||||
"checkForUpdate": {
|
||||
"message": "Check for update",
|
||||
"description": "Label for the button to check a single style for an update"
|
||||
|
@ -175,6 +179,10 @@
|
|||
"message": "Yes",
|
||||
"description": "'Yes' button in a confirm dialog"
|
||||
},
|
||||
"confirmClose": {
|
||||
"message": "Close",
|
||||
"description": "'Close' button in a confirm dialog"
|
||||
},
|
||||
"dbError": {
|
||||
"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"
|
||||
|
@ -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": {
|
||||
"message": "Enter 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';
|
||||
|
||||
// eslint-disable-next-line no-var
|
||||
|
@ -322,6 +322,14 @@ function onRuntimeMessage(request, sender, sendResponse) {
|
|||
saveStyle(request).then(sendResponse);
|
||||
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':
|
||||
dbExec()
|
||||
.then(() => sendResponse(true))
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
/* global LZString */
|
||||
/* global usercss, openEditor */
|
||||
|
||||
'use strict';
|
||||
|
||||
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) {
|
||||
const id = Number(style.id) || null;
|
||||
let id = Number(style.id) || null;
|
||||
const reason = style.reason;
|
||||
const notify = style.notify !== false;
|
||||
delete style.method;
|
||||
|
@ -271,6 +308,11 @@ function saveStyle(style) {
|
|||
}
|
||||
let existed;
|
||||
let codeIsUpdated;
|
||||
|
||||
if (style.usercss) {
|
||||
return processUsercss(style).then(decide);
|
||||
}
|
||||
|
||||
if (reason === 'update' || reason === 'update-digest') {
|
||||
return calcStyleDigest(style).then(digest => {
|
||||
style.originalDigest = digest;
|
||||
|
@ -286,6 +328,27 @@ function saveStyle(style) {
|
|||
}
|
||||
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() {
|
||||
if (id !== null) {
|
||||
// Update or create
|
||||
|
@ -338,6 +401,10 @@ function saveStyle(style) {
|
|||
style, codeIsUpdated, reason,
|
||||
});
|
||||
}
|
||||
if (style.usercss && !existed && reason === 'install') {
|
||||
// open the editor for usercss with the first install?
|
||||
openEditor(style.id);
|
||||
}
|
||||
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({
|
||||
style,
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/* global getStyles, saveStyle, styleSectionsEqual, chromeLocal */
|
||||
/* global calcStyleDigest */
|
||||
/* global calcStyleDigest, usercss */
|
||||
'use strict';
|
||||
|
||||
// eslint-disable-next-line no-var
|
||||
|
@ -15,8 +15,10 @@ var updater = {
|
|||
MAYBE_EDITED: 'may be locally edited',
|
||||
SAME_MD5: 'up-to-date: MD5 is 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_JSON: 'error: JSON is invalid',
|
||||
ERROR_VERSION: 'error: version is invalid',
|
||||
|
||||
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.
|
||||
*/
|
||||
const maybeUpdate = style.usercss ? maybeUpdateUsercss : maybeUpdateUSO;
|
||||
return (ignoreDigest ? Promise.resolve() : calcStyleDigest(style))
|
||||
.then(maybeFetchMd5)
|
||||
.then(maybeFetchCode)
|
||||
.then(checkIfEdited)
|
||||
.then(maybeUpdate)
|
||||
.then(maybeSave)
|
||||
.then(saved => {
|
||||
observer(updater.UPDATED, saved);
|
||||
|
@ -67,25 +70,49 @@ var updater = {
|
|||
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) {
|
||||
return Promise.reject(updater.EDITED);
|
||||
}
|
||||
return download(style.md5Url);
|
||||
}
|
||||
|
||||
function maybeFetchCode(md5) {
|
||||
function maybeUpdateUSO() {
|
||||
return download(style.md5Url).then(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);
|
||||
}
|
||||
return download(style.updateUrl);
|
||||
return download(style.updateUrl)
|
||||
.then(text => tryJSONparse(text));
|
||||
});
|
||||
}
|
||||
|
||||
function maybeSave(text) {
|
||||
const json = tryJSONparse(text);
|
||||
function maybeUpdateUsercss() {
|
||||
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)) {
|
||||
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/prefs.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>
|
||||
<link rel="stylesheet" href="edit/edit.css">
|
||||
<script src="edit/lint.js"></script>
|
||||
|
|
158
edit/edit.js
158
edit/edit.js
|
@ -3,6 +3,8 @@
|
|||
/* global onDOMscripted */
|
||||
/* global css_beautify */
|
||||
/* global CSSLint initLint linterConfig updateLintReport renderLintReport updateLinter */
|
||||
/* global mozParser */
|
||||
|
||||
'use strict';
|
||||
|
||||
let styleId = null;
|
||||
|
@ -1498,17 +1500,7 @@ function showMozillaFormat() {
|
|||
}
|
||||
|
||||
function toMozillaFormat() {
|
||||
return getSectionsHashes().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');
|
||||
return mozParser.format({sections: getSectionsHashes()});
|
||||
}
|
||||
|
||||
function fromMozillaFormat() {
|
||||
|
@ -1542,121 +1534,8 @@ function fromMozillaFormat() {
|
|||
const replaceOldStyle = target.name === 'import-replace';
|
||||
$('.dismiss', popup).onclick();
|
||||
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) {
|
||||
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;
|
||||
}
|
||||
mozParser.parse(mozStyle).then(sections => {
|
||||
if (replaceOldStyle) {
|
||||
editors.slice(0).reverse().forEach(cm => {
|
||||
removeSection({target: cm.getSection().firstElementChild});
|
||||
|
@ -1667,16 +1546,27 @@ function fromMozillaFormat() {
|
|||
removeSection({target: editors.last.getSection()});
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
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']};
|
||||
}
|
||||
|
||||
const firstSection = sections[0];
|
||||
setCleanItem(addSection(null, firstSection), false);
|
||||
const firstAddedCM = editors.last;
|
||||
for (const section of sections.slice(1)) {
|
||||
setCleanItem(addSection(null, section), false);
|
||||
}
|
||||
|
||||
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) {
|
||||
return s.replace(/^[\s\n]+/, '').replace(/[\s\n]+$/, '');
|
||||
|
|
|
@ -393,3 +393,16 @@ function invokeOrPostpone(isInvoke, fn, ...args) {
|
|||
? 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>
|
||||
</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">
|
||||
<span class="updater-icons">
|
||||
<span class="check-update" i18n-title="checkForUpdate">
|
||||
|
|
|
@ -186,12 +186,15 @@ function createStyleElement({style, name}) {
|
|||
(style.enabled ? 'enabled' : 'disabled') +
|
||||
(style.updateUrl ? ' updatable' : '');
|
||||
|
||||
if (style.url) {
|
||||
if (style.url && !style.usercss) {
|
||||
$('.homepage', entry).appendChild(parts.homepageIcon.cloneNode(true));
|
||||
}
|
||||
if (style.updateUrl && newUI.enabled) {
|
||||
$('.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()
|
||||
// which debounces its main loop thus loading the postponed favicons
|
||||
|
@ -275,6 +278,39 @@ Object.assign(handleEvent, {
|
|||
'.update': 'update',
|
||||
'.delete': 'delete',
|
||||
'.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) {
|
||||
|
|
|
@ -22,8 +22,10 @@
|
|||
"scripts": [
|
||||
"js/messaging.js",
|
||||
"vendor-overwrites/lz-string/LZString-2xspeedup.js",
|
||||
"js/usercss.js",
|
||||
"background/storage.js",
|
||||
"js/prefs.js",
|
||||
"js/script-loader.js",
|
||||
"background/background.js",
|
||||
"background/update.js"
|
||||
]
|
||||
|
@ -49,6 +51,13 @@
|
|||
"run_at": "document_start",
|
||||
"all_frames": false,
|
||||
"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": {
|
||||
|
|
|
@ -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