Skip to content

feat: add option trim #162

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Jun 7, 2020
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
30 changes: 30 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,32 @@ See [htmlparser2 options](https://github.com/fb55/htmlparser2/wiki/Parser-option

> **Warning**: By overriding htmlparser2 options, there's a chance of breaking universal rendering. Do this at your own risk.

### trim
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


Normally, whitespace is preserved:

```js
parse('<br>\n'); // [React.createElement('br'), '\n']
```

By enabling the `trim` option, whitespace text nodes will be skipped:

```js
parse('<br>\n', { trim: true }); // React.createElement('br')
```

This addresses the warning:

```
Warning: validateDOMNesting(...): Whitespace text nodes cannot appear as a child of <table>. Make sure you don't have any extra whitespace between tags on each line of your source code.
```

However, this option may strip out intentional whitespace:

```js
parse('<p> </p>', { trim: true }); // React.createElement('p')
```

## FAQ

#### Is this library XSS safe?
Expand Down Expand Up @@ -288,6 +314,10 @@ parse('<div /><div />'); // returns single element instead of array of elements

See [#158](https://github.com/remarkablemark/html-react-parser/issues/158).

#### I get "Warning: validateDOMNesting(...): Whitespace text nodes cannot appear as a child of table."

Enable the [trim](https://github.com/remarkablemark/html-react-parser#trim) option. See [#155](https://github.com/remarkablemark/html-react-parser/issues/155).

## Benchmarks

```sh
Expand Down
2 changes: 2 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ export interface HTMLReactParserOptions {
replace?: (
domNode: DomElement
) => JSX.Element | object | void | undefined | null | false;

trim?: boolean;
}

/**
Expand Down
65 changes: 39 additions & 26 deletions lib/dom-to-react.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ function domToReact(nodes, options) {
var replaceElement;
var props;
var children;
var data;
var trim = options.trim;

for (var i = 0, len = nodes.length; i < len; i++) {
node = nodes[i];
Expand All @@ -33,7 +35,7 @@ function domToReact(nodes, options) {
replaceElement = options.replace(node);

if (isValidElement(replaceElement)) {
// specify a "key" prop if element has siblings
// set "key" prop for sibling elements
// https://fb.me/react-warning-keys
if (len > 1) {
replaceElement = cloneElement(replaceElement, {
Expand All @@ -46,45 +48,54 @@ function domToReact(nodes, options) {
}

if (node.type === 'text') {
result.push(node.data);
// if trim option is enabled, skip whitespace text nodes
if (trim) {
data = node.data.trim();
if (data) {
result.push(node.data);
}
} else {
result.push(node.data);
}
continue;
}

props = node.attribs;
if (!shouldPassAttributesUnaltered(node)) {
// update values
props = attributesToProps(node.attribs);
}

children = null;

// node type for <script> is "script"
// node type for <style> is "style"
if (node.type === 'script' || node.type === 'style') {
// prevent text in <script> or <style> from being escaped
// https://facebook.github.io/react/tips/dangerously-set-inner-html.html
if (node.children[0]) {
props.dangerouslySetInnerHTML = {
__html: node.children[0].data
};
}
} else if (node.type === 'tag') {
// setting textarea value in children is an antipattern in React
// https://reactjs.org/docs/forms.html#the-textarea-tag
if (node.name === 'textarea' && node.children[0]) {
props.defaultValue = node.children[0].data;

// continue recursion of creating React elements (if applicable)
} else if (node.children && node.children.length) {
children = domToReact(node.children, options);
}
switch (node.type) {
case 'script':
case 'style':
// prevent text in <script> or <style> from being escaped
// https://reactjs.org/docs/dom-elements.html#dangerouslysetinnerhtml
if (node.children[0]) {
props.dangerouslySetInnerHTML = {
__html: node.children[0].data
};
}
break;

case 'tag':
// setting textarea value in children is an antipattern in React
// https://reactjs.org/docs/forms.html#the-textarea-tag
if (node.name === 'textarea' && node.children[0]) {
props.defaultValue = node.children[0].data;
} else if (node.children && node.children.length) {
// continue recursion of creating React elements (if applicable)
children = domToReact(node.children, options);
}
break;

// skip all other cases (e.g., comment)
} else {
continue;
default:
continue;
}

// specify a "key" prop if element has siblings
// set "key" prop for sibling elements
// https://fb.me/react-warning-keys
if (len > 1) {
props.key = i;
Expand All @@ -97,6 +108,8 @@ function domToReact(nodes, options) {
}

/**
* Determines whether attributes should be altered or not.
*
* @param {React.ReactElement} node
* @return {Boolean}
*/
Expand Down
22 changes: 22 additions & 0 deletions test/html-to-react.js
Original file line number Diff line number Diff line change
Expand Up @@ -152,5 +152,27 @@ describe('HTML to React', () => {
);
});
});

describe('trim', () => {
it('preserves whitespace text nodes when disabled (default)', () => {
const html = `<table>
<tbody>
</tbody>
</table>`;
const reactElement = parse(html);
assert.strictEqual(render(reactElement), html);
});

it('removes whitespace text nodes when enabled', () => {
const html = `<table>
<tbody><tr><td> text </td><td> </td>\t</tr>\r</tbody>\n</table>`;
const options = { trim: true };
const reactElement = parse(html, options);
assert.strictEqual(
render(reactElement),
'<table><tbody><tr><td> text </td><td></td></tr></tbody></table>'
);
});
});
});
});
3 changes: 3 additions & 0 deletions test/types/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,9 @@ parse('<p/><p/>', {
}
});

// $ExpectType Element | Element[]
parse('\t<p>text \r</p>\n', { trim: true });

// $ExpectType DomElement[]
const domNodes = htmlToDOM('<div>text</div>');

Expand Down
3 changes: 3 additions & 0 deletions test/types/lib/dom-to-react.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,6 @@ domToReact(htmlToDOM('<a id="header" href="#">Heading</a>'), {
}
}
});

// $ExpectType Element | Element[]
domToReact(htmlToDOM('\t<p>text \r</p>\n'), { trim: true });