html-react-parser 4.2.10 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,42 @@
1
+ import type { Element } from 'html-dom-parser';
2
+ import type { Props } from './attributes-to-props';
3
+ /**
4
+ * Check if a tag is a custom component.
5
+ *
6
+ * @see {@link https://github.com/facebook/react/blob/v16.6.3/packages/react-dom/src/shared/isCustomComponent.js}
7
+ *
8
+ * @param tagName - Tag name.
9
+ * @param props - Props passed to the element.
10
+ * @returns - Whether the tag is custom component.
11
+ */
12
+ export declare function isCustomComponent(tagName: string, props?: Record<string, any>): boolean;
13
+ /**
14
+ * Sets style prop.
15
+ *
16
+ * @param style - Inline style.
17
+ * @param props - Props object.
18
+ */
19
+ export declare function setStyleProp(style: string, props: Props): void;
20
+ /**
21
+ * @see https://reactjs.org/blog/2017/09/08/dom-attributes-in-react-16.html
22
+ */
23
+ export declare const PRESERVE_CUSTOM_ATTRIBUTES: boolean;
24
+ /**
25
+ * @see https://github.com/facebook/react/blob/cae635054e17a6f107a39d328649137b83f25972/packages/react-dom/src/client/validateDOMNesting.js#L213
26
+ */
27
+ export declare const ELEMENTS_WITH_NO_TEXT_CHILDREN: Set<string>;
28
+ /**
29
+ * Checks if the given node can contain text nodes
30
+ *
31
+ * @param node - Element node.
32
+ * @returns - Whether the node can contain text nodes.
33
+ */
34
+ export declare const canTextBeChildOfNode: (node: Element) => boolean;
35
+ /**
36
+ * Returns the first argument as is.
37
+ *
38
+ * @param arg - The argument to be returned.
39
+ * @returns - The input argument `arg`.
40
+ */
41
+ export declare const returnFirstArg: (arg: any) => any;
42
+ //# sourceMappingURL=utilities.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utilities.d.ts","sourceRoot":"","sources":["../src/utilities.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAC;AAE/C,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,uBAAuB,CAAC;AAanD;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,MAAM,EACf,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAC1B,OAAO,CAcT;AAMD;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,GAAG,IAAI,CAe9D;AAED;;GAEG;AACH,eAAO,MAAM,0BAA0B,SAAsC,CAAC;AAE9E;;GAEG;AACH,eAAO,MAAM,8BAA8B,aAUzC,CAAC;AAEH;;;;;GAKG;AACH,eAAO,MAAM,oBAAoB,SAAU,OAAO,YACF,CAAC;AAEjD;;;;;GAKG;AACH,eAAO,MAAM,cAAc,QAAS,GAAG,QAAQ,CAAC"}
package/lib/utilities.js CHANGED
@@ -1,105 +1,103 @@
1
- var React = require('react');
2
- var styleToJS = require('style-to-js').default;
3
-
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.returnFirstArg = exports.canTextBeChildOfNode = exports.ELEMENTS_WITH_NO_TEXT_CHILDREN = exports.PRESERVE_CUSTOM_ATTRIBUTES = exports.setStyleProp = exports.isCustomComponent = void 0;
7
+ var react_1 = require("react");
8
+ var style_to_js_1 = __importDefault(require("style-to-js"));
4
9
  var RESERVED_SVG_MATHML_ELEMENTS = new Set([
5
- 'annotation-xml',
6
- 'color-profile',
7
- 'font-face',
8
- 'font-face-src',
9
- 'font-face-uri',
10
- 'font-face-format',
11
- 'font-face-name',
12
- 'missing-glyph'
10
+ 'annotation-xml',
11
+ 'color-profile',
12
+ 'font-face',
13
+ 'font-face-src',
14
+ 'font-face-uri',
15
+ 'font-face-format',
16
+ 'font-face-name',
17
+ 'missing-glyph',
13
18
  ]);
14
-
15
19
  /**
16
- * Check if a given tag is a custom component.
20
+ * Check if a tag is a custom component.
17
21
  *
18
22
  * @see {@link https://github.com/facebook/react/blob/v16.6.3/packages/react-dom/src/shared/isCustomComponent.js}
19
23
  *
20
- * @param {string} tagName - The name of the html tag.
21
- * @param {object} props - The props being passed to the element.
22
- * @returns {boolean} - Whether tag is custom component.
24
+ * @param tagName - Tag name.
25
+ * @param props - Props passed to the element.
26
+ * @returns - Whether the tag is custom component.
23
27
  */
