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 = /^.*<\/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})`); } } ] } };