diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 4812180d..2a9f75c1 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -22,7 +22,7 @@ jobs:
run: npm run lint
- name: Type check
- run: npm run lint:dts
+ run: npm run lint:tsc
- name: Run unit tests
run: npm run test:ci
diff --git a/.gitignore b/.gitignore
index 906d0294..46fcbcae 100644
--- a/.gitignore
+++ b/.gitignore
@@ -41,8 +41,8 @@ jspm_packages
.node_repl_history
# Build files
-build
-dist
+dist/
+lib/
# Vim swap files
*.swp
diff --git a/.prettierrc.json b/.prettierrc.json
index e9c0f50f..544138be 100644
--- a/.prettierrc.json
+++ b/.prettierrc.json
@@ -1,4 +1,3 @@
{
- "trailingComma": "none",
"singleQuote": true
}
diff --git a/README.md b/README.md
index 69a85ba5..4fbed292 100644
--- a/README.md
+++ b/README.md
@@ -21,8 +21,9 @@ To replace an element with another element, check out the [`replace`](#replace)
#### Example
-```js
-const parse = require('html-react-parser');
+```ts
+import parse from 'html-react-parser';
+
parse('
Hello, World!
'); // React.createElement('p', {}, 'Hello, World!')
```
@@ -43,6 +44,7 @@ parse('Hello, World!
'); // React.createElement('p', {}, 'Hello, World!')
- [htmlparser2](#htmlparser2)
- [trim](#trim)
- [Migration](#migration)
+ - [v5](#v5)
- [v4](#v4)
- [v3](#v3)
- [v2](#v2)
@@ -100,31 +102,31 @@ yarn add html-react-parser
Import ES module:
-```js
+```ts
import parse from 'html-react-parser';
```
Or require CommonJS module:
-```js
-const parse = require('html-react-parser');
+```ts
+const parse = require('html-react-parser').default;
```
Parse single element:
-```js
+```ts
parse('single
');
```
Parse multiple elements:
-```js
+```ts
parse('Item 1Item 2');
```
Make sure to render parsed adjacent elements under a parent element:
-```jsx
+```tsx
{parse(`
- Item 1
@@ -135,15 +137,15 @@ Make sure to render parsed adjacent elements under a parent element:
Parse nested elements:
-```js
+```ts
parse('Lorem ipsum
');
```
Parse element with attributes:
-```js
+```ts
parse(
- '
'
+ '
',
);
```
@@ -153,11 +155,11 @@ The `replace` option allows you to replace an element with another element.
The `replace` callback's first argument is [domhandler](https://github.com/fb55/domhandler#example)'s node:
-```js
+```ts
parse('
', {
- replace: (domNode) => {
+ replace(domNode) {
console.dir(domNode, { depth: null });
- }
+ },
});
```
@@ -165,7 +167,7 @@ parse('
', {
Console output
-```js
+```ts
Element {
type: 'tag',
parent: null,
@@ -184,29 +186,43 @@ Element {
The element is replaced if a **valid** React element is returned:
-```jsx
+```tsx
parse('
text
', {
- replace: (domNode) => {
+ replace(domNode) {
if (domNode.attribs && domNode.attribs.id === 'replace') {
return replaced;
}
- }
+ },
});
```
#### replace with TypeScript
-For TypeScript projects, you may need to check that `domNode` is an instance of domhandler's `Element`:
+For TypeScript, you'll need to check that `domNode` is an instance of domhandler's `Element`:
```tsx
import { HTMLReactParserOptions, Element } from 'html-react-parser';
const options: HTMLReactParserOptions = {
- replace: (domNode) => {
+ replace(domNode) {
if (domNode instanceof Element && domNode.attribs) {
// ...
}
- }
+ },
+};
+```
+
+Or use a type assertion:
+
+```tsx
+import { HTMLReactParserOptions, Element } from 'html-react-parser';
+
+const options: HTMLReactParserOptions = {
+ replace(domNode) {
+ if ((domNode as Element).attribs) {
+ // ...
+ }
+ },
};
```
@@ -216,7 +232,7 @@ If you're having issues take a look at our [Create React App example](./examples
Replace the element and its children (see [demo](https://replit.com/@remarkablemark/html-react-parser-replace-example)):
-```jsx
+```tsx
import parse, { domToReact } from 'html-react-parser';
const html = `
@@ -228,7 +244,7 @@ const html = `
`;
const options = {
- replace: ({ attribs, children }) => {
+ replace({ attribs, children }) {
if (!attribs) {
return;
}
@@ -244,7 +260,7 @@ const options = {
);
}
- }
+ },
};
parse(html, options);
@@ -273,7 +289,7 @@ parse(html, options);
Convert DOM attributes to React props with `attributesToProps`:
-```jsx
+```tsx
import parse, { attributesToProps } from 'html-react-parser';
const html = `
@@ -281,12 +297,12 @@ const html = `
`;
const options = {
- replace: (domNode) => {
+ replace(domNode) {
if (domNode.attribs && domNode.name === 'main') {
const props = attributesToProps(domNode.attribs);
return ;
}
- }
+ },
};
parse(html, options);
@@ -307,9 +323,9 @@ parse(html, options);
[Exclude](https://replit.com/@remarkablemark/html-react-parser-56) an element from rendering by replacing it with ``:
-```jsx
+```tsx
parse('
', {
- replace: ({ attribs }) => attribs && attribs.id === 'remove' && <>>
+ replace: ({ attribs }) => attribs?.id === 'remove' && <>>,
});
```
@@ -330,12 +346,12 @@ The `transform` option allows you to transform each element individually after i
The `transform` callback's first argument is the React element:
-```jsx
+```tsx
parse('
', {
- transform: (reactNode, domNode, index) => {
+ transform(reactNode, domNode, index) {
// this will wrap every element in a div
return {reactNode}
;
- }
+ },
});
```
@@ -345,15 +361,15 @@ The `library` option specifies the UI library. The default library is **React**.
To use Preact:
-```js
+```ts
parse('
', {
- library: require('preact')
+ library: require('preact'),
});
```
Or a custom library:
-```js
+```ts
parse('
', {
library: {
cloneElement: () => {
@@ -364,8 +380,8 @@ parse('
', {
},
isValidElement: () => {
/* ... */
- }
- }
+ },
+ },
});
```
@@ -377,11 +393,11 @@ Default [htmlparser2 options](https://github.com/fb55/htmlparser2/wiki/Parser-op
To enable [`xmlMode`](https://github.com/fb55/htmlparser2/wiki/Parser-options#option-xmlmode):
-```js
+```ts
parse('', {
htmlparser2: {
- xmlMode: true
- }
+ xmlMode: true,
+ },
});
```
@@ -389,30 +405,38 @@ parse('', {
By default, whitespace is preserved:
-```js
+```ts
parse('
\n'); // [React.createElement('br'), '\n']
```
But certain elements like `` will strip out invalid whitespace:
-```js
+```ts
parse(''); // React.createElement('table')
```
To remove whitespace, enable the `trim` option:
-```js
+```ts
parse('
\n', { trim: true }); // React.createElement('br')
```
However, intentional whitespace may be stripped out:
-```js
+```ts
parse('
', { trim: true }); // React.createElement('p')
```
## Migration
+### v5
+
+Migrated to TypeScript. CommonJS imports require the `.default` key:
+
+```ts
+const parse = require('html-react-parser').default;
+```
+
### v4
[htmlparser2](https://github.com/fb55/htmlparser2) has been upgraded to [v9](https://github.com/fb55/htmlparser2/releases/tag/v9.0.0).
@@ -437,11 +461,11 @@ For the `replace` option, you may need to do the following:
import { Element } from 'domhandler/lib/node';
parse('
', {
- replace: (domNode) => {
+ replace(domNode) {
if (domNode instanceof Element && domNode.attribs.class === 'remove') {
return <>>;
}
- }
+ },
});
```
@@ -490,8 +514,8 @@ Tags are lowercased by default. To prevent that from happening, pass the [htmlpa
```js
const options = {
htmlparser2: {
- lowerCaseTags: false
- }
+ lowerCaseTags: false,
+ },
};
parse('', options); // React.createElement('CustomElement')
```
@@ -527,8 +551,8 @@ Then update your Webpack config to:
module.exports = {
// ...
resolve: {
- mainFields: ['browser', 'main', 'module']
- }
+ mainFields: ['browser', 'main', 'module'],
+ },
};
```
diff --git a/benchmark/index.js b/benchmark/index.ts
similarity index 69%
rename from benchmark/index.js
rename to benchmark/index.ts
index 6f53a4a0..d80d9989 100644
--- a/benchmark/index.js
+++ b/benchmark/index.ts
@@ -1,6 +1,6 @@
-const Benchmark = require('benchmark');
-const { html } = require('../test/data');
-const parse = require('..');
+import Benchmark from 'benchmark';
+import { html } from '../test/data';
+import parse from '../src';
const suite = new Benchmark.Suite();
@@ -14,10 +14,10 @@ suite
.add('html-to-react - Complex', () => {
parse(html.complex);
})
- .on('cycle', (event) => {
+ .on('cycle', (event: Benchmark.Event) => {
process.stdout.write(String(event.target) + '\n');
})
.run({
minSamples: 100,
- delay: 2
+ delay: 2,
});
diff --git a/esm/attributes-to-props.mjs b/esm/attributes-to-props.mjs
new file mode 100644
index 00000000..77d70e48
--- /dev/null
+++ b/esm/attributes-to-props.mjs
@@ -0,0 +1,3 @@
+import attributesToProps from '../lib/attributes-to-props.js';
+
+export default attributesToProps.default;
diff --git a/esm/dom-to-react.mjs b/esm/dom-to-react.mjs
new file mode 100644
index 00000000..4282198c
--- /dev/null
+++ b/esm/dom-to-react.mjs
@@ -0,0 +1,3 @@
+import domToReact from '../lib/dom-to-react.js';
+
+export default domToReact.default;
diff --git a/esm/index.mjs b/esm/index.mjs
new file mode 100644
index 00000000..203ab17f
--- /dev/null
+++ b/esm/index.mjs
@@ -0,0 +1,13 @@
+import HTMLReactParser from '../lib/index.js';
+
+export {
+ Comment,
+ Element,
+ ProcessingInstruction,
+ Text,
+ attributesToProps,
+ domToReact,
+ htmlToDOM,
+} from '../lib/index.js';
+
+export default HTMLReactParser.default;
diff --git a/esm/utilities.mjs b/esm/utilities.mjs
new file mode 100644
index 00000000..281cb293
--- /dev/null
+++ b/esm/utilities.mjs
@@ -0,0 +1,8 @@
+export {
+ isCustomComponent,
+ setStyleProp,
+ PRESERVE_CUSTOM_ATTRIBUTES,
+ ELEMENTS_WITH_NO_TEXT_CHILDREN,
+ canTextBeChildOfNode,
+ returnFirstArg,
+} from '../lib/utilities.js';
diff --git a/index.d.ts b/index.d.ts
deleted file mode 100644
index e023822d..00000000
--- a/index.d.ts
+++ /dev/null
@@ -1,60 +0,0 @@
-// TypeScript Version: 5.3
-/* eslint-disable no-undef, no-unused-vars */
-
-import {
- Comment,
- Element,
- Node,
- ProcessingInstruction,
- Text
-} from 'domhandler';
-import type { DomHandlerOptions } from 'domhandler';
-import htmlToDOM from 'html-dom-parser';
-import { ParserOptions } from 'htmlparser2';
-
-import attributesToProps from './lib/attributes-to-props';
-import domToReact from './lib/dom-to-react';
-
-export { attributesToProps, domToReact, htmlToDOM };
-export type HTMLParser2Options = ParserOptions & DomHandlerOptions;
-export { Comment, Element, ProcessingInstruction, Text };
-export type DOMNode = Comment | Element | Node | ProcessingInstruction | Text;
-
-export interface HTMLReactParserOptions {
- htmlparser2?: HTMLParser2Options;
-
- library?: {
- cloneElement: (
- element: JSX.Element,
- props?: object,
- ...children: any
- ) => JSX.Element;
- createElement: (type: any, props?: object, ...children: any) => JSX.Element;
- isValidElement: (element: any) => boolean;
- [key: string]: any;
- };
-
- replace?: (
- domNode: DOMNode
- ) => JSX.Element | object | void | undefined | null | false;
-
- transform?: (
- reactNode: JSX.Element | string,
- domNode: DOMNode,
- index: number
- ) => JSX.Element | string | null;
-
- trim?: boolean;
-}
-
-/**
- * Converts HTML string to JSX element(s).
- *
- * @param html - HTML string.
- * @param options - Parser options.
- * @returns - JSX element(s), empty array, or string.
- */
-export default function HTMLReactParser(
- html: string,
- options?: HTMLReactParserOptions
-): ReturnType;
diff --git a/index.js b/index.js
deleted file mode 100644
index cea478cd..00000000
--- a/index.js
+++ /dev/null
@@ -1,50 +0,0 @@
-var domhandler = require('domhandler');
-var htmlToDOM = require('html-dom-parser').default;
-
-var attributesToProps = require('./lib/attributes-to-props');
-var domToReact = require('./lib/dom-to-react');
-
-// support backwards compatibility for ES Module
-htmlToDOM =
- /* istanbul ignore next */
- typeof htmlToDOM.default === 'function' ? htmlToDOM.default : htmlToDOM;
-
-var domParserOptions = { lowerCaseAttributeNames: false };
-
-/**
- * Converts HTML string to React elements.
- *
- * @param {string} html - HTML string.
- * @param {object} [options] - Parser options.
- * @param {object} [options.htmlparser2] - htmlparser2 options.
- * @param {object} [options.library] - Library for React, Preact, etc.
- * @param {Function} [options.replace] - Replace method.
- * @returns {JSX.Element|JSX.Element[]|string} - React element(s), empty array, or string.
- */
-function HTMLReactParser(html, options) {
- if (typeof html !== 'string') {
- throw new TypeError('First argument must be a string');
- }
- if (html === '') {
- return [];
- }
- options = options || {};
- return domToReact(
- htmlToDOM(html, options.htmlparser2 || domParserOptions),
- options
- );
-}
-
-HTMLReactParser.domToReact = domToReact;
-HTMLReactParser.htmlToDOM = htmlToDOM;
-HTMLReactParser.attributesToProps = attributesToProps;
-
-// domhandler
-HTMLReactParser.Comment = domhandler.Comment;
-HTMLReactParser.Element = domhandler.Element;
-HTMLReactParser.ProcessingInstruction = domhandler.ProcessingInstruction;
-HTMLReactParser.Text = domhandler.Text;
-
-// support CommonJS and ES Modules
-module.exports = HTMLReactParser;
-HTMLReactParser.default = HTMLReactParser;
diff --git a/index.mjs b/index.mjs
deleted file mode 100644
index 842eac57..00000000
--- a/index.mjs
+++ /dev/null
@@ -1,13 +0,0 @@
-import HTMLReactParser from './index.js';
-
-export var domToReact = HTMLReactParser.domToReact;
-export var htmlToDOM = HTMLReactParser.htmlToDOM;
-export var attributesToProps = HTMLReactParser.attributesToProps;
-
-// domhandler
-export var Comment = HTMLReactParser.Comment;
-export var Element = HTMLReactParser.Element;
-export var ProcessingInstruction = HTMLReactParser.ProcessingInstruction;
-export var Text = HTMLReactParser.Text;
-
-export default HTMLReactParser;
diff --git a/jest.config.ts b/jest.config.ts
new file mode 100644
index 00000000..aa8939f6
--- /dev/null
+++ b/jest.config.ts
@@ -0,0 +1,16 @@
+import type { Config } from 'jest';
+
+const config: Config = {
+ collectCoverage: true,
+ coverageThreshold: {
+ global: {
+ branches: 100,
+ functions: 100,
+ lines: 100,
+ statements: 100,
+ },
+ },
+ preset: 'ts-jest',
+};
+
+export default config;
diff --git a/lib/attributes-to-props.d.ts b/lib/attributes-to-props.d.ts
deleted file mode 100644
index aab614fc..00000000
--- a/lib/attributes-to-props.d.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-// TypeScript Version: 5.3
-/* eslint-disable no-unused-vars */
-
-export type Attributes = Record;
-
-export type Props = Record & {
- style: Record;
-};
-
-/**
- * Converts HTML/SVG DOM attributes to React props.
- *
- * @param attributes - HTML/SVG DOM attributes.
- * @param nodeName - DOM node name.
- * @returns - React props.
- */
-export default function attributesToProps(
- attributes: Attributes,
- nodeName?: string
-): Props;
diff --git a/lib/attributes-to-props.js b/lib/attributes-to-props.js
deleted file mode 100644
index b3a38bc7..00000000
--- a/lib/attributes-to-props.js
+++ /dev/null
@@ -1,92 +0,0 @@
-var reactProperty = require('react-property');
-var utilities = require('./utilities');
-
-// https://reactjs.org/docs/uncontrolled-components.html
-// https://developer.mozilla.org/docs/Web/HTML/Attributes
-var UNCONTROLLED_COMPONENT_ATTRIBUTES = ['checked', 'value'];
-var UNCONTROLLED_COMPONENT_NAMES = ['input', 'select', 'textarea'];
-
-var VALUE_ONLY_INPUTS = {
- reset: true,
- submit: true
-};
-
-/**
- * Converts HTML/SVG DOM attributes to React props.
- *
- * @param {object} [attributes={}] - HTML/SVG DOM attributes.
- * @param {string} [nodeName] - DOM node name.
- * @returns - React props.
- */
-module.exports = function attributesToProps(attributes, nodeName) {
- attributes = attributes || {};
-
- var attributeName;
- var attributeNameLowerCased;
- var attributeValue;
- var propName;
- var propertyInfo;
- var props = {};
- var inputIsValueOnly = attributes.type && VALUE_ONLY_INPUTS[attributes.type];
-
- for (attributeName in attributes) {
- attributeValue = attributes[attributeName];
-
- // ARIA (aria-*) or custom data (data-*) attribute
- if (reactProperty.isCustomAttribute(attributeName)) {
- props[attributeName] = attributeValue;
- continue;
- }
-
- // convert HTML/SVG attribute to React prop
- attributeNameLowerCased = attributeName.toLowerCase();
- propName = getPropName(attributeNameLowerCased);
-
- if (propName) {
- propertyInfo = reactProperty.getPropertyInfo(propName);
-
- // convert attribute to uncontrolled component prop (e.g., `value` to `defaultValue`)
- if (
- UNCONTROLLED_COMPONENT_ATTRIBUTES.indexOf(propName) !== -1 &&
- UNCONTROLLED_COMPONENT_NAMES.indexOf(nodeName) !== -1 &&
- !inputIsValueOnly
- ) {
- propName = getPropName('default' + attributeNameLowerCased);
- }
-
- props[propName] = attributeValue;
-
- switch (propertyInfo && propertyInfo.type) {
- case reactProperty.BOOLEAN:
- props[propName] = true;
- break;
- case reactProperty.OVERLOADED_BOOLEAN:
- if (attributeValue === '') {
- props[propName] = true;
- }
- break;
- }
- continue;
- }
-
- // preserve custom attribute if React >=16
- if (utilities.PRESERVE_CUSTOM_ATTRIBUTES) {
- props[attributeName] = attributeValue;
- }
- }
-
- // transform inline style to object
- utilities.setStyleProp(attributes.style, props);
-
- return props;
-};
-
-/**
- * Gets prop name from lowercased attribute name.
- *
- * @param {string} attributeName - Lowercased attribute name.
- * @returns - Prop name.
- */
-function getPropName(attributeName) {
- return reactProperty.possibleStandardNames[attributeName];
-}
diff --git a/lib/dom-to-react.d.ts b/lib/dom-to-react.d.ts
deleted file mode 100644
index cb66a038..00000000
--- a/lib/dom-to-react.d.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-// TypeScript Version: 5.3
-/* eslint-disable no-undef, no-unused-vars */
-
-import { DOMNode, HTMLReactParserOptions } from '..';
-
-export { DOMNode, HTMLReactParserOptions };
-
-/**
- * Converts DOM nodes to JSX element(s).
- *
- * @param nodes - DOM nodes.
- * @param options - Parser options.
- * @returns - JSX element(s).
- */
-export default function domToReact(
- nodes: DOMNode[],
- options?: HTMLReactParserOptions
-): string | JSX.Element | JSX.Element[];
diff --git a/lib/dom-to-react.js b/lib/dom-to-react.js
deleted file mode 100644
index fc7a4a6a..00000000
--- a/lib/dom-to-react.js
+++ /dev/null
@@ -1,141 +0,0 @@
-var React = require('react');
-var attributesToProps = require('./attributes-to-props');
-var utilities = require('./utilities');
-
-var setStyleProp = utilities.setStyleProp;
-var canTextBeChildOfNode = utilities.canTextBeChildOfNode;
-
-/**
- * Converts DOM nodes to JSX element(s).
- *
- * @param {DomElement[]} nodes - DOM nodes.
- * @param {object} [options={}] - Options.
- * @param {Function} [options.replace] - Replacer.
- * @param {Function} [options.transform] - Transform.
- * @param {object} [options.library] - Library (React, Preact, etc.).
- * @returns - String or JSX element(s).
- */
-function domToReact(nodes, options) {
- options = options || {};
-
- var library = options.library || React;
- var cloneElement = library.cloneElement;
- var createElement = library.createElement;
- var isValidElement = library.isValidElement;
-
- var result = [];
- var node;
- var isWhitespace;
- var hasReplace = typeof options.replace === 'function';
- var transform = options.transform || utilities.returnFirstArg;
- var replaceElement;
- var props;
- var children;
- var trim = options.trim;
-
- for (var i = 0, len = nodes.length; i < len; i++) {
- node = nodes[i];
-
- // replace with custom React element (if present)
- if (hasReplace) {
- replaceElement = options.replace(node);
-
- if (isValidElement(replaceElement)) {
- // set "key" prop for sibling elements
- // https://fb.me/react-warning-keys
- if (len > 1) {
- replaceElement = cloneElement(replaceElement, {
- key: replaceElement.key || i
- });
- }
- result.push(transform(replaceElement, node, i));
- continue;
- }
- }
-
- if (node.type === 'text') {
- isWhitespace = !node.data.trim().length;
-
- if (isWhitespace && node.parent && !canTextBeChildOfNode(node.parent)) {
- // We have a whitespace node that can't be nested in its parent
- // so skip it
- continue;
- }
-
- if (trim && isWhitespace) {
- // Trim is enabled and we have a whitespace node
- // so skip it
- continue;
- }
-
- // We have a text node that's not whitespace and it can be nested
- // in its parent so add it to the results
- result.push(transform(node.data, node, i));
- continue;
- }
-
- props = node.attribs;
- if (skipAttributesToProps(node)) {
- setStyleProp(props.style, props);
- } else if (props) {
- props = attributesToProps(props, node.name);
- }
-
- children = null;
-
- switch (node.type) {
- case 'script':
- case 'style':
- // prevent text in