24
28
  function isCustomComponent(tagName, props) {
25
- if (tagName.indexOf('-') === -1) {
26
- return props && typeof props.is === 'string';
27
- }
28
- // These are reserved SVG and MathML elements.
29
- // We don't mind this whitelist too much because we expect it to never grow.
30
- // The alternative is to track the namespace in a few places which is convoluted.
31
- // https://w3c.github.io/webcomponents/spec/custom/#custom-elements-core-concepts
32
- if (RESERVED_SVG_MATHML_ELEMENTS.has(tagName)) {
33
- return false;
34
- }
35
- return true;
29
+ if (tagName.indexOf('-') === -1) {
30
+ return Boolean(props && typeof props.is === 'string');
31
+ }
32
+ // These are reserved SVG and MathML elements.
33
+ // We don't mind this whitelist too much because we expect it to never grow.
34
+ // The alternative is to track the namespace in a few places which is convoluted.
35
+ // https://w3c.github.io/webcomponents/spec/custom/#custom-elements-core-concepts
36
+ if (RESERVED_SVG_MATHML_ELEMENTS.has(tagName)) {
37
+ return false;
38
+ }
39
+ return true;
36
40
  }
37
-
38
- // styleToJSOptions
39
- var STYLE_TO_JS_OPTIONS = { reactCompat: true };
40
-
41
+ exports.isCustomComponent = isCustomComponent;
42
+ var styleOptions = {
43
+ reactCompat: true,
44
+ };
41
45
  /**
42
46
  * Sets style prop.
43
47
  *
44
- * @param {null|undefined|string} style
45
- * @param {object} props
48
+ * @param style - Inline style.
49
+ * @param props - Props object.
46
50
  */
47
51
  function setStyleProp(style, props) {
48
- if (style === null || style === undefined) {
49
- return;
50
- }
51
- try {
52
- props.style = styleToJS(style, STYLE_TO_JS_OPTIONS);
53
- } catch (err) {
54
- props.style = {};
55
- }
52
+ if (typeof style !== 'string') {
53
+ return;
54
+ }
55
+ if (!style.trim()) {
56
+ props.style = {};
57
+ return;
58
+ }
59
+ try {
60
+ props.style = (0, style_to_js_1.default)(style, styleOptions);
61
+ }
62
+ catch (error) {
63
+ props.style = {};
64
+ }
56
65
  }
57
-
66
+ exports.setStyleProp = setStyleProp;
67
+ /**
68
+ * @see https://reactjs.org/blog/2017/09/08/dom-attributes-in-react-16.html
69
+ */
70
+ exports.PRESERVE_CUSTOM_ATTRIBUTES = Number(react_1.version.split('.')[0]) >= 16;
58
71
  /**
59
- * @constant {boolean}
60
- * @see {@link https://reactjs.org/blog/2017/09/08/dom-attributes-in-react-16.html}
72
+ * @see https://github.com/facebook/react/blob/cae635054e17a6f107a39d328649137b83f25972/packages/react-dom/src/client/validateDOMNesting.js#L213
61
73
  */
