Skip to content
Open
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
47 changes: 46 additions & 1 deletion packages/parser-jsx/src/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,41 @@ const allowsTextChildren = (node: JSXElement): boolean => {
!HTML_ELEMENTS_WITH_ONLY_NON_TEXT_CHILDREN.includes(node.openingElement.name.name);
};

/**
* Check if the node contains any JSX elements in its descendants.
* This is used to determine if an expression will render JSX content
* rather than plain text.
*/
const containsJSXElement = (node: Node): boolean => {
if (!node || typeof node !== 'object') {
return false;
}

if (node.type === 'JSXElement') {
return true;
}

for (const key of Object.keys(node)) {
if (key === 'loc' || key === 'range' || key === 'parent') {
continue; // Skip location info and parent references to avoid infinite loops
}

const value = (node as any)[key];

if (Array.isArray(value)) {
for (const item of value) {
if (item && typeof item === 'object' && containsJSXElement(item as Node)) {
return true;
}
}
} else if (value && typeof value === 'object' && containsJSXElement(value as Node)) {
return true;
}
}

return false;
};

/**
* Generate an HTML document representing a fragment containing the
* provided roots derived from the specified resource.
Expand Down Expand Up @@ -309,9 +344,19 @@ export default class JSXParser extends Parser<HTMLEvents> {
}
},
JSXExpressionContainer(node, /* istanbul ignore next */ ancestors = []) {
const data = mapExpression(node);
const parent = getParentAttributeOrElement(ancestors);

/*
* Skip if this expression contains JSX elements, since those elements
* will be processed separately and adding a text placeholder would be misleading.
* See: https://github.com/webhintio/hint/issues/4624
*/
if (containsJSXElement(node.expression)) {
return;
}

const data = mapExpression(node);

if (parent && parent.type !== 'JSXAttribute' && allowsTextChildren(parent)) {
addChild(data, parent, childMap);
}
Expand Down
39 changes: 39 additions & 0 deletions packages/parser-jsx/tests/tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -198,3 +198,42 @@ test('It translates JSX attributes to HTML attributes', async (t) => {
t.is(label.getAttribute('class'), 'foo');
t.is(label.getAttribute('for'), 'bar');
});

/*
* Issue #4624: Doesn't recognize div inside function
* https://github.com/webhintio/hint/issues/4624
*/
test('It omits expression placeholder when expression contains JSX (dl with map)', async (t) => {
const { document } = await parseJSX(`const jsx = <dl>{data?.nodes?.map((item) => (<div><dt>{item.name}</dt><dd>{item.value}</dd></div>))}</dl>;`);
const dl = document.querySelectorAll('dl')[0];

/* The outer expression should not produce a text node, only the inner JSX elements. */
t.is(dl.innerHTML, '<div><dt>{expression}</dt><dd>{expression}</dd></div>');
});

test('It omits expression placeholder when expression contains JSX (conditional)', async (t) => {
const { document } = await parseJSX(`const jsx = <dl>{condition ? <div><dt>Term</dt></div> : null}</dl>;`);
const dl = document.querySelectorAll('dl')[0];

/* The conditional expression should not produce a text node. */
t.is(dl.innerHTML, '<div><dt>Term</dt></div>');
});

test('It omits expression placeholder in div when expression contains JSX', async (t) => {
const { document } = await parseJSX(`const jsx = <div>{items.map(item => <span>{item}</span>)}</div>;`);
const div = document.querySelectorAll('div')[0];

/*
* Even in elements that allow text children, expressions containing JSX
* should not produce a text placeholder (only the JSX elements).
*/
t.is(div.innerHTML, '<span>{expression}</span>');
});

test('It keeps expression placeholder when expression does not contain JSX', async (t) => {
const { document } = await parseJSX(`const jsx = <div>{getText()}</div>;`);
const div = document.querySelectorAll('div')[0];

/* Simple function calls that don't contain JSX should still get the placeholder. */
t.is(div.innerHTML, '{expression}');
});
Loading