Format CSS in template literals with expressions (#2102)

* Add support for styled-jsx with expressions

* Lint

* Fix require for Node 4

* Re-create template-literal document when replacing placeholders

* Add support for styled-components with expressions

* Fix merge

* Move css library detection to functions
master
Rodrigo Pombo 2017-06-13 13:45:16 -03:00 committed by Christopher Chedeau
parent 40c41f232f
commit 8dd0cb2a05
5 changed files with 338 additions and 58 deletions

View File

@ -1,18 +1,19 @@
"use strict";
const util = require("./util");
const mapDoc = require("./doc-utils").mapDoc;
const docBuilders = require("./doc-builders");
const indent = docBuilders.indent;
const hardline = docBuilders.hardline;
const softline = docBuilders.softline;
const concat = docBuilders.concat;
function printSubtree(subtreeParser, options) {
function printSubtree(subtreeParser, options, expressionDocs) {
const next = Object.assign({}, { transformDoc: doc => doc }, subtreeParser);
next.options = Object.assign({}, options, next.options);
const ast = require("./parser").parse(next.text, next.options);
const nextDoc = require("./printer").printAstToDoc(ast, next.options);
return next.transformDoc(nextDoc);
return next.transformDoc(nextDoc, expressionDocs);
}
/**
@ -33,63 +34,25 @@ function fromBabylonFlowOrTypeScript(path) {
const node = path.getValue();
switch (node.type) {
case "TemplateLiteral": {
const isCss = [isStyledJsx, isStyledComponents].some(isIt => isIt(path));
if (isCss) {
// Get full template literal with expressions replaced by placeholders
const rawQuasis = node.quasis.map(q => q.value.raw);
const text = rawQuasis.join("@prettier-placeholder");
return {
options: { parser: "postcss" },
transformDoc: transformCssDoc,
text: text
};
}
break;
}
case "TemplateElement": {
const parent = path.getParentNode();
const parentParent = path.getParentNode(1);
const parentParentParent = path.getParentNode(2);
/*
* styled-jsx:
* ```jsx
* <style jsx>{`div{color:red}`}</style>
* ```
*/
if (
parentParentParent &&
parent.quasis &&
parent.quasis.length === 1 &&
parentParent.type === "JSXExpressionContainer" &&
parentParentParent.type === "JSXElement" &&
parentParentParent.openingElement.name.name === "style" &&
parentParentParent.openingElement.attributes.some(
attribute => attribute.name.name === "jsx"
)
) {
return {
options: { parser: "postcss" },
transformDoc: doc =>
concat([
indent(concat([softline, stripTrailingHardline(doc)])),
softline
]),
text: parent.quasis[0].value.raw
};
}
/*
* styled-components:
* styled.button`color: red`
* Foo.extend`color: red`
*/
if (
parentParent &&
parentParent.type === "TaggedTemplateExpression" &&
parent.quasis.length === 1 &&
parentParent.tag.type === "MemberExpression" &&
(parentParent.tag.object.name === "styled" ||
(/^[A-Z]/.test(parentParent.tag.object.name) &&
parentParent.tag.property.name === "extend"))
) {
return {
options: { parser: "postcss" },
transformDoc: doc =>
concat([
indent(concat([softline, stripTrailingHardline(doc)])),
softline
]),
text: parent.quasis[0].value.raw
};
}
/*
* react-relay and graphql-tag
@ -172,6 +135,68 @@ function fromHtmlParser2(path, options) {
}
}
function transformCssDoc(quasisDoc, expressionDocs) {
const newDoc = replacePlaceholders(quasisDoc, expressionDocs);
if (!newDoc) {
throw new Error("Couldn't insert all the expressions");
}
return concat([
"`",
indent(concat([softline, stripTrailingHardline(newDoc)])),
softline,
"`"
]);
}
// Search all the placeholders in the quasisDoc tree
// and replace them with the expression docs one by one
// returns a new doc with all the placeholders replaced,
// or null if it couldn't replace any expression
function replacePlaceholders(quasisDoc, expressionDocs) {
if (!expressionDocs || !expressionDocs.length) {
return quasisDoc;
}
const expressions = expressionDocs.slice();
const newDoc = mapDoc(quasisDoc, doc => {
if (!doc || !doc.parts || !doc.parts.length) {
return doc;
}
let parts = doc.parts;
if (
parts.length > 1 &&
parts[0] === "@" &&
typeof parts[1] === "string" &&
parts[1].startsWith("prettier-placeholder")
) {
// If placeholder is split, join it
const at = parts[0];
const placeholder = parts[1];
const rest = parts.slice(2);
parts = [at + placeholder].concat(rest);
}
if (
typeof parts[0] === "string" &&
parts[0].startsWith("@prettier-placeholder")
) {
const placeholder = parts[0];
const rest = parts.slice(1);
// When the expression has a suffix appended, like:
// animation: linear ${time}s ease-out;
const suffix = placeholder.slice("@prettier-placeholder".length);
const expression = expressions.shift();
parts = ["${", expression, "}" + suffix].concat(rest);
}
return Object.assign({}, doc, {
parts: parts
});
});
return expressions.length === 0 ? newDoc : null;
}
function getText(options, node) {
return options.originalText.slice(util.locStart(node), util.locEnd(node));
}
@ -182,13 +207,55 @@ function stripTrailingHardline(doc) {
doc.type === "concat" &&
doc.parts[0].type === "concat" &&
doc.parts[0].parts.length === 2 &&
doc.parts[0].parts[1] === hardline
// doc.parts[0].parts[1] === hardline :
doc.parts[0].parts[1].type === "concat" &&
doc.parts[0].parts[1].parts.length === 2 &&
doc.parts[0].parts[1].parts[0].hard &&
doc.parts[0].parts[1].parts[1].type === "break-parent"
) {
return doc.parts[0].parts[0];
}
return doc;
}
/**
* Template literal in this context:
* <style jsx>{`div{color:red}`}</style>
*/
function isStyledJsx(path) {
const node = path.getValue();
const parent = path.getParentNode();
const parentParent = path.getParentNode(1);
return (
parentParent &&
node.quasis &&
parent.type === "JSXExpressionContainer" &&
parentParent.type === "JSXElement" &&
parentParent.openingElement.name.name === "style" &&
parentParent.openingElement.attributes.some(
attribute => attribute.name.name === "jsx"
)
);
}
/**
* Template literal in this context:
* styled.button`color: red`
* or
* Foo.extend`color: red`
*/
function isStyledComponents(path) {
const parent = path.getParentNode();
return (
parent &&
parent.type === "TaggedTemplateExpression" &&
parent.tag.type === "MemberExpression" &&
(parent.tag.object.name === "styled" ||
(/^[A-Z]/.test(parent.tag.object.name) &&
parent.tag.property.name === "extend"))
);
}
module.exports = {
getSubtreeParser,
printSubtree

View File

@ -82,7 +82,10 @@ function genericPrint(path, options, printPath, args) {
const next = multiparser.getSubtreeParser(path, options);
if (next) {
try {
return multiparser.printSubtree(next, options);
const expressionDocs = node.expressions
? path.map(printPath, "expressions")
: [];
return multiparser.printSubtree(next, options, expressionDocs);
} catch (error) {
if (process.env.PRETTIER_DEBUG) {
console.error(error);

View File

@ -1,5 +1,56 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`styled-components-with-expressions.js 1`] = `
const Button = styled.a\`
/* Comment */
display: \${props=>props.display};
\`;
styled.div\`
display: \${props=>props.display};
border: \${props=>props.border}px;
margin: 10px \${props=>props.border}px ;
\`;
const EqualDivider = styled.div\`
margin: 0.5rem;
padding: 1rem;
background: papayawhip ;
> * {
flex: 1;
&:not(:first-child) {
\${props => props.vertical ? 'margin-top' : 'margin-left'}: 1rem;
}
}
\`;
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
const Button = styled.a\`
/* Comment */
display: \${props => props.display};
\`;
styled.div\`
display: \${props => props.display};
border: \${props => props.border}px;
margin: 10px \${props => props.border}px;
\`;
const EqualDivider = styled.div\`
margin: 0.5rem;
padding: 1rem;
background: papayawhip;
> * {
flex: 1;
&:not(:first-child) {
\${props => (props.vertical ? "margin-top" : "margin-left")}: 1rem;
}
}
\`;
`;
exports[`styled-jsx.js 1`] = `
<style jsx>{\`
/* a comment */
@ -59,3 +110,97 @@ color: red; display: none
</div>;
`;
exports[`styled-jsx-with-expressions.js 1`] = `
<style jsx>{\`
div {
display: \${expr};
color: \${expr};
\${expr};
\${expr};
background: red;
animation: \${expr} 10s ease-out;
}
@media (\${expr}) {
div.\${expr} {
color: red;
}
\${expr} {
color: red;
}
}
@media (min-width: \${expr}) {
div.\${expr} {
color: red;
}
all\${expr} {
color: red;
}
}
@font-face {
\${expr}
}
\`}</style>;
<style jsx>{\`
div {
animation: linear \${seconds}s ease-out;
}
\`}</style>;
<style jsx>{\`
div {
animation: 3s ease-in 1s \${foo => foo.getIterations()} reverse both paused slidein;
}
\`}</style>;
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
<style jsx>{\`
div {
display: \${expr};
color: \${expr};
\${expr};
\${expr};
background: red;
animation: \${expr} 10s ease-out;
}
@media (\${expr}) {
div.\${expr} {
color: red;
}
\${expr} {
color: red;
}
}
@media (min-width: \${expr}) {
div.\${expr} {
color: red;
}
all\${expr} {
color: red;
}
}
@font-face {
\${expr};
}
\`}</style>;
<style jsx>{\`
div {
animation: linear \${seconds}s ease-out;
}
\`}</style>;
<style jsx>{\`
div {
animation: 3s
ease-in
1s
\${foo => foo.getIterations()}
reverse
both
paused
slidein;
}
\`}</style>;
`;

View File

@ -0,0 +1,24 @@
const Button = styled.a`
/* Comment */
display: ${props=>props.display};
`;
styled.div`
display: ${props=>props.display};
border: ${props=>props.border}px;
margin: 10px ${props=>props.border}px ;
`;
const EqualDivider = styled.div`
margin: 0.5rem;
padding: 1rem;
background: papayawhip ;
> * {
flex: 1;
&:not(:first-child) {
${props => props.vertical ? 'margin-top' : 'margin-left'}: 1rem;
}
}
`;

View File

@ -0,0 +1,41 @@
<style jsx>{`
div {
display: ${expr};
color: ${expr};
${expr};
${expr};
background: red;
animation: ${expr} 10s ease-out;
}
@media (${expr}) {
div.${expr} {
color: red;
}
${expr} {
color: red;
}
}
@media (min-width: ${expr}) {
div.${expr} {
color: red;
}
all${expr} {
color: red;
}
}
@font-face {
${expr}
}
`}</style>;
<style jsx>{`
div {
animation: linear ${seconds}s ease-out;
}
`}</style>;
<style jsx>{`
div {
animation: 3s ease-in 1s ${foo => foo.getIterations()} reverse both paused slidein;
}
`}</style>;