62
- var PRESERVE_CUSTOM_ATTRIBUTES = React.version.split('.')[0] >= 16;
63
-
64
- // Taken from
65
- // https://github.com/facebook/react/blob/cae635054e17a6f107a39d328649137b83f25972/packages/react-dom/src/client/validateDOMNesting.js#L213
66
- var ELEMENTS_WITH_NO_TEXT_CHILDREN = new Set([
67
- 'tr',
68
- 'tbody',
69
- 'thead',
70
- 'tfoot',
71
- 'colgroup',
72
- 'table',
73
- 'head',
74
- 'html',
75
- 'frameset'
74
+ exports.ELEMENTS_WITH_NO_TEXT_CHILDREN = new Set([
75
+ 'tr',
76
+ 'tbody',
77
+ 'thead',
78
+ 'tfoot',
79
+ 'colgroup',
80
+ 'table',
81
+ 'head',
82
+ 'html',
83
+ 'frameset',
76
84
  ]);
77
-
78
85
  /**
79
86
  * Checks if the given node can contain text nodes
80
87
  *
81
- * @param {DomElement} node - Node.
82
- * @returns - Whether node can contain text nodes.
88
+ * @param node - Element node.
89
+ * @returns - Whether the node can contain text nodes.
83
90
  */
84
- function canTextBeChildOfNode(node) {
85
- return !ELEMENTS_WITH_NO_TEXT_CHILDREN.has(node.name);
86
- }
87
-
91
+ var canTextBeChildOfNode = function (node) {
92
+ return !exports.ELEMENTS_WITH_NO_TEXT_CHILDREN.has(node.name);
93
+ };
94
+ exports.canTextBeChildOfNode = canTextBeChildOfNode;
88
95
  /**
89
96
  * Returns the first argument as is.
90
97
  *
91
- * @param {any} arg - The argument to be returned.
92
- * @returns {any} The input argument `arg`.
98
+ * @param arg - The argument to be returned.
99
+ * @returns - The input argument `arg`.
93
100
  */
94
- function returnFirstArg(arg) {
95
- return arg;
96
- }
97
-
98
- module.exports = {
99
- PRESERVE_CUSTOM_ATTRIBUTES: PRESERVE_CUSTOM_ATTRIBUTES,
100
- ELEMENTS_WITH_NO_TEXT_CHILDREN: ELEMENTS_WITH_NO_TEXT_CHILDREN,
101
- isCustomComponent: isCustomComponent,
102
- setStyleProp: setStyleProp,
103
- canTextBeChildOfNode: canTextBeChildOfNode,
104
- returnFirstArg: returnFirstArg
105
- };
101
+ var returnFirstArg = function (arg) { return arg; };
102
+ exports.returnFirstArg = returnFirstArg;
103
+ //# sourceMappingURL=utilities.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utilities.js","sourceRoot":"","sources":["../src/utilities.ts"],"names":[],"mappings":";;;;;;AAAA,+BAAgC;AAChC,4DAAoC;AAKpC,IAAM,4BAA4B,GAAG,IAAI,GAAG,CAAC;IAC3C,gBAAgB;IAChB,eAAe;IACf,WAAW;IACX,eAAe;IACf,eAAe;IACf,kBAAkB;IAClB,gBAAgB;IAChB,eAAe;CAChB,CAAC,CAAC;AAEH;;;;;;;;GAQG;AACH,SAAgB,iBAAiB,CAC/B,OAAe,EACf,KAA2B;IAE3B,IAAI,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE;QAC/B,OAAO,OAAO,CAAC,KAAK,IAAI,OAAO,KAAK,CAAC,EAAE,KAAK,QAAQ,CAAC,CAAC;KACvD;IAED,8CAA8C;IAC9C,4EAA4E;IAC5E,iFAAiF;IACjF,iFAAiF;IACjF,IAAI,4BAA4B,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE;QAC7C,OAAO,KAAK,CAAC;KACd;IAED,OAAO,IAAI,CAAC;AACd,CAAC;AAjBD,8CAiBC;AAED,IAAM,YAAY,GAAG;IACnB,WAAW,EAAE,IAAI;CACT,CAAC;AAEX;;;;;GAKG;AACH,SAAgB,YAAY,CAAC,KAAa,EAAE,KAAY;IACtD,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE;QAC7B,OAAO;KACR;IAED,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,EAAE;QACjB,KAAK,CAAC,KAAK,GAAG,EAAE,CAAC;QACjB,OAAO;KACR;IAED,IAAI;QACF,KAAK,CAAC,KAAK,GAAG,IAAA,qBAAS,EAAC,KAAK,EAAE,YAAY,CAAC,CAAC;KAC9C;IAAC,OAAO,KAAK,EAAE;QACd,KAAK,CAAC,KAAK,GAAG,EAAE,CAAC;KAClB;AACH,CAAC;AAfD,oCAeC;AAED;;GAEG;AACU,QAAA,0BAA0B,GAAG,MAAM,CAAC,eAAO,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;AAE9E;;GAEG;AACU,QAAA,8BAA8B,GAAG,IAAI,GAAG,CAAC;IACpD,IAAI;IACJ,OAAO;IACP,OAAO;IACP,OAAO;IACP,UAAU;IACV,OAAO;IACP,MAAM;IACN,MAAM;IACN,UAAU;CACX,CAAC,CAAC;AAEH;;;;;GAKG;AACI,IAAM,oBAAoB,GAAG,UAAC,IAAa;IAChD,OAAA,CAAC,sCAA8B,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC;AAA9C,CAA8C,CAAC;AADpC,QAAA,oBAAoB,wBACgB;AAEjD;;;;;GAKG;AACI,IAAM,cAAc,GAAG,UAAC,GAAQ,IAAK,OAAA,GAAG,EAAH,CAAG,CAAC;AAAnC,QAAA,cAAc,kBAAqB"}
package/package.json CHANGED
@@ -1,37 +1,40 @@
1
1
  {
2
2
  "name": "html-react-parser",
3
- "version": "4.2.10",
3
+ "version": "5.0.0",
4
4
  "description": "HTML to React parser.",
5
5
  "author": "Mark <mark@remarkablemark.org>",
6
- "main": "index.js",
7
- "module": "index.mjs",
6
+ "main": "./lib/index.js",
7
+ "module": "./esm/index.mjs",
8
+ "types": "./lib/index.d.ts",
8
9
  "exports": {
9
10
  ".": {
10
- "types": "./index.d.ts",
11
- "import": "./index.mjs",
12
- "require": "./index.js"
11
+ "types": "./lib/index.d.ts",
12
+ "import": "./esm/index.mjs",
13
+ "require": "./lib/index.js"
13
14
  },
14
15
  "./lib/*": {
15
16
  "types": "./lib/*.d.ts",
17
+ "import": "./esm/*.mjs",
16
18
  "default": "./lib/*.js"
17
- },
18
- "./package.json": "./package.json"
19
+ }
19
20
  },
20
21
  "scripts": {
21
- "benchmark": "node benchmark",
22
- "build": "rollup --config --failAfterWarnings",
23
- "clean": "rimraf dist",
22
+ "benchmark": "ts-node benchmark",
23
+ "build": "npm run build:cjs && npm run build:umd",
24
+ "build:cjs": "tsc --project tsconfig.build.json",
25
+ "build:umd": "rollup --config --failAfterWarnings",
26
+ "clean": "rm -rf coverage lib dist",
24
27
  "lint": "eslint --ignore-path .gitignore --ignore-pattern /examples/ .",
25
- "lint:dts": "dtslint .",
26
28
  "lint:fix": "npm run lint -- --fix",
29
+ "lint:tsc": "tsc --noEmit",
27
30
  "_postinstall": "husky install",
28
31
  "postpublish": "pinst --enable",
29
- "prepublishOnly": "pinst --disable && npm run lint && npm run lint:dts && npm run test:ci && npm run clean && npm run build",
32
+ "prepublishOnly": "pinst --disable && npm run lint && npm run test:ci && npm run clean && npm run build",
30
33
  "size-limit": "size-limit",
31
- "test": "jest --coverage --testPathIgnorePatterns test/integration/",
32
- "test:ci": "npm test -- --ci --colors",
33
- "test:esm": "node --test test/esm",
34
- "test:integration": "npm run build && jest --env=jsdom test/integration/",
34
+ "test": "jest --testPathIgnorePatterns test/integration",
35
+ "test:ci": "CI=true npm test -- --ci --colors",
36
+ "test:esm": "npm run build:cjs && node --test test/esm",
37
+ "test:integration": "npm run build && jest --coverage=false --env=jsdom test/integration",
35
38
  "test:watch": "npm test -- --watch"
36
39
  },
37
40
  "repository": {
@@ -57,12 +60,15 @@
57
60
  "devDependencies": {
58
61
  "@commitlint/cli": "18.2.0",
59
62
  "@commitlint/config-conventional": "18.1.0",
60
- "@definitelytyped/dtslint": "0.0.182",
61
63
  "@rollup/plugin-commonjs": "25.0.7",
62
64
  "@rollup/plugin-node-resolve": "15.2.3",
63
65
  "@rollup/plugin-terser": "0.4.4",
66
+ "@rollup/plugin-typescript": "11.1.5",
64
67
  "@size-limit/preset-big-lib": "10.0.1",
68
+ "@types/benchmark": "2.1.4",
69
+ "@types/jest": "29.5.6",
65
70
  "@types/react": "18.2.33",
71
+ "@types/react-dom": "18.2.14",
66
72
  "@typescript-eslint/eslint-plugin": "6.9.0",
67
73
  "@typescript-eslint/parser": "6.9.0",
68
74
  "benchmark": "2.1.4",
@@ -76,9 +82,9 @@
76
82
  "preact": "10.18.1",
77
83
  "react": "18.2.0",
78
84
  "react-dom": "18.2.0",
79
- "rimraf": "5.0.5",
80
85
  "rollup": "4.1.4",
81
86
  "size-limit": "10.0.1",
87
+ "ts-jest": "29.1.1",
82
88
  "typescript": "5.2.2"
83
89
  },
84
90
  "peerDependencies": {
@@ -86,9 +92,9 @@
86
92
  },
87
93
  "files": [
88
94
  "/dist",
95
+ "/esm",
89
96
  "/lib",
90
- "index.d.ts",
91
- "index.mjs"
97
+ "/src"
92
98
  ],
93
99
  "collective": {
94
100
  "type": "opencollective",
@@ -0,0 +1,108 @@
1
+ import {
2
+ BOOLEAN,
3
+ OVERLOADED_BOOLEAN,
4
+ getPropertyInfo,
5
+ isCustomAttribute,
6
+ possibleStandardNames,
7
+ } from 'react-property';
8
+ import { PRESERVE_CUSTOM_ATTRIBUTES, setStyleProp } from './utilities';
9
+
10
+ // https://react.dev/learn/sharing-state-between-components#controlled-and-uncontrolled-components
11
+ // https://developer.mozilla.org/docs/Web/HTML/Attributes
12
+ const UNCONTROLLED_COMPONENT_ATTRIBUTES = ['checked', 'value'];
13
+ const UNCONTROLLED_COMPONENT_NAMES = ['input', 'select', 'textarea'];
14
+
15
+ const valueOnlyInputs = {
16
+ reset: true,
17
+ submit: true,
18
+ } as const;
19
+
20
+ export type Attributes = Record<string, string>;
21
+
22
+ export type Props = Record<string, string | boolean> & {
23
+ dangerouslySetInnerHTML?: {
24
+ __html: string;
25
+ };
26
+ key?: string | number;
27
+ style?: Record<string, string>;
28
+ };
29
+
30
+ /**
31
+ * Converts HTML/SVG DOM attributes to React props.
32
+ *
33
+ * @param attributes - HTML/SVG DOM attributes.
34
+ * @param nodeName - DOM node name.
35
+ * @returns - React props.
36
+ */
37
+ export default function attributesToProps(
38
+ attributes: Attributes = {},
39
+ nodeName?: string,
40
+ ): Props {
41
+ const props: Props = {};
42
+
43
+ const isInputValueOnly = Boolean(
44
+ attributes.type &&
45
+ valueOnlyInputs[attributes.type as keyof typeof valueOnlyInputs],
46
+ );
47
+
48
+ for (const attributeName in attributes) {
49
+ const attributeValue = attributes[attributeName];
50
+
51
+ // ARIA (aria-*) or custom data (data-*) attribute
52
+ if (isCustomAttribute(attributeName)) {
53
+ props[attributeName] = attributeValue;
54
+ continue;
55
+ }
56
+
57
+ // convert HTML/SVG attribute to React prop
58
+ const attributeNameLowerCased = attributeName.toLowerCase();
59
+ let propName = getPropName(attributeNameLowerCased);
60
+
61
+ if (propName) {
62
+ const propertyInfo = getPropertyInfo(propName);
63
+
64
+ // convert attribute to uncontrolled component prop (e.g., `value` to `defaultValue`)
65
+ if (
66
+ UNCONTROLLED_COMPONENT_ATTRIBUTES.indexOf(propName) !== -1 &&
67
+ UNCONTROLLED_COMPONENT_NAMES.indexOf(nodeName!) !== -1 &&
68
+ !isInputValueOnly
69
+ ) {
70
+ propName = getPropName('default' + attributeNameLowerCased);
71
+ }
72
+
73
+ props[propName] = attributeValue;
74
+
75
+ switch (propertyInfo && propertyInfo.type) {
76
+ case BOOLEAN:
77
+ props[propName] = true;
78
+ break;
79
+ case OVERLOADED_BOOLEAN:
80
+ if (attributeValue === '') {
81
+ props[propName] = true;
82
+ }
83
+ break;
84
+ }
85
+ continue;
86
+ }
87
+
88
+ // preserve custom attribute if React >=16
89
+ if (PRESERVE_CUSTOM_ATTRIBUTES) {
90
+ props[attributeName] = attributeValue;
91
+ }
92
+ }
93
+
94
+ // transform inline style to object
95
+ setStyleProp(attributes.style, props);
96
+
97
+ return props;
98
+ }
99
+
100
+ /**
101
+ * Gets prop name from lowercased attribute name.
102
+ *
103
+ * @param attributeName - Lowercased attribute name.
104
+ * @returns - Prop name.
105
+ */
106
+ function getPropName(attributeName: string): string {
107
+ return possibleStandardNames[attributeName];
108
+ }
@@ -0,0 +1,156 @@
1
+ import { cloneElement, createElement, isValidElement } from 'react';
2
+ import type { Element, DOMNode, Text } from 'html-dom-parser';
3
+
4
+ import attributesToProps from './attributes-to-props';
5
+ import type { Props } from './attributes-to-props';
6
+ import {
7
+ PRESERVE_CUSTOM_ATTRIBUTES,
8
+ canTextBeChildOfNode,
9
+ isCustomComponent,
10
+ returnFirstArg,
11
+ setStyleProp,
12
+ } from './utilities';
13
+
14
+ import type { HTMLReactParserOptions } from './types';
15
+
16
+ const React = {
17
+ cloneElement,
18
+ createElement,
19
+ isValidElement,
20
+ } as const;
21
+
22
+ /**
23
+ * Converts DOM nodes to JSX element(s).
24
+ *
25
+ * @param nodes - DOM nodes.
26
+ * @param options - Options.
27
+ * @returns - String or JSX element(s).
28
+ */
29
+ export default function domToReact(
30
+ nodes: DOMNode[],
31
+ options?: HTMLReactParserOptions,
32
+ ): string | JSX.Element | JSX.Element[] {
33
+ const reactElements = [];
34
+
35
+ const hasReplace = typeof options?.replace === 'function';
36
+ const transform = options?.transform || returnFirstArg;
37
+ const { cloneElement, createElement, isValidElement } =
38
+ options?.library || React;
39
+
40
+ let index = 0;
41
+ const nodesLength = nodes.length;
42
+
43
+ for (; index < nodesLength; index++) {
44
+ const node = nodes[index];
45
+
46
+ // replace with custom React element (if present)
47
+ if (hasReplace) {
48
+ let replaceElement = options.replace!(node) as JSX.Element;
49
+
50
+ if (isValidElement(replaceElement)) {
51
+ // set "key" prop for sibling elements
52
+ // https://react.dev/learn/rendering-lists#rules-of-keys
53
+ if (nodesLength > 1) {
54
+ replaceElement = cloneElement(replaceElement, {
55
+ key: replaceElement.key || index,
56
+ });
57
+ }
58
+
59
+ reactElements.push(transform(replaceElement, node, index));
60
+ continue;
61
+ }
62
+ }
63
+
64
+ if (node.type === 'text') {
65
+ const isWhitespace = !node.data.trim().length;
66
+
67
+ // We have a whitespace node that can't be nested in its parent
68
+ // so skip it
69
+ if (
70
+ isWhitespace &&
71
+ node.parent &&
72
+ !canTextBeChildOfNode(node.parent as Element)
73
+ ) {
74
+ continue;
75
+ }
76
+
77
+ // Trim is enabled and we have a whitespace node
78
+ // so skip it
79
+ if (options?.trim && isWhitespace) {
80
+ continue;
81
+ }
82
+
83
+ // We have a text node that's not whitespace and it can be nested
84
+ // in its parent so add it to the results
85
+ reactElements.push(transform(node.data, node, index));
86
+ continue;
87
+ }
88
+
89
+ const element = node as Element;
90
+ let props: Props = {};
91
+
92
+ if (skipAttributesToProps(element)) {
93
+ setStyleProp(element.attribs.style, element.attribs);
94
+ props = element.attribs;
95
+ } else if (element.attribs) {
96
+ props = attributesToProps(element.attribs, element.name);
97
+ }
98
+
99
+ let children = null;
100
+
101
+ switch (node.type) {
102
+ case 'script':
103
+ case 'style':
104
+ // prevent text in <script> or <style> from being escaped
105
+ // https://react.dev/reference/react-dom/components/common#dangerously-setting-the-inner-html
106
+ if (node.children[0]) {
107
+ props.dangerouslySetInnerHTML = {
108
+ __html: (node.children[0] as Text).data,
109
+ };
110
+ }
111
+ break;
112
+
113
+ case 'tag':
114
+ // setting textarea value in children is an antipattern in React
115
+ // https://react.dev/reference/react-dom/components/textarea#caveats
116
+ if (node.name === 'textarea' && node.children[0]) {
117
+ props.defaultValue = (node.children[0] as Text).data;
118
+ } else if (node.children && node.children.length) {
119
+ // continue recursion of creating React elements (if applicable)
120
+ children = domToReact(node.children as Text[], options);
121
+ }
122
+ break;
123
+
124
+ // skip all other cases (e.g., comment)
125
+ default:
126
+ continue;
127
+ }
128
+
129
+ // set "key" prop for sibling elements
130
+ // https://react.dev/learn/rendering-lists#rules-of-keys
131
+ if (nodesLength > 1) {
132
+ props.key = index;
133
+ }
134
+
135
+ reactElements.push(
136
+ transform(createElement(node.name, props, children), node, index),
137
+ );
138
+ }
139
+
140
+ return reactElements.length === 1 ? reactElements[0] : reactElements;
141
+ }
142
+
143
+ /**
144
+ * Determines whether DOM element attributes should be transformed to props.
145
+ * Web Components should not have their attributes transformed except for `style`.
146
+ *
147
+ * @param node - Element node.
148
+ * @returns - Whether the node attributes should be converted to props.
149
+ */
150
+ function skipAttributesToProps(node: Element): boolean {
151
+ return (
152
+ PRESERVE_CUSTOM_ATTRIBUTES &&
153
+ node.type === 'tag' &&
154
+ isCustomComponent(node.name, node.attribs)
155
+ );
156
+ }
package/src/index.ts ADDED
@@ -0,0 +1,37 @@
1
+ import htmlToDOM from 'html-dom-parser';
2
+
3
+ import attributesToProps from './attributes-to-props';
4
+ import domToReact from './dom-to-react';
5
+ import type { HTMLReactParserOptions } from './types';
6
+
7
+ export { Comment, Element, ProcessingInstruction, Text } from 'domhandler';
8
+ export type { DOMNode } from 'html-dom-parser';
9
+
10
+ export { HTMLReactParserOptions, attributesToProps, domToReact, htmlToDOM };
11
+
12
+ const domParserOptions = { lowerCaseAttributeNames: false } as const;
13
+
14
+ /**
15
+ * Converts HTML string to React elements.
16
+ *
17
+ * @param html - HTML string.
18
+ * @param options - Parser options.
19
+ * @returns - React element(s), empty array, or string.
20
+ */
21
+ export default function HTMLReactParser(
22
+ html: string,
23
+ options?: HTMLReactParserOptions,
24
+ ): JSX.Element | JSX.Element[] | string {
25
+ if (typeof html !== 'string') {
26
+ throw new TypeError('First argument must be a string');
27
+ }
28
+
29
+ if (!html) {
30
+ return [];
31
+ }
32
+
33
+ return domToReact(
34
+ htmlToDOM(html, options?.htmlparser2 || domParserOptions),
35
+ options,
36
+ );
37
+ }
package/src/types.ts ADDED
@@ -0,0 +1,29 @@
1
+ import type { DomHandlerOptions } from 'domhandler';
2
+ import type { DOMNode } from 'html-dom-parser';
3
+ import type { ParserOptions } from 'htmlparser2';
4
+ import type { ReactNode } from 'react';
5
+
6
+ export interface HTMLReactParserOptions {
7
+ htmlparser2?: ParserOptions & DomHandlerOptions;
8
+
9
+ library?: {
10
+ cloneElement: (
11
+ element: JSX.Element,
12
+ props?: object,
13
+ ...children: any
14
+ ) => JSX.Element;
15
+ createElement: (type: any, props?: object, ...children: any) => JSX.Element;
16
+ isValidElement: (element: any) => boolean;
17
+ [key: string]: any;
18
+ };
19
+
20
+ replace?: (domNode: DOMNode) => JSX.Element | string | null | boolean | void;
21
+
22
+ transform?: (
23
+ reactNode: ReactNode,
24
+ domNode: DOMNode,
25
+ index: number,
26
+ ) => JSX.Element | string | null | void;
27
+
28
+ trim?: boolean;
29
+ }