737 lines
20 KiB
JavaScript
737 lines
20 KiB
JavaScript
"use strict";
|
|
|
|
const { insertPragma, isPragma } = require("./pragma");
|
|
const {
|
|
getAncestorCount,
|
|
getBlockValueLineContents,
|
|
getFlowScalarLineContents,
|
|
getLast,
|
|
getLastDescendantNode,
|
|
hasLeadingComments,
|
|
hasMiddleComments,
|
|
hasIndicatorComment,
|
|
hasTrailingComment,
|
|
hasEndComments,
|
|
hasPrettierIgnore,
|
|
isLastDescendantNode,
|
|
isNextLineEmpty,
|
|
isNode,
|
|
isEmptyNode,
|
|
defineShortcut,
|
|
mapNode
|
|
} = require("./utils");
|
|
const docBuilders = require("../doc").builders;
|
|
const {
|
|
conditionalGroup,
|
|
breakParent,
|
|
concat,
|
|
dedent,
|
|
dedentToRoot,
|
|
fill,
|
|
group,
|
|
hardline,
|
|
ifBreak,
|
|
join,
|
|
line,
|
|
lineSuffix,
|
|
literalline,
|
|
markAsRoot,
|
|
softline
|
|
} = docBuilders;
|
|
const { replaceEndOfLineWith } = require("../common/util");
|
|
|
|
function preprocess(ast) {
|
|
return mapNode(ast, defineShortcuts);
|
|
}
|
|
|
|
function defineShortcuts(node) {
|
|
switch (node.type) {
|
|
case "document":
|
|
defineShortcut(node, "head", () => node.children[0]);
|
|
defineShortcut(node, "body", () => node.children[1]);
|
|
break;
|
|
case "documentBody":
|
|
case "sequenceItem":
|
|
case "flowSequenceItem":
|
|
case "mappingKey":
|
|
case "mappingValue":
|
|
defineShortcut(node, "content", () => node.children[0]);
|
|
break;
|
|
case "mappingItem":
|
|
case "flowMappingItem":
|
|
defineShortcut(node, "key", () => node.children[0]);
|
|
defineShortcut(node, "value", () => node.children[1]);
|
|
break;
|
|
}
|
|
return node;
|
|
}
|
|
|
|
function genericPrint(path, options, print) {
|
|
const node = path.getValue();
|
|
const parentNode = path.getParentNode();
|
|
|
|
const tag = !node.tag ? "" : path.call(print, "tag");
|
|
const anchor = !node.anchor ? "" : path.call(print, "anchor");
|
|
|
|
const nextEmptyLine =
|
|
isNode(node, [
|
|
"mapping",
|
|
"sequence",
|
|
"comment",
|
|
"directive",
|
|
"mappingItem",
|
|
"sequenceItem"
|
|
]) && !isLastDescendantNode(path)
|
|
? printNextEmptyLine(path, options.originalText)
|
|
: "";
|
|
|
|
return concat([
|
|
node.type !== "mappingValue" && hasLeadingComments(node)
|
|
? concat([join(hardline, path.map(print, "leadingComments")), hardline])
|
|
: "",
|
|
tag,
|
|
tag && anchor ? " " : "",
|
|
anchor,
|
|
tag || anchor
|
|
? isNode(node, ["sequence", "mapping"]) && !hasMiddleComments(node)
|
|
? hardline
|
|
: " "
|
|
: "",
|
|
hasMiddleComments(node)
|
|
? concat([
|
|
node.middleComments.length === 1 ? "" : hardline,
|
|
join(hardline, path.map(print, "middleComments")),
|
|
hardline
|
|
])
|
|
: "",
|
|
hasPrettierIgnore(path)
|
|
? concat(
|
|
replaceEndOfLineWith(
|
|
options.originalText.slice(
|
|
node.position.start.offset,
|
|
node.position.end.offset
|
|
),
|
|
literalline
|
|
)
|
|
)
|
|
: group(_print(node, parentNode, path, options, print)),
|
|
hasTrailingComment(node) && !isNode(node, ["document", "documentHead"])
|
|
? lineSuffix(
|
|
concat([
|
|
node.type === "mappingValue" && !node.content ? "" : " ",
|
|
parentNode.type === "mappingKey" &&
|
|
path.getParentNode(2).type === "mapping" &&
|
|
isInlineNode(node)
|
|
? ""
|
|
: breakParent,
|
|
path.call(print, "trailingComment")
|
|
])
|
|
)
|
|
: "",
|
|
nextEmptyLine,
|
|
hasEndComments(node) && !isNode(node, ["documentHead", "documentBody"])
|
|
? align(
|
|
node.type === "sequenceItem" ? 2 : 0,
|
|
concat([hardline, join(hardline, path.map(print, "endComments"))])
|
|
)
|
|
: ""
|
|
]);
|
|
}
|
|
|
|
function _print(node, parentNode, path, options, print) {
|
|
switch (node.type) {
|
|
case "root":
|
|
return concat([
|
|
join(
|
|
hardline,
|
|
path.map((childPath, index) => {
|
|
const document = node.children[index];
|
|
const nextDocument = node.children[index + 1];
|
|
return concat([
|
|
print(childPath),
|
|
shouldPrintDocumentEndMarker(document, nextDocument)
|
|
? concat([
|
|
hardline,
|
|
"...",
|
|
hasTrailingComment(document)
|
|
? concat([" ", path.call(print, "trailingComment")])
|
|
: ""
|
|
])
|
|
: !nextDocument || hasTrailingComment(nextDocument.head)
|
|
? ""
|
|
: concat([hardline, "---"])
|
|
]);
|
|
}, "children")
|
|
),
|
|
node.children.length === 0 ||
|
|
(lastDescendantNode =>
|
|
isNode(lastDescendantNode, ["blockLiteral", "blockFolded"]) &&
|
|
lastDescendantNode.chomping === "keep")(getLastDescendantNode(node))
|
|
? ""
|
|
: hardline
|
|
]);
|
|
case "document": {
|
|
const nextDocument = parentNode.children[path.getName() + 1];
|
|
return join(
|
|
hardline,
|
|
[
|
|
shouldPrintDocumentHeadEndMarker(
|
|
node,
|
|
nextDocument,
|
|
parentNode,
|
|
options
|
|
) === "head"
|
|
? join(
|
|
hardline,
|
|
[
|
|
node.head.children.length === 0 &&
|
|
node.head.endComments.length === 0
|
|
? ""
|
|
: path.call(print, "head"),
|
|
concat([
|
|
"---",
|
|
hasTrailingComment(node.head)
|
|
? concat([
|
|
" ",
|
|
path.call(print, "head", "trailingComment")
|
|
])
|
|
: ""
|
|
])
|
|
].filter(Boolean)
|
|
)
|
|
: "",
|
|
shouldPrintDocumentBody(node) ? path.call(print, "body") : ""
|
|
].filter(Boolean)
|
|
);
|
|
}
|
|
case "documentHead":
|
|
return join(
|
|
hardline,
|
|
[].concat(path.map(print, "children"), path.map(print, "endComments"))
|
|
);
|
|
case "documentBody": {
|
|
const children = join(hardline, path.map(print, "children")).parts;
|
|
const endComments = join(hardline, path.map(print, "endComments")).parts;
|
|
const separator =
|
|
children.length === 0 || endComments.length === 0
|
|
? ""
|
|
: (lastDescendantNode =>
|
|
isNode(lastDescendantNode, ["blockFolded", "blockLiteral"])
|
|
? lastDescendantNode.chomping === "keep"
|
|
? // there's already a newline printed at the end of blockValue (chomping=keep, lastDescendant=true)
|
|
""
|
|
: // an extra newline for better readability
|
|
concat([hardline, hardline])
|
|
: hardline)(getLastDescendantNode(node));
|
|
return concat([].concat(children, separator, endComments));
|
|
}
|
|
case "directive":
|
|
return concat(["%", join(" ", [node.name].concat(node.parameters))]);
|
|
case "comment":
|
|
return concat(["#", node.value]);
|
|
case "alias":
|
|
return concat(["*", node.value]);
|
|
case "tag":
|
|
return options.originalText.slice(
|
|
node.position.start.offset,
|
|
node.position.end.offset
|
|
);
|
|
case "anchor":
|
|
return concat(["&", node.value]);
|
|
case "plain":
|
|
return printFlowScalarContent(
|
|
node.type,
|
|
options.originalText.slice(
|
|
node.position.start.offset,
|
|
node.position.end.offset
|
|
),
|
|
options
|
|
);
|
|
case "quoteDouble":
|
|
case "quoteSingle": {
|
|
const singleQuote = "'";
|
|
const doubleQuote = '"';
|
|
|
|
const raw = options.originalText.slice(
|
|
node.position.start.offset + 1,
|
|
node.position.end.offset - 1
|
|
);
|
|
|
|
if (
|
|
(node.type === "quoteSingle" && raw.includes("\\")) ||
|
|
(node.type === "quoteDouble" && /\\[^"]/.test(raw))
|
|
) {
|
|
// only quoteDouble can use escape chars
|
|
// and quoteSingle do not need to escape backslashes
|
|
const originalQuote =
|
|
node.type === "quoteDouble" ? doubleQuote : singleQuote;
|
|
return concat([
|
|
originalQuote,
|
|
printFlowScalarContent(node.type, raw, options),
|
|
originalQuote
|
|
]);
|
|
} else if (raw.includes(doubleQuote)) {
|
|
return concat([
|
|
singleQuote,
|
|
printFlowScalarContent(
|
|
node.type,
|
|
node.type === "quoteDouble"
|
|
? raw
|
|
// double quote needs to be escaped by backslash in quoteDouble
|
|
.replace(/\\"/g, doubleQuote)
|
|
.replace(/'/g, singleQuote.repeat(2))
|
|
: raw,
|
|
options
|
|
),
|
|
singleQuote
|
|
]);
|
|
}
|
|
|
|
if (raw.includes(singleQuote)) {
|
|
return concat([
|
|
doubleQuote,
|
|
printFlowScalarContent(
|
|
node.type,
|
|
node.type === "quoteSingle"
|
|
? // single quote needs to be escaped by 2 single quotes in quoteSingle
|
|
raw.replace(/''/g, singleQuote)
|
|
: raw,
|
|
options
|
|
),
|
|
doubleQuote
|
|
]);
|
|
}
|
|
|
|
const quote = options.singleQuote ? singleQuote : doubleQuote;
|
|
return concat([
|
|
quote,
|
|
printFlowScalarContent(node.type, raw, options),
|
|
quote
|
|
]);
|
|
}
|
|
case "blockFolded":
|
|
case "blockLiteral": {
|
|
const parentIndent = getAncestorCount(path, ancestorNode =>
|
|
isNode(ancestorNode, ["sequence", "mapping"])
|
|
);
|
|
const isLastDescendant = isLastDescendantNode(path);
|
|
return concat([
|
|
node.type === "blockFolded" ? ">" : "|",
|
|
node.indent === null ? "" : node.indent.toString(),
|
|
node.chomping === "clip" ? "" : node.chomping === "keep" ? "+" : "-",
|
|
hasIndicatorComment(node)
|
|
? concat([" ", path.call(print, "indicatorComment")])
|
|
: "",
|
|
(node.indent === null ? dedent : dedentToRoot)(
|
|
align(
|
|
node.indent === null
|
|
? options.tabWidth
|
|
: node.indent - 1 + parentIndent,
|
|
concat(
|
|
getBlockValueLineContents(node, {
|
|
parentIndent,
|
|
isLastDescendant,
|
|
options
|
|
}).reduce(
|
|
(reduced, lineWords, index, lineContents) =>
|
|
reduced.concat(
|
|
index === 0 ? hardline : "",
|
|
fill(join(line, lineWords).parts),
|
|
index !== lineContents.length - 1
|
|
? lineWords.length === 0
|
|
? hardline
|
|
: markAsRoot(literalline)
|
|
: node.chomping === "keep" && isLastDescendant
|
|
? lineWords.length === 0
|
|
? dedentToRoot(hardline)
|
|
: dedentToRoot(literalline)
|
|
: ""
|
|
),
|
|
[]
|
|
)
|
|
)
|
|
)
|
|
)
|
|
]);
|
|
}
|
|
case "sequence":
|
|
return join(hardline, path.map(print, "children"));
|
|
case "sequenceItem":
|
|
return concat([
|
|
"- ",
|
|
align(2, !node.content ? "" : path.call(print, "content"))
|
|
]);
|
|
case "mappingKey":
|
|
return !node.content ? "" : path.call(print, "content");
|
|
case "mappingValue":
|
|
return !node.content ? "" : path.call(print, "content");
|
|
case "mapping":
|
|
return join(hardline, path.map(print, "children"));
|
|
case "mappingItem":
|
|
case "flowMappingItem": {
|
|
const isEmptyMappingKey = isEmptyNode(node.key);
|
|
const isEmptyMappingValue = isEmptyNode(node.value);
|
|
|
|
if (isEmptyMappingKey && isEmptyMappingValue) {
|
|
return concat([": "]);
|
|
}
|
|
|
|
const key = path.call(print, "key");
|
|
const value = path.call(print, "value");
|
|
|
|
if (isEmptyMappingValue) {
|
|
return node.type === "flowMappingItem" &&
|
|
parentNode.type === "flowMapping"
|
|
? key
|
|
: node.type === "mappingItem" &&
|
|
isAbsolutelyPrintedAsSingleLineNode(node.key.content, options) &&
|
|
!hasTrailingComment(node.key.content) &&
|
|
(!parentNode.tag ||
|
|
parentNode.tag.value !== "tag:yaml.org,2002:set")
|
|
? concat([key, needsSpaceInFrontOfMappingValue(node) ? " " : "", ":"])
|
|
: concat(["? ", align(2, key)]);
|
|
}
|
|
|
|
if (isEmptyMappingKey) {
|
|
return concat([": ", align(2, value)]);
|
|
}
|
|
|
|
const groupId = Symbol("mappingKey");
|
|
|
|
const forceExplicitKey =
|
|
hasLeadingComments(node.value) || !isInlineNode(node.key.content);
|
|
|
|
return forceExplicitKey
|
|
? concat([
|
|
"? ",
|
|
align(2, key),
|
|
hardline,
|
|
join(
|
|
"",
|
|
path
|
|
.map(print, "value", "leadingComments")
|
|
.map(comment => concat([comment, hardline]))
|
|
),
|
|
": ",
|
|
align(2, value)
|
|
])
|
|
: // force singleline
|
|
isSingleLineNode(node.key.content) &&
|
|
!hasLeadingComments(node.key.content) &&
|
|
!hasMiddleComments(node.key.content) &&
|
|
!hasTrailingComment(node.key.content) &&
|
|
!hasEndComments(node.key) &&
|
|
!hasLeadingComments(node.value.content) &&
|
|
!hasMiddleComments(node.value.content) &&
|
|
!hasEndComments(node.value) &&
|
|
isAbsolutelyPrintedAsSingleLineNode(node.value.content, options)
|
|
? concat([
|
|
key,
|
|
needsSpaceInFrontOfMappingValue(node) ? " " : "",
|
|
": ",
|
|
value
|
|
])
|
|
: conditionalGroup([
|
|
concat([
|
|
group(
|
|
concat([ifBreak("? "), group(align(2, key), { id: groupId })])
|
|
),
|
|
ifBreak(
|
|
concat([hardline, ": ", align(2, value)]),
|
|
indent(
|
|
concat([
|
|
needsSpaceInFrontOfMappingValue(node) ? " " : "",
|
|
":",
|
|
hasLeadingComments(node.value.content) ||
|
|
(hasEndComments(node.value) &&
|
|
node.value.content &&
|
|
!isNode(node.value.content, ["mapping", "sequence"])) ||
|
|
(parentNode.type === "mapping" &&
|
|
hasTrailingComment(node.key.content) &&
|
|
isInlineNode(node.value.content)) ||
|
|
(isNode(node.value.content, ["mapping", "sequence"]) &&
|
|
node.value.content.tag === null &&
|
|
node.value.content.anchor === null)
|
|
? hardline
|
|
: !node.value.content
|
|
? ""
|
|
: line,
|
|
value
|
|
])
|
|
),
|
|
{ groupId }
|
|
)
|
|
])
|
|
]);
|
|
}
|
|
case "flowMapping":
|
|
case "flowSequence": {
|
|
const openMarker = node.type === "flowMapping" ? "{" : "[";
|
|
const closeMarker = node.type === "flowMapping" ? "}" : "]";
|
|
const bracketSpacing =
|
|
node.type === "flowMapping" &&
|
|
node.children.length !== 0 &&
|
|
options.bracketSpacing
|
|
? line
|
|
: softline;
|
|
const isLastItemEmptyMappingItem =
|
|
node.children.length !== 0 &&
|
|
(lastItem =>
|
|
lastItem.type === "flowMappingItem" &&
|
|
isEmptyNode(lastItem.key) &&
|
|
isEmptyNode(lastItem.value))(getLast(node.children));
|
|
return concat([
|
|
openMarker,
|
|
indent(
|
|
concat([
|
|
bracketSpacing,
|
|
concat(
|
|
path.map(
|
|
(childPath, index) =>
|
|
concat([
|
|
print(childPath),
|
|
index === node.children.length - 1
|
|
? ""
|
|
: concat([
|
|
",",
|
|
line,
|
|
node.children[index].position.start.line !==
|
|
node.children[index + 1].position.start.line
|
|
? printNextEmptyLine(
|
|
childPath,
|
|
options.originalText
|
|
)
|
|
: ""
|
|
])
|
|
]),
|
|
"children"
|
|
)
|
|
),
|
|
ifBreak(",", "")
|
|
])
|
|
),
|
|
isLastItemEmptyMappingItem ? "" : bracketSpacing,
|
|
closeMarker
|
|
]);
|
|
}
|
|
case "flowSequenceItem":
|
|
return path.call(print, "content");
|
|
// istanbul ignore next
|
|
default:
|
|
throw new Error(`Unexpected node type ${node.type}`);
|
|
}
|
|
|
|
function indent(doc) {
|
|
return docBuilders.align(" ".repeat(options.tabWidth), doc);
|
|
}
|
|
}
|
|
|
|
function align(n, doc) {
|
|
return typeof n === "number" && n > 0
|
|
? docBuilders.align(" ".repeat(n), doc)
|
|
: docBuilders.align(n, doc);
|
|
}
|
|
|
|
function isInlineNode(node) {
|
|
if (!node) {
|
|
return true;
|
|
}
|
|
|
|
switch (node.type) {
|
|
case "plain":
|
|
case "quoteDouble":
|
|
case "quoteSingle":
|
|
case "alias":
|
|
case "flowMapping":
|
|
case "flowSequence":
|
|
return true;
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function isSingleLineNode(node) {
|
|
if (!node) {
|
|
return true;
|
|
}
|
|
|
|
switch (node.type) {
|
|
case "plain":
|
|
case "quoteDouble":
|
|
case "quoteSingle":
|
|
return node.position.start.line === node.position.end.line;
|
|
case "alias":
|
|
return true;
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function shouldPrintDocumentBody(document) {
|
|
return document.body.children.length !== 0 || hasEndComments(document.body);
|
|
}
|
|
|
|
function shouldPrintDocumentEndMarker(document, nextDocument) {
|
|
return (
|
|
/**
|
|
*... # trailingComment
|
|
*/
|
|
hasTrailingComment(document) ||
|
|
(nextDocument &&
|
|
/**
|
|
* ...
|
|
* %DIRECTIVE
|
|
* ---
|
|
*/
|
|
(nextDocument.head.children.length !== 0 ||
|
|
/**
|
|
* ...
|
|
* # endComment
|
|
* ---
|
|
*/
|
|
hasEndComments(nextDocument.head)))
|
|
);
|
|
}
|
|
|
|
function shouldPrintDocumentHeadEndMarker(
|
|
document,
|
|
nextDocument,
|
|
root,
|
|
options
|
|
) {
|
|
if (
|
|
/**
|
|
* ---
|
|
* preserve the first document head end marker
|
|
*/
|
|
(root.children[0] === document &&
|
|
/---(\s|$)/.test(
|
|
options.originalText.slice(
|
|
options.locStart(document),
|
|
options.locStart(document) + 4
|
|
)
|
|
)) ||
|
|
/**
|
|
* %DIRECTIVE
|
|
* ---
|
|
*/
|
|
document.head.children.length !== 0 ||
|
|
/**
|
|
* # end comment
|
|
* ---
|
|
*/
|
|
hasEndComments(document.head) ||
|
|
/**
|
|
* --- # trailing comment
|
|
*/
|
|
hasTrailingComment(document.head)
|
|
) {
|
|
return "head";
|
|
}
|
|
|
|
if (shouldPrintDocumentEndMarker(document, nextDocument)) {
|
|
return false;
|
|
}
|
|
|
|
return nextDocument ? "root" : false;
|
|
}
|
|
|
|
function isAbsolutelyPrintedAsSingleLineNode(node, options) {
|
|
if (!node) {
|
|
return true;
|
|
}
|
|
|
|
switch (node.type) {
|
|
case "plain":
|
|
case "quoteSingle":
|
|
case "quoteDouble":
|
|
break;
|
|
case "alias":
|
|
return true;
|
|
default:
|
|
return false;
|
|
}
|
|
|
|
if (options.proseWrap === "preserve") {
|
|
return node.position.start.line === node.position.end.line;
|
|
}
|
|
|
|
if (
|
|
// backslash-newline
|
|
/\\$/m.test(
|
|
options.originalText.slice(
|
|
node.position.start.offset,
|
|
node.position.end.offset
|
|
)
|
|
)
|
|
) {
|
|
return false;
|
|
}
|
|
|
|
switch (options.proseWrap) {
|
|
case "never":
|
|
return node.value.indexOf("\n") === -1;
|
|
case "always":
|
|
return !/[\n ]/.test(node.value);
|
|
// istanbul ignore next
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function needsSpaceInFrontOfMappingValue(node) {
|
|
return node.key.content && node.key.content.type === "alias";
|
|
}
|
|
|
|
function printNextEmptyLine(path, originalText) {
|
|
const node = path.getValue();
|
|
const root = path.stack[0];
|
|
|
|
root.isNextEmptyLinePrintedChecklist =
|
|
root.isNextEmptyLinePrintedChecklist || [];
|
|
|
|
if (!root.isNextEmptyLinePrintedChecklist[node.position.end.line]) {
|
|
if (isNextLineEmpty(node, originalText)) {
|
|
root.isNextEmptyLinePrintedChecklist[node.position.end.line] = true;
|
|
return softline;
|
|
}
|
|
}
|
|
|
|
return "";
|
|
}
|
|
|
|
function printFlowScalarContent(nodeType, content, options) {
|
|
const lineContents = getFlowScalarLineContents(nodeType, content, options);
|
|
return join(
|
|
hardline,
|
|
lineContents.map(lineContentWords =>
|
|
fill(join(line, lineContentWords).parts)
|
|
)
|
|
);
|
|
}
|
|
|
|
function clean(node, newNode /*, parent */) {
|
|
if (isNode(newNode)) {
|
|
delete newNode.position;
|
|
switch (newNode.type) {
|
|
case "comment":
|
|
// insert pragma
|
|
if (isPragma(newNode.value)) {
|
|
return null;
|
|
}
|
|
break;
|
|
case "quoteDouble":
|
|
case "quoteSingle":
|
|
newNode.type = "quote";
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
module.exports = {
|
|
preprocess,
|
|
print: genericPrint,
|
|
massageAstNode: clean,
|
|
insertPragma
|
|
};
|