1
0
mirror of https://github.com/simple-icons/simple-icons.git synced 2024-12-26 01:13:41 +02:00
simple-icons/.svglintrc.js
2020-07-28 11:33:40 +01:00

112 lines
4.2 KiB
JavaScript

const data = require("./_data/simple-icons.json");
const { htmlFriendlyToTitle } = require("./scripts/utils.js");
const getBounds = require("svg-path-bounding-box");
const titleRegexp = /(.+) icon$/;
const svgRegexp = /^<svg( [^\s]*=".*"){3}><title>.*<\/title><path d=".*"\/><\/svg>\r?\n?$/;
const iconSize = 24;
const iconFloatPrecision = 3;
const iconTolerance = 0.001;
const iconIgnored = require("./.svglint-ignored.json");
module.exports = {
rules: {
elm: {
"svg": 1,
"svg > title": 1,
"svg > path": 1,
"*": false,
},
attr: [
{ // ensure that the SVG elm has the appropriate attrs
"role": "img",
"viewBox": `0 0 ${iconSize} ${iconSize}`,
"xmlns": "http://www.w3.org/2000/svg",
"rule::selector": "svg",
"rule::whitelist": true,
},
{ // ensure that the title elm has the appropriate attr
"rule::selector": "svg > title",
"rule::whitelist": true,
},
{ // ensure that the path element only has the 'd' attr (no style, opacity, etc.)
"d": /^[,a-zA-Z0-9\. -]+$/,
"rule::selector": "svg > path",
"rule::whitelist": true,
}
],
custom: [
function(reporter, $, ast) {
reporter.name = "icon-title";
const iconTitleText = $.find("title").text();
if (!titleRegexp.test(iconTitleText)) {
reporter.error("<title> should follow the format \"[ICON_NAME] icon\"");
} else {
const titleMatch = iconTitleText.match(titleRegexp);
// titleMatch = [ "[ICON_NAME] icon", "[ICON_NAME]" ]
const rawIconName = titleMatch[1];
const iconName = htmlFriendlyToTitle(rawIconName);
const icon = data.icons.find(icon => icon.title === iconName);
if (icon === undefined) {
reporter.error(`No icon with title "${iconName}" found in simple-icons.json`);
}
}
},
function(reporter, $, ast) {
reporter.name = "icon-size";
const iconPath = $.find("path").attr("d");
if (iconIgnored.size.hasOwnProperty(iconPath)) {
return;
}
const bounds = getBounds(iconPath);
const width = +bounds.width.toFixed(iconFloatPrecision);
const height = +bounds.height.toFixed(iconFloatPrecision);
if (width === 0 && height === 0) {
reporter.error("Path bounds were reported as 0 x 0; check if the path is valid");
} else if (width !== iconSize && height !== iconSize) {
reporter.error(`Size of <path> must be exactly ${iconSize} in one dimension; the size is currently ${width} x ${height}`);
}
},
function(reporter, $, ast) {
reporter.name = "extraneous";
const rawSVG = $.html();
if (!svgRegexp.test(rawSVG)) {
reporter.error("Unexpected character(s), most likely extraneous whitespace, detected in SVG markup");
}
},
function(reporter, $, ast) {
reporter.name = "icon-centered";
const iconPath = $.find("path").attr("d");
const bounds = getBounds(iconPath);
if (
iconIgnored.size.hasOwnProperty(iconPath) ||
iconIgnored.center.hasOwnProperty(iconPath)
) {
return;
}
const targetCenter = iconSize / 2;
const centerX = +((bounds.minX + bounds.maxX) / 2).toFixed(iconFloatPrecision);
const devianceX = centerX - targetCenter;
const centerY = +((bounds.minY + bounds.maxY) / 2).toFixed(iconFloatPrecision);
const devianceY = centerY - targetCenter;
if (
Math.abs(devianceX) > iconTolerance ||
Math.abs(devianceY) > iconTolerance
) {
reporter.error(`<path> must be centered at (${targetCenter}, ${targetCenter}); the center is currently (${centerX}, ${centerY})`);
}
}
]
}
};