mirror of
https://github.com/openai/codex.git
synced 2026-09-06 15:29:32 +00:00
fix(tui): keep markdown table links inline
This commit is contained in:
@@ -285,7 +285,13 @@ where
|
||||
Tag::Emphasis => self.push_inline_style(self.styles.emphasis),
|
||||
Tag::Strong => self.push_inline_style(self.styles.strong),
|
||||
Tag::Strikethrough => self.push_inline_style(self.styles.strikethrough),
|
||||
Tag::Link { dest_url, .. } => self.push_link(dest_url.to_string()),
|
||||
Tag::Link { dest_url, .. } => {
|
||||
if let Some(table) = self.table.as_mut() {
|
||||
table.start_link(dest_url.to_string());
|
||||
} else {
|
||||
self.push_link(dest_url.to_string());
|
||||
}
|
||||
}
|
||||
Tag::Table(_) => self.start_table(),
|
||||
Tag::TableHead | Tag::TableRow => self.start_table_row(),
|
||||
Tag::TableCell => self.start_table_cell(),
|
||||
@@ -313,7 +319,13 @@ where
|
||||
self.pending_marker_line = false;
|
||||
}
|
||||
TagEnd::Emphasis | TagEnd::Strong | TagEnd::Strikethrough => self.pop_inline_style(),
|
||||
TagEnd::Link => self.pop_link(),
|
||||
TagEnd::Link => {
|
||||
if let Some(table) = self.table.as_mut() {
|
||||
table.end_link();
|
||||
} else {
|
||||
self.pop_link();
|
||||
}
|
||||
}
|
||||
TagEnd::Table => self.end_table(),
|
||||
TagEnd::TableHead | TagEnd::TableRow => self.end_table_row(),
|
||||
TagEnd::TableCell => self.end_table_cell(),
|
||||
@@ -497,6 +509,10 @@ where
|
||||
}
|
||||
|
||||
fn html(&mut self, html: CowStr<'a>, inline: bool) {
|
||||
if let Some(table) = self.table.as_mut() {
|
||||
table.push_html(&html);
|
||||
return;
|
||||
}
|
||||
if self.suppressing_local_link_label() {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ pub(super) struct TableState {
|
||||
current_row: Vec<String>,
|
||||
current_cell: String,
|
||||
in_cell: bool,
|
||||
current_link_destination: Option<String>,
|
||||
}
|
||||
|
||||
impl TableState {
|
||||
@@ -26,7 +27,35 @@ impl TableState {
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn push_html(&mut self, html: &str) {
|
||||
let trimmed = html.trim();
|
||||
if matches!(
|
||||
trimmed.to_ascii_lowercase().as_str(),
|
||||
"<br>" | "<br/>" | "<br />"
|
||||
) {
|
||||
self.push_text("\n");
|
||||
} else {
|
||||
self.push_text(html);
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn start_link(&mut self, destination: String) {
|
||||
self.current_link_destination = Some(destination);
|
||||
}
|
||||
|
||||
pub(super) fn end_link(&mut self) {
|
||||
let Some(destination) = self.current_link_destination.take() else {
|
||||
return;
|
||||
};
|
||||
if self.in_cell && !destination.is_empty() {
|
||||
self.current_cell.push_str(" (");
|
||||
self.current_cell.push_str(&destination);
|
||||
self.current_cell.push(')');
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn end_cell(&mut self) {
|
||||
self.current_link_destination = None;
|
||||
self.current_row
|
||||
.push(std::mem::take(&mut self.current_cell));
|
||||
self.in_cell = false;
|
||||
@@ -299,18 +328,28 @@ fn wrap_table_cell(cell: &str, width: usize, hard_wrap: bool) -> Vec<String> {
|
||||
if cell.is_empty() {
|
||||
return vec![String::new()];
|
||||
}
|
||||
let mut lines = Vec::new();
|
||||
let options = textwrap::Options::new(width)
|
||||
.break_words(hard_wrap)
|
||||
.word_separator(textwrap::WordSeparator::AsciiSpace)
|
||||
.wrap_algorithm(textwrap::WrapAlgorithm::FirstFit);
|
||||
let wrapped = textwrap::wrap(cell, options)
|
||||
.into_iter()
|
||||
.map(std::borrow::Cow::into_owned)
|
||||
.collect::<Vec<_>>();
|
||||
if wrapped.is_empty() {
|
||||
|
||||
for segment in cell.split('\n') {
|
||||
let wrapped = textwrap::wrap(segment, options.clone())
|
||||
.into_iter()
|
||||
.map(std::borrow::Cow::into_owned)
|
||||
.collect::<Vec<_>>();
|
||||
if wrapped.is_empty() {
|
||||
lines.push(String::new());
|
||||
} else {
|
||||
lines.extend(wrapped);
|
||||
}
|
||||
}
|
||||
|
||||
if lines.is_empty() {
|
||||
vec![String::new()]
|
||||
} else {
|
||||
wrapped
|
||||
lines
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -94,6 +94,39 @@ fn table_resize_lifecycle_renderer_uses_vertical_fallback_only_at_tiny_width() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn table_inline_links_and_html_breaks_stay_inside_table() {
|
||||
let markdown = "| A | B |\n|---|---|\n| [link](https://example.com) | [CLI docs](https://example.com/cli) |\n| one<br>two | three<br>four |\n";
|
||||
let rendered = render_markdown_text_with_width_and_cwd(markdown, Some(72), /*cwd*/ None);
|
||||
let lines = plain_lines(&rendered);
|
||||
|
||||
assert!(
|
||||
lines
|
||||
.iter()
|
||||
.filter(|line| !line.trim().is_empty())
|
||||
.all(|line| line.contains('│') || line.contains('─')),
|
||||
"table inline content should not leak outside table: {lines:?}"
|
||||
);
|
||||
assert!(
|
||||
lines
|
||||
.iter()
|
||||
.any(|line| line.contains("link (https://example.com)")),
|
||||
"link destination should render in the table cell: {lines:?}"
|
||||
);
|
||||
assert!(
|
||||
lines
|
||||
.iter()
|
||||
.any(|line| line.contains("CLI docs (https://example.com/cli)")),
|
||||
"second link destination should render in the table cell: {lines:?}"
|
||||
);
|
||||
assert!(
|
||||
lines.iter().any(|line| line.contains("one"))
|
||||
&& lines.iter().any(|line| line.contains("two"))
|
||||
&& lines.iter().all(|line| !line.contains("<br>")),
|
||||
"HTML breaks should render as table cell line breaks: {lines:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty() {
|
||||
assert_eq!(render_markdown_text(""), Text::default());
|
||||
|
||||
Reference in New Issue
Block a user