Skip to content

Commit

Permalink
Merge pull request #162 from remarkablemark/feat/option-trim
Browse files Browse the repository at this point in the history
feat: add option `trim`
  • Loading branch information
remarkablemark committed Jun 7, 2020
2 parents 4ef89af + 21190a3 commit 8a6589f
Show file tree
Hide file tree
Showing 6 changed files with 99 additions and 26 deletions.
30 changes: 30 additions & 0 deletions README.md
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

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
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
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
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
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
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 });

0 comments on commit 8a6589f

Please sign in to comment.