diff --git a/src/clean-ast.js b/src/clean-ast.js index f1b77fcb..5b4eef36 100644 --- a/src/clean-ast.js +++ b/src/clean-ast.js @@ -128,6 +128,7 @@ function massageAST(ast) { } // Remove raw and cooked values from TemplateElement when it's CSS + // styled-jsx if ( ast.type === "JSXElement" && ast.openingElement.name.name === "style" && @@ -148,6 +149,13 @@ function massageAST(ast) { quasis.forEach(q => delete q.value); } + // styled-components + if ( + ast.type === "TaggedTemplateExpression" && + ast.tag.type === "MemberExpression" + ) { + newObj.quasi.quasis.forEach(quasi => delete quasi.value); + } return newObj; } diff --git a/src/multiparser.js b/src/multiparser.js new file mode 100644 index 00000000..273080bc --- /dev/null +++ b/src/multiparser.js @@ -0,0 +1,158 @@ +"use strict"; + +const util = require("./util"); +const docBuilders = require("./doc-builders"); +const indent = docBuilders.indent; +const hardline = docBuilders.hardline; +const softline = docBuilders.softline; +const concat = docBuilders.concat; + +/** + * @returns {{ parser: string, text: string, wrap?: Function } | void} + */ +function getSubtreeParser(path, options) { + switch (options.parser) { + case "parse5": + return fromHtmlParser2(path, options); + case "babylon": + case "flow": + case "typescript": + return fromBabylonFlowOrTypeScript(path, options); + } +} + +function fromBabylonFlowOrTypeScript(path) { + const node = path.getValue(); + + switch (node.type) { + 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 { + parser: "postcss", + wrap: 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 { + parser: "postcss", + wrap: doc => + concat([ + indent(concat([softline, stripTrailingHardline(doc)])), + softline + ]), + text: parent.quasis[0].value.raw + }; + } + + break; + } + } +} + +function fromHtmlParser2(path, options) { + const node = path.getValue(); + + switch (node.type) { + case "text": { + const parent = path.getParentNode(); + // Inline JavaScript + if ( + parent.type === "script" && + ((!parent.attribs.lang && !parent.attribs.lang) || + parent.attribs.type === "text/javascript" || + parent.attribs.type === "application/javascript") + ) { + const parser = options.parser === "flow" ? "flow" : "babylon"; + return { + parser, + wrap: doc => concat([hardline, doc]), + text: getText(options, node) + }; + } + + // Inline TypeScript + if ( + parent.type === "script" && + (parent.attribs.type === "application/x-typescript" || + parent.attribs.lang === "ts") + ) { + return { + parser: "typescript", + wrap: doc => concat([hardline, doc]), + text: getText(options, node) + }; + } + + // Inline Styles + if (parent.type === "style") { + return { + parser: "postcss", + wrap: doc => concat([hardline, stripTrailingHardline(doc)]), + text: getText(options, node) + }; + } + + break; + } + } +} + +function getText(options, node) { + return options.originalText.slice(util.locStart(node), util.locEnd(node)); +} + +function stripTrailingHardline(doc) { + // HACK remove ending hardline, original PR: #1984 + if ( + doc.type === "concat" && + doc.parts[0].type === "concat" && + doc.parts[0].parts.length === 2 && + doc.parts[0].parts[1] === hardline + ) { + return doc.parts[0].parts[0]; + } + return doc; +} + +module.exports = { + getSubtreeParser +}; diff --git a/src/parser-parse5.js b/src/parser-parse5.js index 09f8c9d4..6f15988b 100644 --- a/src/parser-parse5.js +++ b/src/parser-parse5.js @@ -7,7 +7,8 @@ function parse(text) { const parse5 = require("parse5"); try { const ast = parse5.parse(text, { - treeAdapter: parse5.treeAdapters.htmlparser2 + treeAdapter: parse5.treeAdapters.htmlparser2, + locationInfo: true }); return ast; } catch (error) { diff --git a/src/printer-htmlparser2.js b/src/printer-htmlparser2.js index ae8d7f63..5044a8c2 100644 --- a/src/printer-htmlparser2.js +++ b/src/printer-htmlparser2.js @@ -48,6 +48,8 @@ function genericPrint(path, options, print) { case "text": { return n.data.replace(/\s+/g, " ").trim(); } + case "script": + case "style": case "tag": { const selfClose = voidTags[n.name] ? ">" : " />"; diff --git a/src/printer.js b/src/printer.js index 77b31713..ad9ec8bc 100644 --- a/src/printer.js +++ b/src/printer.js @@ -3,6 +3,7 @@ const assert = require("assert"); const comments = require("./comments"); const FastPath = require("./fast-path"); +const getSubtreeParser = require("./multiparser").getSubtreeParser; const util = require("./util"); const isIdentifierName = require("esutils").keyword.isIdentifierNameES6; @@ -76,6 +77,25 @@ function genericPrint(path, options, printPath, args) { return options.originalText.slice(util.locStart(node), util.locEnd(node)); } + if (node) { + // Potentially switch to a different parser + const nextParser = getSubtreeParser(path, options); + + if (nextParser && nextParser.parser !== options.parser) { + const nextOptions = Object.assign({}, options, { + parser: nextParser.parser + }); + try { + const ast = require("./parser").parse(nextParser.text, nextOptions); + const nextDoc = printAstToDoc(ast, nextOptions); + + return nextParser.wrap ? nextParser.wrap(nextDoc) : nextDoc; + } catch (error) { + // Continue with current parser + } + } + } + let needsParens = false; const linesWithoutParens = getPrintFunction(options)( path, @@ -1751,43 +1771,6 @@ function genericPrintNoParens(path, options, print, args) { case "TemplateElement": return join(literalline, n.value.raw.split(/\r?\n/g)); case "TemplateLiteral": { - const parent = path.getParentNode(); - const parentParent = path.getParentNode(1); - const isCSS = - n.quasis && - n.quasis.length === 1 && - parent.type === "JSXExpressionContainer" && - parentParent.type === "JSXElement" && - parentParent.openingElement.name.name === "style" && - parentParent.openingElement.attributes.some( - attribute => attribute.name.name === "jsx" - ); - - if (isCSS) { - const parseCss = eval("require")("./parser-postcss"); - const newOptions = Object.assign({}, options, { parser: "postcss" }); - const text = n.quasis[0].value.raw; - try { - const ast = parseCss(text, newOptions); - let subtree = printAstToDoc(ast, newOptions); - - // HACK remove ending hardline - assert.ok( - subtree.type === "concat" && - subtree.parts[0].type === "concat" && - subtree.parts[0].parts.length === 2 && - subtree.parts[0].parts[1] === hardline - ); - subtree = subtree.parts[0].parts[0]; - - parts.push("`", indent(concat([line, subtree])), line, "`"); - return group(concat(parts)); - } catch (error) { - // If CSS parsing (or printing) failed - // we give up and just print the TemplateElement as usual - } - } - const expressions = path.map(print, "expressions"); parts.push("`"); diff --git a/src/util.js b/src/util.js index 86e93659..544c86d1 100644 --- a/src/util.js +++ b/src/util.js @@ -218,6 +218,9 @@ function locStart(node) { return locStart(node.decorators[0]); } + if (node.__location) { + return node.__location.startOffset; + } if (node.range) { return node.range[0]; } @@ -239,6 +242,9 @@ function locEnd(node) { loc = lineColumnToIndex(node.source.end, node.source.input.css); } + if (node.__location) { + return node.__location.endOffset; + } if (node.typeAnnotation) { return Math.max(loc, locEnd(node.typeAnnotation)); } diff --git a/tests/multiparser_html_css/__snapshots__/jsfmt.spec.js.snap b/tests/multiparser_html_css/__snapshots__/jsfmt.spec.js.snap new file mode 100644 index 00000000..bdb84402 --- /dev/null +++ b/tests/multiparser_html_css/__snapshots__/jsfmt.spec.js.snap @@ -0,0 +1,28 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`html-with-css-style.html 1`] = ` + + + + + + + + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + + + + + + +`; diff --git a/tests/multiparser_html_css/html-with-css-style.html b/tests/multiparser_html_css/html-with-css-style.html new file mode 100644 index 00000000..35391ca1 --- /dev/null +++ b/tests/multiparser_html_css/html-with-css-style.html @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/tests/multiparser_html_css/jsfmt.spec.js b/tests/multiparser_html_css/jsfmt.spec.js new file mode 100644 index 00000000..04b24727 --- /dev/null +++ b/tests/multiparser_html_css/jsfmt.spec.js @@ -0,0 +1 @@ +run_spec(__dirname, { parser: "parse5" }); diff --git a/tests/multiparser_html_js/__snapshots__/jsfmt.spec.js.snap b/tests/multiparser_html_js/__snapshots__/jsfmt.spec.js.snap new file mode 100644 index 00000000..b690ed2f --- /dev/null +++ b/tests/multiparser_html_js/__snapshots__/jsfmt.spec.js.snap @@ -0,0 +1,26 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`html-with-js-script.html 1`] = ` + + + + + + + + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + + + + + + +`; diff --git a/tests/multiparser_html_js/html-with-js-script.html b/tests/multiparser_html_js/html-with-js-script.html new file mode 100644 index 00000000..3e7634b9 --- /dev/null +++ b/tests/multiparser_html_js/html-with-js-script.html @@ -0,0 +1,11 @@ + + + + + + + + diff --git a/tests/multiparser_html_js/jsfmt.spec.js b/tests/multiparser_html_js/jsfmt.spec.js new file mode 100644 index 00000000..04b24727 --- /dev/null +++ b/tests/multiparser_html_js/jsfmt.spec.js @@ -0,0 +1 @@ +run_spec(__dirname, { parser: "parse5" }); diff --git a/tests/multiparser_html_ts/__snapshots__/jsfmt.spec.js.snap b/tests/multiparser_html_ts/__snapshots__/jsfmt.spec.js.snap new file mode 100644 index 00000000..1d38772d --- /dev/null +++ b/tests/multiparser_html_ts/__snapshots__/jsfmt.spec.js.snap @@ -0,0 +1,38 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`html-with-ts-script.html 1`] = ` + + + + + + + + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + + + + + + +`; diff --git a/tests/multiparser_html_ts/html-with-ts-script.html b/tests/multiparser_html_ts/html-with-ts-script.html new file mode 100644 index 00000000..1a67dbfc --- /dev/null +++ b/tests/multiparser_html_ts/html-with-ts-script.html @@ -0,0 +1,20 @@ + + + + + + + + diff --git a/tests/multiparser_html_ts/jsfmt.spec.js b/tests/multiparser_html_ts/jsfmt.spec.js new file mode 100644 index 00000000..04b24727 --- /dev/null +++ b/tests/multiparser_html_ts/jsfmt.spec.js @@ -0,0 +1 @@ +run_spec(__dirname, { parser: "parse5" }); diff --git a/tests/multiparser_js_css/__snapshots__/jsfmt.spec.js.snap b/tests/multiparser_js_css/__snapshots__/jsfmt.spec.js.snap new file mode 100644 index 00000000..f3ee84f8 --- /dev/null +++ b/tests/multiparser_js_css/__snapshots__/jsfmt.spec.js.snap @@ -0,0 +1,28 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`styled-components.js 1`] = ` +const Button = styled.button\` + color: palevioletred ; + + font-size : 1em ; +\`; + +const TomatoButton = Button.extend\` + color : tomato ; + +border-color : tomato + ; + +\`; +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +const Button = styled.button\` + color: palevioletred; + font-size: 1em; +\`; + +const TomatoButton = Button.extend\` + color: tomato; + border-color: tomato; +\`; + +`; diff --git a/tests/multiparser_js_css/jsfmt.spec.js b/tests/multiparser_js_css/jsfmt.spec.js new file mode 100644 index 00000000..f974ed6c --- /dev/null +++ b/tests/multiparser_js_css/jsfmt.spec.js @@ -0,0 +1 @@ +run_spec(__dirname, { parser: "babylon" }); diff --git a/tests/multiparser_js_css/styled-components.js b/tests/multiparser_js_css/styled-components.js new file mode 100644 index 00000000..f843e4df --- /dev/null +++ b/tests/multiparser_js_css/styled-components.js @@ -0,0 +1,13 @@ +const Button = styled.button` + color: palevioletred ; + + font-size : 1em ; +`; + +const TomatoButton = Button.extend` + color : tomato ; + +border-color : tomato + ; + +`;