Escape unindented indented pseudo setext headers (#19350)

Co-authored-by: fisker <lionkay@gmail.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
authored and GitHub committed 2026-06-17 18:16:21 +08:00
1 parent bfe8858f5e
commit 1c0cf93b41
6 files changed
+144 -11

No files matched your search

+31
View File
@@ -0,0 +1,31 @@
#### Escape unindented indented pseudo setext headers (#19350 by @tats-u)
Indented lines composed of only `=` or `-` were previously unindented without being escaped, which caused them to be mistakenly parsed as setext headers. With this change, Prettier now escapes such lines to prevent the misinterpretation:
[Check parse results on CommonMark playground](https://spec.commonmark.org/dingus/?text=%3C!--%20Input%20--%3E%0ANot%20a%20header%0A%20%20%20%20%3D%3D%3D%0ANot%20a%20header%0A%20%20%20%20---%0ANot%20a%20list%20item%20or%20a%20header%0A%20%20%20%20-%0A%0A------%0A%0A%3C!--%20Prettier%20stable%20--%3E%0ANot%20a%20header%0A%3D%3D%3D%0ANot%20a%20header%0A---%0ANot%20a%20list%20item%20or%20a%20header%20-%0A%0A------%0A%0A%3C!--%20Prettier%20main%20--%3E%0ANot%20a%20header%0A%5C%3D%3D%3D%0ANot%20a%20header%0A%5C---%0ANot%20a%20list%20item%20or%20a%20header%0A%5C-)
<!-- prettier-ignore -->
```markdown
<!-- Input -->
Not a header
===
Not a header
---
Not a list item or a header
-
<!-- Prettier stable -->
Not a header
===
Not a header
---
Not a list item or a header -
<!-- Prettier main -->
Not a header
\===
Not a header
\---
Not a list item or a header
\-
```
+20 -2
View File
@@ -22,6 +22,7 @@ import {
getFencedCodeBlockValue,
getNthListSiblingIndex,
isAutolink,
isNewLine,
isPrettierIgnore,
splitText,
} from "../utilities.js";
@@ -65,6 +66,19 @@ function hasFakeWhitespaceAfterNextToken(path) {
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;
@@ -116,7 +130,9 @@ function printMdast(path, options, print) {
case "sentence":
return printSentence(path, print);
case "word":
return options.parser !== "mdx" ? printWord(path) : printWordLegacy(path);
return options.parser !== "mdx"
? printWord(path, options)
: printWordLegacy(path);
case "whitespace": {
const { next } = path;
@@ -125,7 +141,9 @@ function printMdast(path, options, print) {
next &&
/^>|^(?:[*+-]|#{1,6}|\d+[).])$/.test(next.value) &&
// Avoid https://github.com/prettier/prettier/issues/18861
!hasFakeWhitespaceAfterNextToken(path)
!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;
+22 -9
View File
@@ -1,5 +1,7 @@
import { PUNCTUATION_REGEXP } from "../constants.evaluate.js";
import { isAutolink } from "../utilities.js";
import { isAutolink, isNewLine } from "../utilities.js";
const fakeSetextHeaderRegex = /^(?:=+|-+)$/;
/**
* @import AstPath from "../../common/ast-path.js"
@@ -8,25 +10,36 @@ import { isAutolink } from "../utilities.js";
/**
* @param {AstPath} path
* @param {*} options
* @return {Doc}
*/
function printWord(path) {
function printWord(path, options) {
const { node } = path;
const emphasisOrStrong = path.findAncestor(
(p) => p.type === "emphasis" || p.type === "strong",
);
if (!emphasisOrStrong) {
return node.value;
}
const { previous, next, grandparent } = path;
let text = node.value;
if (!emphasisOrStrong) {
if (
options.proseWrap === "preserve" &&
path.parent.type === "sentence" &&
fakeSetextHeaderRegex.test(text) &&
isNewLine(path.previous) &&
(path.isLast || isNewLine(path.next))
) {
// escape indented pseudo setext header, e.g. `Previous line↵␣␣␣␣===`
return `\\${text}`;
}
return text;
}
// escape leading `*` or `_` if it's the first character in an emphasis/strong
if (
path.isFirst &&
(text.startsWith("*") || text.startsWith("_")) &&
path.callParent(() => path.isFirst) &&
grandparent === emphasisOrStrong
path.grandparent === emphasisOrStrong
) {
text = `\\${text}`;
}
@@ -44,9 +57,9 @@ function printWord(path) {
}
if (
canOpenOrCloseStrongOrEmphasis(
preceding.at(-1) || previous?.value.at(-1),
preceding.at(-1) || path.previous?.value.at(-1),
delimiterRun,
following[0] || next?.value[0],
following[0] || path.next?.value[0],
)
) {
return `${preceding}\\${delimiterRun}${following}`;
+3
View File
@@ -321,6 +321,8 @@ function isSetextHeading(node) {
return start.line !== end.line;
}
const isNewLine = (node) => node?.type === "whitespace" && node.value === "\n";
export {
getFencedCodeBlockValue,
getNthListSiblingIndex,
@@ -330,6 +332,7 @@ export {
INLINE_NODE_TYPES,
INLINE_NODE_WRAPPER_TYPES,
isAutolink,
isNewLine,
isPrettierIgnore,
isSetextHeading,
KIND_CJ_LETTER,
@@ -124,6 +124,55 @@ Bar
================================================================================
`;
exports[`not-header.md format 1`] = `
====================================options=====================================
parsers: ["markdown"]
printWidth: 80 (default) |
=====================================input======================================
\\===
===
\\===
---
\\---
-
\\-
=
\\=
\\\\---
\\\\===
\\-
\\=
\\\\-
\\\\=
-foo ---**bar**===あ===
\\-
-
=====================================output=====================================
\\===
\\===
\\===
\\---
\\---
\\-
\\-
\\=
\\=
\\\\---
\\\\===
\\-
\\=
\\\\-
\\\\=
-foo ---**bar**===あ===
\\-
\\-
================================================================================
`;
exports[`setext.md format 1`] = `
====================================options=====================================
parsers: ["markdown"]
@@ -0,0 +1,19 @@
\===
===
\===
---
\---
-
\-
=
\=
\\---
\\===
\-
\=
\\-
\\=
-foo ---**bar**===あ===
\-
-