From dece4b57f32ef6efba0b74a2eaa3fc942da3f309 Mon Sep 17 00:00:00 2001 From: eight Date: Sun, 6 Aug 2017 00:49:25 +0800 Subject: [PATCH 001/250] 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 --- _locales/en/messages.json | 45 +++++++ background/background.js | 10 +- background/storage.js | 80 +++++++++++- background/update.js | 57 ++++++--- content/install-user-css.js | 72 +++++++++++ edit.html | 2 + edit/edit.js | 158 ++++-------------------- js/messaging.js | 13 ++ js/moz-parser.js | 149 +++++++++++++++++++++++ js/script-loader.js | 34 ++++++ js/usercss.js | 202 +++++++++++++++++++++++++++++++ manage.html | 9 ++ manage/manage.js | 38 +++++- manifest.json | 9 ++ msgbox/msgbox.css | 22 ++++ vendor/stylus-lang/README.md | 1 + vendor/stylus-lang/stylus.min.js | 6 + 17 files changed, 755 insertions(+), 152 deletions(-) create mode 100644 content/install-user-css.js create mode 100644 js/moz-parser.js create mode 100644 js/script-loader.js create mode 100644 js/usercss.js create mode 100644 vendor/stylus-lang/README.md create mode 100644 vendor/stylus-lang/stylus.min.js diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 7e89af27..76becab3 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -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" diff --git a/background/background.js b/background/background.js index 1b3dbc70..70b26e0a 100644 --- a/background/background.js +++ b/background/background.js @@ -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)) diff --git a/background/storage.js b/background/storage.js index bc566874..54ac566f 100644 --- a/background/storage.js +++ b/background/storage.js @@ -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, diff --git a/background/update.js b/background/update.js index 04f368f5..74a99bf2 100644 --- a/background/update.js +++ b/background/update.js @@ -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) { - 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); + 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) + .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); } diff --git a/content/install-user-css.js b/content/install-user-css.js new file mode 100644 index 00000000..a8bb4f09 --- /dev/null +++ b/content/install-user-css.js @@ -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); +} diff --git a/edit.html b/edit.html index b8d2b243..0d51cc3b 100644 --- a/edit.html +++ b/edit.html @@ -6,6 +6,8 @@ + + diff --git a/edit/edit.js b/edit/edit.js index d28b15f4..39804c61 100644 --- a/edit/edit.js +++ b/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]+$/, ''); diff --git a/js/messaging.js b/js/messaging.js index 594beeb0..acc48a8b 100644 --- a/js/messaging.js +++ b/js/messaging.js @@ -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}); + } +} diff --git a/js/moz-parser.js b/js/moz-parser.js new file mode 100644 index 00000000..df446aac --- /dev/null +++ b/js/moz-parser.js @@ -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'); + } + }; +})(); diff --git a/js/script-loader.js b/js/script-loader.js new file mode 100644 index 00000000..ea31ba0d --- /dev/null +++ b/js/script-loader.js @@ -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); + }); + }; +})(); diff --git a/js/usercss.js b/js/usercss.js new file mode 100644 index 00000000..62ead841 --- /dev/null +++ b/js/usercss.js @@ -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}; +})(); diff --git a/manage.html b/manage.html index bd6f73d8..76ecf071 100644 --- a/manage.html +++ b/manage.html @@ -73,6 +73,15 @@ + + From 9c2acd5cc92cef3f2728d62192cc0865d2cdaecc Mon Sep 17 00:00:00 2001 From: eight Date: Fri, 1 Sep 2017 14:36:13 +0800 Subject: [PATCH 005/250] Fix: remove unused variable 'event' --- js/script-loader.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/script-loader.js b/js/script-loader.js index feb56bdf..38a9c648 100644 --- a/js/script-loader.js +++ b/js/script-loader.js @@ -22,7 +22,7 @@ var loadScript = (function () { cache.set(path, script); }; - script.onerror = event => { + script.onerror = () => { reject(new Error(`failed to load script: ${path}`)); script.onload = null; script.onerror = null; From 8607d779f96f228d174e6ea1f7d54457407536fe Mon Sep 17 00:00:00 2001 From: eight Date: Fri, 1 Sep 2017 14:38:46 +0800 Subject: [PATCH 006/250] Change how var is saved --- js/usercss.js | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/js/usercss.js b/js/usercss.js index 62ead841..51a44fed 100644 --- a/js/usercss.js +++ b/js/usercss.js @@ -152,7 +152,8 @@ var usercss = (function () { style.vars[key] = { type: guessType(value), label: label1 || label2, - value: value + value: null, // '.value' holds the value set by users. + default: value // '.default' holds the value extract from meta. } )) ); @@ -168,10 +169,12 @@ var usercss = (function () { builder = BUILDER.default; } + const vars = simpleVars(style.vars); + return Promise.resolve().then(() => { // preprocess if (builder.preprocess) { - return builder.preprocess(style.source, style.vars); + return builder.preprocess(style.source, vars); } return style.source; }).then(mozStyle => @@ -184,11 +187,24 @@ var usercss = (function () { ).then(() => { // postprocess if (builder.postprocess) { - return builder.postprocess(style.sections, style.vars); + return builder.postprocess(style.sections, vars); } }).then(() => style); } + function simpleVars(vars) { + // simplify vars by merging `va.default` to `va.value`, so BUILDER don't + // need to test each va's default value. + return Object.keys(vars).reduce((output, key) => { + const va = vars[key]; + output[key] = { + value: va.value === null || va.value === undefined ? + va.default : va.value + }; + return output; + }, {}); + } + function validate(style) { // mandatory fields for (const prop of ['name', 'namespace', 'version']) { From dfb7ac9b4470c2f437d1f0ec8c8660cb66fbb1df Mon Sep 17 00:00:00 2001 From: eight Date: Fri, 1 Sep 2017 14:46:00 +0800 Subject: [PATCH 007/250] Extend messageBox to set onclick handler on buttons --- msgbox/msgbox.js | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/msgbox/msgbox.js b/msgbox/msgbox.js index 4e418832..3f23e273 100644 --- a/msgbox/msgbox.js +++ b/msgbox/msgbox.js @@ -4,7 +4,7 @@ function messageBox({ title, // [mandatory] string contents, // [mandatory] 1) DOM element 2) string className = '', // string, CSS class name of the message box element - buttons = [], // array of strings used as labels + buttons = [], // array of strings or objects like {textContent[string], onclick[function]}. onshow, // function(messageboxElement) invoked after the messagebox is shown blockScroll, // boolean, blocks the page scroll }) { // RETURNS: Promise resolved to {button[number], enter[boolean], esc[boolean]} @@ -67,14 +67,21 @@ function messageBox({ onclick: messageBox.listeners.closeIcon}), $element({id: `${id}-contents`, appendChild: tHTML(contents)}), $element({id: `${id}-buttons`, appendChild: - buttons.map((textContent, buttonIndex) => textContent && - $element({ + buttons.map((textContent, buttonIndex) => { + if (!textContent) { + return; + } + let onclick = messageBox.listeners.button; + if (typeof textContent === 'object') { + ({onclick = onclick, textContent} = textContent); + } + return $element({ tag: 'button', buttonIndex, textContent, - onclick: messageBox.listeners.button, - }) - ) + onclick, + }); + }) }), ]}), ]}); From acd9befc9e518e445c87181c326d45610823188c Mon Sep 17 00:00:00 2001 From: eight Date: Fri, 1 Sep 2017 14:48:11 +0800 Subject: [PATCH 008/250] Change how configure dialog works. --- _locales/en/messages.json | 8 +++++ manage/manage.js | 76 ++++++++++++++++++++++++++++++++------- msgbox/msgbox.css | 14 -------- 3 files changed, 71 insertions(+), 27 deletions(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 76becab3..38202629 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -171,6 +171,14 @@ "message": "No", "description": "'No' button in a confirm dialog" }, + "confirmDefault": { + "message": "Use default", + "description": "'Set to default' button in a confirm dialog" + }, + "confirmSave": { + "message": "Save", + "description": "'Save' button in a confirm dialog" + }, "confirmStop": { "message": "Stop", "description": "'Stop' button in a confirm dialog" diff --git a/manage/manage.js b/manage/manage.js index 081b3d22..1df09ba9 100644 --- a/manage/manage.js +++ b/manage/manage.js @@ -282,34 +282,84 @@ Object.assign(handleEvent, { }, config(event, {styleMeta: style}) { - let isChanged = false; + const form = buildConfigForm(); messageBox({ title: `Configure ${style.name}`, className: 'regular-form', - contents: buildConfigForm(), - buttons: [t('confirmClose')] - }).then(() => { - if (!isChanged) { + contents: form.el, + buttons: [ + t('confirmSave'), + { + textContent: t('confirmDefault'), + onclick: form.useDefault + }, + t('confirmCancel') + ] + }).then(result => { + if (result.button !== 0 && !result.enter) { return; } style.reason = 'config'; + const vars = form.getVars(); + let dirty = false; + for (const key of Object.keys(vars)) { + if (vars[key].dirty) { + dirty = true; + style.vars[key].value = vars[key].value; + } + } + if (!dirty) { + return; + } 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 vars = deepCopy(style.vars); + for (const key of Object.keys(vars)) { + const va = vars[key]; + va.input = $element({tag: 'input', type: 'text', value: va.value}); + va.input.oninput = () => { + va.dirty = true; + va.value = va.input.value; }; - const label = $element({tag: 'label', appendChild: [va.label, input]}); + const label = $element({ + tag: 'label', + appendChild: [va.label, va.input] + }); labels.push(label); } - return labels; + drawValues(); + + function drawValues() { + for (const key of Object.keys(vars)) { + const va = vars[key]; + va.input.value = va.value === null || va.value === undefined ? + va.default : va.value; + } + } + + function useDefault() { + for (const key of Object.keys(vars)) { + const va = vars[key]; + va.dirty = va.value !== null && va.value !== undefined && + va.value !== va.default; + va.value = null; + } + drawValues(); + } + + function getVars() { + return vars; + } + + return { + el: labels, + useDefault, + getVars + }; } }, diff --git a/msgbox/msgbox.css b/msgbox/msgbox.css index 59a49636..59b87984 100644 --- a/msgbox/msgbox.css +++ b/msgbox/msgbox.css @@ -145,17 +145,3 @@ 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; - } -} From 0e5ab44f67ed89128588bdc64f83f7916032fe8b Mon Sep 17 00:00:00 2001 From: eight Date: Fri, 1 Sep 2017 18:21:01 +0800 Subject: [PATCH 009/250] Fix: remove message.js dependency from localization.js --- js/localization.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/js/localization.js b/js/localization.js index 0065d22c..0176f949 100644 --- a/js/localization.js +++ b/js/localization.js @@ -103,7 +103,11 @@ function tNodeList(nodes) { function tDocLoader() { t.DOMParser = new DOMParser(); - t.cache = tryJSONparse(localStorage.L10N) || {}; + try { + t.cache = JSON.parse(localStorage.L10N); + } catch (err) { + t.cache = {}; + } // reset L10N cache on UI language change const UIlang = chrome.i18n.getUILanguage(); From 3c40b52f96692017f008801dcfc73c985e391a1e Mon Sep 17 00:00:00 2001 From: eight Date: Fri, 1 Sep 2017 18:21:45 +0800 Subject: [PATCH 010/250] Add 'injectResource' message to inject js/css --- background/background.js | 51 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/background/background.js b/background/background.js index 70b26e0a..61da3214 100644 --- a/background/background.js +++ b/background/background.js @@ -341,5 +341,56 @@ function onRuntimeMessage(request, sender, sendResponse) { .then(sendResponse) .catch(() => sendResponse(null)); return KEEP_CHANNEL_OPEN; + + case 'injectResource': + injectResource(request, sender.tab.id).then(sendResponse); + return KEEP_CHANNEL_OPEN; + } +} + +function injectResource({resources}, tabId) { + return Promise.all(doInject()) + .then(() => ({status: 'success'})) + .catch(err => ({status: 'error', error: err.message})); + + function *doInject() { + for (const resource of resources) { + const type = resource.match(/\.\w+$/i)[0]; + if (type === '.js') { + yield injectScript(resource, tabId); + } else if (type === '.css') { + yield injectStyle(resource, tabId); + } + } + } + + function injectScript(url, tabId) { + return new Promise((resolve, reject) => { + chrome.tabs.executeScript(tabId, { + file: url, + runAt: 'document_start' + }, () => { + if (chrome.runtime.lastError) { + reject(chrome.runtime.lastError); + } else { + resolve(); + } + }); + }); + } + + function injectStyle(url, tabId) { + return new Promise((resolve, reject) => { + chrome.tabs.insertCSS(tabId, { + file: url, + runAt: 'document_start' + }, () => { + if (chrome.runtime.lastError) { + reject(chrome.runtime.lastError); + } else { + resolve(); + } + }); + }); } } From 4dec09708cf151ce7a2b3bc860c087fb69618748 Mon Sep 17 00:00:00 2001 From: eight Date: Fri, 1 Sep 2017 18:23:50 +0800 Subject: [PATCH 011/250] Rewrite usercss installation page --- content/install-user-css.css | 55 ++++++++++++++++++ content/install-user-css.js | 109 +++++++++++++++++++++++++++++------ 2 files changed, 145 insertions(+), 19 deletions(-) create mode 100644 content/install-user-css.css diff --git a/content/install-user-css.css b/content/install-user-css.css new file mode 100644 index 00000000..5488964f --- /dev/null +++ b/content/install-user-css.css @@ -0,0 +1,55 @@ +body { + margin: 0; + font: 12px arial, sans-serif; +} + +* { + box-sizing: border-box; +} + +.container { + display: flex; + min-height: 100vh; + align-items: stretch; +} + +.header { + width: 280px; + padding: 15px; + border-right: 1px dashed #aaa; + box-shadow: 0 0 50px -18px black; +} + +.header h1:first-child { + margin-top: 0; +} + +.meta { + font-size: 1.4em; +} + +.warning { + padding: 3px 6px; + border: 1px dashed black; + + border-color: #ef6969; + background: #ffe2e2; +} + +.header .warning { + margin: 3px 0; +} + +.actions { + margin: 15px 0; +} + +.actions > * { + display: inline-block; +} + +.code { + padding: 2em; + font-family: monospace; + white-space: pre-wrap; +} diff --git a/content/install-user-css.js b/content/install-user-css.js index a8bb4f09..9247a358 100644 --- a/content/install-user-css.js +++ b/content/install-user-css.js @@ -1,5 +1,9 @@ +/* global usercss */ + 'use strict'; +let pendingResource; + function fetchText(url) { return new Promise((resolve, reject) => { // you can't use fetch in Chrome under 'file:' protocol @@ -18,7 +22,16 @@ function install(style) { url: location.href, updateUrl: location.href }); - return communicate(request); + return communicate(request) + .then(() => { + $$('.meta-version + .warning') + .forEach(el => el.remove()); + $('button.install').textContent = 'Installed'; + $('button.install').disabled = true; + }) + .catch(err => { + alert(chrome.i18n.getMessage('styleInstallFailed', String(err))); + }); } function communicate(request) { @@ -33,26 +46,85 @@ function communicate(request) { }); } -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); +function initInstallPage({style, dup}) { + pendingResource.then(() => { + const versionTest = dup && usercss.semverTest(style.version, dup.version); + document.body.innerHTML = ''; + // FIXME: i18n + document.body.appendChild(tHTML(` +
+
+

Install Usercss

+

Name

+ ${style.name} +

Version

