2020-10-17 14:01:56 +03:00
|
|
|
const fs = require('fs');
|
|
|
|
|
2019-07-04 00:33:03 +03:00
|
|
|
const data = require("./_data/simple-icons.json");
|
|
|
|
const { htmlFriendlyToTitle } = require("./scripts/utils.js");
|
2020-06-10 12:59:42 +03:00
|
|
|
const getBounds = require("svg-path-bounding-box");
|
2019-07-04 00:33:03 +03:00
|
|
|
|
|
|
|
const titleRegexp = /(.+) icon$/;
|
2020-07-05 16:57:00 +03:00
|
|
|
const svgRegexp = /^<svg( [^\s]*=".*"){3}><title>.*<\/title><path d=".*"\/><\/svg>\r?\n?$/;
|
2020-06-22 19:24:56 +03:00
|
|
|
|
2020-06-10 12:59:42 +03:00
|
|
|
const iconSize = 24;
|
|
|
|
const iconFloatPrecision = 3;
|
2020-07-10 11:06:08 +03:00
|
|
|
const iconTolerance = 0.001;
|
2020-10-17 14:01:56 +03:00
|
|
|
|
|
|
|
// set env SI_UPDATE_IGNORE to recreate the ignore file
|
|
|
|
const updateIgnoreFile = process.env.SI_UPDATE_IGNORE === 'true'
|
|
|
|
const ignoreFile = "./.svglint-ignored.json";
|
|
|
|
const iconIgnored = !updateIgnoreFile ? require(ignoreFile) : {};
|
|
|
|
|
|
|
|
function sortObjectByKey(obj) {
|
|
|
|
return Object
|
|
|
|
.keys(obj)
|
|
|
|
.sort()
|
|
|
|
.reduce((r, k) => Object.assign(r, { [k]: obj[k] }), {});
|
|
|
|
}
|
|
|
|
|
|
|
|
function sortObjectByValue(obj) {
|
|
|
|
return Object
|
|
|
|
.keys(obj)
|
|
|
|
.sort((a, b) => ('' + obj[a]).localeCompare(obj[b]))
|
|
|
|
.reduce((r, k) => Object.assign(r, { [k]: obj[k] }), {});
|
|
|
|
}
|
|
|
|
|
|
|
|
if (updateIgnoreFile) {
|
|
|
|
process.on('exit', () => {
|
|
|
|
// ensure object output order is consistent due to async svglint processing
|
|
|
|
const sorted = sortObjectByKey(iconIgnored)
|
|
|
|
for (const linterName in sorted) {
|
|
|
|
sorted[linterName] = sortObjectByValue(sorted[linterName])
|
|
|
|
}
|
|
|
|
|
|
|
|
fs.writeFileSync(
|
|
|
|
ignoreFile,
|
|
|
|
JSON.stringify(sorted, null, 2) + '\n',
|
|
|
|
{flag: 'w'}
|
|
|
|
);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
function isIgnored(linterName, path) {
|
2020-10-30 00:03:24 +03:00
|
|
|
return iconIgnored[linterName].hasOwnProperty(path);
|
2020-10-17 14:01:56 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
function ignoreIcon(linterName, path, $) {
|
|
|
|
if (!iconIgnored[linterName]) {
|
|
|
|
iconIgnored[linterName] = {};
|
|
|
|
}
|
|
|
|
|
|
|
|
const title = $.find("title").text().replace(/(.*) icon/, '$1');
|
|
|
|
const iconName = htmlFriendlyToTitle(title);
|
|
|
|
|
|
|
|
iconIgnored[linterName][path] = iconName;
|
|
|
|
}
|
2019-07-04 00:33:03 +03:00
|
|
|
|
2018-08-16 12:33:32 +03:00
|
|
|
module.exports = {
|
|
|
|
rules: {
|
|
|
|
elm: {
|
|
|
|
"svg": 1,
|
|
|
|
"svg > title": 1,
|
2019-03-28 22:40:31 +03:00
|
|
|
"svg > path": 1,
|
|
|
|
"*": false,
|
2018-08-16 12:33:32 +03:00
|
|
|
},
|
|
|
|
attr: [
|
|
|
|
{ // ensure that the SVG elm has the appropriate attrs
|
|
|
|
"role": "img",
|
2020-06-10 12:59:42 +03:00
|
|
|
"viewBox": `0 0 ${iconSize} ${iconSize}`,
|
2018-08-16 12:33:32 +03:00
|
|
|
"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,
|
2019-03-20 10:55:03 +03:00
|
|
|
},
|
|
|
|
{ // 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,
|
2018-08-16 12:33:32 +03:00
|
|
|
}
|
2019-07-04 00:33:03 +03:00
|
|
|
],
|
|
|
|
custom: [
|
|
|
|
function(reporter, $, ast) {
|
2020-06-10 12:59:42 +03:00
|
|
|
reporter.name = "icon-title";
|
|
|
|
|
2019-07-04 00:33:03 +03:00
|
|
|
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`);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
2020-06-10 12:59:42 +03:00
|
|
|
function(reporter, $, ast) {
|
|
|
|
reporter.name = "icon-size";
|
|
|
|
|
|
|
|
const iconPath = $.find("path").attr("d");
|
2020-10-17 14:01:56 +03:00
|
|
|
if (!updateIgnoreFile && isIgnored(reporter.name, iconPath)) {
|
2020-06-10 12:59:42 +03:00
|
|
|
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");
|
2020-10-17 14:01:56 +03:00
|
|
|
if (updateIgnoreFile) {
|
|
|
|
ignoreIcon(reporter.name, iconPath, $);
|
|
|
|
}
|
2020-06-10 12:59:42 +03:00
|
|
|
} 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}`);
|
2020-10-17 14:01:56 +03:00
|
|
|
if (updateIgnoreFile) {
|
|
|
|
ignoreIcon(reporter.name, iconPath, $);
|
|
|
|
}
|
2020-06-10 12:59:42 +03:00
|
|
|
}
|
|
|
|
},
|
2020-06-22 19:24:56 +03:00
|
|
|
function(reporter, $, ast) {
|
|
|
|
reporter.name = "extraneous";
|
|
|
|
|
|
|
|
const rawSVG = $.html();
|
|
|
|
if (!svgRegexp.test(rawSVG)) {
|
2020-07-05 16:57:00 +03:00
|
|
|
reporter.error("Unexpected character(s), most likely extraneous whitespace, detected in SVG markup");
|
2020-06-22 19:24:56 +03:00
|
|
|
}
|
|
|
|
},
|
2020-07-10 11:06:08 +03:00
|
|
|
function(reporter, $, ast) {
|
|
|
|
reporter.name = "icon-centered";
|
|
|
|
|
2020-10-17 14:01:56 +03:00
|
|
|
const iconPath = $.find("path").attr("d");
|
|
|
|
if (!updateIgnoreFile && isIgnored(reporter.name, iconPath)) {
|
2020-07-28 13:33:40 +03:00
|
|
|
return;
|
2020-07-10 11:06:08 +03:00
|
|
|
}
|
|
|
|
|
2020-10-17 14:01:56 +03:00
|
|
|
const bounds = getBounds(iconPath);
|
2020-07-10 11:06:08 +03:00
|
|
|
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})`);
|
2020-10-17 14:01:56 +03:00
|
|
|
if (updateIgnoreFile) {
|
|
|
|
ignoreIcon(reporter.name, iconPath, $);
|
|
|
|
}
|
2020-07-10 11:06:08 +03:00
|
|
|
}
|
|
|
|
}
|
2018-08-16 12:33:32 +03:00
|
|
|
]
|
|
|
|
}
|
|
|
|
};
|