const { concat, group, hardline, ifBreak, indent, join, line } = require("../prettier"); const { getTrailingComma, prefix, skipAssignIndent } = require("../utils"); // When attempting to convert a hash rocket into a hash label, you need to take // care because only certain patterns are allowed. Ruby source says that they // have to match keyword arguments to methods, but don't specify what that is. // After some experimentation, it looks like it's: // // * Starts with a letter (either case) or an underscore // * Does not end in equal // // This function represents that check, as it determines if it can convert the // symbol node into a hash label. function isValidHashLabel(symbolLiteral) { const label = symbolLiteral.body[0].body; return label.match(/^[_A-Za-z]/) && !label.endsWith("="); } function canUseHashLabels(contentsNode) { return contentsNode.body.every((assocNode) => { if (assocNode.type === "assoc_splat") { return true; } switch (assocNode.body[0].type) { case "@label": return true; case "symbol_literal": return isValidHashLabel(assocNode.body[0]); case "dyna_symbol": return true; default: return false; } }); } function printHashKeyLabel(path, print) { const node = path.getValue(); switch (node.type) { case "@label": return print(path); case "symbol_literal": return concat([path.call(print, "body", 0), ":"]); case "dyna_symbol": return concat(print(path).parts.slice(1).concat(":")); } } function printHashKeyRocket(path, print) { const node = path.getValue(); const doc = print(path); if (node.type === "@label") { return `:${doc.slice(0, doc.length - 1)} =>`; } return concat([doc, " =>"]); } function printAssocNew(path, opts, print) { const { keyPrinter } = path.getParentNode(); const parts = [path.call((keyPath) => keyPrinter(keyPath, print), "body", 0)]; const valueDoc = path.call(print, "body", 1); if (skipAssignIndent(path.getValue().body[1])) { parts.push(" ", valueDoc); } else { parts.push(indent(concat([line, valueDoc]))); } return group(concat(parts)); } function printHashContents(path, opts, print) { const node = path.getValue(); // First determine which key printer we're going to use, so that the child // nodes can reference it when they go to get printed. node.keyPrinter = opts.rubyHashLabel && canUseHashLabels(path.getValue()) ? printHashKeyLabel : printHashKeyRocket; const contents = join(concat([",", line]), path.map(print, "body")); // If we're inside a hash literal, then we want to add the braces at this // level so that the grouping is correct. Otherwise you could end up with // opening and closing braces being split up, but the contents not being split // correctly. if (path.getParentNode().type === "hash") { return group( concat([ "{", indent( concat([ line, contents, getTrailingComma(opts) ? ifBreak(",", "") : "" ]) ), line, "}" ]) ); } // Otherwise, we're inside a bare_assoc_hash, so we don't want to print // braces at all. return group(contents); } function printEmptyHashWithComments(path, opts) { const hashNode = path.getValue(); const printComment = (commentPath, index) => { hashNode.comments[index].printed = true; return opts.printer.printComment(commentPath); }; return concat([ "{", indent( concat([hardline, join(hardline, path.map(printComment, "comments"))]) ), line, "}" ]); } function printHash(path, opts, print) { const hashNode = path.getValue(); // Hashes normally have a single assoclist_from_args child node. If it's // missing, then it means we're dealing with an empty hash, so we can just // exit here and print. if (hashNode.body[0] === null) { return hashNode.comments ? printEmptyHashWithComments(path, opts) : "{}"; } return path.call(print, "body", 0); } module.exports = { assoc_new: printAssocNew, assoc_splat: prefix("**"), assoclist_from_args: printHashContents, bare_assoc_hash: printHashContents, hash: printHash };