"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 };