-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Refactor(web-react): Introduce
htmlReactParser
for rendering SVG icons
* 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
Showing
4 changed files
with
268 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
246 changes: 246 additions & 0 deletions
246
packages/web-react/src/utils/__tests__/htmlReactParser.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 & ÿ ü ''; | ||
// 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> | ||
<em>text</em> | ||
</title> | ||
`); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)); | ||
}; |