Skip to content

Commit 140e95f

Browse files
committed
AB#64926 make svgs use styles with randomized names
1 parent 67653ca commit 140e95f

File tree

7 files changed

+13934
-2
lines changed

7 files changed

+13934
-2
lines changed

scripts/store.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ const createEmptyTemplate = require('./util/createEmptyTemplate');
1414
const cleanup = require('./util/cleanup');
1515

1616
const { JORE_GRAPHQL_URL } = require('../constants');
17+
const { processSVGWithUniqueIds } = require('../src/util/processSVG');
1718

1819
// Must cleanup knex, otherwise the process keeps going.
1920
cleanup(() => {
@@ -269,9 +270,10 @@ async function saveAreaImages(slots) {
269270
.where({ name: imageName })
270271
.first();
271272

272-
const svgContent = get(slot, 'image.svg', '');
273+
let svgContent = get(slot, 'image.svg', '');
273274

274275
if (svgContent) {
276+
svgContent = processSVGWithUniqueIds(svgContent);
275277
const newImage = {
276278
name: imageName,
277279
svg: svgContent,

src/components/inlineSVG.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useRef, useEffect } from 'react';
1+
import React from 'react';
22
import PropTypes from 'prop-types';
33

44
const InlineSVG = ({ src, ...otherProps }) => {

src/util/processSVG.js

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
const uuidv4 = require('uuid/v4');
2+
3+
// Pre-compile regexes outside the function
4+
const MAIN_REGEX = /<style>([\s\S]*?)<\/style>|class="([^"]+)"|(?<!xlink:)id="([^"]+)"|url\(#([^)]+)\)|xlink:href="#([^"]+)"|(?<!xlink:)href="#([^"]+)"/g;
5+
const STYLE_CLASS_REGEX = /(^|[^\w-])\.([a-zA-Z_][\w-]*)(?=\s*[{,])/gm;
6+
const STYLE_ID_REGEX = /(^|[^\w-])#([a-zA-Z_][\w-]*)(?=\s*[{,])/gm;
7+
const STYLE_URL_REGEX = /url\(#([^)]+)\)/g;
8+
const CLASS_ATTR_REGEX = /\S+/g;
9+
const EXISTING_PREFIX_REGEX = /svg-[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}-/g;
10+
11+
/**
12+
* @param {string} src - The SVG source string
13+
* @returns {string} The SVG with existing prefixes removed
14+
*/
15+
const stripExistingPrefixes = src => src.replace(EXISTING_PREFIX_REGEX, '');
16+
17+
/**
18+
* Processes SVG content by adding unique prefixes to class names and IDs
19+
* to avoid conflicts when multiple SVGs are embedded in the same page.
20+
*
21+
* @param {string} src - The SVG source string to process
22+
* @returns {string} The processed SVG with unique prefixed classes and IDs
23+
*/
24+
const processSVGWithUniqueIds = src => {
25+
const cleaned = stripExistingPrefixes(src);
26+
const prefix = `svg-${uuidv4()}`;
27+
28+
return cleaned.replace(
29+
MAIN_REGEX,
30+
(match, styles, classAttr, idAttr, urlId, xlinkHref, hrefAttr) => {
31+
if (styles) {
32+
const prefixed = styles
33+
.replace(STYLE_CLASS_REGEX, `$1.${prefix}-$2`)
34+
.replace(STYLE_ID_REGEX, `$1#${prefix}-$2`)
35+
.replace(STYLE_URL_REGEX, `url(#${prefix}-$1)`);
36+
return `<style>${prefixed}</style>`;
37+
}
38+
if (classAttr) {
39+
// Use replace instead of split/map/join to avoid array allocation
40+
return `class="${classAttr.replace(CLASS_ATTR_REGEX, c => `${prefix}-${c}`)}"`;
41+
}
42+
if (idAttr) return `id="${prefix}-${idAttr}"`;
43+
if (urlId) return `url(#${prefix}-${urlId})`;
44+
if (xlinkHref) return `xlink:href="#${prefix}-${xlinkHref}"`;
45+
if (hrefAttr) return `href="#${prefix}-${hrefAttr}"`;
46+
return match;
47+
},
48+
);
49+
};
50+
51+
module.exports = {
52+
processSVGWithUniqueIds,
53+
};

test/svg-processing/no_smoking.svg

Lines changed: 50 additions & 0 deletions
Loading
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import test from 'node:test';
2+
import assert from 'node:assert';
3+
import fs from 'node:fs';
4+
import path from 'node:path';
5+
import { fileURLToPath } from 'node:url';
6+
import puppeteer from 'puppeteer';
7+
import { processSVGWithUniqueIds } from '../../src/util/processSVG.js';
8+
9+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
10+
const failedDir = path.join(__dirname, 'failed');
11+
const processedDir = path.join(__dirname, 'processed');
12+
13+
// delete failed directory before running tests
14+
if (fs.existsSync(failedDir)) {
15+
fs.rmSync(failedDir, { recursive: true });
16+
}
17+
18+
// Get all SVG files in the current directory
19+
const svgFiles = fs.readdirSync(__dirname).filter(file => file.endsWith('.svg'));
20+
21+
async function renderSvgToBuffer(browser, svgContent) {
22+
const page = await browser.newPage();
23+
await page.setViewport({ width: 600, height: 400 });
24+
25+
const html = `
26+
<!DOCTYPE html>
27+
<html>
28+
<head>
29+
<style>
30+
body { margin: 0; padding: 0; background: white; }
31+
</style>
32+
</head>
33+
<body>${svgContent}</body>
34+
</html>
35+
`;
36+
37+
await page.setContent(html, { waitUntil: 'networkidle0' });
38+
const screenshot = await page.screenshot({ type: 'png' });
39+
await page.close();
40+
return screenshot;
41+
}
42+
43+
function buffersAreEqual(buf1, buf2) {
44+
if (buf1.length !== buf2.length) return false;
45+
return buf1.equals(buf2);
46+
}
47+
48+
for (const svgFile of svgFiles) {
49+
test(`processSVGWithUniqueIds visual regression - ${svgFile} should render identically`, async () => {
50+
const svgPath = path.join(__dirname, svgFile);
51+
const originalSvg = fs.readFileSync(svgPath, 'utf-8');
52+
const processedSvg = processSVGWithUniqueIds(originalSvg);
53+
54+
const browser = await puppeteer.launch({ headless: true });
55+
56+
try {
57+
const originalScreenshot = await renderSvgToBuffer(browser, originalSvg);
58+
const processedScreenshot = await renderSvgToBuffer(browser, processedSvg);
59+
60+
const isEqual = buffersAreEqual(originalScreenshot, processedScreenshot);
61+
62+
if (!isEqual) {
63+
if (!fs.existsSync(failedDir)) {
64+
fs.mkdirSync(failedDir, { recursive: true });
65+
}
66+
const processedPath = path.join(failedDir, `processed_${svgFile}`);
67+
fs.writeFileSync(processedPath, processedSvg);
68+
console.log(`Saved processed SVG to: ${processedPath}`);
69+
}
70+
71+
assert.ok(
72+
isEqual,
73+
`Processed SVG (${svgFile}) should render identically to the original SVG`,
74+
);
75+
} finally {
76+
await browser.close();
77+
}
78+
});
79+
80+
test(`processSVGWithUniqueIds idempotency - ${svgFile} should render identically after double processing`, async () => {
81+
const svgPath = path.join(__dirname, svgFile);
82+
const originalSvg = fs.readFileSync(svgPath, 'utf-8');
83+
const processedOnce = processSVGWithUniqueIds(originalSvg);
84+
const processedTwice = processSVGWithUniqueIds(processedOnce);
85+
86+
const browser = await puppeteer.launch({ headless: true });
87+
88+
try {
89+
const originalScreenshot = await renderSvgToBuffer(browser, originalSvg);
90+
const doubleProcessedScreenshot = await renderSvgToBuffer(browser, processedTwice);
91+
92+
const isEqual = buffersAreEqual(originalScreenshot, doubleProcessedScreenshot);
93+
94+
if (!isEqual) {
95+
if (!fs.existsSync(failedDir)) {
96+
fs.mkdirSync(failedDir, { recursive: true });
97+
}
98+
const processedPath = path.join(failedDir, `double_processed_${svgFile}`);
99+
fs.writeFileSync(processedPath, processedTwice);
100+
console.log(`Saved double-processed SVG to: ${processedPath}`);
101+
}
102+
103+
assert.ok(
104+
isEqual,
105+
`Double-processed SVG (${svgFile}) should render identically to the original SVG`,
106+
);
107+
} finally {
108+
await browser.close();
109+
}
110+
});
111+
}

0 commit comments

Comments
 (0)