diff --git a/src/language-markdown/print/children.js b/src/language-markdown/print/children.js new file mode 100644 index 000000000..8980a9c1a --- /dev/null +++ b/src/language-markdown/print/children.js @@ -0,0 +1,113 @@ +import { hardline } from "../../document/index.js"; +import { + INLINE_NODE_TYPES, + INLINE_NODE_WRAPPER_TYPES, + isPrettierIgnore, +} from "../utils.js"; + +/** + * @import AstPath from "../../common/ast-path.js" + * @import {Doc} from "../../document/index.js" + */ + +/** + * @param {AstPath} path + * @param {*} options + * @param {() => Doc} print + * @param {*} [events] + * @return {Doc} + */ +function printChildren(path, options, print, events = {}) { + const { processor = print } = events; + + const parts = []; + + path.each(() => { + const result = processor(path); + if (result !== false) { + if (parts.length > 0 && shouldPrePrintHardline(path)) { + parts.push(hardline); + + if ( + shouldPrePrintDoubleHardline(path, options) || + shouldPrePrintTripleHardline(path) + ) { + parts.push(hardline); + } + + if (shouldPrePrintTripleHardline(path)) { + parts.push(hardline); + } + } + + parts.push(result); + } + }, "children"); + + return parts; +} + +function shouldPrePrintHardline({ node, parent }) { + const isInlineNode = INLINE_NODE_TYPES.has(node.type); + + const isInlineHTML = + node.type === "html" && INLINE_NODE_WRAPPER_TYPES.has(parent.type); + + return !isInlineNode && !isInlineHTML; +} + +const SIBLING_NODE_TYPES = new Set(["listItem", "definition"]); + +function shouldPrePrintDoubleHardline({ node, previous, parent }, options) { + if ( + isLooseListItem(previous, options) || + (node.type === "list" && + parent.type === "listItem" && + previous.type === "code") + ) { + return true; + } + + const isSequence = previous.type === node.type; + const isSiblingNode = isSequence && SIBLING_NODE_TYPES.has(node.type); + const isInTightListItem = + parent.type === "listItem" && + (node.type === "list" || !isLooseListItem(parent, options)); + const isPrevNodePrettierIgnore = isPrettierIgnore(previous) === "next"; + const isBlockHtmlWithoutBlankLineBetweenPrevHtml = + node.type === "html" && + previous.type === "html" && + previous.position.end.line + 1 === node.position.start.line; + const isHtmlDirectAfterListItem = + node.type === "html" && + parent.type === "listItem" && + previous.type === "paragraph" && + previous.position.end.line + 1 === node.position.start.line; + + return !( + isSiblingNode || + isInTightListItem || + isPrevNodePrettierIgnore || + isBlockHtmlWithoutBlankLineBetweenPrevHtml || + isHtmlDirectAfterListItem + ); +} + +function shouldPrePrintTripleHardline({ node, previous }) { + const isPrevNodeList = previous.type === "list"; + const isIndentedCode = node.type === "code" && node.isIndented; + + return isPrevNodeList && isIndentedCode; +} + +function isLooseListItem(node, options) { + return ( + node.type === "listItem" && + (node.spread || + // Check if `listItem` ends with `\n` + // since it can't be empty, so we only need check the last character + options.originalText.charAt(node.position.end.offset - 1) === "\n") + ); +} + +export { printChildren }; diff --git a/src/language-markdown/print/list.js b/src/language-markdown/print/list.js new file mode 100644 index 000000000..5f9ee860a --- /dev/null +++ b/src/language-markdown/print/list.js @@ -0,0 +1,111 @@ +import { align } from "../../document/index.js"; +import { + getNthListSiblingIndex, + hasGitDiffFriendlyOrderedList, +} from "../utils.js"; +import { printChildren } from "./children.js"; + +/** + * @import AstPath from "../../common/ast-path.js" + * @import {Doc} from "../../document/index.js" + */ + +/** + * @param {AstPath} path + * @param {*} options + * @param {() => Doc} print + * @return {Doc} + */ +function printList(path, options, print) { + const { node } = path; + const nthSiblingIndex = getNthListSiblingIndex(node, path.parent); + + const isGitDiffFriendlyOrderedList = hasGitDiffFriendlyOrderedList( + node, + options, + ); + + return printChildren(path, options, print, { + processor() { + const prefix = getPrefix(); + const { node: childNode } = path; + + if ( + childNode.children.length === 2 && + childNode.children[1].type === "html" && + childNode.children[0].position.start.column !== + childNode.children[1].position.start.column + ) { + return [prefix, printListItem(path, options, print, prefix)]; + } + + return [ + prefix, + align( + " ".repeat(prefix.length), + printListItem(path, options, print, prefix), + ), + ]; + + function getPrefix() { + const rawPrefix = node.ordered + ? (path.isFirst + ? node.start + : isGitDiffFriendlyOrderedList + ? 1 + : node.start + path.index) + + (nthSiblingIndex % 2 === 0 ? ". " : ") ") + : nthSiblingIndex % 2 === 0 + ? "- " + : "* "; + + return (node.isAligned || + /* workaround for https://github.com/remarkjs/remark/issues/315 */ node.hasIndentedCodeblock) && + node.ordered + ? alignListPrefix(rawPrefix, options) + : rawPrefix; + } + }, + }); +} + +function printListItem(path, options, print, listPrefix) { + const { node } = path; + const prefix = node.checked === null ? "" : node.checked ? "[x] " : "[ ] "; + return [ + prefix, + printChildren(path, options, print, { + processor({ node, isFirst }) { + if (isFirst && node.type !== "list") { + return align(" ".repeat(prefix.length), print()); + } + + const alignment = " ".repeat( + clamp(options.tabWidth - listPrefix.length, 0, 3), // 4+ will cause indented code block + ); + return [alignment, align(alignment, print())]; + }, + }), + ]; +} + +function alignListPrefix(prefix, options) { + const additionalSpaces = getAdditionalSpaces(); + return ( + prefix + + " ".repeat( + additionalSpaces >= 4 ? 0 : additionalSpaces, // 4+ will cause indented code block + ) + ); + + function getAdditionalSpaces() { + const restSpaces = prefix.length % options.tabWidth; + return restSpaces === 0 ? 0 : options.tabWidth - restSpaces; + } +} + +function clamp(value, min, max) { + return Math.max(min, Math.min(value, max)); +} + +export { printList }; diff --git a/src/language-markdown/printer-markdown.js b/src/language-markdown/printer-markdown.js index 2176a57b8..c0ea8f8ff 100644 --- a/src/language-markdown/printer-markdown.js +++ b/src/language-markdown/printer-markdown.js @@ -24,6 +24,8 @@ import embed from "./embed.js"; import getVisitorKeys from "./get-visitor-keys.js"; import { locEnd, locStart } from "./loc.js"; import { insertPragma } from "./pragma.js"; +import { printChildren } from "./print/children.js"; +import { printList } from "./print/list.js"; import { printTable } from "./print/table.js"; import { printParagraph } from "./print-paragraph.js"; import preprocess from "./print-preprocess.js"; @@ -31,10 +33,9 @@ import { printSentence } from "./print-sentence.js"; import { printWhitespace } from "./print-whitespace.js"; import { getFencedCodeBlockValue, - hasGitDiffFriendlyOrderedList, - INLINE_NODE_TYPES, - INLINE_NODE_WRAPPER_TYPES, + getNthListSiblingIndex, isAutolink, + isPrettierIgnore, splitText, } from "./utils.js"; @@ -42,8 +43,6 @@ import { * @import {Doc} from "../document/index.js" */ -const SIBLING_NODE_TYPES = new Set(["listItem", "definition"]); - function prevOrNextWord(path) { const { previous, next } = path; const hasPrevOrNextWord = @@ -285,57 +284,8 @@ function genericPrint(path, options, print) { isHtmlComment ? hardline : markAsRoot(literalline), ); } - case "list": { - const nthSiblingIndex = getNthListSiblingIndex(node, path.parent); - - const isGitDiffFriendlyOrderedList = hasGitDiffFriendlyOrderedList( - node, - options, - ); - - return printChildren(path, options, print, { - processor() { - const prefix = getPrefix(); - const { node: childNode } = path; - - if ( - childNode.children.length === 2 && - childNode.children[1].type === "html" && - childNode.children[0].position.start.column !== - childNode.children[1].position.start.column - ) { - return [prefix, printListItem(path, options, print, prefix)]; - } - - return [ - prefix, - align( - " ".repeat(prefix.length), - printListItem(path, options, print, prefix), - ), - ]; - - function getPrefix() { - const rawPrefix = node.ordered - ? (path.isFirst - ? node.start - : isGitDiffFriendlyOrderedList - ? 1 - : node.start + path.index) + - (nthSiblingIndex % 2 === 0 ? ". " : ") ") - : nthSiblingIndex % 2 === 0 - ? "- " - : "* "; - - return (node.isAligned || - /* workaround for https://github.com/remarkjs/remark/issues/315 */ node.hasIndentedCodeblock) && - node.ordered - ? alignListPrefix(rawPrefix, options) - : rawPrefix; - } - }, - }); - } + case "list": + return printList(path, options, print); case "thematicBreak": { const { ancestors } = path; const counter = ancestors.findIndex((node) => node.type === "list"); @@ -457,65 +407,6 @@ function genericPrint(path, options, print) { } } -function printListItem(path, options, print, listPrefix) { - const { node } = path; - const prefix = node.checked === null ? "" : node.checked ? "[x] " : "[ ] "; - return [ - prefix, - printChildren(path, options, print, { - processor({ node, isFirst }) { - if (isFirst && node.type !== "list") { - return align(" ".repeat(prefix.length), print()); - } - - const alignment = " ".repeat( - clamp(options.tabWidth - listPrefix.length, 0, 3), // 4+ will cause indented code block - ); - return [alignment, align(alignment, print())]; - }, - }), - ]; -} - -function alignListPrefix(prefix, options) { - const additionalSpaces = getAdditionalSpaces(); - return ( - prefix + - " ".repeat( - additionalSpaces >= 4 ? 0 : additionalSpaces, // 4+ will cause indented code block - ) - ); - - function getAdditionalSpaces() { - const restSpaces = prefix.length % options.tabWidth; - return restSpaces === 0 ? 0 : options.tabWidth - restSpaces; - } -} - -function getNthListSiblingIndex(node, parentNode) { - return getNthSiblingIndex( - node, - parentNode, - (siblingNode) => siblingNode.ordered === node.ordered, - ); -} - -function getNthSiblingIndex(node, parentNode, condition) { - 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 printRoot(path, options, print) { /** @typedef {{ index: number, offset: number }} IgnorePosition */ /** @type {Array<{start: IgnorePosition, end: IgnorePosition}>} */ @@ -578,36 +469,6 @@ function printRoot(path, options, print) { }); } -function printChildren(path, options, print, events = {}) { - const { processor = print } = events; - - const parts = []; - - path.each(() => { - const result = processor(path); - if (result !== false) { - if (parts.length > 0 && shouldPrePrintHardline(path)) { - parts.push(hardline); - - if ( - shouldPrePrintDoubleHardline(path, options) || - shouldPrePrintTripleHardline(path) - ) { - parts.push(hardline); - } - - if (shouldPrePrintTripleHardline(path)) { - parts.push(hardline); - } - } - - parts.push(result); - } - }, "children"); - - return parts; -} - function printIgnoreComment(node) { if (node.type === "html") { return node.value; @@ -623,96 +484,6 @@ function printIgnoreComment(node) { } } -/** @return {false | 'next' | 'start' | 'end'} */ -function isPrettierIgnore(node) { - let match; - - if (node.type === "html") { - match = node.value.match( - /^$/u, - ); - } else { - let comment; - - if (node.type === "esComment") { - comment = node; - } else if ( - node.type === "paragraph" && - node.children.length === 1 && - node.children[0].type === "esComment" - ) { - comment = node.children[0]; - } - - if (comment) { - match = comment.value.match(/^prettier-ignore(?:-(start|end))?$/u); - } - } - - return match ? match[1] || "next" : false; -} - -function shouldPrePrintHardline({ node, parent }) { - const isInlineNode = INLINE_NODE_TYPES.has(node.type); - - const isInlineHTML = - node.type === "html" && INLINE_NODE_WRAPPER_TYPES.has(parent.type); - - return !isInlineNode && !isInlineHTML; -} - -function isLooseListItem(node, options) { - return ( - node.type === "listItem" && - (node.spread || - // Check if `listItem` ends with `\n` - // since it can't be empty, so we only need check the last character - options.originalText.charAt(node.position.end.offset - 1) === "\n") - ); -} - -function shouldPrePrintDoubleHardline({ node, previous, parent }, options) { - if ( - isLooseListItem(previous, options) || - (node.type === "list" && - parent.type === "listItem" && - previous.type === "code") - ) { - return true; - } - - const isSequence = previous.type === node.type; - const isSiblingNode = isSequence && SIBLING_NODE_TYPES.has(node.type); - const isInTightListItem = - parent.type === "listItem" && - (node.type === "list" || !isLooseListItem(parent, options)); - const isPrevNodePrettierIgnore = isPrettierIgnore(previous) === "next"; - const isBlockHtmlWithoutBlankLineBetweenPrevHtml = - node.type === "html" && - previous.type === "html" && - previous.position.end.line + 1 === node.position.start.line; - const isHtmlDirectAfterListItem = - node.type === "html" && - parent.type === "listItem" && - previous.type === "paragraph" && - previous.position.end.line + 1 === node.position.start.line; - - return !( - isSiblingNode || - isInTightListItem || - isPrevNodePrettierIgnore || - isBlockHtmlWithoutBlankLineBetweenPrevHtml || - isHtmlDirectAfterListItem - ); -} - -function shouldPrePrintTripleHardline({ node, previous }) { - const isPrevNodeList = previous.type === "list"; - const isIndentedCode = node.type === "code" && node.isIndented; - - return isPrevNodeList && isIndentedCode; -} - function shouldRemainTheSameContent(path) { const node = path.findAncestor( (node) => node.type === "linkReference" || node.type === "imageReference", @@ -770,10 +541,6 @@ function printTitle(title, options, printSpace = true) { return `${quote}${title}${quote}`; } -function clamp(value, min, max) { - return Math.max(min, Math.min(value, max)); -} - function hasPrettierIgnore(path) { return path.index > 0 && isPrettierIgnore(path.previous) === "next"; } diff --git a/src/language-markdown/utils.js b/src/language-markdown/utils.js index 13a3745f3..8ce02f4dd 100644 --- a/src/language-markdown/utils.js +++ b/src/language-markdown/utils.js @@ -252,13 +252,68 @@ function isAutolink(node) { return locStart(node) === locStart(child) && locEnd(node) === locEnd(child); } +/** @return {false | 'next' | 'start' | 'end'} */ +function isPrettierIgnore(node) { + let match; + + if (node.type === "html") { + match = node.value.match( + /^$/u, + ); + } else { + let comment; + + if (node.type === "esComment") { + comment = node; + } else if ( + node.type === "paragraph" && + node.children.length === 1 && + node.children[0].type === "esComment" + ) { + comment = node.children[0]; + } + + if (comment) { + match = comment.value.match(/^prettier-ignore(?:-(start|end))?$/u); + } + } + + return match ? match[1] || "next" : false; +} + +function getNthListSiblingIndex(node, parentNode) { + return getNthSiblingIndex( + node, + parentNode, + (siblingNode) => siblingNode.ordered === node.ordered, + ); + + function getNthSiblingIndex(node, parentNode, condition) { + 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; + } + } + } +} + export { getFencedCodeBlockValue, + getNthListSiblingIndex, getOrderedListItemInfo, hasGitDiffFriendlyOrderedList, INLINE_NODE_TYPES, INLINE_NODE_WRAPPER_TYPES, isAutolink, + isPrettierIgnore, KIND_CJ_LETTER, KIND_CJK_PUNCTUATION, KIND_K_LETTER,