Compare commits

...

1 Commits

Author SHA1 Message Date
pash
eb4d5967a9 Harden local file link target rendering 2026-04-13 12:21:46 -07:00
2 changed files with 60 additions and 4 deletions

View File

@@ -581,15 +581,16 @@ where
}
fn push_link(&mut self, dest_url: String) {
let show_destination = should_render_link_destination(&dest_url);
let destination = normalize_local_link_destination(&dest_url);
let show_destination = should_render_link_destination(&destination);
self.link = Some(LinkState {
show_destination,
local_target_display: if is_local_path_like_link(&dest_url) {
render_local_link_target(&dest_url, self.cwd.as_deref())
local_target_display: if is_local_path_like_link(&destination) {
render_local_link_target(&destination, self.cwd.as_deref())
} else {
None
},
destination: dest_url,
destination,
});
}
@@ -740,6 +741,27 @@ fn is_local_path_like_link(dest_url: &str) -> bool {
)
}
fn normalize_local_link_destination(dest_url: &str) -> String {
let mut candidate = dest_url.trim();
if let Some(inner) = strip_balanced_wrapper(candidate, '`', '`') {
candidate = inner.trim();
}
if let Some(inner) = strip_balanced_wrapper(candidate, '<', '>') {
candidate = inner.trim();
}
if is_local_path_like_link(candidate) {
candidate.to_string()
} else {
dest_url.to_string()
}
}
fn strip_balanced_wrapper(value: &str, start: char, end: char) -> Option<&str> {
value
.strip_prefix(start)
.and_then(|inner| inner.strip_suffix(end))
}
/// Parse a local link target into normalized path text plus an optional location suffix.
///
/// This accepts the path shapes Codex emits today: `file://` URLs, absolute and relative paths,

View File

@@ -700,6 +700,40 @@ fn file_link_appends_line_number_when_label_lacks_it() {
assert_eq!(text, expected);
}
#[test]
fn file_link_uses_target_when_label_is_code() {
let text = render_markdown_text_for_cwd(
"[`markdown_render.rs`](/Users/example/code/codex/codex-rs/tui/src/markdown_render.rs:74)",
Path::new("/Users/example/code/codex"),
);
let expected = Text::from(Line::from_iter([
"codex-rs/tui/src/markdown_render.rs:74".cyan(),
]));
assert_eq!(text, expected);
}
#[test]
fn file_link_uses_target_wrapped_in_backticks() {
let text = render_markdown_text_for_cwd(
"[markdown_render.rs](`/Users/example/code/codex/codex-rs/tui/src/markdown_render.rs:74`)",
Path::new("/Users/example/code/codex"),
);
let expected = Text::from(Line::from_iter([
"codex-rs/tui/src/markdown_render.rs:74".cyan(),
]));
assert_eq!(text, expected);
}
#[test]
fn file_link_uses_angle_wrapped_target_with_spaces() {
let text = render_markdown_text_for_cwd(
"[My Report.md](</Users/example/code/codex/docs/My Report.md:12>)",
Path::new("/Users/example/code/codex"),
);
let expected = Text::from(Line::from_iter(["docs/My Report.md:12".cyan()]));
assert_eq!(text, expected);
}
#[test]
fn file_link_keeps_absolute_paths_outside_cwd() {
let text = render_markdown_text_for_cwd(