Skip to content

Commit 1659e4d

Browse files
authored
Merge pull request #605 from kivikakk/push-wzoqutxutvtr
cm::escape_link_destination: add function to escape URL for use as link destination.
2 parents 61e4f3c + 9029d85 commit 1659e4d

File tree

6 files changed

+72
-3
lines changed

6 files changed

+72
-3
lines changed

Cargo.lock

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ caseless = "0.2.1"
4848

4949
[dev-dependencies]
5050
ntest = "0.9"
51+
percent-encoding-rfc3986 = "0.1.3"
5152
strum = { version = "0.26.3", features = ["derive"] }
5253
toml = "0.7.3"
5354

changelog.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ New APIs:
55
* `cm::escape_inline` (aliased at crate level as `escape_commonmark_inline`)
66
is added; escapes input text suitable for inclusion in a CommonMark document
77
where regular inline processing takes place.
8+
* `cm::escape_link_destination` (aliased at crate level as
9+
`escape_commonmark_link_destination`) is added; escapes input URL suitable for
10+
use as a link destination in a CommonMark document.
811

912
Changed APIs:
1013

src/cm.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1083,3 +1083,27 @@ pub fn escape_inline(text: &str) -> String {
10831083

10841084
result
10851085
}
1086+
1087+
/// Escapes the input URL, rendering it suitable for inclusion as a [link
1088+
/// destination] per the CommonMark spec.
1089+
///
1090+
/// [link destination]: https://spec.commonmark.org/0.31.2/#link-destination
1091+
pub fn escape_link_destination(url: &str) -> String {
1092+
let mut result = String::with_capacity(url.len() * 3 / 2);
1093+
1094+
result.push('<');
1095+
for c in url.chars() {
1096+
match c {
1097+
'<' | '>' => {
1098+
result.push('\\');
1099+
result.push(c);
1100+
}
1101+
'\n' => result.push_str("%0A"),
1102+
'\r' => result.push_str("%0D"),
1103+
_ => result.push(c),
1104+
}
1105+
}
1106+
result.push('>');
1107+
1108+
result
1109+
}

src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ mod tests;
8282
mod xml;
8383

8484
pub use cm::escape_inline as escape_commonmark_inline;
85+
pub use cm::escape_link_destination as escape_commonmark_link_destination;
8586
pub use cm::format_document as format_commonmark;
8687
pub use cm::format_document_with_plugins as format_commonmark_with_plugins;
8788
pub use html::format_document as format_html;

src/tests/escape.rs

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
use crate::{cm::escape_inline, entity, markdown_to_html, Options};
1+
use crate::cm::{escape_inline, escape_link_destination};
2+
use crate::{entity, markdown_to_html, Options};
23

3-
/// Assert that the input escapes to the expected result, and that the expected
4-
/// result renders to HTML which displays the input text.
4+
/// Assert that the input text escapes to the expected result in inline context,
5+
/// and that the expected result renders to HTML which displays the input text.
56
#[track_caller]
67
fn assert_escape_inline(input: &str, expected: &str) {
78
let actual = escape_inline(input);
@@ -34,3 +35,35 @@ fn escape_inline_baseline() {
3435
r#"some \<\"complicated\"\> \& '/problematic\\' input"#,
3536
);
3637
}
38+
39+
/// Assert that the URL is escaped as expected, and that the result is rendered
40+
/// into HTML in such a way that preserves the meaning of the input.
41+
///
42+
/// [link destination]: https://spec.commonmark.org/0.31.2/#link-destination
43+
#[test]
44+
fn escape_link_target() {
45+
let url = "rabbits) <cup\rcakes\n> [hyacinth](";
46+
let escaped = r#"<rabbits) \<cup%0Dcakes%0A\> [hyacinth](>"#;
47+
48+
assert_eq!(escaped, escape_link_destination(url));
49+
50+
let md = format!("[link]({escaped})");
51+
let mut html = markdown_to_html(&md, &Options::default());
52+
html = html
53+
.strip_prefix("<p><a href=\"")
54+
.expect("html should be one anchor in a paragraph")
55+
.to_string();
56+
html = html
57+
.strip_suffix("\">link</a></p>\n")
58+
.expect("html should be one anchor in a paragraph")
59+
.to_string();
60+
61+
assert_eq!("rabbits)%20%3Ccup%0Dcakes%0A%3E%20%5Bhyacinth%5D(", html);
62+
assert_eq!(
63+
url,
64+
percent_encoding_rfc3986::percent_decode_str(&html)
65+
.unwrap()
66+
.decode_utf8()
67+
.unwrap()
68+
);
69+
}

0 commit comments

Comments
 (0)