Canvas: Avoid conflicting stylesheets when loading SVG icons (#74461)

This commit is contained in:
Adela Almasan 2023-09-18 12:38:45 -05:00 committed by GitHub
parent b779ce5687
commit 7171b35095
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 112 additions and 2 deletions

View File

@ -0,0 +1,58 @@
import { getSvgId, getSvgStyle, svgStyleCleanup } from './utils';
const ID = 'TEST_ID';
const svgNoId =
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><style type="text/css">.st0{fill:purple;}</style><circle cx="12" cy="12" r="10" class="st0"/></svg>';
const svgWithId = `<svg id="${ID}" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><style type="text/css">.st0{fill:blue;}</style><circle cx="12" cy="12" r="10" class="st0"/></svg>`;
const svgWithWrongIdInStyle =
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><style type="text/css">#WRONG .st0{fill:green;}</style><circle cx="12" cy="12" r="10" class="st0"/></svg>';
describe('SanitizedSVG', () => {
it('should cleanup the style and generate an ID', () => {
const cleanStyle = svgStyleCleanup(svgNoId);
const updatedStyle = getSvgStyle(cleanStyle);
const svgId = getSvgId(cleanStyle);
expect(cleanStyle.indexOf('id="')).toBeGreaterThan(-1);
expect(svgId).toBeDefined();
expect(svgId?.startsWith('x')).toBeTruthy();
expect(updatedStyle?.indexOf(`#${svgId}`)).toBeGreaterThan(-1);
expect(cleanStyle).toEqual(
`<svg id="${svgId}" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><style type="text/css">#${svgId} .st0{fill:purple;}</style><circle cx="12" cy="12" r="10" class="st0"/></svg>`
);
});
it('should cleanup the style and use the existing ID', () => {
const cleanStyle = svgStyleCleanup(svgWithId);
const updatedStyle = getSvgStyle(cleanStyle);
const svgId = getSvgId(cleanStyle);
expect(cleanStyle.indexOf('id="')).toBeGreaterThan(-1);
expect(svgId).toBeDefined();
expect(svgId).toEqual(ID);
expect(updatedStyle?.indexOf(`#${svgId}`)).toBeGreaterThan(-1);
expect(cleanStyle).toEqual(
`<svg id="${svgId}" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><style type="text/css">#${svgId} .st0{fill:blue;}</style><circle cx="12" cy="12" r="10" class="st0"/></svg>`
);
});
it('should cleanup the style and replace the wrong ID', () => {
const cleanStyle = svgStyleCleanup(svgWithWrongIdInStyle);
const updatedStyle = getSvgStyle(cleanStyle);
const svgId = getSvgId(cleanStyle);
expect(cleanStyle.indexOf('id="')).toBeGreaterThan(-1);
expect(svgId).toBeDefined();
expect(svgId?.startsWith('x')).toBeTruthy();
expect(updatedStyle?.indexOf(`#${svgId}`)).toBeGreaterThan(-1);
expect(cleanStyle).toEqual(
`<svg id="${svgId}" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><style type="text/css">#${svgId} .st0{fill:green;}</style><circle cx="12" cy="12" r="10" class="st0"/></svg>`
);
});
});

View File

@ -3,8 +3,13 @@ import SVG, { Props } from 'react-inlinesvg';
import { textUtil } from '@grafana/data'; import { textUtil } from '@grafana/data';
export const SanitizedSVG = (props: Props) => { import { svgStyleCleanup } from './utils';
return <SVG {...props} cacheRequests={true} preProcessor={getCleanSVG} />;
type SanitizedSVGProps = Props & { cleanStyle?: boolean };
export const SanitizedSVG = (props: SanitizedSVGProps) => {
const { cleanStyle, ...inlineSvgProps } = props;
return <SVG {...inlineSvgProps} cacheRequests={true} preProcessor={cleanStyle ? getCleanSVGAndStyle : getCleanSVG} />;
}; };
let cache = new Map<string, string>(); let cache = new Map<string, string>();
@ -15,5 +20,21 @@ function getCleanSVG(code: string): string {
clean = textUtil.sanitizeSVGContent(code); clean = textUtil.sanitizeSVGContent(code);
cache.set(code, clean); cache.set(code, clean);
} }
return clean;
}
function getCleanSVGAndStyle(code: string): string {
let clean = cache.get(code);
if (!clean) {
clean = textUtil.sanitizeSVGContent(code);
if (clean.indexOf('<style type="text/css">') > -1) {
clean = svgStyleCleanup(clean);
}
cache.set(code, clean);
}
return clean; return clean;
} }

View File

@ -0,0 +1,30 @@
import { v4 as uuidv4 } from 'uuid';
const MATCH_ID_INDEX = 2;
const SVG_ID_INSERT_POS = 5;
export const getSvgStyle = (svgCode: string) => {
const svgStyle = svgCode.match(new RegExp('<style type="text/css">([\\s\\S]*?)<\\/style>'));
return svgStyle ? svgStyle[0] : null;
};
export const getSvgId = (svgCode: string) => {
return svgCode.match(new RegExp('<svg.*id\\s*=\\s*([\'"])(.*?)\\1'))?.[MATCH_ID_INDEX];
};
export const svgStyleCleanup = (svgCode: string) => {
let svgId = getSvgId(svgCode);
if (!svgId) {
svgId = `x${uuidv4()}`;
const pos = svgCode.indexOf('<svg') + SVG_ID_INSERT_POS;
svgCode = svgCode.substring(0, pos) + `id="${svgId}" ` + svgCode.substring(pos);
}
let svgStyle = getSvgStyle(svgCode);
if (svgStyle) {
let replacedId = svgStyle.replace(/(#(.*?))?\./g, `#${svgId} .`);
svgCode = svgCode.replace(svgStyle, replacedId);
}
return svgCode;
};

View File

@ -60,6 +60,7 @@ export function IconDisplay(props: CanvasElementProps) {
src={data.path} src={data.path}
style={svgStyle} style={svgStyle}
className={svgStyle.strokeWidth ? svgStrokePathClass : undefined} className={svgStyle.strokeWidth ? svgStrokePathClass : undefined}
cleanStyle={true}
/> />
); );
} }