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 functionsmaster
parent
40c41f232f
commit
8dd0cb2a05
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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>;
|
||||
|
||||
`;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
`;
|
|
@ -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>;
|
Loading…
Reference in New Issue