Co-authored-by: fisker <lionkay@gmail.com> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
581 lines
17 KiB
JavaScript
581 lines
17 KiB
JavaScript
import collapseWhiteSpace from "collapse-white-space";
|
|
import {
|
|
align,
|
|
DOC_TYPE_STRING,
|
|
fill,
|
|
getDocType,
|
|
group,
|
|
hardline,
|
|
indent,
|
|
line,
|
|
literalline,
|
|
markAsRoot,
|
|
replaceEndOfLine,
|
|
softline,
|
|
} from "../../document/index.js";
|
|
import getMaxContinuousCount from "../../utilities/get-max-continuous-count.js";
|
|
import getMinNotPresentContinuousCount from "../../utilities/get-min-not-present-continuous-count.js";
|
|
import { getPreferredQuote } from "../../utilities/get-preferred-quote.js";
|
|
import UnexpectedNodeError from "../../utilities/unexpected-node-error.js";
|
|
import { locEnd, locStart } from "../loc.js";
|
|
import {
|
|
getFencedCodeBlockValue,
|
|
getNthListSiblingIndex,
|
|
isAutolink,
|
|
isNewLine,
|
|
isPrettierIgnore,
|
|
splitText,
|
|
} from "../utilities.js";
|
|
import { printChildren } from "./children.js";
|
|
import { printHeading } from "./heading.js";
|
|
import { printList, printListLegacy } from "./list.js";
|
|
import { printParagraph } from "./paragraph.js";
|
|
import { printSentence } from "./sentence.js";
|
|
import { printTable } from "./table.js";
|
|
import { printWhitespace } from "./whitespace.js";
|
|
import { printWord, printWordLegacy } from "./word.js";
|
|
|
|
/**
|
|
* @import AstPath from "../../common/ast-path.js";
|
|
* @import {Doc} from "../../document/index.js";
|
|
*/
|
|
|
|
function prevOrNextWord(path) {
|
|
const { previous, next } = path;
|
|
const hasPrevOrNextWord =
|
|
(previous?.type === "sentence" &&
|
|
previous.children.at(-1)?.type === "word" &&
|
|
!previous.children.at(-1).hasTrailingPunctuation &&
|
|
// https://spec.commonmark.org/0.31.2/#unicode-whitespace-character
|
|
!/[\p{Space_Separator}\t\n\f\r]$/u.test(
|
|
previous.children.at(-1).value,
|
|
)) ||
|
|
(next?.type === "sentence" &&
|
|
next.children[0]?.type === "word" &&
|
|
!next.children[0].hasLeadingPunctuation &&
|
|
!/^[\p{Space_Separator}\t\n\f\r]/u.test(next.children[0].value));
|
|
return hasPrevOrNextWord;
|
|
}
|
|
|
|
function hasFakeWhitespaceAfterNextToken(path) {
|
|
const { siblings, index } = path;
|
|
if (!siblings || typeof index !== "number") {
|
|
return false;
|
|
}
|
|
const afterNext = siblings[index + 2];
|
|
return afterNext?.type === "whitespace" && afterNext.value === "";
|
|
}
|
|
|
|
/**
|
|
* @param {AstPath} path
|
|
* @returns {boolean}
|
|
*/
|
|
function isNextTokenFakeSetextH2Line(path) {
|
|
if (!isNewLine(path.node) || path.next?.value !== "-") {
|
|
return false;
|
|
}
|
|
|
|
const afterNext = path.siblings[path.index + 2];
|
|
return !afterNext || isNewLine(afterNext);
|
|
}
|
|
|
|
function printMdast(path, options, print) {
|
|
const { node } = path;
|
|
|
|
if (shouldRemainTheSameContent(path)) {
|
|
/*
|
|
* We assume parts always meet following conditions:
|
|
* - parts.length is odd
|
|
* - odd (0-indexed) elements are line-like doc
|
|
*/
|
|
/** @type {Doc[]} */
|
|
const parts = [""];
|
|
const textsNodes = splitText(
|
|
options.originalText.slice(
|
|
node.position.start.offset,
|
|
node.position.end.offset,
|
|
),
|
|
);
|
|
for (const node of textsNodes) {
|
|
if (node.type === "word") {
|
|
parts.push([parts.pop(), node.value]);
|
|
continue;
|
|
}
|
|
const doc = printWhitespace(
|
|
path,
|
|
node.value,
|
|
options.proseWrap,
|
|
true,
|
|
options,
|
|
);
|
|
if (getDocType(doc) === DOC_TYPE_STRING) {
|
|
parts.push([parts.pop(), doc]);
|
|
continue;
|
|
}
|
|
// In this path, doc is line. To meet the condition, we need additional element "".
|
|
parts.push(doc, "");
|
|
}
|
|
return fill(parts);
|
|
}
|
|
|
|
switch (node.type) {
|
|
case "root":
|
|
/* c8 ignore next 3 */
|
|
if (node.children.length === 0) {
|
|
return "";
|
|
}
|
|
return [printRoot(path, options, print), hardline];
|
|
case "paragraph":
|
|
return printParagraph(path, options, print);
|
|
case "sentence":
|
|
return printSentence(path, print);
|
|
case "word":
|
|
return options.parser !== "mdx"
|
|
? printWord(path, options)
|
|
: printWordLegacy(path);
|
|
case "whitespace": {
|
|
const { next } = path;
|
|
|
|
const proseWrap =
|
|
// leading char that may cause different syntax
|
|
next &&
|
|
/^>|^(?:[*+-]|#{1,6}|\d+[).])$/.test(next.value) &&
|
|
// Avoid https://github.com/prettier/prettier/issues/18861
|
|
!hasFakeWhitespaceAfterNextToken(path) &&
|
|
// Next fake setext h2 `-` is going to be escaped, so no need to join adjacent words
|
|
!(options.proseWrap === "preserve" && isNextTokenFakeSetextH2Line(path))
|
|
? "never"
|
|
: options.proseWrap;
|
|
|
|
return printWhitespace(path, node.value, proseWrap, false, options);
|
|
}
|
|
case "emphasis": {
|
|
let style;
|
|
if (isAutolink(node.children[0])) {
|
|
style = options.originalText[node.position.start.offset];
|
|
} else {
|
|
const hasPrevOrNextWord = prevOrNextWord(path); // `1*2*3` is considered emphasis but `1_2_3` is not
|
|
const inStrongAndHasPrevOrNextWord = // `1***2***3` is considered strong emphasis but `1**_2_**3` is not
|
|
path.callParent(
|
|
({ node }) => node.type === "strong" && prevOrNextWord(path),
|
|
);
|
|
style =
|
|
hasPrevOrNextWord ||
|
|
inStrongAndHasPrevOrNextWord ||
|
|
path.hasAncestor((node) => node.type === "emphasis")
|
|
? "*"
|
|
: "_";
|
|
}
|
|
return [style, printChildren(path, options, print), style];
|
|
}
|
|
case "strong":
|
|
return ["**", printChildren(path, options, print), "**"];
|
|
case "delete":
|
|
return ["~~", printChildren(path, options, print), "~~"];
|
|
case "inlineCode": {
|
|
let code =
|
|
options.proseWrap === "preserve"
|
|
? node.value
|
|
: node.value.replaceAll("\n", " ");
|
|
if (
|
|
options.parser !== "mdx" &&
|
|
path.hasAncestor((node) => node.type === "tableCell")
|
|
) {
|
|
code = code.replaceAll("|", String.raw`\|`);
|
|
}
|
|
const backtickCount = getMinNotPresentContinuousCount(code, "`");
|
|
const backtickString = "`".repeat(backtickCount);
|
|
const padding =
|
|
code.startsWith("`") ||
|
|
code.endsWith("`") ||
|
|
(/^[\n ]/.test(code) && /[\n ]$/.test(code) && /[^\n ]/.test(code))
|
|
? " "
|
|
: "";
|
|
return [backtickString, padding, code, padding, backtickString];
|
|
}
|
|
case "wikiLink": {
|
|
let contents;
|
|
if (options.proseWrap === "preserve") {
|
|
contents = node.value;
|
|
} else {
|
|
contents = node.value.replaceAll(/[\t\n]+/g, " ");
|
|
}
|
|
|
|
return ["[[", contents, "]]"];
|
|
}
|
|
case "link":
|
|
switch (options.originalText[node.position.start.offset]) {
|
|
case "<": {
|
|
const mailto = "mailto:";
|
|
const url =
|
|
// <hello@example.com> is parsed as { url: "mailto:hello@example.com" }
|
|
node.url.startsWith(mailto) &&
|
|
options.originalText.slice(
|
|
node.position.start.offset + 1,
|
|
node.position.start.offset + 1 + mailto.length,
|
|
) !== mailto
|
|
? node.url.slice(mailto.length)
|
|
: node.url;
|
|
return ["<", url, ">"];
|
|
}
|
|
case "[":
|
|
return [
|
|
"[",
|
|
printChildren(path, options, print),
|
|
"](",
|
|
node.url === "" && node.title !== null ? "<>" : printUrl(node.url),
|
|
printTitle(node.title, options),
|
|
")",
|
|
];
|
|
default:
|
|
return options.originalText.slice(
|
|
node.position.start.offset,
|
|
node.position.end.offset,
|
|
);
|
|
}
|
|
case "image":
|
|
return [
|
|
",
|
|
printTitle(node.title, options),
|
|
")",
|
|
];
|
|
case "blockquote":
|
|
return ["> ", align("> ", printChildren(path, options, print))];
|
|
case "heading":
|
|
return printHeading(path, options, print);
|
|
case "code": {
|
|
if (node.isIndented) {
|
|
// indented code block
|
|
const alignment = " ".repeat(4);
|
|
return align(alignment, [
|
|
alignment,
|
|
replaceEndOfLine(node.value, hardline),
|
|
]);
|
|
}
|
|
|
|
// fenced code block
|
|
const styleUnit = options.__inJsTemplate ? "~" : "`";
|
|
const style = styleUnit.repeat(
|
|
Math.max(3, getMaxContinuousCount(node.value, styleUnit) + 1),
|
|
);
|
|
return [
|
|
style,
|
|
node.lang || "",
|
|
node.meta ? " " + node.meta : "",
|
|
hardline,
|
|
replaceEndOfLine(
|
|
options.parser === "mdx"
|
|
? getFencedCodeBlockValue(node, options.originalText)
|
|
: node.value,
|
|
hardline,
|
|
),
|
|
hardline,
|
|
style,
|
|
];
|
|
}
|
|
case "html": {
|
|
const { parent, isLast } = path;
|
|
const value =
|
|
parent.type === "root" && isLast ? node.value.trimEnd() : node.value;
|
|
const isHtmlComment = /^<!--.*-->$/s.test(value);
|
|
|
|
return replaceEndOfLine(
|
|
value,
|
|
isHtmlComment ? hardline : markAsRoot(literalline),
|
|
);
|
|
}
|
|
case "list":
|
|
if (options.parser === "mdx") {
|
|
return printListLegacy(path, options, print);
|
|
}
|
|
return printList(path, options, print);
|
|
case "thematicBreak": {
|
|
const { ancestors } = path;
|
|
const counter = ancestors.findIndex((node) => node.type === "list");
|
|
if (counter === -1) {
|
|
return "---";
|
|
}
|
|
const nthSiblingIndex = getNthListSiblingIndex(
|
|
ancestors[counter],
|
|
ancestors[counter + 1],
|
|
);
|
|
return nthSiblingIndex % 2 === 0 ? "***" : "---";
|
|
}
|
|
case "linkReference":
|
|
return [
|
|
"[",
|
|
printChildren(path, options, print),
|
|
"]",
|
|
node.referenceType === "full"
|
|
? printLinkReference(node)
|
|
: node.referenceType === "collapsed"
|
|
? "[]"
|
|
: "",
|
|
];
|
|
case "imageReference": {
|
|
const alt = printImageAlt(node, options);
|
|
|
|
switch (node.referenceType) {
|
|
case "full":
|
|
return ["![", alt, "]", printLinkReference(node)];
|
|
default:
|
|
return [
|
|
...(options.parser === "mdx"
|
|
? ["![", alt, "]"]
|
|
: ["!", printLinkReference(node)]),
|
|
node.referenceType === "collapsed" ? "[]" : "",
|
|
];
|
|
}
|
|
}
|
|
case "definition": {
|
|
const lineOrSpace = options.proseWrap === "always" ? line : " ";
|
|
return group([
|
|
printLinkReference(node),
|
|
":",
|
|
indent([
|
|
lineOrSpace,
|
|
options.parser !== "mdx" && node.url === ""
|
|
? "<>"
|
|
: printUrl(node.url),
|
|
node.title === null
|
|
? ""
|
|
: [lineOrSpace, printTitle(node.title, options, false)],
|
|
]),
|
|
]);
|
|
}
|
|
// `footnote` requires `.use(footnotes, {inlineNotes: true})`, we are not using this option
|
|
// https://github.com/remarkjs/remark-footnotes#optionsinlinenotes
|
|
/* c8 ignore next 2 */
|
|
case "footnote":
|
|
return ["[^", printChildren(path, options, print), "]"];
|
|
case "footnoteReference":
|
|
return printFootnoteReference(node);
|
|
case "footnoteDefinition": {
|
|
const shouldInlineFootnote =
|
|
node.children.length === 1 &&
|
|
node.children[0].type === "paragraph" &&
|
|
(options.proseWrap === "never" ||
|
|
(options.proseWrap === "preserve" &&
|
|
node.children[0].position.start.line ===
|
|
node.children[0].position.end.line));
|
|
return [
|
|
printFootnoteReference(node),
|
|
": ",
|
|
shouldInlineFootnote
|
|
? printChildren(path, options, print)
|
|
: group([
|
|
align(
|
|
" ".repeat(4),
|
|
printChildren(path, options, print, {
|
|
processor: ({ isFirst }) =>
|
|
isFirst ? group([softline, print()]) : print(),
|
|
}),
|
|
),
|
|
]),
|
|
];
|
|
}
|
|
case "table":
|
|
return printTable(path, options, print);
|
|
case "tableCell":
|
|
return printChildren(path, options, print);
|
|
case "break":
|
|
return /\s/.test(options.originalText[node.position.start.offset])
|
|
? [" ", markAsRoot(literalline)]
|
|
: ["\\", hardline];
|
|
case "liquidNode":
|
|
return replaceEndOfLine(node.value, hardline);
|
|
// MDX
|
|
// fallback to the original text if multiparser failed
|
|
// or `embeddedLanguageFormatting: "off"`
|
|
case "import":
|
|
case "export":
|
|
case "jsx":
|
|
return node.value.trimEnd();
|
|
case "esComment":
|
|
return ["{/* ", node.value, " */}"];
|
|
case "math":
|
|
return [
|
|
"$$",
|
|
node.meta ? " " + node.meta : "",
|
|
hardline,
|
|
node.value ? [replaceEndOfLine(node.value, hardline), hardline] : "",
|
|
"$$",
|
|
];
|
|
case "inlineMath":
|
|
// remark-math trims content but we don't want to remove whitespaces
|
|
// since it's very possible that it's recognized as math accidentally
|
|
return options.originalText.slice(locStart(node), locEnd(node));
|
|
case "text":
|
|
return replaceEndOfLine(node.value, hardline);
|
|
|
|
case "frontMatter": // Handled in core
|
|
case "tableRow": // handled in "table"
|
|
case "listItem": // handled in "list"
|
|
default:
|
|
/* c8 ignore next */
|
|
throw new UnexpectedNodeError(node, "Markdown");
|
|
}
|
|
}
|
|
|
|
function printRoot(path, options, print) {
|
|
/** @typedef {{ index: number, offset: number }} IgnorePosition */
|
|
/** @type {Array<{start: IgnorePosition, end: IgnorePosition}>} */
|
|
const ignoreRanges = [];
|
|
|
|
/** @type {IgnorePosition | null} */
|
|
let ignoreStart = null;
|
|
|
|
const { children } = path.node;
|
|
for (const [index, childNode] of children.entries()) {
|
|
switch (isPrettierIgnore(childNode)) {
|
|
case "start":
|
|
if (ignoreStart === null) {
|
|
ignoreStart = { index, offset: childNode.position.end.offset };
|
|
}
|
|
break;
|
|
case "end":
|
|
if (ignoreStart !== null) {
|
|
ignoreRanges.push({
|
|
start: ignoreStart,
|
|
end: { index, offset: childNode.position.start.offset },
|
|
});
|
|
ignoreStart = null;
|
|
}
|
|
break;
|
|
default:
|
|
// do nothing
|
|
break;
|
|
}
|
|
}
|
|
|
|
return printChildren(path, options, print, {
|
|
processor({ index }) {
|
|
if (ignoreRanges.length > 0) {
|
|
const ignoreRange = ignoreRanges[0];
|
|
|
|
if (index === ignoreRange.start.index) {
|
|
return [
|
|
printIgnoreComment(children[ignoreRange.start.index]),
|
|
options.originalText.slice(
|
|
ignoreRange.start.offset,
|
|
ignoreRange.end.offset,
|
|
),
|
|
printIgnoreComment(children[ignoreRange.end.index]),
|
|
];
|
|
}
|
|
|
|
if (ignoreRange.start.index < index && index < ignoreRange.end.index) {
|
|
return false;
|
|
}
|
|
|
|
if (index === ignoreRange.end.index) {
|
|
ignoreRanges.shift();
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return print();
|
|
},
|
|
});
|
|
}
|
|
|
|
function printIgnoreComment(node) {
|
|
if (node.type === "html") {
|
|
return node.value;
|
|
}
|
|
|
|
if (
|
|
node.type === "paragraph" &&
|
|
Array.isArray(node.children) &&
|
|
node.children.length === 1 &&
|
|
node.children[0].type === "esComment"
|
|
) {
|
|
return ["{/* ", node.children[0].value, " */}"];
|
|
}
|
|
}
|
|
|
|
function shouldRemainTheSameContent(path) {
|
|
const node = path.findAncestor(
|
|
(node) => node.type === "linkReference" || node.type === "imageReference",
|
|
);
|
|
return (
|
|
node && (node.type !== "linkReference" || node.referenceType !== "full")
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @param {string} url
|
|
* @returns {string}
|
|
*/
|
|
function printUrl(url) {
|
|
// Backslash followed by ASCII punctuation would be misinterpreted as an
|
|
// escape sequence, so must itself be escaped.
|
|
url = url.replaceAll(/\\(?![^!"#$%&'()*+,\-./:;<=>?@[\\\]^_`{|}~])/g, "\\\\");
|
|
// CommonMark forbids ASCII controls, space, unbalanced parentheses, and
|
|
// initial <, unless wrapped in <> with any inner < or > escaped. CommonMark
|
|
// only suggests implementations "should" support at least three levels of
|
|
// parenthesis nesting, so it's unclear whether we can safely rely on three
|
|
// levels as we do here, but we certainly can't expect more.
|
|
// eslint-disable-next-line no-control-regex
|
|
return /[\x00-\x1f\x7f ]|^</.test(url) ||
|
|
!/^(?:[^()]|\((?:[^()]|\((?:[^()]|\([^()]*\))*\))*\))*$/.test(url)
|
|
? `<${url.replaceAll(/([<>])/g, String.raw`\$1`)}>`
|
|
: url;
|
|
}
|
|
|
|
function printTitle(title, options, printSpace = true) {
|
|
if (!title) {
|
|
return "";
|
|
}
|
|
if (printSpace) {
|
|
return " " + printTitle(title, options, false);
|
|
}
|
|
|
|
// title is escaped before `remark-parse` v10
|
|
if (options.parser === "mdx") {
|
|
title = title.replaceAll(/\\(?=["')])/g, "");
|
|
}
|
|
|
|
if (
|
|
title.includes('"') &&
|
|
title.includes("'") &&
|
|
!title.includes("(") &&
|
|
!title.includes(")")
|
|
) {
|
|
title = title.replaceAll("\\", "\\\\");
|
|
return `(${title})`; // avoid escaped quotes
|
|
}
|
|
const quote = getPreferredQuote(title, options.singleQuote);
|
|
title = title.replaceAll("\\", "\\\\");
|
|
title = title.replaceAll(quote, `\\${quote}`);
|
|
return `${quote}${title}${quote}`;
|
|
}
|
|
|
|
function printLinkReference(node, options) {
|
|
// `remark-parse` lowercase the `label` as `identifier`, we don't want do that
|
|
// https://github.com/remarkjs/remark/blob/daddcb463af2d5b2115496c395d0571c0ff87d15/packages/remark-parse/lib/tokenize/reference.js
|
|
const label = collapseWhiteSpace(node.label);
|
|
if (options?.parser === "mdx") {
|
|
return `[${label}]`;
|
|
}
|
|
const name = label.replaceAll(/[\\[\]]/g, (s) => `\\${s}`);
|
|
return `[${name}]`;
|
|
}
|
|
|
|
function printFootnoteReference(node) {
|
|
return `[^${node.label}]`;
|
|
}
|
|
|
|
function printImageAlt(node, options) {
|
|
if (options.parser !== "mdx" && node.originalAltText) {
|
|
return node.originalAltText;
|
|
}
|
|
|
|
return node.alt || "";
|
|
}
|
|
|
|
export { printMdast };
|