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
20 changes: 20 additions & 0 deletions src/formatter/formatComplexDataStructure.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,19 @@ import formatFunction from './formatFunction';
import spacer from './spacer';
import type { Options } from './../options';

const escapeStringForSingleQuotedLiteral = (s: string): string =>
// Avoid regex literals with control characters to keep ESLint happy.
s
.replace(/\\/g, '\\\\') // Keep backslashes literal (avoid \1-style escapes)
.replace(/'/g, "\\'")
.replace(/\r/g, '\\r')
.replace(/\n/g, '\\n')
.replace(/\t/g, '\\t')
.split(String.fromCharCode(8))
.join('\\b')
.split(String.fromCharCode(12))
.join('\\f');

export default (
value: Object | Array<any>,
inline: boolean,
Expand All @@ -30,6 +43,13 @@ export default (
);
}

if (typeof currentValue === 'string') {
// pretty-print-object can output strings without escaping backslashes
// enough for JS string literal correctness, e.g. "\1" vs "\\1".
// We always output a valid single-quoted JS literal here.
return `'${escapeStringForSingleQuotedLiteral(currentValue)}'`;
}

if (typeof currentValue === 'function') {
return formatFunction(currentValue, options);
}
Expand Down
30 changes: 25 additions & 5 deletions src/formatter/formatProp.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,12 @@

import spacer from './spacer';
import formatPropValue from './formatPropValue';
import formatComplexDataStructure from './formatComplexDataStructure';
import type { Options } from './../options';

const isValidJSXPropName = (propName: string): boolean =>
/^[$A-Z_a-z][$\w-]*$/.test(propName);

export default (
name: string,
hasValue: boolean,
Expand All @@ -27,12 +31,14 @@ export default (
const usedValue = hasValue ? value : defaultValue;

const { useBooleanShorthandSyntax, tabStop } = options;

const formattedPropValue = formatPropValue(usedValue, inline, lvl, options);
const hasValidJSXPropName = isValidJSXPropName(name);
const formattedPropValue = hasValidJSXPropName
? formatPropValue(usedValue, inline, lvl, options)
: null;

let attributeFormattedInline = ' ';
let attributeFormattedMultiline = `\n${spacer(lvl + 1, tabStop)}`;
const isMultilineAttribute = formattedPropValue.includes('\n');
let attributePayload = '';

if (
useBooleanShorthandSyntax &&
Expand All @@ -42,14 +48,28 @@ export default (
// If a boolean is false and not different from it's default, we do not render the attribute
attributeFormattedInline = '';
attributeFormattedMultiline = '';
} else if (!hasValidJSXPropName) {
const formattedObjectSpreadValue = `{...${formatComplexDataStructure(
{ [name]: usedValue },
true,
lvl,
options
)}}`;
attributePayload = formattedObjectSpreadValue;
attributeFormattedInline += formattedObjectSpreadValue;
attributeFormattedMultiline += formattedObjectSpreadValue;
} else if (useBooleanShorthandSyntax && formattedPropValue === '{true}') {
attributePayload = `${name}`;
attributeFormattedInline += `${name}`;
attributeFormattedMultiline += `${name}`;
} else {
attributeFormattedInline += `${name}=${formattedPropValue}`;
attributeFormattedMultiline += `${name}=${formattedPropValue}`;
attributePayload = `${name}=${String(formattedPropValue)}`;
attributeFormattedInline += `${name}=${String(formattedPropValue)}`;
attributeFormattedMultiline += `${name}=${String(formattedPropValue)}`;
}

const isMultilineAttribute = attributePayload.includes('\n');

return {
attributeFormattedInline,
attributeFormattedMultiline,
Expand Down
46 changes: 46 additions & 0 deletions src/formatter/formatProp.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@

import formatProp from './formatProp';
import formatPropValue from './formatPropValue';
import formatComplexDataStructure from './formatComplexDataStructure';

jest.mock('./formatPropValue');
jest.mock('./formatComplexDataStructure');

const defaultOptions = {
useBooleanShorthandSyntax: true,
Expand Down Expand Up @@ -219,4 +221,48 @@ describe('formatProp', () => {

expect(formatPropValue).toHaveBeenCalledWith('bar', true, 4, options);
});

it('should format non-jsx prop names with object spread syntax', () => {
formatComplexDataStructure.mockReturnValue(
`{'foo.bar': "MockedPropValue"}`
);

expect(
formatProp('foo.bar', true, 'bar', false, null, true, 0, defaultOptions)
).toEqual({
attributeFormattedInline: ` {...{'foo.bar': "MockedPropValue"}}`,
attributeFormattedMultiline: `
{...{'foo.bar': "MockedPropValue"}}`,
isMultilineAttribute: false,
});

expect(formatComplexDataStructure).toHaveBeenCalledWith(
{ 'foo.bar': 'bar' },
true,
0,
defaultOptions
);
expect(formatPropValue).not.toHaveBeenCalled();
});

it('should format non-jsx boolean prop names with object spread syntax', () => {
formatComplexDataStructure.mockReturnValue(`{'@flag': true}`);

expect(
formatProp('@flag', true, true, false, null, true, 0, defaultOptions)
).toEqual({
attributeFormattedInline: ` {...{'@flag': true}}`,
attributeFormattedMultiline: `
{...{'@flag': true}}`,
isMultilineAttribute: false,
});

expect(formatComplexDataStructure).toHaveBeenCalledWith(
{ '@flag': true },
true,
0,
defaultOptions
);
expect(formatPropValue).not.toHaveBeenCalled();
});
});
6 changes: 3 additions & 3 deletions src/formatter/formatReactElementNode.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,18 +57,18 @@ describe('formatReactElementNode', () => {
type: 'ReactElement',
displayName: 'div',
defaultProps: {
a: { aa: '1', bb: { cc: '3' } },
a: { aa: '\\1', bb: { cc: '3' } },
},
props: {
a: { aa: '1', bb: { cc: '3' } },
a: { aa: '\\1', bb: { cc: '3' } },
},
childrens: [],
};

expect(formatReactElementNode(tree, false, 0, defaultOptions)).toEqual(
`<div
a={{
aa: '1',
aa: '\\\\1',
bb: {
cc: '3'
}
Expand Down
19 changes: 16 additions & 3 deletions src/formatter/formatTreeNode.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,30 @@ const escape = (s: string) => {
return s;
}

return `{\`${s}\`}`;
const escapedString = s.replace('${', '\\${').replace('`', '\\`');

return `{\`${escapedString}\`}`;
};

const preserveTrailingSpace = (s: string) => {
const escapeNewlinesInSingleQuotedString = (str: string) =>
// When we wrap whitespace into a JSX expression like `{'...'}`
// the content must be valid JS inside single quotes.
str.replace(/\r/g, '\\r').replace(/\n/g, '\\n');

let result = s;
if (result.endsWith(' ')) {
result = result.replace(/^(.*?)(\s+)$/, "$1{'$2'}");
result = result.replace(/^(.*?)(\s+)$/, (m, p1, p2) => {
const escapedP2 = escapeNewlinesInSingleQuotedString(p2);
return `${p1}{'${escapedP2}'}`;
});
}

if (result.startsWith(' ')) {
result = result.replace(/^(\s+)(.*)$/, "{'$1'}$2");
result = result.replace(/^(\s+)(.*)$/, (m, p1, p2) => {
const escapedP1 = escapeNewlinesInSingleQuotedString(p1);
return `{'${escapedP1}'}${p2}`;
});
}

return result;
Expand Down
12 changes: 12 additions & 0 deletions src/formatter/formatTreeNode.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,4 +68,16 @@ bar`);
"foo": "bar"
}\`}`);
});

it('should escape newlines when preserving leading/trailing spaces', () => {
expect(
formatTreeNode({ type: 'string', value: ' \n ' }, true, 0, {})
).toBe(`{' \\n '}`);
});

it('should escape template string interpolation marker', () => {
expect(formatTreeNode({ type: 'string', value: '${' }, true, 0, {})).toBe(
['{`\\', '{`}'].join('$')
);
});
});
20 changes: 20 additions & 0 deletions src/index.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,18 @@ describe('reactElementToJSXString(ReactElement)', () => {
);
});

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

it("reactElementToJSXString(<div {...{'@name': 'value'}} />)", () => {
expect(reactElementToJSXString(<div {...{ '@name': 'value' }} />)).toEqual(
`<div {...{'@name': 'value'}} />`
);
});

it('reactElementToJSXString(<div re={/^Hello world$/} />)', () => {
expect(reactElementToJSXString(<div re={/^Hello world$/} />)).toEqual(
'<div re={/^Hello world$/} />'
Expand Down Expand Up @@ -1360,4 +1372,12 @@ describe('reactElementToJSXString(ReactElement)', () => {
}}
/>`);
});

it('should escape ${ for string interpolation', () => {
expect(reactElementToJSXString(<div>${'{'}</div>)).toEqual(
`<div>
{\`\\\${\`}
</div>`
);
});
});
Loading