diff --git a/.github/ISSUE_TEMPLATE/documentation.yml b/.github/ISSUE_TEMPLATE/documentation.yml index 3f100766..8eda84b4 100644 --- a/.github/ISSUE_TEMPLATE/documentation.yml +++ b/.github/ISSUE_TEMPLATE/documentation.yml @@ -41,7 +41,7 @@ body: This is an open source project and we welcome contributions. Do you want to work on this issue? options: - - "Yes" - - "No" + - Yes + - No validations: required: true diff --git a/.github/ISSUE_TEMPLATE/icon_removal.yml b/.github/ISSUE_TEMPLATE/icon_removal.yml index ec93af12..52c7ec45 100644 --- a/.github/ISSUE_TEMPLATE/icon_removal.yml +++ b/.github/ISSUE_TEMPLATE/icon_removal.yml @@ -1,6 +1,6 @@ name: Icon removal description: Report an icon for removal -title: "Remove: " +title: 'Remove: ' labels: [removal request] body: @@ -18,7 +18,7 @@ body: - type: input attributes: label: Brand Name - placeholder: "Example: Simple Icons" + placeholder: 'Example: Simple Icons' validations: required: true diff --git a/.github/ISSUE_TEMPLATE/icon_request.yml b/.github/ISSUE_TEMPLATE/icon_request.yml index 36bd1db3..c739288f 100644 --- a/.github/ISSUE_TEMPLATE/icon_request.yml +++ b/.github/ISSUE_TEMPLATE/icon_request.yml @@ -1,6 +1,6 @@ name: Icon request description: Request a new icon for Simple Icons -title: "Request: " +title: 'Request: ' labels: [new icon] body: @@ -25,7 +25,7 @@ body: - type: input attributes: label: Brand Name - placeholder: "Example: Simple Icons" + placeholder: 'Example: Simple Icons' validations: required: true @@ -35,7 +35,7 @@ body: description: >- For non-web brands you can add a relevant link. You can put "None" if you don't think there's a website. - placeholder: "Example: https://simpleicons.org" + placeholder: 'Example: https://simpleicons.org' validations: required: true @@ -50,7 +50,7 @@ body: [alexa rank]: https://www.alexa.com/siteinfo - placeholder: "Example: The Alexa rank is 249,089. See https://www.alexa.com/siteinfo/simpleicons.org" + placeholder: 'Example: The Alexa rank is 249,089. See https://www.alexa.com/siteinfo/simpleicons.org' validations: required: true @@ -75,7 +75,7 @@ body: - Are there multiple options for the logo and/or color? - Is the icon released under a license? - If you think the brand might not be accepted, why do you think it should be considered? - placeholder: "Example: There are two variants of this icon..." + placeholder: 'Example: There are two variants of this icon...' - type: dropdown attributes: @@ -84,7 +84,7 @@ body: This is an open source project and we welcome contributions. Do you want to add this icon? options: - - "Yes" - - "No" + - Yes + - No validations: required: true diff --git a/.github/ISSUE_TEMPLATE/icon_update.yml b/.github/ISSUE_TEMPLATE/icon_update.yml index bd4c5518..56582090 100644 --- a/.github/ISSUE_TEMPLATE/icon_update.yml +++ b/.github/ISSUE_TEMPLATE/icon_update.yml @@ -1,6 +1,6 @@ name: Icon update description: Help us improve by reporting outdated icons -title: "Update: " +title: 'Update: ' labels: [icon outdated] body: @@ -18,7 +18,7 @@ body: - type: input attributes: label: Brand Name - placeholder: "Example: Simple Icons" + placeholder: 'Example: Simple Icons' validations: required: true @@ -42,7 +42,7 @@ body: Is there anything else we should know about the brand? Remember that not everyone knows the brand as well as you do. For example: - Are there multiple options for the logo and/or color? - Is the icon released under a license? - placeholder: "Example: There are two variants of this icon..." + placeholder: 'Example: There are two variants of this icon...' - type: dropdown attributes: @@ -51,7 +51,7 @@ body: This is an open source project and we welcome contributions. Do you want to update this icon? options: - - "Yes" - - "No" + - Yes + - No validations: required: true diff --git a/.github/ISSUE_TEMPLATE/package.yml b/.github/ISSUE_TEMPLATE/package.yml index 0b802eb5..557d7b0d 100644 --- a/.github/ISSUE_TEMPLATE/package.yml +++ b/.github/ISSUE_TEMPLATE/package.yml @@ -34,7 +34,7 @@ body: - type: input attributes: label: Package Version - placeholder: "Example: 5.11.0" + placeholder: 'Example: 5.11.0' validations: required: true @@ -59,6 +59,6 @@ body: - For bugs: "Steps to reproduce" and "Expected behavior" - For feature requests: An example of a use case - For performance: An example where performance is poor - placeholder: "Example: The NPM package does not work for my use case..." + placeholder: 'Example: The NPM package does not work for my use case...' validations: required: true diff --git a/.github/renovate.json5 b/.github/renovate.json5 index 7614541f..96af6135 100644 --- a/.github/renovate.json5 +++ b/.github/renovate.json5 @@ -1,19 +1,19 @@ { extends: [ - "config:base", + 'config:base', // Make sure we get a single PR combining all updates - "group:all", + 'group:all', ], // Disable dependency dashboard dependencyDashboard: false, // Use our labelling system - labels: ["dependencies"], + labels: ['dependencies'], // Schedule the PRs to interleave with our release schedule - schedule: "on the 2nd and 4th day instance on sunday after 11pm", + schedule: 'on the 2nd and 4th day instance on sunday after 11pm', // We generally always want the major version separateMajorMinor: false, diff --git a/.github/workflows/create-release.yml b/.github/workflows/create-release.yml index ca08509e..9bf4be3a 100644 --- a/.github/workflows/create-release.yml +++ b/.github/workflows/create-release.yml @@ -5,7 +5,7 @@ on: workflow_dispatch: schedule: # "At 00:00 on Sunday" (https://crontab.guru/once-a-week) - - cron: "0 0 * * 0" + - cron: '0 0 * * 0' # This Workflow can be triggered manually through the GitHub UI or API. For the # API use the following request: @@ -51,6 +51,6 @@ jobs: uses: stefanzweifel/git-auto-commit-action@v4.12.0 with: commit_message: version bump - commit_user_name: "github-actions[bot]" - commit_user_email: "github-actions[bot]@users.noreply.github.com" - commit_author: "github-actions[bot] " + commit_user_name: 'github-actions[bot]' + commit_user_email: 'github-actions[bot]@users.noreply.github.com' + commit_author: 'github-actions[bot] ' diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100644 index 00000000..77a5cc0e --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,7 @@ +#!/bin/sh +. "$(dirname $0)/_/husky.sh" + +git stash -q --keep-index +npm run format +git add . +git stash pop -q diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 00000000..74ccaa93 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,10 @@ +# We prefer our own custom formatting for MarkDown files. +# See the following thread for the discussion: +# https://github.com/simple-icons/simple-icons-font/pull/73 +*.md + +# We use our own formatting for the data files. +_data/simple-icons.json + +# JavaScript templates are invalid JavaScript so cannot be formatted. +scripts/build/templates/*.js diff --git a/.prettierrc.js b/.prettierrc.js new file mode 100644 index 00000000..9166a044 --- /dev/null +++ b/.prettierrc.js @@ -0,0 +1,4 @@ +module.exports = { + trailingComma: 'all', + singleQuote: true, +}; diff --git a/.svglintrc.js b/.svglintrc.js index 45b1be49..d9ec9f12 100644 --- a/.svglintrc.js +++ b/.svglintrc.js @@ -1,13 +1,14 @@ const fs = require('fs'); -const data = require("./_data/simple-icons.json"); -const { htmlFriendlyToTitle } = require("./scripts/utils.js"); -const htmlNamedEntities = require("named-html-entities-json"); -const svgpath = require("svgpath"); -const svgPathBbox = require("svg-path-bbox"); -const parsePath = require("svg-path-segments"); +const data = require('./_data/simple-icons.json'); +const { htmlFriendlyToTitle } = require('./scripts/utils.js'); +const htmlNamedEntities = require('named-html-entities-json'); +const svgpath = require('svgpath'); +const svgPathBbox = require('svg-path-bbox'); +const parsePath = require('svg-path-segments'); -const svgRegexp = /^.*<\/title><path d=".*"\/><\/svg>\r?\n?$/; +const svgRegexp = + /^<svg( [^\s]*=".*"){3}><title>.*<\/title><path d=".*"\/><\/svg>\r?\n?$/; const negativeZerosRegexp = /-0(?=[^\.]|[\s\d\w]|$)/g; const iconSize = 24; @@ -17,46 +18,46 @@ const iconTolerance = 0.001; // set env SI_UPDATE_IGNORE to recreate the ignore file const updateIgnoreFile = process.env.SI_UPDATE_IGNORE === 'true'; -const ignoreFile = "./.svglint-ignored.json"; +const ignoreFile = './.svglint-ignored.json'; const iconIgnored = !updateIgnoreFile ? require(ignoreFile) : {}; -function sortObjectByKey(obj) { - return Object - .keys(obj) +const sortObjectByKey = (obj) => { + return Object.keys(obj) .sort() .reduce((r, k) => Object.assign(r, { [k]: obj[k] }), {}); -} +}; -function sortObjectByValue(obj) { - return Object - .keys(obj) +const sortObjectByValue = (obj) => { + return Object.keys(obj) .sort((a, b) => ('' + obj[a]).localeCompare(obj[b])) .reduce((r, k) => Object.assign(r, { [k]: obj[k] }), {}); -} +}; -function removeLeadingZeros(number) { +const removeLeadingZeros = (number) => { // convert 0.03 to '.03' return number.toString().replace(/^(-?)(0)(\.?.+)/, '$1$3'); -} +}; /** * Given three points, returns if the middle one (x2, y2) is collinear * to the line formed by the two limit points. **/ -function collinear(x1, y1, x2, y2, x3, y3) { - return (x1 * (y2 - y3) + x2 * (y3 - y1) + x3 * (y1 - y2)) === 0; -} +const collinear = (x1, y1, x2, y2, x3, y3) => { + return x1 * (y2 - y3) + x2 * (y3 - y1) + x3 * (y1 - y2) === 0; +}; /** * Returns the number of digits after the decimal point. * @param num The number of interest. */ -function countDecimals(num) { +const countDecimals = (num) => { if (num && num % 1) { let [base, op, trail] = num.toExponential().split(/e([+-])/); let elen = parseInt(trail, 10); let idx = base.indexOf('.'); - return idx == -1 ? elen : base.length - idx - 1 + (op === '+' ? -elen : elen); + return idx == -1 + ? elen + : base.length - idx - 1 + (op === '+' ? -elen : elen); } return 0; }; @@ -65,674 +66,839 @@ function countDecimals(num) { * Get the index at which the first path value of an SVG starts. * @param svgFileContent The raw SVG as text. */ -function getPathDIndex(svgFileContent) { +const getPathDIndex = (svgFileContent) => { const pathDStart = '<path d="'; return svgFileContent.indexOf(pathDStart) + pathDStart.length; -} +}; /** * Get the index at which the text of the first `<title>` tag starts. * @param svgFileContent The raw SVG as text. **/ -function getTitleTextIndex(svgFileContent) { +const getTitleTextIndex = (svgFileContent) => { const titleStart = ''; return svgFileContent.indexOf(titleStart) + titleStart.length; -} +}; /** * Convert a hexadecimal number passed as string to decimal number as integer. * @param hex The hexadecimal number representation to convert. **/ -function hexadecimalToDecimal(hex) { - let result = 0, digitValue; +const hexadecimalToDecimal = (hex) => { + let result = 0, + digitValue; hex = hex.toLowerCase(); for (var i = 0; i < hex.length; i++) { digitValue = '0123456789abcdefgh'.indexOf(hex[i]); result = result * 16 + digitValue; } return result; -} +}; if (updateIgnoreFile) { process.on('exit', () => { // ensure object output order is consistent due to async svglint processing - const sorted = sortObjectByKey(iconIgnored) + const sorted = sortObjectByKey(iconIgnored); for (const linterName in sorted) { - sorted[linterName] = sortObjectByValue(sorted[linterName]) + sorted[linterName] = sortObjectByValue(sorted[linterName]); } - fs.writeFileSync( - ignoreFile, - JSON.stringify(sorted, null, 2) + '\n', - {flag: 'w'} - ); + fs.writeFileSync(ignoreFile, JSON.stringify(sorted, null, 2) + '\n', { + flag: 'w', + }); }); } -function isIgnored(linterName, path) { - return iconIgnored[linterName] && iconIgnored[linterName].hasOwnProperty(path); -} +const isIgnored = (linterName, path) => { + return ( + iconIgnored[linterName] && iconIgnored[linterName].hasOwnProperty(path) + ); +}; -function ignoreIcon(linterName, path, $) { +const ignoreIcon = (linterName, path, $) => { if (!iconIgnored[linterName]) { iconIgnored[linterName] = {}; } - const title = $.find("title").text(); + const title = $.find('title').text(); const iconName = htmlFriendlyToTitle(title); iconIgnored[linterName][path] = iconName; -} +}; 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, + 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: [ + (reporter, $, ast) => { + reporter.name = 'icon-title'; + + const iconTitleText = $.find('title').text(), + xmlNamedEntitiesCodepoints = [38, 60, 62], + xmlNamedEntities = ['amp', 'lt', 'gt']; + let _validCodepointsRepr = true; + + // avoid character codepoints as hexadecimal representation + const hexadecimalCodepoints = Array.from( + iconTitleText.matchAll(/&#x([A-Fa-f0-9]+);/g), + ); + if (hexadecimalCodepoints.length > 0) { + _validCodepointsRepr = false; + + hexadecimalCodepoints.forEach((match) => { + const charHexReprIndex = + getTitleTextIndex(ast.source) + match.index + 1; + const charDec = hexadecimalToDecimal(match[1]); + + let charRepr; + if (xmlNamedEntitiesCodepoints.includes(charDec)) { + charRepr = `&${ + xmlNamedEntities[xmlNamedEntitiesCodepoints.indexOf(charDec)] + };`; + } else if (charDec < 128) { + charRepr = String.fromCodePoint(charDec); + } else { + charRepr = `&#${charDec};`; } - ], - custom: [ - function(reporter, $, ast) { - reporter.name = "icon-title"; - const iconTitleText = $.find("title").text(), - xmlNamedEntitiesCodepoints = [38, 60, 62], - xmlNamedEntities = ["amp", "lt", "gt"]; - let _validCodepointsRepr = true; - - // avoid character codepoints as hexadecimal representation - const hexadecimalCodepoints = Array.from( - iconTitleText.matchAll(/&#x([A-Fa-f0-9]+);/g) + reporter.error( + `Hexadecimal representation of encoded character "${match[0]}" found at index ${charHexReprIndex}:` + + ` replace it with "${charRepr}".`, ); - if (hexadecimalCodepoints.length > 0) { + }); + } + + // avoid character codepoints as named entities + const namedEntitiesCodepoints = Array.from( + iconTitleText.matchAll(/&([A-Za-z0-9]+);/g), + ); + if (namedEntitiesCodepoints.length > 0) { + namedEntitiesCodepoints.forEach((match) => { + const namedEntiyReprIndex = + getTitleTextIndex(ast.source) + match.index + 1; + + if (!xmlNamedEntities.includes(match[1].toLowerCase())) { _validCodepointsRepr = false; + const namedEntityJsRepr = htmlNamedEntities[match[1]]; + let replacement; - hexadecimalCodepoints.forEach(match => { - const charHexReprIndex = getTitleTextIndex(ast.source) + match.index + 1; - const charDec = hexadecimalToDecimal(match[1]); - - let charRepr; - if (xmlNamedEntitiesCodepoints.includes(charDec)) { - charRepr = `&${xmlNamedEntities[xmlNamedEntitiesCodepoints.indexOf(charDec)]};`; - } else if (charDec < 128) { - charRepr = String.fromCodePoint(charDec); + if ( + namedEntityJsRepr === undefined || + namedEntityJsRepr.length != 1 + ) { + replacement = 'its decimal or literal representation'; + } else { + const namedEntityDec = namedEntityJsRepr.codePointAt(0); + if (namedEntityDec < 128) { + replacement = `"${namedEntityJsRepr}"`; } else { - charRepr = `&#${charDec};`; - } - - reporter.error( - `Hexadecimal representation of encoded character "${match[0]}" found at index ${charHexReprIndex}:` - + ` replace it with "${charRepr}".` - ); - }) - } - - // avoid character codepoints as named entities - const namedEntitiesCodepoints = Array.from( - iconTitleText.matchAll(/&([A-Za-z0-9]+);/g) - ); - if (namedEntitiesCodepoints.length > 0) { - namedEntitiesCodepoints.forEach(match => { - const namedEntiyReprIndex = getTitleTextIndex(ast.source) + match.index + 1; - - if (!xmlNamedEntities.includes(match[1].toLowerCase())) { - _validCodepointsRepr = false; - const namedEntityJsRepr = htmlNamedEntities[match[1]]; - let replacement; - - if (namedEntityJsRepr === undefined || namedEntityJsRepr.length != 1) { - replacement = 'its decimal or literal representation'; - } else { - const namedEntityDec = namedEntityJsRepr.codePointAt(0); - if (namedEntityDec < 128) { - replacement = `"${namedEntityJsRepr}"`; - } else { - replacement = `"&#${namedEntityDec};"`; - } - } - - reporter.error( - `Named entity representation of encoded character "${match[0]}" found at index ${namedEntiyReprIndex}.` - + ` Replace it with ${replacement}.` - ); - } - }) - } - - if (_validCodepointsRepr) { - // compare encoded title with original title and report error if not equal - const encodingMatches = Array.from(iconTitleText.matchAll(/&(#([0-9]+)|(amp|quot|lt|gt));/g)), - encodedBuf = []; - - const _indexesToIgnore = []; - for (let m = 0; m < encodingMatches.length; m++) { - let index = encodingMatches[m].index; - for (let r = index; r < index + encodingMatches[m][0].length; r++) { - _indexesToIgnore.push(r) + replacement = `"&#${namedEntityDec};"`; } } - for (let i = iconTitleText.length - 1; i >= 0; i--) { - if (_indexesToIgnore.includes(i)) { - encodedBuf.unshift(iconTitleText[i]); - } else { - // encode all non ascii characters plus "'&<> (XML named entities) - let charDecimalCode = iconTitleText.charCodeAt(i); - - if (charDecimalCode > 127) { - encodedBuf.unshift(`&#${charDecimalCode};`); - } else if (xmlNamedEntitiesCodepoints.includes(charDecimalCode)) { - encodedBuf.unshift( - `&${xmlNamedEntities[xmlNamedEntitiesCodepoints.indexOf(charDecimalCode)]};` - ); - } else { - encodedBuf.unshift(iconTitleText[i]); - } - } - } - const encodedIconTitleText = encodedBuf.join(''); - if (encodedIconTitleText !== iconTitleText) { - _validCodepointsRepr = false; - - reporter.error( - `Unencoded unicode characters found in title "${iconTitleText}":` - + ` rewrite it as "${encodedIconTitleText}".` - ); - } - - // check if there are some other encoded characters in decimal notation - // which shouldn't be encoded - encodingMatches.filter(m => !isNaN(m[2])).forEach(match => { - const decimalNumber = parseInt(match[2]); - if (decimalNumber < 128) { - _validCodepointsRepr = false; - - const decimalCodepointCharIndex = getTitleTextIndex(ast.source) + match.index + 1; - if (xmlNamedEntitiesCodepoints.includes(decimalNumber)) { - replacement = `"&${xmlNamedEntities[xmlNamedEntitiesCodepoints.indexOf(decimalNumber)]};"`; - } else { - replacement = String.fromCharCode(decimalNumber); - replacement = replacement == '"' ? `'"'` : `"${replacement}"`; - } - - reporter.error( - `Unnecessary encoded character "${match[0]}" found at index ${decimalCodepointCharIndex}:` - + ` replace it with ${replacement}.` - ); - } - }); - - if (_validCodepointsRepr) { - const iconName = htmlFriendlyToTitle(iconTitleText); - const iconExists = data.icons.some(icon => icon.title === iconName); - if (!iconExists) { - reporter.error(`No icon with title "${iconName}" found in simple-icons.json`); - } - } + reporter.error( + `Named entity representation of encoded character "${match[0]}" found at index ${namedEntiyReprIndex}.` + + ` Replace it with ${replacement}.`, + ); } - }, - function(reporter, $, ast) { - reporter.name = "icon-size"; + }); + } - const iconPath = $.find("path").attr("d"); - if (!updateIgnoreFile && isIgnored(reporter.name, iconPath)) { - return; - } + if (_validCodepointsRepr) { + // compare encoded title with original title and report error if not equal + const encodingMatches = Array.from( + iconTitleText.matchAll(/&(#([0-9]+)|(amp|quot|lt|gt));/g), + ), + encodedBuf = []; - const [minX, minY, maxX, maxY] = svgPathBbox(iconPath); - const width = +(maxX - minX).toFixed(iconFloatPrecision); - const height = +(maxY - minY).toFixed(iconFloatPrecision); - - if (width === 0 && height === 0) { - reporter.error("Path bounds were reported as 0 x 0; check if the path is valid"); - if (updateIgnoreFile) { - ignoreIcon(reporter.name, iconPath, $); - } - } 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}`); - if (updateIgnoreFile) { - ignoreIcon(reporter.name, iconPath, $); - } - } - }, - function(reporter, $, ast) { - reporter.name = "icon-precision"; - - const iconPath = $.find("path").attr("d"); - if (!updateIgnoreFile && isIgnored(reporter.name, iconPath)) { - return; - } - - const segments = parsePath(iconPath), - svgFileContent = $.html(); - - segments.forEach((segment) => { - const precisionMax = Math.max(...segment.params.slice(1).map(countDecimals)); - if (precisionMax > iconMaxFloatPrecision) { - let errorMsg = `found ${precisionMax} decimals in segment "${iconPath.substring(segment.start, segment.end)}"`; - if (segment.chained) { - let readableChain = iconPath.substring(segment.chainStart, segment.chainEnd); - if (readableChain.length > 20) { - readableChain = `${readableChain.substring(0, 20)}...`; - } - errorMsg += ` of chain "${readableChain}"` - } - errorMsg += ` at index ${segment.start + getPathDIndex(svgFileContent)}`; - reporter.error(`Maximum precision should not be greater than ${iconMaxFloatPrecision}; ${errorMsg}`); - if (updateIgnoreFile) { - ignoreIcon(reporter.name, iconPath, $); - } - } - }) - }, - function(reporter, $, ast) { - reporter.name = "ineffective-segments"; - - const iconPath = $.find("path").attr("d"); - if (!updateIgnoreFile && isIgnored(reporter.name, iconPath)) { - return; - } - - const segments = parsePath(iconPath); - const absSegments = svgpath(iconPath).abs().unshort().segments; - - const lowerMovementCommands = ['m', 'l']; - const lowerDirectionCommands = ['h', 'v']; - const lowerCurveCommand = 'c'; - const lowerShorthandCurveCommand = 's'; - const lowerCurveCommands = [lowerCurveCommand, lowerShorthandCurveCommand]; - const upperMovementCommands = ['M', 'L']; - const upperHorDirectionCommand = 'H'; - const upperVerDirectionCommand = 'V'; - const upperDirectionCommands = [upperHorDirectionCommand, upperVerDirectionCommand]; - const upperCurveCommand = 'C'; - const upperShorthandCurveCommand = 'S'; - const upperCurveCommands = [upperCurveCommand, upperShorthandCurveCommand]; - const curveCommands = [...lowerCurveCommands, ...upperCurveCommands]; - const commands = [...lowerMovementCommands, ...lowerDirectionCommands, ...upperMovementCommands, ...upperDirectionCommands, ...curveCommands]; - const isInvalidSegment = ([command, x1Coord, y1Coord, ...rest], index) => { - if (commands.includes(command)) { - // Relative directions (h or v) having a length of 0 - if (lowerDirectionCommands.includes(command) && x1Coord === 0) { - return true; - } - // Relative movement (m or l) having a distance of 0 - if (index > 0 && lowerMovementCommands.includes(command) && x1Coord === 0 && y1Coord === 0) { - return true; - } - if (lowerCurveCommands.includes(command) && x1Coord === 0 && y1Coord === 0) { - const [x2Coord, y2Coord] = rest; - if ( - // Relative shorthand curve (s) having a control point of 0 - command === lowerShorthandCurveCommand || - // Relative bézier curve (c) having control points of 0 - (command === lowerCurveCommand && x2Coord === 0 && y2Coord === 0) - ) { - return true; - } - } - if (index > 0) { - let [yPrevCoord, xPrevCoord] = [...absSegments[index - 1]].reverse(); - // If the previous command was a direction one, we need to iterate back until we find the missing coordinates - if (upperDirectionCommands.includes(xPrevCoord)) { - xPrevCoord = undefined; - yPrevCoord = undefined; - let idx = index; - while (--idx > 0 && (xPrevCoord === undefined || yPrevCoord === undefined)) { - let [yPrevCoordDeep, xPrevCoordDeep] = [...absSegments[idx]].reverse(); - // If the previous command was a horizontal movement, we need to consider the single coordinate as x - if (upperHorDirectionCommand === xPrevCoordDeep) { - xPrevCoordDeep = yPrevCoordDeep; - yPrevCoordDeep = undefined; - } - // If the previous command was a vertical movement, we need to consider the single coordinate as y - if (upperVerDirectionCommand === xPrevCoordDeep) { - xPrevCoordDeep = undefined; - } - if (xPrevCoord === undefined && xPrevCoordDeep !== undefined) { - xPrevCoord = xPrevCoordDeep; - } - if (yPrevCoord === undefined && yPrevCoordDeep !== undefined) { - yPrevCoord = yPrevCoordDeep; - } - } - } - - if (upperCurveCommands.includes(command)) { - const [x2Coord, y2Coord, xCoord, yCoord] = rest; - // Absolute shorthand curve (S) having the same coordinate as the previous segment and a control point equal to the ending point - if (upperShorthandCurveCommand === command && x1Coord === xPrevCoord && y1Coord === yPrevCoord && x1Coord === x2Coord && y1Coord === y2Coord) { - return true; - } - // Absolute bézier curve (C) having the same coordinate as the previous segment and last control point equal to the ending point - if (upperCurveCommand === command && x1Coord === xPrevCoord && y1Coord === yPrevCoord && x2Coord === xCoord && y2Coord === yCoord) { - return true; - } - } - - return ( - // Absolute horizontal direction (H) having the same x coordinate as the previous segment - (upperHorDirectionCommand === command && x1Coord === xPrevCoord) || - // Absolute vertical direction (V) having the same y coordinate as the previous segment - (upperVerDirectionCommand === command && x1Coord === yPrevCoord) || - // Absolute movement (M or L) having the same coordinate as the previous segment - (upperMovementCommands.includes(command) && x1Coord === xPrevCoord && y1Coord === yPrevCoord) - ); - } - } - }; - - const svgFileContent = $.html(); - - segments.forEach((segment, index) => { - if (isInvalidSegment(segment.params, index)) { - const [command, x1, y1, ...rest] = segment.params; - - let errorMsg = `Innefective segment "${iconPath.substring(segment.start, segment.end)}" found`, - resolutionTip = 'should be removed'; - - if (curveCommands.includes(command)) { - const [x2, y2, x, y] = rest; - - if (command === lowerShorthandCurveCommand && (x2 !== 0 || y2 !== 0)) { - resolutionTip = `should be "l${removeLeadingZeros(x2)} ${removeLeadingZeros(y2)}" or removed`; - } - if (command === upperShorthandCurveCommand) { - resolutionTip = `should be "L${removeLeadingZeros(x2)} ${removeLeadingZeros(y2)}" or removed`; - } - if (command === lowerCurveCommand && (x !== 0 || y !== 0)) { - resolutionTip = `should be "l${removeLeadingZeros(x)} ${removeLeadingZeros(y)}" or removed`; - } - if (command === upperCurveCommand) { - resolutionTip = `should be "L${removeLeadingZeros(x)} ${removeLeadingZeros(y)}" or removed`; - } - }; - - if (segment.chained) { - let readableChain = iconPath.substring(segment.chainStart, segment.chainEnd); - if (readableChain.length > 20) { - readableChain = `${chain.substring(0, 20)}...` - } - errorMsg += ` in chain "${readableChain}"` - } - errorMsg += ` at index ${segment.start + getPathDIndex(svgFileContent)}`; - - reporter.error(`${errorMsg} (${resolutionTip})`); - - if (updateIgnoreFile) { - ignoreIcon(reporter.name, iconPath, $); - } - } - }) - }, - function(reporter, $, ast) { - reporter.name = "collinear-segments"; - - const iconPath = $.find("path").attr("d"); - if (!updateIgnoreFile && isIgnored(reporter.name, iconPath)) { - return; - } - - /** - * Extracts collinear coordinates from SVG path straight lines - * (does not extracts collinear coordinates from curves). - **/ - const getCollinearSegments = (iconPath) => { - const segments = parsePath(iconPath), - collinearSegments = [], - straightLineCommands = 'HhVvLlMm', - zCommands = 'Zz'; - - let currLine = [], - currAbsCoord = [undefined, undefined], - startPoint, - _inStraightLine = false, - _nextInStraightLine = false, - _resetStartPoint = false; - - for (let s = 0; s < segments.length; s++) { - let seg = segments[s].params, - cmd = seg[0], - nextCmd = s + 1 < segments.length ? segments[s + 1][0] : null; - - if (cmd === 'L') { - currAbsCoord[0] = seg[1]; - currAbsCoord[1] = seg[2]; - } else if (cmd === 'l') { - currAbsCoord[0] = (!currAbsCoord[0] ? 0 : currAbsCoord[0]) + seg[1]; - currAbsCoord[1] = (!currAbsCoord[1] ? 0 : currAbsCoord[1]) + seg[2]; - } else if (cmd === 'm') { - currAbsCoord[0] = (!currAbsCoord[0] ? 0 : currAbsCoord[0]) + seg[1]; - currAbsCoord[1] = (!currAbsCoord[1] ? 0 : currAbsCoord[1]) + seg[2]; - startPoint = undefined; - } else if (cmd === 'M') { - currAbsCoord[0] = seg[1]; - currAbsCoord[1] = seg[2]; - startPoint = undefined; - } else if (cmd === 'H') { - currAbsCoord[0] = seg[1]; - } else if (cmd === 'h') { - currAbsCoord[0] = (!currAbsCoord[0] ? 0 : currAbsCoord[0]) + seg[1]; - } else if (cmd === 'V') { - currAbsCoord[1] = seg[1]; - } else if (cmd === 'v') { - currAbsCoord[1] = (!currAbsCoord[1] ? 0 : currAbsCoord[1]) + seg[1]; - } else if (cmd === 'C') { - currAbsCoord[0] = seg[5]; - currAbsCoord[1] = seg[6]; - } else if (cmd === "a") { - currAbsCoord[0] = (!currAbsCoord[0] ? 0 : currAbsCoord[0]) + seg[6]; - currAbsCoord[1] = (!currAbsCoord[1] ? 0 : currAbsCoord[1]) + seg[7]; - } else if (cmd === "A") { - currAbsCoord[0] = seg[6]; - currAbsCoord[1] = seg[7]; - } else if (cmd === "s") { - currAbsCoord[0] = (!currAbsCoord[0] ? 0 : currAbsCoord[0]) + seg[1]; - currAbsCoord[1] = (!currAbsCoord[1] ? 0 : currAbsCoord[1]) + seg[2]; - } else if (cmd === "S") { - currAbsCoord[0] = seg[1]; - currAbsCoord[1] = seg[2]; - } else if (cmd === "t") { - currAbsCoord[0] = (!currAbsCoord[0] ? 0 : currAbsCoord[0]) + seg[1]; - currAbsCoord[1] = (!currAbsCoord[1] ? 0 : currAbsCoord[1]) + seg[2]; - } else if (cmd === "T") { - currAbsCoord[0] = seg[1]; - currAbsCoord[1] = seg[2]; - } else if (cmd === 'c') { - currAbsCoord[0] = (!currAbsCoord[0] ? 0 : currAbsCoord[0]) + seg[5]; - currAbsCoord[1] = (!currAbsCoord[1] ? 0 : currAbsCoord[1]) + seg[6]; - } else if (cmd === 'Q') { - currAbsCoord[0] = seg[3]; - currAbsCoord[1] = seg[4]; - } else if (cmd === 'q') { - currAbsCoord[0] = (!currAbsCoord[0] ? 0 : currAbsCoord[0]) + seg[3]; - currAbsCoord[1] = (!currAbsCoord[1] ? 0 : currAbsCoord[1]) + seg[4]; - } else if (zCommands.includes(cmd)) { - // Overlapping in Z should be handled in another rule - currAbsCoord = [startPoint[0], startPoint[1]]; - _resetStartPoint = true; - } else { - throw new Error(`"${cmd}" command not handled`); - } - - if (startPoint === undefined) { - startPoint = [currAbsCoord[0], currAbsCoord[1]]; - } else if (_resetStartPoint) { - startPoint = undefined; - _resetStartPoint = false; - } - - _nextInStraightLine = straightLineCommands.includes(nextCmd); - let _exitingStraightLine = (_inStraightLine && !_nextInStraightLine); - _inStraightLine = straightLineCommands.includes(cmd); - - if (_inStraightLine) { - currLine.push([currAbsCoord[0], currAbsCoord[1]]); - } else { - if (_exitingStraightLine) { - if (straightLineCommands.includes(cmd)) { - currLine.push([currAbsCoord[0], currAbsCoord[1]]); - } - // Get collinear coordinates - for (let p = 1; p < currLine.length - 1; p++) { - let _collinearCoord = collinear(currLine[p - 1][0], - currLine[p - 1][1], - currLine[p][0], - currLine[p][1], - currLine[p + 1][0], - currLine[p + 1][1]); - if (_collinearCoord) { - collinearSegments.push( - segments[s - currLine.length + p + 1] - ); - } - } - } - currLine = []; - } - } - - return collinearSegments; - } - - const collinearSegments = getCollinearSegments(iconPath), - pathDIndex = getPathDIndex($.html()); - collinearSegments.forEach((segment) => { - let errorMsg = `Collinear segment "${iconPath.substring(segment.start, segment.end)}" found` - if (segment.chained) { - let readableChain = iconPath.substring(segment.chainStart, segment.chainEnd); - if (readableChain.length > 20) { - readableChain = `${readableChain.substring(0, 20)}...` - } - errorMsg += ` in chain "${readableChain}"`; - } - errorMsg += ` at index ${segment.start + pathDIndex} (should be removed)`; - reporter.error(errorMsg); - }); - - if (collinearSegments.length) { - if (updateIgnoreFile) { - ignoreIcon(reporter.name, iconPath, $); - } - } - }, - function(reporter, $, ast) { - reporter.name = "extraneous"; - - if (!svgRegexp.test(ast.source)) { - reporter.error("Unexpected character(s), most likely extraneous whitespace, detected in SVG markup"); - } - }, - function(reporter, $, ast) { - reporter.name = "negative-zeros"; - - const iconPath = $.find("path").attr("d"); - if (!updateIgnoreFile && isIgnored(reporter.name, iconPath)) { - return; - } - - // Find negative zeros inside path - const negativeZeroMatches = Array.from(iconPath.matchAll(negativeZerosRegexp)); - if (negativeZeroMatches.length) { - // Calculate the index for each match in the file - const svgFileContent = $.html(); - const pathDIndex = getPathDIndex(svgFileContent); - - negativeZeroMatches.forEach((match) => { - const negativeZeroFileIndex = match.index + pathDIndex; - const previousChar = svgFileContent[negativeZeroFileIndex - 1]; - const replacement = "0123456789".includes(previousChar) ? " 0" : "0"; - reporter.error(`Found "-0" at index ${negativeZeroFileIndex} (should be "${replacement}")`); - }) - } - }, - function(reporter, $, ast) { - reporter.name = "icon-centered"; - - const iconPath = $.find("path").attr("d"); - if (!updateIgnoreFile && isIgnored(reporter.name, iconPath)) { - return; - } - - const [minX, minY, maxX, maxY] = svgPathBbox(iconPath); - const targetCenter = iconSize / 2; - const centerX = +((minX + maxX) / 2).toFixed(iconFloatPrecision); - const devianceX = centerX - targetCenter; - const centerY = +((minY + 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})`); - if (updateIgnoreFile) { - ignoreIcon(reporter.name, iconPath, $); - } - } - }, - function(reporter, $, ast) { - reporter.name = "path-format"; - - const iconPath = $.find("path").attr("d"); - - const validPathFormatRegex = /^[Mm][MmZzLlHhVvCcSsQqTtAaEe0-9-,.\s]+$/; - if (!validPathFormatRegex.test(iconPath)) { - let errorMsg = "Invalid path format", reason; - - if (!(/^[Mm]/.test(iconPath))) { - // doesn't start with moveto - reason = `should start with \"moveto\" command (\"M\" or \"m\"), but starts with \"${iconPath[0]}\"`; - reporter.error(`${errorMsg}: ${reason}`); - } - - const validPathCharacters = "MmZzLlHhVvCcSsQqTtAaEe0123456789-,. ", - invalidCharactersMsgs = [], - pathDIndex = getPathDIndex($.html()); - - for (let [i, char] of Object.entries(iconPath)) { - if (validPathCharacters.indexOf(char) === -1) { - invalidCharactersMsgs.push(`"${char}" at index ${pathDIndex + parseInt(i)}`); - } - } - - // contains invalid characters - if (invalidCharactersMsgs.length > 0) { - reason = `unexpected character${invalidCharactersMsgs.length > 1 ? 's' : ''} found`; - reason += ` (${invalidCharactersMsgs.join(", ")})`; - reporter.error(`${errorMsg}: ${reason}`); - } - } - }, - function(reporter, $, ast) { - reporter.name = 'svg-format'; - - // Don't allow explicit '</path>' closing tag - if (ast.source.includes('</path>')) { - const reason = `found a closing "path" tag at index ${ast.source.indexOf('</path>')}.` - + ' The path should be self-closing, use \'/>\' instead of \'></path>\'.'; - reporter.error(`Invalid SVG content format: ${reason}`); + const _indexesToIgnore = []; + for (let m = 0; m < encodingMatches.length; m++) { + let index = encodingMatches[m].index; + for (let r = index; r < index + encodingMatches[m][0].length; r++) { + _indexesToIgnore.push(r); } } - ] - } + + for (let i = iconTitleText.length - 1; i >= 0; i--) { + if (_indexesToIgnore.includes(i)) { + encodedBuf.unshift(iconTitleText[i]); + } else { + // encode all non ascii characters plus "'&<> (XML named entities) + let charDecimalCode = iconTitleText.charCodeAt(i); + + if (charDecimalCode > 127) { + encodedBuf.unshift(`&#${charDecimalCode};`); + } else if (xmlNamedEntitiesCodepoints.includes(charDecimalCode)) { + encodedBuf.unshift( + `&${ + xmlNamedEntities[ + xmlNamedEntitiesCodepoints.indexOf(charDecimalCode) + ] + };`, + ); + } else { + encodedBuf.unshift(iconTitleText[i]); + } + } + } + const encodedIconTitleText = encodedBuf.join(''); + if (encodedIconTitleText !== iconTitleText) { + _validCodepointsRepr = false; + + reporter.error( + `Unencoded unicode characters found in title "${iconTitleText}":` + + ` rewrite it as "${encodedIconTitleText}".`, + ); + } + + // check if there are some other encoded characters in decimal notation + // which shouldn't be encoded + encodingMatches + .filter((m) => !isNaN(m[2])) + .forEach((match) => { + const decimalNumber = parseInt(match[2]); + if (decimalNumber < 128) { + _validCodepointsRepr = false; + + const decimalCodepointCharIndex = + getTitleTextIndex(ast.source) + match.index + 1; + if (xmlNamedEntitiesCodepoints.includes(decimalNumber)) { + replacement = `"&${ + xmlNamedEntities[ + xmlNamedEntitiesCodepoints.indexOf(decimalNumber) + ] + };"`; + } else { + replacement = String.fromCharCode(decimalNumber); + replacement = replacement == '"' ? `'"'` : `"${replacement}"`; + } + + reporter.error( + `Unnecessary encoded character "${match[0]}" found at index ${decimalCodepointCharIndex}:` + + ` replace it with ${replacement}.`, + ); + } + }); + + if (_validCodepointsRepr) { + const iconName = htmlFriendlyToTitle(iconTitleText); + const iconExists = data.icons.some( + (icon) => icon.title === iconName, + ); + if (!iconExists) { + reporter.error( + `No icon with title "${iconName}" found in simple-icons.json`, + ); + } + } + } + }, + (reporter, $, ast) => { + reporter.name = 'icon-size'; + + const iconPath = $.find('path').attr('d'); + if (!updateIgnoreFile && isIgnored(reporter.name, iconPath)) { + return; + } + + const [minX, minY, maxX, maxY] = svgPathBbox(iconPath); + const width = +(maxX - minX).toFixed(iconFloatPrecision); + const height = +(maxY - minY).toFixed(iconFloatPrecision); + + if (width === 0 && height === 0) { + reporter.error( + 'Path bounds were reported as 0 x 0; check if the path is valid', + ); + if (updateIgnoreFile) { + ignoreIcon(reporter.name, iconPath, $); + } + } 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}`, + ); + if (updateIgnoreFile) { + ignoreIcon(reporter.name, iconPath, $); + } + } + }, + (reporter, $, ast) => { + reporter.name = 'icon-precision'; + + const iconPath = $.find('path').attr('d'); + if (!updateIgnoreFile && isIgnored(reporter.name, iconPath)) { + return; + } + + const segments = parsePath(iconPath), + svgFileContent = $.html(); + + segments.forEach((segment) => { + const precisionMax = Math.max( + ...segment.params.slice(1).map(countDecimals), + ); + if (precisionMax > iconMaxFloatPrecision) { + let errorMsg = `found ${precisionMax} decimals in segment "${iconPath.substring( + segment.start, + segment.end, + )}"`; + if (segment.chained) { + let readableChain = iconPath.substring( + segment.chainStart, + segment.chainEnd, + ); + if (readableChain.length > 20) { + readableChain = `${readableChain.substring(0, 20)}...`; + } + errorMsg += ` of chain "${readableChain}"`; + } + errorMsg += ` at index ${ + segment.start + getPathDIndex(svgFileContent) + }`; + reporter.error( + `Maximum precision should not be greater than ${iconMaxFloatPrecision}; ${errorMsg}`, + ); + if (updateIgnoreFile) { + ignoreIcon(reporter.name, iconPath, $); + } + } + }); + }, + (reporter, $, ast) => { + reporter.name = 'ineffective-segments'; + + const iconPath = $.find('path').attr('d'); + if (!updateIgnoreFile && isIgnored(reporter.name, iconPath)) { + return; + } + + const segments = parsePath(iconPath); + const absSegments = svgpath(iconPath).abs().unshort().segments; + + const lowerMovementCommands = ['m', 'l']; + const lowerDirectionCommands = ['h', 'v']; + const lowerCurveCommand = 'c'; + const lowerShorthandCurveCommand = 's'; + const lowerCurveCommands = [ + lowerCurveCommand, + lowerShorthandCurveCommand, + ]; + const upperMovementCommands = ['M', 'L']; + const upperHorDirectionCommand = 'H'; + const upperVerDirectionCommand = 'V'; + const upperDirectionCommands = [ + upperHorDirectionCommand, + upperVerDirectionCommand, + ]; + const upperCurveCommand = 'C'; + const upperShorthandCurveCommand = 'S'; + const upperCurveCommands = [ + upperCurveCommand, + upperShorthandCurveCommand, + ]; + const curveCommands = [...lowerCurveCommands, ...upperCurveCommands]; + const commands = [ + ...lowerMovementCommands, + ...lowerDirectionCommands, + ...upperMovementCommands, + ...upperDirectionCommands, + ...curveCommands, + ]; + const isInvalidSegment = ( + [command, x1Coord, y1Coord, ...rest], + index, + ) => { + if (commands.includes(command)) { + // Relative directions (h or v) having a length of 0 + if (lowerDirectionCommands.includes(command) && x1Coord === 0) { + return true; + } + // Relative movement (m or l) having a distance of 0 + if ( + index > 0 && + lowerMovementCommands.includes(command) && + x1Coord === 0 && + y1Coord === 0 + ) { + return true; + } + if ( + lowerCurveCommands.includes(command) && + x1Coord === 0 && + y1Coord === 0 + ) { + const [x2Coord, y2Coord] = rest; + if ( + // Relative shorthand curve (s) having a control point of 0 + command === lowerShorthandCurveCommand || + // Relative bézier curve (c) having control points of 0 + (command === lowerCurveCommand && + x2Coord === 0 && + y2Coord === 0) + ) { + return true; + } + } + if (index > 0) { + let [yPrevCoord, xPrevCoord] = [ + ...absSegments[index - 1], + ].reverse(); + // If the previous command was a direction one, we need to iterate back until we find the missing coordinates + if (upperDirectionCommands.includes(xPrevCoord)) { + xPrevCoord = undefined; + yPrevCoord = undefined; + let idx = index; + while ( + --idx > 0 && + (xPrevCoord === undefined || yPrevCoord === undefined) + ) { + let [yPrevCoordDeep, xPrevCoordDeep] = [ + ...absSegments[idx], + ].reverse(); + // If the previous command was a horizontal movement, we need to consider the single coordinate as x + if (upperHorDirectionCommand === xPrevCoordDeep) { + xPrevCoordDeep = yPrevCoordDeep; + yPrevCoordDeep = undefined; + } + // If the previous command was a vertical movement, we need to consider the single coordinate as y + if (upperVerDirectionCommand === xPrevCoordDeep) { + xPrevCoordDeep = undefined; + } + if ( + xPrevCoord === undefined && + xPrevCoordDeep !== undefined + ) { + xPrevCoord = xPrevCoordDeep; + } + if ( + yPrevCoord === undefined && + yPrevCoordDeep !== undefined + ) { + yPrevCoord = yPrevCoordDeep; + } + } + } + + if (upperCurveCommands.includes(command)) { + const [x2Coord, y2Coord, xCoord, yCoord] = rest; + // Absolute shorthand curve (S) having the same coordinate as the previous segment and a control point equal to the ending point + if ( + upperShorthandCurveCommand === command && + x1Coord === xPrevCoord && + y1Coord === yPrevCoord && + x1Coord === x2Coord && + y1Coord === y2Coord + ) { + return true; + } + // Absolute bézier curve (C) having the same coordinate as the previous segment and last control point equal to the ending point + if ( + upperCurveCommand === command && + x1Coord === xPrevCoord && + y1Coord === yPrevCoord && + x2Coord === xCoord && + y2Coord === yCoord + ) { + return true; + } + } + + return ( + // Absolute horizontal direction (H) having the same x coordinate as the previous segment + (upperHorDirectionCommand === command && + x1Coord === xPrevCoord) || + // Absolute vertical direction (V) having the same y coordinate as the previous segment + (upperVerDirectionCommand === command && + x1Coord === yPrevCoord) || + // Absolute movement (M or L) having the same coordinate as the previous segment + (upperMovementCommands.includes(command) && + x1Coord === xPrevCoord && + y1Coord === yPrevCoord) + ); + } + } + }; + + const svgFileContent = $.html(); + + segments.forEach((segment, index) => { + if (isInvalidSegment(segment.params, index)) { + const [command, x1, y1, ...rest] = segment.params; + + let errorMsg = `Innefective segment "${iconPath.substring( + segment.start, + segment.end, + )}" found`, + resolutionTip = 'should be removed'; + + if (curveCommands.includes(command)) { + const [x2, y2, x, y] = rest; + + if ( + command === lowerShorthandCurveCommand && + (x2 !== 0 || y2 !== 0) + ) { + resolutionTip = `should be "l${removeLeadingZeros( + x2, + )} ${removeLeadingZeros(y2)}" or removed`; + } + if (command === upperShorthandCurveCommand) { + resolutionTip = `should be "L${removeLeadingZeros( + x2, + )} ${removeLeadingZeros(y2)}" or removed`; + } + if (command === lowerCurveCommand && (x !== 0 || y !== 0)) { + resolutionTip = `should be "l${removeLeadingZeros( + x, + )} ${removeLeadingZeros(y)}" or removed`; + } + if (command === upperCurveCommand) { + resolutionTip = `should be "L${removeLeadingZeros( + x, + )} ${removeLeadingZeros(y)}" or removed`; + } + } + + if (segment.chained) { + let readableChain = iconPath.substring( + segment.chainStart, + segment.chainEnd, + ); + if (readableChain.length > 20) { + readableChain = `${chain.substring(0, 20)}...`; + } + errorMsg += ` in chain "${readableChain}"`; + } + errorMsg += ` at index ${ + segment.start + getPathDIndex(svgFileContent) + }`; + + reporter.error(`${errorMsg} (${resolutionTip})`); + + if (updateIgnoreFile) { + ignoreIcon(reporter.name, iconPath, $); + } + } + }); + }, + (reporter, $, ast) => { + reporter.name = 'collinear-segments'; + + const iconPath = $.find('path').attr('d'); + if (!updateIgnoreFile && isIgnored(reporter.name, iconPath)) { + return; + } + + /** + * Extracts collinear coordinates from SVG path straight lines + * (does not extracts collinear coordinates from curves). + **/ + const getCollinearSegments = (iconPath) => { + const segments = parsePath(iconPath), + collinearSegments = [], + straightLineCommands = 'HhVvLlMm', + zCommands = 'Zz'; + + let currLine = [], + currAbsCoord = [undefined, undefined], + startPoint, + _inStraightLine = false, + _nextInStraightLine = false, + _resetStartPoint = false; + + for (let s = 0; s < segments.length; s++) { + let seg = segments[s].params, + cmd = seg[0], + nextCmd = s + 1 < segments.length ? segments[s + 1][0] : null; + + if (cmd === 'L') { + currAbsCoord[0] = seg[1]; + currAbsCoord[1] = seg[2]; + } else if (cmd === 'l') { + currAbsCoord[0] = + (!currAbsCoord[0] ? 0 : currAbsCoord[0]) + seg[1]; + currAbsCoord[1] = + (!currAbsCoord[1] ? 0 : currAbsCoord[1]) + seg[2]; + } else if (cmd === 'm') { + currAbsCoord[0] = + (!currAbsCoord[0] ? 0 : currAbsCoord[0]) + seg[1]; + currAbsCoord[1] = + (!currAbsCoord[1] ? 0 : currAbsCoord[1]) + seg[2]; + startPoint = undefined; + } else if (cmd === 'M') { + currAbsCoord[0] = seg[1]; + currAbsCoord[1] = seg[2]; + startPoint = undefined; + } else if (cmd === 'H') { + currAbsCoord[0] = seg[1]; + } else if (cmd === 'h') { + currAbsCoord[0] = + (!currAbsCoord[0] ? 0 : currAbsCoord[0]) + seg[1]; + } else if (cmd === 'V') { + currAbsCoord[1] = seg[1]; + } else if (cmd === 'v') { + currAbsCoord[1] = + (!currAbsCoord[1] ? 0 : currAbsCoord[1]) + seg[1]; + } else if (cmd === 'C') { + currAbsCoord[0] = seg[5]; + currAbsCoord[1] = seg[6]; + } else if (cmd === 'a') { + currAbsCoord[0] = + (!currAbsCoord[0] ? 0 : currAbsCoord[0]) + seg[6]; + currAbsCoord[1] = + (!currAbsCoord[1] ? 0 : currAbsCoord[1]) + seg[7]; + } else if (cmd === 'A') { + currAbsCoord[0] = seg[6]; + currAbsCoord[1] = seg[7]; + } else if (cmd === 's') { + currAbsCoord[0] = + (!currAbsCoord[0] ? 0 : currAbsCoord[0]) + seg[1]; + currAbsCoord[1] = + (!currAbsCoord[1] ? 0 : currAbsCoord[1]) + seg[2]; + } else if (cmd === 'S') { + currAbsCoord[0] = seg[1]; + currAbsCoord[1] = seg[2]; + } else if (cmd === 't') { + currAbsCoord[0] = + (!currAbsCoord[0] ? 0 : currAbsCoord[0]) + seg[1]; + currAbsCoord[1] = + (!currAbsCoord[1] ? 0 : currAbsCoord[1]) + seg[2]; + } else if (cmd === 'T') { + currAbsCoord[0] = seg[1]; + currAbsCoord[1] = seg[2]; + } else if (cmd === 'c') { + currAbsCoord[0] = + (!currAbsCoord[0] ? 0 : currAbsCoord[0]) + seg[5]; + currAbsCoord[1] = + (!currAbsCoord[1] ? 0 : currAbsCoord[1]) + seg[6]; + } else if (cmd === 'Q') { + currAbsCoord[0] = seg[3]; + currAbsCoord[1] = seg[4]; + } else if (cmd === 'q') { + currAbsCoord[0] = + (!currAbsCoord[0] ? 0 : currAbsCoord[0]) + seg[3]; + currAbsCoord[1] = + (!currAbsCoord[1] ? 0 : currAbsCoord[1]) + seg[4]; + } else if (zCommands.includes(cmd)) { + // Overlapping in Z should be handled in another rule + currAbsCoord = [startPoint[0], startPoint[1]]; + _resetStartPoint = true; + } else { + throw new Error(`"${cmd}" command not handled`); + } + + if (startPoint === undefined) { + startPoint = [currAbsCoord[0], currAbsCoord[1]]; + } else if (_resetStartPoint) { + startPoint = undefined; + _resetStartPoint = false; + } + + _nextInStraightLine = straightLineCommands.includes(nextCmd); + let _exitingStraightLine = _inStraightLine && !_nextInStraightLine; + _inStraightLine = straightLineCommands.includes(cmd); + + if (_inStraightLine) { + currLine.push([currAbsCoord[0], currAbsCoord[1]]); + } else { + if (_exitingStraightLine) { + if (straightLineCommands.includes(cmd)) { + currLine.push([currAbsCoord[0], currAbsCoord[1]]); + } + // Get collinear coordinates + for (let p = 1; p < currLine.length - 1; p++) { + let _collinearCoord = collinear( + currLine[p - 1][0], + currLine[p - 1][1], + currLine[p][0], + currLine[p][1], + currLine[p + 1][0], + currLine[p + 1][1], + ); + if (_collinearCoord) { + collinearSegments.push( + segments[s - currLine.length + p + 1], + ); + } + } + } + currLine = []; + } + } + + return collinearSegments; + }; + + const collinearSegments = getCollinearSegments(iconPath), + pathDIndex = getPathDIndex($.html()); + collinearSegments.forEach((segment) => { + let errorMsg = `Collinear segment "${iconPath.substring( + segment.start, + segment.end, + )}" found`; + if (segment.chained) { + let readableChain = iconPath.substring( + segment.chainStart, + segment.chainEnd, + ); + if (readableChain.length > 20) { + readableChain = `${readableChain.substring(0, 20)}...`; + } + errorMsg += ` in chain "${readableChain}"`; + } + errorMsg += ` at index ${ + segment.start + pathDIndex + } (should be removed)`; + reporter.error(errorMsg); + }); + + if (collinearSegments.length) { + if (updateIgnoreFile) { + ignoreIcon(reporter.name, iconPath, $); + } + } + }, + (reporter, $, ast) => { + reporter.name = 'extraneous'; + + if (!svgRegexp.test(ast.source)) { + reporter.error( + 'Unexpected character(s), most likely extraneous whitespace, detected in SVG markup', + ); + } + }, + (reporter, $, ast) => { + reporter.name = 'negative-zeros'; + + const iconPath = $.find('path').attr('d'); + if (!updateIgnoreFile && isIgnored(reporter.name, iconPath)) { + return; + } + + // Find negative zeros inside path + const negativeZeroMatches = Array.from( + iconPath.matchAll(negativeZerosRegexp), + ); + if (negativeZeroMatches.length) { + // Calculate the index for each match in the file + const svgFileContent = $.html(); + const pathDIndex = getPathDIndex(svgFileContent); + + negativeZeroMatches.forEach((match) => { + const negativeZeroFileIndex = match.index + pathDIndex; + const previousChar = svgFileContent[negativeZeroFileIndex - 1]; + const replacement = '0123456789'.includes(previousChar) + ? ' 0' + : '0'; + reporter.error( + `Found "-0" at index ${negativeZeroFileIndex} (should be "${replacement}")`, + ); + }); + } + }, + (reporter, $, ast) => { + reporter.name = 'icon-centered'; + + const iconPath = $.find('path').attr('d'); + if (!updateIgnoreFile && isIgnored(reporter.name, iconPath)) { + return; + } + + const [minX, minY, maxX, maxY] = svgPathBbox(iconPath); + const targetCenter = iconSize / 2; + const centerX = +((minX + maxX) / 2).toFixed(iconFloatPrecision); + const devianceX = centerX - targetCenter; + const centerY = +((minY + 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})`, + ); + if (updateIgnoreFile) { + ignoreIcon(reporter.name, iconPath, $); + } + } + }, + (reporter, $, ast) => { + reporter.name = 'path-format'; + + const iconPath = $.find('path').attr('d'); + + const validPathFormatRegex = /^[Mm][MmZzLlHhVvCcSsQqTtAaEe0-9-,.\s]+$/; + if (!validPathFormatRegex.test(iconPath)) { + let errorMsg = 'Invalid path format', + reason; + + if (!/^[Mm]/.test(iconPath)) { + // doesn't start with moveto + reason = `should start with \"moveto\" command (\"M\" or \"m\"), but starts with \"${iconPath[0]}\"`; + reporter.error(`${errorMsg}: ${reason}`); + } + + const validPathCharacters = 'MmZzLlHhVvCcSsQqTtAaEe0123456789-,. ', + invalidCharactersMsgs = [], + pathDIndex = getPathDIndex($.html()); + + for (let [i, char] of Object.entries(iconPath)) { + if (validPathCharacters.indexOf(char) === -1) { + invalidCharactersMsgs.push( + `"${char}" at index ${pathDIndex + parseInt(i)}`, + ); + } + } + + // contains invalid characters + if (invalidCharactersMsgs.length > 0) { + reason = `unexpected character${ + invalidCharactersMsgs.length > 1 ? 's' : '' + } found`; + reason += ` (${invalidCharactersMsgs.join(', ')})`; + reporter.error(`${errorMsg}: ${reason}`); + } + } + }, + (reporter, $, ast) => { + reporter.name = 'svg-format'; + + // Don't allow explicit '</path>' closing tag + if (ast.source.includes('</path>')) { + const reason = + `found a closing "path" tag at index ${ast.source.indexOf( + '</path>', + )}.` + + " The path should be self-closing, use '/>' instead of '></path>'."; + reporter.error(`Invalid SVG content format: ${reason}`); + } + }, + ], + }, }; diff --git a/jest.config.js b/jest.config.js index 0699d7e7..239cf058 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,3 +1,3 @@ module.exports = { - cacheDirectory: "./.cache/jest", + cacheDirectory: './.cache/jest', }; diff --git a/package.json b/package.json index 16255592..ddfde18d 100644 --- a/package.json +++ b/package.json @@ -19,11 +19,14 @@ "license": "CC0-1.0", "devDependencies": { "editorconfig-checker": "4.0.2", + "husky": "7.0.2", + "is-ci": "3.0.0", "jest": "27.2.5", "jest-diff": "27.2.5", "jsonschema": "1.4.0", "named-html-entities-json": "0.1.0", "npm-run-all": "4.1.5", + "prettier": "2.4.1", "rimraf": "3.0.2", "svg-path-bbox": "1.0.1", "svg-path-segments": "1.0.0", @@ -35,11 +38,14 @@ "scripts": { "build": "node scripts/build/package.js", "clean": "rimraf icons/*.js index.js", - "lint": "run-s our-lint jsonlint svglint wslint", + "format": "prettier --write .", + "lint": "run-s our-lint jslint jsonlint svglint wslint", "our-lint": "node scripts/lint/ourlint.js", + "jslint": "prettier --check .", "jsonlint": "node scripts/lint/jsonlint.js", "svglint": "svglint icons/*.svg --ci", "wslint": "editorconfig-checker -exclude \\.svg$", + "postinstall": "is-ci || husky install", "prepublishOnly": "npm run build", "postpublish": "npm run clean", "test": "jest", diff --git a/scripts/build/package.js b/scripts/build/package.js index a55e0e21..c92963e7 100644 --- a/scripts/build/package.js +++ b/scripts/build/package.js @@ -7,36 +7,36 @@ * tree-shakeable */ -const fs = require("fs"); -const path = require("path"); -const util = require("util"); -const minify = require("uglify-js").minify; +const fs = require('fs'); +const path = require('path'); +const util = require('util'); +const { minify } = require('uglify-js'); -const UTF8 = "utf8"; +const UTF8 = 'utf8'; -const rootDir = path.resolve(__dirname, "..", ".."); -const dataFile = path.resolve(rootDir, "_data", "simple-icons.json"); -const indexFile = path.resolve(rootDir, "index.js"); -const iconsDir = path.resolve(rootDir, "icons"); +const rootDir = path.resolve(__dirname, '..', '..'); +const dataFile = path.resolve(rootDir, '_data', 'simple-icons.json'); +const indexFile = path.resolve(rootDir, 'index.js'); +const iconsDir = path.resolve(rootDir, 'icons'); -const templatesDir = path.resolve(__dirname, "templates"); -const indexTemplateFile = path.resolve(templatesDir, "index.js"); -const iconObjectTemplateFile = path.resolve(templatesDir, "icon-object.js"); +const templatesDir = path.resolve(__dirname, 'templates'); +const indexTemplateFile = path.resolve(templatesDir, 'index.js'); +const iconObjectTemplateFile = path.resolve(templatesDir, 'icon-object.js'); const indexTemplate = fs.readFileSync(indexTemplateFile, UTF8); const iconObjectTemplate = fs.readFileSync(iconObjectTemplateFile, UTF8); const data = require(dataFile); -const { getIconSlug, titleToSlug } = require("../utils.js"); +const { getIconSlug } = require('../utils.js'); // Local helper functions -function escape(value) { +const escape = (value) => { return value.replace(/(?<!\\)'/g, "\\'"); -} -function iconToKeyValue(icon) { +}; +const iconToKeyValue = (icon) => { return `'${icon.slug}':${iconToObject(icon)}`; -} -function licenseToObject(license) { +}; +const licenseToObject = (license) => { if (license === undefined) { return; } @@ -45,9 +45,10 @@ function licenseToObject(license) { license.url = `https://spdx.org/licenses/${license.type}`; } return license; -} -function iconToObject(icon) { - return util.format(iconObjectTemplate, +}; +const iconToObject = (icon) => { + return util.format( + iconObjectTemplate, escape(icon.title), escape(icon.slug), escape(icon.svg), @@ -56,8 +57,8 @@ function iconToObject(icon) { icon.guidelines ? `'${escape(icon.guidelines)}'` : undefined, licenseToObject(icon.license), ); -} -function minifyAndWrite(filepath, rawJavaScript) { +}; +const minifyAndWrite = (filepath, rawJavaScript) => { const { error, code } = minify(rawJavaScript); if (error) { console.error(error); @@ -65,11 +66,11 @@ function minifyAndWrite(filepath, rawJavaScript) { } else { fs.writeFileSync(filepath, code); } -} +}; // 'main' const icons = []; -data.icons.forEach(icon => { +data.icons.forEach((icon) => { const filename = getIconSlug(icon); const svgFilepath = path.resolve(iconsDir, `${filename}.svg`); icon.svg = fs.readFileSync(svgFilepath, UTF8).replace(/\r?\n/, ''); @@ -82,5 +83,8 @@ data.icons.forEach(icon => { }); // write our generic index.js -const rawIndexJs = util.format(indexTemplate, icons.map(iconToKeyValue).join(',')); +const rawIndexJs = util.format( + indexTemplate, + icons.map(iconToKeyValue).join(','), +); minifyAndWrite(indexFile, rawIndexJs); diff --git a/scripts/get-filename.js b/scripts/get-filename.js index 610eb5c8..088a1489 100644 --- a/scripts/get-filename.js +++ b/scripts/get-filename.js @@ -5,13 +5,14 @@ * icon SVG filename to standard output. */ -const { titleToSlug } = require("./utils.js"); +const { titleToSlug } = require('./utils.js'); if (process.argv.length < 3) { - console.error("Provide a brand name as argument"); + console.error('Provide a brand name as argument'); process.exit(1); } else { - const brandName = process.argv.slice(3) + const brandName = process.argv + .slice(3) .reduce((acc, arg) => `${acc} ${arg}`, process.argv[2]); const filename = titleToSlug(brandName); diff --git a/scripts/lint/jsonlint.js b/scripts/lint/jsonlint.js index b89e97de..059a2831 100644 --- a/scripts/lint/jsonlint.js +++ b/scripts/lint/jsonlint.js @@ -1,9 +1,15 @@ -const path = require("path"); -const Validator = require("jsonschema").Validator; +#!/usr/bin/env node +/** + * @fileoverview + * CLI tool to run jsonschema on the simple-icons.json data file. + */ -const rootDir = path.resolve(__dirname, "..", ".."); -const schemaFile = path.resolve(rootDir, ".jsonschema.json"); -const dataFile = path.resolve(rootDir, "_data", "simple-icons.json"); +const path = require('path'); +const { Validator } = require('jsonschema'); + +const rootDir = path.resolve(__dirname, '..', '..'); +const schemaFile = path.resolve(rootDir, '.jsonschema.json'); +const dataFile = path.resolve(rootDir, '_data', 'simple-icons.json'); const schema = require(schemaFile); const data = require(dataFile); diff --git a/scripts/lint/ourlint.js b/scripts/lint/ourlint.js index d275a5d7..5a79dd97 100644 --- a/scripts/lint/ourlint.js +++ b/scripts/lint/ourlint.js @@ -5,15 +5,15 @@ * linters (e.g. jsonlint/svglint). */ -const fs = require("fs"); -const path = require("path"); +const fs = require('fs'); +const path = require('path'); -const { diffLinesUnified } = require("jest-diff"); +const { diffLinesUnified } = require('jest-diff'); -const UTF8 = "utf8"; +const UTF8 = 'utf8'; -const rootDir = path.resolve(__dirname, "..", ".."); -const dataFile = path.resolve(rootDir, "_data", "simple-icons.json"); +const rootDir = path.resolve(__dirname, '..', '..'); +const dataFile = path.resolve(rootDir, '_data', 'simple-icons.json'); const data = require(dataFile); /** @@ -22,7 +22,7 @@ const data = require(dataFile); */ const TESTS = { /* Tests whether our icons are in alphabetical order */ - alphabetical: function() { + alphabetical: () => { const collector = (invalidEntries, icon, index, array) => { if (index > 0) { const prev = array[index - 1]; @@ -39,7 +39,7 @@ const TESTS = { } return invalidEntries; }; - const format = icon => { + const format = (icon) => { if (icon.slug) { return `${icon.title} (${icon.slug})`; } @@ -49,35 +49,35 @@ const TESTS = { const invalids = data.icons.reduce(collector, []); if (invalids.length) { return `Some icons aren't in alphabetical order: - ${invalids.map(icon => format(icon)).join(", ")}`; + ${invalids.map((icon) => format(icon)).join(', ')}`; } }, /* Check the formatting of the data file */ - prettified: function() { + prettified: () => { const dataString = fs.readFileSync(dataFile, UTF8).replace(/\r\n/g, '\n'); - const dataPretty = `${JSON.stringify(data, null, " ")}\n`; + const dataPretty = `${JSON.stringify(data, null, ' ')}\n`; if (dataString !== dataPretty) { const dataDiff = diffLinesUnified( - dataString.split("\n"), - dataPretty.split("\n"), + dataString.split('\n'), + dataPretty.split('\n'), { expand: false, - omitAnnotationLines: true + omitAnnotationLines: true, }, ); return `Data file is formatted incorrectly:\n\n${dataDiff}`; } - } + }, }; // execute all tests and log all errors const errors = Object.keys(TESTS) - .map(k => TESTS[k]()) + .map((k) => TESTS[k]()) .filter(Boolean); if (errors.length > 0) { - errors.forEach(error => console.error(`\u001b[31m${error}\u001b[0m`)); + errors.forEach((error) => console.error(`\u001b[31m${error}\u001b[0m`)); process.exit(1); } diff --git a/scripts/release/bump-version.js b/scripts/release/bump-version.js index 9648a833..7755b75d 100644 --- a/scripts/release/bump-version.js +++ b/scripts/release/bump-version.js @@ -4,34 +4,34 @@ * Updates the version of this package to the CLI specified version. */ -const fs = require("fs"); -const path = require("path"); +const fs = require('fs'); +const path = require('path'); -const rootDir = path.resolve(__dirname, "..", ".."); -const packageJsonFile = path.resolve(rootDir, "package.json"); +const rootDir = path.resolve(__dirname, '..', '..'); +const packageJsonFile = path.resolve(rootDir, 'package.json'); -function readManifest(file) { +const readManifest = (file) => { const manifestRaw = fs.readFileSync(file).toString(); const manifestJson = JSON.parse(manifestRaw); return manifestJson; -} +}; -function writeManifest(file, json) { - const manifestRaw = JSON.stringify(json, null, 2) + "\n"; +const writeManifest = (file, json) => { + const manifestRaw = JSON.stringify(json, null, 2) + '\n'; fs.writeFileSync(file, manifestRaw); -} +}; -function main(newVersion) { +const main = (newVersion) => { try { const manifest = readManifest(packageJsonFile); - manifest.version = newVersion + manifest.version = newVersion; writeManifest(packageJsonFile, manifest); } catch (error) { console.error(`Failed to bump package version to ${newVersion}:`, error); process.exit(1); } -} +}; main(process.argv[2]); diff --git a/scripts/release/update-cdn-urls.js b/scripts/release/update-cdn-urls.js index adf96763..a37c0087 100644 --- a/scripts/release/update-cdn-urls.js +++ b/scripts/release/update-cdn-urls.js @@ -5,24 +5,24 @@ * NPM package manifest. Does nothing if the README.md is already up-to-date. */ -const fs = require("fs"); -const path = require("path"); +const fs = require('fs'); +const path = require('path'); -const rootDir = path.resolve(__dirname, "..", ".."); -const packageJsonFile = path.resolve(rootDir, "package.json"); -const readmeFile = path.resolve(rootDir, "README.md"); +const rootDir = path.resolve(__dirname, '..', '..'); +const packageJsonFile = path.resolve(rootDir, 'package.json'); +const readmeFile = path.resolve(rootDir, 'README.md'); -function getMajorVersion(semVerVersion) { +const getMajorVersion = (semVerVersion) => { const majorVersionAsString = semVerVersion.split('.')[0]; return parseInt(majorVersionAsString); -} +}; -function getManifest() { +const getManifest = () => { const manifestRaw = fs.readFileSync(packageJsonFile).toString(); return JSON.parse(manifestRaw); -} +}; -function updateVersionInReadmeIfNecessary(majorVersion) { +const updateVersionInReadmeIfNecessary = (majorVersion) => { let content = fs.readFileSync(readmeFile).toString(); content = content.replace( @@ -31,17 +31,17 @@ function updateVersionInReadmeIfNecessary(majorVersion) { ); fs.writeFileSync(readmeFile, content); -} +}; -function main() { +const main = () => { try { const manifest = getManifest(); const majorVersion = getMajorVersion(manifest.version); updateVersionInReadmeIfNecessary(majorVersion); } catch (error) { - console.error("Failed to update CDN version number:", error); + console.error('Failed to update CDN version number:', error); process.exit(1); } -} +}; main(); diff --git a/scripts/release/update-slugs-table.js b/scripts/release/update-slugs-table.js index 69657e8b..84bb9132 100644 --- a/scripts/release/update-slugs-table.js +++ b/scripts/release/update-slugs-table.js @@ -4,15 +4,15 @@ * Generates a MarkDown file that lists every brand name and their slug. */ -const fs = require("fs"); -const path = require("path"); +const fs = require('fs'); +const path = require('path'); -const rootDir = path.resolve(__dirname, "..", ".."); -const dataFile = path.resolve(rootDir, "_data", "simple-icons.json"); -const slugsFile = path.resolve(rootDir, "slugs.md"); +const rootDir = path.resolve(__dirname, '..', '..'); +const dataFile = path.resolve(rootDir, '_data', 'simple-icons.json'); +const slugsFile = path.resolve(rootDir, 'slugs.md'); const data = require(dataFile); -const { getIconSlug } = require("../utils.js"); +const { getIconSlug } = require('../utils.js'); let content = `<!-- This file is automatically generated. If you want to change something, please @@ -25,10 +25,10 @@ update the script at '${path.relative(rootDir, __filename)}'. | :--- | :--- | `; -data.icons.forEach(icon => { +data.icons.forEach((icon) => { const brandName = icon.title; const brandSlug = getIconSlug(icon); - content += `| \`${brandName}\` | \`${brandSlug}\` |\n` + content += `| \`${brandName}\` | \`${brandSlug}\` |\n`; }); fs.writeFileSync(slugsFile, content); diff --git a/scripts/utils.js b/scripts/utils.js index 6e918cef..eabb533a 100644 --- a/scripts/utils.js +++ b/scripts/utils.js @@ -8,41 +8,39 @@ module.exports = { * Get the slug/filename for an icon. * @param {Object} icon The icon data as it appears in _data/simple-icons.json */ - getIconSlug: icon => icon.slug || module.exports.titleToSlug(icon.title), + getIconSlug: (icon) => icon.slug || module.exports.titleToSlug(icon.title), /** * Converts a brand title into a slug/filename. * @param {String} title The title to convert */ - titleToSlug: title => ( - title.toLowerCase() - .replace(/\+/g, "plus") - .replace(/\./g, "dot") - .replace(/&/g, "and") - .replace(/đ/g, "d") - .replace(/ħ/g, "h") - .replace(/ı/g, "i") - .replace(/ĸ/g, "k") - .replace(/ŀ/g, "l") - .replace(/ł/g, "l") - .replace(/ß/g, "ss") - .replace(/ŧ/g, "t") - .normalize("NFD") - .replace(/[^a-z0-9]/g, "") - ), + titleToSlug: (title) => + title + .toLowerCase() + .replace(/\+/g, 'plus') + .replace(/\./g, 'dot') + .replace(/&/g, 'and') + .replace(/đ/g, 'd') + .replace(/ħ/g, 'h') + .replace(/ı/g, 'i') + .replace(/ĸ/g, 'k') + .replace(/ŀ/g, 'l') + .replace(/ł/g, 'l') + .replace(/ß/g, 'ss') + .replace(/ŧ/g, 't') + .normalize('NFD') + .replace(/[^a-z0-9]/g, ''), /** * Converts a brand title in HTML/SVG friendly format into a brand title (as * it is seen in simple-icons.json) * @param {String} htmlFriendlyTitle The title to convert */ - htmlFriendlyToTitle: htmlFriendlyTitle => ( - htmlFriendlyTitle.replace( - /&#([0-9]+);/g, - (_, num) => String.fromCharCode(parseInt(num)) - ).replace( - /&(quot|amp|lt|gt);/g, - (_, ref) => ({quot: '"', amp: '&', lt: '<', gt: '>'}[ref]) - ) - ), -} + htmlFriendlyToTitle: (htmlFriendlyTitle) => + htmlFriendlyTitle + .replace(/&#([0-9]+);/g, (_, num) => String.fromCharCode(parseInt(num))) + .replace( + /&(quot|amp|lt|gt);/g, + (_, ref) => ({ quot: '"', amp: '&', lt: '<', gt: '>' }[ref]), + ), +}; diff --git a/svgo.config.js b/svgo.config.js index 062d00b1..98a08c73 100644 --- a/svgo.config.js +++ b/svgo.config.js @@ -38,9 +38,7 @@ module.exports = { // Keep the role="img" attribute and automatically add it // to the <svg> tag if it's not there already addAttributesToSVGElement: { - attributes: [ - {role: 'img'}, - ], + attributes: [{ role: 'img' }], }, // Keep the 'role' attribute, if it's already defined @@ -51,11 +49,7 @@ module.exports = { // Remove all attributes except 'role', 'viewBox', and 'xmlns' from // <svg> tags removeAttrs: { - attrs: [ - 'baseProfile', - 'version', - 'fill-rule', - ], + attrs: ['baseProfile', 'version', 'fill-rule'], }, // Remove paths with fill="none" diff --git a/tests/icons.test.js b/tests/icons.test.js index 760ee103..f9913c77 100644 --- a/tests/icons.test.js +++ b/tests/icons.test.js @@ -1,7 +1,7 @@ const { icons } = require('../_data/simple-icons.json'); const { getIconSlug } = require('../scripts/utils.js'); -icons.forEach(icon => { +icons.forEach((icon) => { const filename = getIconSlug(icon); const subject = require(`../icons/${filename}.js`); @@ -34,7 +34,9 @@ icons.forEach(icon => { expect(subject.path).toMatch(/^[MmZzLlHhVvCcSsQqTtAaEe0-9-,.\s]+$/g); }); - test(`${icon.title} has ${icon.guidelines ? "the correct" : "no"} "guidelines"`, () => { + test(`${icon.title} has ${ + icon.guidelines ? 'the correct' : 'no' + } "guidelines"`, () => { if (icon.guidelines) { expect(typeof subject.guidelines).toBe('string'); expect(subject.guidelines).toEqual(icon.guidelines); @@ -43,11 +45,13 @@ icons.forEach(icon => { } }); - test(`${icon.title} has ${icon.license ? "the correct" : "no"} "license"`, () => { + test(`${icon.title} has ${ + icon.license ? 'the correct' : 'no' + } "license"`, () => { if (icon.license) { expect(typeof subject.license).toBe('object'); expect(subject.license).toHaveProperty('type', icon.license.type); - if (icon.license.type === "custom") { + if (icon.license.type === 'custom') { expect(subject.license).toHaveProperty('url', icon.license.url); } else { expect(typeof subject.license.url).toBe('string'); diff --git a/tests/index.test.js b/tests/index.test.js index 88d86e77..52a2a0b3 100644 --- a/tests/index.test.js +++ b/tests/index.test.js @@ -1,8 +1,8 @@ const { icons } = require('../_data/simple-icons.json'); const simpleIcons = require('../index.js'); -const { getIconSlug } = require("../scripts/utils.js"); +const { getIconSlug } = require('../scripts/utils.js'); -icons.forEach(icon => { +icons.forEach((icon) => { const slug = getIconSlug(icon); const subject = simpleIcons[slug]; @@ -35,7 +35,9 @@ icons.forEach(icon => { expect(subject.path).toMatch(/^[MmZzLlHhVvCcSsQqTtAaEe0-9-,.\s]+$/g); }); - test(`${icon.title} has ${icon.guidelines ? "the correct" : "no"} "guidelines"`, () => { + test(`${icon.title} has ${ + icon.guidelines ? 'the correct' : 'no' + } "guidelines"`, () => { if (icon.guidelines) { expect(typeof subject.guidelines).toBe('string'); expect(subject.guidelines).toEqual(icon.guidelines); @@ -44,11 +46,13 @@ icons.forEach(icon => { } }); - test(`${icon.title} has ${icon.license ? "the correct" : "no"} "license"`, () => { + test(`${icon.title} has ${ + icon.license ? 'the correct' : 'no' + } "license"`, () => { if (icon.license) { expect(typeof subject.license).toBe('object'); expect(subject.license).toHaveProperty('type', icon.license.type); - if (icon.license.type === "custom") { + if (icon.license.type === 'custom') { expect(subject.license).toHaveProperty('url', icon.license.url); } else { expect(typeof subject.license.url).toBe('string');