[OTHER] Validation
- Dominant language
- CSS
- Stars
- 11.8k
- Forks
- 2.4k
- Avg merge
- 5d 9h
- Merged PRs (30d)
- 4
Description
### I have searched through the issues and didn't find my problem.
- [X] Confirm
### What would you like to share?
I wanted to share validation script, which could help find errors in icons quicker. It is based on tools I use to validate icon sets for Iconify.
It checks for:
- Files that aren't supposed to be there and bad filenames.
- Icon dimensions.
- If icons contain any raster images, text, scripts or just any unknown tags that should not be there.
- Palette to see if there are missing/bad colors. That function also analyses SVG structure, reporting any missing elements.
Not sure if you'd want it in repository because:
- It adds yet another dependency to keep up to date.
- It is quite opinionated.
Code below so you could see if you want it.
Requires installing dev dependency: `@iconify/tools`.
Run from root directory. It checks icons in `icons` subdirectory.
To reduce number of warnings, `disableWarnings` is set to false, so only serious errors show up. If you set it to true, it will show a lot more warnings, but be aware that most of those warnings can be safely ignored or are result of another problem shown in error.
```js
const { readFile } = require("node:fs/promises");
const {
scanDirectory,
SVG,
cleanupSVG,
parseColors,
} = require("@iconify/tools");
const { analyseTagError } = require("@iconify/tools/lib/svg/analyse/error");
const baseDir = __dirname + "/icons";
// Set to true to disable warnings that could clutter output
const disableWarnings = true;
// All possible themes, value is palette
const palette = {
original: true,
plain: false,
line: false,
};
// All possible optional suffixes
const suffixes = new Set(["wordmark"]);
// Do stuff
(async () => {
const { setFilename, getWarnings } = prepareConsole();
// Find all icons
const files = await scanDirectory(baseDir, (ext, file, subdir, path) => {
const filename = subdir + file + ext;
setFilename(filename);
// Check if subdirectory matches icon name
const dirParts = subdir.split("/");
if (dirParts.length !== 2) {
console.error("unexpected file");
return;
}
const software = dirParts.shift();
// Check extension
switch (ext) {
// Allowed optional extensions
case ".eps":
return;
// SVG: break, validate after switch()
case ".svg":
break;
// Unknown extension
default:
console.error("unexpected file");
return;
}
// Check SVG file
// Check name: must start with software + '-'
if (file.slice(0, software.length + 1) !== software + "-") {
console.error("unexpected file");
return;
}
const themeParts = file.slice(software.length + 1).split("-");
const theme = themeParts.shift();
if (!theme || !(theme in palette)) {
console.error("unexpected file");
return;
}
if (themeParts.length) {
// Check for optional suffix after theme
const suffix = themeParts.join("-");
if (!suffixes.has(suffix)) {
console.error("unexpected file");
return;
}
}
return {
filename,
theme,
};
});
// Check each file
for (let i = 0; i < files.length; i++) {
const { filename, theme } = files[i];
setFilename(filename);
try {
// Read file
const content = await readFile(baseDir + "/" + filename, "utf8");
const svg = new SVG(content);
// Check dimensions
const viewBox = svg.viewBox;
const viewBoxStr = `${viewBox.left} ${viewBox.top} ${viewBox.width} ${viewBox.height}`;
if (viewBoxStr !== "0 0 128 128") {
console.error("invalid viewBox:", viewBoxStr);
}
// Check for bad elements
try {
await cleanupSVG(svg);
} catch (err) {
if (err.message) {
console.error(err.message);
}
continue;
}
// Check if warnings were logged
const warnings = getWarnings();
if (warnings.length) {
console.warn("has a lot of junk code");
}
// Check colors
if (palette[theme]) {
// Check palette
const foundColors = new Set();
await parseColors(svg, {
defaultColor: (prop, item) => {
console.warn(
`uses default color for "${prop}" in ${analyseTagError(item)}`
);
return {
type: "rgb",
r: 0,
g: 0,
b: 0,
alpha: 1,
};
},
callback: (attr, colorString, color) => {
if (!color) {
console.error("Invalid color:", colorString);
return "none";
}
switch (color.type) {
case "transparent":
case "none":
return color;
case "rgb": {
const strValue = colorString.toLowerCase();
foundColors.add(strValue);
return color;
}
default:
console.error("Invalid color:", colorString);
return color;
}
},
});
if (!foundColors.size) {
console.warn("has no colors");
}
} else {
// Check monotone icon
const foundColors = new Set();
await parseColors(svg, {
defaultColor: "currentColor",
callback: (attr, colorString, color) => {
if (!color) {
console.error("Invalid color:", colorString);
return "none";
}
switch (color.type) {
case "transparent":
case "none":
return color;
case "rgb":
case "current": {
const strValue = colorString.toLowerCase();
foundColors.add(strValue);
return "currentColor";
}
default:
console.error("Invalid color:", colorString);
return color;
}
},
});
if (foundColors.size > 1) {
console.warn(
"Contains multiple colors:",
Array.from(foundColors).join(", ")
);
}
}
} catch (err) {
console.error(err.message || err);
}
}
})();
/**
* Override console.*
*/
function prepareConsole() {
let filename = "";
let warnings = [];
const oldConsoleLog = console.log;
const oldConsoleError = console.error;
const oldConsoleWarn = console.warn;
function log(func, ...params) {
if (!filename) {
func(...params);
return;
}
// Check for warning
if (func === oldConsoleWarn) {
const firstParam = params[0];
if (typeof firstParam === "string") {
// Intercept some warnings logged in tools
if (firstParam.indexOf("Removing unexpected style") === 0) {
warnings.push(params);
return;
}
}
if (disableWarnings) {
return;
}
}
// Add filename
const color =
func === oldConsoleError
? "\x1b[31m"
: func === oldConsoleWarn
? // ? "\x1b[35m"
"\x1b[36m"
: "\x1b[32m";
func("[" + color + filename + "\x1b[0m]:", ...params);
}
console.log = log.bind(this, oldConsoleLog);
console.error = log.bind(this, oldConsoleError);
console.warn = log.bind(this, oldConsoleWarn);
// Return function to set filename
const setFilename = (file) => {
filename = file;
warnings = [];
};
const getWarnings = () => {
return warnings;
};
return {
setFilename,
getWarnings,
};
}
```
### Additional information
_No response_
Contributor guide
Research direction
Review the proposed JavaScript validation script and the repository's root icons directory first. Confirm whether the project wants to adopt the @iconify/tools dependency and these validation rules; done would require an agreed integration point and a documented way to run the checks against icons.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- javascript
- Domain
- testing, tooling
- Issue type
- Feature
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Stale
- Clarity
- Needs clarification
- Newbie friendliness
- 25/100