Skip to content

Commit

Permalink
Improved colopocalypse that uses jscodeshift
Browse files Browse the repository at this point in the history
  • Loading branch information
tlrobinson committed Jul 3, 2018
1 parent 4087cb0 commit f629500
Show file tree
Hide file tree
Showing 4 changed files with 441 additions and 69 deletions.
258 changes: 195 additions & 63 deletions bin/colopocalypse
Original file line number Diff line number Diff line change
Expand Up @@ -6,56 +6,75 @@ const path = require("path");
const Color = require("color");
const colorDiff = require("color-diff");
const _ = require("underscore");
const j = require("jscodeshift");

const { replaceStrings } = require("./lib/codemod");

const POSTCSS_CONFIG = require("../postcss.config.js");
const cssVariables =
POSTCSS_CONFIG.plugins["postcss-cssnext"].features.customProperties.variables;
// console.log(cssVariables);

// these are a bit liberal regexes but that's probably ok
const COLOR_REGEX = /(?:#[a-fA-F0-9]{3}(?:[a-fA-F0-9]{3})?|(?:rgb|hsl)a?\(\s*\d+\s*(?:,\s*\d+(?:\.\d+)?%?\s*){2,3}\))/g;
const COLOR_REGEX_WITH_LINE = /(?:#[a-fA-F0-9]{3}(?:[a-fA-F0-9]{3})?|(?:rgb|hsl)a?\(\s*\d+\s*(?:,\s*\d+(?:\.\d+)?%?\s*){2,3}\)).*/g;
const COLOR_REGEX = /(?:#[a-fA-F0-9]{3}(?:[a-fA-F0-9]{3})?\b|(?:rgb|hsl)a?\(\s*\d+\s*(?:,\s*\d+(?:\.\d+)?%?\s*){2,3}\))/g;
const COLOR_REGEX_WITH_LINE = /(?:#[a-fA-F0-9]{3}(?:[a-fA-F0-9]{3})?\b|(?:rgb|hsl)a?\(\s*\d+\s*(?:,\s*\d+(?:\.\d+)?%?\s*){2,3}\)).*/g;

const CSS_SIMPLE_VAR_REGEX = /^var\(([^)]+)\)$/;
const CSS_COLOR_VAR_REGEX = /^color\(var\(([^)]+)\) shade\(([^)]+)\)\)$/;
const CSS_VAR_REGEX = /var\([^)]+\)|color\(var\([^)]+\) shade\([^)]+\)\)/g;

const FILE_GLOB = process.argv[2] || "frontend/**/*.{css,js,jsx}";
const FILE_GLOB = "frontend/**/*.{css,js,jsx}";
const FILE_GLOB_IGNORE = [
// "**/metabase/lib/colors.js"
// // recast messes up these file and they don't have any colors so just ignore them:
// "**/metabase/query_builder/components/FieldList.jsx",
// "**/metabase/query_builder/components/filters/FilterPopover.jsx",
// "**/metabase/visualizations/components/TableInteractive.jsx",
];

const COLORS_CSS_PATH = "frontend/src/metabase/css/core/colors.css";
const COLORS_JS_PATH = "frontend/src/metabase/lib/colors.js";

const varForName = name => `--color-${name}`;

const colors = {
// themeable colors

brand: "#509EE3",

accent1: "#9CC177",
accent2: "#A989C5",
accent3: "#EF8C8C",
accent4: "#F9D45C",

accent5: "#F1B556",
accent6: "#A6E7F3",
accent7: "#7172AD",

// general purpose

white: "#FFFFFF",
black: "#2E353B",

"text-dark": "#2E353B", // same as "black"
// semantic colors

success: "#84BB4C",
error: "#ED6E6E",
warning: "#F9CF48",

"text-dark": "#2E353B", // "black"
"text-medium": "#93A1AB",
"text-light": "#DCE1E4",
"text-white": "#FFFFFF", // "white"

"bg-black": "#2E353B", // "black"
"bg-dark": "#93A1AB",
"bg-medium": "#EDF2F5",
"bg-light": "#F9FBFC",
"bg-white": "#FFFFFF", // "white"

shadow: "#F4F5F6",
border: "#D7DBDE",

success: "#84BB4C",
error: "#ED6E6E",
warning: "#F9CF48",
};

function paletteForColors(colors) {
Expand All @@ -71,35 +90,77 @@ function paletteForColors(colors) {
});
}

