diff --git a/changelog_unreleased/markdown/19802.md b/changelog_unreleased/markdown/19802.md new file mode 100644 index 000000000..b3e670c80 --- /dev/null +++ b/changelog_unreleased/markdown/19802.md @@ -0,0 +1,13 @@ +#### Fix Markdown blockquote containing `>` characters (#19802 by @seiyab) + + +```md + +> **test**\> + + +> **test**>\> + + +> **test**\> +``` diff --git a/src/language-markdown/print/preprocess.js b/src/language-markdown/print/preprocess.js index 84b203131..d521e31cc 100644 --- a/src/language-markdown/print/preprocess.js +++ b/src/language-markdown/print/preprocess.js @@ -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") { diff --git a/src/language-markdown/utilities/get-blockquote-raw-text.js b/src/language-markdown/utilities/get-blockquote-raw-text.js new file mode 100644 index 000000000..8c9279bb3 --- /dev/null +++ b/src/language-markdown/utilities/get-blockquote-raw-text.js @@ -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]*(?:>|\\>|>|>|�*62;|&#[xX]0*3[eE];)[ \t]*/y; + /** @type {Array} */ + 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 = /(? { + 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 }; diff --git a/tests/format/markdown/blockquote/prose-wrap-always/__snapshots__/format.test.js.snap b/tests/format/markdown/blockquote/prose-wrap-always/__snapshots__/format.test.js.snap new file mode 100644 index 000000000..c1b9abfa9 --- /dev/null +++ b/tests/format/markdown/blockquote/prose-wrap-always/__snapshots__/format.test.js.snap @@ -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 +> > b + +> > a +> > b + +> > a +> > b + +> > a +> > b + +> > a +> > b + +> > a +> > b + +=====================================output===================================== +> > a > b + +> > a > b + +> > a > b + +> > a > b + +> > a > b + +> > a > b + +================================================================================ +`; + +exports[`encoded-newline.md - {"proseWrap":"always"} format 1`] = ` +====================================options===================================== +parsers: ["markdown"] +proseWrap: "always" + printWidth: 80 (default) | +=====================================input====================================== +> a b +> > c + +> a b +> > c + +> a b +> \\> c + +> a b + > c + +> a &newline; b +> \\> c + +> a \\ b +> \\> c + +> a \\\\ b +> \\> c + +=====================================output===================================== +> a b > c + +> a b > c + +> a b \\> c + +> a b > c + +> a &newline; b \\> c + +> a \\ b \\> c + +> a \\\\ 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**\\> + +> \\> + +================================================================================ +`; diff --git a/tests/format/markdown/blockquote/prose-wrap-always/encoded-greater-than.md b/tests/format/markdown/blockquote/prose-wrap-always/encoded-greater-than.md new file mode 100644 index 000000000..be0031e9b --- /dev/null +++ b/tests/format/markdown/blockquote/prose-wrap-always/encoded-greater-than.md @@ -0,0 +1,17 @@ +> > a +> > b + +> > a +> > b + +> > a +> > b + +> > a +> > b + +> > a +> > b + +> > a +> > b diff --git a/tests/format/markdown/blockquote/prose-wrap-always/encoded-newline.md b/tests/format/markdown/blockquote/prose-wrap-always/encoded-newline.md new file mode 100644 index 000000000..b901e9021 --- /dev/null +++ b/tests/format/markdown/blockquote/prose-wrap-always/encoded-newline.md @@ -0,0 +1,20 @@ +> a b +> > c + +> a b +> > c + +> a b +> \> c + +> a b + > c + +> a &newline; b +> \> c + +> a \ b +> \> c + +> a \\ b +> \> c diff --git a/tests/format/markdown/blockquote/prose-wrap-always/escaped-greater-than.md b/tests/format/markdown/blockquote/prose-wrap-always/escaped-greater-than.md new file mode 100644 index 000000000..23fe71348 --- /dev/null +++ b/tests/format/markdown/blockquote/prose-wrap-always/escaped-greater-than.md @@ -0,0 +1,7 @@ +> \> +> \> + +> *text* > \> + +> abc +> \> abc diff --git a/tests/format/markdown/blockquote/prose-wrap-always/format.test.js b/tests/format/markdown/blockquote/prose-wrap-always/format.test.js new file mode 100644 index 000000000..6ecc0af19 --- /dev/null +++ b/tests/format/markdown/blockquote/prose-wrap-always/format.test.js @@ -0,0 +1 @@ +runFormatTest(import.meta, ["markdown"], { proseWrap: "always" }); diff --git a/tests/format/markdown/blockquote/prose-wrap-always/indented-greater-than.md b/tests/format/markdown/blockquote/prose-wrap-always/indented-greater-than.md new file mode 100644 index 000000000..72a302ffe --- /dev/null +++ b/tests/format/markdown/blockquote/prose-wrap-always/indented-greater-than.md @@ -0,0 +1,11 @@ +> first + > second + +> first + > second + +> > first + > second + +> first + >> second diff --git a/tests/format/markdown/blockquote/prose-wrap-always/issue-19528.md b/tests/format/markdown/blockquote/prose-wrap-always/issue-19528.md new file mode 100644 index 000000000..1d1aa74b9 --- /dev/null +++ b/tests/format/markdown/blockquote/prose-wrap-always/issue-19528.md @@ -0,0 +1,5 @@ +> test\> + +> **test**\> + +> \>