553 lines
15 KiB
JavaScript
553 lines
15 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 printChildren(path, options, print) {
|
|
return concat(
|
|
path.map((childPath, childIndex) => {
|
|
const childNode = path.getValue();
|
|
const isFirstNode = childIndex === 0;
|
|
const isLastNode =
|
|
childIndex == path.getParentNode(0).children.length - 1;
|
|
const isLastNodeInMultiNodeList = isLastNode && !isFirstNode;
|
|
const isWhitespace = isWhitespaceNode(childNode);
|
|
|
|
if (isWhitespace && isLastNodeInMultiNodeList) {
|
|
return print(childPath, options, print);
|
|
} else if (isFirstNode) {
|
|
return concat([softline, print(childPath, options, print)]);
|
|
}
|
|
return print(childPath, options, print);
|
|
}, "children")
|
|
);
|
|
}
|
|
|
|
function print(path, options, print) {
|
|
const n = path.getValue();
|
|
|
|
/* istanbul ignore if*/
|
|
if (!n) {
|
|
return "";
|
|
}
|
|
|
|
switch (n.type) {
|
|
case "Block":
|
|
case "Program":
|
|
case "Template": {
|
|
return group(concat(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 hasNonWhitespaceChildren = n.children.some(
|
|
n => !isWhitespaceNode(n)
|
|
);
|
|
|
|
const isVoid =
|
|
(isGlimmerComponent && (!hasChildren || !hasNonWhitespaceChildren)) ||
|
|
voidTags.indexOf(n.tag) !== -1;
|
|
const closeTagForNoBreak = isVoid ? concat([" />", softline]) : ">";
|
|
const closeTagForBreak = isVoid ? "/>" : ">";
|
|
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"))
|
|
])
|
|
);
|
|
|
|
const nextNode = getNextNode(path);
|
|
|
|
return concat([
|
|
group(
|
|
concat([
|
|
"<",
|
|
n.tag,
|
|
getParams(path, print),
|
|
n.blockParams.length ? ` as |${n.blockParams.join(" ")}|` : "",
|
|
ifBreak(softline, ""),
|
|
ifBreak(closeTagForBreak, closeTagForNoBreak)
|
|
])
|
|
),
|
|
!isVoid
|
|
? group(
|
|
concat([
|
|
hasNonWhitespaceChildren
|
|
? indent(printChildren(path, options, print))
|
|
: "",
|
|
ifBreak(hasChildren ? hardline : "", ""),
|
|
concat(["</", n.tag, ">"])
|
|
])
|
|
)
|
|
: "",
|
|
nextNode && nextNode.type === "ElementNode" ? hardline : ""
|
|
]);
|
|
}
|
|
case "BlockStatement": {
|
|
const pp = path.getParentNode(1);
|
|
const isElseIf =
|
|
pp &&
|
|
pp.inverse &&
|
|
pp.inverse.body.length === 1 &&
|
|
pp.inverse.body[0] === n &&
|
|
pp.inverse.body[0].path.parts[0] === "if";
|
|
const hasElseIf =
|
|
n.inverse &&
|
|
n.inverse.body.length === 1 &&
|
|
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")]))
|
|
]);
|
|
}
|
|
|
|
const hasNonWhitespaceChildren = n.program.body.some(
|
|
n => !isWhitespaceNode(n)
|
|
);
|
|
|
|
return concat([
|
|
printOpenBlock(path, print),
|
|
group(
|
|
concat([
|
|
indent(concat([softline, path.call(print, "program")])),
|
|
hasNonWhitespaceChildren ? 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, { group: false }),
|
|
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 value = path.call(print, "value");
|
|
const quotedValue = isText
|
|
? printStringLiteral(value.parts.join(), options)
|
|
: value;
|
|
return concat([n.name, "=", quotedValue]);
|
|
}
|
|
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": {
|
|
const maxLineBreaksToPreserve = 2;
|
|
const isFirstElement = !getPreviousNode(path);
|
|
const isLastElement = !getNextNode(path);
|
|
const isWhitespaceOnly = !/\S/.test(n.chars);
|
|
const lineBreaksCount = countNewLines(n.chars);
|
|
const hasBlockParent = path.getParentNode(0).type === "Block";
|
|
const hasElementParent = path.getParentNode(0).type === "ElementNode";
|
|
const hasTemplateParent = path.getParentNode(0).type === "Template";
|
|
|
|
let leadingLineBreaksCount = countLeadingNewLines(n.chars);
|
|
let trailingLineBreaksCount = countTrailingNewLines(n.chars);
|
|
|
|
if (
|
|
(isFirstElement || isLastElement) &&
|
|
isWhitespaceOnly &&
|
|
(hasBlockParent || hasElementParent || hasTemplateParent)
|
|
) {
|
|
return "";
|
|
}
|
|
|
|
if (isWhitespaceOnly && lineBreaksCount) {
|
|
leadingLineBreaksCount = Math.min(
|
|
lineBreaksCount,
|
|
maxLineBreaksToPreserve
|
|
);
|
|
trailingLineBreaksCount = 0;
|
|
} else {
|
|
if (
|
|
isNextNodeOfType(path, "ElementNode") ||
|
|
isNextNodeOfType(path, "BlockStatement")
|
|
) {
|
|
trailingLineBreaksCount = Math.max(trailingLineBreaksCount, 1);
|
|
}
|
|
|
|
if (
|
|
isPreviousNodeOfSomeType(path, ["ElementNode"]) ||
|
|
isPreviousNodeOfSomeType(path, ["BlockStatement"])
|
|
) {
|
|
leadingLineBreaksCount = Math.max(leadingLineBreaksCount, 1);
|
|
}
|
|
}
|
|
|
|
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 = " ";
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
if (
|
|
trailingLineBreaksCount === 0 &&
|
|
isNextNodeOfType(path, "MustacheStatement")
|
|
) {
|
|
trailingSpace = " ";
|
|
}
|
|
|
|
if (
|
|
leadingLineBreaksCount === 0 &&
|
|
isPreviousNodeOfSomeType(path, ["MustacheStatement"])
|
|
) {
|
|
leadingSpace = " ";
|
|
}
|
|
|
|
if (isFirstElement) {
|
|
leadingLineBreaksCount = 0;
|
|
leadingSpace = "";
|
|
}
|
|
|
|
if (isLastElement) {
|
|
trailingLineBreaksCount = 0;
|
|
trailingSpace = "";
|
|
}
|
|
}
|
|
|
|
return concat(
|
|
[
|
|
...generateHardlines(leadingLineBreaksCount, maxLineBreaksToPreserve),
|
|
n.chars
|
|
.replace(/^[\s ]+/g, leadingSpace)
|
|
.replace(/[\s ]+$/, trailingSpace),
|
|
...generateHardlines(trailingLineBreaksCount, maxLineBreaksToPreserve)
|
|
].filter(Boolean)
|
|
);
|
|
}
|
|
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, options) {
|
|
let parts = [];
|
|
options = Object.assign({ group: true }, options || {});
|
|
|
|
parts.push(printPath(path, print));
|
|
parts = parts.concat(getParams(path, print));
|
|
|
|
if (!options.group) {
|
|
return indent(join(line, parts));
|
|
}
|
|
|
|
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 isWhitespaceNode(node) {
|
|
return node.type === "TextNode" && !/\S/.test(node.chars);
|
|
}
|
|
|
|
function getPreviousNode(path) {
|
|
const node = path.getValue();
|
|
const parentNode = path.getParentNode(0);
|
|
|
|
const children = parentNode.children || parentNode.body;
|
|
if (children) {
|
|
const nodeIndex = children.indexOf(node);
|
|
if (nodeIndex > 0) {
|
|
const previousNode = children[nodeIndex - 1];
|
|
return previousNode;
|
|
}
|
|
}
|
|
}
|
|
|
|
function getNextNode(path) {
|
|
const node = path.getValue();
|
|
const parentNode = path.getParentNode(0);
|
|
|
|
const children = parentNode.children || parentNode.body;
|
|
if (children) {
|
|
const nodeIndex = children.indexOf(node);
|
|
if (nodeIndex < children.length) {
|
|
const nextNode = children[nodeIndex + 1];
|
|
return nextNode;
|
|
}
|
|
}
|
|
}
|
|
|
|
function isPreviousNodeOfSomeType(path, types) {
|
|
const previousNode = getPreviousNode(path);
|
|
|
|
if (previousNode) {
|
|
return types.some(type => previousNode.type === type);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function isNextNodeOfType(path, type) {
|
|
const nextNode = getNextNode(path);
|
|
return nextNode && nextNode.type === type;
|
|
}
|
|
|
|
function clean(ast, newObj) {
|
|
delete newObj.loc;
|
|
delete newObj.selfClosing;
|
|
|
|
// (Glimmer/HTML) ignore TextNode whitespace
|
|
if (ast.type === "TextNode") {
|
|
if (ast.chars.replace(/\s+/, "") === "") {
|
|
return null;
|
|
}
|
|
newObj.chars = ast.chars.replace(/^\s+/, "").replace(/\s+$/, "");
|
|
}
|
|
}
|
|
|
|
function countNewLines(string) {
|
|
/* istanbul ignore next */
|
|
string = typeof string === "string" ? string : "";
|
|
return string.split("\n").length - 1;
|
|
}
|
|
|
|
function countLeadingNewLines(string) {
|
|
/* istanbul ignore next */
|
|
string = typeof string === "string" ? string : "";
|
|
const newLines = (string.match(/^([^\S\r\n]*[\r\n])+/g) || [])[0] || "";
|
|
return countNewLines(newLines);
|
|
}
|
|
|
|
function countTrailingNewLines(string) {
|
|
/* istanbul ignore next */
|
|
string = typeof string === "string" ? string : "";
|
|
const newLines = (string.match(/([\r\n][^\S\r\n]*)+$/g) || [])[0] || "";
|
|
return countNewLines(newLines);
|
|
}
|
|
|
|
function generateHardlines(number = 0, max = 0) {
|
|
return new Array(Math.min(number, max)).fill(hardline);
|
|
}
|
|
|
|
module.exports = {
|
|
print,
|
|
massageAstNode: clean
|
|
};
|