Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .tool-versions
Original file line number Diff line number Diff line change
@@ -1 +1 @@
bun 1.2.1
bun 1.2.22
58 changes: 39 additions & 19 deletions source/components/JsxParser.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -116,30 +116,37 @@ describe('JsxParser Component', () => {

describe('unary operations', () => {
const testCases = [
['+60', 60],
['-60', -60],
['!true', false],
['!false', true],
['!""', true],
['!0', true],
['!1', false],
['!false', true],
['!NaN', true],
['!null', true],
['!undefined', true],
['!NaN', true],
['!""', true],
['!{}', false],
['![]', false],
['+true', 1],
['+false', 0],
['+null', 0],
['+undefined', NaN],
['+""', 0],
['+"123"', 123],
['+"-123"', -123],
['+"123"', 123],
['+60', 60],
['+true', 1],
['~1', -2],
['~2', -3],
['typeof "abc"', 'string'],
['typeof 123', 'number'],

// react doesn't render `false`
['![]', undefined],
['!{}', undefined],
['!1', undefined],
['!true', undefined],
['+""', undefined],
['+false', undefined],
['+null', undefined],
['+undefined', undefined],
]

test.each(testCases)(
'should evaluate unary %s correctly',
({ operation, expected }) => {
(operation, expected) => {
console.log('operation', operation)
const { instance } = render(<JsxParser jsx={`{${operation}}`} />)
if (Number.isNaN(expected)) {
expect(Number.isNaN(instance.ParsedChildren[0])).toBe(true)
Expand Down Expand Up @@ -1038,6 +1045,19 @@ describe('JsxParser Component', () => {
expect(() => render(<JsxParser jsx='{ document.querySelector("body") }' onError={e => { throw e }} />)).toThrow()
expect(() => render(<JsxParser jsx='{ document.createElement("script") }' onError={e => { throw e }} />)).toThrow()
})

test('renderError catches errors', () => {
const renderError = jest.fn((...args) => console.error(...args))
render(
<JsxParser
bindings={{ badFn() { throw new Error('Test error') } }}
jsx="<div>{badFn()}</div>"
renderError={renderError}
/>,
)
expect(renderError).toHaveBeenCalledWith({ error: 'Error: Test error' })
})

test('supports className prop', () => {
const { node } = render(<JsxParser className="foo" jsx="Text" />)
expect(node.classList.contains('foo')).toBeTruthy()
Expand Down Expand Up @@ -1192,13 +1212,13 @@ describe('JsxParser Component', () => {
jsx={`
<Outer id="outer-id" name="Outer Name">
{outer => <>
<h1>Outer ({outer.id}): {outer.name}</h1>
<Inner id="inner-id" name={outer.name}>
{inner => <>
<h1>Outer ({outer.id}): {outer.name}</h1>
<Inner id="inner-id" name={outer.name}>
{inner => <>
<h2>Inner ({inner.id}): {inner.name}</h2>
<div>{outer.id} &gt; {outer.name}</div>
</>}
</Inner>
</Inner>
</>}
</Outer>
`}
Expand Down
24 changes: 13 additions & 11 deletions source/components/JsxParser.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,14 @@ function handleNaN<T>(child: T): T | 'NaN' {
type ParsedJSX = React.ReactNode | boolean | string
type ParsedTree = ParsedJSX | ParsedJSX[] | null

type ComponentType =
| React.ComponentType // allows for class components
| React.ExoticComponent // allows for forwardRef
| (() => React.ReactNode) // allows for function components
type ComponentsType =
| ComponentType
| Record<string, ComponentType>

/**
* Props for the JsxParser component
*/
Expand Down Expand Up @@ -47,12 +55,7 @@ export type TProps = {
className?: string,

/** 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
>,
components?: Record<string, ComponentsType>,

/** If true, only renders custom components defined in the components prop */
componentsOnly?: boolean,
Expand Down Expand Up @@ -124,16 +127,13 @@ export default class JsxParser extends React.Component<TProps> {
parsed = parser.parse(wrappedJsx, { ecmaVersion: 'latest' })
// @ts-ignore - AcornJsx doesn't have typescript typings
parsed = parsed.body[0].expression.children || []
return parsed.map(p => this.#parseExpression(p)).filter(Boolean)
} catch (error) {
if (this.props.showWarnings) console.warn(error) // eslint-disable-line no-console
if (this.props.onError) this.props.onError(error as Error)
if (this.props.renderError) {
return this.props.renderError({ error: String(error) })
}
if (this.props.renderError) return this.props.renderError({ error: String(error) })
return null
}

return parsed.map(p => this.#parseExpression(p)).filter(Boolean)
}

/**
Expand Down Expand Up @@ -247,6 +247,8 @@ export default class JsxParser extends React.Component<TProps> {
case '+': return +unaryValue
case '-': return -unaryValue
case '!': return !unaryValue
case '~': return ~unaryValue // eslint-disable-line no-bitwise
case 'typeof': return typeof unaryValue
}
return undefined
case 'ArrowFunctionExpression':
Expand Down