"use strict"; const stripBom = require("strip-bom"); const comments = require("./src/comments"); const version = require("./package.json").version; const printAstToDoc = require("./src/printer").printAstToDoc; const util = require("./src/util"); const printDocToString = require("./src/doc-printer").printDocToString; const normalizeOptions = require("./src/options").normalize; const parser = require("./src/parser"); const printDocToDebug = require("./src/doc-debug").printDocToDebug; const config = require("./src/resolve-config"); function guessLineEnding(text) { const index = text.indexOf("\n"); if (index >= 0 && text.charAt(index - 1) === "\r") { return "\r\n"; } return "\n"; } function attachComments(text, ast, opts) { const astComments = ast.comments; if (astComments) { delete ast.comments; comments.attach(astComments, ast, text, opts); } ast.tokens = []; opts.originalText = text.trimRight(); return astComments; } function ensureAllCommentsPrinted(astComments) { if (!astComments) { return; } for (let i = 0; i < astComments.length; ++i) { if (astComments[i].value.trim() === "prettier-ignore") { // If there's a prettier-ignore, we're not printing that sub-tree so we // don't know if the comments was printed or not. return; } } astComments.forEach(comment => { if (!comment.printed) { throw new Error( 'Comment "' + comment.value.trim() + '" was not printed. Please report this error!' ); } delete comment.printed; }); } function formatWithCursor(text, opts, addAlignmentSize) { text = stripBom(text); addAlignmentSize = addAlignmentSize || 0; const ast = parser.parse(text, opts); const formattedRangeOnly = formatRange(text, opts, ast); if (formattedRangeOnly) { return { formatted: formattedRangeOnly }; } let cursorOffset; if (opts.cursorOffset >= 0) { const cursorNodeAndParents = findNodeAtOffset(ast, opts.cursorOffset); const cursorNode = cursorNodeAndParents.node; if (cursorNode) { cursorOffset = opts.cursorOffset - util.locStart(cursorNode); opts.cursorNode = cursorNode; } } const astComments = attachComments(text, ast, opts); const doc = printAstToDoc(ast, opts, addAlignmentSize); opts.newLine = guessLineEnding(text); const toStringResult = printDocToString(doc, opts); const str = toStringResult.formatted; const cursorOffsetResult = toStringResult.cursor; ensureAllCommentsPrinted(astComments); // Remove extra leading indentation as well as the added indentation after last newline if (addAlignmentSize > 0) { return { formatted: str.trim() + opts.newLine }; } if (cursorOffset !== undefined) { return { formatted: str, cursorOffset: cursorOffsetResult + cursorOffset }; } return { formatted: str }; } function format(text, opts, addAlignmentSize) { return formatWithCursor(text, opts, addAlignmentSize).formatted; } function findSiblingAncestors(startNodeAndParents, endNodeAndParents) { let resultStartNode = startNodeAndParents.node; let resultEndNode = endNodeAndParents.node; if (resultStartNode === resultEndNode) { return { startNode: resultStartNode, endNode: resultEndNode }; } for (const endParent of endNodeAndParents.parentNodes) { if ( endParent.type !== "Program" && endParent.type !== "File" && util.locStart(endParent) >= util.locStart(startNodeAndParents.node) ) { resultEndNode = endParent; } else { break; } } for (const startParent of startNodeAndParents.parentNodes) { if ( startParent.type !== "Program" && startParent.type !== "File" && util.locEnd(startParent) <= util.locEnd(endNodeAndParents.node) ) { resultStartNode = startParent; } else { break; } } return { startNode: resultStartNode, endNode: resultEndNode }; } function findNodeAtOffset(node, offset, predicate, parentNodes) { predicate = predicate || (() => true); parentNodes = parentNodes || []; const start = util.locStart(node); const end = util.locEnd(node); if (start <= offset && offset <= end) { for (const childNode of comments.getSortedChildNodes(node)) { const childResult = findNodeAtOffset( childNode, offset, predicate, [node].concat(parentNodes) ); if (childResult) { return childResult; } } if (predicate(node)) { return { node: node, parentNodes: parentNodes }; } } } // See https://www.ecma-international.org/ecma-262/5.1/#sec-A.5 function isSourceElement(opts, node) { if (node == null) { return false; } switch (node.type || node.kind) { case "ObjectExpression": // JSON case "ArrayExpression": // JSON case "StringLiteral": // JSON case "NumericLiteral": // JSON case "BooleanLiteral": // JSON case "NullLiteral": // JSON case "json-identifier": // JSON return opts.parser === "json"; case "FunctionDeclaration": case "BlockStatement": case "BreakStatement": case "ContinueStatement": case "DebuggerStatement": case "DoWhileStatement": case "EmptyStatement": case "ExpressionStatement": case "ForInStatement": case "ForStatement": case "IfStatement": case "LabeledStatement": case "ReturnStatement": case "SwitchStatement": case "ThrowStatement": case "TryStatement": case "VariableDeclaration": case "WhileStatement": case "WithStatement": case "ClassDeclaration": // ES 2015 case "ImportDeclaration": // Module case "ExportDefaultDeclaration": // Module case "ExportNamedDeclaration": // Module case "ExportAllDeclaration": // Module case "TypeAlias": // Flow case "InterfaceDeclaration": // Flow, Typescript case "TypeAliasDeclaration": // Typescript case "ExportAssignment": // Typescript case "ExportDeclaration": // Typescript case "OperationDefinition": // GraphQL case "FragmentDefinition": // GraphQL case "VariableDefinition": // GraphQL case "TypeExtensionDefinition": // GraphQL case "ObjectTypeDefinition": // GraphQL case "FieldDefinition": // GraphQL case "DirectiveDefinition": // GraphQL case "EnumTypeDefinition": // GraphQL case "EnumValueDefinition": // GraphQL case "InputValueDefinition": // GraphQL case "InputObjectTypeDefinition": // GraphQL case "SchemaDefinition": // GraphQL case "OperationTypeDefinition": // GraphQL case "InterfaceTypeDefinition": // GraphQL case "UnionTypeDefinition": // GraphQL case "ScalarTypeDefinition": // GraphQL return true; } return false; } function calculateRange(text, opts, ast) { // Contract the range so that it has non-whitespace characters at its endpoints. // This ensures we can format a range that doesn't end on a node. const rangeStringOrig = text.slice(opts.rangeStart, opts.rangeEnd); const startNonWhitespace = Math.max( opts.rangeStart + rangeStringOrig.search(/\S/), opts.rangeStart ); let endNonWhitespace; for ( endNonWhitespace = opts.rangeEnd; endNonWhitespace > opts.rangeStart; --endNonWhitespace ) { if (text[endNonWhitespace - 1].match(/\S/)) { break; } } const startNodeAndParents = findNodeAtOffset(ast, startNonWhitespace, node => isSourceElement(opts, node) ); const endNodeAndParents = findNodeAtOffset(ast, endNonWhitespace, node => isSourceElement(opts, node) ); if (!startNodeAndParents || !endNodeAndParents) { return { rangeStart: 0, rangeEnd: 0 }; } const siblingAncestors = findSiblingAncestors( startNodeAndParents, endNodeAndParents ); const startNode = siblingAncestors.startNode; const endNode = siblingAncestors.endNode; const rangeStart = Math.min(util.locStart(startNode), util.locStart(endNode)); const rangeEnd = Math.max(util.locEnd(startNode), util.locEnd(endNode)); return { rangeStart: rangeStart, rangeEnd: rangeEnd }; } function formatRange(text, opts, ast) { if (opts.rangeStart <= 0 && text.length <= opts.rangeEnd) { return; } const range = calculateRange(text, opts, ast); const rangeStart = range.rangeStart; const rangeEnd = range.rangeEnd; const rangeString = text.slice(rangeStart, rangeEnd); // Try to extend the range backwards to the beginning of the line. // This is so we can detect indentation correctly and restore it. // Use `Math.min` since `lastIndexOf` returns 0 when `rangeStart` is 0 const rangeStart2 = Math.min( rangeStart, text.lastIndexOf("\n", rangeStart) + 1 ); const indentString = text.slice(rangeStart2, rangeStart); const alignmentSize = util.getAlignmentSize(indentString, opts.tabWidth); const rangeFormatted = format( rangeString, Object.assign({}, opts, { rangeStart: 0, rangeEnd: Infinity, printWidth: opts.printWidth - alignmentSize }), alignmentSize ); // Since the range contracts to avoid trailing whitespace, // we need to remove the newline that was inserted by the `format` call. const rangeTrimmed = rangeFormatted.trimRight(); return text.slice(0, rangeStart) + rangeTrimmed + text.slice(rangeEnd); } module.exports = { formatWithCursor: function(text, opts) { return formatWithCursor(text, normalizeOptions(opts)); }, format: function(text, opts) { return format(text, normalizeOptions(opts)); }, check: function(text, opts) { try { const formatted = format(text, normalizeOptions(opts)); return formatted === text; } catch (e) { return false; } }, resolveConfig: config.resolveConfig, clearConfigCache: config.clearCache, version, /* istanbul ignore next */ __debug: { parse: function(text, opts) { return parser.parse(text, opts); }, formatAST: function(ast, opts) { opts = normalizeOptions(opts); const doc = printAstToDoc(ast, opts); const str = printDocToString(doc, opts); return str; }, // Doesn't handle shebang for now formatDoc: function(doc, opts) { opts = normalizeOptions(opts); const debug = printDocToDebug(doc); const str = format(debug, opts); return str; }, printToDoc: function(text, opts) { opts = normalizeOptions(opts); const ast = parser.parse(text, opts); attachComments(text, ast, opts); const doc = printAstToDoc(ast, opts); return doc; }, printDocToString: function(doc, opts) { opts = normalizeOptions(opts); const str = printDocToString(doc, opts); return str; } } };