Preserve table cell source metadata through TUI rendering (#48548)

## What changed

- Retain table identity, column alignments, cell coordinates, source byte ranges, and inline formatting in copy metadata.
- Carry cell fragments through grid padding, wrapping, key/value layouts, and pipe output, including escaped pipes and empty cells.
- Preserve logical source metadata when remapping wrapped lines, even without hyperlinks.

## Testing

Add coverage for cell fragments across narrow and wide layouts, blank table continuations, and Markdown selection copy structure and visible transformations.

GitOrigin-RevId: 8422f224cffe2e74813394f0623b9167bdd4ac59
This commit is contained in:
Felipe Coury
2026-09-26 21:58:17 +00:00
committed by copyberry
parent d6f25a54ba
commit 16c21e3f36
9 changed files with 462 additions and 23 deletions
+3
View File
@@ -14,6 +14,8 @@ use pulldown_cmark::Tag;
use crate::clipboard_copy::CopyFormat;
use crate::terminal_hyperlinks::LogicalLineSource;
pub(crate) mod table;
// Bound retained inline stacks and container prefixes independently of parser nesting.
pub(crate) const MAX_COPY_DEPTH: usize = 64;
@@ -56,6 +58,7 @@ pub(crate) struct CopyLine {
/// Restore the containing item when a multiline selection starts in its later paragraph.
pub(crate) item_prefix: String,
pub(crate) code: bool,
pub(crate) table: Option<table::TableLine>,
pub(crate) rule: bool,
pub(crate) heading: usize,
pub(crate) hard_break: bool,
+98
View File
@@ -0,0 +1,98 @@
//! Retain original table-cell ranges through grid padding, wrapping, and record labels.
//!
//! Each displayed fragment keeps its source line and cell coordinates so selection
//! serialization can recover only visible, selected content.
use super::CopyLine;
use crate::terminal_hyperlinks::HyperlinkLine;
use crate::terminal_hyperlinks::LogicalLineSource;
use std::ops::Range;
use std::sync::Arc;
#[cfg(test)]
#[path = "table_tests.rs"]
mod tests;
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct TableLine {
pub(crate) table: Arc<[&'static str]>,
fragments: Vec<Fragment>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
struct Fragment {
output: Range<usize>,
row: usize,
column: usize,
source: LogicalLineSource,
}
impl TableLine {
pub(crate) fn empty(table: &Arc<[&'static str]>) -> Self {
Self {
table: Arc::clone(table),
fragments: Vec::new(),
}
}
}
pub(crate) fn annotate_cell(
lines: &mut [HyperlinkLine],
copies: Vec<CopyLine>,
table: &Arc<[&'static str]>,
row: usize,
column: usize,
) {
for (line, copy) in lines.iter_mut().zip(copies) {
let mut source = LogicalLineSource::from_line(&line.line);
source.copy = Some(Arc::new(copy));
attach(
line,
Some(TableLine {
table: Arc::clone(table),
fragments: vec![Fragment {
output: source.range.clone(),
row,
column,
source,
}],
}),
);
}
}
pub(crate) fn attach(line: &mut HyperlinkLine, table: Option<TableLine>) {
if let Some(table) = table {
let mut source = LogicalLineSource::from_line(&line.line);
source.copy = Some(Arc::new(CopyLine {
table: Some(table),
..Default::default()
}));
line.source = Some(source);
}
}
/// Project a displayed cell fragment into the byte coordinates of its composed row.
pub(crate) fn append(output: &mut Option<TableLine>, source: &LogicalLineSource, offset: usize) {
let Some(table) = source.copy.as_ref().and_then(|copy| copy.table.as_ref()) else {
return;
};
let output = output.get_or_insert_with(|| TableLine::empty(&table.table));
for fragment in &table.fragments {
let start = source.range.start.max(fragment.output.start);
let end = source.range.end.min(fragment.output.end);
if start > end || start == end && !fragment.source.text.is_empty() {
continue;
}
let mut cell = fragment.source.clone();
cell.range = cell.range.start + start - fragment.output.start
..cell.range.start + end - fragment.output.start;
output.fragments.push(Fragment {
output: offset + source.prefix_bytes + start - source.range.start
..offset + source.prefix_bytes + end - source.range.start,
row: fragment.row,
column: fragment.column,
source: cell,
});
}
}
@@ -0,0 +1,68 @@
//! Exercise retained cell coordinates, inline styles, and byte ranges through each layout.
use super::*;
use pretty_assertions::assert_eq;
use std::collections::BTreeMap;
#[test]
fn table_layouts_preserve_cell_fragments() {
for (markdown, expected) in [
(
"| **alpha head** | B |\n|---|---|\n| **one two three four** | `value` |\n| | last |",
BTreeMap::from([
((0, 0), "**alpha head**"),
((0, 1), "B"),
((1, 0), "**one two three four**"),
((1, 1), "`value`"),
((2, 0), ""),
((2, 1), "last"),
]),
),
(
"| **alpha head** | `a\\|b` |\n|---|---|",
BTreeMap::from([((0, 0), "**alpha head**"), ((0, 1), "`a|b`")]),
),
] {
for width in [4, 12, 80] {
let lines = crate::markdown_render::render_markdown_lines_with_width_and_cwd(
markdown,
Some(width),
/*cwd*/ None,
);
let mut seen = BTreeMap::new();
let mut identity = None;
for source in lines.into_iter().filter_map(|line| line.source) {
let Some(table) = source.copy.as_ref().and_then(|copy| copy.table.as_ref()) else {
continue;
};
let identity = identity.get_or_insert_with(|| Arc::clone(&table.table));
assert!(Arc::ptr_eq(identity, &table.table));
for fragment in &table.fragments {
let cell = &fragment.source;
assert_eq!(
&source.text[fragment.output.clone()],
&cell.text[cell.range.clone()],
"width {width}, cell ({}, {})",
fragment.row,
fragment.column,
);
let markdown = cell.copy.as_ref().unwrap().render(
&cell.text,
0..cell.text.len(),
/*depth*/ 0,
);
let key = (fragment.row, fragment.column);
assert_eq!(markdown, expected[&key]);
seen.insert(key, markdown);
}
}
assert_eq!(
seen,
expected
.iter()
.map(|(key, value)| (*key, value.to_string()))
.collect()
);
}
}
}
+148 -15
View File
@@ -193,6 +193,7 @@ impl IndentContext {
#[derive(Clone, Debug, Default)]
struct TableCell {
lines: Vec<HyperlinkLine>,
copies: Vec<crate::markdown_copy::CopyLine>,
}
// TableCell mutators inlined — called per-span during table event parsing.
@@ -201,11 +202,27 @@ impl TableCell {
fn ensure_line(&mut self) {
if self.lines.is_empty() {
self.lines.push(HyperlinkLine::new(Line::default()));
self.copies.push(Default::default());
}
}
fn push_annotated(&mut self, mut appended: HyperlinkLine) {
fn push_annotated(
&mut self,
mut appended: HyperlinkLine,
inline: &[crate::markdown_copy::Inline],
) {
self.ensure_line();
if let Some(copy) = self.copies.last_mut() {
copy.push(
appended
.line
.spans
.iter()
.map(|span| span.content.len())
.sum(),
inline,
);
}
if let Some(line) = self.lines.last_mut() {
let shift = line.width();
line.line.spans.append(&mut appended.line.spans);
@@ -220,6 +237,7 @@ impl TableCell {
#[inline]
fn hard_break(&mut self) {
self.lines.push(HyperlinkLine::new(Line::default()));
self.copies.push(Default::default());
}
fn plain_text(&self) -> String {
@@ -1111,17 +1129,44 @@ impl<'a, 'policy> Writer<'a, 'policy> {
return;
};
let table: std::sync::Arc<[&str]> = table_state
.alignments
.iter()
.map(|alignment| match alignment {
Alignment::None => "---",
Alignment::Left => ":---",
Alignment::Center => ":---:",
Alignment::Right => "---:",
})
.collect();
let RenderedTableLines {
table_lines,
table_lines_prewrapped,
spillover_lines,
} = self.render_table_lines(table_state);
} = self.render_table_lines(table_state, &table);
let mut pending_marker_line = self.pending_marker_line;
for line in table_lines {
for mut line in table_lines {
if line
.source
.as_ref()
.and_then(|source| source.copy.as_ref())
.is_none()
{
crate::markdown_copy::table::attach(
&mut line,
Some(crate::markdown_copy::table::TableLine::empty(&table)),
);
}
if table_lines_prewrapped {
self.push_prewrapped_line(line, pending_marker_line);
} else {
let table = line
.source
.as_ref()
.and_then(|source| source.copy.as_ref())
.and_then(|copy| copy.table.clone());
self.push_hyperlink_line(line);
self.copy_line.table = table;
self.copy_line.code = true;
self.flush_current_line();
}
@@ -1237,7 +1282,7 @@ impl<'a, 'policy> Writer<'a, 'policy> {
if let Some(table_state) = self.table_state.as_mut()
&& let Some(cell) = table_state.current_cell.as_mut()
{
cell.push_annotated(annotated);
cell.push_annotated(annotated, &self.copy_inline);
}
}
@@ -1277,7 +1322,7 @@ impl<'a, 'policy> Writer<'a, 'policy> {
if let Some(table_state) = self.table_state.as_mut()
&& let Some(cell) = table_state.current_cell.as_mut()
{
cell.push_annotated(std::mem::take(&mut annotated));
cell.push_annotated(std::mem::take(&mut annotated), &self.copy_inline);
}
}
@@ -1292,7 +1337,11 @@ impl<'a, 'policy> Writer<'a, 'policy> {
/// Falls back to key/value records when body rows cannot fit in the aligned
/// grid; header-only tables retain raw pipe output because they contain no
/// records to transpose.
fn render_table_lines(&self, mut table_state: TableState) -> RenderedTableLines {
fn render_table_lines(
&self,
mut table_state: TableState,
table: &std::sync::Arc<[&'static str]>,
) -> RenderedTableLines {
let column_count = table_state.alignments.len();
if column_count == 0 {
return RenderedTableLines {
@@ -1326,6 +1375,21 @@ impl<'a, 'policy> Writer<'a, 'policy> {
for row in &mut rows {
Self::normalize_row(row, column_count);
}
for (row_index, row) in std::iter::once(&mut header)
.chain(rows.iter_mut())
.enumerate()
{
for (column, cell) in row.iter_mut().enumerate() {
cell.ensure_line();
crate::markdown_copy::table::annotate_cell(
&mut cell.lines,
std::mem::take(&mut cell.copies),
table,
row_index,
column,
);
}
}
let metrics = Self::collect_table_column_metrics(&header, &rows, column_count);
let available_width = self.available_table_width(column_count);
@@ -1691,16 +1755,25 @@ impl<'a, 'policy> Writer<'a, 'policy> {
.get(row_line)
.is_some_and(|line| Self::line_display_width(&line.line) > 0)
}) else {
out.push(HyperlinkLine::new(Line::default().style(row_style)));
let mut line = wrapped_cells
.iter()
.find_map(|cell| cell.get(row_line))
.cloned()
.unwrap_or_default();
line.line.style = row_style;
out.push(line);
continue;
};
let mut spans = Vec::new();
let mut copy = None;
let mut byte_offset = 0;
for (column, width) in column_widths
.iter()
.enumerate()
.take(last_visible_column + 1)
{
spans.push(Span::raw(" ".repeat(TABLE_CELL_PADDING)));
byte_offset += TABLE_CELL_PADDING;
let mut line = wrapped_cells[column]
.get(row_line)
.cloned()
@@ -1715,19 +1788,33 @@ impl<'a, 'policy> Writer<'a, 'policy> {
if left_padding > 0 {
spans.push(Span::raw(" ".repeat(left_padding)));
}
byte_offset += left_padding;
if let Some(source) = &line.source {
crate::markdown_copy::table::append(&mut copy, source, byte_offset);
}
byte_offset += line
.line
.spans
.iter()
.map(|span| span.content.len())
.sum::<usize>();
spans.append(&mut line.line.spans);
let is_last_column = column == last_visible_column;
if right_padding > 0 && !is_last_column {
spans.push(Span::raw(" ".repeat(right_padding)));
byte_offset += right_padding;
}
if !is_last_column {
spans.push(Span::raw(" ".repeat(TABLE_CELL_PADDING)));
byte_offset += TABLE_CELL_PADDING;
}
if !is_last_column {
spans.push(Span::raw(" ".repeat(TABLE_COLUMN_GAP)));
byte_offset += TABLE_COLUMN_GAP;
}
}
let mut out_line = HyperlinkLine::new(Line::from(spans).style(row_style));
crate::markdown_copy::table::attach(&mut out_line, copy);
let mut column_start = 0usize;
for (column, width) in column_widths
.iter()
@@ -1779,12 +1866,16 @@ impl<'a, 'policy> Writer<'a, 'policy> {
fn row_to_pipe_line(row: &[TableCell]) -> HyperlinkLine {
let mut out = HyperlinkLine::new(Line::default());
let mut copy = None;
let mut byte_offset = 1;
out.push_span("|".into(), /*destination*/ None);
for cell in row {
out.push_span(" ".into(), /*destination*/ None);
byte_offset += 1;
for (index, line) in cell.lines.iter().enumerate() {
if index > 0 {
out.push_span(" ".into(), /*destination*/ None);
byte_offset += 1;
}
let text = line
.line
@@ -1802,7 +1893,7 @@ impl<'a, 'policy> Writer<'a, 'policy> {
out.push_span(Span::raw(std::mem::take(current_text)), destination);
}
};
for ch in text.chars() {
for (byte, ch) in text.char_indices() {
let destination = line
.hyperlinks
.iter()
@@ -1814,15 +1905,23 @@ impl<'a, 'policy> Writer<'a, 'policy> {
}
if ch == '|' {
current_text.push_str("\\|");
byte_offset += 1;
} else {
current_text.push(ch);
}
if let Some(mut source) = line.source.clone() {
source.range = byte..byte + ch.len_utf8();
crate::markdown_copy::table::append(&mut copy, &source, byte_offset);
}
byte_offset += ch.len_utf8();
column += char_width(ch);
}
flush(&mut out, &mut current_text, current_destination);
}
out.push_span(" |".into(), /*destination*/ None);
byte_offset += 2;
}
crate::markdown_copy::table::attach(&mut out, copy);
out
}
@@ -1860,7 +1959,7 @@ impl<'a, 'policy> Writer<'a, 'policy> {
.map(|line| line_to_static(&line))
.collect::<Vec<_>>();
if rendered.is_empty() {
wrapped.push(HyperlinkLine::new(Line::default()));
wrapped.push(source_line.clone());
} else {
wrapped.extend(remap_wrapped_line(source_line, rendered));
};
@@ -2057,7 +2156,7 @@ impl<'a, 'policy> Writer<'a, 'policy> {
if let Some(table_state) = self.table_state.as_mut()
&& let Some(cell) = table_state.current_cell.as_mut()
{
cell.push_annotated(destination);
cell.push_annotated(destination, &self.copy_inline);
}
self.push_span_to_table_cell(")".into());
} else {
@@ -2087,8 +2186,14 @@ impl<'a, 'policy> Writer<'a, 'policy> {
let span = Span::styled(local_target_display, style);
if self.in_table_cell() {
if show_label {
for label_span in link.local_label_spans {
for (label_span, inline) in link
.local_label_spans
.into_iter()
.zip(link.local_label_inline)
{
let previous = std::mem::replace(&mut self.copy_inline, inline);
self.push_span_to_table_cell(label_span);
self.copy_inline = previous;
}
self.push_span_to_table_cell(" (".into());
}
@@ -2218,7 +2323,12 @@ impl<'a, 'policy> Writer<'a, 'policy> {
let mut spans = self.prefix_spans(pending_marker_line);
let mut source = crate::terminal_hyperlinks::LogicalLineSource::from_line(&line.line);
let mut copy = crate::markdown_copy::CopyLine::default();
let mut copy = line
.source
.as_ref()
.and_then(|source| source.copy.as_deref())
.cloned()
.unwrap_or_default();
copy.prefix = self.copy_prefix(pending_marker_line);
copy.continuation = self.copy_prefix(/*pending_marker_line*/ false);
copy.item_prefix = self.copy_prefix(/*pending_marker_line*/ true);
@@ -2612,9 +2722,9 @@ mod tests {
#[test]
fn wrap_cell_preserves_hard_break_lines() {
let mut cell = TableCell::default();
cell.push_annotated(Line::from("first line").into());
cell.push_annotated(Line::from("first line").into(), &[]);
cell.hard_break();
cell.push_annotated(Line::from("second line").into());
cell.push_annotated(Line::from("second line").into(), &[]);
let writer = W::new(
"",
@@ -2640,6 +2750,29 @@ mod tests {
);
}
#[test]
fn blank_table_continuations_do_not_repeat_the_first_cell() {
let mut cell = make_cell("first");
cell.hard_break();
cell.hard_break();
cell.push_annotated(Line::from("last").into(), &[]);
cell.hard_break();
let writer = W::new(
"",
/*wrap_width*/ Some(80),
/*cwd*/ None,
&never_hide_link_destination,
);
let lines = writer.render_table_row(
&[make_cell("short"), cell],
&[5, 5],
&[Alignment::None, Alignment::None],
Style::default(),
);
let rendered: Vec<_> = lines.iter().map(|line| line.line.to_string()).collect();
insta::assert_debug_snapshot!(rendered);
}
// ---------------------------------------------------------------
// Type alias for calling private associated functions on Writer.
// ---------------------------------------------------------------
@@ -2648,7 +2781,7 @@ mod tests {
/// Build a single-line `TableCell` from plain text.
fn make_cell(text: &str) -> TableCell {
let mut cell = TableCell::default();
cell.push_annotated(Line::from(text.to_string()).into());
cell.push_annotated(Line::from(text.to_string()).into(), &[]);
cell
}
@@ -163,8 +163,13 @@ fn render_aligned_field(
let wrapped_value = wrap_cell(value, value_width);
for (line_index, value_line) in wrapped_value.into_iter().enumerate() {
let mut spans = Vec::new();
let mut copy = None;
if line_index == 0 {
let label = header.plain_text();
let label_source = label_line(header, label_style);
if let Some(source) = &label_source.source {
crate::markdown_copy::table::append(&mut copy, source, FIELD_LEADING_PADDING);
}
spans.push(Span::raw(" ".repeat(FIELD_LEADING_PADDING)));
spans.push(Span::styled(label.clone(), label_style));
spans.push(Span::raw(" ".repeat(
@@ -173,7 +178,7 @@ fn render_aligned_field(
} else {
spans.push(Span::raw(" ".repeat(value_indent)));
}
push_prefixed_value_line(out, spans, value_line);
push_prefixed_value_line(out, spans, value_line, copy);
}
}
@@ -187,16 +192,22 @@ fn render_stacked_field(
let label_width = available_width
.map(|width| width.saturating_sub(FIELD_LEADING_PADDING).max(1))
.unwrap_or_else(|| display_width(&header.plain_text()).max(1));
let label = Line::from(Span::styled(header.plain_text(), label_style));
let label = label_line(header, label_style);
let mut wrapped_labels = Vec::new();
push_owned_lines(
&word_wrap_line(&label, RtOptions::new(label_width)),
&word_wrap_line(&label.line, RtOptions::new(label_width)),
&mut wrapped_labels,
);
for label_line in wrapped_labels {
for label_line in remap_wrapped_line(&label, wrapped_labels) {
let mut spans = vec![Span::raw(" ".repeat(FIELD_LEADING_PADDING))];
spans.extend(label_line.spans);
out.push(HyperlinkLine::new(Line::from(spans)));
let mut copy = None;
if let Some(source) = &label_line.source {
crate::markdown_copy::table::append(&mut copy, source, FIELD_LEADING_PADDING);
}
spans.extend(label_line.line.spans);
let mut line = HyperlinkLine::new(Line::from(spans));
crate::markdown_copy::table::attach(&mut line, copy);
out.push(line);
}
let value_width = available_width
@@ -207,6 +218,7 @@ fn render_stacked_field(
out,
vec![Span::raw(" ".repeat(STACKED_VALUE_INDENT))],
value_line,
/*copy*/ None,
);
}
}
@@ -215,13 +227,19 @@ fn push_prefixed_value_line(
out: &mut Vec<HyperlinkLine>,
mut prefix: Vec<Span<'static>>,
mut value_line: HyperlinkLine,
mut copy: Option<crate::markdown_copy::table::TableLine>,
) {
let bytes = prefix.iter().map(|span| span.content.len()).sum();
if let Some(source) = &value_line.source {
crate::markdown_copy::table::append(&mut copy, source, bytes);
}
let shift = prefix
.iter()
.map(|span| display_width(span.content.as_ref()))
.sum::<usize>();
prefix.append(&mut value_line.line.spans);
let mut output_line = HyperlinkLine::new(Line::from(prefix));
crate::markdown_copy::table::attach(&mut output_line, copy);
output_line
.hyperlinks
.extend(value_line.hyperlinks.into_iter().map(|mut link| {
@@ -231,6 +249,26 @@ fn push_prefixed_value_line(
out.push(output_line);
}
fn label_line(header: &TableCell, style: Style) -> HyperlinkLine {
let mut line = HyperlinkLine::new(Line::from(Span::styled(header.plain_text(), style)));
let mut copy = None;
let mut offset = 0;
for header_line in &header.lines {
if let Some(source) = &header_line.source {
crate::markdown_copy::table::append(&mut copy, source, offset);
}
offset += header_line
.line
.spans
.iter()
.map(|span| span.content.len())
.sum::<usize>()
+ 1;
}
crate::markdown_copy::table::attach(&mut line, copy);
line
}
fn wrap_cell(cell: &TableCell, width: usize) -> Vec<HyperlinkLine> {
if cell.lines.is_empty() {
return vec![HyperlinkLine::new(Line::default())];
@@ -243,7 +281,7 @@ fn wrap_cell(cell: &TableCell, width: usize) -> Vec<HyperlinkLine> {
.map(|line| line_to_static(&line))
.collect::<Vec<_>>();
if rendered.is_empty() {
wrapped.push(HyperlinkLine::new(Line::default()));
wrapped.push(source_line.clone());
} else {
wrapped.extend(remap_wrapped_line(source_line, rendered));
}
@@ -0,0 +1,11 @@
---
source: tui/src/markdown_render.rs
assertion_line: 2773
expression: rendered
---
[
" short first",
"",
" last",
"",
]
+11 -1
View File
@@ -426,10 +426,16 @@ pub(crate) fn remap_wrapped_line(
wrapped: Vec<Line<'static>>,
) -> Vec<HyperlinkLine> {
let mut out = plain_hyperlink_lines(wrapped);
if source.hyperlinks.is_empty() {
if source.hyperlinks.is_empty() && source.source.is_none() {
return out;
}
let source_text = line_text(&source.line);
if source_text.trim().is_empty() {
for line in &mut out {
line.source = source.source.clone();
}
return out;
}
let mut source_byte = 0usize;
let mut source_column = 0usize;
let mut link_index = 0usize;
@@ -447,6 +453,10 @@ pub(crate) fn remap_wrapped_line(
continue;
};
let mapped = &rendered[rendered_start..];
line.source = source
.source
.as_ref()
.map(|source| source.wrapped(source_byte..source_byte + mapped.len(), rendered_start));
let mut output_column = display_width(&rendered[..rendered_start]);
for grapheme in mapped.graphemes(/*is_extended*/ true) {
let width = display_width(grapheme);
@@ -7,6 +7,9 @@ use ratatui::buffer::Buffer;
use ratatui::layout::Rect;
use std::path::Path;
#[path = "markdown_element_copy_tests.rs"]
mod elements;
#[test]
fn selected_markdown_preserves_lists_and_inline_code() {
let cells: Vec<Arc<dyn HistoryCell>> = vec![Arc::new(AgentMarkdownCell::new(
@@ -0,0 +1,75 @@
//! Coverage of the Markdown grammar and visible transformations used by selection copy.
use super::*;
use pretty_assertions::assert_eq;
#[test]
fn supported_elements_preserve_structure_at_narrow_and_wide_widths() {
let samples = [
"# One\n\n## Two\n\n### Three\n\n#### Four\n\n##### Five\n\n###### Six",
"Setext heading\n===\n\nSubheading\n---",
"**strong** _emphasis_ ~~deleted~~ and `inline_code` with ``a`b``.",
"Escaped \\*stars\\*, &amp; entity, café 界, and 👩‍💻.",
"- first\n- second\n - nested\n\n3. third\n4. fourth",
"- [ ] pending\n- [x] complete",
"> quoted **text**\n>\n> - item\n> - nested",
"First paragraph.\n\nHard break: \nnext line.\n\n---\n\nLast paragraph.",
"Prose\n\n```\nlet x = \"*literal* <tag>\";\n```\n\nAfter",
"Image ![**alt** text](https://example.com/image.png).",
"Literal <b>HTML</b> and unsupported [^footnote].",
];
for source in samples {
for width in [18, 100] {
let layout = markdown_layout(source, width);
let copied = payload(&layout, 0..layout.text().len()).0;
assert_eq!(
crate::clipboard_html::render_markdown(&copied),
crate::clipboard_html::render_markdown(source),
"width {width}: {source}\n{copied}"
);
}
}
}
#[test]
fn transformed_content_copies_visible_text_without_active_html_or_images() {
for (source, expected) in [
("Soft break\nsame paragraph.", "Soft break\nsame paragraph."),
("[**local**](/repo/file.rs)", "**local** (repo/file.rs)"),
(
"[label][reference]\n\n[reference]: https://example.com",
"[label (https://example.com)](https://example.com)",
),
(
"<https://example.com>",
"[https://example.com (https://example.com)](https://example.com)",
),
("![image alt](https://example.com/image.png)", "image alt"),
] {
let layout = markdown_layout(source, /*width*/ 40);
let copied = payload(&layout, 0..layout.text().len()).0;
assert_eq!(
crate::clipboard_html::render_markdown(&copied),
crate::clipboard_html::render_markdown(expected),
"{source}\n{copied}"
);
}
}
#[test]
fn math_and_mermaid_copy_the_visible_rendering() {
for source in [
"Inline $\\alpha_1$ and \\(x^{2}\\).",
"\\[\n\\frac{a}{b}\n\\]",
"```mermaid\nflowchart LR\nA[Start] --> B[Finish]\n```",
] {
let layout = markdown_layout(source, /*width*/ 80);
let (copied, format) = payload(&layout, 0..layout.text().len());
if format == CopyFormat::PlainText {
assert_eq!(copied, layout.text());
} else {
let rendered = markdown_layout(&copied, /*width*/ 80);
assert_eq!(rendered.text(), layout.text());
}
}
}