diff --git a/changelog_unreleased/markdown/19150.md b/changelog_unreleased/markdown/19150.md deleted file mode 100644 index bc600e1f0..000000000 --- a/changelog_unreleased/markdown/19150.md +++ /dev/null @@ -1,15 +0,0 @@ -#### Fix URL formatting edge cases (#19150 by @andersk) - -Avoid corrupting a link, image, or link reference definition with an empty URL and a title, or with certain characters like `<`, `(`, `\`, by using `<...>` wrapping and `\` escaping as appropriate. - - -```md - -[link](<> "title"), ![image](<\<(\\>) - - -[link]( "title"), ![image](<(\) - - -[link](<> "title"), ![image](<\<(\\>) -``` diff --git a/src/language-markdown/print/mdast.js b/src/language-markdown/print/mdast.js index 21205f44a..e70751cc8 100644 --- a/src/language-markdown/print/mdast.js +++ b/src/language-markdown/print/mdast.js @@ -1,4 +1,5 @@ import collapseWhiteSpace from "collapse-white-space"; +import escapeStringRegexp from "escape-string-regexp"; import { align, DOC_TYPE_STRING, @@ -223,7 +224,7 @@ function printMdast(path, options, print) { "[", printChildren(path, options, print), "](", - node.url === "" && node.title !== null ? "<>" : printUrl(node.url), + printUrl(node.url, ")"), printTitle(node.title, options), ")", ]; @@ -238,7 +239,7 @@ function printMdast(path, options, print) { "![", printImageAlt(node, options), "](", - node.url === "" && node.title !== null ? "<>" : printUrl(node.url), + printUrl(node.url, ")"), printTitle(node.title, options), ")", ]; @@ -506,23 +507,30 @@ function shouldRemainTheSameContent(path) { ); } +const encodeUrl = (url, characters) => { + for (const character of characters) { + url = url.replaceAll(character, encodeURIComponent(character)); + } + return url; +}; + /** * @param {string} url + * @param {string[] | string} [dangerousCharOrChars] * @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 ]|^])/g, String.raw`\$1`)}>` +function printUrl(url, dangerousCharOrChars = []) { + const dangerousChars = [ + " ", + ...(Array.isArray(dangerousCharOrChars) + ? dangerousCharOrChars + : [dangerousCharOrChars]), + ]; + + return new RegExp( + dangerousChars.map((x) => escapeStringRegexp(x)).join("|"), + ).test(url) + ? `<${encodeUrl(url, "<>")}>` : url; } @@ -539,13 +547,7 @@ function printTitle(title, options, printSpace = true) { title = title.replaceAll(/\\(?=["')])/g, ""); } - if ( - title.includes('"') && - title.includes("'") && - !title.includes("(") && - !title.includes(")") - ) { - title = title.replaceAll("\\", "\\\\"); + if (title.includes('"') && title.includes("'") && !title.includes(")")) { return `(${title})`; // avoid escaped quotes } const quote = getPreferredQuote(title, options.singleQuote); diff --git a/tests/format/markdown/commonmark-test-suite-legacy/__snapshots__/format.test.js.snap b/tests/format/markdown/commonmark-test-suite-legacy/__snapshots__/format.test.js.snap index 1db6092de..5198dbda4 100644 --- a/tests/format/markdown/commonmark-test-suite-legacy/__snapshots__/format.test.js.snap +++ b/tests/format/markdown/commonmark-test-suite-legacy/__snapshots__/format.test.js.snap @@ -982,7 +982,7 @@ proseWrap: "always" [link]((foo)and(bar)) =====================================output===================================== -[link]((foo)and(bar)) +[link](<(foo)and(bar)>) ================================================================================ `; @@ -996,7 +996,7 @@ proseWrap: "always" [link](foo(and\\(bar\\))) =====================================output===================================== -[link](foo(and(bar))) +[link]() ================================================================================ `; @@ -1010,7 +1010,7 @@ proseWrap: "always" [link]() =====================================output===================================== -[link](foo(and(bar))) +[link]() ================================================================================ `; diff --git a/tests/format/markdown/commonmark-test-suite/__snapshots__/format.test.js.snap b/tests/format/markdown/commonmark-test-suite/__snapshots__/format.test.js.snap index c1aa7636c..9a3664f8d 100644 --- a/tests/format/markdown/commonmark-test-suite/__snapshots__/format.test.js.snap +++ b/tests/format/markdown/commonmark-test-suite/__snapshots__/format.test.js.snap @@ -8340,7 +8340,7 @@ proseWrap: "always" [link](\\(foo\\)) =====================================output===================================== -[link]((foo)) +[link](<(foo)>) ================================================================================ `; @@ -8354,7 +8354,7 @@ proseWrap: "always" [link](foo(and(bar))) =====================================output===================================== -[link](foo(and(bar))) +[link]() ================================================================================ `; diff --git a/tests/format/markdown/link/__snapshots__/format.test.js.snap b/tests/format/markdown/link/__snapshots__/format.test.js.snap index 5541b91b0..ee4b4539a 100644 --- a/tests/format/markdown/link/__snapshots__/format.test.js.snap +++ b/tests/format/markdown/link/__snapshots__/format.test.js.snap @@ -57,63 +57,28 @@ singleQuote: true [link](https://www.google.fr/foo-%3Ebar) [link](https://www.google.fr/foo-) -[link](<> "title") -[link](<>) -[link]() - ![link](https://www.google.fr/()foo->bar) ![link](https://www.google.fr/foo->bar) ![link](https://www.google.fr/foo-%3Ebar) ![link](https://www.google.fr/foo-) -![link](<> "title") -![link](<>) -![link]() - [link]: https://www.google.fr/()foo->bar [link]: https://www.google.fr/foo->bar [link]: https://www.google.fr/foo-%3Ebar [link]: https://www.google.fr/foo- -[link]: <> "title" -[link]: <> - =====================================output===================================== -[link](https://www.google.fr/()foo->bar) [link](https://www.google.fr/foo->bar) -[link](https://www.google.fr/foo-%3Ebar) [link](https://www.google.fr/foo-) [link](<(>) [link](\\\\) -[link](< >) [link](<> 'title') [link]() [link]() - -![link](https://www.google.fr/()foo->bar) +[link]() +[link](https://www.google.fr/foo->bar) [link](https://www.google.fr/foo-%3Ebar) +[link](https://www.google.fr/foo-) ![link](https://www.google.fr/foo->bar) ![link](https://www.google.fr/foo-%3Ebar) ![link](https://www.google.fr/foo-) ![link](<(>) ![link](\\\\) -![link](< >) ![link](<> 'title') ![link]() ![link]() - -[link]: https://www.google.fr/()foo->bar -[link]: https://www.google.fr/foo->bar -[link]: https://www.google.fr/foo-%3Ebar -[link]: https://www.google.fr/foo- -[link]: <(> -[link]: \\\\ -[link]: < > -[link]: <> 'title' -[link]: <> +![link](https://www.google.fr/foo-%3Cbar) [link]: +https://www.google.fr/()foo->bar [link]: https://www.google.fr/foo->bar [link]: +https://www.google.fr/foo-%3Ebar [link]: https://www.google.fr/foo-) -[link](<> "title") -[link](<>) -[link]() - ![link](https://www.google.fr/()foo->bar) ![link](https://www.google.fr/foo->bar) ![link](https://www.google.fr/foo-%3Ebar) ![link](https://www.google.fr/foo-) -![link](<> "title") -![link](<>) -![link]() - [link]: https://www.google.fr/()foo->bar [link]: https://www.google.fr/foo->bar [link]: https://www.google.fr/foo-%3Ebar [link]: https://www.google.fr/foo- -[link]: <> "title" -[link]: <> - =====================================output===================================== -[link](https://www.google.fr/()foo->bar) [link](https://www.google.fr/foo->bar) -[link](https://www.google.fr/foo-%3Ebar) [link](https://www.google.fr/foo-) [link](<(>) [link](\\\\) -[link](< >) [link](<> "title") [link]() [link]() - -![link](https://www.google.fr/()foo->bar) +[link]() +[link](https://www.google.fr/foo->bar) [link](https://www.google.fr/foo-%3Ebar) +[link](https://www.google.fr/foo-) ![link](https://www.google.fr/foo->bar) ![link](https://www.google.fr/foo-%3Ebar) ![link](https://www.google.fr/foo-) ![link](<(>) ![link](\\\\) -![link](< >) ![link](<> "title") ![link]() ![link]() - -[link]: https://www.google.fr/()foo->bar -[link]: https://www.google.fr/foo->bar -[link]: https://www.google.fr/foo-%3Ebar -[link]: https://www.google.fr/foo- -[link]: <(> -[link]: \\\\ -[link]: < > -[link]: <> "title" -[link]: <> +![link](https://www.google.fr/foo-%3Cbar) [link]: +https://www.google.fr/()foo->bar [link]: https://www.google.fr/foo->bar [link]: +https://www.google.fr/foo-%3Ebar [link]: https://www.google.fr/foo-) -[link](<> "title") -[link](<>) -[link]() - ![link](https://www.google.fr/()foo->bar) ![link](https://www.google.fr/foo->bar) ![link](https://www.google.fr/foo-%3Ebar) ![link](https://www.google.fr/foo-) -![link](<> "title") -![link](<>) -![link]() - [link]: https://www.google.fr/()foo->bar [link]: https://www.google.fr/foo->bar [link]: https://www.google.fr/foo-%3Ebar [link]: https://www.google.fr/foo- -[link]: <> "title" -[link]: <> +[link]: https://www.google.fr/foo-%3Cbar \ No newline at end of file