Fix unstable JSX output by ensuring we follow `fill` rules. (#1827)

* Fix unstable JSX output by ensuring we follow `fill` rules.

This changes makes us more strict about ensuring our JSX children follow the alternating content/whitespace format expected by the `fill` primitive.

Previously there were some cases where could get out of sync which would throw out the formatting.

* Simplify whitespace wrangling

# Conflicts:
#	src/printer.js
master
Karl O'Keeffe 2017-05-31 17:42:36 +01:00 committed by Christopher Chedeau
parent fa708d102a
commit a680cd8534
3 changed files with 128 additions and 41 deletions

View File

@ -3861,6 +3861,11 @@ function isEmptyJSXElement(node) {
// //
// For another, leading, trailing, and lone whitespace all need to // For another, leading, trailing, and lone whitespace all need to
// turn themselves into the rather ugly `{' '}` when breaking. // turn themselves into the rather ugly `{' '}` when breaking.
//
// Finally we print JSX using the `fill` doc primitive.
// This requires that we give it an array of alternating
// content and whitespace elements.
// To ensure this we add dummy `""` content elements as needed.
function printJSXChildren(path, options, print, jsxWhitespace) { function printJSXChildren(path, options, print, jsxWhitespace) {
const n = path.getValue(); const n = path.getValue();
const children = []; const children = [];
@ -3877,10 +3882,12 @@ function printJSXChildren(path, options, print, jsxWhitespace) {
value.split(/(\r?\n\s*)/).forEach(textLine => { value.split(/(\r?\n\s*)/).forEach(textLine => {
const newlines = textLine.match(/\n/g); const newlines = textLine.match(/\n/g);
if (newlines) { if (newlines) {
children.push("");
children.push(hardline); children.push(hardline);
// allow one extra newline // allow one extra newline
if (newlines.length > 1) { if (newlines.length > 1) {
children.push("");
children.push(hardline); children.push(hardline);
} }
return; return;
@ -3892,6 +3899,23 @@ function printJSXChildren(path, options, print, jsxWhitespace) {
const beginSpace = /^[ \n\r\t]+/.test(textLine); const beginSpace = /^[ \n\r\t]+/.test(textLine);
if (beginSpace) { if (beginSpace) {
children.push("");
children.push(jsxWhitespace);
}
const stripped = textLine.replace(/^[ \n\r\t]+|[ \n\r\t]+$/g, "");
// Split text into words separated by "line"s.
stripped.split(/([ \n\r\t]+)/).forEach(word => {
const space = /[ \n\r\t]+/.test(word);
if (space) {
children.push(line);
} else {
children.push(word);
}
});
const endSpace = /[ \n\r\t]+$/.test(textLine);
if (endSpace) {
children.push(jsxWhitespace); children.push(jsxWhitespace);
} else { } else {
// Ideally this would be a `softline` to allow a break between // Ideally this would be a `softline` to allow a break between
@ -3905,57 +3929,39 @@ function printJSXChildren(path, options, print, jsxWhitespace) {
// adverse effect on formatting algorithm. // adverse effect on formatting algorithm.
children.push(""); children.push("");
} }
const stripped = textLine.replace(/^[ \n\r\t]+|[ \n\r\t]+$/g, "");
if (stripped) {
// Split text into words separated by "line"s.
stripped.split(/([ \n\r\t]+)/).forEach(word => {
const space = /[ \n\r\t]+/.test(word);
if (space) {
children.push(line);
} else {
children.push(word);
}
});
}
const endSpace = /[ \n\r\t]+$/.test(textLine);
if (endSpace) {
children.push(jsxWhitespace);
} else {
// As above this would ideally be a `softline`.
children.push("");
}
}); });
} else if (/\n/.test(value)) { } else if (/\n/.test(value)) {
children.push("");
children.push(hardline); children.push(hardline);
// allow one extra newline // allow one extra newline
if (value.match(/\n/g).length > 1) { if (value.match(/\n/g).length > 1) {
children.push("");
children.push(hardline); children.push(hardline);
} }
} else if (/[ \n\r\t]/.test(value)) { } else if (/[ \n\r\t]/.test(value)) {
// whitespace(s)-only without newlines, // whitespace(s)-only without newlines,
// eg; one or more spaces separating two elements // eg; one or more spaces separating two elements
for (let i = 0; i < value.length; ++i) { for (let i = 0; i < value.length; ++i) {
children.push(jsxWhitespace);
// Because fill expects alternating content and whitespace parts // Because fill expects alternating content and whitespace parts
// we need to include an empty content part between each JSX // we need to include an empty content part before each JSX
// whitespace. // whitespace.
if (i + 1 < value.length) { children.push("");
children.push(""); children.push(jsxWhitespace);
}
} }
} }
} else { } else {
children.push(print(childPath)); children.push(print(childPath));
// add a softline where we have two adjacent elements that are not
// literals
const next = n.children[i + 1]; const next = n.children[i + 1];
const followedByJSXElement = next && !isLiteral(next); const followedByJSXElement = next && !isLiteral(next);
if (followedByJSXElement) { if (followedByJSXElement) {
children.push(softline); children.push(softline);
} else {
// Ideally this would be a softline as well.
// See the comment above about the Facebook translation pipeline as
// to why this is an empty string.
children.push("");
} }
} }
}, "children"); }, "children");
@ -4019,9 +4025,20 @@ function printJSXElement(path, options, print) {
const children = printJSXChildren(path, options, print, jsxWhitespace); const children = printJSXChildren(path, options, print, jsxWhitespace);
// Trim trailing lines, recording if there was a hardline // Remove multiple filler empty strings
// These can occur when a text element is followed by a newline.
for (let i = children.length - 2; i >= 0; i--) {
if (children[i] === "" && children[i + 1] === "") {
children.splice(i, 2);
}
}
// Trim trailing lines (or empty strings), recording if there was a hardline
let numTrailingHard = 0; let numTrailingHard = 0;
while (children.length && isLineNext(util.getLast(children))) { while (
children.length &&
(isLineNext(util.getLast(children)) || isEmpty(util.getLast(children)))
) {
if (willBreak(util.getLast(children))) { if (willBreak(util.getLast(children))) {
++numTrailingHard; ++numTrailingHard;
forcedBreak = true; forcedBreak = true;
@ -4030,21 +4047,28 @@ function printJSXElement(path, options, print) {
} }
// allow one extra newline // allow one extra newline
if (numTrailingHard > 1) { if (numTrailingHard > 1) {
children.push("");
children.push(hardline); children.push(hardline);
} }
// Trim leading lines (or empty strings), recording if there was a hardline // Trim leading lines (or empty strings), recording if there was a hardline
let numLeadingHard = 0; let numLeadingHard = 0;
while (children.length && (isLineNext(children[0]) || isEmpty(children[0]))) { while (
if (willBreak(children[0])) { children.length &&
(isLineNext(children[0]) || isEmpty(children[0])) &&
(isLineNext(children[1]) || isEmpty(children[1]))
) {
if (willBreak(children[0]) || willBreak(children[1])) {
++numLeadingHard; ++numLeadingHard;
forcedBreak = true; forcedBreak = true;
} }
children.shift(); children.shift();
children.shift();
} }
// allow one extra newline // allow one extra newline
if (numLeadingHard > 1) { if (numLeadingHard > 1) {
children.unshift(hardline); children.unshift(hardline);
children.unshift("");
} }
// Tweak how we format children if outputting this element over multiple lines. // Tweak how we format children if outputting this element over multiple lines.
@ -4054,15 +4078,11 @@ function printJSXElement(path, options, print) {
// Ensure that we display leading, trailing, and solitary whitespace as // Ensure that we display leading, trailing, and solitary whitespace as
// `{" "}` when outputting this element over multiple lines. // `{" "}` when outputting this element over multiple lines.
if (child === jsxWhitespace) { if (child === jsxWhitespace) {
if (children.length === 1) { if (i === 1 && children[i - 1] === "") {
multilineChildren.push(rawJsxWhitespace); if (children.length === 2) {
return; multilineChildren.push(rawJsxWhitespace);
} else if (i === 0) { return;
// Fill expects alternating content & whitespace parts }
// always starting with content.
// So we add a dummy content element if we would otherwise start
// with whitespace.
multilineChildren.push("");
multilineChildren.push(concat([rawJsxWhitespace, hardline])); multilineChildren.push(concat([rawJsxWhitespace, hardline]));
return; return;
} else if (i === children.length - 1) { } else if (i === children.length - 1) {

View File

@ -117,6 +117,24 @@ this_really_should_split_across_lines =
<div> <div>
before{stuff}after{stuff}after{stuff}after{stuff}after{stuff}after{stuff}after{stuff}after{stuff}after{stuff}after{stuff}after{stuff}after{stuff}after{stuff}after{stuff}after before{stuff}after{stuff}after{stuff}after{stuff}after{stuff}after{stuff}after{stuff}after{stuff}after{stuff}after{stuff}after{stuff}after{stuff}after{stuff}after{stuff}after
</div> </div>
unstable_before =
<div className="yourScore">
Your score: <span className="score">{\`\${mini.crosstable.users[sessionUserId]} - \${mini.crosstable.users[user.id]}\`}</span>
</div>
unstable_after_first_run = (
<div className="yourScore">
Your score:
{" "}
<span className="score">{\`\${mini.crosstable.users[sessionUserId]} - \${mini
.crosstable.users[user.id]}\`}</span>
</div>
);
solitary_whitespace =
<div first="first" second="second" third="third" fourth="fourth" fifth="fifth" sixth="sixth"> </div>
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Wrapping text // Wrapping text
x = ( x = (
@ -293,4 +311,35 @@ this_really_should_split_across_lines = (
</div> </div>
); );
unstable_before = (
<div className="yourScore">
Your score:
{" "}
<span className="score">{\`\${mini.crosstable.users[sessionUserId]} - \${mini
.crosstable.users[user.id]}\`}</span>
</div>
);
unstable_after_first_run = (
<div className="yourScore">
Your score:
{" "}
<span className="score">{\`\${mini.crosstable.users[sessionUserId]} - \${mini
.crosstable.users[user.id]}\`}</span>
</div>
);
solitary_whitespace = (
<div
first="first"
second="second"
third="third"
fourth="fourth"
fifth="fifth"
sixth="sixth"
>
{" "}
</div>
);
`; `;

View File

@ -114,3 +114,21 @@ this_really_should_split_across_lines =
<div> <div>
before{stuff}after{stuff}after{stuff}after{stuff}after{stuff}after{stuff}after{stuff}after{stuff}after{stuff}after{stuff}after{stuff}after{stuff}after{stuff}after{stuff}after before{stuff}after{stuff}after{stuff}after{stuff}after{stuff}after{stuff}after{stuff}after{stuff}after{stuff}after{stuff}after{stuff}after{stuff}after{stuff}after{stuff}after
</div> </div>
unstable_before =
<div className="yourScore">
Your score: <span className="score">{`${mini.crosstable.users[sessionUserId]} - ${mini.crosstable.users[user.id]}`}</span>
</div>
unstable_after_first_run = (
<div className="yourScore">
Your score:
{" "}
<span className="score">{`${mini.crosstable.users[sessionUserId]} - ${mini
.crosstable.users[user.id]}`}</span>
</div>
);
solitary_whitespace =
<div first="first" second="second" third="third" fourth="fourth" fifth="fifth" sixth="sixth"> </div>