Skip to content

Commit

Permalink
[v10.1.x] Canvas: Avoid conflicting stylesheets when loading SVG icons (
Browse files Browse the repository at this point in the history
  • Loading branch information
grafana-delivery-bot[bot] committed Sep 18, 2023
1 parent 36704b2 commit 476771b
Show file tree
Hide file tree
Showing 4 changed files with 112 additions and 2 deletions.
58 changes: 58 additions & 0 deletions public/app/core/components/SVG/SanitizedSVG.test.tsx
@@ -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>`
);
});
});
25 changes: 23 additions & 2 deletions public/app/core/components/SVG/SanitizedSVG.tsx
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;
}
30 changes: 30 additions & 0 deletions public/app/core/components/SVG/utils.ts
@@ -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;
};
1 change: 1 addition & 0 deletions public/app/features/canvas/elements/icon.tsx
Expand Up @@ -60,6 +60,7 @@ export function IconDisplay(props: CanvasElementProps) {
src={data.path}
style={svgStyle}
className={svgStyle.strokeWidth ? svgStrokePathClass : undefined}
cleanStyle={true}
/>
);
}
Expand Down

0 comments on commit 476771b

Please sign in to comment.