From 65ac351699d42e509af9c48c7440b87ed622b214 Mon Sep 17 00:00:00 2001 From: tophf Date: Fri, 5 Mar 2021 17:25:05 +0300 Subject: [PATCH] preserve opacity in colorpicker for preprocessor uso config (#1200) USO has always produced 6-digit #rrggbb so some styles rely on it and use /*[[color]]*/11 notation to specify the transparency. Now we will try to preserve the opacity customized by the user via colorpicker unless the style specifies it inline. --- background/style-manager.js | 14 ++++---- js/color/color-converter.js | 32 +++++++++-------- js/usercss-compiler.js | 71 ++++++++++++++++++------------------- manage/import-export.js | 2 +- 4 files changed, 59 insertions(+), 60 deletions(-) diff --git a/background/style-manager.js b/background/style-manager.js index 81898e82..ed074854 100644 --- a/background/style-manager.js +++ b/background/style-manager.js @@ -5,6 +5,7 @@ /* global db */ /* global prefs */ /* global tabMan */ +/* global usercssMan */ 'use strict'; /* @@ -202,7 +203,12 @@ const styleMan = (() => { /** @returns {Promise} */ async importMany(items) { if (ready.then) await ready; - items.forEach(beforeSave); + for (const style of items) { + beforeSave(style); + if (style.sourceCode && style.usercssData) { + await usercssMan.buildCode(style); + } + } const events = await db.exec('putMany', items); return Promise.all(items.map((item, i) => { afterSave(item, events[i]); @@ -210,12 +216,6 @@ const styleMan = (() => { })); }, - /** @returns {Promise} */ - async import(data) { - if (ready.then) await ready; - return handleSave(await saveStyle(data), 'import'); - }, - /** @returns {Promise} */ async install(style, reason = null) { if (ready.then) await ready; diff --git a/js/color/color-converter.js b/js/color/color-converter.js index 2889ef44..38dae511 100644 --- a/js/color/color-converter.js +++ b/js/color/color-converter.js @@ -16,29 +16,27 @@ const colorConverter = (() => { // NAMED_COLORS is added below }; - function format(color = '', type = color.type, hexUppercase) { + function format(color = '', type = color.type, hexUppercase, usoMode) { if (!color || !type) return typeof color === 'string' ? color : ''; - const a = formatAlpha(color.a); - const hasA = Boolean(a); - if (type === 'rgb' && color.type === 'hsl') { + const {a} = color; + let aStr = formatAlpha(a); + if (aStr) aStr = ', ' + aStr; + if (type !== 'hsl' && color.type === 'hsl') { color = HSVtoRGB(HSLtoHSV(color)); } const {r, g, b, h, s, l} = color; switch (type) { case 'hex': { - const rgbStr = (0x1000000 + (r << 16) + (g << 8) + (b | 0)).toString(16).slice(1); - const aStr = hasA ? (0x100 + Math.round(a * 255)).toString(16).slice(1) : ''; - const hexStr = `#${rgbStr + aStr}`.replace(/^#(.)\1(.)\2(.)\3(?:(.)\4)?$/, '#$1$2$3$4'); - return hexUppercase ? hexStr.toUpperCase() : hexStr.toLowerCase(); + let res = '#' + hex2(r) + hex2(g) + hex2(b) + (aStr ? hex2(Math.round(a * 255)) : ''); + if (!usoMode) res = res.replace(/^#(.)\1(.)\2(.)\3(?:(.)\4)?$/, '#$1$2$3$4'); + return hexUppercase ? res.toUpperCase() : res; + } + case 'rgb': { + const rgb = [r, g, b].map(Math.round).join(', '); + return usoMode ? rgb : `rgb${aStr ? 'a' : ''}(${rgb}${aStr})`; } - case 'rgb': - return hasA ? - `rgba(${Math.round(r)}, ${Math.round(g)}, ${Math.round(b)}, ${a})` : - `rgb(${Math.round(r)}, ${Math.round(g)}, ${Math.round(b)})`; case 'hsl': - return hasA ? - `hsla(${h}, ${s}%, ${l}%, ${a})` : - `hsl(${h}, ${s}%, ${l}%)`; + return `hsl${aStr ? 'a' : ''}(${h}, ${s}%, ${l}%${aStr})`; } } @@ -215,6 +213,10 @@ const colorConverter = (() => { const int = Math.round(num); return Math.abs(int - num) < 1e-3 ? int : num; } + + function hex2(val) { + return (val < 16 ? '0' : '') + (val >> 0).toString(16); + } })(); colorConverter.NAMED_COLORS = new Map([ diff --git a/js/usercss-compiler.js b/js/usercss-compiler.js index ca03e83a..ae9c2c78 100644 --- a/js/usercss-compiler.js +++ b/js/usercss-compiler.js @@ -47,53 +47,50 @@ const BUILDERS = Object.assign(Object.create(null), { uso: { pre(source, vars) { require(['/js/color/color-converter']); /* global colorConverter */ - const pool = new Map(); + const pool = Object.create(null); return doReplace(source); - function getValue(name, rgbName) { - if (!vars.hasOwnProperty(name)) { - if (name.endsWith('-rgb')) { - return getValue(name.slice(0, -4), name); + function doReplace(text) { + return text.replace(/(\/\*\[\[([\w-]+)]]\*\/)([0-9a-f]{2}(?=\W))?/gi, (_, cmt, name, alpha) => { + const key = alpha ? name + '[A]' : name; + let val = pool[key]; + if (val === undefined) { + val = pool[key] = getValue(name, null, alpha); } - return null; + return (val != null ? val : cmt) + (alpha || ''); + }); + } + + function getValue(name, isUsoRgb, alpha) { + const v = vars[name]; + if (!v) { + return name.endsWith('-rgb') + ? getValue(name.slice(0, -4), true) + : null; } - const {type, value} = vars[name]; - switch (type) { - case 'color': { - let color = pool.get(rgbName || name); - if (color == null) { - color = colorConverter.parse(value); - if (color) { - if (color.type === 'hsl') { - color = colorConverter.HSVtoRGB(colorConverter.HSLtoHSV(color)); - } - const {r, g, b} = color; - color = rgbName - ? `${r}, ${g}, ${b}` - : `#${(0x1000000 + (r << 16) + (g << 8) + b).toString(16).slice(1)}`; - } - // the pool stores `false` for bad colors to differentiate from a yet unknown color - pool.set(rgbName || name, color || false); + let {value} = v; + switch (v.type) { + case 'color': + value = colorConverter.parse(value) || null; + if (value) { + /* #rrggbb - inline alpha is present; an opaque hsl/a; #rrggbb originally + * rgba(r, g, b, a) - transparency <1 is present (Chrome pre-66 compatibility) + * rgb(r, g, b) - if color is rgb/a with a=1, note: r/g/b will be rounded + * r, g, b - if the var has `-rgb` suffix per USO specification + * TODO: when minimum_chrome_version >= 66 try to keep `value` intact */ + if (alpha) delete value.a; + const isRgb = isUsoRgb || value.type === 'rgb' || value.a != null && value.a !== 1; + const usoMode = isUsoRgb || !isRgb; + value = colorConverter.format(value, isRgb ? 'rgb' : 'hex', undefined, usoMode); } - return color || null; - } + return value; case 'dropdown': - case 'select': // prevent infinite recursion - pool.set(name, ''); + case 'select': + pool[name] = ''; // prevent infinite recursion return doReplace(value); } return value; } - - function doReplace(text) { - return text.replace(/\/\*\[\[([\w-]+)\]\]\*\//g, (match, name) => { - if (!pool.has(name)) { - const value = getValue(name); - pool.set(name, value === null ? match : value); - } - return pool.get(name); - }); - } }, }, }); diff --git a/manage/import-export.js b/manage/import-export.js index 146b82a3..a3db48b9 100644 --- a/manage/import-export.js +++ b/manage/import-export.js @@ -278,7 +278,7 @@ async function importFromString(jsonString) { tasks = tasks.then(() => API.styles.delete(id)); const oldStyle = oldStylesById.get(id); if (oldStyle) { - tasks = tasks.then(() => API.styles.import(oldStyle)); + tasks = tasks.then(() => API.styles.importMany([oldStyle])); } } // taskUI is superfast and updates style list only in this page,