prettier/src/language-handlebars/printer-glimmer.js

381 lines
10 KiB
JavaScript

"use strict";
const {
concat,
join,
softline,
hardline,
line,
group,
indent,
ifBreak
} = require("../doc").builders;
// http://w3c.github.io/html/single-page.html#void-elements
const voidTags = [
"area",
"base",
"br",
"col",
"embed",
"hr",
"img",
"input",
"link",
"meta",
"param",
"source",
"track",
"wbr"
];
// Formatter based on @glimmerjs/syntax's built-in test formatter:
// https://github.com/glimmerjs/glimmer-vm/blob/master/packages/%40glimmer/syntax/lib/generation/print.ts
function print(path, options, print) {
const n = path.getValue();
/* istanbul ignore if*/
if (!n) {
return "";
}
switch (n.type) {
case "Program": {
return group(
join(softline, path.map(print, "body").filter(text => text !== ""))
);
}
case "ElementNode": {
const tagFirstChar = n.tag[0];
const isLocal = n.tag.indexOf(".") !== -1;
const isGlimmerComponent =
tagFirstChar.toUpperCase() === tagFirstChar || isLocal;
const hasChildren = n.children.length > 0;
const isVoid =
(isGlimmerComponent && !hasChildren) || voidTags.indexOf(n.tag) !== -1;
const closeTag = isVoid ? concat([" />", softline]) : ">";
const getParams = (path, print) =>
indent(
concat([
n.attributes.length ? line : "",
join(line, path.map(print, "attributes")),
n.modifiers.length ? line : "",
join(line, path.map(print, "modifiers")),
n.comments.length ? line : "",
join(line, path.map(print, "comments"))
])
);
// The problem here is that I want to not break at all if the children
// would not break but I need to force an indent, so I use a hardline.
/**
* What happens now:
* <div>
* Hello
* </div>
* ==>
* <div>Hello</div>
* This is due to me using hasChildren to decide to put the hardline in.
* I would rather use a {DOES THE WHOLE THING NEED TO BREAK}
*/
return concat([
group(
concat([
"<",
n.tag,
getParams(path, print),
n.blockParams.length ? ` as |${n.blockParams.join(" ")}|` : "",
ifBreak(softline, ""),
closeTag
])
),
group(
concat([
indent(join(softline, [""].concat(path.map(print, "children")))),
ifBreak(hasChildren ? hardline : "", ""),
!isVoid ? concat(["</", n.tag, ">"]) : ""
])
)
]);
}
case "BlockStatement": {
const pp = path.getParentNode(1);
const isElseIf =
pp &&
pp.inverse &&
pp.inverse.body[0] === n &&
pp.inverse.body[0].path.parts[0] === "if";
const hasElseIf =
n.inverse &&
n.inverse.body[0] &&
n.inverse.body[0].type === "BlockStatement" &&
n.inverse.body[0].path.parts[0] === "if";
const indentElse = hasElseIf ? a => a : indent;
if (n.inverse) {
return concat([
isElseIf
? concat(["{{else ", printPathParams(path, print), "}}"])
: printOpenBlock(path, print),
indent(concat([hardline, path.call(print, "program")])),
n.inverse && !hasElseIf ? concat([hardline, "{{else}}"]) : "",
n.inverse
? indentElse(concat([hardline, path.call(print, "inverse")]))
: "",
isElseIf ? "" : concat([hardline, printCloseBlock(path, print)])
]);
} else if (isElseIf) {
return concat([
concat(["{{else ", printPathParams(path, print), "}}"]),
indent(concat([hardline, path.call(print, "program")]))
]);
}
/**
* I want this boolean to be: if params are going to cause a break,
* not that it has params.
*/
const hasParams = n.params.length > 0 || n.hash.pairs.length > 0;
const hasChildren = n.program.body.length > 0;
return concat([
printOpenBlock(path, print),
group(
concat([
indent(concat([softline, path.call(print, "program")])),
hasParams && hasChildren ? hardline : softline,
printCloseBlock(path, print)
])
)
]);
}
case "ElementModifierStatement":
case "MustacheStatement": {
const pp = path.getParentNode(1);
const isConcat = pp && pp.type === "ConcatStatement";
return group(
concat([
n.escaped === false ? "{{{" : "{{",
printPathParams(path, print),
isConcat ? "" : softline,
n.escaped === false ? "}}}" : "}}"
])
);
}
case "SubExpression": {
const params = getParams(path, print);
const printedParams =
params.length > 0
? indent(concat([line, group(join(line, params))]))
: "";
return group(
concat(["(", printPath(path, print), printedParams, softline, ")"])
);
}
case "AttrNode": {
const isText = n.value.type === "TextNode";
if (isText && n.value.loc.start.column === n.value.loc.end.column) {
return concat([n.name]);
}
const quote = isText ? '"' : "";
return concat([n.name, "=", quote, path.call(print, "value"), quote]);
}
case "ConcatStatement": {
return concat([
'"',
group(
indent(
join(
softline,
path
.map(partPath => print(partPath), "parts")
.filter(a => a !== "")
)
)
),
'"'
]);
}
case "Hash": {
return concat([join(line, path.map(print, "pairs"))]);
}
case "HashPair": {
return concat([n.key, "=", path.call(print, "value")]);
}
case "TextNode": {
let leadingSpace = "";
let trailingSpace = "";
// preserve a space inside of an attribute node where whitespace present, when next to mustache statement.
const inAttrNode = path.stack.indexOf("attributes") >= 0;
if (inAttrNode) {
const parentNode = path.getParentNode(0);
const isConcat = parentNode.type === "ConcatStatement";
if (isConcat) {
const parts = parentNode.parts;
const partIndex = parts.indexOf(n);
if (partIndex > 0) {
const partType = parts[partIndex - 1].type;
const isMustache = partType === "MustacheStatement";
if (isMustache) {
leadingSpace = " ";
}
}
if (partIndex < parts.length - 1) {
const partType = parts[partIndex + 1].type;
const isMustache = partType === "MustacheStatement";
if (isMustache) {
trailingSpace = " ";
}
}
}
}
return n.chars
.replace(/^\s+/, leadingSpace)
.replace(/\s+$/, trailingSpace);
}
case "MustacheCommentStatement": {
const dashes = n.value.indexOf("}}") > -1 ? "--" : "";
return concat(["{{!", dashes, n.value, dashes, "}}"]);
}
case "PathExpression": {
return n.original;
}
case "BooleanLiteral": {
return String(n.value);
}
case "CommentStatement": {
return concat(["<!--", n.value, "-->"]);
}
case "StringLiteral": {
return printStringLiteral(n.value, options);
}
case "NumberLiteral": {
return String(n.value);
}
case "UndefinedLiteral": {
return "undefined";
}
case "NullLiteral": {
return "null";
}
/* istanbul ignore next */
default:
throw new Error("unknown glimmer type: " + JSON.stringify(n.type));
}
}
/**
* Prints a string literal with the correct surrounding quotes based on
* `options.singleQuote` and the number of escaped quotes contained in
* the string literal. This function is the glimmer equivalent of `printString`
* in `common/util`, but has differences because of the way escaped characters
* are treated in hbs string literals.
* @param {string} stringLiteral - the string literal value
* @param {object} options - the prettier options object
*/
function printStringLiteral(stringLiteral, options) {
const double = { quote: '"', regex: /"/g };
const single = { quote: "'", regex: /'/g };
const preferred = options.singleQuote ? single : double;
const alternate = preferred === single ? double : single;
let shouldUseAlternateQuote = false;
// If `stringLiteral` contains at least one of the quote preferred for
// enclosing the string, we might want to enclose with the alternate quote
// instead, to minimize the number of escaped quotes.
if (
stringLiteral.includes(preferred.quote) ||
stringLiteral.includes(alternate.quote)
) {
const numPreferredQuotes = (stringLiteral.match(preferred.regex) || [])
.length;
const numAlternateQuotes = (stringLiteral.match(alternate.regex) || [])
.length;
shouldUseAlternateQuote = numPreferredQuotes > numAlternateQuotes;
}
const enclosingQuote = shouldUseAlternateQuote ? alternate : preferred;
const escapedStringLiteral = stringLiteral.replace(
enclosingQuote.regex,
`\\${enclosingQuote.quote}`
);
return `${enclosingQuote.quote}${escapedStringLiteral}${
enclosingQuote.quote
}`;
}
function printPath(path, print) {
return path.call(print, "path");
}
function getParams(path, print) {
const node = path.getValue();
let parts = [];
if (node.params.length > 0) {
parts = parts.concat(path.map(print, "params"));
}
if (node.hash && node.hash.pairs.length > 0) {
parts.push(path.call(print, "hash"));
}
return parts;
}
function printPathParams(path, print) {
let parts = [];
parts.push(printPath(path, print));
parts = parts.concat(getParams(path, print));
return indent(group(join(line, parts)));
}
function printBlockParams(path) {
const block = path.getValue();
if (!block.program || !block.program.blockParams.length) {
return "";
}
return concat([" as |", block.program.blockParams.join(" "), "|"]);
}
function printOpenBlock(path, print) {
return group(
concat([
"{{#",
printPathParams(path, print),
printBlockParams(path),
softline,
"}}"
])
);
}
function printCloseBlock(path, print) {
return concat(["{{/", path.call(print, "path"), "}}"]);
}
function clean(ast, newObj) {
delete newObj.loc;
// (Glimmer/HTML) ignore TextNode whitespace
if (ast.type === "TextNode") {
if (ast.chars.replace(/\s+/, "") === "") {
return null;
}
newObj.chars = ast.chars.replace(/^\s+/, "").replace(/\s+$/, "");
}
}
module.exports = {
print,
massageAstNode: clean
};