diff --git a/package-lock.json b/package-lock.json index 64ef0d2ced..e440a75aa1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -97,7 +97,6 @@ "semantic-release": "25.0.3", "semver": "7.7.4", "style-loader": "3.3.1", - "svg-prep": "1.0.4", "typescript": "5.9.3", "webpack": "5.105.1", "webpack-cli": "6.0.1", @@ -28255,12 +28254,6 @@ "node": ">=18" } }, - "node_modules/sax": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", - "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", - "dev": true - }, "node_modules/saxes": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", @@ -30081,28 +30074,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/svg-prep": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/svg-prep/-/svg-prep-1.0.4.tgz", - "integrity": "sha512-RT9dKj+l9yRsrQIZhdWqIftN0E4agtnPCn8pFJm9yM9bEWrNJhtUjG2g2AA/K+9pa4gh/CRFMXIPtcHOviQE2w==", - "dev": true, - "dependencies": { - "commander": "^2.8.1", - "xml2js": "^0.4.12" - }, - "bin": { - "svg-prep": "bin/index.js" - }, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/svg-prep/node_modules/commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "dev": true - }, "node_modules/symbol-observable": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz", @@ -32148,28 +32119,6 @@ "node": ">=18" } }, - "node_modules/xml2js": { - "version": "0.4.23", - "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz", - "integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==", - "dev": true, - "dependencies": { - "sax": ">=0.6.0", - "xmlbuilder": "~11.0.0" - }, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/xmlbuilder": { - "version": "11.0.1", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", - "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", - "dev": true, - "engines": { - "node": ">=4.0" - } - }, "node_modules/xmlchars": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", diff --git a/package.json b/package.json index 6ecf473106..f4d3d4e0a4 100644 --- a/package.json +++ b/package.json @@ -122,7 +122,6 @@ "semantic-release": "25.0.3", "semver": "7.7.4", "style-loader": "3.3.1", - "svg-prep": "1.0.4", "typescript": "5.9.3", "webpack": "5.105.1", "webpack-cli": "6.0.1", diff --git a/webpack/plugins/svg-prep.js b/webpack/plugins/svg-prep.js index af6a70f4cb..b5db1bdfa8 100644 --- a/webpack/plugins/svg-prep.js +++ b/webpack/plugins/svg-prep.js @@ -9,10 +9,57 @@ const fs = require('fs'); const path = require('path'); -const SvgPrep = require('svg-prep'); const { Compilation, sources } = require('webpack'); const { RawSource } = sources; +/** + * Builds an SVG sprite from individual SVG files. Each SVG becomes a + * element identified by its filename (without extension). + */ +function buildSvgSprite(files) { + const symbols = files.map(file => { + const name = path.basename(file, '.svg'); + const svg = fs.readFileSync(file, 'utf-8'); + + // Extract viewBox from the root element + const viewBoxMatch = svg.match(/viewBox="([^"]*)"/); + const viewBox = viewBoxMatch ? viewBoxMatch[1] : '0 0 100 100'; + + // Extract inner content between and + const innerMatch = svg.match(/]*>([\s\S]*)<\/svg>/); + const inner = innerMatch ? innerMatch[1] : ''; + + // Strip elements that are unnecessary or potential XSS vectors + // Remove attributes that interfere with sprite styling or pose security risks + const cleaned = inner + .replace(//gi, '') + .replace(//gi, '') + .replace(//gi, '') + .replace(//gi, '') + .replace(//gi, '') + .replace(//g, '') + .replace(/\s+id="[^"]*"/g, '') + .replace(/\s+fill="[^"]*"/g, '') + .replace(/\s+class="[^"]*"/g, '') + .replace(/\s+style="[^"]*"/g, '') + .replace(/\s+stroke="[^"]*"/g, '') + .replace(/\s+stroke-[a-z]+="[^"]*"/g, '') + .replace(/\s+on[a-zA-Z]+="[^"]*"/g, '') + .replace(/\s+on[a-zA-Z]+='[^']*'/g, '') + .replace(/\s+href="[^"]*"/g, '') + .replace(/\s+xlink:href="[^"]*"/g, ''); + + return ` \n ${cleaned.trim()}\n `; + }); + + return [ + '', + '', + ].join('\n'); +} + function SvgPrepPlugin(options) { this.options = {}; Object.assign( @@ -33,17 +80,16 @@ SvgPrepPlugin.prototype.apply = function (compiler) { }, async () => { if (!this.options.source) { - return Promise.resolve(); + return; } - // TODO: Keep track of file hashes, so we can avoid recompiling when none have changed const files = fs .readdirSync(this.options.source) .filter(name => name.endsWith('.svg')) + .sort() .map(name => path.join(this.options.source, name)); - const sprited = await SvgPrep(files).filter({ removeIds: true, noFill: true }).output(); - + const sprited = buildSvgSprite(files); compilation.emitAsset(this.options.output, new RawSource(sprited)); } );