1 Star 0 Fork 0

MT / antd-theme-generator

加入 Gitee
与超过 1200万 开发者一起发现、参与优秀开源项目,私有仓库也完全免费 :)
免费加入
该仓库未声明开源许可证文件(LICENSE),使用请关注具体项目描述及其代码上游依赖。
克隆/下载
index.js 20.29 KB
一键复制 编辑 原始数据 按行查看 历史
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767
const fs = require("fs");
const path = require("path");
const glob = require("glob");
const postcss = require("postcss");
const less = require("less");
const hash = require("hash.js");
const bundle = require("less-bundle-promise");
const NpmImportPlugin = require("less-plugin-npm-import");
const stripCssComments = require("strip-css-comments");
let hashCache = "";
let cssCache = "";
const COLOR_FUNCTIONS = [
"color",
"lighten",
"darken",
"saturate",
"desaturate",
"fadein",
"fadeout",
"fade",
"spin",
"mix",
"hsv",
"tint",
"shade",
"greyscale",
"multiply",
"contrast",
"screen",
"overlay",
];
const defaultColorRegexArray = COLOR_FUNCTIONS.map(
(name) => new RegExp(`${name}\(.*\)`)
);
defaultColorRegexArray.matches = (color) => {
return defaultColorRegexArray.reduce((prev, regex) => {
return prev || regex.test(color);
}, false);
};
/*
Generated random hex color code
e.g. #fe12ee
*/
function randomColor() {
return "#" + (0x1000000 + Math.random() * 0xffffff).toString(16).substr(1, 6);
}
/*
Recursively get the color code assigned to a variable e.g.
@primary-color: #1890ff;
@link-color: @primary-color;
@link-color -> @primary-color -> #1890ff
Which means
@link-color: #1890ff
*/
function getColor(varName, mappings) {
const color = mappings[varName];
if (color in mappings) {
return getColor(color, mappings);
} else {
return color;
}
}
/*
Read following files and generate color variables and color codes mapping
- Ant design color.less, themes/default.less
- Your own variables.less
It will generate map like this
{
'@primary-color': '#00375B',
'@info-color': '#1890ff',
'@success-color': '#52c41a',
'@error-color': '#f5222d',
'@normal-color': '#d9d9d9',
'@primary-6': '#1890ff',
'@heading-color': '#fa8c16',
'@text-color': '#cccccc',
....
}
*/
function generateColorMap(content, customColorRegexArray = []) {
return content
.split("\n")
.filter((line) => line.startsWith("@") && line.indexOf(":") > -1)
.reduce((prev, next) => {
try {
const matches = next.match(
/(?=\S*['-])([@a-zA-Z0-9'-]+).*:[ ]{1,}(.*);/
);
if (!matches) {
return prev;
}
let [, varName, color] = matches;
if (color && color.startsWith("@")) {
color = getColor(color, prev);
if (!isValidColor(color, customColorRegexArray)) return prev;
// if (defaultColorRegexArray.matches(color) && !color.includes('~')) {
// color = `~'${color}'`;
// }
prev[varName] = color;
} else if (isValidColor(color, customColorRegexArray)) {
// if (defaultColorRegexArray.matches(color) && !color.includes('~')) {
// color = `~'${color}'`;
// }
prev[varName] = color;
}
return prev;
} catch (e) {
console.log("e", e);
return prev;
}
}, {});
}
/*
This plugin will remove all css rules except those are related to colors
e.g.
Input:
.body {
font-family: 'Lato';
background: #cccccc;
color: #000;
padding: 0;
pargin: 0
}
Output:
.body {
background: #cccccc;
color: #000;
}
*/
const reducePlugin = postcss.plugin("reducePlugin", () => {
const cleanRule = (rule) => {
if (rule.selector.startsWith(".main-color .palatte-")) {
rule.remove();
return;
}
let removeRule = true;
rule.walkDecls((decl) => {
let matched = false;
if (String(decl.value).match(/url\(.*\)/g)) {
decl.remove();
matched = true;
}
// Removing transparent adds Link Button border color
// https://github.com/mzohaibqc/antd-theme-generator/issues/64
// if (!matched && decl.value === 'transparent') {
// decl.remove();
// matched = true;
// }
/*
this block causing https://github.com/ant-design/ant-design/issues/24777
if (decl.prop !== 'background' && decl.prop.includes('background') && !decl.prop.match(/^background-(.*)color$/ig)) {
decl.remove();
matched = true;
}
if (decl.prop !== 'border' && decl.prop.includes('border') && !decl.prop.match(/^border-(.*)color$/ig)) {
decl.remove();
matched = true;
}
if (['transparent', 'inherit', 'none', '0'].includes(decl.value)) {
decl.remove();
matched = true;
}
*/
if (
!decl.prop.includes("color") &&
!decl.prop.includes("background") &&
!decl.prop.includes("border") &&
!decl.prop.includes("box-shadow") &&
!Number.isNaN(decl.value)
) {
// if (!matched) decl.remove();
decl.remove();
} else {
removeRule = matched ? removeRule : false;
}
});
if (removeRule) {
rule.remove();
}
};
return (css) => {
css.walkAtRules((atRule) => {
atRule.remove();
});
css.walkRules(cleanRule);
css.walkComments((c) => c.remove());
};
});
function getMatches(string, regex) {
const matches = {};
let match;
while ((match = regex.exec(string))) {
if (match[2].startsWith("rgba") || match[2].startsWith("#")) {
matches[`@${match[1]}`] = match[2];
}
}
return matches;
}
/*
This function takes less input as string and compiles into css.
*/
function render(text, paths) {
return less.render(text, {
paths: paths,
javascriptEnabled: true,
plugins: [new NpmImportPlugin({ prefix: "~" })],
});
}
/*
This funtion reads a less file and create an object with keys as variable names
and values as variables respective values. e.g.
//variabables.less
@primary-color : #1890ff;
@heading-color : #fa8c16;
@text-color : #cccccc;
to
{
'@primary-color' : '#1890ff',
'@heading-color' : '#fa8c16',
'@text-color' : '#cccccc'
}
*/
function getLessVars(filtPath) {
const sheet = fs.readFileSync(filtPath).toString();
const lessVars = {};
const matches = sheet.match(/@(.*:[^;]*)/g) || [];
matches.forEach((variable) => {
const definition = variable.split(/:\s*/);
const varName = definition[0].replace(/['"]+/g, "").trim();
lessVars[varName] = definition.splice(1).join(":");
});
return lessVars;
}
/*
This function take primary color palette name and returns @primary-color dependent value
.e.g
Input: @primary-1
Output: color(~`colorPalette("@{primary-color}", ' 1 ')`)
*/
function getShade(varName) {
let [, className, number] = varName.match(/(.*)-(\d)/);
if (/primary-\d/.test(varName)) className = "@primary-color";
return (
'color(~`colorPalette("@{' +
className.replace("@", "") +
'}", ' +
number +
")`)"
);
}
/*
This function takes color string as input and return true if string is a valid color otherwise returns false.
e.g.
isValidColor('#ffffff'); //true
isValidColor('#fff'); //true
isValidColor('rgba(0, 0, 0, 0.5)'); //true
isValidColor('20px'); //false
*/
function isValidColor(color, customColorRegexArray = []) {
if (color && color.includes("rgb")) return true;
if (!color || color.match(/px/g)) return false;
if (color.match(/colorPalette|fade/g)) return true;
if (color.charAt(0) === "#") {
color = color.substring(1);
return (
[3, 4, 6, 8].indexOf(color.length) > -1 && !isNaN(parseInt(color, 16))
);
}
// eslint-disable-next-line
const isColor = /^(rgb|hsl|hsv)a?\((\d+%?(deg|rad|grad|turn)?[,\s]+){2,3}[\s\/]*[\d\.]+%?\)$/i.test(
color
);
if (isColor) return true;
if (customColorRegexArray.length > 0) {
return customColorRegexArray.reduce((prev, regex) => {
return prev || regex.test(color);
}, false);
}
return false;
}
async function compileAllLessFilesToCss(
stylesDir,
antdStylesDir,
varMap = {},
varPath
) {
/*
Get all less files path in styles directory
and then compile all to css and join
*/
const stylesDirs = [].concat(stylesDir);
let styles = [];
stylesDirs.forEach((s) => {
styles = styles.concat(glob.sync(path.join(s, "./**/*.less")));
});
const csss = await Promise.all(
styles.map((filePath) => {
let fileContent = fs.readFileSync(filePath).toString();
// Removed imports to avoid duplicate styles due to reading file separately as well as part of parent file (which is importing)
// if (avoidDuplicates) fileContent = fileContent.replace(/@import\ ["'](.*)["'];/g, '\n');
const r = /@import ["'](.*)["'];/g;
const directory = path.dirname(filePath);
fileContent = fileContent.replace(r, function (
match,
importPath,
index,
content
) {
if (!importPath.endsWith(".less")) {
importPath += ".less";
}
const newPath = path.join(directory, importPath);
// If imported path/file already exists in styles paths then replace import statement with empty line
if (styles.indexOf(newPath) === -1) {
return match;
} else {
return "";
}
});
Object.keys(varMap).forEach((varName) => {
fileContent = fileContent.replace(
new RegExp(`(:.*)(${varName})`, "g"),
(match, group, a) => {
return match.replace(varName, varMap[varName]);
}
);
});
fileContent = `@import "${varPath}";\n${fileContent}`;
// fileContent = `@import "~antd/lib/style/themes/default.less";\n${fileContent}`;
return less
.render(fileContent, {
paths: [antdStylesDir].concat(stylesDir),
filename: path.resolve(filePath),
javascriptEnabled: true,
plugins: [new NpmImportPlugin({ prefix: "~" })],
})
.then((res) => {
return res;
})
.catch((e) => {
console.error(`Error occurred compiling file ${filePath}`);
console.error("Error", e);
return "\n";
});
})
);
const hashes = {};
return csss
.map((c) => {
const css = stripCssComments(c.css || "", { preserve: false });
const hashCode = hash.sha256().update(css).digest("hex");
if (hashCode in hashes) {
return "";
} else {
hashes[hashCode] = hashCode;
return css;
}
})
.join("\n");
}
/*
This is main function which call all other functions to generate color.less file which contins all color
related css rules based on Ant Design styles and your own custom styles
By default color.less will be generated in /public directory
*/
async function generateTheme({
antDir,
antdStylesDir,
stylesDir,
varFile,
outputFilePath,
themeVariables = ["@primary-color"],
customColorRegexArray = [],
}) {
try {
const antdPath = antdStylesDir || path.join(antDir, "lib");
const nodeModulesPath = path.join(
antDir.slice(0, antDir.indexOf("node_modules")),
"./node_modules"
);
/*
stylesDir can be array or string
*/
const stylesDirs = [].concat(stylesDir);
let styles = [];
stylesDirs.forEach((s) => {
styles = styles.concat(glob.sync(path.join(s, "./**/*.less")));
});
const antdStylesFile = path.join(antDir, "./dist/antd.less"); // path.join(antdPath, './style/index.less');
/*
You own custom styles (Change according to your project structure)
- stylesDir - styles directory containing all less files
- varFile - variable file containing ant design specific and your own custom variables
*/
varFile = varFile || path.join(antdPath, "./style/themes/default.less");
let content = "";
styles.forEach((filePath) => {
content += fs.readFileSync(filePath).toString();
});
const hashCode = hash.sha256().update(content).digest("hex");
if (hashCode === hashCache) {
return cssCache;
}
hashCache = hashCode;
let themeCompiledVars = {};
let themeVars = themeVariables || ["@primary-color"];
const lessPaths = [path.join(antdPath, "./style")].concat(stylesDir);
const randomColors = {};
const randomColorsVars = {};
/*
Ant Design Specific Files (Change according to your project structure)
You can even use different less based css framework and create color.less for that
- antDir - ant design instalation path
- entry - Ant Design less main file / entry file
- styles - Ant Design less styles for each component
1. Bundle all variables into one file
2. process vars and create a color name, color value key value map
3. Get variables which are part of theme
4.
*/
const varFileContent = combineLess(varFile, nodeModulesPath);
let antdLess = await bundle({
src: antdStylesFile,
});
customColorRegexArray = [
...customColorRegexArray,
...defaultColorRegexArray,
];
const mappings = Object.assign(
generateColorMap(varFileContent, customColorRegexArray),
getLessVars(varFile)
);
let css = "";
const PRIMARY_RANDOM_COLOR = "#123456";
themeVars = themeVars.filter(
(name) => name in mappings && !name.match(/(.*)-(\d)/)
);
themeVars.forEach((varName) => {
let color = randomColor();
if (varName === "@primary-color") {
color = PRIMARY_RANDOM_COLOR;
} else {
while (
(randomColorsVars[color] && color === PRIMARY_RANDOM_COLOR) ||
color === "#000000" ||
color === "#ffffff"
) {
color = randomColor();
}
}
randomColors[varName] = color;
randomColorsVars[color] = varName;
css = `.${varName.replace("@", "")} { color: ${color}; }\n ${css}`;
});
const colorFuncMap = {};
let varsContent = "";
themeVars.forEach((varName) => {
[1, 2, 3, 4, 5, 7, 8, 9, 10].forEach((key) => {
const name =
varName === "@primary-color"
? `@primary-${key}`
: `${varName}-${key}`;
css = `.${name.replace("@", "")} { color: ${getShade(
name
)}; }\n ${css}`;
});
varsContent += `${varName}: ${randomColors[varName]};\n`;
});
// This is to compile colors
// Put colors.less content first,
// then add random color variables to override the variables values for given theme variables with random colors
// Then add css containinf color variable classes
const colorFileContent = combineLess(
path.join(antdPath, "./style/color/colors.less"),
nodeModulesPath
);
css = `${colorFileContent}\n${varsContent}\n${css}`;
let results = await render(css, lessPaths);
css = results.css;
css = css.replace(/(\/.*\/)/g, "");
const regex = /.(?=\S*['-])([.a-zA-Z0-9'-]+)\ {\n {2}color: (.*);/g;
themeCompiledVars = getMatches(css, regex);
// Convert all custom user less files to css
const userCustomCss = await compileAllLessFilesToCss(
stylesDir,
antdStylesDir,
themeCompiledVars,
varFile
);
let fadeMap = {};
const fades = antdLess.match(/fade\(.*\)/g);
if (fades) {
fades.forEach((fade) => {
if (
!fade.startsWith("fade(@black") &&
!fade.startsWith("fade(@white") &&
!fade.startsWith("fade(#") &&
!fade.startsWith("fade(@color")
) {
fadeMap[fade] = randomColor();
}
});
}
let varsCombined = "";
themeVars.forEach((varName) => {
let color;
if (/(.*)-(\d)/.test(varName)) {
color = getShade(varName);
return;
} else {
color = themeCompiledVars[varName];
}
varsCombined = `${varsCombined}\n${varName}: ${color};`;
});
COLOR_FUNCTIONS.slice(1).forEach((name) => {
antdLess = antdLess.replace(
new RegExp(`${name}\\((.*), \\d+%\\)`, "g"),
(fullmatch, group) => {
if (mappings[group]) {
return `~'${fullmatch}'`;
}
return fullmatch;
}
);
});
antdLess = `${antdLess}\n${varsCombined}`;
const { css: antCss } = await render(antdLess, [antdPath, antdStylesDir]);
const allCss = `${antCss}\n${userCustomCss}`;
results = await postcss([reducePlugin]).process(allCss, {
from: antdStylesFile,
});
css = results.css;
Object.keys(fadeMap).forEach((fade) => {
css = css.replace(new RegExp(fadeMap[fade], "g"), fade);
});
Object.keys(themeCompiledVars).forEach((varName) => {
let color;
if (/(.*)-(\d)/.test(varName)) {
color = themeCompiledVars[varName];
varName = getShade(varName);
} else {
color = themeCompiledVars[varName];
}
color = color.replace("(", "\\(").replace(")", "\\)");
if (varName === "@slider-handle-color-focus") {
console.log("color", color, varName);
}
css = css.replace(new RegExp(color, "g"), varName);
});
Object.keys(colorFuncMap).forEach((varName) => {
const color = colorFuncMap[varName];
css = css.replace(new RegExp(color, "g"), varName);
});
COLOR_FUNCTIONS.forEach((name) => {
css = css.replace(new RegExp(`~'(${name}\(.*\))'`), (a, b) => {
console.log("b", b);
return b;
});
});
// Handle special cases
// https://github.com/mzohaibqc/antd-theme-webpack-plugin/issues/69
// 1. Replace fade(@primary-color, 20%) value i.e. rgba(18, 52, 86, 0.2)
css = css.replace(
new RegExp("rgba\\(18, 52, 86, 0.2\\)", "g"),
"fade(@primary-color, 20%)"
);
css = css.replace(/@[\w-_]+:\s*.*;[\/.]*/gm, "");
// This is to replace \9 in Ant Design styles
css = css.replace(/\\9/g, "");
css = `${css.trim()}\n${combineLess(
path.join(antdPath, "./style/themes/default.less"),
nodeModulesPath
)}`;
themeVars.reverse().forEach((varName) => {
css = css.replace(new RegExp(`${varName}( *):(.*);`, "g"), "");
css = `${varName}: ${mappings[varName]};\n${css}\n`;
});
css = minifyCss(css);
if (outputFilePath) {
fs.writeFileSync(outputFilePath, css);
console.log(
`🌈 Theme generated successfully. OutputFile: ${outputFilePath}`
);
} else {
console.log("Theme generated successfully");
}
cssCache = css;
return cssCache;
} catch (error) {
console.log("error", error);
return "";
}
}
module.exports = {
generateTheme,
isValidColor,
getLessVars,
randomColor,
minifyCss,
renderLessContent: render,
};
function minifyCss(css) {
// Removed all comments and empty lines
css = css
.replace(/\/\*[\s\S]*?\*\/|\/\/.*/g, "")
.replace(/^\s*$(?:\r\n?|\n)/gm, "");
/*
Converts from
.abc,
.def {
color: red;
background: blue;
border: grey;
}
to
.abc,
.def {color: red;
background: blue;
border: grey;
}
*/
css = css.replace(/\{(\r\n?|\n)\s+/g, "{");
/*
Converts from
.abc,
.def {color: red;
}
to
.abc,
.def {color: red;
background: blue;
border: grey;}
*/
css = css.replace(/;(\r\n?|\n)\}/g, ";}");
/*
Converts from
.abc,
.def {color: red;
background: blue;
border: grey;}
to
.abc,
.def {color: red;background: blue;border: grey;}
*/
css = css.replace(/;(\r\n?|\n)\s+/g, ";");
/*
Converts from
.abc,
.def {color: red;background: blue;border: grey;}
to
.abc, .def {color: red;background: blue;border: grey;}
*/
css = css.replace(/,(\r\n?|\n)[.]/g, ", .");
return css;
}
// const removeColorCodesPlugin = postcss.plugin('removeColorCodesPlugin', () => {
// const cleanRule = rule => {
// let removeRule = true;
// rule.walkDecls(decl => {
// if (
// !decl.value.includes('@')
// ) {
// decl.remove();
// } else {
// removeRule = false;
// }
// });
// if (removeRule) {
// rule.remove();
// }
// };
// return css => {
// css.walkRules(cleanRule);
// };
// });
function combineLess(filePath, nodeModulesPath) {
const fileContent = fs.readFileSync(filePath).toString();
const directory = path.dirname(filePath);
return fileContent
.split("\n")
.map((line) => {
if (line.startsWith("@import")) {
let importPath = line.match(/@import\ ["'](.*)["'];/)[1];
if (!importPath.endsWith(".less")) {
importPath += ".less";
}
let newPath = path.join(directory, importPath);
if (importPath.startsWith("~")) {
importPath = importPath.replace("~", "");
newPath = path.join(nodeModulesPath, `./${importPath}`);
}
return combineLess(newPath, nodeModulesPath);
}
return line;
})
.join("\n");
}
1
https://gitee.com/MT007/antd-theme-generator.git
git@gitee.com:MT007/antd-theme-generator.git
MT007
antd-theme-generator
antd-theme-generator
master

搜索帮助