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 @@ +; + +; + +;