Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Canvas: Avoid conflicting stylesheets when loading SVG icons #74461

Merged
merged 13 commits into from
Sep 18, 2023
55 changes: 55 additions & 0 deletions public/app/core/components/SVG/SanitizedSVG.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { getSvgId, getSvgStyle, svgStyleCleanup } from './utils';

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="TEST_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('TEST_ID');
adela-almasan marked this conversation as resolved.
Show resolved Hide resolved
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>`
);
});
});
25 changes: 23 additions & 2 deletions public/app/core/components/SVG/SanitizedSVG.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,13 @@ import SVG, { Props } from 'react-inlinesvg';

import { textUtil } from '@grafana/data';

export const SanitizedSVG = (props: Props) => {
return <SVG {...props} cacheRequests={true} preProcessor={getCleanSVG} />;
import { svgStyleCleanup } from './utils';

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>();
Expand All @@ -15,5 +20,21 @@ function getCleanSVG(code: string): string {
clean = textUtil.sanitizeSVGContent(code);
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;
}
27 changes: 27 additions & 0 deletions public/app/core/components/SVG/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { v4 as uuidv4 } from 'uuid';

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'))?.[2];
adela-almasan marked this conversation as resolved.
Show resolved Hide resolved
};

export const svgStyleCleanup = (svgCode: string) => {
let svgId = getSvgId(svgCode);
if (!svgId) {
svgId = `x${uuidv4()}`;
const pos = svgCode.indexOf('<svg') + 5;
adela-almasan marked this conversation as resolved.
Show resolved Hide resolved
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;
};
1 change: 1 addition & 0 deletions public/app/features/canvas/elements/icon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ export function IconDisplay(props: CanvasElementProps) {
src={data.path}
style={svgStyle}
className={svgStyle.strokeWidth ? svgStrokePathClass : undefined}
cleanStyle={true}
adela-almasan marked this conversation as resolved.
Show resolved Hide resolved
/>
);
}
Expand Down