prettierx
Version:
prettierX - a less opinionated fork of the Prettier code formatter
301 lines (268 loc) • 7.99 kB
JavaScript
;
const { hasComment, CommentCheckFlags, isObjectProperty } = require("./utils");
const formatMarkdown = require("./embed/markdown");
const formatCss = require("./embed/css");
const formatGraphql = require("./embed/graphql");
const formatHtml = require("./embed/html");
function getLanguage(path) {
if (
isStyledJsx(path) ||
isStyledComponents(path) ||
isCssProp(path) ||
isAngularComponentStyles(path)
) {
return "css";
}
if (isGraphQL(path)) {
return "graphql";
}
if (isHtml(path)) {
return "html";
}
if (isAngularComponentTemplate(path)) {
return "angular";
}
if (isMarkdown(path)) {
return "markdown";
}
}
function embed(path, print, textToDoc, options) {
const node = path.getValue();
if (
node.type !== "TemplateLiteral" ||
// Bail out if any of the quasis have an invalid escape sequence
// (which would make the `cooked` value be `null`)
hasInvalidCookedValue(node)
) {
return;
}
const language = getLanguage(path);
if (!language) {
return;
}
if (language === "markdown") {
return formatMarkdown(path, print, textToDoc);
}
if (language === "css") {
// [prettierx] --template-curly-spacing option support (...)
return formatCss(path, print, textToDoc, options);
}
if (language === "graphql") {
// [prettierx] --template-curly-spacing option support (...)
return formatGraphql(path, print, textToDoc, options);
}
if (language === "html" || language === "angular") {
return formatHtml(path, print, textToDoc, options, { parser: language });
}
}
/**
* md`...`
* markdown`...`
*/
function isMarkdown(path) {
const node = path.getValue();
const parent = path.getParentNode();
return (
parent &&
parent.type === "TaggedTemplateExpression" &&
node.quasis.length === 1 &&
parent.tag.type === "Identifier" &&
(parent.tag.name === "md" || parent.tag.name === "markdown")
);
}
/**
* Template literal in these contexts:
* <style jsx>{`div{color:red}`}</style>
* css``
* css.global``
* css.resolve``
*/
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"
)) ||
(parent &&
parent.type === "TaggedTemplateExpression" &&
parent.tag.type === "Identifier" &&
parent.tag.name === "css") ||
(parent &&
parent.type === "TaggedTemplateExpression" &&
parent.tag.type === "MemberExpression" &&
parent.tag.object.name === "css" &&
(parent.tag.property.name === "global" ||
parent.tag.property.name === "resolve"))
);
}
/**
* Angular Components can have:
* - Inline HTML template
* - Inline CSS styles
*
* ...which are both within template literals somewhere
* inside of the Component decorator factory.
*
* E.g.
* @Component({
* template: `<div>...</div>`,
* styles: [`h1 { color: blue; }`]
* })
*/
function isAngularComponentStyles(path) {
return path.match(
(node) => node.type === "TemplateLiteral",
(node, name) => node.type === "ArrayExpression" && name === "elements",
(node, name) =>
isObjectProperty(node) &&
node.key.type === "Identifier" &&
node.key.name === "styles" &&
name === "value",
...angularComponentObjectExpressionPredicates
);
}
function isAngularComponentTemplate(path) {
return path.match(
(node) => node.type === "TemplateLiteral",
(node, name) =>
isObjectProperty(node) &&
node.key.type === "Identifier" &&
node.key.name === "template" &&
name === "value",
...angularComponentObjectExpressionPredicates
);
}
const angularComponentObjectExpressionPredicates = [
(node, name) => node.type === "ObjectExpression" && name === "properties",
(node, name) =>
node.type === "CallExpression" &&
node.callee.type === "Identifier" &&
node.callee.name === "Component" &&
name === "arguments",
(node, name) => node.type === "Decorator" && name === "expression",
];
/**
* styled-components template literals
*/
function isStyledComponents(path) {
const parent = path.getParentNode();
if (!parent || parent.type !== "TaggedTemplateExpression") {
return false;
}
const { tag } = parent;
switch (tag.type) {
case "MemberExpression":
return (
// styled.foo``
isStyledIdentifier(tag.object) ||
// Component.extend``
isStyledExtend(tag)
);
case "CallExpression":
return (
// styled(Component)``
isStyledIdentifier(tag.callee) ||
(tag.callee.type === "MemberExpression" &&
((tag.callee.object.type === "MemberExpression" &&
// styled.foo.attrs({})``
(isStyledIdentifier(tag.callee.object.object) ||
// Component.extend.attrs({})``
isStyledExtend(tag.callee.object))) ||
// styled(Component).attrs({})``
(tag.callee.object.type === "CallExpression" &&
isStyledIdentifier(tag.callee.object.callee))))
);
case "Identifier":
// css``
return tag.name === "css";
default:
return false;
}
}
/**
* JSX element with CSS prop
*/
function isCssProp(path) {
const parent = path.getParentNode();
const parentParent = path.getParentNode(1);
return (
parentParent &&
parent.type === "JSXExpressionContainer" &&
parentParent.type === "JSXAttribute" &&
parentParent.name.type === "JSXIdentifier" &&
parentParent.name.name === "css"
);
}
function isStyledIdentifier(node) {
return node.type === "Identifier" && node.name === "styled";
}
function isStyledExtend(node) {
return /^[A-Z]/.test(node.object.name) && node.property.name === "extend";
}
/*
* react-relay and graphql-tag
* graphql`...`
* graphql.experimental`...`
* gql`...`
* GraphQL comment block
*
* This intentionally excludes Relay Classic tags, as Prettier does not
* support Relay Classic formatting.
*/
function isGraphQL(path) {
const node = path.getValue();
const parent = path.getParentNode();
return (
hasLanguageComment(node, "GraphQL") ||
(parent &&
((parent.type === "TaggedTemplateExpression" &&
((parent.tag.type === "MemberExpression" &&
parent.tag.object.name === "graphql" &&
parent.tag.property.name === "experimental") ||
(parent.tag.type === "Identifier" &&
(parent.tag.name === "gql" || parent.tag.name === "graphql")))) ||
(parent.type === "CallExpression" &&
parent.callee.type === "Identifier" &&
parent.callee.name === "graphql")))
);
}
function hasLanguageComment(node, languageName) {
// This checks for a leading comment that is exactly `/* GraphQL */`
// In order to be in line with other implementations of this comment tag
// we will not trim the comment value and we will expect exactly one space on
// either side of the GraphQL string
// Also see ./clean.js
return hasComment(
node,
CommentCheckFlags.Block | CommentCheckFlags.Leading,
({ value }) => value === ` ${languageName} `
);
}
/**
* - html`...`
* - HTML comment block
*/
function isHtml(path) {
return (
hasLanguageComment(path.getValue(), "HTML") ||
path.match(
(node) => node.type === "TemplateLiteral",
(node, name) =>
node.type === "TaggedTemplateExpression" &&
node.tag.type === "Identifier" &&
node.tag.name === "html" &&
name === "quasi"
)
);
}
function hasInvalidCookedValue({ quasis }) {
return quasis.some(({ value: { cooked } }) => cooked === null);
}
module.exports = embed;