docs(tui): clarify markdown table streaming contracts

This commit is contained in:
Felipe Coury
2026-04-06 13:16:13 -03:00
committed by Felipe Coury
parent 38664bc2fb
commit d9035a61b0
4 changed files with 48 additions and 7 deletions
+22 -2
View File
@@ -1,5 +1,8 @@
//! Markdown rendering for the TUI transcript.
//! Low-level markdown event renderer for the TUI transcript.
//!
//! This module consumes `pulldown-cmark` events and emits styled `ratatui`
//! lines, including table layout, width-aware wrapping, and local file-link
//! display.
//! This renderer intentionally treats local file links differently from normal web links. For
//! local paths, the displayed text comes from the destination, not the markdown label, so
//! transcripts show the real file target (including normalized location suffixes) and can shorten
@@ -198,11 +201,23 @@ struct TableColumnMetrics {
kind: TableColumnKind,
}
/// Render markdown with default wrapping behavior.
///
/// Use this when the caller does not have a concrete render width yet (for
/// example, snapshot tests or contexts that intentionally defer wrapping). If
/// a viewport width is known, prefer [`render_markdown_text_with_width`] so
/// table fallback and line wrapping decisions match the visible terminal.
pub fn render_markdown_text(input: &str) -> Text<'static> {
render_markdown_text_with_width(input, /*width*/ None)
}
/// Render markdown using the current process working directory for local file-link display.
/// Render markdown constrained to a known terminal width.
///
/// The renderer preserves table structure when possible and falls back to
/// pipe-table output when a box table cannot fit the available width. Passing
/// `None` keeps intrinsic line widths and disables width-driven wrapping in the
/// markdown writer. Local file links render relative to the current process
/// working directory.
pub(crate) fn render_markdown_text_with_width(input: &str, width: Option<usize>) -> Text<'static> {
let cwd = std::env::current_dir().ok();
render_markdown_text_with_width_and_cwd(input, width, cwd.as_deref())
@@ -1240,6 +1255,8 @@ where
out.push('|');
for cell in row {
out.push(' ');
// Preserve literal `|` inside cell text in markdown fallback mode so
// downstream markdown parsers keep the cell content intact.
out.push_str(&cell.plain_text().replace('|', "\\|"));
out.push(' ');
out.push('|');
@@ -1374,6 +1391,9 @@ where
fn pop_link(&mut self) {
if let Some(link) = self.link.take() {
if link.show_destination {
// Link destinations are rendered as " (url)" suffixes. When parsing table cells,
// append the suffix into the active cell buffer rather than the outer paragraph
// line to avoid detached url lines.
if self.in_table_cell() {
self.push_span_to_table_cell(" (".into());
self.push_span_to_table_cell(Span::styled(link.destination, self.styles.link));
+7 -5
View File
@@ -35,12 +35,11 @@ pub(crate) struct MarkdownStreamCollector {
}
impl MarkdownStreamCollector {
/// Create a collector that renders markdown using `cwd` for local file-link display.
/// Create a collector that accumulates raw markdown deltas.
///
/// The collector snapshots `cwd` into owned state because stream commits can happen long after
/// construction. The same `cwd` should be reused for the entire stream lifecycle; mixing
/// different working directories within one stream would make the same link render with
/// different path prefixes across incremental commits.
/// `width` and `cwd` are only used by test-only rendering helpers; production stream commits
/// operate on raw source boundaries. The collector snapshots `cwd` so test rendering keeps
/// local file-link display stable across incremental commits.
pub fn new(width: Option<usize>, cwd: &Path) -> Self {
#[cfg(not(test))]
let _ = cwd;
@@ -56,10 +55,12 @@ impl MarkdownStreamCollector {
}
}
/// Update the rendering width used by test-only line-commit helpers.
pub fn set_width(&mut self, width: Option<usize>) {
self.width = width;
}
/// Reset all buffered source and commit bookkeeping.
pub fn clear(&mut self) {
self.buffer.clear();
self.committed_source_len = 0;
@@ -69,6 +70,7 @@ impl MarkdownStreamCollector {
}
}
/// Append a raw streaming delta to the internal source buffer.
pub fn push_delta(&mut self, delta: &str) {
tracing::trace!("push_delta: {delta:?}");
self.buffer.push_str(delta);
+6
View File
@@ -532,6 +532,7 @@ fn source_bytes_for_rendered_count(
// Table holdback infrastructure
// ---------------------------------------------------------------------------
/// Return fence marker character and run length for a potential fence line.
fn parse_fence_marker(line: &str) -> Option<(char, usize)> {
let mut chars = line.chars();
let first = chars.next()?;
@@ -582,6 +583,11 @@ fn is_markdown_fence(trimmed_line: &str, marker_len: usize) -> bool {
info.eq_ignore_ascii_case("md") || info.eq_ignore_ascii_case("markdown")
}
/// Parse source into lines tagged with fenced-code context for table scanning.
///
/// Fence close markers must use the same marker character and at least the
/// opening marker length, with no trailing content. This avoids accidentally
/// closing on shorter markers that appear inside longer fences.
fn parse_lines_with_fence_state(source: &str) -> Vec<ParsedLine<'_>> {
let mut in_fence = false;
let mut fence_char = '\0';
+13
View File
@@ -1,7 +1,17 @@
//! Shared width guards for transcript rendering.
//!
//! Callers use these helpers when they reserve fixed prefix columns (for
//! bullets, gutters, or labels) and need to know whether any content width
//! remains.
/// Returns usable content width after reserving fixed columns.
///
/// Guarantees a strict positive width (`Some(n)` where `n > 0`) or `None` when
/// the reserved columns consume the full width.
///
/// Treat `None` as "render prefix-only fallback". Coercing it to `0` and still
/// attempting wrapped rendering often produces empty or unstable output at very
/// narrow terminal widths.
pub(crate) fn usable_content_width(total_width: usize, reserved_cols: usize) -> Option<usize> {
total_width
.checked_sub(reserved_cols)
@@ -9,6 +19,9 @@ pub(crate) fn usable_content_width(total_width: usize, reserved_cols: usize) ->
}
/// `u16` convenience wrapper around [`usable_content_width`].
///
/// This keeps width math at callsites that receive terminal dimensions as
/// `u16` while preserving the same `None` contract for exhausted width.
pub(crate) fn usable_content_width_u16(total_width: u16, reserved_cols: u16) -> Option<usize> {
usable_content_width(usize::from(total_width), usize::from(reserved_cols))
}