Vue: pretty-print binding syntax (#2108)

* feat(vue): pretty-print :class attributes

* feat(vue): switch to brand new custom parser API! 🎉

* refactor(vue): move AST manipulation to post-parse
master
Lucas Azzola 2017-06-14 22:33:15 +10:00 committed by GitHub
parent 9b772cb4fd
commit b7e1c366e0
12 changed files with 211 additions and 55 deletions

View File

@ -152,11 +152,27 @@ function propagateBreaks(doc) {
);
}
function removeLines(doc) {
// Force this doc into flat mode by statically converting all
// lines into spaces (or soft lines into nothing). Hard lines
// should still output because there's too great of a chance
// of breaking existing assumptions otherwise.
return mapDoc(doc, d => {
if (d.type === "line" && !d.hard) {
return d.soft ? "" : " ";
} else if (d.type === "if-break") {
return d.flatContents || "";
}
return d;
});
}
module.exports = {
isEmpty,
willBreak,
isLineNext,
traverseDoc,
mapDoc,
propagateBreaks
propagateBreaks,
removeLines
};

View File

@ -1,7 +1,7 @@
"use strict";
const util = require("./util");
const mapDoc = require("./doc-utils").mapDoc;
const docUtils = require("./doc-utils");
const docBuilders = require("./doc-builders");
const indent = docBuilders.indent;
const hardline = docBuilders.hardline;
@ -64,9 +64,6 @@ function fromBabylonFlowOrTypeScript(path) {
parentParent &&
parentParent.type === "TaggedTemplateExpression" &&
parent.quasis.length === 1 &&
// ((parentParent.tag.type === "MemberExpression" &&
// parentParent.tag.object.name === "Relay" &&
// parentParent.tag.property.name === "QL") ||
((parentParent.tag.type === "MemberExpression" &&
parentParent.tag.object.name === "graphql" &&
parentParent.tag.property.name === "experimental") ||
@ -132,6 +129,37 @@ function fromHtmlParser2(path, options) {
break;
}
case "attribute": {
/*
* Vue binding sytax: JS expressions
* :class="{ 'some-key': value }"
* v-bind:id="'list-' + id"
* v-if="foo && !bar"
* @click="someFunction()"
*/
if (/(^@)|(^v-)|:/.test(node.key) && !/^\w+$/.test(node.value)) {
return {
text: node.value,
options: {
parser: parseJavaScriptExpression,
// Use singleQuote since HTML attributes use double-quotes.
// TODO(azz): We still need to do an entity escape on the attribute.
singleQuote: true
},
transformDoc: doc => {
return concat([
node.key,
'="',
util.hasNewlineInRange(node.value, 0, node.value.length)
? doc
: docUtils.removeLines(doc),
'"'
]);
}
};
}
}
}
}
@ -158,7 +186,7 @@ function replacePlaceholders(quasisDoc, expressionDocs) {
}
const expressions = expressionDocs.slice();
const newDoc = mapDoc(quasisDoc, doc => {
const newDoc = docUtils.mapDoc(quasisDoc, doc => {
if (!doc || !doc.parts || !doc.parts.length) {
return doc;
}
@ -197,6 +225,16 @@ function replacePlaceholders(quasisDoc, expressionDocs) {
return expressions.length === 0 ? newDoc : null;
}
function parseJavaScriptExpression(text, parsers) {
// Force parsing as an expression
const ast = parsers.babylon(`(${text})`);
// Extract expression from the declaration
return {
type: "File",
program: ast.program.body[0].expression
};
}
function getText(options, node) {
return options.originalText.slice(util.locStart(node), util.locEnd(node));
}

View File

@ -11,7 +11,7 @@ function parse(text) {
treeAdapter: parse5.treeAdapters.htmlparser2,
locationInfo: true
});
return ast;
return extendAst(ast);
} catch (error) {
// throw createError(error.message, {
// start: {
@ -24,4 +24,27 @@ function parse(text) {
}
}
function extendAst(ast) {
if (!ast || !ast.children) {
return ast;
}
for (const child of ast.children) {
extendAst(child);
if (child.attribs) {
child.attributes = convertAttribs(child.attribs);
}
}
return ast;
}
function convertAttribs(attribs) {
return Object.keys(attribs).map(attributeKey => {
return {
type: "attribute",
key: attributeKey,
value: attribs[attributeKey]
};
});
}
module.exports = parse;

View File

@ -5,7 +5,7 @@ const docBuilders = require("./doc-builders");
const concat = docBuilders.concat;
const join = docBuilders.join;
const hardline = docBuilders.hardline;
// const line = docBuilders.line;
const line = docBuilders.line;
const softline = docBuilders.softline;
const group = docBuilders.group;
const indent = docBuilders.indent;
@ -41,7 +41,7 @@ function genericPrint(path, options, print) {
switch (n.type) {
case "root": {
return concat(path.map(print, "children"));
return printChildren(path, print);
}
case "directive": {
return concat(["<", n.data, ">", hardline]);
@ -53,15 +53,7 @@ function genericPrint(path, options, print) {
case "style":
case "tag": {
const selfClose = voidTags[n.name] ? ">" : " />";
const children = [];
path.each(childPath => {
const child = childPath.getValue();
if (child.type !== "text") {
children.push(softline);
}
children.push(childPath.call(print));
}, "children");
const children = printChildren(path, print);
const hasNewline = util.hasNewlineInRange(
options.originalText,
@ -74,39 +66,51 @@ function genericPrint(path, options, print) {
hasNewline ? hardline : "",
"<",
n.name,
printAttributes(n.attribs),
printAttributes(path, print),
n.children.length ? ">" : selfClose,
n.name.toLowerCase() === "html"
? concat(children)
: indent(concat(children)),
n.children.length ? concat([softline, "</", n.name, ">"]) : ""
? concat([hardline, children])
: indent(children),
n.children.length ? concat([softline, "</", n.name, ">"]) : hardline
])
);
}
case "comment": {
return concat(["<!-- ", n.data.trim(), " -->"]);
}
case "attribute": {
if (!n.value) {
return n.key;
}
return concat([n.key, '="', n.value, '"']);
}
default:
throw new Error("unknown htmlparser2 type: " + n.type);
}
}
function printAttributes(attribs) {
const attributeKeys = Object.keys(attribs);
function printAttributes(path, print) {
const node = path.getValue();
return concat([
attributeKeys.length ? " " : "",
join(
" ",
attributeKeys.map(name => {
if (attribs[name] === "") {
return name;
}
return concat([name, '="', attribs[name], '"']);
})
)
node.attributes.length ? " " : "",
indent(join(line, path.map(print, "attributes")))
]);
}
function printChildren(path, print) {
const children = [];
path.each(childPath => {
const child = childPath.getValue();
if (child.type !== "text") {
children.push(hardline);
}
children.push(childPath.call(print));
}, "children");
return concat(children);
}
module.exports = genericPrint;

View File

@ -2959,9 +2959,9 @@ function printFunctionParams(path, print, options, expandArg, printTypeParams) {
if (expandArg) {
return group(
concat([
removeLines(typeParams),
docUtils.removeLines(typeParams),
"(",
join(", ", printed.map(removeLines)),
join(", ", printed.map(docUtils.removeLines)),
")"
])
);
@ -4572,21 +4572,6 @@ function isStringLiteral(node) {
);
}
function removeLines(doc) {
// Force this doc into flat mode by statically converting all
// lines into spaces (or soft lines into nothing). Hard lines
// should still output because there's too great of a chance
// of breaking existing assumptions otherwise.
return docUtils.mapDoc(doc, d => {
if (d.type === "line" && !d.hard) {
return d.soft ? "" : " ";
} else if (d.type === "if-break") {
return d.flatContents || "";
}
return d;
});
}
function isObjectType(n) {
return n.type === "ObjectTypeAnnotation" || n.type === "TSTypeLiteral";
}
@ -4621,7 +4606,7 @@ function printAstToDoc(ast, options, addAlignmentSize) {
// Add a hardline to make the indents take effect
// It should be removed in index.js format()
doc = addAlignmentToDoc(
removeLines(concat([hardline, doc])),
docUtils.removeLines(concat([hardline, doc])),
addAlignmentSize,
options.tabWidth
);

View File

@ -16,14 +16,22 @@ exports[`hello-world.html 1`] = `
</html>
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<meta name="viewport"
content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible"
content="ie=edge">
<title>Document</title>
</head>
@ -39,5 +47,8 @@ exports[`html-fragment.html 1`] = `
<textarea>
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
<a href="#">Link</a><textarea />
<a href="#">Link</a>
<textarea />
`;

View File

@ -14,10 +14,13 @@ exports[`html-with-css-style.html 1`] = `
</html>
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
<!DOCTYPE html>
<html lang="en">
<head>
<style>

View File

@ -13,10 +13,13 @@ exports[`html-with-js-script.html 1`] = `
</html>
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
<!DOCTYPE html>
<html lang="en">
<head>
<script type="text/javascript">

View File

@ -22,10 +22,13 @@ exports[`html-with-ts-script.html 1`] = `
</html>
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
<!DOCTYPE html>
<html lang="en">
<head>
<script lang="ts">

View File

@ -1,5 +1,55 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`template-bind.vue 1`] = `
<template>
<div v-bind:id=" 'list-' + id "></div>
<div v-bind:id=" rawId | formatId "></div>
<div v-bind:id=" ok ? 'YES' : 'NO' "></div>
<button @click=" foo ( arg, 'string' ) "></button>
</template>
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
<template>
<div v-bind:id="'list-' + id" />
<div v-bind:id="rawId | formatId" />
<div v-bind:id="ok ? 'YES' : 'NO'" />
<button @click="foo(arg, 'string')" />
</template>
`;
exports[`template-class.vue 1`] = `
<template>
<h2
class="title"
:class="{ 'issue-realtime-pre-pulse': preAnimation,
'issue-realtime-trigger-pulse': pulseAnimation}"
v-html="titleHtml"
>
</h2>
</template>
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
<template>
<h2 class="title"
:class="{
'issue-realtime-pre-pulse': preAnimation,
'issue-realtime-trigger-pulse': pulseAnimation
}"
v-html="titleHtml">
</h2>
</template>
`;
exports[`vue-component.vue 1`] = `
<template >
<h1 >{{greeting}} world</h1 >
@ -19,9 +69,12 @@ p { font-size : 2em ; text-align : center ; }
</style >
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
<template>
<h1>{{greeting}} world</h1>
</template>
<script>
module.exports = {
data: function() {
@ -32,6 +85,7 @@ p { font-size : 2em ; text-align : center ; }
};
</script>
<style scoped>
p {
font-size: 2em;

View File

@ -0,0 +1,7 @@
<template>
<div v-bind:id=" 'list-' + id "></div>
<div v-bind:id=" rawId | formatId "></div>
<div v-bind:id=" ok ? 'YES' : 'NO' "></div>
<button @click=" foo ( arg, 'string' ) "></button>
</template>

View File

@ -0,0 +1,9 @@
<template>
<h2
class="title"
:class="{ 'issue-realtime-pre-pulse': preAnimation,
'issue-realtime-trigger-pulse': pulseAnimation}"
v-html="titleHtml"
>
</h2>
</template>