+ ${style.version} +
+ +
+
+
+
+ `)); + if (versionTest < 0) { + // FIXME: i18n + $('.meta-version').after(tHTML(` +
+ The version is older then installed style. +
+ `)); } - }).catch(err => { - alert(chrome.i18n.getMessage('styleInstallFailed', String(err))); + $('.code').textContent = style.source; + $('button.install').onclick = () => { + if (dup) { + if (confirm(chrome.i18n.getMessage('styleInstallOverwrite', [style.name, dup.version, style.version]))) { + install(style); + } + } else if (confirm(chrome.i18n.getMessage('styleInstall', [style.name]))) { + install(style); + } + }; }); } +function initErrorPage(err, source) { + pendingResource.then(() => { + document.body.innerHTML = ''; + // FIXME: i18n + document.body.appendChild(tHTML(` +
+ Stylus failed to parse usercss: ${err} +
+
+ `)); + $('.code').textContent = source; + }); +} + +function initUsercssInstall() { + let source; + pendingResource = communicate({ + method: 'injectResource', + resources: [ + '/js/dom.js', + '/js/localization.js', + '/js/usercss.js', + '/content/install-user-css.css' + ] + }); + fetchText(location.href) + .then(_source => { + source = _source; + return communicate({ + method: 'filterUsercss', + source, + checkDup: true + }); + }) + .then(initInstallPage) + .catch(err => initErrorPage(err, source)); +} + function isUsercss() { if (!/\.user\.(css|styl|less|scss|sass)$/i.test(location.pathname)) { return false; @@ -67,6 +139,5 @@ function isUsercss() { } if (isUsercss()) { - // It seems that we need to wait some time to redraw the page. - setTimeout(initUsercssInstall, 500); + initUsercssInstall(); } From b3b47697ca33ab108d4dcae276e90bafb73e1011 Mon Sep 17 00:00:00 2001 From: eight Date: Fri, 1 Sep 2017 18:24:32 +0800 Subject: [PATCH 012/250] Fix: display homepage icon for usercss --- manage/manage.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manage/manage.js b/manage/manage.js index 1df09ba9..36d02a43 100644 --- a/manage/manage.js +++ b/manage/manage.js @@ -186,7 +186,7 @@ function createStyleElement({style, name}) { (style.enabled ? 'enabled' : 'disabled') + (style.updateUrl ? ' updatable' : ''); - if (style.url && !style.usercss) { + if (style.url) { $('.homepage', entry).appendChild(parts.homepageIcon.cloneNode(true)); } if (style.updateUrl && newUI.enabled) { From 78264a1c345afae1750b798b5fe8bce48edecf04 Mon Sep 17 00:00:00 2001 From: eight Date: Tue, 5 Sep 2017 08:16:08 +0800 Subject: [PATCH 013/250] Add: parse more metas, add variable type --- js/usercss.js | 145 ++++++++++++++++++++++++++++++-------------------- 1 file changed, 87 insertions(+), 58 deletions(-) diff --git a/js/usercss.js b/js/usercss.js index 51a44fed..7ff85ad2 100644 --- a/js/usercss.js +++ b/js/usercss.js @@ -4,6 +4,12 @@ // eslint-disable-next-line no-var var usercss = (function () { + const METAS = [ + 'author', 'description', 'homepageURL', 'icon', 'license', 'name', + 'namespace', 'noframes', 'preprocessor', 'supportURL', 'var', 'version' + ]; + + // FIXME: use a real semver module function semverTest(a, b) { a = a.split('.').map(Number); b = b.split('.').map(Number); @@ -27,23 +33,6 @@ var usercss = (function () { 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) { @@ -102,6 +91,70 @@ var usercss = (function () { return style; } + function *parseMetas(source) { + for (const line of source.split(/\r?\n/)) { + const match = line.match(/@(\w+)/); + if (!match) { + continue; + } + yield [match[1], line.slice(match.index + match[0].length).trim()]; + } + } + + function matchString(s) { + const match = matchFollow(s, /^(?:\w+|(['"])(?:\\\1|.)*?\1)/); + match.value = match[1] ? match[0].slice(1, -1) : match[0]; + return match; + } + + function matchFollow(s, re) { + const match = s.match(re); + match.follow = s.slice(match.index + match[0].length).trim(); + return match; + } + + // FIXME: need color converter + function normalizeColor(color) { + return color; + } + + function parseVar(source) { + const result = { + label: null, + name: null, + value: null, + default: null, + select: null + }; + + { + // type & name + const match = matchFollow(source, /^([\w-]+)\s+([\w-]+)/); + ([, result.type, result.name] = match); + source = match.follow; + } + + { + // label + const match = matchString(source); + result.label = match.value; + source = match.follow; + } + + // value + if (result.type === 'color') { + source = normalizeColor(source); + } else if (result.type === 'select') { + const match = matchString(source); + result.select = JSON.parse(match.follow); + source = match.value; + } + + result.default = source; + + return result; + } + function _buildMeta(source) { const style = { name: null, @@ -111,52 +164,27 @@ var usercss = (function () { enabled: true, sections: [], vars: {}, - preprocessor: null + preprocessor: null, + noframes: false }; 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); - } + for (const [key, value] of parseMetas(metaSource)) { + if (!METAS.includes(key)) { + continue; } - }; - - // 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: null, // '.value' holds the value set by users. - default: value // '.default' holds the value extract from meta. - } - )) - ); + if (key === 'noframes') { + style.noframes = true; + } else if (key === 'var') { + const va = parseVar(value); + style.vars[va.name] = va; + } else if (key === 'homepageURL') { + style.url = value; + } else { + style[key] = value; + } + } return style; } @@ -212,6 +240,7 @@ var usercss = (function () { throw new Error(chrome.i18n.getMessage('styleMissingMeta', prop)); } } + // FIXME: validate variable formats } return {buildMeta, buildCode, semverTest}; From f74641e20d32388f47f7e3dce6a37d0d31259e48 Mon Sep 17 00:00:00 2001 From: eight Date: Tue, 5 Sep 2017 10:31:24 +0800 Subject: [PATCH 014/250] Add: make filterUsercss build code to get section includes --- background/storage.js | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/background/storage.js b/background/storage.js index 54ac566f..3249eb6c 100644 --- a/background/storage.js +++ b/background/storage.js @@ -274,11 +274,17 @@ function filterUsercss(req) { if (!style.id && req.id) { style.id = req.id; } - if (!style.id && req.checkDup) { - return findDupUsercss(style) - .then(dup => ({status: 'success', style, dup})); + let pending; + if (!style.sections || !style.sections.length) { + pending = usercss.buildCode(style); + } else { + pending = Promise.resolve(style); } - return {status: 'success', style}; + if (!style.id && req.checkDup) { + return Promise.all([pending, findDupUsercss(style)]) + .then(([, dup]) => ({status: 'success', style, dup})); + } + return pending.then(() => ({status: 'success', style})); }).catch(err => ({status: 'error', error: String(err)})); } From 6e52d48c6cf8b8cd35b3fdf5fb2327302bdf31a6 Mon Sep 17 00:00:00 2001 From: eight Date: Tue, 5 Sep 2017 10:32:30 +0800 Subject: [PATCH 015/250] Add: Add 'applies to' to install page --- content/install-user-css.css | 19 ++++++++++++++-- content/install-user-css.js | 43 +++++++++++++++++++++++++++++------- 2 files changed, 52 insertions(+), 10 deletions(-) diff --git a/content/install-user-css.css b/content/install-user-css.css index 5488964f..4cba66ab 100644 --- a/content/install-user-css.css +++ b/content/install-user-css.css @@ -24,8 +24,8 @@ body { margin-top: 0; } -.meta { - font-size: 1.4em; +h1 small { + font-size: 0.6em; } .warning { @@ -48,6 +48,21 @@ body { display: inline-block; } +.external { + text-align: center; +} + +.external > * { + margin: 0 7.5px; +} + +button.install { + display: block; + margin: 0 auto; + font-size: 2em; + padding: 0.4em 0.8em; +} + .code { padding: 2em; font-family: monospace; diff --git a/content/install-user-css.js b/content/install-user-css.js index 9247a358..e5ca4b5b 100644 --- a/content/install-user-css.js +++ b/content/install-user-css.js @@ -24,7 +24,7 @@ function install(style) { }); return communicate(request) .then(() => { - $$('.meta-version + .warning') + $$('.warning') .forEach(el => el.remove()); $('button.install').textContent = 'Installed'; $('button.install').disabled = true; @@ -46,6 +46,23 @@ function communicate(request) { }); } +function getAppliesTo(style) { + function *_gen() { + for (const section of style.sections) { + for (const type of ['urls', 'urlPrefixes', 'domains', 'regexps']) { + if (section[type]) { + yield *section[type]; + } + } + } + } + const result = [..._gen()]; + if (!result.length) { + result.push('All URLs'); + } + return result; +} + function initInstallPage({style, dup}) { pendingResource.then(() => { const versionTest = dup && usercss.semverTest(style.version, dup.version); @@ -54,21 +71,30 @@ function initInstallPage({style, dup}) { document.body.appendChild(tHTML(`
-

Install Usercss

-

Name

- ${style.name} -

Version

- ${style.version} +

${style.name} v${style.version}

+

${style.description}

+

Author

+ ${style.author} +

License

+ ${style.license} +

Applies to

+
    + ${getAppliesTo(style).map(s => `
  • ${s}
  • `)} +
