diff --git a/src/multiparser.js b/src/multiparser.js
index 3a8339db..854cd4c4 100644
--- a/src/multiparser.js
+++ b/src/multiparser.js
@@ -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
- *
- * ```
- */
- 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:
+ *
+ */
+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
diff --git a/src/printer.js b/src/printer.js
index c0f84d6e..b2cbe688 100644
--- a/src/printer.js
+++ b/src/printer.js
@@ -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);
diff --git a/tests/template_literals/__snapshots__/jsfmt.spec.js.snap b/tests/template_literals/__snapshots__/jsfmt.spec.js.snap
index fa867b5d..b4caca47 100644
--- a/tests/template_literals/__snapshots__/jsfmt.spec.js.snap
+++ b/tests/template_literals/__snapshots__/jsfmt.spec.js.snap
@@ -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`] = `
;
+
+;
+
+;
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+;
+
+;
+
+;
+
+`;
diff --git a/tests/template_literals/styled-components-with-expressions.js b/tests/template_literals/styled-components-with-expressions.js
new file mode 100644
index 00000000..fe0aa7c7
--- /dev/null
+++ b/tests/template_literals/styled-components-with-expressions.js
@@ -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;
+ }
+ }
+`;
diff --git a/tests/template_literals/styled-jsx-with-expressions.js b/tests/template_literals/styled-jsx-with-expressions.js
new file mode 100644
index 00000000..fbbcab0d
--- /dev/null
+++ b/tests/template_literals/styled-jsx-with-expressions.js
@@ -0,0 +1,41 @@
+;
+
+;
+
+;