diff --git a/bun.lockb b/bun.lockb index 3a6aadb..cb4c543 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index 7e198ac..ccc4224 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "@types/bun": "^1.1.6", "@typescript-eslint/eslint-plugin": "^7.15.0", "@typescript-eslint/parser": "^7.15.0", - "basis": "TroyAlford/basis", + "basis": "github:TroyAlford/basis#v1.1.0", "concurrently": "^8.2.2", "cross-env": "^7.0.3", "eslint": "^8.57.0", @@ -55,7 +55,7 @@ "repository": "TroyAlford/react-jsx-parser", "scripts": { "build": "bun build:types && bun build:code", - "build:code": "bun build --target=browser --outfile=./dist/react-jsx-parser.min.js ./source/index.ts", + "build:code": "bun build --target=browser --outfile=./dist/react-jsx-parser.min.js ./source/index.ts --external react --external react-dom", "build:types": "bun run tsc -p ./tsconfig.json -d --emitDeclarationOnly", "develop": "NODE_ENV=production concurrently -n build,ts,demo -c green,cyan,yellow \"bun build:code --watch\" \"bun build:types --watch\" \"bun serve\"", "lint": "bun eslint --ext .js,.ts,.tsx source/", diff --git a/source/components/JsxParser.tsx b/source/components/JsxParser.tsx index 2344986..57d2e15 100644 --- a/source/components/JsxParser.tsx +++ b/source/components/JsxParser.tsx @@ -1,6 +1,6 @@ import * as Acorn from 'acorn' import * as AcornJSX from 'acorn-jsx' -import React, { ComponentType, ExoticComponent, Fragment } from 'react' +import React, { Fragment } from 'react' import ATTRIBUTES from '../constants/attributeNames' import { canHaveChildren, canHaveWhitespace } from '../constants/specialTags' import { NullishShortCircuit } from '../errors/NullishShortCircuit' @@ -14,24 +14,74 @@ function handleNaN(child: T): T | 'NaN' { type ParsedJSX = React.ReactNode | boolean | string type ParsedTree = ParsedJSX | ParsedJSX[] | null + +/** + * Props for the JsxParser component + */ export type TProps = { + /** Whether to allow rendering of unrecognized HTML elements. Defaults to true. */ allowUnknownElements?: boolean, + + /** + * Whether to auto-close void elements like ,
,
etc. in HTML style. + * Defaults to false. + */ autoCloseVoidElements?: boolean, + + /** Object containing values that can be referenced in the JSX string */ bindings?: { [key: string]: unknown; }, + + /** + * Array of attribute names or RegExp patterns to blacklist. + * By default removes 'on*' attributes + */ blacklistedAttrs?: Array, + + /** + * Array of HTML tag names to blacklist. + * By default removes 'script' tags + */ blacklistedTags?: string[], + + /** CSS class name(s) to add to the wrapper div */ className?: string, - components?: Record, + + /** Map of component names to their React component definitions */ + components?: Record< + string, + | React.ComponentType // allows for class components + | React.ExoticComponent // allows for forwardRef + | (() => React.ReactNode) // allows for function components + >, + + /** If true, only renders custom components defined in the components prop */ componentsOnly?: boolean, + + /** If true, disables usage of React.Fragment. May affect whitespace handling */ disableFragments?: boolean, + + /** If true, disables automatic generation of key props */ disableKeyGeneration?: boolean, + + /** The JSX string to parse and render */ jsx?: string, + + /** Callback function when parsing/rendering errors occur */ onError?: (error: Error) => void, + + /** If true, shows parsing/rendering warnings in console */ showWarnings?: boolean, + + /** Custom error renderer function */ renderError?: (props: { error: string }) => React.ReactNode | null, + + /** Whether to wrap output in a div. If false, renders children directly */ renderInWrapper?: boolean, + + /** Custom renderer for unrecognized elements */ renderUnrecognized?: (tagName: string) => React.ReactNode | null, } + type Scope = Record export default class JsxParser extends React.Component { @@ -55,8 +105,14 @@ export default class JsxParser extends React.Component { renderUnrecognized: () => null, } + /** Stores the parsed React elements */ private ParsedChildren: ParsedTree = null + /** + * Parses a JSX string into React elements + * @param jsx - The JSX string to parse + * @returns The parsed React node(s) or null if parsing fails + */ #parseJSX = (jsx: string): React.ReactNode | React.ReactNode[] | null => { const parser = Acorn.Parser.extend(AcornJSX.default({ autoCloseVoidElements: this.props.autoCloseVoidElements, @@ -80,6 +136,12 @@ export default class JsxParser extends React.Component { return parsed.map(p => this.#parseExpression(p)).filter(Boolean) } + /** + * Parses a single JSX expression into its corresponding value/element + * @param expression - The JSX expression to parse + * @param scope - Optional scope for variable resolution + * @returns The parsed value/element + */ #parseExpression = (expression: AcornJSX.Expression, scope?: Scope): any => { switch (expression.type) { case 'JSXAttribute': @@ -204,6 +266,12 @@ export default class JsxParser extends React.Component { } } + /** + * Parses a chain expression (optional chaining) + * @param expression - The chain expression to parse + * @param scope - Optional scope for variable resolution + * @returns The parsed value + */ #parseChainExpression = (expression: AcornJSX.ChainExpression, scope?: Scope): any => { try { return this.#parseExpression(expression.expression, scope) @@ -213,6 +281,12 @@ export default class JsxParser extends React.Component { } } + /** + * Parses a member expression (e.g., obj.prop or obj['prop']) + * @param expression - The member expression to parse + * @param scope - Optional scope for variable resolution + * @returns The resolved member value + */ #parseMemberExpression = (expression: AcornJSX.MemberExpression, scope?: Scope): any => { const object = this.#parseExpression(expression.object, scope) @@ -242,11 +316,22 @@ export default class JsxParser extends React.Component { return member } + /** + * Parses a JSX element name (simple or member expression) + * @param element - The JSX identifier or member expression + * @returns The parsed element name as a string + */ #parseName = (element: AcornJSX.JSXIdentifier | AcornJSX.JSXMemberExpression): string => { if (element.type === 'JSXIdentifier') { return element.name } return `${this.#parseName(element.object)}.${this.#parseName(element.property)}` } + /** + * Parses a JSX element into React elements + * @param element - The JSX element to parse + * @param scope - Optional scope for variable resolution + * @returns The parsed React node(s) or null + */ #parseElement = ( element: AcornJSX.JSXElement | AcornJSX.JSXFragment, scope?: Scope, @@ -357,6 +442,10 @@ export default class JsxParser extends React.Component { return React.createElement(component || lowerName, props, children) } + /** + * Renders the parsed JSX content + * @returns The rendered React elements wrapped in a div (if renderInWrapper is true) + */ render() { const jsx = (this.props.jsx || '').trim().replace(/]*)>/g, '') this.ParsedChildren = this.#parseJSX(jsx) diff --git a/yarn.lock b/yarn.lock index e4a57e1..05d1074 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1,6 +1,6 @@ # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. # yarn lockfile v1 -# bun ./bun.lockb --hash: 033B634DA0583800-43756a934708d338-AB2E8226CA6E6215-f5a9da1d383ecd4e +# bun ./bun.lockb --hash: 1614930542A7AB39-1847a6ab6c9799dc-70F392D8ECD54409-1a4a46a39b28aca1 "@babel/runtime@^7.21.0": @@ -31,6 +31,11 @@ debug "^4.3.1" minimatch "^3.1.2" +"@eslint/core@^0.7.0": + version "0.7.0" + resolved "https://registry.npmjs.org/@eslint/core/-/core-0.7.0.tgz" + integrity sha512-xp5Jirz5DyPYlPiKat8jaq0EmYvDXKKpzTbxXMpT9eqlRJkRKIz9AGMdlvYjih+im+QlhWrpvVjl8IPC/lHlUw== + "@eslint/eslintrc@^2.1.4": version "2.1.4" resolved "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz" @@ -71,6 +76,11 @@ resolved "https://registry.npmjs.org/@eslint/js/-/js-9.10.0.tgz" integrity sha512-fuXtbiP5GWIn8Fz+LWoOMVf/Jxm+aajZYkhi6CuEm4SxymFM+eUWzbO9qXT+L0iCkL5+KGYMCSGxo686H19S1g== +"@eslint/js@9.13.0": + version "9.13.0" + resolved "https://registry.npmjs.org/@eslint/js/-/js-9.13.0.tgz" + integrity sha512-IFLyoY4d72Z5y/6o/BazFBezupzI/taV8sGumxTAVw3lXG9A6md1Dc34T9s1FoD/an9pJH8RHbAxsaEbBed9lA== + "@eslint/object-schema@^2.1.4": version "2.1.4" resolved "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.4.tgz" @@ -83,6 +93,13 @@ dependencies: levn "^0.4.1" +"@eslint/plugin-kit@^0.2.0": + version "0.2.2" + resolved "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.2.tgz" + integrity sha512-CXtq5nR4Su+2I47WPOlWud98Y5Lv8Kyxp2ukhgFx/eW6Blm18VXJO5WuQylPugRo8nbluoi6GvvxBLqHcvqUUw== + dependencies: + levn "^0.4.1" + "@happy-dom/global-registrator@^14.12.3": version "14.12.3" resolved "https://registry.npmjs.org/@happy-dom/global-registrator/-/global-registrator-14.12.3.tgz" @@ -90,6 +107,19 @@ dependencies: happy-dom "^14.12.3" +"@humanfs/core@^0.19.1": + version "0.19.1" + resolved "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz" + integrity sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA== + +"@humanfs/node@^0.16.5": + version "0.16.6" + resolved "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz" + integrity sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw== + dependencies: + "@humanwhocodes/retry" "^0.3.0" + "@humanfs/core" "^0.19.1" + "@humanwhocodes/config-array@^0.11.14": version "0.11.14" resolved "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz" @@ -114,6 +144,11 @@ resolved "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.0.tgz" integrity sha512-d2CGZR2o7fS6sWB7DG/3a95bGKQyHMACZ5aW8qGkkqQpUoZV6C0X7Pc7l4ZNMZkfNBf4VWNe9E1jRsf0G146Ew== +"@humanwhocodes/retry@^0.3.0", "@humanwhocodes/retry@^0.3.1": + version "0.3.1" + resolved "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz" + integrity sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA== + "@nodelib/fs.scandir@2.1.5": version "2.1.5" resolved "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz" @@ -159,6 +194,16 @@ resolved "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz" integrity sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw== +"@types/estree@^1.0.6": + version "1.0.6" + resolved "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz" + integrity sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw== + +"@types/json-schema@^7.0.15": + version "7.0.15" + resolved "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz" + integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== + "@types/json5@^0.0.29": version "0.0.29" resolved "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz" @@ -289,6 +334,11 @@ resolved "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz" integrity sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg== +acorn@^8.14.0: + version "8.14.0" + resolved "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz" + integrity sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA== + acorn-jsx@^5.3.2: version "5.3.2" resolved "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz" @@ -449,11 +499,11 @@ balanced-match@^1.0.0: resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz" integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== -"basis@TroyAlford/basis": - version "github:TroyAlford/basis#5c364bd" - resolved "github:TroyAlford/basis#5c364bd" +"basis@github:TroyAlford/basis#v1.1.0": + version "github:TroyAlford/basis#b682bef" + resolved "github:TroyAlford/basis#b682bef" dependencies: - eslint "^9.9.0" + eslint "^9.12.0" brace-expansion@^1.1.7: version "1.1.11" @@ -897,7 +947,7 @@ escape-string-regexp@^4.0.0: "@humanwhocodes/module-importer" "^1.0.1" json-stable-stringify-without-jsonify "^1.0.1" -"eslint@^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7", eslint@^9.9.0: +"eslint@^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7": version "9.10.0" resolved "https://registry.npmjs.org/eslint/-/eslint-9.10.0.tgz" integrity sha512-Y4D0IgtBZfOcOUAIQTSXBKoNGfY0REGqHJG6+Q81vNippW5YlKjHFj4soMxamKK1NXHUWuBZTLdU3Km+L/pcHw== @@ -937,6 +987,47 @@ escape-string-regexp@^4.0.0: strip-ansi "^6.0.1" text-table "^0.2.0" +eslint@^9.12.0: + version "9.13.0" + resolved "https://registry.npmjs.org/eslint/-/eslint-9.13.0.tgz" + integrity sha512-EYZK6SX6zjFHST/HRytOdA/zE72Cq/bfw45LSyuwrdvcclb/gqV8RRQxywOBEWO2+WDpva6UZa4CcDeJKzUCFA== + dependencies: + "@eslint-community/eslint-utils" "^4.2.0" + "@eslint-community/regexpp" "^4.11.0" + "@eslint/config-array" "^0.18.0" + "@eslint/core" "^0.7.0" + "@eslint/eslintrc" "^3.1.0" + "@eslint/js" "9.13.0" + "@eslint/plugin-kit" "^0.2.0" + "@humanfs/node" "^0.16.5" + "@humanwhocodes/module-importer" "^1.0.1" + "@humanwhocodes/retry" "^0.3.1" + "@types/estree" "^1.0.6" + "@types/json-schema" "^7.0.15" + ajv "^6.12.4" + chalk "^4.0.0" + cross-spawn "^7.0.2" + debug "^4.3.2" + escape-string-regexp "^4.0.0" + eslint-scope "^8.1.0" + eslint-visitor-keys "^4.1.0" + espree "^10.2.0" + esquery "^1.5.0" + esutils "^2.0.2" + fast-deep-equal "^3.1.3" + file-entry-cache "^8.0.0" + find-up "^5.0.0" + glob-parent "^6.0.2" + ignore "^5.2.0" + imurmurhash "^0.1.4" + is-glob "^4.0.0" + json-stable-stringify-without-jsonify "^1.0.1" + lodash.merge "^4.6.2" + minimatch "^3.1.2" + natural-compare "^1.4.0" + optionator "^0.9.3" + text-table "^0.2.0" + eslint-config-airbnb@^19.0.4: version "19.0.4" resolved "https://registry.npmjs.org/eslint-config-airbnb/-/eslint-config-airbnb-19.0.4.tgz" @@ -1063,6 +1154,14 @@ eslint-scope@^8.0.2: esrecurse "^4.3.0" estraverse "^5.2.0" +eslint-scope@^8.1.0: + version "8.2.0" + resolved "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.2.0.tgz" + integrity sha512-PHlWUfG6lvPc3yvP5A4PNyBL1W8fkDUccmI21JUu/+GKZBoH/W5u6usENXUrWFRsyoW5ACUjFGgAFQp5gUlb/A== + dependencies: + esrecurse "^4.3.0" + estraverse "^5.2.0" + eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.1, eslint-visitor-keys@^3.4.3: version "3.4.3" resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz" @@ -1073,6 +1172,11 @@ eslint-visitor-keys@^4.0.0: resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz" integrity sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw== +eslint-visitor-keys@^4.1.0, eslint-visitor-keys@^4.2.0: + version "4.2.0" + resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz" + integrity sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw== + espree@^9.6.0, espree@^9.6.1: version "9.6.1" resolved "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz" @@ -1091,6 +1195,15 @@ espree@^10.0.1, espree@^10.1.0: acorn-jsx "^5.3.2" eslint-visitor-keys "^4.0.0" +espree@^10.2.0: + version "10.3.0" + resolved "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz" + integrity sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg== + dependencies: + acorn "^8.14.0" + acorn-jsx "^5.3.2" + eslint-visitor-keys "^4.2.0" + esquery@^1.4.2, esquery@^1.5.0: version "1.6.0" resolved "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz"