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.
- package/README.md +79 -55
- package/dist/html-react-parser.js +491 -514
- package/dist/html-react-parser.js.map +1 -1
- package/dist/html-react-parser.min.js +1 -1
- package/dist/html-react-parser.min.js.map +1 -1
- package/esm/attributes-to-props.mjs +3 -0
- package/esm/dom-to-react.mjs +3 -0
- package/esm/index.mjs +13 -0
- package/esm/utilities.mjs +8 -0
- package/lib/attributes-to-props.d.ts +8 -11
- package/lib/attributes-to-props.d.ts.map +1 -0
- package/lib/attributes-to-props.js +58 -75
- package/lib/attributes-to-props.js.map +1 -0
- package/lib/dom-to-react.d.ts +7 -13
- package/lib/dom-to-react.d.ts.map +1 -0
- package/lib/dom-to-react.js +104 -123
- package/lib/dom-to-react.js.map +1 -0
- package/lib/index.d.ts +17 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +36 -0
- package/lib/index.js.map +1 -0
- package/lib/types.d.ts +17 -0
- package/lib/types.d.ts.map +1 -0
- package/lib/types.js +3 -0
- package/lib/types.js.map +1 -0
- package/lib/utilities.d.ts +42 -0
- package/lib/utilities.d.ts.map +1 -0
- package/lib/utilities.js +77 -79
- package/lib/utilities.js.map +1 -0
- package/package.json +27 -21
- package/src/attributes-to-props.ts +108 -0
- package/src/dom-to-react.ts +156 -0
- package/src/index.ts +37 -0
- package/src/types.ts +29 -0
- package/src/utilities.ts +108 -0
- package/index.d.ts +0 -60
- package/index.js +0 -50
- package/index.mjs +0 -13
|
@@ -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
|
-
|
|
2
|
-
var
|
|
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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
|
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
|
|
21
|
-
* @param
|
|
22
|
-
* @returns
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
+
exports.isCustomComponent = isCustomComponent;
|
|
42
|
+
var styleOptions = {
|
|
43
|
+
reactCompat: true,
|
|
44
|
+
};
|
|
41
45
|
/**
|
|
42
46
|
* Sets style prop.
|
|
43
47
|
*
|
|
44
|
-
* @param
|
|
45
|
-
* @param
|
|
48
|
+
* @param style - Inline style.
|
|
49
|
+
* @param props - Props object.
|
|
46
50
|
*/
|
|
47
51
|
function setStyleProp(style, props) {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
* @
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
|
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
|
|
85
|
-
|
|
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
|
|
92
|
-
* @returns
|
|
98
|
+
* @param arg - The argument to be returned.
|
|
99
|
+
* @returns - The input argument `arg`.
|
|
93
100
|
*/
|
|
94
|
-
function
|
|
95
|
-
|
|
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": "
|
|
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": "
|
|
23
|
-
"
|
|
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
|
|
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 --
|
|
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
|
-
"
|
|
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
|
+
}
|