const TEXT_COLOR_NAMES = ["text-dark", "text-medium", "text-light"];
const BACKGROUND_COLOR_NAMES = ["bg-dark", "bg-medium", "bg-light"];
const PRIMARY_AND_SECONDARY_NAMES = [
"brand",
"accent1",
"accent2",
"accent3",
"accent4",
];
const TEXT_COLOR_NAMES = [
"text-dark",
"text-medium",
"text-light",
"text-white",
];
const BACKGROUND_COLOR_NAMES = [
"bg-black",
"bg-dark",
"bg-medium",
"bg-light",
"bg-white",
];
const SEMANTIC_NAMES = ["success", "error", "warning"];

const PALETTE_ALL = paletteForColors(_.omit(colors, "black"));
const PALETTE_FOREGROUND = paletteForColors(
_.omit(colors, ...BACKGROUND_COLOR_NAMES, "black", "shadow", "border"),
_.pick(
colors,
...TEXT_COLOR_NAMES,
...PRIMARY_AND_SECONDARY_NAMES,
...SEMANTIC_NAMES,
),
);
const PALETTE_BACKGROUND = paletteForColors(
_.omit(colors, ...TEXT_COLOR_NAMES, "shadow", "border"),
_.pick(
colors,
...BACKGROUND_COLOR_NAMES,
...PRIMARY_AND_SECONDARY_NAMES,
...SEMANTIC_NAMES,
),
);
const PALETTE_BORDER = paletteForColors(_.pick(colors, "border"));
const PALETTE_SHADOW = paletteForColors(_.pick(colors, "shadow"));

// basically everything except border/shadow
const PALETTE_OTHER = paletteForColors(
_.pick(
colors,
...TEXT_COLOR_NAMES,
...BACKGROUND_COLOR_NAMES,
...PRIMARY_AND_SECONDARY_NAMES,
...SEMANTIC_NAMES,
),
);

