Skip to content

Commit

Permalink
Add new fill primitive and use it to wrap text in JSX
Browse files Browse the repository at this point in the history
This adds a new `fill` primitive that can be used to fill lines with as much code as possible before moving to a new line with the same indentation.

It is used here layout JSX children. This gives us nicer wrapping for JSX elements containings lots of text interspersed with tags.
  • Loading branch information
karl committed Apr 20, 2017
1 parent d823fe6 commit e51b623
Show file tree
Hide file tree
Showing 12 changed files with 244 additions and 47 deletions.
7 changes: 7 additions & 0 deletions src/doc-builders.js
Expand Up @@ -57,6 +57,12 @@ function conditionalGroup(states, opts) {
);
}

function fill(parts) {
parts.forEach(assertDoc);

return { type: "fill", parts };
}

function ifBreak(breakContents, flatContents) {
if (breakContents) {
assertDoc(breakContents);
Expand Down Expand Up @@ -106,6 +112,7 @@ module.exports = {
literalline,
group,
conditionalGroup,
fill,
lineSuffix,
lineSuffixBoundary,
breakParent,
Expand Down
7 changes: 7 additions & 0 deletions src/doc-debug.js
Expand Up @@ -101,6 +101,13 @@ function printDoc(doc) {
);
}

if (doc.type === "fill") {
return ("fill") +
"(" +
doc.parts.map(printDoc).join(", ") +
")";
}

if (doc.type === "line-suffix") {
return "lineSuffix(" + printDoc(doc.contents) + ")";
}
Expand Down
89 changes: 88 additions & 1 deletion src/doc-printer.js
@@ -1,5 +1,12 @@
"use strict";

const docBuilders = require("./doc-builders");
const concat = docBuilders.concat;
const line = docBuilders.line;
const softline = docBuilders.softline;
const fill = docBuilders.fill;
const ifBreak = docBuilders.ifBreak;

const MODE_BREAK = 1;
const MODE_FLAT = 2;

Expand Down Expand Up @@ -30,7 +37,7 @@ function makeAlign(ind, n) {
};
}

function fits(next, restCommands, width) {
function fits(next, restCommands, width, mustBeFlat) {
let restIdx = restCommands.length;
const cmds = [next];
while (width >= 0) {
Expand Down Expand Up @@ -70,8 +77,17 @@ function fits(next, restCommands, width) {

break;
case "group":
if (mustBeFlat && doc.break) {
return false;
}
cmds.push([ind, doc.break ? MODE_BREAK : mode, doc.contents]);

break;
case "fill":
for (var i = doc.parts.length - 1; i >= 0; i--) {
cmds.push([ind, mode, doc.parts[i]]);
}

break;
case "if-break":
if (mode === MODE_BREAK) {
Expand Down Expand Up @@ -210,6 +226,77 @@ function printDocToString(doc, options) {
break;
}
break;

// Fills each line with as much code as possible before moving to a new
// line with the same indentation.
//
// Expects doc.parts to be an array of alternating code and
// whitespace. The whitespace contains the linebreaks.
//
// For example:
// ["I", line, "love", line, "monkeys"]
// or
// [{ type: group, ... }, softline, { type: group, ... }]
//
// It uses this parts structure to handle three main layout cases:
// * The first two non-whitespace items fit on the same line without
// breaking -> output both items and the whitespace "flat".
// * Only the first item fits on the line without breaking -> output the
// first item "flat" and the whitespace with "break".
// * Neither item fits on the line without breaking -> output the first
// item and the whitespace with "break".
case "fill": {
let rem = width - pos;

const parts = doc.parts;
if (parts.length === 0) {
break;
}

const first = parts[0];
const firstCmd = [ind, MODE_FLAT, first];
if (parts.length === 1) {
if (fits(firstCmd, cmds, width - rem, true)) {
cmds.push(firstCmd);
} else {
cmds.push([ind, mode, first]);
}
break;
}

const split = parts[1];
if (parts.length === 2) {
if (fits(firstCmd, cmds, width - rem, true)) {
cmds.push([ind, MODE_FLAT, split]);
cmds.push(firstCmd);
} else {
cmds.push([ind, mode, split]);
cmds.push([ind, mode, first]);
}
break;
}

const second = parts[2];
const remaining = parts.slice(2);
const remainingCmd = [ind, MODE_BREAK, fill(remaining)];

const firstAndSecondCmd = [ind, MODE_FLAT, concat([first, split, second])];

if (fits(firstAndSecondCmd, cmds, rem, true)) {
cmds.push(remainingCmd)
cmds.push([ind, MODE_FLAT, split]);
cmds.push(firstCmd);
} else if (fits(firstCmd, cmds, width - rem, true)) {
cmds.push(remainingCmd)
cmds.push([ind, MODE_BREAK, split]);
cmds.push(firstCmd);
} else {
cmds.push(remainingCmd);
cmds.push([ind, MODE_BREAK, split]);
cmds.push([ind, MODE_BREAK, first]);
}
break;
}
case "if-break":
if (mode === MODE_BREAK) {
if (doc.breakContents) {
Expand Down
4 changes: 2 additions & 2 deletions src/doc-utils.js
Expand Up @@ -10,7 +10,7 @@ function traverseDoc(doc, onEnter, onExit, shouldTraverseConditionalGroups) {
}

if (shouldRecurse) {
if (doc.type === "concat") {
if (doc.type === "concat" || doc.type === "fill") {
for (var i = 0; i < doc.parts.length; i++) {
traverseDocRec(doc.parts[i]);
}
Expand Down Expand Up @@ -43,7 +43,7 @@ function traverseDoc(doc, onEnter, onExit, shouldTraverseConditionalGroups) {
function mapDoc(doc, func) {
doc = func(doc);

if (doc.type === "concat") {
if (doc.type === "concat" || doc.type === "fill") {
return Object.assign({}, doc, {
parts: doc.parts.map(d => mapDoc(d, func))
});
Expand Down
43 changes: 24 additions & 19 deletions src/printer.js
Expand Up @@ -17,6 +17,7 @@ var group = docBuilders.group;
var indent = docBuilders.indent;
var align = docBuilders.align;
var conditionalGroup = docBuilders.conditionalGroup;
var fill = docBuilders.fill;
var ifBreak = docBuilders.ifBreak;
var breakParent = docBuilders.breakParent;
var lineSuffixBoundary = docBuilders.lineSuffixBoundary;
Expand Down Expand Up @@ -2915,8 +2916,8 @@ function printJSXChildren(path, options, print, jsxWhitespace) {

if (/\S/.test(value)) {
// treat each line of text as its own entity
value.split(/(\r?\n\s*)/).forEach(line => {
const newlines = line.match(/\n/g);
value.split(/(\r?\n\s*)/).forEach(textLine => {
const newlines = textLine.match(/\n/g);
if (newlines) {
children.push(hardline);

Expand All @@ -2927,27 +2928,30 @@ function printJSXChildren(path, options, print, jsxWhitespace) {
return;
}

const beginSpace = /^\s+/.test(line);
const beginSpace = /^\s+/.test(textLine);
if (beginSpace) {
children.push(jsxWhitespace);
children.push(softline);
}

const stripped = line.replace(/^\s+|\s+$/g, "");
const stripped = textLine.replace(/^\s+|\s+$/g, "");
if (stripped) {
children.push(stripped);

// Split text into words separated by "line"s.
stripped.split(/(\s+)/).forEach(word => {
const space = /\s+/.test(word);
if (space) {
children.push(line);
} else {
children.push(word);
}
});
}

const endSpace = /\s+$/.test(line);
const endSpace = /\s+$/.test(textLine);
if (endSpace) {
children.push(softline);
children.push(jsxWhitespace);
}
});

if (!isLineNext(util.getLast(children))) {
children.push(softline);
}
} else if (/\n/.test(value)) {
children.push(hardline);

Expand All @@ -2959,14 +2963,15 @@ function printJSXChildren(path, options, print, jsxWhitespace) {
// whitespace-only without newlines,
// eg; a single space separating two elements
children.push(jsxWhitespace);
children.push(softline);
}
} else {
children.push(print(childPath));

// add a line unless it's followed by a JSX newline
// add a softline where we have two adjacent JSX elements without
// any text or whitespace between them.
let next = n.children[i + 1];
if (!(next && /^\s*\n/.test(next.value))) {
const followedByJSXElement = next && next.type === 'JSXElement';
if (followedByJSXElement) {
children.push(softline);
}
}
Expand Down Expand Up @@ -3024,8 +3029,8 @@ function printJSXElement(path, options, print) {
let forcedBreak = willBreak(openingLines);

const jsxWhitespace = options.singleQuote
? ifBreak("{' '}", " ")
: ifBreak('{" "}', " ");
? ifBreak(concat(["{' '}", softline]), " ")
: ifBreak(concat(['{" "}', softline]), " ");
const children = printJSXChildren(path, options, print, jsxWhitespace);

// Trim trailing lines, recording if there was a hardline
Expand Down Expand Up @@ -3096,7 +3101,7 @@ function printJSXElement(path, options, print) {
groups.map(
contents =>
(Array.isArray(contents)
? conditionalGroup([concat(contents)])
? conditionalGroup([fill(contents)])
: contents)
)
)
Expand All @@ -3116,7 +3121,7 @@ function printJSXElement(path, options, print) {
}

return conditionalGroup([
group(concat([openingLines, concat(children), closingLines])),
group(concat([openingLines, fill(children), closingLines])),
multiLineElem
]);
}
Expand Down
3 changes: 2 additions & 1 deletion tests/flow/more_react/__snapshots__/jsfmt.spec.js.snap
Expand Up @@ -164,7 +164,8 @@ var App = require("App.react");
var app = (
<App y={42}>
{" "}// error, y: number but foo expects string in App.react
{" "}
// error, y: number but foo expects string in App.react
Some text.
</App>
);
Expand Down
34 changes: 14 additions & 20 deletions tests/jsx-significant-space/__snapshots__/jsfmt.spec.js.snap
Expand Up @@ -90,8 +90,9 @@ before = (
before_break1 = (
<span>
<span barbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbar />
{" "}
<span
barbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbar
/>{" "}
foo
</span>
);
Expand All @@ -100,16 +101,14 @@ before_break2 = (
<span>
<span
barbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbar
/>
{" "}
/>{" "}
foo
</span>
);
after_break = (
<span>
foo
{" "}
foo{" "}
<span barbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbar />
</span>
);
Expand Down Expand Up @@ -146,13 +145,15 @@ nest_plz = (
regression_not_transformed_1 = (
<span>
{" "}<Icon icon="aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" />
{" "}
<Icon icon="aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" />
</span>
);
regression_not_transformed_2 = (
<span>
<Icon icon="aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" />{" "}
</span>
);
Expand All @@ -165,8 +166,8 @@ similar_1 = (
similar_2 = (
<span>
<Icon icon="aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" />
{" "}
<Icon icon="aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" />{" "}
</span>
);
Expand All @@ -178,22 +179,15 @@ similar_3 = (
not_broken_end = (
<div>
long text long text long text long text long text long text long text long text
{" "}
<link>url</link>
{" "}
long text long text
long text long text long text long text long text long text long text long
text <link>url</link> long text long text
</div>
);
not_broken_begin = (
<div>
<br />
{" "}
long text long text long text long text long text long text long text long text
<link>url</link>
{" "}
long text long text
<br /> long text long text long text long text long text long text long text
long text<link>url</link> long text long text
</div>
);
Expand Down
4 changes: 2 additions & 2 deletions tests/jsx-split-attrs/__snapshots__/jsfmt.spec.js.snap
Expand Up @@ -122,8 +122,7 @@ long_open_long_children = (
colour="blue"
size="large"
submitLabel="Sign in with Google"
/>
d
/>d
</BaseForm>
<BaseForm
url="/auth/google"
Expand Down Expand Up @@ -179,6 +178,7 @@ leave_opening = (
submitLabel="Sign in with Google"
>
{" "}
</BaseForm>
);
Expand Down

0 comments on commit e51b623

Please sign in to comment.