pub fn split_cells(body: &str) -> Vec<String> {
if body.is_empty() {
return vec![String::new()];
}
let mut cells = Vec::new();
let mut current = String::new();
let mut first_line_in_cell = true;
for line in body.split_inclusive('\n') {
let content_no_eol = line.strip_suffix('\n').unwrap_or(line);
if content_no_eol.trim() == "+++" {
if let Some(stripped) = current.strip_suffix('\n') {
current.truncate(stripped.len());
}
cells.push(std::mem::take(&mut current));
first_line_in_cell = true;
continue;
}
if first_line_in_cell {
first_line_in_cell = false;
if content_no_eol.trim().is_empty() {
continue;
}
}
current.push_str(line);
}
if let Some(stripped) = current.strip_suffix('\n') {
current.truncate(stripped.len());
}
cells.push(current);
cells
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn no_divider_returns_single_cell() {
let cells = split_cells("line one\nline two\n");
assert_eq!(cells, vec!["line one\nline two"]);
}
#[test]
fn empty_body_returns_one_empty_cell() {
let cells = split_cells("");
assert_eq!(cells, vec![""]);
}
#[test]
fn single_divider_splits_into_two_cells() {
let cells = split_cells("a\n+++\nb\n");
assert_eq!(cells, vec!["a", "b"]);
}
#[test]
fn two_dividers_three_cells() {
let cells = split_cells("a\n+++\nb\n+++\nc\n");
assert_eq!(cells, vec!["a", "b", "c"]);
}
#[test]
fn multi_line_cells_preserved_verbatim() {
let cells = split_cells("alpha\nbeta\n+++\ngamma\ndelta\n");
assert_eq!(cells, vec!["alpha\nbeta", "gamma\ndelta"]);
}
#[test]
fn divider_with_trailing_whitespace_recognized() {
let cells = split_cells("a\n+++ \nb\n");
assert_eq!(cells, vec!["a", "b"]);
}
#[test]
fn divider_with_leading_whitespace_recognized() {
let cells = split_cells("a\n +++\nb\n");
assert_eq!(cells, vec!["a", "b"]);
}
#[test]
fn four_plus_signs_is_not_a_divider() {
let cells = split_cells("a\n++++\nb\n");
assert_eq!(cells, vec!["a\n++++\nb"]);
}
#[test]
fn plus_with_trailing_text_is_not_a_divider() {
let cells = split_cells("a\n+++ extra\nb\n");
assert_eq!(cells, vec!["a\n+++ extra\nb"]);
}
#[test]
fn empty_cells_preserved() {
let cells = split_cells("a\n+++\n+++\nb\n");
assert_eq!(cells, vec!["a", "", "b"]);
}
#[test]
fn divider_at_start_creates_empty_first_cell() {
let cells = split_cells("+++\nb\n");
assert_eq!(cells, vec!["", "b"]);
}
#[test]
fn divider_at_end_creates_empty_last_cell() {
let cells = split_cells("a\n+++\n");
assert_eq!(cells, vec!["a", ""]);
}
#[test]
fn body_without_trailing_newline_works() {
let cells = split_cells("a\n+++\nb");
assert_eq!(cells, vec!["a", "b"]);
}
#[test]
fn leading_blank_line_after_divider_is_dropped() {
let cells = split_cells("a\n+++\n\nb\n");
assert_eq!(cells, vec!["a", "b"]);
}
#[test]
fn buttons_style_one_link_per_line_no_divider() {
let body = "[Get started](/start)\n[Read the docs](/docs)\n";
let cells = split_cells(body);
assert_eq!(cells.len(), 1);
assert_eq!(cells[0], "[Get started](/start)\n[Read the docs](/docs)");
}
#[test]
fn spec_buttons_two_cells_with_divider() {
let body = "[Get started](/start)\n+++\n[Read the docs](/docs)\n";
let cells = split_cells(body);
assert_eq!(cells.len(), 2);
assert_eq!(cells[0], "[Get started](/start)");
assert_eq!(cells[1], "[Read the docs](/docs)");
}
#[test]
fn spec_grid_three_ratio_cells() {
let body = "[Card one](/work/one)\n+++\n[Card two](/work/two)\n+++\n[Card three](/work/three)\n";
let cells = split_cells(body);
assert_eq!(cells.len(), 3);
assert_eq!(cells[0], "[Card one](/work/one)");
assert_eq!(cells[2], "[Card three](/work/three)");
}
}