fix(render): suppress blank lines above rendered table

This commit is contained in:
2026-07-22 12:37:30 -06:00
parent 8cfd5ee2c4
commit dfacf31f6a
2 changed files with 52 additions and 12 deletions
+49 -11
View File
@@ -473,6 +473,7 @@ enum TableState {
enum TableAction { enum TableAction {
Consumed(String), Consumed(String),
ConsumedSilent,
FlushAndContinue(String), FlushAndContinue(String),
Passthrough, Passthrough,
} }
@@ -532,7 +533,7 @@ impl MarkdownRender {
pub fn render(&mut self, text: &str) -> String { pub fn render(&mut self, text: &str) -> String {
text.split('\n') text.split('\n')
.map(|line| self.render_line_mut(line)) .filter_map(|line| self.render_line_mut(line))
.collect::<Vec<String>>() .collect::<Vec<String>>()
.join("\n") .join("\n")
} }
@@ -548,7 +549,7 @@ impl MarkdownRender {
} }
} }
fn render_line_mut(&mut self, line: &str) -> String { fn render_line_mut(&mut self, line: &str) -> Option<String> {
let (line_type, line_kind, code_syntax, is_code) = self.check_line(line); let (line_type, line_kind, code_syntax, is_code) = self.check_line(line);
let table_prefix = if self.options.raw_markdown { let table_prefix = if self.options.raw_markdown {
@@ -563,7 +564,12 @@ impl MarkdownRender {
TableAction::Consumed(s) => { TableAction::Consumed(s) => {
self.prev_line_type = line_type; self.prev_line_type = line_type;
self.code_syntax = code_syntax; self.code_syntax = code_syntax;
return s; return Some(s);
}
TableAction::ConsumedSilent => {
self.prev_line_type = line_type;
self.code_syntax = code_syntax;
return None;
} }
TableAction::FlushAndContinue(s) => Some(s), TableAction::FlushAndContinue(s) => Some(s),
TableAction::Passthrough => None, TableAction::Passthrough => None,
@@ -580,10 +586,10 @@ impl MarkdownRender {
self.prev_line_type = line_type; self.prev_line_type = line_type;
self.code_syntax = code_syntax; self.code_syntax = code_syntax;
match table_prefix { Some(match table_prefix {
Some(prefix) => format!("{prefix}\n{output}"), Some(prefix) => format!("{prefix}\n{output}"),
None => output, None => output,
} })
} }
fn render_as_paragraph(&self, line: &str) -> String { fn render_as_paragraph(&self, line: &str) -> String {
@@ -594,7 +600,7 @@ impl MarkdownRender {
match (self.table_state.take(), kind) { match (self.table_state.take(), kind) {
(None, LineKind::TableRow) => { (None, LineKind::TableRow) => {
self.table_state = Some(TableState::PendingHeader(line.to_string())); self.table_state = Some(TableState::PendingHeader(line.to_string()));
TableAction::Consumed(String::new()) TableAction::ConsumedSilent
} }
(None, LineKind::TableSeparator) => TableAction::Passthrough, (None, LineKind::TableSeparator) => TableAction::Passthrough,
(None, _) => TableAction::Passthrough, (None, _) => TableAction::Passthrough,
@@ -606,7 +612,7 @@ impl MarkdownRender {
alignments, alignments,
rows: Vec::new(), rows: Vec::new(),
}); });
TableAction::Consumed(String::new()) TableAction::ConsumedSilent
} }
(Some(TableState::PendingHeader(header_line)), LineKind::TableRow) => { (Some(TableState::PendingHeader(header_line)), LineKind::TableRow) => {
let a = self.render_as_paragraph(&header_line); let a = self.render_as_paragraph(&header_line);
@@ -631,7 +637,7 @@ impl MarkdownRender {
alignments, alignments,
rows, rows,
}); });
TableAction::Consumed(String::new()) TableAction::ConsumedSilent
} }
( (
Some(TableState::Active { Some(TableState::Active {
@@ -1617,11 +1623,11 @@ std::error::Error>> {
let options = RenderOptions::default(); let options = RenderOptions::default();
let mut render = MarkdownRender::init(options).unwrap(); let mut render = MarkdownRender::init(options).unwrap();
let header = render.render_line_mut("| A | B |"); let header = render.render_line_mut("| A | B |");
assert!(header.is_empty(), "header row silently buffered"); assert!(header.is_none(), "header row silently consumed");
let sep = render.render_line_mut("|---|---|"); let sep = render.render_line_mut("|---|---|");
assert!(sep.is_empty(), "separator silently buffered"); assert!(sep.is_none(), "separator silently consumed");
let data = render.render_line_mut("| 1 | 2 |"); let data = render.render_line_mut("| 1 | 2 |");
assert!(data.is_empty(), "data row silently buffered"); assert!(data.is_none(), "data row silently consumed");
} }
#[test] #[test]
@@ -1889,6 +1895,38 @@ std::error::Error>> {
); );
} }
#[test]
fn table_render_does_not_emit_blank_lines_from_silent_accumulation() {
let options = RenderOptions::default();
let mut render = MarkdownRender::init(options).unwrap();
let text = "## Head\n\n\
| A | B |\n\
|---|---|\n\
| 1 | 2 |\n\
| 3 | 4 |\n\
| 5 | 6 |\n\
| 7 | 8 |\n\
| 9 | 10 |\n\n\
trailing\n";
let output = render.render(text);
let tail = render.finalize();
let combined = format!("{output}{tail}");
let mut consecutive_blank = 0usize;
let mut max_consecutive_blank = 0usize;
for line in combined.split('\n') {
if line.is_empty() {
consecutive_blank += 1;
max_consecutive_blank = max_consecutive_blank.max(consecutive_blank);
} else {
consecutive_blank = 0;
}
}
assert!(
max_consecutive_blank <= 1,
"at most one blank line between blocks, got {max_consecutive_blank}: {combined:?}",
);
}
#[test] #[test]
fn table_tolerates_column_count_mismatch() { fn table_tolerates_column_count_mismatch() {
let options = RenderOptions::default(); let options = RenderOptions::default();
+3 -1
View File
@@ -132,7 +132,9 @@ async fn markdown_stream_inner(
let text = format!("{buffer}{text}"); let text = format!("{buffer}{text}");
let (head, tail) = split_line_tail(&text); let (head, tail) = split_line_tail(&text);
let output = render.render(head); let output = render.render(head);
print_block(writer, &output, columns)?; if !output.is_empty() {
print_block(writer, &output, columns)?;
}
buffer = tail.to_string(); buffer = tail.to_string();
} else { } else {
buffer = format!("{buffer}{text}"); buffer = format!("{buffer}{text}");