Improve > handling in blockquote in Markdown (#19802)

Co-authored-by: codex <codex@openai.com>
Co-authored-by: fisker Cheung <lionkay@gmail.com>
Co-authored-by: fisker <lionkay@gmail.com>
This commit is contained in:
authored and fisker committed 2026-09-16 15:37:43 +08:00
1 parent b766f660b1
commit ca76fda52d
10 files changed
+304 -13

No files matched your search

+13
View File
@@ -0,0 +1,13 @@
#### Fix Markdown blockquote containing `>` characters (#19802 by @seiyab)
<!-- prettier-ignore -->
```md
<!-- Input -->
> **test**\>
<!-- Prettier stable -->
> **test**>\>
<!-- Prettier main -->
> **test**\>
```
+1 -13
View File
@@ -1,5 +1,6 @@
import htmlWhitespace from "../../utilities/html-whitespace.js";
import { getOrderedListItemInfo, mapAst, splitText } from "../utilities.js";
import { getBlockquoteRawText } from "../utilities/get-blockquote-raw-text.js";
// 0x0 ~ 0x10ffff
const isSingleCharRegex = /^\\?.$/su;
@@ -248,19 +249,6 @@ function splitTextIntoSentences(ast) {
}
}
function getBlockquoteRawText(text, node) {
const angleBracketsRegex = /^([ \t]*>[ \t]*)*/;
const rawLines = text.split("\n");
const valueLines = node.value.split("\n");
const resultLines = rawLines.map((rawLine, index) => {
const valueLine = valueLines[index] ?? "";
const leadingTextAngleBrackets =
valueLine.match(angleBracketsRegex)[0] ?? "";
return rawLine.replace(angleBracketsRegex, leadingTextAngleBrackets);
});
return resultLines.join("\n");
}
function transformIndentedCodeblock(ast, options) {
return mapAst(ast, (node) => {
if (node.type !== "code") {
@@ -0,0 +1,67 @@
/**
* @param line {string}
* @returns {number}
*/
function countValueLeadingGreaterThan(line) {
let count = 0;
for (const character of line) {
if (character === ">") {
count++;
} else if (character !== " " && character !== "\t") {
break;
}
}
return count;
}
/**
* @param line {string}
* @param greaterThanToKeep {number}
* @returns {string}
*/
function removeBlockquoteMarkers(line, greaterThanToKeep) {
const rawBlockquoteRegexp =
/[ \t]*(?:>|\\>|&gt;|&GT;|&#0*62;|&#[xX]0*3[eE];)[ \t]*/y;
/** @type {Array<number>} */
const greaterThanIndexes = [];
let { lastIndex } = rawBlockquoteRegexp;
let match;
while ((match = rawBlockquoteRegexp.exec(line))) {
greaterThanIndexes.push(match.index);
lastIndex = rawBlockquoteRegexp.lastIndex;
}
if (greaterThanToKeep === 0) {
return line.slice(lastIndex);
}
const firstGreaterThanToKeep = greaterThanIndexes.at(-greaterThanToKeep);
/* c8 ignore next 3 */
if (firstGreaterThanToKeep === null) {
return line;
}
return line.slice(firstGreaterThanToKeep);
}
const newLineRegexp = /(?<!\\)(?:\\\\)*(?:&NewLine;|&#0*10;|&#[Xx]0*[Aa];)/g;
function getBlockquoteRawText(text, node) {
const rawLines = text.split("\n");
const valueLines = node.value.split("\n");
let valueIndex = 0;
const resultLines = rawLines.map((rawLine, index) => {
const valueLine = valueLines[valueIndex++];
const decodedNewlines = rawLine.matchAll(newLineRegexp);
valueIndex += [...decodedNewlines].length;
if (index === 0) {
return rawLine;
}
const valueMarkerCount = countValueLeadingGreaterThan(valueLine);
return removeBlockquoteMarkers(rawLine, valueMarkerCount);
});
return resultLines.join("\n");
}
export { getBlockquoteRawText };
@@ -0,0 +1,162 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`encoded-greater-than.md - {"proseWrap":"always"} format 1`] = `
====================================options=====================================
parsers: ["markdown"]
proseWrap: "always"
printWidth: 80 (default) |
=====================================input======================================
> > a
> &gt; b
> > a
> &#62; b
> > a
> &#x3e; b
> > a
> &GT; b
> > a
> &#062; b
> > a
> &#x3E; b
=====================================output=====================================
> > a &gt; b
> > a &#62; b
> > a &#x3e; b
> > a &GT; b
> > a &#062; b
> > a &#x3E; b
================================================================================
`;
exports[`encoded-newline.md - {"proseWrap":"always"} format 1`] = `
====================================options=====================================
parsers: ["markdown"]
proseWrap: "always"
printWidth: 80 (default) |
=====================================input======================================
> a &#10; b
> &gt; c
> a &#xA; b
> &#62; c
> a &NewLine; b
> \\> c
> a &#10; b
> c
> a &newline; b
> \\> c
> a \\&NewLine; b
> \\> c
> a \\\\&NewLine; b
> \\> c
=====================================output=====================================
> a &#10; b &gt; c
> a &#xA; b &#62; c
> a &NewLine; b \\> c
> a &#10; b > c
> a &newline; b \\> c
> a \\&NewLine; b \\> c
> a \\\\&NewLine; b \\> c
================================================================================
`;
exports[`escaped-greater-than.md - {"proseWrap":"always"} format 1`] = `
====================================options=====================================
parsers: ["markdown"]
proseWrap: "always"
printWidth: 80 (default) |
=====================================input======================================
> \\>
> \\>
> *text* > \\>
> abc
> \\> abc
=====================================output=====================================
> \\> \\>
> _text_ > \\>
> abc \\> abc
================================================================================
`;
exports[`indented-greater-than.md - {"proseWrap":"always"} format 1`] = `
====================================options=====================================
parsers: ["markdown"]
proseWrap: "always"
printWidth: 80 (default) |
=====================================input======================================
> first
> second
> first
> second
> > first
> second
> first
>> second
=====================================output=====================================
> first > second
> first > second
> > first > second
> first >> second
================================================================================
`;
exports[`issue-19528.md - {"proseWrap":"always"} format 1`] = `
====================================options=====================================
parsers: ["markdown"]
proseWrap: "always"
printWidth: 80 (default) |
=====================================input======================================
> test\\>
> **test**\\>
> \\>
=====================================output=====================================
> test\\>
> **test**\\>
> \\>
================================================================================
`;
@@ -0,0 +1,17 @@
> > a
> &gt; b
> > a
> &#62; b
> > a
> &#x3e; b
> > a
> &GT; b
> > a
> &#062; b
> > a
> &#x3E; b
@@ -0,0 +1,20 @@
> a &#10; b
> &gt; c
> a &#xA; b
> &#62; c
> a &NewLine; b
> \> c
> a &#10; b
> c
> a &newline; b
> \> c
> a \&NewLine; b
> \> c
> a \\&NewLine; b
> \> c
@@ -0,0 +1,7 @@
> \>
> \>
> *text* > \>
> abc
> \> abc
@@ -0,0 +1 @@
runFormatTest(import.meta, ["markdown"], { proseWrap: "always" });
@@ -0,0 +1,11 @@
> first
> second
> first
> second
> > first
> second
> first
>> second
@@ -0,0 +1,5 @@
> test\>
> **test**\>
> \>