function paletteForCSSProperty(property) {
if (!property) {
return PALETTE_ALL;
} else if (property === "color" || ~property.indexOf("text")) {
return PALETTE_FOREGROUND;
} else if (~property.indexOf("bg") || ~property.indexOf("background")) {
return PALETTE_BACKGROUND;
} else if (~property.indexOf("border")) {
return PALETTE_BORDER;
} else if (~property.indexOf("shadow")) {
return PALETTE_SHADOW;
if (property) {
if (property === "color" || /text|font/i.test(property)) {
return PALETTE_FOREGROUND;
} else if (/bg|background/i.test(property)) {
return PALETTE_BACKGROUND;
} else if (/border/i.test(property)) {
return PALETTE_BORDER;
} else if (/shadow/i.test(property)) {
return PALETTE_SHADOW;
}
}
if (property != undefined) {
console.log("unknown pallet for property", property);
}
return PALETTE_ALL;
return PALETTE_OTHER;
}

function getBestCandidate(color, palette = PALETTE_ALL) {
function getBestCandidate(color, palette) {
const closest = colorDiff.closest(
{ R: color.red(), G: color.green(), B: color.blue() },
palette,
Expand Down Expand Up @@ -140,10 +201,26 @@ function lineAtIndex(lines, index) {
}
}

function lineUpToIndex(lines, index) {
let charIndex = 0;
for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) {
const lineStart = charIndex;
charIndex += lines[lineIndex].length + 1;
if (charIndex >= index) {
return lines[lineIndex].slice(0, index - lineStart);
}
}
}

function cssPropertyAtIndex(lines, index) {
const line = lineAtIndex(lines, index);
const prefix = lineUpToIndex(lines, index);
if (line) {
const match = line.match(/^\s*([a-z0-9-]+):/);
const match =
// matches property names at the beginning of the line
line.match(/^\s*([a-zA-Z0-9-]+):/) ||
// matches property names leading up to the rule value
prefix.match(/(^|[^a-zA-Z0-9-])([a-zA-Z0-9-]+)\s*:\s*"?$/);
if (match) {
return match[1].trim();
} else {
Expand All @@ -154,21 +231,38 @@ function cssPropertyAtIndex(lines, index) {
}
}

function replaceSimpleColorValues(content, isCSS) {
function replaceCSSColorValues(content) {
const lines = content.split("\n");
return content.replace(COLOR_REGEX, (color, index) => {
if (!isCSS) {
const [newColorName, newColor] = getBestCandidate(Color(color));
return toJSValue(newColorName, newColor);
} else {
const palette = paletteForCSSProperty(cssPropertyAtIndex(lines, index));
const [newColorName, newColor] = getBestCandidate(Color(color), palette);
return toCSSValue(newColorName, newColor);
}
const palette = paletteForCSSProperty(cssPropertyAtIndex(lines, index));
const [newColorName, newColor] = getBestCandidate(Color(color), palette);
return toCSSValue(newColorName, newColor);
});
}

function replaceCSSVariables(content) {
function replaceJSColorValues(content) {
if (COLOR_REGEX.test(content)) {
// console.log("processing");
return replaceStrings(content, COLOR_REGEX, (value, propertyName) => {
const palette = paletteForCSSProperty(propertyName);
const [newColorName, newColor] = getBestCandidate(Color(value), palette);
// console.log(value, propertyName, "=>", newColorName);
// return j.identifier(newColorName.replace(/\W/g, "_"));
// return j.stringLiteral(toJSValue(newColorName, newColor));
return j.memberExpression(
j.identifier("colors"),
/\W/.test(newColorName)
? j.literal(newColorName)
: j.identifier(newColorName),
);
});
} else {
// console.log("skipping");
return content;
}
}

function replaceCSSColorVariables(content) {
const lines = content.split("\n");
return content.replace(CSS_VAR_REGEX, (variable, index) => {
const color = resolveCSSVariableColor(variable);
Expand Down Expand Up @@ -212,41 +306,79 @@ function resolveCSSVariableColor(value) {

function processFiles(files) {
for (const file of files) {
const isCSS = /\.css/.test(file);

let content = fs.readFileSync(file, "utf-8");
content = replaceSimpleColorValues(content, isCSS);
if (isCSS) {
content = replaceCSSVariables(content);
try {
if (/\.css/.test(file)) {
content = replaceCSSColorVariables(replaceCSSColorValues(content));
} else if (/\.jsx?/.test(file)) {
let newContent = replaceJSColorValues(content);
if (newContent !== content && !/\/colors.js/.test(file)) {
newContent = ensureHasColorsImport(newContent);
}
content = newContent;
} else {
console.warn("unknown file type", file);
}
fs.writeFileSync(file, content);
} catch (e) {
console.log("failed to process", file, e);
}
fs.writeFileSync(file, content);
}

// do this last so we don't replace them
prependColorVarsBlock();
prependCSSVariablesBlock();
prependJSVariablesBlock();
}

function prependColorVarsBlock() {
const colorsVarsBlock =
`:root {\n` +
Object.entries(colors)
.map(([name, color]) => ` ${varForName(name)}: ${color};`)
.join("\n") +
`\n}\n\n`;
function ensureHasColorsImport(content) {
// TODO: implement
return content;
}

function prependCSSVariablesBlock() {
const colorsVarsBlock = `
/* NOTE: DO NOT ADD COLORS WITHOUT EXTREMELY GOOD REASON AND DESIGN REVIEW
* NOTE: KEEP SYNCRONIZED WITH COLORS.JS
*/
:root {
${Object.entries(colors)
.map(([name, color]) => ` ${varForName(name)}: ${color};`)
.join("\n")}
}\n\n`;

const content = fs.readFileSync(COLORS_CSS_PATH, "utf-8");
fs.writeFileSync(COLORS_CSS_PATH, colorsVarsBlock + content);
}

function prependJSVariablesBlock() {
// TODO: remove window.colors and inject `import colors from "metabase/lib/colors";` in each file where it's required
const colorsVarsBlock = `
// NOTE: DO NOT ADD COLORS WITHOUT EXTREMELY GOOD REASON AND DESIGN REVIEW
// NOTE: KEEP SYNCRONIZED WITH COLORS.CSS
const colors = window.colors = ${JSON.stringify(colors, null, 2)};
export default colors;\n\n`;

const content = fs.readFileSync(COLORS_JS_PATH, "utf-8");
const anchor = "export const brand = ";
fs.writeFileSync(
COLORS_CSS_PATH,
colorsVarsBlock + fs.readFileSync(COLORS_CSS_PATH, "utf-8"),
COLORS_JS_PATH,
content.replace(anchor, colorsVarsBlock + anchor),
);
}

glob(
path.join(__dirname, "..", FILE_GLOB),
{ ignore: FILE_GLOB_IGNORE },
(err, files) => {
if (err) {
console.error(err);
} else {
processFiles(files);
}
},
);
function run() {
const fileGlob = process.argv[2] || FILE_GLOB;
glob(
path.join(__dirname, "..", fileGlob),
{ ignore: FILE_GLOB_IGNORE },
(err, files) => {
if (err) {
console.error(err);
} else {
processFiles(files);
}
},
);
}

run();
Loading

0 comments on commit f629500

Please sign in to comment.