devicons / devicons/devicon

[OTHER] Validation

Open
#1,541 2 comments 0 reactions 0 assignees View on GitHub
awaiting triage devops
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.