Implement parser switching (HTML, Vue, styled-components) (#2086)

* feat(multiparser): implement switching from html -> css,js,ts

* feat(multiparser): use quasi value instead of originalText
master
Lucas Azzola 2017-06-11 01:03:39 +10:00 committed by Christopher Chedeau
parent 77f0c05d2a
commit d1b94c540c
18 changed files with 376 additions and 38 deletions

View File

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

158
src/multiparser.js Normal file
View File

@ -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
* <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 {
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
};

View File

@ -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) {

View File

@ -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] ? ">" : " />";

View File

@ -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("`");

View File

@ -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));
}

View File

@ -0,0 +1,28 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`html-with-css-style.html 1`] = `
<!DOCTYPE html>
<html lang="en">
<head>
<style>
blink{
display: none ;}
</style>
</head>
<body></body>
</html>
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
<!DOCTYPE html>
<html lang="en">
<head>
<style>
blink {
display: none;
}
</style>
</head>
<body></body>
</html>
`;

View File

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<style>
blink{
display: none ;}
</style>
</head>
<body></body>
</html>

View File

@ -0,0 +1 @@
run_spec(__dirname, { parser: "parse5" });

View File

@ -0,0 +1,26 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`html-with-js-script.html 1`] = `
<!DOCTYPE html>
<html lang="en">
<head>
<script type="text/javascript">
hello( 'world'
)
</script>
</head>
<body></body>
</html>
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
<!DOCTYPE html>
<html lang="en">
<head>
<script type="text/javascript">
hello("world");
</script>
</head>
<body></body>
</html>
`;

View File

@ -0,0 +1,11 @@
<!DOCTYPE html>
<html lang="en">
<head>
<script type="text/javascript">
hello( 'world'
)
</script>
</head>
<body></body>
</html>

View File

@ -0,0 +1 @@
run_spec(__dirname, { parser: "parse5" });

View File

@ -0,0 +1,38 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`html-with-ts-script.html 1`] = `
<!DOCTYPE html>
<html lang="en">
<head>
<script lang="ts">
type X = { [
K in keyof Y
]: Partial < K > } ;
class Foo< T >{
constructor ( private foo: keyof Apple ){
}
}
</script>
</head>
<body></body>
</html>
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
<!DOCTYPE html>
<html lang="en">
<head>
<script lang="ts">
type X = { [K in keyof Y]: Partial<K> };
class Foo<T> {
constructor(private foo: keyof Apple) {}
}
</script>
</head>
<body></body>
</html>
`;

View File

@ -0,0 +1,20 @@
<!DOCTYPE html>
<html lang="en">
<head>
<script lang="ts">
type X = { [
K in keyof Y
]: Partial < K > } ;
class Foo< T >{
constructor ( private foo: keyof Apple ){
}
}
</script>
</head>
<body></body>
</html>

View File

@ -0,0 +1 @@
run_spec(__dirname, { parser: "parse5" });

View File

@ -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;
\`;
`;

View File

@ -0,0 +1 @@
run_spec(__dirname, { parser: "babylon" });

View File

@ -0,0 +1,13 @@
const Button = styled.button`
color: palevioletred ;
font-size : 1em ;
`;
const TomatoButton = Button.extend`
color : tomato ;
border-color : tomato
;
`;