Implement parser switching (HTML, Vue, styled-components) (#2086)
* feat(multiparser): implement switching from html -> css,js,ts * feat(multiparser): use quasi value instead of originalTextmaster
parent
77f0c05d2a
commit
d1b94c540c
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
};
|
|
@ -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) {
|
||||
|
|
|
@ -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] ? ">" : " />";
|
||||
|
||||
|
|
|
@ -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("`");
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
`;
|
|
@ -0,0 +1,12 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<style>
|
||||
blink{
|
||||
|
||||
display: none ;}
|
||||
</style>
|
||||
</head>
|
||||
<body></body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1 @@
|
|||
run_spec(__dirname, { parser: "parse5" });
|
|
@ -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>
|
||||
`;
|
|
@ -0,0 +1,11 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<script type="text/javascript">
|
||||
hello( 'world'
|
||||
)
|
||||
</script>
|
||||
</head>
|
||||
<body></body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1 @@
|
|||
run_spec(__dirname, { parser: "parse5" });
|
|
@ -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>
|
||||
`;
|
|
@ -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>
|
||||
|
|
@ -0,0 +1 @@
|
|||
run_spec(__dirname, { parser: "parse5" });
|
|
@ -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;
|
||||
\`;
|
||||
|
||||
`;
|
|
@ -0,0 +1 @@
|
|||
run_spec(__dirname, { parser: "babylon" });
|
|
@ -0,0 +1,13 @@
|
|||
const Button = styled.button`
|
||||
color: palevioletred ;
|
||||
|
||||
font-size : 1em ;
|
||||
`;
|
||||
|
||||
const TomatoButton = Button.extend`
|
||||
color : tomato ;
|
||||
|
||||
border-color : tomato
|
||||
;
|
||||
|
||||
`;
|
Loading…
Reference in New Issue