Skip to content

Commit

Permalink
Merge pull request #133 from Spy-Seth/feature/escape-jsx-incompatible…
Browse files Browse the repository at this point in the history
…-string

Fix string formatting edge case
  • Loading branch information
armandabric authored Aug 3, 2017
2 parents 35d76e4 + 6e0eea3 commit f9d0cf1
Show file tree
Hide file tree
Showing 5 changed files with 177 additions and 38 deletions.
41 changes: 38 additions & 3 deletions src/formatter/formatReactElementNode.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,43 @@ import propNameSorter from './propNameSorter';
import type { Options } from './../options';
import type { ReactElementTreeNode } from './../tree';

const recurse = (lvl: number, inline: boolean, options: Options) => element =>
formatTreeNode(element, inline, lvl, options);
const compensateMultilineStringElementIndentation = (
element,
formattedElement: string,
inline: boolean,
lvl: number,
options: Options
) => {
const { tabStop } = options;

if (element.type === 'string') {
return formattedElement
.split('\n')
.map((line, offset) => {
if (offset === 0) {
return line;
}

return `${spacer(lvl, tabStop)}${line}`;
})
.join('\n');
}

return formattedElement;
};

const formatOneChildren = (
inline: boolean,
lvl: number,
options: Options
) => element =>
compensateMultilineStringElementIndentation(
element,
formatTreeNode(element, inline, lvl, options),
inline,
lvl,
options
);

const onlyPropsWithOriginalValue = (defaultProps, props) => propName => {
const haveDefaultValue = Object.keys(defaultProps).includes(propName);
Expand Down Expand Up @@ -158,7 +193,7 @@ export default (

out += childrens
.reduce(mergeSiblingPlainStringChildrenReducer, [])
.map(recurse(newLvl, inline, options))
.map(formatOneChildren(inline, newLvl, options))
.join(`\n${spacer(newLvl, tabStop)}`);

if (!inline) {
Expand Down
31 changes: 31 additions & 0 deletions src/formatter/formatReactElementNode.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -94,4 +94,35 @@ describe('formatReactElementNode', () => {
'<div a={<span b="42" />} />'
);
});

it('should format a react element with multiline children', () => {
const tree = {
type: 'ReactElement',
displayName: 'div',
defaultProps: {},
props: {},
childrens: [
{
type: 'string',
value: 'first line\nsecond line\nthird line',
},
],
};

expect(formatReactElementNode(tree, false, 0, defaultOptions)).toEqual(
`<div>
first line
second line
third line
</div>`
);

expect(formatReactElementNode(tree, false, 2, defaultOptions)).toEqual(
`<div>
first line
second line
third line
</div>`
);
});
});
21 changes: 17 additions & 4 deletions src/formatter/formatTreeNode.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,30 @@ import formatReactElementNode from './formatReactElementNode';
import type { Options } from './../options';
import type { TreeNode } from './../tree';

const escape = (s: string): string =>
s.replace(/{/g, '&lbrace;').replace(/}/g, '&rbrace;');
const jsxStopChars = ['<', '>', '{', '}'];
const shouldBeEscaped = (s: string) =>
jsxStopChars.some(jsxStopChar => s.includes(jsxStopChar));

const escape = (s: string) => {
if (!shouldBeEscaped(s)) {
return s;
}

return `{\`${s}\`}`;
};

