Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 | 'use strict' /** * SVG dimension rules, mirrored from the repo's own test contract * (tests/prebuild.js): * * - width/height attributes must equal the canonical px size for the type * - the viewBox width/height must be a power-of-10 multiple of those (1x, * 10x, 100x), preserving aspect ratio * - viewBox must start at "0 0" * * This module is the single source of truth for (a) validating those rules and * (b) a *gated* normalization that only repairs the safe, deterministic case: * a correct viewBox with non-conforming width/height attributes (e.g. inch * units like width=".78in"). It never rewrites geometry — if the viewBox is * missing or not a clean multiple of the expected size, it refuses to touch the * file so the defect surfaces as a validation failure instead. */ const TYPE_DIMS = { action: { width: 52, height: 52 }, custom: { width: 100, height: 100 }, doctype: { width: 56, height: 64 }, standard: { width: 100, height: 100 }, utility: { width: 52, height: 52 } } function isPowerOfTen(n) { if (!(n > 0)) return false return Number.isInteger(Math.log10(n)) } /** * Pull the opening <svg ...> tag and parse the attributes we care about. * Uses parseFloat (like the repo test) so unit-suffixed values such as * ".78in" are read as 0.78 rather than NaN. */ function parseGeom(svgContent) { if (!svgContent) return null const tagMatch = svgContent.match(/<svg\b[^>]*>/i) if (!tagMatch) return null const tag = tagMatch[0] const attr = name => { const m = tag.match(new RegExp(`${name}\\s*=\\s*"([^"]*)"`, 'i')) return m ? m[1] : null } const vbRaw = attr('viewBox') let viewBox = null if (vbRaw) { const parts = vbRaw.trim().split(/[\s,]+/).map(parseFloat) if (parts.length === 4 && parts.every(p => !Number.isNaN(p))) { viewBox = { x: parts[0], y: parts[1], width: parts[2], height: parts[3] } } } const widthRaw = attr('width') const heightRaw = attr('height') return { tag, viewBox, widthRaw, heightRaw, width: widthRaw === null ? null : parseFloat(widthRaw), height: heightRaw === null ? null : parseFloat(heightRaw) } } /** * Validate the dimensions of one icon's SVG against its type contract. * Returns an array of human-readable error strings (empty = valid). */ function checkDimensions(iconType, svgContent) { const expected = TYPE_DIMS[iconType] if (!expected) return [`Unknown icon type "${iconType}" for dimension check.`] const geom = parseGeom(svgContent) if (!geom) return ['Could not parse an <svg> tag.'] const errors = [] const { viewBox, width, height } = geom if (!viewBox) { errors.push('Missing or malformed viewBox.') return errors } if (viewBox.x !== 0 || viewBox.y !== 0) { errors.push(`viewBox must start at "0 0" (got "${viewBox.x} ${viewBox.y}").`) } if (width === null || Number.isNaN(width)) { errors.push('Missing width attribute.') } else if (width !== expected.width) { errors.push(`width must be ${expected.width} (got ${geom.widthRaw}).`) } else if (!isPowerOfTen(viewBox.width / width)) { errors.push(`viewBox width ${viewBox.width} is not a power-of-10 multiple of ${width}.`) } if (height === null || Number.isNaN(height)) { errors.push('Missing height attribute.') } else if (height !== expected.height) { errors.push(`height must be ${expected.height} (got ${geom.heightRaw}).`) } else if (!isPowerOfTen(viewBox.height / height)) { errors.push(`viewBox height ${viewBox.height} is not a power-of-10 multiple of ${height}.`) } return errors } function setAttr(tag, name, value) { const re = new RegExp(`(${name}\\s*=\\s*")[^"]*(")`, 'i') if (re.test(tag)) return tag.replace(re, `$1${value}$2`) // insert right after "<svg" return tag.replace(/<svg\b/i, `<svg ${name}="${value}"`) } /** * Gated normalization of the width/height attributes. * * Only acts when the viewBox is present, starts at 0 0, and is a clean * power-of-10 multiple of the type's canonical size in BOTH dimensions with * the SAME scale factor (so aspect ratio is provably preserved). In that case * it sets width/height to the canonical px values. * * Returns one of: * { changed: false, safe: true } already conformant * { changed: true, content, from: {...}, to: {...} } repaired * { changed: false, safe: false, reason } unsafe — left untouched */ function normalizeDimensions(svgContent, iconType) { const expected = TYPE_DIMS[iconType] if (!expected) return { changed: false, safe: false, reason: `Unknown type "${iconType}".` } const geom = parseGeom(svgContent) if (!geom) return { changed: false, safe: false, reason: 'No <svg> tag.' } if (!geom.viewBox) return { changed: false, safe: false, reason: 'Missing/malformed viewBox.' } const { viewBox } = geom if (viewBox.x !== 0 || viewBox.y !== 0) { return { changed: false, safe: false, reason: `viewBox does not start at "0 0".` } } const kw = viewBox.width / expected.width const kh = viewBox.height / expected.height if (!isPowerOfTen(kw) || !isPowerOfTen(kh) || kw !== kh) { return { changed: false, safe: false, reason: `viewBox ${viewBox.width}x${viewBox.height} is not a clean multiple of ${expected.width}x${expected.height}.` } } const alreadyOk = geom.width === expected.width && geom.height === expected.height if (alreadyOk) return { changed: false, safe: true } let newTag = setAttr(geom.tag, 'width', expected.width) newTag = setAttr(newTag, 'height', expected.height) const content = svgContent.replace(geom.tag, newTag) return { changed: true, content, from: { width: geom.widthRaw, height: geom.heightRaw }, to: { width: expected.width, height: expected.height } } } module.exports = { TYPE_DIMS, parseGeom, checkDimensions, normalizeDimensions } |