+
`)); if (versionTest < 0) { // FIXME: i18n - $('.meta-version').after(tHTML(` + $('.actions').before(tHTML(`
The version is older then installed style.
@@ -93,7 +119,8 @@ function initErrorPage(err, source) { // FIXME: i18n document.body.appendChild(tHTML(`
- Stylus failed to parse usercss: ${err} + Stylus failed to parse usercss: +
${err}
`)); From 3f06ce8152f0d75cd9d5b974674838b950f5bcc5 Mon Sep 17 00:00:00 2001 From: eight Date: Tue, 5 Sep 2017 18:38:38 +0800 Subject: [PATCH 016/250] Fix: don't mix url and updateUrl --- content/install-user-css.js | 1 - 1 file changed, 1 deletion(-) diff --git a/content/install-user-css.js b/content/install-user-css.js index e5ca4b5b..ccabada9 100644 --- a/content/install-user-css.js +++ b/content/install-user-css.js @@ -19,7 +19,6 @@ function install(style) { const request = Object.assign(style, { method: 'saveUsercss', reason: 'install', - url: location.href, updateUrl: location.href }); return communicate(request) From f7a43d780f8424662c58cf8af53958daf521822f Mon Sep 17 00:00:00 2001 From: eight Date: Tue, 5 Sep 2017 18:39:27 +0800 Subject: [PATCH 017/250] Add: draw different type of input --- manage/manage.css | 27 ++++++++++++++++++++ manage/manage.js | 65 +++++++++++++++++++++++++++++++++++++++-------- msgbox/msgbox.css | 9 ------- 3 files changed, 81 insertions(+), 20 deletions(-) diff --git a/manage/manage.css b/manage/manage.css index 762f7c45..5b04087f 100644 --- a/manage/manage.css +++ b/manage/manage.css @@ -641,6 +641,33 @@ fieldset > *:not(legend) { text-overflow: ellipsis; } +/* config dialog */ +#message-box.config-dialog input, +#message-box.config-dialog select { + display: block; + width: 100%; + margin: .4rem 0 .6rem; + padding-left: .25rem; + border-radius: .25rem; + border-width: 1px; +} + +#message-box.config-dialog .config-checkbox::after { + content: ""; + display: block; +} + +#message-box.config-dialog .config-checkbox > * { + display: inline-block; + vertical-align: middle; + margin: .4rem 0 .6rem; +} + +#message-box.config-dialog input[type=checkbox] { + width: auto; + margin-right: 0.4em; +} + @keyframes fadein { from { opacity: 0; diff --git a/manage/manage.js b/manage/manage.js index 36d02a43..9524406e 100644 --- a/manage/manage.js +++ b/manage/manage.js @@ -286,7 +286,7 @@ Object.assign(handleEvent, { messageBox({ title: `Configure ${style.name}`, - className: 'regular-form', + className: 'config-dialog', contents: form.el, buttons: [ t('confirmSave'), @@ -320,24 +320,67 @@ Object.assign(handleEvent, { const vars = deepCopy(style.vars); for (const key of Object.keys(vars)) { const va = vars[key]; - va.input = $element({tag: 'input', type: 'text', value: va.value}); - va.input.oninput = () => { - va.dirty = true; - va.value = va.input.value; - }; - const label = $element({ + let appendChild; + if (va.type === 'color') { + va.inputColor = $element({tag: 'input', type: 'color'}); + // FIXME: i18n + va.inputAlpha = $element({tag: 'input', type: 'range', min: 0, max: 255, title: 'Opacity'}); + va.inputColor.onchange = va.inputAlpha.oninput = () => { + va.dirty = true; + va.value = va.inputColor.value + Number(va.inputAlpha.value).toString(16); + va.inputColor.style.opacity = va.inputAlpha.value / 255; + }; + appendChild = [va.label, va.inputColor, va.inputAlpha]; + } else if (va.type === 'checkbox') { + va.input = $element({tag: 'input', type: 'checkbox'}); + va.input.onchange = () => { + va.dirty = true; + va.value = String(Number(va.input.checked)); + }; + appendChild = [va.input, $element({tag: 'span', appendChild: va.label})]; + } else if (va.type === 'select') { + va.input = $element({ + tag: 'select', + appendChild: Object.keys(va.select).map(key => $element({ + tag: 'option', value: key, appendChild: va.select[key] + })) + }); + va.input.onchange = () => { + va.dirty = true; + va.value = va.input.value; + }; + appendChild = [va.label, va.input]; + } else { + va.input = $element({tag: 'input', type: 'text'}); + va.input.oninput = () => { + va.dirty = true; + va.value = va.input.value; + }; + appendChild = [va.label, va.input]; + } + labels.push($element({ tag: 'label', - appendChild: [va.label, va.input] - }); - labels.push(label); + className: `config-${va.type}`, + appendChild + })); } drawValues(); function drawValues() { for (const key of Object.keys(vars)) { const va = vars[key]; - va.input.value = va.value === null || va.value === undefined ? + const value = va.value === null || va.value === undefined ? va.default : va.value; + + if (va.type === 'color') { + va.inputColor.value = value.slice(0, -2); + va.inputAlpha.value = parseInt(value.slice(-2), 16); + va.inputColor.style.opacity = va.inputAlpha.value / 255; + } else if (va.type === 'checkbox') { + va.input.checked = Number(value); + } else { + va.input.value = value; + } } } diff --git a/msgbox/msgbox.css b/msgbox/msgbox.css index 59b87984..36c78405 100644 --- a/msgbox/msgbox.css +++ b/msgbox/msgbox.css @@ -136,12 +136,3 @@ opacity: 0; } } - -#message-box.regular-form input[type=text] { - display: block; - width: 100%; - margin: .4rem 0 .6rem; - padding-left: .25rem; - border-radius: .25rem; - border-width: 1px; -} From 1f4489847569aeaf9e7ba4c045b253a5754d6c4b Mon Sep 17 00:00:00 2001 From: eight Date: Wed, 6 Sep 2017 03:08:03 +0800 Subject: [PATCH 018/250] Add: adopt node-semver --- background/update.js | 7 ++++--- content/install-user-css.js | 9 +++++---- js/usercss.js | 26 +------------------------- manifest.json | 1 + vendor/node-semver/README.md | 1 + vendor/node-semver/semver.js | 1 + 6 files changed, 13 insertions(+), 32 deletions(-) create mode 100644 vendor/node-semver/README.md create mode 100644 vendor/node-semver/semver.js diff --git a/background/update.js b/background/update.js index 74a99bf2..fcd238e6 100644 --- a/background/update.js +++ b/background/update.js @@ -1,5 +1,6 @@ /* global getStyles, saveStyle, styleSectionsEqual, chromeLocal */ -/* global calcStyleDigest, usercss */ +/* global calcStyleDigest */ +/* global usercss semverCompare */ 'use strict'; // eslint-disable-next-line no-var @@ -100,10 +101,10 @@ var updater = { return Promise.reject(updater.ERROR_VERSION); } if (style.version) { - if (usercss.semverTest(style.version, json.version) === 0) { + if (semverCompare(style.version, json.version) === 0) { return Promise.reject(updater.SAME_VERSION); } - if (usercss.semverTest(style.version, json.version) > 0) { + if (semverCompare(style.version, json.version) > 0) { return Promise.reject(updater.ERROR_VERSION); } } diff --git a/content/install-user-css.js b/content/install-user-css.js index ccabada9..2cd17e95 100644 --- a/content/install-user-css.js +++ b/content/install-user-css.js @@ -1,4 +1,4 @@ -/* global usercss */ +/* global semverCompare */ 'use strict'; @@ -63,8 +63,8 @@ function getAppliesTo(style) { } function initInstallPage({style, dup}) { - pendingResource.then(() => { - const versionTest = dup && usercss.semverTest(style.version, dup.version); + return pendingResource.then(() => { + const versionTest = dup && semverCompare(style.version, dup.version); document.body.innerHTML = ''; // FIXME: i18n document.body.appendChild(tHTML(` @@ -113,7 +113,7 @@ function initInstallPage({style, dup}) { } function initErrorPage(err, source) { - pendingResource.then(() => { + return pendingResource.then(() => { document.body.innerHTML = ''; // FIXME: i18n document.body.appendChild(tHTML(` @@ -135,6 +135,7 @@ function initUsercssInstall() { '/js/dom.js', '/js/localization.js', '/js/usercss.js', + '/vendor/node-semver/semver.js', '/content/install-user-css.css' ] }); diff --git a/js/usercss.js b/js/usercss.js index 7ff85ad2..551fcd39 100644 --- a/js/usercss.js +++ b/js/usercss.js @@ -9,30 +9,6 @@ var usercss = (function () { 'namespace', 'noframes', 'preprocessor', 'supportURL', 'var', 'version' ]; - // FIXME: use a real semver module - 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; - } - const BUILDER = { default: { postprocess(sections, vars) { @@ -243,5 +219,5 @@ var usercss = (function () { // FIXME: validate variable formats } - return {buildMeta, buildCode, semverTest}; + return {buildMeta, buildCode}; })(); diff --git a/manifest.json b/manifest.json index 6fd8bd66..500e251b 100644 --- a/manifest.json +++ b/manifest.json @@ -27,6 +27,7 @@ "js/prefs.js", "js/script-loader.js", "background/background.js", + "vendor/node-semver/semver.js", "background/update.js" ] }, diff --git a/vendor/node-semver/README.md b/vendor/node-semver/README.md new file mode 100644 index 00000000..7810bff9 --- /dev/null +++ b/vendor/node-semver/README.md @@ -0,0 +1 @@ +See https://github.com/eight04/node-semver-bundle. \ No newline at end of file diff --git a/vendor/node-semver/semver.js b/vendor/node-semver/semver.js new file mode 100644 index 00000000..f7aec67a --- /dev/null +++ b/vendor/node-semver/semver.js @@ -0,0 +1 @@ +var semverCompare=function(){"use strict";function r(e,t){if(e instanceof r){if(e.loose===t)return e;e=e.version}else if("string"!=typeof e)throw new TypeError("Invalid Version: "+e);if(e.length>k)throw new TypeError("version is longer than "+k+" characters");if(!(this instanceof r))return new r(e,t);this.loose=t;var n=e.trim().match(t?I[L]:I[_]);if(!n)throw new TypeError("Invalid Version: "+e);if(this.raw=e,this.major=+n[1],this.minor=+n[2],this.patch=+n[3],this.major>T||this.major<0)throw new TypeError("Invalid major version");if(this.minor>T||this.minor<0)throw new TypeError("Invalid minor version");if(this.patch>T||this.patch<0)throw new TypeError("Invalid patch version");n[4]?this.prerelease=n[4].split(".").map(function(r){if(/^[0-9]+$/.test(r)){var e=+r;if(e>=0&&ee?1:0}function t(e,t,n){return new r(e,n).compare(new r(t,n))}function n(r,e,n){return t(r,e,n)>0}function i(r,e,n){return t(r,e,n)<0}function s(r,e,n){return 0===t(r,e,n)}function o(r,e,n){return 0!==t(r,e,n)}function a(r,e,n){return t(r,e,n)>=0}function h(r,e,n){return t(r,e,n)<=0}function p(r,e,t,p){var u;switch(e){case"===":"object"==typeof r&&(r=r.version),"object"==typeof t&&(t=t.version),u=r===t;break;case"!==":"object"==typeof r&&(r=r.version),"object"==typeof t&&(t=t.version),u=r!==t;break;case"":case"=":case"==":u=s(r,t,p);break;case"!=":u=o(r,t,p);break;case">":u=n(r,t,p);break;case">=":u=a(r,t,p);break;case"<":u=i(r,t,p);break;case"<=":u=h(r,t,p);break;default:throw new TypeError("Invalid operator: "+e)}return u}function u(r,e){if(r instanceof u){if(r.loose===e)return r;r=r.value}if(!(this instanceof u))return new u(r,e);this.loose=e,this.parse(r),this.semver===vr?this.value="":this.value=this.operator+this.semver.version}function c(r,e){if(r instanceof c)return r.loose===e?r:new c(r.raw,e);if(r instanceof u)return new c(r.value,e);if(!(this instanceof c))return new c(r,e);if(this.loose=e,this.raw=r,this.set=r.split(/\s*\|\|\s*/).map(function(r){return this.parseRange(r.trim())},this).filter(function(r){return r.length}),!this.set.length)throw new TypeError("Invalid SemVer Range: "+r);this.format()}function f(r,e){return r=w(r,e),r=l(r,e),r=y(r,e),r=b(r,e)}function v(r){return!r||"x"===r.toLowerCase()||"*"===r}function l(r,e){return r.trim().split(/\s+/).map(function(r){return m(r,e)}).join(" ")}function m(r,e){var t=e?I[rr]:I[Y];return r.replace(t,function(r,e,t,n,i){var s;return v(e)?s="":v(t)?s=">="+e+".0.0 <"+(+e+1)+".0.0":v(n)?s=">="+e+"."+t+".0 <"+e+"."+(+t+1)+".0":i?("-"!==i.charAt(0)&&(i="-"+i),s=">="+e+"."+t+"."+n+i+" <"+e+"."+(+t+1)+".0"):s=">="+e+"."+t+"."+n+" <"+e+"."+(+t+1)+".0",s})}function w(r,e){return r.trim().split(/\s+/).map(function(r){return g(r,e)}).join(" ")}function g(r,e){var t=e?I[ir]:I[nr];return r.replace(t,function(r,e,t,n,i){var s;return v(e)?s="":v(t)?s=">="+e+".0.0 <"+(+e+1)+".0.0":v(n)?s="0"===e?">="+e+"."+t+".0 <"+e+"."+(+t+1)+".0":">="+e+"."+t+".0 <"+(+e+1)+".0.0":i?("-"!==i.charAt(0)&&(i="-"+i),s="0"===e?"0"===t?">="+e+"."+t+"."+n+i+" <"+e+"."+t+"."+(+n+1):">="+e+"."+t+"."+n+i+" <"+e+"."+(+t+1)+".0":">="+e+"."+t+"."+n+i+" <"+(+e+1)+".0.0"):s="0"===e?"0"===t?">="+e+"."+t+"."+n+" <"+e+"."+t+"."+(+n+1):">="+e+"."+t+"."+n+" <"+e+"."+(+t+1)+".0":">="+e+"."+t+"."+n+" <"+(+e+1)+".0.0",s})}function y(r,e){return r.split(/\s+/).map(function(r){return j(r,e)}).join(" ")}function j(r,e){r=r.trim();var t=e?I[Q]:I[O];return r.replace(t,function(r,e,t,n,i,s){var o=v(t),a=o||v(n),h=a||v(i),p=h;return"="===e&&p&&(e=""),o?r=">"===e||"<"===e?"<0.0.0":"*":e&&p?(a&&(n=0),h&&(i=0),">"===e?(e=">=",a?(t=+t+1,n=0,i=0):h&&(n=+n+1,i=0)):"<="===e&&(e="<",a?t=+t+1:n=+n+1),r=e+t+"."+n+"."+i):a?r=">="+t+".0.0 <"+(+t+1)+".0.0":h&&(r=">="+t+"."+n+".0 <"+t+"."+(+n+1)+".0"),r})}function b(r,e){return r.trim().replace(I[ur],"")}function d(r,e,t,n,i,s,o,a,h,p,u,c,f){return e=v(t)?"":v(n)?">="+t+".0.0":v(i)?">="+t+"."+n+".0":">="+e,a=v(h)?"":v(p)?"<"+(+h+1)+".0.0":v(u)?"<"+h+"."+(+p+1)+".0":c?"<="+h+"."+p+"."+u+"-"+c:"<="+a,(e+" "+a).trim()}function $(r,e){for(t=0;t0){var n=r[t].semver;if(n.major===e.major&&n.minor===e.minor&&n.patch===e.patch)return!0}return!1}return!0}function E(r,e,t){try{e=new c(e,t)}catch(r){return!1}return e.test(r)}var k=256,T=Number.MAX_SAFE_INTEGER||9007199254740991,I=[],R=[],x=0,A=x++;R[A]="0|[1-9]\\d*";var S=x++;R[S]="[0-9]+";var N=x++;R[N]="\\d*[a-zA-Z-][a-zA-Z0-9-]*";var z=x++;R[z]="("+R[A]+")\\.("+R[A]+")\\.("+R[A]+")";var C=x++;R[C]="("+R[S]+")\\.("+R[S]+")\\.("+R[S]+")";var M=x++;R[M]="(?:"+R[A]+"|"+R[N]+")";var V=x++;R[V]="(?:"+R[S]+"|"+R[N]+")";var X=x++;R[X]="(?:-("+R[M]+"(?:\\."+R[M]+")*))";var Z=x++;R[Z]="(?:-?("+R[V]+"(?:\\."+R[V]+")*))";var q=x++;R[q]="[0-9A-Za-z-]+";var P=x++;R[P]="(?:\\+("+R[q]+"(?:\\."+R[q]+")*))";var _=x++,F="v?"+R[z]+R[X]+"?"+R[P]+"?";R[_]="^"+F+"$";var G="[v=\\s]*"+R[C]+R[Z]+"?"+R[P]+"?",L=x++;R[L]="^"+G+"$";var B=x++;R[B]="((?:<|>)?=?)";var D=x++;R[D]=R[S]+"|x|X|\\*";var H=x++;R[H]=R[A]+"|x|X|\\*";var J=x++;R[J]="[v=\\s]*("+R[H]+")(?:\\.("+R[H]+")(?:\\.("+R[H]+")(?:"+R[X]+")?"+R[P]+"?)?)?";var K=x++;R[K]="[v=\\s]*("+R[D]+")(?:\\.("+R[D]+")(?:\\.("+R[D]+")(?:"+R[Z]+")?"+R[P]+"?)?)?";var O=x++;R[O]="^"+R[B]+"\\s*"+R[J]+"$";var Q=x++;R[Q]="^"+R[B]+"\\s*"+R[K]+"$";var U=x++;R[U]="(?:~>?)";var W=x++;R[W]="(\\s*)"+R[U]+"\\s+",I[W]=new RegExp(R[W],"g");var Y=x++;R[Y]="^"+R[U]+R[J]+"$";var rr=x++;R[rr]="^"+R[U]+R[K]+"$";var er=x++;R[er]="(?:\\^)";var tr=x++;R[tr]="(\\s*)"+R[er]+"\\s+",I[tr]=new RegExp(R[tr],"g");var nr=x++;R[nr]="^"+R[er]+R[J]+"$";var ir=x++;R[ir]="^"+R[er]+R[K]+"$";var sr=x++;R[sr]="^"+R[B]+"\\s*("+G+")$|^$";var or=x++;R[or]="^"+R[B]+"\\s*("+F+")$|^$";var ar=x++;R[ar]="(\\s*)"+R[B]+"\\s*("+G+"|"+R[J]+")",I[ar]=new RegExp(R[ar],"g");var hr=x++;R[hr]="^\\s*("+R[J]+")\\s+-\\s+("+R[J]+")\\s*$";var pr=x++;R[pr]="^\\s*("+R[K]+")\\s+-\\s+("+R[K]+")\\s*$";var ur=x++;R[ur]="(<|>)?=?\\s*\\*";for(var cr=0;cr=0;)"number"==typeof this.prerelease[t]&&(this.prerelease[t]++,t=-2);-1===t&&this.prerelease.push(0)}e&&(this.prerelease[0]===e?isNaN(this.prerelease[1])&&(this.prerelease=[e,0]):this.prerelease=[e,0]);break;default:throw new Error("invalid increment argument: "+r)}return this.format(),this.raw=this.version,this};var fr=/^[0-9]+$/,vr={};return u.prototype.parse=function(e){var t=this.loose?I[sr]:I[or],n=e.match(t);if(!n)throw new TypeError("Invalid comparator: "+e);this.operator=n[1],"="===this.operator&&(this.operator=""),n[2]?this.semver=new r(n[2],this.loose):this.semver=vr},u.prototype.toString=function(){return this.value},u.prototype.test=function(e){return this.semver===vr||("string"==typeof e&&(e=new r(e,this.loose)),p(e,this.operator,this.semver,this.loose))},u.prototype.intersects=function(r,e){if(!(r instanceof u))throw new TypeError("a Comparator is required");var t;if(""===this.operator)return t=new c(r.value,e),E(this.value,t,e);if(""===r.operator)return t=new c(this.value,e),E(r.semver,t,e);var n=!(">="!==this.operator&&">"!==this.operator||">="!==r.operator&&">"!==r.operator),i=!("<="!==this.operator&&"<"!==this.operator||"<="!==r.operator&&"<"!==r.operator),s=this.semver.version===r.semver.version,o=!(">="!==this.operator&&"<="!==this.operator||">="!==r.operator&&"<="!==r.operator),a=p(this.semver,"<",r.semver,e)&&(">="===this.operator||">"===this.operator)&&("<="===r.operator||"<"===r.operator),h=p(this.semver,">",r.semver,e)&&("<="===this.operator||"<"===this.operator)&&(">="===r.operator||">"===r.operator);return n||i||s&&o||a||h},c.prototype.format=function(){return this.range=this.set.map(function(r){return r.join(" ").trim()}).join("||").trim(),this.range},c.prototype.toString=function(){return this.range},c.prototype.parseRange=function(r){var e=this.loose;r=r.trim();var t=e?I[pr]:I[hr];r=(r=(r=(r=(r=r.replace(t,d)).replace(I[ar],"$1$2$3")).replace(I[W],"$1~")).replace(I[tr],"$1^")).split(/\s+/).join(" ");var n=e?I[sr]:I[or],i=r.split(" ").map(function(r){return f(r,e)}).join(" ").split(/\s+/);return this.loose&&(i=i.filter(function(r){return!!r.match(n)})),i=i.map(function(r){return new u(r,e)})},c.prototype.intersects=function(r,e){if(!(r instanceof c))throw new TypeError("a Range is required");return this.set.some(function(t){return t.every(function(t){return r.set.some(function(r){return r.every(function(r){return t.intersects(r,e)})})})})},c.prototype.test=function(e){if(!e)return!1;"string"==typeof e&&(e=new r(e,this.loose));for(var t=0;t Date: Wed, 6 Sep 2017 04:26:01 +0800 Subject: [PATCH 019/250] Add: colorParser --- js/usercss.js | 45 ++++++++++++++- manage.html | 2 + manage/config-dialog.js | 121 ++++++++++++++++++++++++++++++++++++++ manage/manage.js | 125 +++------------------------------------- 4 files changed, 176 insertions(+), 117 deletions(-) create mode 100644 manage/config-dialog.js diff --git a/js/usercss.js b/js/usercss.js index 551fcd39..7e2f41ad 100644 --- a/js/usercss.js +++ b/js/usercss.js @@ -46,6 +46,49 @@ var usercss = (function () { } }; + const colorParser = (function () { + const el = document.createElement('div'); + // https://bugs.webkit.org/show_bug.cgi?id=14563 + document.head.appendChild(el); + + function _parse(color) { + const [r, g, b, a = 1] = color.match(/[.\d]+/g).map(Number); + return {r, g, b, a}; + } + + function parse(color) { + el.style.color = color; + if (el.style.color === '') { + throw new Error(`"${color}" is not a valid color`); + } + color = getComputedStyle(el).color; + el.style.color = ''; + return _parse(color); + } + + function format({r, g, b, a = 1}) { + return `rgba(${r}, ${g}, ${b}, ${a})`; + } + + function pad(s) { + if (s.padStart) { + // chrome 57+ + return s.padStart(2, '0'); + } + return `00${s}`.slice(-2); + } + + function formatHex({r, g, b, a = null}) { + const values = [r, g, b]; + if (a !== null) { + values.push(Math.floor(a * 255)); + } + return '#' + values.map(n => pad(n.toString(16))).join(''); + } + + return {parse, format, formatHex}; + })(); + function getMetaSource(source) { const commentRe = /\/\*[\s\S]*?\*\//g; const metaRe = /==userstyle==[\s\S]*?==\/userstyle==/i; @@ -219,5 +262,5 @@ var usercss = (function () { // FIXME: validate variable formats } - return {buildMeta, buildCode}; + return {buildMeta, buildCode, colorParser}; })(); diff --git a/manage.html b/manage.html index 9f7752ca..308c954f 100644 --- a/manage.html +++ b/manage.html @@ -139,6 +139,8 @@ + + diff --git a/manage/config-dialog.js b/manage/config-dialog.js new file mode 100644 index 00000000..6c7d9905 --- /dev/null +++ b/manage/config-dialog.js @@ -0,0 +1,121 @@ +/* global usercss messageBox */ + +'use strict'; + +function configDialog(style) { + const {colorParser} = usercss; + const form = buildConfigForm(); + + return messageBox({ + title: `Configure ${style.name}`, + className: 'config-dialog', + contents: form.el, + buttons: [ + t('confirmSave'), + { + textContent: t('confirmDefault'), + onclick: form.useDefault + }, + t('confirmCancel') + ] + }).then(result => { + if (result.button !== 0 && !result.enter) { + return; + } + return form.getVars(); + }); + + function buildConfigForm() { + const labels = []; + const vars = deepCopy(style.vars); + for (const key of Object.keys(vars)) { + const va = vars[key]; + let appendChild; + if (va.type === 'color') { + va.inputColor = $element({tag: 'input', type: 'color'}); + // FIXME: i18n + va.inputAlpha = $element({tag: 'input', type: 'range', min: 0, max: 1, title: 'Opacity', step: 'any'}); + va.inputColor.onchange = va.inputAlpha.oninput = () => { + va.dirty = true; + const color = colorParser.parse(va.inputColor.value); + color.a = Number(va.inputAlpha.value); + va.value = colorParser.format(color); + va.inputColor.style.opacity = color.a; + }; + appendChild = [va.label, va.inputColor, va.inputAlpha]; + } else if (va.type === 'checkbox') { + va.input = $element({tag: 'input', type: 'checkbox'}); + va.input.onchange = () => { + va.dirty = true; + va.value = String(Number(va.input.checked)); + }; + appendChild = [va.input, $element({tag: 'span', appendChild: va.label})]; + } else if (va.type === 'select') { + va.input = $element({ + tag: 'select', + appendChild: Object.keys(va.select).map(key => $element({ + tag: 'option', value: key, appendChild: va.select[key] + })) + }); + va.input.onchange = () => { + va.dirty = true; + va.value = va.input.value; + }; + appendChild = [va.label, va.input]; + } else { + va.input = $element({tag: 'input', type: 'text'}); + va.input.oninput = () => { + va.dirty = true; + va.value = va.input.value; + }; + appendChild = [va.label, va.input]; + } + labels.push($element({ + tag: 'label', + className: `config-${va.type}`, + appendChild + })); + } + drawValues(); + + function drawValues() { + for (const key of Object.keys(vars)) { + const va = vars[key]; + const value = va.value === null || va.value === undefined ? + va.default : va.value; + + if (va.type === 'color') { + const color = colorParser.parse(value); + va.inputAlpha.value = color.a; + va.inputColor.style.opacity = color.a; + delete color.a; + va.inputColor.value = colorParser.formatHex(color); + } else if (va.type === 'checkbox') { + va.input.checked = Number(value); + } else { + va.input.value = value; + } + } + } + + function useDefault() { + for (const key of Object.keys(vars)) { + const va = vars[key]; + va.dirty = va.value !== null && va.value !== undefined && + va.value !== va.default; + va.value = null; + } + drawValues(); + } + + function getVars() { + return vars; + } + + return { + el: labels, + useDefault, + getVars + }; + } +} diff --git a/manage/manage.js b/manage/manage.js index 9524406e..4e8d447a 100644 --- a/manage/manage.js +++ b/manage/manage.js @@ -2,6 +2,7 @@ /* global filtersSelector, filterAndAppend */ /* global checkUpdate, handleUpdateInstalled */ /* global objectDiff */ +/* global configDialog */ 'use strict'; let installed; @@ -282,128 +283,20 @@ Object.assign(handleEvent, { }, config(event, {styleMeta: style}) { - const form = buildConfigForm(); - - messageBox({ - title: `Configure ${style.name}`, - className: 'config-dialog', - contents: form.el, - buttons: [ - t('confirmSave'), - { - textContent: t('confirmDefault'), - onclick: form.useDefault - }, - t('confirmCancel') - ] - }).then(result => { - if (result.button !== 0 && !result.enter) { + configDialog(style).then(vars => { + if (!vars) { + return; + } + const keys = Object.keys(vars).filter(k => vars[k].dirty); + if (!keys.length) { return; } style.reason = 'config'; - const vars = form.getVars(); - let dirty = false; - for (const key of Object.keys(vars)) { - if (vars[key].dirty) { - dirty = true; - style.vars[key].value = vars[key].value; - } - } - if (!dirty) { - return; + for (const key of keys) { + style.vars[key].value = vars[key].value; } saveStyleSafe(style); }); - - function buildConfigForm() { - const labels = []; - const vars = deepCopy(style.vars); - for (const key of Object.keys(vars)) { - const va = vars[key]; - let appendChild; - if (va.type === 'color') { - va.inputColor = $element({tag: 'input', type: 'color'}); - // FIXME: i18n - va.inputAlpha = $element({tag: 'input', type: 'range', min: 0, max: 255, title: 'Opacity'}); - va.inputColor.onchange = va.inputAlpha.oninput = () => { - va.dirty = true; - va.value = va.inputColor.value + Number(va.inputAlpha.value).toString(16); - va.inputColor.style.opacity = va.inputAlpha.value / 255; - }; - appendChild = [va.label, va.inputColor, va.inputAlpha]; - } else if (va.type === 'checkbox') { - va.input = $element({tag: 'input', type: 'checkbox'}); - va.input.onchange = () => { - va.dirty = true; - va.value = String(Number(va.input.checked)); - }; - appendChild = [va.input, $element({tag: 'span', appendChild: va.label})]; - } else if (va.type === 'select') { - va.input = $element({ - tag: 'select', - appendChild: Object.keys(va.select).map(key => $element({ - tag: 'option', value: key, appendChild: va.select[key] - })) - }); - va.input.onchange = () => { - va.dirty = true; - va.value = va.input.value; - }; - appendChild = [va.label, va.input]; - } else { - va.input = $element({tag: 'input', type: 'text'}); - va.input.oninput = () => { - va.dirty = true; - va.value = va.input.value; - }; - appendChild = [va.label, va.input]; - } - labels.push($element({ - tag: 'label', - className: `config-${va.type}`, - appendChild - })); - } - drawValues(); - - function drawValues() { - for (const key of Object.keys(vars)) { - const va = vars[key]; - const value = va.value === null || va.value === undefined ? - va.default : va.value; - - if (va.type === 'color') { - va.inputColor.value = value.slice(0, -2); - va.inputAlpha.value = parseInt(value.slice(-2), 16); - va.inputColor.style.opacity = va.inputAlpha.value / 255; - } else if (va.type === 'checkbox') { - va.input.checked = Number(value); - } else { - va.input.value = value; - } - } - } - - function useDefault() { - for (const key of Object.keys(vars)) { - const va = vars[key]; - va.dirty = va.value !== null && va.value !== undefined && - va.value !== va.default; - va.value = null; - } - drawValues(); - } - - function getVars() { - return vars; - } - - return { - el: labels, - useDefault, - getVars - }; - } }, entryClicked(event) { From bf455752ec8f3828625470c5597bceb52e06e830 Mon Sep 17 00:00:00 2001 From: eight Date: Wed, 6 Sep 2017 05:33:08 +0800 Subject: [PATCH 020/250] Use options dialog style --- manage.html | 1 + manage/config-dialog.js | 18 +++++++++---- manage/manage.css | 56 +++++++++++++++++++++++++------------- options.html | 1 + options/onoffswitch.css | 59 ++++++++++++++++++++++++++++++++++++++++ options/options.css | 60 ----------------------------------------- 6 files changed, 111 insertions(+), 84 deletions(-) create mode 100644 options/onoffswitch.css diff --git a/manage.html b/manage.html index 308c954f..6db1a624 100644 --- a/manage.html +++ b/manage.html @@ -5,6 +5,7 @@ + diff --git a/manage/config-dialog.js b/manage/config-dialog.js index 6c7d9905..6386629c 100644 --- a/manage/config-dialog.js +++ b/manage/config-dialog.js @@ -7,7 +7,7 @@ function configDialog(style) { const form = buildConfigForm(); return messageBox({ - title: `Configure ${style.name}`, + title: style.name, className: 'config-dialog', contents: form.el, buttons: [ @@ -42,14 +42,21 @@ function configDialog(style) { va.value = colorParser.format(color); va.inputColor.style.opacity = color.a; }; - appendChild = [va.label, va.inputColor, va.inputAlpha]; + appendChild = [ + $element({appendChild: [va.inputColor, va.inputAlpha]}) + ]; } else if (va.type === 'checkbox') { va.input = $element({tag: 'input', type: 'checkbox'}); va.input.onchange = () => { va.dirty = true; va.value = String(Number(va.input.checked)); }; - appendChild = [va.input, $element({tag: 'span', appendChild: va.label})]; + appendChild = [ + $element({tag: 'span', className: 'onoffswitch', appendChild: [ + va.input, + $element({tag: 'span'}) + ]}) + ]; } else if (va.type === 'select') { va.input = $element({ tag: 'select', @@ -61,15 +68,16 @@ function configDialog(style) { va.dirty = true; va.value = va.input.value; }; - appendChild = [va.label, va.input]; + appendChild = [va.input]; } else { va.input = $element({tag: 'input', type: 'text'}); va.input.oninput = () => { va.dirty = true; va.value = va.input.value; }; - appendChild = [va.label, va.input]; + appendChild = [va.input]; } + appendChild.unshift($element({tag: 'span', appendChild: va.label})); labels.push($element({ tag: 'label', className: `config-${va.type}`, diff --git a/manage/manage.css b/manage/manage.css index 5b04087f..02f4495f 100644 --- a/manage/manage.css +++ b/manage/manage.css @@ -642,30 +642,48 @@ fieldset > *:not(legend) { } /* config dialog */ -#message-box.config-dialog input, -#message-box.config-dialog select { - display: block; +.config-dialog label { + display: flex; + padding: .75em 0; + align-items: center; + border-top: 1px dotted #ccc; +} + +.config-dialog label:first-child { + padding-top: 0; + border-top: none; +} + +.config-dialog label > :first-child { + margin-right: 8px; + flex-grow: 1; +} + +.config-dialog label:not([disabled]) > :first-child { + cursor: default; +} + +.config-dialog label:not([disabled]):hover > :first-child { + text-shadow: 0 0 0.01px rgba(0, 0, 0, .25); + cursor: pointer; +} + +.config-dialog input, +.config-dialog select { width: 100%; - margin: .4rem 0 .6rem; - padding-left: .25rem; - border-radius: .25rem; - border-width: 1px; + margin: 0; + height: 2em; + box-sizing: border-box; } -#message-box.config-dialog .config-checkbox::after { - content: ""; - display: block; +.config-dialog input[type="text"] { + padding-left: 0.25em; } -#message-box.config-dialog .config-checkbox > * { - display: inline-block; - vertical-align: middle; - margin: .4rem 0 .6rem; -} - -#message-box.config-dialog input[type=checkbox] { - width: auto; - margin-right: 0.4em; +.config-dialog label > :last-child { + width: 60px; + box-sizing: border-box; + flex-shrink: 0; } @keyframes fadein { diff --git a/options.html b/options.html index a32e4663..d2e078ad 100644 --- a/options.html +++ b/options.html @@ -3,6 +3,7 @@ Stylus + diff --git a/options/onoffswitch.css b/options/onoffswitch.css new file mode 100644 index 00000000..72630602 --- /dev/null +++ b/options/onoffswitch.css @@ -0,0 +1,59 @@ +/* On/Off FlipSwitch https://proto.io/freebies/onoff/ */ + +.onoffswitch { + position: relative; + margin: 1ex 0; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; +} + +.onoffswitch input { + display: none; +} + +.onoffswitch span { + display: block; + overflow: hidden; + cursor: pointer; + height: 12px; + padding: 0; + line-height: 12px; + border: 0 solid #E3E3E3; + border-radius: 12px; + background-color: #E0E0E0; + box-shadow: inset 2px 2px 4px rgba(0,0,0,0.1); +} + +.onoffswitch span::before { + content: ""; + display: block; + width: 18px; + height: 18px; + margin: -3px; + background: #efefef; + position: absolute; + top: 0; + bottom: 0; + right: 46px; + border-radius: 18px; + box-shadow: 0 3px 13px 0 rgba(0, 0, 0, 0.4); +} + +.onoffswitch input:checked + span { + background-color: #CAEBE3; +} + +.onoffswitch input:checked + span, .onoffswitch input:checked + span::before { + border-color: #CAEBE3; +} + +.onoffswitch input:checked + span .onoffswitch-inner { + margin-left: 0; +} + +.onoffswitch input:checked + span::before { + right: 0; + background-color: #04BA9F; + box-shadow: 3px 6px 18px 0 rgba(0, 0, 0, 0.2); +} diff --git a/options/options.css b/options/options.css index 9ccd6a7d..3d59def4 100644 --- a/options/options.css +++ b/options/options.css @@ -212,63 +212,3 @@ sup { 25% { opacity: 1 } 100% { opacity: 0 } } - -/* On/Off FlipSwitch https://proto.io/freebies/onoff/ */ - -.onoffswitch { - position: relative; - margin: 1ex 0; - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; -} - -.onoffswitch input { - display: none; -} - -.onoffswitch span { - display: block; - overflow: hidden; - cursor: pointer; - height: 12px; - padding: 0; - line-height: 12px; - border: 0 solid #E3E3E3; - border-radius: 12px; - background-color: #E0E0E0; - box-shadow: inset 2px 2px 4px rgba(0,0,0,0.1); -} - -.onoffswitch span::before { - content: ""; - display: block; - width: 18px; - height: 18px; - margin: -3px; - background: #efefef; - position: absolute; - top: 0; - bottom: 0; - right: 46px; - border-radius: 18px; - box-shadow: 0 3px 13px 0 rgba(0, 0, 0, 0.4); -} - -.onoffswitch input:checked + span { - background-color: #CAEBE3; -} - -.onoffswitch input:checked + span, .onoffswitch input:checked + span::before { - border-color: #CAEBE3; -} - -.onoffswitch input:checked + span .onoffswitch-inner { - margin-left: 0; -} - -.onoffswitch input:checked + span::before { - right: 0; - background-color: #04BA9F; - box-shadow: 3px 6px 18px 0 rgba(0, 0, 0, 0.2); -} From 3daff40acfcb3cf07ea979dbe9e689256b9b764f Mon Sep 17 00:00:00 2001 From: eight Date: Sat, 9 Sep 2017 19:29:35 +0800 Subject: [PATCH 021/250] Add: vars validation --- background/storage.js | 6 +----- js/usercss.js | 48 ++++++++++++++++++++++++++++++++----------- 2 files changed, 37 insertions(+), 17 deletions(-) diff --git a/background/storage.js b/background/storage.js index 3249eb6c..14b49673 100644 --- a/background/storage.js +++ b/background/storage.js @@ -346,11 +346,7 @@ function saveStyle(style) { 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; - } - } + usercss.assignVars(style, dup); }) .then(() => usercss.buildCode(style)); } diff --git a/js/usercss.js b/js/usercss.js index 7e2f41ad..7c5835b0 100644 --- a/js/usercss.js +++ b/js/usercss.js @@ -1,4 +1,4 @@ -/* global loadScript mozParser */ +/* global loadScript mozParser semverCompare */ 'use strict'; @@ -132,11 +132,6 @@ var usercss = (function () { return match; } - // FIXME: need color converter - function normalizeColor(color) { - return color; - } - function parseVar(source) { const result = { label: null, @@ -160,10 +155,8 @@ var usercss = (function () { source = match.follow; } - // value - if (result.type === 'color') { - source = normalizeColor(source); - } else if (result.type === 'select') { + // select type has an additional field + if (result.type === 'select') { const match = matchString(source); result.select = JSON.parse(match.follow); source = match.value; @@ -259,8 +252,39 @@ var usercss = (function () { throw new Error(chrome.i18n.getMessage('styleMissingMeta', prop)); } } - // FIXME: validate variable formats + // validate version + semverCompare(style.version, '0.0.0'); + + // validate vars + for (const key of Object.keys(style.vars)) { + validVar(style.vars[key]); + } } - return {buildMeta, buildCode, colorParser}; + function validVar(va, value = 'default') { + // FIXME: i18n + if (va.type === 'select' && !va.select[va[value]]) { + throw new Error(`Invalid @var select: missing key '${va[value]}'`); + } else if (va.type === 'checkbox' && !/^[01]$/.test(va[value])) { + throw new Error('Invalid @var checkbox: value must be 0 or 1'); + } else if (va.type === 'color') { + va[value] = colorParser.format(colorParser.parse(va[value])); + } + } + + function assignVars(style, old) { + // The type of var might be changed during the update. Set value to null if the value is invalid. + for (const key of Object.keys(style.vars)) { + if (old.vars[key] && old.vars[key].value) { + style.vars[key].value = old.vars[key].value; + try { + validVar(style.vars[key], 'value'); + } catch (err) { + style.vars[key].value = null; + } + } + } + } + + return {buildMeta, buildCode, colorParser, assignVars}; })(); From cb23f89b6aaff4ff761f757a095d4b8432eaafe3 Mon Sep 17 00:00:00 2001 From: eight Date: Sun, 10 Sep 2017 22:04:43 +0800 Subject: [PATCH 022/250] Add: allow saveUsercss to build style --- background/storage.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/background/storage.js b/background/storage.js index 14b49673..674a705b 100644 --- a/background/storage.js +++ b/background/storage.js @@ -290,7 +290,14 @@ function filterUsercss(req) { function saveUsercss(style) { // This function use `saveStyle`, however the response is different. - return saveStyle(style) + return Promise.resolve() + .then(() => { + if (!style.name || !style.namespace) { + return Object.assign(usercss.buildMeta(style.source), style); + } + return style; + }) + .then(saveStyle) .then(result => ({ status: 'success', style: result From 8c374db353496e664495514fb399aacdc35c4095 Mon Sep 17 00:00:00 2001 From: eight Date: Sun, 10 Sep 2017 22:05:44 +0800 Subject: [PATCH 023/250] Add: live-reload --- content/install-user-css.css | 15 +++- content/install-user-css.js | 153 +++++++++++++++++++++++++++++------ 2 files changed, 142 insertions(+), 26 deletions(-) diff --git a/content/install-user-css.css b/content/install-user-css.css index 4cba66ab..8d8c030d 100644 --- a/content/install-user-css.css +++ b/content/install-user-css.css @@ -44,8 +44,15 @@ h1 small { margin: 15px 0; } -.actions > * { - display: inline-block; +.live-reload { + width: fit-content; + display: flex; + align-items: center; + margin: 0.5em auto; +} + +.live-reload input { + margin: 0 0.5em 0 0; } .external { @@ -68,3 +75,7 @@ button.install { font-family: monospace; white-space: pre-wrap; } + +.main { + flex-grow: 1; +} diff --git a/content/install-user-css.js b/content/install-user-css.js index 2cd17e95..8dce7539 100644 --- a/content/install-user-css.js +++ b/content/install-user-css.js @@ -4,17 +4,6 @@ let pendingResource; -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', @@ -22,11 +11,12 @@ function install(style) { updateUrl: location.href }); return communicate(request) - .then(() => { + .then(result => { $$('.warning') .forEach(el => el.remove()); $('button.install').textContent = 'Installed'; $('button.install').disabled = true; + window.dispatchEvent(new CustomEvent('installed', {detail: result})); }) .catch(err => { alert(chrome.i18n.getMessage('styleInstallFailed', String(err))); @@ -62,7 +52,7 @@ function getAppliesTo(style) { return result; } -function initInstallPage({style, dup}) { +function initInstallPage({style, dup}, sourceLoader) { return pendingResource.then(() => { const versionTest = dup && semverCompare(style.version, dup.version); document.body.innerHTML = ''; @@ -88,7 +78,9 @@ function initInstallPage({style, dup}) { Support -
+
+
+
`)); if (versionTest < 0) { @@ -109,9 +101,64 @@ function initInstallPage({style, dup}) { install(style); } }; + + if (location.protocol === 'file:') { + initLiveReload(sourceLoader); + } }); } +function initLiveReload(sourceLoader) { + let installed; + const watcher = sourceLoader.watch(source => { + $('.code').textContent = source; + return communicate({ + method: 'saveUsercss', + id: installed.id, + source: source + }).then(() => { + $$('.main .warning').forEach(e => e.remove()); + }).catch(err => { + const oldWarning = $('.main .warning'); + // FIXME: i18n + const warning = tHTML(` +
+ Stylus failed to parse usercss: +
${err}
+
+ `); + if (oldWarning) { + oldWarning.replaceWith(warning); + } else { + $('.main').prepend(warning); + } + }); + }); + window.addEventListener('installed', ({detail: {style}}) => { + installed = style; + if ($('.live-reload-checkbox').checked) { + watcher.start(); + } + }); + // FIXME: i18n + $('.actions').append(tHTML(` + + `)); + $('.live-reload-checkbox').onchange = e => { + if (!installed) { + return; + } + if (e.target.checked) { + watcher.start(); + } else { + watcher.stop(); + } + }; +} + function initErrorPage(err, source) { return pendingResource.then(() => { document.body.innerHTML = ''; @@ -127,8 +174,65 @@ function initErrorPage(err, source) { }); } -function initUsercssInstall() { +function createSourceLoader() { let source; + + 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 load() { + return fetchText(location.href) + .then(_source => { + source = _source; + return source; + }); + } + + function watch(cb) { + let timer; + const DELAY = 1000; + + function start() { + if (timer) { + return; + } + timer = setTimeout(check, DELAY); + } + + function stop() { + clearTimeout(timer); + timer = null; + } + + function check() { + fetchText(location.href) + .then(_source => { + if (source !== _source) { + source = _source; + return cb(source); + } + }) + .catch(console.log) + .then(() => { + timer = setTimeout(check, DELAY); + }); + } + + return {start, stop}; + } + + return {load, watch, source: () => source}; +} + +function initUsercssInstall() { pendingResource = communicate({ method: 'injectResource', resources: [ @@ -139,17 +243,18 @@ function initUsercssInstall() { '/content/install-user-css.css' ] }); - fetchText(location.href) - .then(_source => { - source = _source; - return communicate({ + + const sourceLoader = createSourceLoader(); + sourceLoader.load() + .then(() => + communicate({ method: 'filterUsercss', - source, + source: sourceLoader.source(), checkDup: true - }); - }) - .then(initInstallPage) - .catch(err => initErrorPage(err, source)); + }) + ) + .then(result => initInstallPage(result, sourceLoader)) + .catch(err => initErrorPage(err, sourceLoader.source())); } function isUsercss() { From 1c3317202febb9a1a6caba71ca2a3b11debc65f7 Mon Sep 17 00:00:00 2001 From: eight Date: Sun, 10 Sep 2017 23:46:54 +0800 Subject: [PATCH 024/250] Refactor: init --- edit/edit.js | 82 ++++++++++++++++++++++++++++++---------------------- 1 file changed, 48 insertions(+), 34 deletions(-) diff --git a/edit/edit.js b/edit/edit.js index 39804c61..fef86a84 100644 --- a/edit/edit.js +++ b/edit/edit.js @@ -1197,47 +1197,61 @@ function beautify(event) { document.addEventListener('DOMContentLoaded', init); +function createEmptyStyle() { + const params = getParams(); + const style = { + id: null, + name: '', + enabled: true, + sections: [{code: ''}] + }; + for (const i in CssToProperty) { + if (params[i]) { + style.sections[0][CssToProperty[i]] = [params[i]]; + } + } + return style; +} + +function windowLoaded() { + if (document.readyState !== 'loading') { + return Promise.resolve(); + } + return new Promise(resolve => { + window.addEventListener('load', function self() { + window.removeEventListener('load', self); + resolve(); + }); + }); +} + function init() { initCodeMirror(); const params = getParams(); - if (!params.id) { - // match should be 2 - one for the whole thing, one for the parentheses - // This is an add - $('#heading').textContent = t('addStyleTitle'); - const section = {code: ''}; - for (const i in CssToProperty) { - if (params[i]) { - section[CssToProperty[i]] = [params[i]]; + return Promise.resolve().then(() => { + if (!params.id) { + // match should be 2 - one for the whole thing, one for the parentheses + // This is an add + $('#heading').textContent = t('addStyleTitle'); + return createEmptyStyle(); + } + $('#heading').textContent = t('editStyleHeading'); + // This is an edit + return getStylesSafe({id: params.id}).then(styles => { + let style = styles[0]; + if (!style) { + style = createEmptyStyle(); + history.replaceState({}, document.title, location.pathname); } - } - window.onload = () => { - window.onload = null; - addSection(null, section); - editors[0].setOption('lint', CodeMirror.defaults.lint); - // default to enabled - $('#enabled').checked = true; - initHooks(); - }; - return; - } - // This is an edit - $('#heading').textContent = t('editStyleHeading'); - getStylesSafe({id: params.id}).then(styles => { - let style = styles[0]; - if (!style) { - style = {id: null, sections: []}; - history.replaceState({}, document.title, location.pathname); - } + return style; + }); + }).then(style => { styleId = style.id; sessionStorage.justEditedStyleId = styleId; - setStyleMeta(style); - window.onload = () => { - window.onload = null; + + return windowLoaded().then(() => { initWithStyle({style}); - }; - if (document.readyState !== 'loading') { - window.onload(); - } + }); }); } From cfdb0b4eebc18b21c6ecdffb837c02f22efcf7aa Mon Sep 17 00:00:00 2001 From: eight Date: Mon, 11 Sep 2017 22:05:19 +0800 Subject: [PATCH 025/250] Add mode stylus, loadmode --- vendor/codemirror/addon/mode/loadmode.js | 64 ++ vendor/codemirror/mode/stylus/stylus.js | 771 +++++++++++++++++++++++ 2 files changed, 835 insertions(+) create mode 100644 vendor/codemirror/addon/mode/loadmode.js create mode 100644 vendor/codemirror/mode/stylus/stylus.js diff --git a/vendor/codemirror/addon/mode/loadmode.js b/vendor/codemirror/addon/mode/loadmode.js new file mode 100644 index 00000000..10117ec2 --- /dev/null +++ b/vendor/codemirror/addon/mode/loadmode.js @@ -0,0 +1,64 @@ +// CodeMirror, copyright (c) by Marijn Haverbeke and others +// Distributed under an MIT license: http://codemirror.net/LICENSE + +(function(mod) { + if (typeof exports == "object" && typeof module == "object") // CommonJS + mod(require("../../lib/codemirror"), "cjs"); + else if (typeof define == "function" && define.amd) // AMD + define(["../../lib/codemirror"], function(CM) { mod(CM, "amd"); }); + else // Plain browser env + mod(CodeMirror, "plain"); +})(function(CodeMirror, env) { + if (!CodeMirror.modeURL) CodeMirror.modeURL = "../mode/%N/%N.js"; + + var loading = {}; + function splitCallback(cont, n) { + var countDown = n; + return function() { if (--countDown == 0) cont(); }; + } + function ensureDeps(mode, cont) { + var deps = CodeMirror.modes[mode].dependencies; + if (!deps) return cont(); + var missing = []; + for (var i = 0; i < deps.length; ++i) { + if (!CodeMirror.modes.hasOwnProperty(deps[i])) + missing.push(deps[i]); + } + if (!missing.length) return cont(); + var split = splitCallback(cont, missing.length); + for (var i = 0; i < missing.length; ++i) + CodeMirror.requireMode(missing[i], split); + } + + CodeMirror.requireMode = function(mode, cont) { + if (typeof mode != "string") mode = mode.name; + if (CodeMirror.modes.hasOwnProperty(mode)) return ensureDeps(mode, cont); + if (loading.hasOwnProperty(mode)) return loading[mode].push(cont); + + var file = CodeMirror.modeURL.replace(/%N/g, mode); + if (env == "plain") { + var script = document.createElement("script"); + script.src = file; + var others = document.getElementsByTagName("script")[0]; + var list = loading[mode] = [cont]; + CodeMirror.on(script, "load", function() { + ensureDeps(mode, function() { + for (var i = 0; i < list.length; ++i) list[i](); + }); + }); + others.parentNode.insertBefore(script, others); + } else if (env == "cjs") { + require(file); + cont(); + } else if (env == "amd") { + requirejs([file], cont); + } + }; + + CodeMirror.autoLoadMode = function(instance, mode) { + if (!CodeMirror.modes.hasOwnProperty(mode)) + CodeMirror.requireMode(mode, function() { + instance.setOption("mode", instance.getOption("mode")); + }); + }; +}); diff --git a/vendor/codemirror/mode/stylus/stylus.js b/vendor/codemirror/mode/stylus/stylus.js new file mode 100644 index 00000000..b83be16f --- /dev/null +++ b/vendor/codemirror/mode/stylus/stylus.js @@ -0,0 +1,771 @@ +// CodeMirror, copyright (c) by Marijn Haverbeke and others +// Distributed under an MIT license: http://codemirror.net/LICENSE + +// Stylus mode created by Dmitry Kiselyov http://git.io/AaRB + +(function(mod) { + if (typeof exports == "object" && typeof module == "object") // CommonJS + mod(require("../../lib/codemirror")); + else if (typeof define == "function" && define.amd) // AMD + define(["../../lib/codemirror"], mod); + else // Plain browser env + mod(CodeMirror); +})(function(CodeMirror) { + "use strict"; + + CodeMirror.defineMode("stylus", function(config) { + var indentUnit = config.indentUnit, + indentUnitString = '', + tagKeywords = keySet(tagKeywords_), + tagVariablesRegexp = /^(a|b|i|s|col|em)$/i, + propertyKeywords = keySet(propertyKeywords_), + nonStandardPropertyKeywords = keySet(nonStandardPropertyKeywords_), + valueKeywords = keySet(valueKeywords_), + colorKeywords = keySet(colorKeywords_), + documentTypes = keySet(documentTypes_), + documentTypesRegexp = wordRegexp(documentTypes_), + mediaFeatures = keySet(mediaFeatures_), + mediaTypes = keySet(mediaTypes_), + fontProperties = keySet(fontProperties_), + operatorsRegexp = /^\s*([.]{2,3}|&&|\|\||\*\*|[?!=:]?=|[-+*\/%<>]=?|\?:|\~)/, + wordOperatorKeywordsRegexp = wordRegexp(wordOperatorKeywords_), + blockKeywords = keySet(blockKeywords_), + vendorPrefixesRegexp = new RegExp(/^\-(moz|ms|o|webkit)-/i), + commonAtoms = keySet(commonAtoms_), + firstWordMatch = "", + states = {}, + ch, + style, + type, + override; + + while (indentUnitString.length < indentUnit) indentUnitString += ' '; + + /** + * Tokenizers + */ + function tokenBase(stream, state) { + firstWordMatch = stream.string.match(/(^[\w-]+\s*=\s*$)|(^\s*[\w-]+\s*=\s*[\w-])|(^\s*(\.|#|@|\$|\&|\[|\d|\+|::?|\{|\>|~|\/)?\s*[\w-]*([a-z0-9-]|\*|\/\*)(\(|,)?)/); + state.context.line.firstWord = firstWordMatch ? firstWordMatch[0].replace(/^\s*/, "") : ""; + state.context.line.indent = stream.indentation(); + ch = stream.peek(); + + // Line comment + if (stream.match("//")) { + stream.skipToEnd(); + return ["comment", "comment"]; + } + // Block comment + if (stream.match("/*")) { + state.tokenize = tokenCComment; + return tokenCComment(stream, state); + } + // String + if (ch == "\"" || ch == "'") { + stream.next(); + state.tokenize = tokenString(ch); + return state.tokenize(stream, state); + } + // Def + if (ch == "@") { + stream.next(); + stream.eatWhile(/[\w\\-]/); + return ["def", stream.current()]; + } + // ID selector or Hex color + if (ch == "#") { + stream.next(); + // Hex color + if (stream.match(/^[0-9a-f]{6}|[0-9a-f]{3}/i)) { + return ["atom", "atom"]; + } + // ID selector + if (stream.match(/^[a-z][\w-]*/i)) { + return ["builtin", "hash"]; + } + } + // Vendor prefixes + if (stream.match(vendorPrefixesRegexp)) { + return ["meta", "vendor-prefixes"]; + } + // Numbers + if (stream.match(/^-?[0-9]?\.?[0-9]/)) { + stream.eatWhile(/[a-z%]/i); + return ["number", "unit"]; + } + // !important|optional + if (ch == "!") { + stream.next(); + return [stream.match(/^(important|optional)/i) ? "keyword": "operator", "important"]; + } + // Class + if (ch == "." && stream.match(/^\.[a-z][\w-]*/i)) { + return ["qualifier", "qualifier"]; + } + // url url-prefix domain regexp + if (stream.match(documentTypesRegexp)) { + if (stream.peek() == "(") state.tokenize = tokenParenthesized; + return ["property", "word"]; + } + // Mixins / Functions + if (stream.match(/^[a-z][\w-]*\(/i)) { + stream.backUp(1); + return ["keyword", "mixin"]; + } + // Block mixins + if (stream.match(/^(\+|-)[a-z][\w-]*\(/i)) { + stream.backUp(1); + return ["keyword", "block-mixin"]; + } + // Parent Reference BEM naming + if (stream.string.match(/^\s*&/) && stream.match(/^[-_]+[a-z][\w-]*/)) { + return ["qualifier", "qualifier"]; + } + // / Root Reference & Parent Reference + if (stream.match(/^(\/|&)(-|_|:|\.|#|[a-z])/)) { + stream.backUp(1); + return ["variable-3", "reference"]; + } + if (stream.match(/^&{1}\s*$/)) { + return ["variable-3", "reference"]; + } + // Word operator + if (stream.match(wordOperatorKeywordsRegexp)) { + return ["operator", "operator"]; + } + // Word + if (stream.match(/^\$?[-_]*[a-z0-9]+[\w-]*/i)) { + // Variable + if (stream.match(/^(\.|\[)[\w-\'\"\]]+/i, false)) { + if (!wordIsTag(stream.current())) { + stream.match(/\./); + return ["variable-2", "variable-name"]; + } + } + return ["variable-2", "word"]; + } + // Operators + if (stream.match(operatorsRegexp)) { + return ["operator", stream.current()]; + } + // Delimiters + if (/[:;,{}\[\]\(\)]/.test(ch)) { + stream.next(); + return [null, ch]; + } + // Non-detected items + stream.next(); + return [null, null]; + } + + /** + * Token comment + */ + function tokenCComment(stream, state) { + var maybeEnd = false, ch; + while ((ch = stream.next()) != null) { + if (maybeEnd && ch == "/") { + state.tokenize = null; + break; + } + maybeEnd = (ch == "*"); + } + return ["comment", "comment"]; + } + + /** + * Token string + */ + function tokenString(quote) { + return function(stream, state) { + var escaped = false, ch; + while ((ch = stream.next()) != null) { + if (ch == quote && !escaped) { + if (quote == ")") stream.backUp(1); + break; + } + escaped = !escaped && ch == "\\"; + } + if (ch == quote || !escaped && quote != ")") state.tokenize = null; + return ["string", "string"]; + }; + } + + /** + * Token parenthesized + */ + function tokenParenthesized(stream, state) { + stream.next(); // Must be "(" + if (!stream.match(/\s*[\"\')]/, false)) + state.tokenize = tokenString(")"); + else + state.tokenize = null; + return [null, "("]; + } + + /** + * Context management + */ + function Context(type, indent, prev, line) { + this.type = type; + this.indent = indent; + this.prev = prev; + this.line = line || {firstWord: "", indent: 0}; + } + + function pushContext(state, stream, type, indent) { + indent = indent >= 0 ? indent : indentUnit; + state.context = new Context(type, stream.indentation() + indent, state.context); + return type; + } + + function popContext(state, currentIndent) { + var contextIndent = state.context.indent - indentUnit; + currentIndent = currentIndent || false; + state.context = state.context.prev; + if (currentIndent) state.context.indent = contextIndent; + return state.context.type; + } + + function pass(type, stream, state) { + return states[state.context.type](type, stream, state); + } + + function popAndPass(type, stream, state, n) { + for (var i = n || 1; i > 0; i--) + state.context = state.context.prev; + return pass(type, stream, state); + } + + + /** + * Parser + */ + function wordIsTag(word) { + return word.toLowerCase() in tagKeywords; + } + + function wordIsProperty(word) { + word = word.toLowerCase(); + return word in propertyKeywords || word in fontProperties; + } + + function wordIsBlock(word) { + return word.toLowerCase() in blockKeywords; + } + + function wordIsVendorPrefix(word) { + return word.toLowerCase().match(vendorPrefixesRegexp); + } + + function wordAsValue(word) { + var wordLC = word.toLowerCase(); + var override = "variable-2"; + if (wordIsTag(word)) override = "tag"; + else if (wordIsBlock(word)) override = "block-keyword"; + else if (wordIsProperty(word)) override = "property"; + else if (wordLC in valueKeywords || wordLC in commonAtoms) override = "atom"; + else if (wordLC == "return" || wordLC in colorKeywords) override = "keyword"; + + // Font family + else if (word.match(/^[A-Z]/)) override = "string"; + return override; + } + + function typeIsBlock(type, stream) { + return ((endOfLine(stream) && (type == "{" || type == "]" || type == "hash" || type == "qualifier")) || type == "block-mixin"); + } + + function typeIsInterpolation(type, stream) { + return type == "{" && stream.match(/^\s*\$?[\w-]+/i, false); + } + + function typeIsPseudo(type, stream) { + return type == ":" && stream.match(/^[a-z-]+/, false); + } + + function startOfLine(stream) { + return stream.sol() || stream.string.match(new RegExp("^\\s*" + escapeRegExp(stream.current()))); + } + + function endOfLine(stream) { + return stream.eol() || stream.match(/^\s*$/, false); + } + + function firstWordOfLine(line) { + var re = /^\s*[-_]*[a-z0-9]+[\w-]*/i; + var result = typeof line == "string" ? line.match(re) : line.string.match(re); + return result ? result[0].replace(/^\s*/, "") : ""; + } + + + /** + * Block + */ + states.block = function(type, stream, state) { + if ((type == "comment" && startOfLine(stream)) || + (type == "," && endOfLine(stream)) || + type == "mixin") { + return pushContext(state, stream, "block", 0); + } + if (typeIsInterpolation(type, stream)) { + return pushContext(state, stream, "interpolation"); + } + if (endOfLine(stream) && type == "]") { + if (!/^\s*(\.|#|:|\[|\*|&)/.test(stream.string) && !wordIsTag(firstWordOfLine(stream))) { + return pushContext(state, stream, "block", 0); + } + } + if (typeIsBlock(type, stream)) { + return pushContext(state, stream, "block"); + } + if (type == "}" && endOfLine(stream)) { + return pushContext(state, stream, "block", 0); + } + if (type == "variable-name") { + if (stream.string.match(/^\s?\$[\w-\.\[\]\'\"]+$/) || wordIsBlock(firstWordOfLine(stream))) { + return pushContext(state, stream, "variableName"); + } + else { + return pushContext(state, stream, "variableName", 0); + } + } + if (type == "=") { + if (!endOfLine(stream) && !wordIsBlock(firstWordOfLine(stream))) { + return pushContext(state, stream, "block", 0); + } + return pushContext(state, stream, "block"); + } + if (type == "*") { + if (endOfLine(stream) || stream.match(/\s*(,|\.|#|\[|:|{)/,false)) { + override = "tag"; + return pushContext(state, stream, "block"); + } + } + if (typeIsPseudo(type, stream)) { + return pushContext(state, stream, "pseudo"); + } + if (/@(font-face|media|supports|(-moz-)?document)/.test(type)) { + return pushContext(state, stream, endOfLine(stream) ? "block" : "atBlock"); + } + if (/@(-(moz|ms|o|webkit)-)?keyframes$/.test(type)) { + return pushContext(state, stream, "keyframes"); + } + if (/@extends?/.test(type)) { + return pushContext(state, stream, "extend", 0); + } + if (type && type.charAt(0) == "@") { + + // Property Lookup + if (stream.indentation() > 0 && wordIsProperty(stream.current().slice(1))) { + override = "variable-2"; + return "block"; + } + if (/(@import|@require|@charset)/.test(type)) { + return pushContext(state, stream, "block", 0); + } + return pushContext(state, stream, "block"); + } + if (type == "reference" && endOfLine(stream)) { + return pushContext(state, stream, "block"); + } + if (type == "(") { + return pushContext(state, stream, "parens"); + } + + if (type == "vendor-prefixes") { + return pushContext(state, stream, "vendorPrefixes"); + } + if (type == "word") { + var word = stream.current(); + override = wordAsValue(word); + + if (override == "property") { + if (startOfLine(stream)) { + return pushContext(state, stream, "block", 0); + } else { + override = "atom"; + return "block"; + } + } + + if (override == "tag") { + + // tag is a css value + if (/embed|menu|pre|progress|sub|table/.test(word)) { + if (wordIsProperty(firstWordOfLine(stream))) { + override = "atom"; + return "block"; + } + } + + // tag is an attribute + if (stream.string.match(new RegExp("\\[\\s*" + word + "|" + word +"\\s*\\]"))) { + override = "atom"; + return "block"; + } + + // tag is a variable + if (tagVariablesRegexp.test(word)) { + if ((startOfLine(stream) && stream.string.match(/=/)) || + (!startOfLine(stream) && + !stream.string.match(/^(\s*\.|#|\&|\[|\/|>|\*)/) && + !wordIsTag(firstWordOfLine(stream)))) { + override = "variable-2"; + if (wordIsBlock(firstWordOfLine(stream))) return "block"; + return pushContext(state, stream, "block", 0); + } + } + + if (endOfLine(stream)) return pushContext(state, stream, "block"); + } + if (override == "block-keyword") { + override = "keyword"; + + // Postfix conditionals + if (stream.current(/(if|unless)/) && !startOfLine(stream)) { + return "block"; + } + return pushContext(state, stream, "block"); + } + if (word == "return") return pushContext(state, stream, "block", 0); + + // Placeholder selector + if (override == "variable-2" && stream.string.match(/^\s?\$[\w-\.\[\]\'\"]+$/)) { + return pushContext(state, stream, "block"); + } + } + return state.context.type; + }; + + + /** + * Parens + */ + states.parens = function(type, stream, state) { + if (type == "(") return pushContext(state, stream, "parens"); + if (type == ")") { + if (state.context.prev.type == "parens") { + return popContext(state); + } + if ((stream.string.match(/^[a-z][\w-]*\(/i) && endOfLine(stream)) || + wordIsBlock(firstWordOfLine(stream)) || + /(\.|#|:|\[|\*|&|>|~|\+|\/)/.test(firstWordOfLine(stream)) || + (!stream.string.match(/^-?[a-z][\w-\.\[\]\'\"]*\s*=/) && + wordIsTag(firstWordOfLine(stream)))) { + return pushContext(state, stream, "block"); + } + if (stream.string.match(/^[\$-]?[a-z][\w-\.\[\]\'\"]*\s*=/) || + stream.string.match(/^\s*(\(|\)|[0-9])/) || + stream.string.match(/^\s+[a-z][\w-]*\(/i) || + stream.string.match(/^\s+[\$-]?[a-z]/i)) { + return pushContext(state, stream, "block", 0); + } + if (endOfLine(stream)) return pushContext(state, stream, "block"); + else return pushContext(state, stream, "block", 0); + } + if (type && type.charAt(0) == "@" && wordIsProperty(stream.current().slice(1))) { + override = "variable-2"; + } + if (type == "word") { + var word = stream.current(); + override = wordAsValue(word); + if (override == "tag" && tagVariablesRegexp.test(word)) { + override = "variable-2"; + } + if (override == "property" || word == "to") override = "atom"; + } + if (type == "variable-name") { + return pushContext(state, stream, "variableName"); + } + if (typeIsPseudo(type, stream)) { + return pushContext(state, stream, "pseudo"); + } + return state.context.type; + }; + + + /** + * Vendor prefixes + */ + states.vendorPrefixes = function(type, stream, state) { + if (type == "word") { + override = "property"; + return pushContext(state, stream, "block", 0); + } + return popContext(state); + }; + + + /** + * Pseudo + */ + states.pseudo = function(type, stream, state) { + if (!wordIsProperty(firstWordOfLine(stream.string))) { + stream.match(/^[a-z-]+/); + override = "variable-3"; + if (endOfLine(stream)) return pushContext(state, stream, "block"); + return popContext(state); + } + return popAndPass(type, stream, state); + }; + + + /** + * atBlock + */ + states.atBlock = function(type, stream, state) { + if (type == "(") return pushContext(state, stream, "atBlock_parens"); + if (typeIsBlock(type, stream)) { + return pushContext(state, stream, "block"); + } + if (typeIsInterpolation(type, stream)) { + return pushContext(state, stream, "interpolation"); + } + if (type == "word") { + var word = stream.current().toLowerCase(); + if (/^(only|not|and|or)$/.test(word)) + override = "keyword"; + else if (documentTypes.hasOwnProperty(word)) + override = "tag"; + else if (mediaTypes.hasOwnProperty(word)) + override = "attribute"; + else if (mediaFeatures.hasOwnProperty(word)) + override = "property"; + else if (nonStandardPropertyKeywords.hasOwnProperty(word)) + override = "string-2"; + else override = wordAsValue(stream.current()); + if (override == "tag" && endOfLine(stream)) { + return pushContext(state, stream, "block"); + } + } + if (type == "operator" && /^(not|and|or)$/.test(stream.current())) { + override = "keyword"; + } + return state.context.type; + }; + + states.atBlock_parens = function(type, stream, state) { + if (type == "{" || type == "}") return state.context.type; + if (type == ")") { + if (endOfLine(stream)) return pushContext(state, stream, "block"); + else return pushContext(state, stream, "atBlock"); + } + if (type == "word") { + var word = stream.current().toLowerCase(); + override = wordAsValue(word); + if (/^(max|min)/.test(word)) override = "property"; + if (override == "tag") { + tagVariablesRegexp.test(word) ? override = "variable-2" : override = "atom"; + } + return state.context.type; + } + return states.atBlock(type, stream, state); + }; + + + /** + * Keyframes + */ + states.keyframes = function(type, stream, state) { + if (stream.indentation() == "0" && ((type == "}" && startOfLine(stream)) || type == "]" || type == "hash" + || type == "qualifier" || wordIsTag(stream.current()))) { + return popAndPass(type, stream, state); + } + if (type == "{") return pushContext(state, stream, "keyframes"); + if (type == "}") { + if (startOfLine(stream)) return popContext(state, true); + else return pushContext(state, stream, "keyframes"); + } + if (type == "unit" && /^[0-9]+\%$/.test(stream.current())) { + return pushContext(state, stream, "keyframes"); + } + if (type == "word") { + override = wordAsValue(stream.current()); + if (override == "block-keyword") { + override = "keyword"; + return pushContext(state, stream, "keyframes"); + } + } + if (/@(font-face|media|supports|(-moz-)?document)/.test(type)) { + return pushContext(state, stream, endOfLine(stream) ? "block" : "atBlock"); + } + if (type == "mixin") { + return pushContext(state, stream, "block", 0); + } + return state.context.type; + }; + + + /** + * Interpolation + */ + states.interpolation = function(type, stream, state) { + if (type == "{") popContext(state) && pushContext(state, stream, "block"); + if (type == "}") { + if (stream.string.match(/^\s*(\.|#|:|\[|\*|&|>|~|\+|\/)/i) || + (stream.string.match(/^\s*[a-z]/i) && wordIsTag(firstWordOfLine(stream)))) { + return pushContext(state, stream, "block"); + } + if (!stream.string.match(/^(\{|\s*\&)/) || + stream.match(/\s*[\w-]/,false)) { + return pushContext(state, stream, "block", 0); + } + return pushContext(state, stream, "block"); + } + if (type == "variable-name") { + return pushContext(state, stream, "variableName", 0); + } + if (type == "word") { + override = wordAsValue(stream.current()); + if (override == "tag") override = "atom"; + } + return state.context.type; + }; + + + /** + * Extend/s + */ + states.extend = function(type, stream, state) { + if (type == "[" || type == "=") return "extend"; + if (type == "]") return popContext(state); + if (type == "word") { + override = wordAsValue(stream.current()); + return "extend"; + } + return popContext(state); + }; + + + /** + * Variable name + */ + states.variableName = function(type, stream, state) { + if (type == "string" || type == "[" || type == "]" || stream.current().match(/^(\.|\$)/)) { + if (stream.current().match(/^\.[\w-]+/i)) override = "variable-2"; + return "variableName"; + } + return popAndPass(type, stream, state); + }; + + + return { + startState: function(base) { + return { + tokenize: null, + state: "block", + context: new Context("block", base || 0, null) + }; + }, + token: function(stream, state) { + if (!state.tokenize && stream.eatSpace()) return null; + style = (state.tokenize || tokenBase)(stream, state); + if (style && typeof style == "object") { + type = style[1]; + style = style[0]; + } + override = style; + state.state = states[state.state](type, stream, state); + return override; + }, + indent: function(state, textAfter, line) { + + var cx = state.context, + ch = textAfter && textAfter.charAt(0), + indent = cx.indent, + lineFirstWord = firstWordOfLine(textAfter), + lineIndent = line.match(/^\s*/)[0].replace(/\t/g, indentUnitString).length, + prevLineFirstWord = state.context.prev ? state.context.prev.line.firstWord : "", + prevLineIndent = state.context.prev ? state.context.prev.line.indent : lineIndent; + + if (cx.prev && + (ch == "}" && (cx.type == "block" || cx.type == "atBlock" || cx.type == "keyframes") || + ch == ")" && (cx.type == "parens" || cx.type == "atBlock_parens") || + ch == "{" && (cx.type == "at"))) { + indent = cx.indent - indentUnit; + } else if (!(/(\})/.test(ch))) { + if (/@|\$|\d/.test(ch) || + /^\{/.test(textAfter) || +/^\s*\/(\/|\*)/.test(textAfter) || + /^\s*\/\*/.test(prevLineFirstWord) || + /^\s*[\w-\.\[\]\'\"]+\s*(\?|:|\+)?=/i.test(textAfter) || +/^(\+|-)?[a-z][\w-]*\(/i.test(textAfter) || +/^return/.test(textAfter) || + wordIsBlock(lineFirstWord)) { + indent = lineIndent; + } else if (/(\.|#|:|\[|\*|&|>|~|\+|\/)/.test(ch) || wordIsTag(lineFirstWord)) { + if (/\,\s*$/.test(prevLineFirstWord)) { + indent = prevLineIndent; + } else if (/^\s+/.test(line) && (/(\.|#|:|\[|\*|&|>|~|\+|\/)/.test(prevLineFirstWord) || wordIsTag(prevLineFirstWord))) { + indent = lineIndent <= prevLineIndent ? prevLineIndent : prevLineIndent + indentUnit; + } else { + indent = lineIndent; + } + } else if (!/,\s*$/.test(line) && (wordIsVendorPrefix(lineFirstWord) || wordIsProperty(lineFirstWord))) { + if (wordIsBlock(prevLineFirstWord)) { + indent = lineIndent <= prevLineIndent ? prevLineIndent : prevLineIndent + indentUnit; + } else if (/^\{/.test(prevLineFirstWord)) { + indent = lineIndent <= prevLineIndent ? lineIndent : prevLineIndent + indentUnit; + } else if (wordIsVendorPrefix(prevLineFirstWord) || wordIsProperty(prevLineFirstWord)) { + indent = lineIndent >= prevLineIndent ? prevLineIndent : lineIndent; + } else if (/^(\.|#|:|\[|\*|&|@|\+|\-|>|~|\/)/.test(prevLineFirstWord) || + /=\s*$/.test(prevLineFirstWord) || + wordIsTag(prevLineFirstWord) || + /^\$[\w-\.\[\]\'\"]/.test(prevLineFirstWord)) { + indent = prevLineIndent + indentUnit; + } else { + indent = lineIndent; + } + } + } + return indent; + }, + electricChars: "}", + lineComment: "//", + fold: "indent" + }; + }); + + // developer.mozilla.org/en-US/docs/Web/HTML/Element + var tagKeywords_ = ["a","abbr","address","area","article","aside","audio", "b", "base","bdi", "bdo","bgsound","blockquote","body","br","button","canvas","caption","cite", "code","col","colgroup","data","datalist","dd","del","details","dfn","div", "dl","dt","em","embed","fieldset","figcaption","figure","footer","form","h1", "h2","h3","h4","h5","h6","head","header","hgroup","hr","html","i","iframe", "img","input","ins","kbd","keygen","label","legend","li","link","main","map", "mark","marquee","menu","menuitem","meta","meter","nav","nobr","noframes", "noscript","object","ol","optgroup","option","output","p","param","pre", "progress","q","rp","rt","ruby","s","samp","script","section","select", "small","source","span","strong","style","sub","summary","sup","table","tbody","td","textarea","tfoot","th","thead","time","tr","track", "u","ul","var","video"]; + + // github.com/codemirror/CodeMirror/blob/master/mode/css/css.js + var documentTypes_ = ["domain", "regexp", "url", "url-prefix"]; + var mediaTypes_ = ["all","aural","braille","handheld","print","projection","screen","tty","tv","embossed"]; + var mediaFeatures_ = ["width","min-width","max-width","height","min-height","max-height","device-width","min-device-width","max-device-width","device-height","min-device-height","max-device-height","aspect-ratio","min-aspect-ratio","max-aspect-ratio","device-aspect-ratio","min-device-aspect-ratio","max-device-aspect-ratio","color","min-color","max-color","color-index","min-color-index","max-color-index","monochrome","min-monochrome","max-monochrome","resolution","min-resolution","max-resolution","scan","grid"]; + var propertyKeywords_ = ["align-content","align-items","align-self","alignment-adjust","alignment-baseline","anchor-point","animation","animation-delay","animation-direction","animation-duration","animation-fill-mode","animation-iteration-count","animation-name","animation-play-state","animation-timing-function","appearance","azimuth","backface-visibility","background","background-attachment","background-clip","background-color","background-image","background-origin","background-position","background-repeat","background-size","baseline-shift","binding","bleed","bookmark-label","bookmark-level","bookmark-state","bookmark-target","border","border-bottom","border-bottom-color","border-bottom-left-radius","border-bottom-right-radius","border-bottom-style","border-bottom-width","border-collapse","border-color","border-image","border-image-outset","border-image-repeat","border-image-slice","border-image-source","border-image-width","border-left","border-left-color","border-left-style","border-left-width","border-radius","border-right","border-right-color","border-right-style","border-right-width","border-spacing","border-style","border-top","border-top-color","border-top-left-radius","border-top-right-radius","border-top-style","border-top-width","border-width","bottom","box-decoration-break","box-shadow","box-sizing","break-after","break-before","break-inside","caption-side","clear","clip","color","color-profile","column-count","column-fill","column-gap","column-rule","column-rule-color","column-rule-style","column-rule-width","column-span","column-width","columns","content","counter-increment","counter-reset","crop","cue","cue-after","cue-before","cursor","direction","display","dominant-baseline","drop-initial-after-adjust","drop-initial-after-align","drop-initial-before-adjust","drop-initial-before-align","drop-initial-size","drop-initial-value","elevation","empty-cells","fit","fit-position","flex","flex-basis","flex-direction","flex-flow","flex-grow","flex-shrink","flex-wrap","float","float-offset","flow-from","flow-into","font","font-feature-settings","font-family","font-kerning","font-language-override","font-size","font-size-adjust","font-stretch","font-style","font-synthesis","font-variant","font-variant-alternates","font-variant-caps","font-variant-east-asian","font-variant-ligatures","font-variant-numeric","font-variant-position","font-weight","grid","grid-area","grid-auto-columns","grid-auto-flow","grid-auto-position","grid-auto-rows","grid-column","grid-column-end","grid-column-start","grid-row","grid-row-end","grid-row-start","grid-template","grid-template-areas","grid-template-columns","grid-template-rows","hanging-punctuation","height","hyphens","icon","image-orientation","image-rendering","image-resolution","inline-box-align","justify-content","left","letter-spacing","line-break","line-height","line-stacking","line-stacking-ruby","line-stacking-shift","line-stacking-strategy","list-style","list-style-image","list-style-position","list-style-type","margin","margin-bottom","margin-left","margin-right","margin-top","marker-offset","marks","marquee-direction","marquee-loop","marquee-play-count","marquee-speed","marquee-style","max-height","max-width","min-height","min-width","move-to","nav-down","nav-index","nav-left","nav-right","nav-up","object-fit","object-position","opacity","order","orphans","outline","outline-color","outline-offset","outline-style","outline-width","overflow","overflow-style","overflow-wrap","overflow-x","overflow-y","padding","padding-bottom","padding-left","padding-right","padding-top","page","page-break-after","page-break-before","page-break-inside","page-policy","pause","pause-after","pause-before","perspective","perspective-origin","pitch","pitch-range","play-during","position","presentation-level","punctuation-trim","quotes","region-break-after","region-break-before","region-break-inside","region-fragment","rendering-intent","resize","rest","rest-after","rest-before","richness","right","rotation","rotation-point","ruby-align","ruby-overhang","ruby-position","ruby-span","shape-image-threshold","shape-inside","shape-margin","shape-outside","size","speak","speak-as","speak-header","speak-numeral","speak-punctuation","speech-rate","stress","string-set","tab-size","table-layout","target","target-name","target-new","target-position","text-align","text-align-last","text-decoration","text-decoration-color","text-decoration-line","text-decoration-skip","text-decoration-style","text-emphasis","text-emphasis-color","text-emphasis-position","text-emphasis-style","text-height","text-indent","text-justify","text-outline","text-overflow","text-shadow","text-size-adjust","text-space-collapse","text-transform","text-underline-position","text-wrap","top","transform","transform-origin","transform-style","transition","transition-delay","transition-duration","transition-property","transition-timing-function","unicode-bidi","vertical-align","visibility","voice-balance","voice-duration","voice-family","voice-pitch","voice-range","voice-rate","voice-stress","voice-volume","volume","white-space","widows","width","will-change","word-break","word-spacing","word-wrap","z-index","clip-path","clip-rule","mask","enable-background","filter","flood-color","flood-opacity","lighting-color","stop-color","stop-opacity","pointer-events","color-interpolation","color-interpolation-filters","color-rendering","fill","fill-opacity","fill-rule","image-rendering","marker","marker-end","marker-mid","marker-start","shape-rendering","stroke","stroke-dasharray","stroke-dashoffset","stroke-linecap","stroke-linejoin","stroke-miterlimit","stroke-opacity","stroke-width","text-rendering","baseline-shift","dominant-baseline","glyph-orientation-horizontal","glyph-orientation-vertical","text-anchor","writing-mode","font-smoothing","osx-font-smoothing"]; + var nonStandardPropertyKeywords_ = ["scrollbar-arrow-color","scrollbar-base-color","scrollbar-dark-shadow-color","scrollbar-face-color","scrollbar-highlight-color","scrollbar-shadow-color","scrollbar-3d-light-color","scrollbar-track-color","shape-inside","searchfield-cancel-button","searchfield-decoration","searchfield-results-button","searchfield-results-decoration","zoom"]; + var fontProperties_ = ["font-family","src","unicode-range","font-variant","font-feature-settings","font-stretch","font-weight","font-style"]; + var colorKeywords_ = ["aliceblue","antiquewhite","aqua","aquamarine","azure","beige","bisque","black","blanchedalmond","blue","blueviolet","brown","burlywood","cadetblue","chartreuse","chocolate","coral","cornflowerblue","cornsilk","crimson","cyan","darkblue","darkcyan","darkgoldenrod","darkgray","darkgreen","darkkhaki","darkmagenta","darkolivegreen","darkorange","darkorchid","darkred","darksalmon","darkseagreen","darkslateblue","darkslategray","darkturquoise","darkviolet","deeppink","deepskyblue","dimgray","dodgerblue","firebrick","floralwhite","forestgreen","fuchsia","gainsboro","ghostwhite","gold","goldenrod","gray","grey","green","greenyellow","honeydew","hotpink","indianred","indigo","ivory","khaki","lavender","lavenderblush","lawngreen","lemonchiffon","lightblue","lightcoral","lightcyan","lightgoldenrodyellow","lightgray","lightgreen","lightpink","lightsalmon","lightseagreen","lightskyblue","lightslategray","lightsteelblue","lightyellow","lime","limegreen","linen","magenta","maroon","mediumaquamarine","mediumblue","mediumorchid","mediumpurple","mediumseagreen","mediumslateblue","mediumspringgreen","mediumturquoise","mediumvioletred","midnightblue","mintcream","mistyrose","moccasin","navajowhite","navy","oldlace","olive","olivedrab","orange","orangered","orchid","palegoldenrod","palegreen","paleturquoise","palevioletred","papayawhip","peachpuff","peru","pink","plum","powderblue","purple","rebeccapurple","red","rosybrown","royalblue","saddlebrown","salmon","sandybrown","seagreen","seashell","sienna","silver","skyblue","slateblue","slategray","snow","springgreen","steelblue","tan","teal","thistle","tomato","turquoise","violet","wheat","white","whitesmoke","yellow","yellowgreen"]; + var valueKeywords_ = ["above","absolute","activeborder","additive","activecaption","afar","after-white-space","ahead","alias","all","all-scroll","alphabetic","alternate","always","amharic","amharic-abegede","antialiased","appworkspace","arabic-indic","armenian","asterisks","attr","auto","avoid","avoid-column","avoid-page","avoid-region","background","backwards","baseline","below","bidi-override","binary","bengali","blink","block","block-axis","bold","bolder","border","border-box","both","bottom","break","break-all","break-word","bullets","button","button-bevel","buttonface","buttonhighlight","buttonshadow","buttontext","calc","cambodian","capitalize","caps-lock-indicator","caption","captiontext","caret","cell","center","checkbox","circle","cjk-decimal","cjk-earthly-branch","cjk-heavenly-stem","cjk-ideographic","clear","clip","close-quote","col-resize","collapse","column","compact","condensed","contain","content","contents","content-box","context-menu","continuous","copy","counter","counters","cover","crop","cross","crosshair","currentcolor","cursive","cyclic","dashed","decimal","decimal-leading-zero","default","default-button","destination-atop","destination-in","destination-out","destination-over","devanagari","disc","discard","disclosure-closed","disclosure-open","document","dot-dash","dot-dot-dash","dotted","double","down","e-resize","ease","ease-in","ease-in-out","ease-out","element","ellipse","ellipsis","embed","end","ethiopic","ethiopic-abegede","ethiopic-abegede-am-et","ethiopic-abegede-gez","ethiopic-abegede-ti-er","ethiopic-abegede-ti-et","ethiopic-halehame-aa-er","ethiopic-halehame-aa-et","ethiopic-halehame-am-et","ethiopic-halehame-gez","ethiopic-halehame-om-et","ethiopic-halehame-sid-et","ethiopic-halehame-so-et","ethiopic-halehame-ti-er","ethiopic-halehame-ti-et","ethiopic-halehame-tig","ethiopic-numeric","ew-resize","expanded","extends","extra-condensed","extra-expanded","fantasy","fast","fill","fixed","flat","flex","footnotes","forwards","from","geometricPrecision","georgian","graytext","groove","gujarati","gurmukhi","hand","hangul","hangul-consonant","hebrew","help","hidden","hide","higher","highlight","highlighttext","hiragana","hiragana-iroha","horizontal","hsl","hsla","icon","ignore","inactiveborder","inactivecaption","inactivecaptiontext","infinite","infobackground","infotext","inherit","initial","inline","inline-axis","inline-block","inline-flex","inline-table","inset","inside","intrinsic","invert","italic","japanese-formal","japanese-informal","justify","kannada","katakana","katakana-iroha","keep-all","khmer","korean-hangul-formal","korean-hanja-formal","korean-hanja-informal","landscape","lao","large","larger","left","level","lighter","line-through","linear","linear-gradient","lines","list-item","listbox","listitem","local","logical","loud","lower","lower-alpha","lower-armenian","lower-greek","lower-hexadecimal","lower-latin","lower-norwegian","lower-roman","lowercase","ltr","malayalam","match","matrix","matrix3d","media-controls-background","media-current-time-display","media-fullscreen-button","media-mute-button","media-play-button","media-return-to-realtime-button","media-rewind-button","media-seek-back-button","media-seek-forward-button","media-slider","media-sliderthumb","media-time-remaining-display","media-volume-slider","media-volume-slider-container","media-volume-sliderthumb","medium","menu","menulist","menulist-button","menulist-text","menulist-textfield","menutext","message-box","middle","min-intrinsic","mix","mongolian","monospace","move","multiple","myanmar","n-resize","narrower","ne-resize","nesw-resize","no-close-quote","no-drop","no-open-quote","no-repeat","none","normal","not-allowed","nowrap","ns-resize","numbers","numeric","nw-resize","nwse-resize","oblique","octal","open-quote","optimizeLegibility","optimizeSpeed","oriya","oromo","outset","outside","outside-shape","overlay","overline","padding","padding-box","painted","page","paused","persian","perspective","plus-darker","plus-lighter","pointer","polygon","portrait","pre","pre-line","pre-wrap","preserve-3d","progress","push-button","radial-gradient","radio","read-only","read-write","read-write-plaintext-only","rectangle","region","relative","repeat","repeating-linear-gradient","repeating-radial-gradient","repeat-x","repeat-y","reset","reverse","rgb","rgba","ridge","right","rotate","rotate3d","rotateX","rotateY","rotateZ","round","row-resize","rtl","run-in","running","s-resize","sans-serif","scale","scale3d","scaleX","scaleY","scaleZ","scroll","scrollbar","scroll-position","se-resize","searchfield","searchfield-cancel-button","searchfield-decoration","searchfield-results-button","searchfield-results-decoration","semi-condensed","semi-expanded","separate","serif","show","sidama","simp-chinese-formal","simp-chinese-informal","single","skew","skewX","skewY","skip-white-space","slide","slider-horizontal","slider-vertical","sliderthumb-horizontal","sliderthumb-vertical","slow","small","small-caps","small-caption","smaller","solid","somali","source-atop","source-in","source-out","source-over","space","spell-out","square","square-button","start","static","status-bar","stretch","stroke","sub","subpixel-antialiased","super","sw-resize","symbolic","symbols","table","table-caption","table-cell","table-column","table-column-group","table-footer-group","table-header-group","table-row","table-row-group","tamil","telugu","text","text-bottom","text-top","textarea","textfield","thai","thick","thin","threeddarkshadow","threedface","threedhighlight","threedlightshadow","threedshadow","tibetan","tigre","tigrinya-er","tigrinya-er-abegede","tigrinya-et","tigrinya-et-abegede","to","top","trad-chinese-formal","trad-chinese-informal","translate","translate3d","translateX","translateY","translateZ","transparent","ultra-condensed","ultra-expanded","underline","up","upper-alpha","upper-armenian","upper-greek","upper-hexadecimal","upper-latin","upper-norwegian","upper-roman","uppercase","urdu","url","var","vertical","vertical-text","visible","visibleFill","visiblePainted","visibleStroke","visual","w-resize","wait","wave","wider","window","windowframe","windowtext","words","x-large","x-small","xor","xx-large","xx-small","bicubic","optimizespeed","grayscale","row","row-reverse","wrap","wrap-reverse","column-reverse","flex-start","flex-end","space-between","space-around", "unset"]; + + var wordOperatorKeywords_ = ["in","and","or","not","is not","is a","is","isnt","defined","if unless"], + blockKeywords_ = ["for","if","else","unless", "from", "to"], + commonAtoms_ = ["null","true","false","href","title","type","not-allowed","readonly","disabled"], + commonDef_ = ["@font-face", "@keyframes", "@media", "@viewport", "@page", "@host", "@supports", "@block", "@css"]; + + var hintWords = tagKeywords_.concat(documentTypes_,mediaTypes_,mediaFeatures_, + propertyKeywords_,nonStandardPropertyKeywords_, + colorKeywords_,valueKeywords_,fontProperties_, + wordOperatorKeywords_,blockKeywords_, + commonAtoms_,commonDef_); + + function wordRegexp(words) { + words = words.sort(function(a,b){return b > a;}); + return new RegExp("^((" + words.join(")|(") + "))\\b"); + } + + function keySet(array) { + var keys = {}; + for (var i = 0; i < array.length; ++i) keys[array[i]] = true; + return keys; + } + + function escapeRegExp(text) { + return text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&"); + } + + CodeMirror.registerHelper("hintWords", "stylus", hintWords); + CodeMirror.defineMIME("text/x-styl", "stylus"); +}); From 8bc6986cacb7c5d64c62aef0dd801cb0b99daa4a Mon Sep 17 00:00:00 2001 From: eight Date: Tue, 12 Sep 2017 00:08:54 +0800 Subject: [PATCH 026/250] Change: make unknown preprocessor throw --- js/usercss.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/js/usercss.js b/js/usercss.js index 7c5835b0..5099e8b0 100644 --- a/js/usercss.js +++ b/js/usercss.js @@ -203,7 +203,10 @@ var usercss = (function () { function buildCode(style) { let builder; - if (style.preprocessor && style.preprocessor in BUILDER) { + if (style.preprocessor) { + if (!BUILDER.hasOwnProperty(style.preprocessor)) { + return Promise.reject(new Error(`Unsupported preprocessor: ${style.preprocessor}`)); + } builder = BUILDER[style.preprocessor]; } else { builder = BUILDER.default; From a15493bfb93120508a1ec57be650fac710fab2ae Mon Sep 17 00:00:00 2001 From: eight Date: Tue, 12 Sep 2017 00:09:25 +0800 Subject: [PATCH 027/250] Add: source editor --- edit.html | 4 ++ edit/edit.css | 11 ++++ edit/edit.js | 58 ++++++++++++++---- edit/lint.js | 6 +- edit/source-editor.js | 133 ++++++++++++++++++++++++++++++++++++++++++ edit/util.js | 90 ++++++++++++++++++++++++++++ 6 files changed, 287 insertions(+), 15 deletions(-) create mode 100644 edit/source-editor.js create mode 100644 edit/util.js diff --git a/edit.html b/edit.html index 0d51cc3b..678a6e98 100644 --- a/edit.html +++ b/edit.html @@ -11,6 +11,8 @@ + + @@ -41,6 +43,8 @@ + + diff --git a/edit/edit.css b/edit/edit.css index 5e54a2b1..a3ac2d43 100644 --- a/edit/edit.css +++ b/edit/edit.css @@ -496,6 +496,17 @@ body[data-match-highlight="selection"] .CodeMirror-selection-highlight-scrollbar background-color: rgba(0, 0, 0, 0.05); } +/************ single editor **************/ +#sections .single-editor { + margin: 0; + height: 100%; + box-sizing: border-box; +} + +.single-editor .CodeMirror { + height: 100%; +} + /************ reponsive layouts ************/ @media(max-width:737px) { #header { diff --git a/edit/edit.js b/edit/edit.js index fef86a84..35ebe1b7 100644 --- a/edit/edit.js +++ b/edit/edit.js @@ -3,7 +3,7 @@ /* global onDOMscripted */ /* global css_beautify */ /* global CSSLint initLint linterConfig updateLintReport renderLintReport updateLinter */ -/* global mozParser */ +/* global mozParser createSourceEditor */ 'use strict'; @@ -20,6 +20,8 @@ let useHistoryBack; const propertyToCss = {urls: 'url', urlPrefixes: 'url-prefix', domains: 'domain', regexps: 'regexp'}; const CssToProperty = {'url': 'urls', 'url-prefix': 'urlPrefixes', 'domain': 'domains', 'regexp': 'regexps'}; +let editor; + // if background page hasn't been loaded yet, increase the chances it has before DOMContentLoaded onBackgroundReady(); @@ -271,11 +273,13 @@ function initCodeMirror() { CM.getOption = o => CodeMirror.defaults[o]; CM.setOption = (o, v) => { CodeMirror.defaults[o] = v; - editors.forEach(editor => { + $$('.CodeMirror').map(e => e.CodeMirror).forEach(editor => { editor.setOption(o, v); }); }; + CM.modeURL = '/vendor/codemirror/mode/%N/%N.js'; + CM.prototype.getSection = function () { return this.display.wrapper.parentNode; }; @@ -355,11 +359,9 @@ function acmeEventListener(event) { return; } case 'autocompleteOnTyping': - editors.forEach(cm => { - const onOff = el.checked ? 'on' : 'off'; - cm[onOff]('change', autocompleteOnTyping); - cm[onOff]('pick', autocompletePicked); - }); + $$('.CodeMirror') + .map(e => e.CodeMirror) + .forEach(cm => setupAutocomplete(cm, el.checked)); return; case 'matchHighlight': switch (value) { @@ -384,8 +386,7 @@ function setupCodeMirror(textarea, index) { cm.on('change', indicateCodeChange); if (prefs.get('editor.autocompleteOnTyping')) { - cm.on('change', autocompleteOnTyping); - cm.on('pick', autocompletePicked); + setupAutocomplete(cm); } cm.on('blur', () => { editors.lastActive = cm; @@ -996,6 +997,13 @@ function jumpToLine(cm) { } function toggleStyle() { + if (!editor) { + return _toggleStyle(); + } + editor.toggleStyle(); +} + +function _toggleStyle() { $('#enabled').checked = !$('#enabled').checked; save(); } @@ -1021,6 +1029,12 @@ function toggleSectionHeight(cm) { } } +function setupAutocomplete(cm, enable = true) { + const onOff = enable ? 'on' : 'off'; + cm[onOff]('change', autocompleteOnTyping); + cm[onOff]('pick', autocompletePicked); +} + function autocompleteOnTyping(cm, info, debounced) { if ( cm.state.completionActive || @@ -1079,7 +1093,7 @@ function getEditorInSight(nearbyElement) { cm = editors.lastActive; } if (!cm || offscreenDistance(cm) > 0) { - const sorted = editors + const sorted = $$('#sections .CodeMirror').map(e => e.CodeMirror) .map((cm, index) => ({cm: cm, distance: offscreenDistance(cm), index: index})) .sort((a, b) => a.distance - b.distance || a.index - b.index); cm = sorted[0].cm; @@ -1120,7 +1134,7 @@ function beautify(event) { options.indent_char = tabs ? '\t' : ' '; const section = getSectionForChild(event.target); - const scope = section ? [section.CodeMirror] : editors; + const scope = section ? [section.CodeMirror] : $$('#sections .CodeMirror').map(e => e.CodeMirror); showHelp(t('styleBeautify'), '
' + optionHtml('.selector1,', 'selector_separator_newline') + @@ -1261,7 +1275,20 @@ function setStyleMeta(style) { $('#url').href = style.url; } -function initWithStyle({style, codeIsUpdated}) { +function initWithStyle({style}) { + // FIXME: what does codeIsUpdated do? + if (!style.usercss) { + return _initWithStyle({style}); + } + + if (editor) { + editor.replaceStyle(style); + } else { + editor = createSourceEditor(style); + } +} + +function _initWithStyle({style, codeIsUpdated}) { setStyleMeta(style); if (codeIsUpdated === false) { @@ -1440,6 +1467,13 @@ function updateLintReportIfEnabled(cm, time) { } function save() { + if (!editor) { + return _save(); + } + editor.save(); +} + +function _save() { updateLintReportIfEnabled(null, 0); // save the contents of the CodeMirror editors back into the textareas diff --git a/edit/lint.js b/edit/lint.js index e8cc1fc9..df103833 100644 --- a/edit/lint.js +++ b/edit/lint.js @@ -147,7 +147,7 @@ function updateLinter({immediately} = {}) { function updateEditors() { CodeMirror.defaults.lint = linterConfig.getForCodeMirror(linter); const guttersOption = prepareGuttersOption(); - editors.forEach(cm => { + $$('#sections .CodeMirror').map(e => e.CodeMirror).forEach(cm => { cm.setOption('lint', CodeMirror.defaults.lint); if (guttersOption) { cm.setOption('guttersOption', guttersOption); @@ -217,7 +217,7 @@ function updateLintReport(cm, delay) { state.postponeNewIssues = delay === undefined || delay === null; function update(cm) { - const scope = cm ? [cm] : editors; + const scope = cm ? [cm] : $$('#sections .CodeMirror').map(e => e.CodeMirror); let changed = false; let fixedOldIssues = false; scope.forEach(cm => { @@ -284,7 +284,7 @@ function renderLintReport(someBlockChanged) { const label = t('sectionCode'); const newContent = content.cloneNode(false); let issueCount = 0; - editors.forEach((cm, index) => { + $$('#sections .CodeMirror').map(e => e.CodeMirror).forEach((cm, index) => { if (cm.state.lint && cm.state.lint.html) { const html = '' + label + ' ' + (index + 1) + '' + cm.state.lint.html; const newBlock = newContent.appendChild(tHTML(html, 'table')); diff --git a/edit/source-editor.js b/edit/source-editor.js new file mode 100644 index 00000000..2fbc2188 --- /dev/null +++ b/edit/source-editor.js @@ -0,0 +1,133 @@ +/* global CodeMirror dirtyReporter initLint beautify showKeyMapHelp */ +/* global showToggleStyleHelp goBackToManage updateLintReportIfEnabled */ +/* global hotkeyRerouter setupAutocomplete */ + +'use strict'; + +function createSourceEditor(style) { + // draw HTML + $('#sections').innerHTML = ''; + $('#name').disabled = true; + $('#mozilla-format-heading').parentNode.remove(); + + $('#sections').appendChild(tHTML(` +
+ +
+ `)); + + // draw CodeMirror + $('#sections textarea').value = style.source; + const cm = CodeMirror.fromTextArea($('#sections textarea')); + + // dirty reporter + const dirty = dirtyReporter(); + dirty.onChange(() => { + const DIRTY = dirty.isDirty(); + document.title = (DIRTY ? '* ' : '') + t('editStyleTitle', [style.name]); + document.body.classList.toggle('dirty', DIRTY); + $('#save-button').disabled = !DIRTY; + }); + + // draw metas info + updateMetas(); + initHooks(); + initLint(); + + function initHooks() { + // sidebar commands + $('#save-button').onclick = save; + $('#beautify').onclick = beautify; + $('#keyMap-help').onclick = showKeyMapHelp; + $('#toggle-style-help').onclick = showToggleStyleHelp; + $('#cancel-button').onclick = goBackToManage; + + // enable + $('#enabled').onchange = e => { + const value = e.target.checked; + dirty.modify('enabled', style.enabled, value); + style.enabled = value; + }; + + // source + cm.on('change', () => { + const value = cm.getValue(); + dirty.modify('source', style.source, value); + style.source = value; + + updateLintReportIfEnabled(cm); + }); + + // hotkeyRerouter + cm.on('focus', () => { + hotkeyRerouter.setState(false); + }); + cm.on('blur', () => { + hotkeyRerouter.setState(true); + }); + + // autocomplete + if (prefs.get('editor.autocompleteOnTyping')) { + setupAutocomplete(cm); + } + } + + function updateMetas() { + $('#name').value = style.name; + $('#enabled').checked = style.enabled; + $('#url').href = style.url; + cm.setOption('mode', style.preprocessor || 'css'); + CodeMirror.autoLoadMode(cm, style.preprocessor || 'css'); + // beautify only works with regular CSS + $('#beautify').disabled = Boolean(style.preprocessor); + } + + function replaceStyle(_style) { + style = _style; + updateMetas(); + if (style.source !== cm.getValue()) { + const cursor = cm.getCursor(); + cm.setValue(style.source); + cm.setCursor(cursor); + } + dirty.clear(); + } + + function toggleStyle() { + const value = !style.enabled; + dirty.modify('enabled', style.enabled, value); + style.enabled = value; + updateMetas(); + // save when toggle enable state? + save(); + } + + function save() { + if (!dirty.isDirty()) { + return; + } + const req = { + method: 'saveUsercss', + reason: 'editSave', + id: style.id, + enabled: style.enabled, + source: style.source + }; + return onBackgroundReady().then(() => BG.saveUsercss(req)) + .then(result => { + if (result.status === 'error') { + throw new Error(result.error); + } + return result; + }) + .then(({style}) => { + replaceStyle(style); + }) + .catch(err => { + console.error(err); + alert(err); + }); + } + + return {replaceStyle, save, toggleStyle}; +} diff --git a/edit/util.js b/edit/util.js new file mode 100644 index 00000000..e76e289e --- /dev/null +++ b/edit/util.js @@ -0,0 +1,90 @@ +'use strict'; + +function dirtyReporter() { + const dirty = new Map(); + const onchanges = []; + + function add(obj, value) { + const saved = dirty.get(obj); + if (!saved) { + dirty.set(obj, {type: 'add', newValue: value}); + } else if (saved.type === 'remove') { + if (saved.savedValue === value) { + dirty.delete(obj); + } else { + saved.newValue = value; + saved.type = 'modify'; + } + } + } + + function remove(obj, value) { + const saved = dirty.get(obj); + if (!saved) { + dirty.set(obj, {type: 'remove', savedValue: value}); + } else if (saved.type === 'add') { + dirty.delete(obj); + } else if (saved.type === 'modify') { + saved.type = 'remove'; + } + } + + function modify(obj, oldValue, newValue) { + const saved = dirty.get(obj); + if (!saved) { + if (oldValue !== newValue) { + dirty.set(obj, {type: 'modify', savedValue: oldValue, newValue}); + } + } else if (saved.type === 'modify') { + if (saved.savedValue === newValue) { + dirty.delete(obj); + } else { + saved.newValue = newValue; + } + } else if (saved.type === 'add') { + saved.newValue = newValue; + } + } + + function clear() { + dirty.clear(); + } + + function isDirty() { + return dirty.size > 0; + } + + function onChange(cb) { + onchanges.push(cb); + } + + function wrap(obj) { + for (const key of ['add', 'remove', 'modify', 'clear']) { + obj[key] = trackChange(obj[key]); + } + return obj; + } + + function emitChange() { + for (const cb of onchanges) { + try { + cb(); + } catch (err) { + console.error(err); + } + } + } + + function trackChange(fn) { + return function () { + const dirty = isDirty(); + const result = fn.apply(null, arguments); + if (dirty !== isDirty()) { + emitChange(); + } + return result; + }; + } + + return wrap({add, remove, modify, clear, isDirty, onChange}); +} From f305719db3b12ee879302aeebc1f6d1dd5026d02 Mon Sep 17 00:00:00 2001 From: eight Date: Tue, 12 Sep 2017 01:23:32 +0800 Subject: [PATCH 028/250] Fix: update progress --- background/update.js | 56 ++++++++++++++++++++++++------------------- edit/source-editor.js | 1 + edit/util.js | 6 ++++- js/usercss.js | 1 + manage/updater-ui.js | 4 ++-- 5 files changed, 41 insertions(+), 27 deletions(-) diff --git a/background/update.js b/background/update.js index fcd238e6..eb08d128 100644 --- a/background/update.js +++ b/background/update.js @@ -19,10 +19,18 @@ var updater = { 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', + ERROR_VERSION: 'error: version is older than installed style', lastUpdateTime: parseInt(localStorage.lastUpdateTime) || Date.now(), + isSame(code) { + return code === updater.SAME_MD5 || code === updater.SAME_CODE || code === updater.SAME_VERSION; + }, + + isEdited(code) { + return code === updater.EDITED || code === updater.MAYBE_EDITED; + }, + checkAllStyles({observer = () => {}, save = true, ignoreDigest} = {}) { updater.resetInterval(); updater.checkAllStyles.running = true; @@ -72,11 +80,12 @@ var updater = { }); function checkIfEdited(digest) { - if (style.usercss) { - // FIXME: remove this after we can calculate digest from style.source + if (ignoreDigest) { return; } - if (!ignoreDigest && style.originalDigest && style.originalDigest !== digest) { + if (style.usercss && style.edited) { + return Promise.reject(updater.EDITED); + } else if (style.originalDigest && style.originalDigest !== digest) { return Promise.reject(updater.EDITED); } } @@ -97,34 +106,33 @@ var updater = { function maybeUpdateUsercss() { return download(style.updateUrl).then(text => { const json = usercss.buildMeta(text); - if (!json.version) { + // re-install is invalid in a soft upgrade + if (semverCompare(style.version, json.version) === 0 && !ignoreDigest) { + return Promise.reject(updater.SAME_VERSION); + } + // downgrade is always invalid + if (semverCompare(style.version, json.version) > 0) { return Promise.reject(updater.ERROR_VERSION); } - if (style.version) { - if (semverCompare(style.version, json.version) === 0) { - return Promise.reject(updater.SAME_VERSION); - } - if (semverCompare(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); - } json.id = style.id; - if (styleSectionsEqual(json, style)) { - // JSONs may have different order of items even if sections are effectively equal - // so we'll update the digest anyway - saveStyle(Object.assign(json, {reason: 'update-digest'})); - return Promise.reject(updater.SAME_CODE); - } else if (!style.originalDigest && !ignoreDigest) { - return Promise.reject(updater.MAYBE_EDITED); + // no need to compare section code for usercss, they are built dynamically + if (!json.usercss) { + if (!styleJSONseemsValid(json)) { + return Promise.reject(updater.ERROR_JSON); + } + if (styleSectionsEqual(json, style)) { + // JSONs may have different order of items even if sections are effectively equal + // so we'll update the digest anyway + saveStyle(Object.assign(json, {reason: 'update-digest'})); + return Promise.reject(updater.SAME_CODE); + } else if (!style.originalDigest && !ignoreDigest) { + return Promise.reject(updater.MAYBE_EDITED); + } } return !save ? json : saveStyle(Object.assign(json, { diff --git a/edit/source-editor.js b/edit/source-editor.js index 2fbc2188..77e4e478 100644 --- a/edit/source-editor.js +++ b/edit/source-editor.js @@ -111,6 +111,7 @@ function createSourceEditor(style) { reason: 'editSave', id: style.id, enabled: style.enabled, + edited: dirty.has('source'), source: style.source }; return onBackgroundReady().then(() => BG.saveUsercss(req)) diff --git a/edit/util.js b/edit/util.js index e76e289e..d5291fcf 100644 --- a/edit/util.js +++ b/edit/util.js @@ -86,5 +86,9 @@ function dirtyReporter() { }; } - return wrap({add, remove, modify, clear, isDirty, onChange}); + function has(key) { + return dirty.has(key); + } + + return wrap({add, remove, modify, clear, isDirty, onChange, has}); } diff --git a/js/usercss.js b/js/usercss.js index 5099e8b0..894a7219 100644 --- a/js/usercss.js +++ b/js/usercss.js @@ -173,6 +173,7 @@ var usercss = (function () { usercss: true, version: null, source: source, + edited: false, enabled: true, sections: [], vars: {}, diff --git a/manage/updater-ui.js b/manage/updater-ui.js index a17a170b..358d9a63 100644 --- a/manage/updater-ui.js +++ b/manage/updater-ui.js @@ -114,8 +114,8 @@ function reportUpdateState(state, style, details) { if (entry.classList.contains('can-update')) { break; } - const same = details === BG.updater.SAME_MD5 || details === BG.updater.SAME_CODE; - const edited = details === BG.updater.EDITED || details === BG.updater.MAYBE_EDITED; + const same = BG.updater.isSame(details); + const edited = BG.updater.isEdited(details); entry.dataset.details = details; if (!details) { details = t('updateCheckFailServerUnreachable'); From 381ee88e9498c3b1b13e3de68d6fee12a60d392b Mon Sep 17 00:00:00 2001 From: eight Date: Tue, 12 Sep 2017 01:48:10 +0800 Subject: [PATCH 029/250] Fix: i18n error message --- _locales/en/messages.json | 31 +++++++++++++++++++++++++++++++ js/usercss.js | 13 ++++++++----- 2 files changed, 39 insertions(+), 5 deletions(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 38202629..eb92a02c 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -673,6 +673,37 @@ } } }, + "styleMetaErrorCheckbox": { + "message": "Invalid @var checkbox: value must be 0 or 1", + "description": "Error displayed when the value of @var checkbox is invalid" + }, + "styleMetaErrorColor": { + "message": "$COLOR$ is not a valid color", + "description": "Error displayed when the value of @var color is invalid", + "placeholders": { + "color": { + "content": "$1" + } + } + }, + "styleMetaErrorSelectMissingKey": { + "message": "Invalid @var select: missing key '$KEY$'", + "description": "Error displayed when the value of @var select is not a valid key", + "placeholders": { + "key": { + "content": "$1" + } + } + }, + "styleMetaErrorSelect": { + "message": "Invalid @var select: $ERROR$", + "description": "Error displayed when the value of @var select is invalid", + "placeholders": { + "error": { + "content": "$1" + } + } + }, "styleMissingMeta": { "message": "Missing medata @$KEY$", "description": "Error displayed when a mandatory metadata is missing", diff --git a/js/usercss.js b/js/usercss.js index 894a7219..ae5d864c 100644 --- a/js/usercss.js +++ b/js/usercss.js @@ -59,7 +59,7 @@ var usercss = (function () { function parse(color) { el.style.color = color; if (el.style.color === '') { - throw new Error(`"${color}" is not a valid color`); + throw new Error(chrome.i18n.getMessage('styleMetaErrorColor', color)); } color = getComputedStyle(el).color; el.style.color = ''; @@ -158,7 +158,11 @@ var usercss = (function () { // select type has an additional field if (result.type === 'select') { const match = matchString(source); - result.select = JSON.parse(match.follow); + try { + result.select = JSON.parse(match.follow); + } catch (err) { + throw new Error(chrome.i18n.getMessage('styleMetaErrorSelect', err.message)); + } source = match.value; } @@ -266,11 +270,10 @@ var usercss = (function () { } function validVar(va, value = 'default') { - // FIXME: i18n if (va.type === 'select' && !va.select[va[value]]) { - throw new Error(`Invalid @var select: missing key '${va[value]}'`); + throw new Error(chrome.i18n.getMessage('styleMetaErrorSelectMissingKey', va[value])); } else if (va.type === 'checkbox' && !/^[01]$/.test(va[value])) { - throw new Error('Invalid @var checkbox: value must be 0 or 1'); + throw new Error(chrome.i18n.getMessage('styleMetaErrorCheckbox')); } else if (va.type === 'color') { va[value] = colorParser.format(colorParser.parse(va[value])); } From 20481c9180ac9dbff52567a8cbfeebb4f1ce77f1 Mon Sep 17 00:00:00 2001 From: eight Date: Tue, 12 Sep 2017 01:59:56 +0800 Subject: [PATCH 030/250] Fix: i18n config dialog --- _locales/en/messages.json | 4 ++++ manage/config-dialog.js | 10 ++++++++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index eb92a02c..8c297dbc 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -7,6 +7,10 @@ "message": "Add Style", "description": "Title of the page for adding styles" }, + "alphaChannel": { + "message": "Alpha channel", + "description": "Label of alpha channel input" + }, "appliesAdd": { "message": "Add", "description": "Label for the button to add an 'applies' entry" diff --git a/manage/config-dialog.js b/manage/config-dialog.js index 6386629c..82e0a38f 100644 --- a/manage/config-dialog.js +++ b/manage/config-dialog.js @@ -33,8 +33,14 @@ function configDialog(style) { let appendChild; if (va.type === 'color') { va.inputColor = $element({tag: 'input', type: 'color'}); - // FIXME: i18n - va.inputAlpha = $element({tag: 'input', type: 'range', min: 0, max: 1, title: 'Opacity', step: 'any'}); + va.inputAlpha = $element({ + tag: 'input', + type: 'range', + min: 0, + max: 1, + title: chrome.i18n.getMessage('alphaChannel'), + step: 'any' + }); va.inputColor.onchange = va.inputAlpha.oninput = () => { va.dirty = true; const color = colorParser.parse(va.inputColor.value); From 3730a4e48314ca9d9eb603f36d995e57ef88f4e6 Mon Sep 17 00:00:00 2001 From: eight Date: Tue, 12 Sep 2017 02:32:27 +0800 Subject: [PATCH 031/250] Fix: i18n and escapeHtml, url --- _locales/en/messages.json | 40 ++++++++++++++++++++++++++++ content/install-user-css.js | 53 +++++++++++++++++-------------------- edit/lint.js | 10 +------ js/dom.js | 11 ++++++++ 4 files changed, 77 insertions(+), 37 deletions(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 8c297dbc..03207d6b 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -68,6 +68,10 @@ "message": "Apply all updates", "description": "Label for the button to apply all detected updates" }, + "author": { + "message": "Author", + "description": "Label for the style author" + }, "backupButtons": { "message": "Backup", "description": "Heading for backup" @@ -277,6 +281,14 @@ "message": "Export", "description": "Label for the button to export a style ('edit' page) or all styles ('manage' page)" }, + "externalHomepage": { + "message": "Homepage", + "description": "Label for the external link to style's homepage" + }, + "externalSupport": { + "message": "Support", + "description": "Label for the external link to style's support site" + }, "filteredStyles": { "message": "$numShown$ shown of $numTotal$ total", "description": "TL note - make this message short", @@ -365,10 +377,26 @@ "message": "Discard contents of current style and overwrite it with the imported style", "description": "Label for the button to import and overwrite current style" }, + "installButton": { + "message": "Install", + "description": "Label for install button" + }, + "installButtonUpdate": { + "message": "Update", + "description": "Label for update button" + }, + "installButtonReinstall": { + "message": "Reinstall", + "description": "Label for reinstall button" + }, "installUpdate": { "message": "Install update", "description": "Label for the button to install an update for a single style" }, + "license": { + "message": "License", + "description": "Label for the license" + }, "linterConfigPopupTitle": { "message": "Set $linter$ rules configuration", "description": "Stylelint or CSSLint popup header", @@ -415,6 +443,10 @@ "message": "See a full list of rules", "description": "Stylelint or CSSLint rules label added immediately before a link" }, + "liveReloadLabel": { + "message": "Live reload", + "description": "The label of live-reload feature" + }, "manageFilters": { "message": "Filters", "description": "Label for filters container" @@ -503,6 +535,10 @@ "message": "More Options", "description": "Subheading for options section on manage page." }, + "parseUsercssError": { + "message": "Stylus failed to parse usercss:", + "description": "The error message to show when stylus failed to parse usercss" + }, "popupManageTooltip": { "message": "Shift-click or right-click opens manager with styles applicable for current site", "description": "Tooltip for the 'Manage' button in the popup." @@ -831,6 +867,10 @@ "message": "Updates installed:", "description": "Text that displays when an update is installed on options page. Followed by the number of currently installed updates." }, + "versionInvalidOlder": { + "message": "The version is older then installed style.", + "description": "Displayed when the version of style is older then installed one" + }, "writeStyleFor": { "message": "Write style for: ", "description": "Label for toolbar pop-up that precedes the links to write a new style" diff --git a/content/install-user-css.js b/content/install-user-css.js index 8dce7539..43b13eef 100644 --- a/content/install-user-css.js +++ b/content/install-user-css.js @@ -1,4 +1,4 @@ -/* global semverCompare */ +/* global semverCompare escapeHtml */ 'use strict'; @@ -47,7 +47,7 @@ function getAppliesTo(style) { } const result = [..._gen()]; if (!result.length) { - result.push('All URLs'); + result.push(chrome.i18n.getMessage('appliesToEverything')); } return result; } @@ -56,26 +56,31 @@ function initInstallPage({style, dup}, sourceLoader) { return pendingResource.then(() => { const versionTest = dup && semverCompare(style.version, dup.version); document.body.innerHTML = ''; - // FIXME: i18n document.body.appendChild(tHTML(`
-

${style.name} v${style.version}

-

${style.description}

-

Author

- ${style.author} -

License

- ${style.license} -

Applies to

+

+ ${escapeHtml(style.name)} + v${escapeHtml(style.version)} +

+

${escapeHtml(style.description)}

+

+ ${escapeHtml(style.author)} +

+ ${escapeHtml(style.license)} +

    - ${getAppliesTo(style).map(s => `
  • ${s}
  • `)} + ${getAppliesTo(style).map(s => `
  • ${escapeHtml(s)}
  • `)}
- +
- Homepage - Support + +
@@ -84,11 +89,8 @@ function initInstallPage({style, dup}, sourceLoader) {
`)); if (versionTest < 0) { - // FIXME: i18n $('.actions').before(tHTML(` -
- The version is older then installed style. -
+
`)); } $('.code').textContent = style.source; @@ -120,11 +122,9 @@ function initLiveReload(sourceLoader) { $$('.main .warning').forEach(e => e.remove()); }).catch(err => { const oldWarning = $('.main .warning'); - // FIXME: i18n const warning = tHTML(` -
- Stylus failed to parse usercss: -
${err}
+
+
${escapeHtml(err)}
`); if (oldWarning) { @@ -140,11 +140,10 @@ function initLiveReload(sourceLoader) { watcher.start(); } }); - // FIXME: i18n $('.actions').append(tHTML(` `)); $('.live-reload-checkbox').onchange = e => { @@ -162,11 +161,9 @@ function initLiveReload(sourceLoader) { function initErrorPage(err, source) { return pendingResource.then(() => { document.body.innerHTML = ''; - // FIXME: i18n document.body.appendChild(tHTML(` -
- Stylus failed to parse usercss: -
${err}
+
+
${escapeHtml(err)}
`)); diff --git a/edit/lint.js b/edit/lint.js index df103833..3ec7282c 100644 --- a/edit/lint.js +++ b/edit/lint.js @@ -1,6 +1,7 @@ /* global CodeMirror messageBox */ /* global editors makeSectionVisible showCodeMirrorPopup showHelp */ /* global onDOMscripted injectCSS require CSSLint stylelint */ +/* global escapeHtml */ 'use strict'; loadLinterAssets(); @@ -267,15 +268,6 @@ function updateLintReport(cm, delay) { } } } - function escapeHtml(html, {limit} = {}) { - const chars = {'&': '&', '<': '<', '>': '>', '"': '"', "'": ''', '/': '/'}; - let ellipsis = ''; - if (limit && html.length > limit) { - html = html.substr(0, limit); - ellipsis = '...'; - } - return html.replace(/[&<>"'/]/g, char => chars[char]) + ellipsis; - } } function renderLintReport(someBlockChanged) { diff --git a/js/dom.js b/js/dom.js index 091f58be..5c1d5897 100644 --- a/js/dom.js +++ b/js/dom.js @@ -286,3 +286,14 @@ function dieOnDysfunction() { } }); } + + +function escapeHtml(html, {limit} = {}) { + const chars = {'&': '&', '<': '<', '>': '>', '"': '"', "'": ''', '/': '/'}; + let ellipsis = ''; + if (limit && html.length > limit) { + html = html.substr(0, limit); + ellipsis = '...'; + } + return html.replace(/[&<>"'/]/g, char => chars[char]) + ellipsis; +} From 41f0174362ad770963836ef797325ef41f773ee3 Mon Sep 17 00:00:00 2001 From: eight Date: Tue, 12 Sep 2017 02:46:37 +0800 Subject: [PATCH 032/250] Add: valid url --- js/usercss.js | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/js/usercss.js b/js/usercss.js index ae5d864c..7440044a 100644 --- a/js/usercss.js +++ b/js/usercss.js @@ -263,12 +263,26 @@ var usercss = (function () { // validate version semverCompare(style.version, '0.0.0'); + // validate URLs + validUrl(style.url); + validUrl(style.supportURL); + // validate vars for (const key of Object.keys(style.vars)) { validVar(style.vars[key]); } } + function validUrl(url) { + if (!url) { + return; + } + url = new URL(url); + if (url.protocol !== 'http:' && url.protocol !== 'https:') { + throw new Error(`${url.protocol} is not a valid protocol`); + } + } + function validVar(va, value = 'default') { if (va.type === 'select' && !va.select[va[value]]) { throw new Error(chrome.i18n.getMessage('styleMetaErrorSelectMissingKey', va[value])); From de84248e05e363afd2cdbd28a928afe1610526f4 Mon Sep 17 00:00:00 2001 From: eight Date: Tue, 12 Sep 2017 03:44:19 +0800 Subject: [PATCH 033/250] Fix: add editors hack --- edit/source-editor.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/edit/source-editor.js b/edit/source-editor.js index 77e4e478..49299bc7 100644 --- a/edit/source-editor.js +++ b/edit/source-editor.js @@ -19,6 +19,8 @@ function createSourceEditor(style) { // draw CodeMirror $('#sections textarea').value = style.source; const cm = CodeMirror.fromTextArea($('#sections textarea')); + // too many functions depend on this global + editors.push(cm); // dirty reporter const dirty = dirtyReporter(); From e2ea93a3c75f3174ee3b14562ea9aca17fceef4c Mon Sep 17 00:00:00 2001 From: eight Date: Tue, 12 Sep 2017 03:56:05 +0800 Subject: [PATCH 034/250] Fix: decodeURI -> encodeURI --- content/install-user-css.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/content/install-user-css.js b/content/install-user-css.js index 43b13eef..63eeba92 100644 --- a/content/install-user-css.js +++ b/content/install-user-css.js @@ -79,8 +79,8 @@ function initInstallPage({style, dup}, sourceLoader) { )}
- - + +
From 5fecd7e91bcf498585ea84926182820b38ee89d9 Mon Sep 17 00:00:00 2001 From: eight Date: Tue, 12 Sep 2017 04:10:20 +0800 Subject: [PATCH 035/250] Drop .before, .after, .prepend, .append --- content/install-user-css.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/content/install-user-css.js b/content/install-user-css.js index 63eeba92..512c8785 100644 --- a/content/install-user-css.js +++ b/content/install-user-css.js @@ -89,9 +89,9 @@ function initInstallPage({style, dup}, sourceLoader) {
`)); if (versionTest < 0) { - $('.actions').before(tHTML(` + $('.actions').parentNode.insertBefore(tHTML(`
- `)); + `), $('.actions')); } $('.code').textContent = style.source; $('button.install').onclick = () => { @@ -130,7 +130,7 @@ function initLiveReload(sourceLoader) { if (oldWarning) { oldWarning.replaceWith(warning); } else { - $('.main').prepend(warning); + $('.main').insertBefore(warning, $('.main').childNodes[0]); } }); }); @@ -140,7 +140,7 @@ function initLiveReload(sourceLoader) { watcher.start(); } }); - $('.actions').append(tHTML(` + $('.actions').appendChild(tHTML(`
+ \ No newline at end of file diff --git a/install-usercss/install-usercss.js b/install-usercss/install-usercss.js index 7a2862fc..0a8846d4 100644 --- a/install-usercss/install-usercss.js +++ b/install-usercss/install-usercss.js @@ -1,4 +1,4 @@ -/* global CodeMirror semverCompare makeLink closeCurrentTab */ +/* global CodeMirror semverCompare makeLink closeCurrentTab runtimeSend */ 'use strict'; (() => { @@ -153,15 +153,6 @@ main.insertBefore(buildWarning(err), main.firstChild); } - function runtimeSend(request) { - return new Promise((resolve, reject) => { - chrome.runtime.sendMessage( - request, - ({status, result}) => (status === 'error' ? reject : resolve)(result) - ); - }); - } - function install(style) { const request = Object.assign(style, { method: 'saveUsercss', diff --git a/manifest.json b/manifest.json index ffc059b5..4d567ffb 100644 --- a/manifest.json +++ b/manifest.json @@ -59,7 +59,7 @@ "include_globs": ["*.user.css", "*.user.styl"], "run_at": "document_idle", "all_frames": false, - "js": ["content/install-user-css.js"] + "js": ["content/util.js", "content/install-user-css.js"] } ], "browser_action": { From 398056c2622394a6ed518e362bfb09cc3e4be275 Mon Sep 17 00:00:00 2001 From: eight Date: Wed, 1 Nov 2017 08:37:18 +0800 Subject: [PATCH 172/250] Fix: _source -> newSource --- content/install-user-css.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/content/install-user-css.js b/content/install-user-css.js index 8e4c61fc..8aebcfc1 100644 --- a/content/install-user-css.js +++ b/content/install-user-css.js @@ -17,8 +17,8 @@ function createSourceLoader() { function load() { return fetchText(location.href) - .then(_source => { - source = _source; + .then(newSource => { + source = newSource; return source; }); } @@ -41,9 +41,9 @@ function createSourceLoader() { function check() { fetchText(location.href) - .then(_source => { - if (source !== _source) { - source = _source; + .then(newSource => { + if (source !== newSource) { + source = newSource; return cb(source); } }) From 13ca45a104290e8118ccd5fb79b813b5d4c862c4 Mon Sep 17 00:00:00 2001 From: eight Date: Wed, 1 Nov 2017 08:40:42 +0800 Subject: [PATCH 173/250] Fix: reorder global comment --- edit.html | 4 ++-- {vendor-overwrites/codemirror => edit}/codemirror-default.css | 0 {vendor-overwrites/codemirror => edit}/codemirror-default.js | 0 edit/edit.js | 3 ++- install-usercss.html | 4 ++-- 5 files changed, 6 insertions(+), 5 deletions(-) rename {vendor-overwrites/codemirror => edit}/codemirror-default.css (100%) rename {vendor-overwrites/codemirror => edit}/codemirror-default.js (100%) diff --git a/edit.html b/edit.html index 88c3ded6..6d92f359 100644 --- a/edit.html +++ b/edit.html @@ -61,8 +61,8 @@ - - + +