fix(markdown): stop escaped link destinations from swallowing later content (#1630)

This commit is contained in:
Hampus
2026-08-15 15:48:58 +02:00
committed by GitHub
parent 235399cfd6
commit 2d51600b6c
2 changed files with 66 additions and 5 deletions
+29 -5
View File
@@ -412,10 +412,7 @@ fn extract_escaped_url(text: &str, start: usize) -> Option<UrlInfo<'_>> {
if byte_at(text, pos) == b'>' {
let url = &text[start..pos];
pos += 1;
while pos < text.len() && byte_at(text, pos) != b')' {
pos += advance_one(text, pos);
}
if pos >= text.len() {
if pos >= text.len() || byte_at(text, pos) != b')' {
return None;
}
return Some(UrlInfo {
@@ -1410,7 +1407,34 @@ pub fn max_inline_scan() -> usize {
#[cfg(test)]
mod tests {
use super::has_valid_code_fence_language;
use super::{extract_escaped_url, has_valid_code_fence_language};
#[test]
fn escaped_url_accepts_paren_immediately_after_destination() {
let info = extract_escaped_url("https://example.com>) trailing", 0).expect("valid");
assert_eq!(info.url, "https://example.com");
assert!(info.escaped);
assert_eq!(info.advance_by, "https://example.com>)".len());
}
#[test]
fn escaped_url_rejects_content_between_destination_and_paren() {
assert!(extract_escaped_url("https://example.com> and more)", 0).is_none());
assert!(extract_escaped_url("https://example.com> )", 0).is_none());
assert!(extract_escaped_url("https://example.com>text)", 0).is_none());
}
#[test]
fn escaped_url_rejects_missing_paren() {
assert!(extract_escaped_url("https://example.com>", 0).is_none());
assert!(extract_escaped_url("https://example.com", 0).is_none());
}
#[test]
fn escaped_url_keeps_paren_inside_destination() {
let info = extract_escaped_url("https://example.com/a.yaml)>)", 0).expect("valid");
assert_eq!(info.url, "https://example.com/a.yaml)");
}
#[test]
fn rejects_leading_whitespace() {
@@ -259,6 +259,43 @@ fn native_parser_rejects_apostrophe_in_masked_link_authority() {
);
}
#[test]
fn native_parser_keeps_content_after_unterminated_escaped_destination() {
let flags = ParserFlags::ALLOW_MASKED_LINKS | ParserFlags::ALLOW_AUTOLINKS;
assert_eq!(
parse(
"a [x](<https://example.com/y)> b [e](<https://example.com/f>)",
flags,
""
),
json!({"nodes":[
{"type":"Text","content":"a [x]("},
{"type":"Link","url":"https://example.com/y)","escaped":true,"rawUrl":"https://example.com/y)","source":"<https://example.com/y)>"},
{"type":"Text","content":" b "},
{"type":"Link","text":{"type":"Text","content":"e"},"url":"https://example.com/f","escaped":true,"rawUrl":"https://example.com/f","source":"[e](<https://example.com/f>)"}
]})
);
assert_eq!(
parse("[bad](<https://example.com/a> )", flags, ""),
json!({"nodes":[
{"type":"Text","content":"[bad]("},
{"type":"Link","url":"https://example.com/a","escaped":true,"rawUrl":"https://example.com/a","source":"<https://example.com/a>"},
{"type":"Text","content":" )"}
]})
);
}
#[test]
fn native_parser_still_accepts_well_formed_escaped_destinations() {
let flags = ParserFlags::ALLOW_MASKED_LINKS | ParserFlags::ALLOW_AUTOLINKS;
assert_eq!(
parse("[ok](<https://example.com/a>)", flags, ""),
json!({"nodes":[
{"type":"Link","text":{"type":"Text","content":"ok"},"url":"https://example.com/a","escaped":true,"rawUrl":"https://example.com/a","source":"[ok](<https://example.com/a>)"}
]})
);
}
#[test]
fn native_parser_rejects_masked_links_without_visible_text() {
for input in [