Avoid corrupting empty URL + title, or URL with <, (, \ (#19150)

Signed-off-by: Anders Kaseorg <andersk@mit.edu>
This commit is contained in:
Anders Kaseorg authored and GitHub committed 2026-06-03 06:11:47 +08:00
1 parent 37255cd58b
commit 98074bc210
6 files changed
+151 -46

No files matched your search

+15
View File
@@ -0,0 +1,15 @@
#### 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.
<!-- prettier-ignore -->
```md
<!-- Input -->
[link](<> "title"), ![image](<\<(\\>)
<!-- Prettier stable -->
[link]( "title"), ![image](<(\)
<!-- Prettier main -->
[link](<> "title"), ![image](<\<(\\>)
```
+22 -24
View File
@@ -1,5 +1,4 @@
import collapseWhiteSpace from "collapse-white-space";
import escapeStringRegexp from "escape-string-regexp";
import {
align,
DOC_TYPE_STRING,
@@ -206,7 +205,7 @@ function printMdast(path, options, print) {
"[",
printChildren(path, options, print),
"](",
printUrl(node.url, ")"),
node.url === "" && node.title !== null ? "<>" : printUrl(node.url),
printTitle(node.title, options),
")",
];
@@ -221,7 +220,7 @@ function printMdast(path, options, print) {
"![",
printImageAlt(node, options),
"](",
printUrl(node.url, ")"),
node.url === "" && node.title !== null ? "<>" : printUrl(node.url),
printTitle(node.title, options),
")",
];
@@ -489,30 +488,23 @@ 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, dangerousCharOrChars = []) {
const dangerousChars = [
" ",
...(Array.isArray(dangerousCharOrChars)
? dangerousCharOrChars
: [dangerousCharOrChars]),
];
return new RegExp(
dangerousChars.map((x) => escapeStringRegexp(x)).join("|"),
).test(url)
? `<${encodeUrl(url, "<>")}>`
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;
}
@@ -529,7 +521,13 @@ function printTitle(title, options, printSpace = true) {
title = title.replaceAll(/\\(?=["')])/g, "");
}
if (title.includes('"') && title.includes("'") && !title.includes(")")) {
if (
title.includes('"') &&
title.includes("'") &&
!title.includes("(") &&
!title.includes(")")
) {
title = title.replaceAll("\\", "\\\\");
return `(${title})`; // avoid escaped quotes
}
const quote = getPreferredQuote(title, options.singleQuote);
@@ -981,7 +981,7 @@ proseWrap: "always"
[link]((foo)and(bar))
=====================================output=====================================
[link](<(foo)and(bar)>)
[link]((foo)and(bar))
================================================================================
`;
@@ -995,7 +995,7 @@ proseWrap: "always"
[link](foo(and\\(bar\\)))
=====================================output=====================================
[link](<foo(and(bar))>)
[link](foo(and(bar)))
================================================================================
`;
@@ -1009,7 +1009,7 @@ proseWrap: "always"
[link](<foo(and(bar))>)
=====================================output=====================================
[link](<foo(and(bar))>)
[link](foo(and(bar)))
================================================================================
`;
@@ -8341,7 +8341,7 @@ proseWrap: "always"
[link](\\(foo\\))
=====================================output=====================================
[link](<(foo)>)
[link]((foo))
================================================================================
`;
@@ -8355,7 +8355,7 @@ proseWrap: "always"
[link](foo(and(bar)))
=====================================output=====================================
[link](<foo(and(bar))>)
[link](foo(and(bar)))
================================================================================
`;
@@ -57,28 +57,63 @@ singleQuote: true
[link](https://www.google.fr/foo-%3Ebar)
[link](https://www.google.fr/foo-<bar)
[link](https://www.google.fr/foo-%3Cbar)
[link](\\<)
[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-<bar)
![link](https://www.google.fr/foo-%3Cbar)
![link](\\<)
![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-<bar
[link]: https://www.google.fr/foo-%3Cbar
[link]: \\<
[link]: \\(
[link]: \\\\
[link]: < >
[link]: <> "title"
[link]: <>
=====================================output=====================================
[link](<https://www.google.fr/()foo-%3Ebar>)
[link](https://www.google.fr/foo->bar) [link](https://www.google.fr/foo-%3Ebar)
[link](https://www.google.fr/foo-<bar) [link](https://www.google.fr/foo-%3Cbar)
![link](<https://www.google.fr/()foo-%3Ebar>)
[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-<bar)
[link](https://www.google.fr/foo-%3Cbar) [link](<\\<>) [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-<bar)
![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-<bar [link]:
https://www.google.fr/foo-%3Cbar
![link](https://www.google.fr/foo-%3Cbar) ![link](<\\<>) ![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-<bar
[link]: https://www.google.fr/foo-%3Cbar
[link]: <\\<>
[link]: <(>
[link]: \\\\
[link]: < >
[link]: <> 'title'
[link]: <>
================================================================================
`;
@@ -94,28 +129,63 @@ proseWrap: "always"
[link](https://www.google.fr/foo-%3Ebar)
[link](https://www.google.fr/foo-<bar)
[link](https://www.google.fr/foo-%3Cbar)
[link](\\<)
[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-<bar)
![link](https://www.google.fr/foo-%3Cbar)
![link](\\<)
![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-<bar
[link]: https://www.google.fr/foo-%3Cbar
[link]: \\<
[link]: \\(
[link]: \\\\
[link]: < >
[link]: <> "title"
[link]: <>
=====================================output=====================================
[link](<https://www.google.fr/()foo-%3Ebar>)
[link](https://www.google.fr/foo->bar) [link](https://www.google.fr/foo-%3Ebar)
[link](https://www.google.fr/foo-<bar) [link](https://www.google.fr/foo-%3Cbar)
![link](<https://www.google.fr/()foo-%3Ebar>)
[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-<bar)
[link](https://www.google.fr/foo-%3Cbar) [link](<\\<>) [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-<bar)
![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-<bar [link]:
https://www.google.fr/foo-%3Cbar
![link](https://www.google.fr/foo-%3Cbar) ![link](<\\<>) ![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-<bar
[link]: https://www.google.fr/foo-%3Cbar
[link]: <\\<>
[link]: <(>
[link]: \\\\
[link]: < >
[link]: <> "title"
[link]: <>
================================================================================
`;
+23 -1
View File
@@ -3,13 +3,35 @@
[link](https://www.google.fr/foo-%3Ebar)
[link](https://www.google.fr/foo-<bar)
[link](https://www.google.fr/foo-%3Cbar)
[link](\<)
[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-<bar)
![link](https://www.google.fr/foo-%3Cbar)
![link](\<)
![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-<bar
[link]: https://www.google.fr/foo-%3Cbar
[link]: https://www.google.fr/foo-%3Cbar
[link]: \<
[link]: \(
[link]: \\
[link]: < >
[link]: <> "title"
[link]: <>