export default (
node: TreeNode,
inline: boolean,
lvl: number,
options: Options
): string => {
if (node.type === 'string' || node.type === 'number') {
return node.value ? escape(node.value.toString()) : '';
if (node.type === 'number') {
return String(node.value);
}

if (node.type === 'string') {
return node.value ? escape(String(node.value)) : '';
}

if (node.type === 'ReactElement') {
Expand Down
66 changes: 63 additions & 3 deletions src/formatter/formatTreeNode.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,70 @@

import formatTreeNode from './formatTreeNode';

jest.mock('./formatReactElementNode', () => () =>
'<MockedFormatReactElementNodeResult />'
);

describe('formatTreeNode', () => {
it('should escape JSX entity on string node', () => {
it('should format number tree node', () => {
expect(formatTreeNode({ type: 'number', value: 42 }, true, 0, {})).toBe(
'42'
);
});

it('should format string tree node', () => {
expect(formatTreeNode({ type: 'string', value: 'foo' }, true, 0, {})).toBe(
'foo'
);
});

it('should format react element tree node', () => {
expect(
formatTreeNode(
{
type: 'ReactElement',
displayName: 'Foo',
},
true,
0,
{}
)
).toBe('<MockedFormatReactElementNodeResult />');
});

const jsxDelimiters = ['<', '>', '{', '}'];
jsxDelimiters.forEach(char => {
it(`should escape string that contains the JSX delimiter "${char}"`, () => {
expect(
formatTreeNode(
{ type: 'string', value: `I contain ${char}, is will be escaped` },
true,
0,
{}
)
).toBe(`{\`I contain ${char}, is will be escaped\`}`);
});
});

it('should preserve the format of string', () => {
expect(formatTreeNode({ type: 'string', value: 'foo\nbar' }, true, 0, {}))
.toBe(`foo
bar`);

expect(
formatTreeNode({ type: 'string', value: '{ foo: "bar" }' }, true, 0, {})
).toBe('&lbrace; foo: "bar" &rbrace;');
formatTreeNode(
{
type: 'string',
value: JSON.stringify({ foo: 'bar' }, null, 2),
},
false,
0,
{
tabStop: 2,
}
)
).toBe(`{\`{
"foo": "bar"
}\`}`);
});
});
56 changes: 28 additions & 28 deletions src/index.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -206,16 +206,16 @@ describe('reactElementToJSXString(ReactElement)', () => {
);
});

it('reactElementToJSXString(<script type="application/json+ld">&lbrace; hello: \'world\' &rbrace;</script>)', () => {
it('reactElementToJSXString(<script type="application/json+ld">{`{ hello: \'world\' }`}</script>)', () => {
expect(
reactElementToJSXString(
<script type="application/json+ld">
&lbrace; hello: 'world' &rbrace;
{`{ hello: 'world' }`}
</script>
)
).toEqual(
`<script type="application/json+ld">
&lbrace; hello: 'world' &rbrace;
{\`{ hello: 'world' }\`}
</script>`
);
});
Expand All @@ -227,31 +227,7 @@ describe('reactElementToJSXString(ReactElement)', () => {
)
).toEqual(
`<script type="application/json+ld">
&lbrace; hello: 'world' &rbrace;
</script>`
);
});

it('reactElementToJSXString(<script type="application/json+ld">\\u007B hello: \'world\' \\u007D</script>)', () => {
expect(
reactElementToJSXString(
<script type="application/json+ld">\u007B hello: 'world' \u007D</script>
)
).toEqual(
`<script type="application/json+ld">
\\u007B hello: 'world' \\u007D
</script>`
);
});

it('reactElementToJSXString(<script type="application/json+ld">{ hello: \'world\' }</script>)', () => {
expect(
reactElementToJSXString(
<script type="application/json+ld">{`{ hello: 'world' }`}</script>
)
).toEqual(
`<script type="application/json+ld">
&lbrace; hello: 'world' &rbrace;
{\`{ hello: 'world' }\`}
</script>`
);
});
Expand Down Expand Up @@ -347,6 +323,30 @@ describe('reactElementToJSXString(ReactElement)', () => {
);
});

it('reactElementToJSXString(<div>{`foo\nbar`}</div>)', () => {
expect(reactElementToJSXString(<div>{`foo\nbar`}</div>)).toEqual(
`<div>
foo
bar
</div>`
);

expect(
reactElementToJSXString(
<div>
<div>{`foo\nbar`}</div>
</div>
)
).toEqual(
`<div>
<div>
foo
bar
</div>
</div>`
);
});

it('reactElementToJSXString(<div>Hello</div>, {tabStop: 4})', () => {
expect(reactElementToJSXString(<div>Hello</div>, { tabStop: 4 })).toEqual(
`<div>
Expand Down

0 comments on commit f9d0cf1

Please sign in to comment.