Skip to content

Commit

Permalink
Refactor(web-react): Introduce htmlReactParser for rendering SVG icons
Browse files Browse the repository at this point in the history
  * this is a better workaround than postprocessing a generated CJS files
  * it is based on the named exports which works quite well
  • Loading branch information
literat committed Mar 8, 2024
1 parent 4a00ca5 commit 63a4a7e
Show file tree
Hide file tree
Showing 4 changed files with 268 additions and 1 deletion.
1 change: 1 addition & 0 deletions packages/web-react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"@react-hook/resize-observer": "^1.2.6",
"classnames": "^2.3.1",
"html-react-parser": "5.0.11",
"html-dom-parser": "5.0.8",
"react-transition-group": "^4.4.5"
},
"devDependencies": {
Expand Down
2 changes: 1 addition & 1 deletion packages/web-react/src/components/Icon/Icon.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import htmlReactParser from 'html-react-parser';
import React, { ForwardedRef, forwardRef } from 'react';
import { useIcon, useStyleProps } from '../../hooks';
import { IconProps } from '../../types';
import { htmlReactParser } from '../../utils/htmlReactParser';

const defaultProps = {
ariaHidden: true,
Expand Down
246 changes: 246 additions & 0 deletions packages/web-react/src/utils/__tests__/htmlReactParser.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,246 @@
/**
* This test file is based on the original `html-react-parser` test file.
*
* @see { @link https://github.com/remarkablemark/html-react-parser/blob/master/__tests__/index.test.tsx }
*/
import { htmlReactParser } from '../htmlReactParser';

const html = {
single: '<p>foo</p>',
multiple: '<p>foo</p><p>bar</p>',
nested: '<div><p>foo <em>bar</em></p></div>',
attributes: '<hr id="foo" class="bar baz" style="background:#fff;text-align:center" data-foo="bar"/>',
complex:
'<html><head><meta charSet="utf-8"/><title>Title</title><link rel="stylesheet" href="style.css"/></head><body><header id="header">Header</header><h1 style="color:#000;font-size:42px">Heading</h1><hr/><p>Paragraph</p><img src="image.jpg"/><div class="class1 class2">Some<em>text</em>.</div><script>alert();</script></body></html>',
textarea: '<textarea>foo</textarea>',
script: '<script>alert(1 < 2);</script>',
style: '<style>body > .foo { color: #f00; }</style>',
img: '<img src="http://stat.ic/img.jpg" alt="Image"/>',
void: '<link/><meta/><img/><br/><hr/><input/>',
comment: '<!-- comment -->',
doctype: '<!DOCTYPE html>',
title: '<title><em>text</em></title>',
customElement:
'<custom-element class="myClass" custom-attribute="value" style="-o-transition: all .5s; line-height: 1;"></custom-element>',
form: '<input type="text" value="foo" checked="checked">',
list: '<ol><li>One</li><li value="2">Two</li></ol>',
template: '<template><article><p>Test</p></article></template>',
} as const;

const svg = {
simple: '<svg viewBox="0 0 512 512" id="foo">Inner</svg>',
complex:
'<svg height="400" width="450"><path id="lineAB" d="M 100 350 l 150 -300" stroke="red" stroke-width="3" fill="none"></path><g stroke="black" stroke-width="3" fill="black"><circle id="pointA" cx="100" cy="350" r="3"></circle></g><g font-size="30" font-family="sans-serif" fill="black" stroke="none" text-anchor="middle"><text x="100" y="350" dx="-30">A</text></g>Your browser does not support inline SVG.</svg>',
} as const;

describe('htmlReactParser', () => {
it.each([undefined, null, {}, [], true, false, 0, 1, () => {}, new Date()])('throws error for value: %p', (value) => {
expect(() => {
htmlReactParser(value as string);
}).toThrow(TypeError);
});

it('parses "" to []', () => {
expect(htmlReactParser('')).toEqual([]);
});

it('returns string if it is not HTML', () => {
const string = 'text';
expect(htmlReactParser(string)).toBe(string);
});

it('parses single HTML element', () => {
expect(htmlReactParser(html.single)).toMatchInlineSnapshot(`
<p>
foo
</p>
`);
});

it('parses single HTML element with comment', () => {
// comment should be ignored
expect(htmlReactParser(html.single + html.comment)).toMatchInlineSnapshot(`
<p>
foo
</p>
`);
});

it('parses multiple HTML elements', () => {
expect(htmlReactParser(html.multiple)).toMatchInlineSnapshot(`
[
<p>
foo
</p>,
<p>
bar
</p>,
]
`);
});

it('parses complex HTML with doctype', () => {
expect(htmlReactParser(html.doctype + html.complex)).toMatchInlineSnapshot(`
<html>
<head>
<meta
charSet="utf-8"
/>
<title>
Title
</title>
<link
href="style.css"
rel="stylesheet"
/>
</head>
<body>
<header
id="header"
>
Header
</header>
<h1
style={
{
"color": "#000",
"fontSize": "42px",
}
}
>
Heading
</h1>
<hr />
<p>
Paragraph
</p>
<img
src="image.jpg"
/>
<div
className="class1 class2"
>
Some
<em>
text
</em>
.
</div>
<script
dangerouslySetInnerHTML={
{
"__html": "alert();",
}
}
/>
</body>
</html>
`);
});

it('parses empty <script>', () => {
expect(htmlReactParser('<script></script>')).toMatchInlineSnapshot('<script />');
});

it('parses empty <style>', () => {
expect(htmlReactParser('<style></style>')).toMatchInlineSnapshot('<style />');
});

it('parses form', () => {
expect(htmlReactParser(html.form)).toMatchInlineSnapshot(`
<input
defaultChecked={true}
defaultValue="foo"
type="text"
/>
`);
});

it('parses list', () => {
expect(htmlReactParser(html.list)).toMatchInlineSnapshot(`
<ol>
<li>
One
</li>
<li
value="2"
>
Two
</li>
</ol>
`);
});

it('parses template', () => {
expect(htmlReactParser(html.template)).toMatchInlineSnapshot(`
<template>
<article>
<p>
Test
</p>
</article>
</template>
`);
});

it('parses SVG', () => {
expect(htmlReactParser(svg.complex)).toMatchInlineSnapshot(`
<svg
height="400"
width="450"
>
<path
d="M 100 350 l 150 -300"
fill="none"
id="lineAB"
stroke="red"
strokeWidth="3"
/>
<g
fill="black"
stroke="black"
strokeWidth="3"
>
<circle
cx="100"
cy="350"
id="pointA"
r="3"
/>
</g>
<g
fill="black"
fontFamily="sans-serif"
fontSize="30"
stroke="none"
textAnchor="middle"
>
<text
dx="-30"
x="100"
y="350"
>
A
</text>
</g>
Your browser does not support inline SVG.
</svg>
`);
});

it('decodes HTML entities', () => {
const encodedEntities = 'asdf &amp; &yuml; &uuml; &apos;';
// eslint-disable-next-line quotes -- using double quotes to avoid escaping
const decodedEntities = "asdf & ÿ ü '";
const reactElement = htmlReactParser(`<i>${encodedEntities}</i>`) as JSX.Element;
expect(reactElement.props.children).toBe(decodedEntities);
});

it('escapes tags inside of <title>', () => {
expect(htmlReactParser(html.title)).toMatchInlineSnapshot(`
<title>
&lt;em&gt;text&lt;/em&gt;
</title>
`);
});
});
20 changes: 20 additions & 0 deletions packages/web-react/src/utils/htmlReactParser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/**
* This is a workaround for the `html-react-parser` package.
* The package weirdly exports the default function as `HTMLReactParser` and it is not working in our CJS builds.
*
* @see { @link https://github.com/remarkablemark/html-react-parser/issues/1329 }
*/
import htmlToDOM from 'html-dom-parser';
import domToReact from 'html-react-parser/lib/dom-to-react';

export const htmlReactParser = (html: string): ReturnType<typeof domToReact> => {
if (typeof html !== 'string') {
throw new TypeError('First argument must be a string');
}

if (!html) {
return [];
}

return domToReact(htmlToDOM(html));
};

0 comments on commit 63a4a7e

Please sign in to comment.