1pub fn split_cells(body: &str) -> Vec<String> {
28 if body.is_empty() {
29 return vec![String::new()];
30 }
31
32 let mut cells = Vec::new();
33 let mut current = String::new();
34 let mut first_line_in_cell = true;
35
36 for line in body.split_inclusive('\n') {
37 let content_no_eol = line.strip_suffix('\n').unwrap_or(line);
40 if content_no_eol.trim() == "+++" {
41 if let Some(stripped) = current.strip_suffix('\n') {
46 current.truncate(stripped.len());
47 }
48 cells.push(std::mem::take(&mut current));
49 first_line_in_cell = true;
50 continue;
51 }
52
53 if first_line_in_cell {
56 first_line_in_cell = false;
57 if content_no_eol.trim().is_empty() {
58 continue;
59 }
60 }
61
62 current.push_str(line);
63 }
64
65 if let Some(stripped) = current.strip_suffix('\n') {
67 current.truncate(stripped.len());
68 }
69 cells.push(current);
70
71 cells
72}
73
74#[cfg(test)]
75mod tests {
76 use super::*;
77
78 #[test]
79 fn no_divider_returns_single_cell() {
80 let cells = split_cells("line one\nline two\n");
81 assert_eq!(cells, vec!["line one\nline two"]);
82 }
83
84 #[test]
85 fn empty_body_returns_one_empty_cell() {
86 let cells = split_cells("");
87 assert_eq!(cells, vec![""]);
88 }
89
90 #[test]
91 fn single_divider_splits_into_two_cells() {
92 let cells = split_cells("a\n+++\nb\n");
93 assert_eq!(cells, vec!["a", "b"]);
94 }
95
96 #[test]
97 fn two_dividers_three_cells() {
98 let cells = split_cells("a\n+++\nb\n+++\nc\n");
99 assert_eq!(cells, vec!["a", "b", "c"]);
100 }
101
102 #[test]
103 fn multi_line_cells_preserved_verbatim() {
104 let cells = split_cells("alpha\nbeta\n+++\ngamma\ndelta\n");
105 assert_eq!(cells, vec!["alpha\nbeta", "gamma\ndelta"]);
106 }
107
108 #[test]
109 fn divider_with_trailing_whitespace_recognized() {
110 let cells = split_cells("a\n+++ \nb\n");
111 assert_eq!(cells, vec!["a", "b"]);
112 }
113
114 #[test]
115 fn divider_with_leading_whitespace_recognized() {
116 let cells = split_cells("a\n +++\nb\n");
117 assert_eq!(cells, vec!["a", "b"]);
118 }
119
120 #[test]
121 fn four_plus_signs_is_not_a_divider() {
122 let cells = split_cells("a\n++++\nb\n");
123 assert_eq!(cells, vec!["a\n++++\nb"]);
124 }
125
126 #[test]
127 fn plus_with_trailing_text_is_not_a_divider() {
128 let cells = split_cells("a\n+++ extra\nb\n");
129 assert_eq!(cells, vec!["a\n+++ extra\nb"]);
130 }
131
132 #[test]
133 fn empty_cells_preserved() {
134 let cells = split_cells("a\n+++\n+++\nb\n");
136 assert_eq!(cells, vec!["a", "", "b"]);
137 }
138
139 #[test]
140 fn divider_at_start_creates_empty_first_cell() {
141 let cells = split_cells("+++\nb\n");
142 assert_eq!(cells, vec!["", "b"]);
143 }
144
145 #[test]
146 fn divider_at_end_creates_empty_last_cell() {
147 let cells = split_cells("a\n+++\n");
148 assert_eq!(cells, vec!["a", ""]);
149 }
150
151 #[test]
152 fn body_without_trailing_newline_works() {
153 let cells = split_cells("a\n+++\nb");
154 assert_eq!(cells, vec!["a", "b"]);
155 }
156
157 #[test]
158 fn leading_blank_line_after_divider_is_dropped() {
159 let cells = split_cells("a\n+++\n\nb\n");
162 assert_eq!(cells, vec!["a", "b"]);
163 }
164
165 #[test]
166 fn buttons_style_one_link_per_line_no_divider() {
167 let body = "[Get started](/start)\n[Read the docs](/docs)\n";
171 let cells = split_cells(body);
172 assert_eq!(cells.len(), 1);
173 assert_eq!(cells[0], "[Get started](/start)\n[Read the docs](/docs)");
174 }
175
176 #[test]
177 fn spec_buttons_two_cells_with_divider() {
178 let body = "[Get started](/start)\n+++\n[Read the docs](/docs)\n";
180 let cells = split_cells(body);
181 assert_eq!(cells.len(), 2);
182 assert_eq!(cells[0], "[Get started](/start)");
183 assert_eq!(cells[1], "[Read the docs](/docs)");
184 }
185
186 #[test]
187 fn spec_grid_three_ratio_cells() {
188 let body = "[Card one](/work/one)\n+++\n[Card two](/work/two)\n+++\n[Card three](/work/three)\n";
189 let cells = split_cells(body);
190 assert_eq!(cells.len(), 3);
191 assert_eq!(cells[0], "[Card one](/work/one)");
192 assert_eq!(cells[2], "[Card three](/work/three)");
193 }
194}