"use strict"; const util = require("./util"); const docBuilders = require("./doc-builders"); const concat = docBuilders.concat; const join = docBuilders.join; const line = docBuilders.line; const hardline = docBuilders.hardline; const softline = docBuilders.softline; const fill = docBuilders.fill; const align = docBuilders.align; const docPrinter = require("./doc-printer"); const printDocToString = docPrinter.printDocToString; const SINGLE_LINE_NODE_TYPES = [ "heading", "tableCell", "footnoteDefinition", "link" ]; const SIBLING_NODE_TYPES = ["listItem", "definition", "footnoteDefinition"]; const INLINE_NODE_TYPES = [ "inlineCode", "emphasis", "strong", "delete", "link", "linkReference", "image", "imageReference", "footnote", "footnoteReference", "sentence", "whitespace", "word", "break" ]; const INLINE_NODE_WRAPPER_TYPES = INLINE_NODE_TYPES.concat([ "tableCell", "paragraph", "heading" ]); function genericPrint(path, options, print) { const node = path.getValue(); if (shouldRemainTheSameContent(path)) { return concat( util .splitText( options.originalText.slice( node.position.start.offset, node.position.end.offset ) ) .map( node => node.type === "word" ? node.value : node.value === "" ? "" : printLine(path, node.value, options) ) ); } switch (node.type) { case "root": return concat([ normalizeDoc(printChildren(path, options, print)), hardline ]); case "paragraph": return printChildren(path, options, print, { postprocessor: fill }); case "sentence": return printChildren(path, options, print); case "word": return node.value .replace(/[*]/g, "\\*") // escape all `*` .replace( new RegExp( [ `(^|[${util.punctuationCharRange}])(_+)`, `(_+)([${util.punctuationCharRange}]|$)` ].join("|"), "g" ), (_, text1, underscore1, underscore2, text2) => (underscore1 ? `${text1}${underscore1}` : `${underscore2}${text2}` ).replace(/_/g, "\\_") ); // escape all `_` except concating with non-punctuation, e.g. `1_2_3` is not considered emphasis case "whitespace": { const parentNode = path.getParentNode(); const index = parentNode.children.indexOf(node); const nextNode = parentNode.children[index + 1]; const proseWrap = // leading char that may cause different syntax nextNode && /^>|^([-+*]|#{1,6}|[0-9]+[.)])$/.test(nextNode.value) ? "never" : options.proseWrap; return printLine(path, node.value, { proseWrap }); } case "emphasis": { const parentNode = path.getParentNode(); const index = parentNode.children.indexOf(node); const prevNode = parentNode.children[index - 1]; const nextNode = parentNode.children[index + 1]; const hasPrevOrNextWord = // `1*2*3` is considered emphais but `1_2_3` is not (prevNode && prevNode.type === "sentence" && prevNode.children.length > 0 && util.getLast(prevNode.children).type === "word" && !util.getLast(prevNode.children).hasTrailingPunctuation) || (nextNode && nextNode.type === "sentence" && nextNode.children.length > 0 && nextNode.children[0].type === "word" && !nextNode.children[0].hasLeadingPunctuation); const style = hasPrevOrNextWord || getAncestorNode(path, "emphasis") ? "*" : "_"; return concat([style, printChildren(path, options, print), style]); } case "strong": return concat(["**", printChildren(path, options, print), "**"]); case "delete": return concat(["~~", printChildren(path, options, print), "~~"]); case "inlineCode": { const backtickCount = util.getMaxContinuousCount(node.value, "`"); const style = backtickCount === 1 ? "``" : "`"; const gap = backtickCount ? " " : ""; return concat([style, gap, node.value, gap, style]); } case "link": switch (options.originalText[node.position.start.offset]) { case "<": return concat(["<", node.url, ">"]); case "[": return concat([ "[", printChildren(path, options, print), "](", printUrl(node.url, ")"), node.title ? ` ${printTitle(node.title)}` : "", ")" ]); default: return options.originalText.slice( node.position.start.offset, node.position.end.offset ); } case "image": return concat([ "![", node.alt || "", "](", printUrl(node.url, ")"), node.title ? ` ${printTitle(node.title)}` : "", ")" ]); case "blockquote": return concat(["> ", align("> ", printChildren(path, options, print))]); case "heading": return concat([ "#".repeat(node.depth) + " ", printChildren(path, options, print) ]); case "code": { if ( // the first char may point to `\n`, e.g. `\n\t\tbar`, just ignore it /^\n?( {4,}|\t)/.test( options.originalText.slice( node.position.start.offset, node.position.end.offset ) ) ) { // indented code block const alignment = " ".repeat(4); return align( alignment, concat([alignment, join(hardline, node.value.split("\n"))]) ); } // fenced code block const styleUnit = options.__inJsTemplate ? "~" : "`"; const style = styleUnit.repeat( Math.max(3, util.getMaxContinuousCount(node.value, styleUnit) + 1) ); return concat([ style, node.lang || "", hardline, join(hardline, node.value.split("\n")), hardline, style ]); } case "yaml": return concat(["---", hardline, node.value, hardline, "---"]); case "toml": return concat(["+++", hardline, node.value, hardline, "+++"]); case "html": { const parentNode = path.getParentNode(); return parentNode.type === "root" && util.getLast(parentNode.children) === node ? node.value.trimRight() : node.value; } case "list": { const nthSiblingIndex = getNthListSiblingIndex( node, path.getParentNode() ); const isGitDiffFriendlyOrderedList = node.ordered && node.children.length > 1 && /^\s*1(\.|\))/.test( options.originalText.slice( node.children[1].position.start.offset, node.children[1].position.end.offset ) ); return printChildren(path, options, print, { processor: (childPath, index) => { const prefix = node.ordered ? (index === 0 ? node.start : isGitDiffFriendlyOrderedList ? 1 : node.start + index) + (nthSiblingIndex % 2 === 0 ? ". " : ") ") : nthSiblingIndex % 2 === 0 ? "* " : "- "; return concat([ prefix, align(" ".repeat(prefix.length), childPath.call(print)) ]); } }); } case "listItem": { const prefix = node.checked === null ? "" : node.checked ? "[x] " : "[ ] "; return concat([ prefix, printChildren(path, options, print, { processor: (childPath, index) => index === 0 && childPath.getValue().type !== "list" ? align(" ".repeat(prefix.length), childPath.call(print)) : childPath.call(print) }) ]); } case "thematicBreak": { const counter = getAncestorCounter(path, "list"); if (counter === -1) { return "---"; } const nthSiblingIndex = getNthListSiblingIndex( path.getParentNode(counter), path.getParentNode(counter + 1) ); return nthSiblingIndex % 2 === 0 ? "---" : "***"; } case "linkReference": return concat([ "[", printChildren(path, options, print), "]", node.referenceType === "full" ? concat(["[", node.identifier, "]"]) : node.referenceType === "collapsed" ? "[]" : "" ]); case "imageReference": switch (node.referenceType) { case "full": return concat(["![", node.alt, "][", node.identifier, "]"]); default: return concat([ "![", node.alt, "]", node.referenceType === "collapsed" ? "[]" : "" ]); } case "definition": return concat([ "[", node.identifier, "]: ", printUrl(node.url), node.title === null ? "" : ` ${printTitle(node.title)}` ]); case "footnote": return concat(["[^", printChildren(path, options, print), "]"]); case "footnoteReference": return concat(["[^", node.identifier, "]"]); case "footnoteDefinition": return concat([ "[^", node.identifier, "]: ", printChildren(path, options, print) ]); case "table": return printTable(path, options, print); case "tableCell": return printChildren(path, options, print); case "break": return concat([ /\s/.test(options.originalText[node.position.start.offset]) ? " " : "\\", hardline ]); case "tableRow": // handled in "table" default: throw new Error(`Unknown markdown type ${JSON.stringify(node.type)}`); } } function getNthListSiblingIndex(node, parentNode) { return getNthSiblingIndex( node, parentNode, siblingNode => siblingNode.ordered === node.ordered ); } function getNthSiblingIndex(node, parentNode, condition) { condition = condition || (() => true); let index = -1; for (const childNode of parentNode.children) { if (childNode.type === node.type && condition(childNode)) { index++; } else { index = -1; } if (childNode === node) { return index; } } } function getAncestorCounter(path, typeOrTypes) { const types = [].concat(typeOrTypes); let counter = -1; let ancestorNode; while ((ancestorNode = path.getParentNode(++counter))) { if (types.indexOf(ancestorNode.type) !== -1) { return counter; } } return -1; } function getAncestorNode(path, typeOrTypes) { const counter = getAncestorCounter(path, typeOrTypes); return counter === -1 ? null : path.getParentNode(counter); } function printLine(path, value, options) { if (options.proseWrap === "preserve" && value === "\n") { return hardline; } const isBreakable = options.proseWrap === "always" && !getAncestorNode(path, SINGLE_LINE_NODE_TYPES); return value !== "" ? isBreakable ? line : " " : isBreakable ? softline : ""; } function printTable(path, options, print) { const node = path.getValue(); const contents = []; // { [rowIndex: number]: { [columnIndex: number]: string } } path.map(rowPath => { const rowContents = []; rowPath.map(cellPath => { rowContents.push( printDocToString(cellPath.call(print), options).formatted ); }, "children"); contents.push(rowContents); }, "children"); const columnMaxWidths = contents.reduce( (currentWidths, rowContents) => currentWidths.map((width, columnIndex) => Math.max(width, util.getStringWidth(rowContents[columnIndex])) ), contents[0].map(() => 3) // minimum width = 3 (---, :--, :-:, --:) ); return join(hardline, [ printRow(contents[0]), printSeparator(), join(hardline, contents.slice(1).map(printRow)) ]); function printSeparator() { return concat([ "| ", join( " | ", columnMaxWidths.map((width, index) => { switch (node.align[index]) { case "left": return ":" + "-".repeat(width - 1); case "right": return "-".repeat(width - 1) + ":"; case "center": return ":" + "-".repeat(width - 2) + ":"; default: return "-".repeat(width); } }) ), " |" ]); } function printRow(rowContents) { return concat([ "| ", join( " | ", rowContents.map((rowContent, columnIndex) => { switch (node.align[columnIndex]) { case "right": return alignRight(rowContent, columnMaxWidths[columnIndex]); case "center": return alignCenter(rowContent, columnMaxWidths[columnIndex]); default: return alignLeft(rowContent, columnMaxWidths[columnIndex]); } }) ), " |" ]); } function alignLeft(text, width) { return concat([text, " ".repeat(width - util.getStringWidth(text))]); } function alignRight(text, width) { return concat([" ".repeat(width - util.getStringWidth(text)), text]); } function alignCenter(text, width) { const spaces = width - util.getStringWidth(text); const left = Math.floor(spaces / 2); const right = spaces - left; return concat([" ".repeat(left), text, " ".repeat(right)]); } } function printChildren(path, options, print, events) { events = events || {}; const postprocessor = events.postprocessor || concat; const processor = events.processor || (childPath => childPath.call(print)); const node = path.getValue(); const parts = []; let counter = 0; let lastChildNode; let prettierIgnore = false; path.map((childPath, index) => { const childNode = childPath.getValue(); const result = prettierIgnore ? options.originalText.slice( childNode.position.start.offset, childNode.position.end.offset ) : processor(childPath, index); prettierIgnore = false; if (result !== false) { prettierIgnore = isPrettierIgnore(childNode); const data = { parts, index: counter++, prevNode: lastChildNode, parentNode: node, options }; if (!shouldNotPrePrintHardline(childNode, data)) { parts.push(hardline); if ( shouldPrePrintDoubleHardline(childNode, data) || shouldPrePrintTripleHardline(childNode, data) ) { parts.push(hardline); } if (shouldPrePrintTripleHardline(childNode, data)) { parts.push(hardline); } } parts.push(result); lastChildNode = childNode; } }, "children"); return postprocessor(parts); } function isPrettierIgnore(node) { return ( node.type === "html" && /^$/.test(node.value) ); } function shouldNotPrePrintHardline(node, data) { const isFirstNode = data.parts.length === 0; const isInlineNode = INLINE_NODE_TYPES.indexOf(node.type) !== -1; const isInlineHTML = node.type === "html" && INLINE_NODE_WRAPPER_TYPES.indexOf(data.parentNode.type) !== -1; return isFirstNode || isInlineNode || isInlineHTML; } function shouldPrePrintDoubleHardline(node, data) { const isSequence = (data.prevNode && data.prevNode.type) === node.type; const isSiblingNode = isSequence && SIBLING_NODE_TYPES.indexOf(node.type) !== -1; const isInTightListItem = data.parentNode.type === "listItem" && !data.parentNode.loose; const isPrevNodeLooseListItem = data.prevNode && data.prevNode.type === "listItem" && data.prevNode.loose; const isPrevNodePrettierIgnore = isPrettierIgnore(data.prevNode); return ( isPrevNodeLooseListItem || !(isSiblingNode || isInTightListItem || isPrevNodePrettierIgnore) ); } function shouldPrePrintTripleHardline(node, data) { const isPrevNodeList = data.prevNode && data.prevNode.type === "list"; const isIndentedCode = node.type === "code" && /\s/.test(data.options.originalText[node.position.start.offset]); return isPrevNodeList && isIndentedCode; } function shouldRemainTheSameContent(path) { const ancestorNode = getAncestorNode(path, [ "linkReference", "imageReference" ]); return ( ancestorNode && (ancestorNode.type !== "linkReference" || ancestorNode.referenceType !== "full") ); } function normalizeDoc(doc) { return util.mapDoc(doc, currentDoc => { if (!currentDoc.parts) { return currentDoc; } if (currentDoc.type === "concat" && currentDoc.parts.length === 1) { return currentDoc.parts[0]; } const parts = []; currentDoc.parts.forEach(part => { if (part.type === "concat") { parts.push.apply(parts, part.parts); } else if (part !== "") { parts.push(part); } }); return Object.assign({}, currentDoc, { parts: normalizeParts(parts) }); }); } function printUrl(url, dangerousCharOrChars) { const dangerousChars = [" "].concat(dangerousCharOrChars || []); return new RegExp(dangerousChars.map(x => `\\${x}`).join("|")).test(url) ? `<${url}>` : url; } function printTitle(title) { return title.includes('"') && !title.includes("'") ? `'${title}'` : `"${title.replace(/"/g, '\\"')}"`; } function normalizeParts(parts) { return parts.reduce((current, part) => { const lastPart = util.getLast(current); if (typeof lastPart === "string" && typeof part === "string") { current.splice(-1, 1, lastPart + part); } else { current.push(part); } return current; }, []); } module.exports = genericPrint;