1#![cfg_attr(not(feature = "std"), no_std)]
83#![cfg_attr(docsrs, feature(doc_cfg))]
84#![deny(missing_docs)]
85#![forbid(unsafe_code)]
86
87extern crate alloc;
88
89mod doc;
90mod render;
91
92pub use doc::Doc;
93
94#[cfg(test)]
95mod tests {
96 use super::Doc;
97 use alloc::string::String;
98 use alloc::vec::Vec;
99
100 #[test]
101 fn test_nil_renders_empty() {
102 assert_eq!(Doc::nil().render(80), "");
103 assert_eq!(Doc::default().render(80), "");
104 }
105
106 #[test]
107 fn test_text_renders_verbatim() {
108 assert_eq!(Doc::text("hello").render(80), "hello");
109 assert_eq!(Doc::text(String::from("owned")).render(80), "owned");
110 }
111
112 #[test]
113 fn test_append_is_left_to_right() {
114 let doc = Doc::text("a").append(Doc::text("b")).append(Doc::text("c"));
115 assert_eq!(doc.render(80), "abc");
116 }
117
118 #[test]
119 fn test_nil_is_append_identity() {
120 let a = Doc::text("x");
121 assert_eq!(a.clone().append(Doc::nil()).render(80), "x");
122 assert_eq!(Doc::nil().append(a).render(80), "x");
123 }
124
125 #[test]
126 fn test_line_flat_is_space_broken_is_newline() {
127 let doc = Doc::text("a").append(Doc::line()).append(Doc::text("b"));
128 assert_eq!(doc.clone().group().render(80), "a b");
130 assert_eq!(doc.clone().group().render(1), "a\nb");
132 assert_eq!(doc.render(80), "a\nb");
134 }
135
136 #[test]
137 fn test_softline_flat_is_empty() {
138 let doc = Doc::text("(")
139 .append(Doc::softline())
140 .append(Doc::text("x"))
141 .group();
142 assert_eq!(doc.clone().render(80), "(x");
143 assert_eq!(doc.render(1), "(\nx");
144 }
145
146 #[test]
147 fn test_hardline_forces_break_even_when_it_fits() {
148 let doc = Doc::text("a")
149 .append(Doc::hardline())
150 .append(Doc::text("b"))
151 .group();
152 assert_eq!(doc.render(80), "a\nb");
153 }
154
155 #[test]
156 fn test_hardline_forces_all_enclosing_groups() {
157 let inner = Doc::text("x")
160 .append(Doc::line())
161 .append(Doc::text("y"))
162 .group();
163 let doc = inner.append(Doc::hardline()).append(Doc::text("z")).group();
164 assert_eq!(doc.render(80), "x y\nz");
165 }
166
167 #[test]
168 fn test_nest_indents_broken_lines_only() {
169 let doc = Doc::text("{")
170 .append(Doc::line().append(Doc::text("body")).nest(4))
171 .append(Doc::line())
172 .append(Doc::text("}"))
173 .group();
174 assert_eq!(doc.clone().render(80), "{ body }");
175 assert_eq!(doc.render(4), "{\n body\n}");
176 }
177
178 #[test]
179 fn test_nest_nests_additively() {
180 let doc = Doc::text("a")
181 .append(
182 Doc::line()
183 .append(Doc::text("b"))
184 .append(Doc::line().append(Doc::text("c")).nest(2))
185 .nest(2),
186 )
187 .group();
188 assert_eq!(doc.render(1), "a\n b\n c");
189 }
190
191 #[test]
192 fn test_negative_nest_clamps_at_zero() {
193 let doc = Doc::text("a")
194 .append(Doc::line().append(Doc::text("b")).nest(-10))
195 .group();
196 assert_eq!(doc.render(1), "a\nb");
197 }
198
199 #[test]
200 fn test_group_all_or_nothing() {
201 let doc = Doc::join(Doc::line(), ["a", "b", "c"].map(Doc::text)).group();
203 assert_eq!(doc.clone().render(80), "a b c");
204 assert_eq!(doc.render(3), "a\nb\nc");
205 }
206
207 #[test]
208 fn test_concat_folds_in_order() {
209 let doc = Doc::concat(["1", "2", "3"].map(Doc::text));
210 assert_eq!(doc.render(80), "123");
211 }
212
213 #[test]
214 fn test_concat_empty_is_nil() {
215 assert_eq!(Doc::concat(core::iter::empty()).render(80), "");
216 }
217
218 #[test]
219 fn test_join_intersperses_separator() {
220 let doc = Doc::join(Doc::text("::"), ["a", "b", "c"].map(Doc::text));
221 assert_eq!(doc.render(80), "a::b::c");
222 }
223
224 #[test]
225 fn test_join_single_item_has_no_separator() {
226 let doc = Doc::join(Doc::text(","), core::iter::once(Doc::text("solo")));
227 assert_eq!(doc.render(80), "solo");
228 }
229
230 #[test]
231 fn test_join_empty_is_nil() {
232 assert_eq!(
233 Doc::join(Doc::text(","), core::iter::empty()).render(80),
234 ""
235 );
236 }
237
238 #[test]
239 fn test_render_into_matches_render() {
240 let doc = Doc::text("a")
241 .append(Doc::line())
242 .append(Doc::text("b"))
243 .group();
244 let mut buf = String::new();
245 doc.render_into(80, &mut buf).unwrap();
246 assert_eq!(buf, doc.render(80));
247 }
248
249 #[test]
250 fn test_wide_text_overflows_when_no_break_offered() {
251 let doc = Doc::text("unbreakable");
254 assert_eq!(doc.render(3), "unbreakable");
255 }
256
257 #[test]
258 fn test_from_impls() {
259 let a: Doc = "static".into();
260 let b: Doc = String::from("owned").into();
261 assert_eq!(a.render(80), "static");
262 assert_eq!(b.render(80), "owned");
263 }
264
265 #[test]
266 fn test_unicode_width_counts_scalars_not_bytes() {
267 let doc = Doc::text("café")
269 .append(Doc::line())
270 .append(Doc::text("x"))
271 .group();
272 assert_eq!(doc.render(4), "café\nx");
273 }
274
275 #[test]
276 fn test_deeply_nested_does_not_overflow_stack() {
277 let mut doc = Doc::text("end");
280 for _ in 0..100_000 {
281 doc = Doc::text("x").append(doc);
282 }
283 let out = doc.render(80);
284 assert!(out.ends_with("end"));
285 assert_eq!(out.len(), 100_000 + 3);
286 }
287
288 #[test]
289 fn test_debug_is_structural() {
290 let doc = Doc::text("a").append(Doc::line()).group();
291 let s = alloc::format!("{doc:?}");
292 assert_eq!(s, "Group(Cat(Text(\"a\"), Line))");
293 }
294
295 #[cfg(feature = "std")]
296 #[test]
297 fn test_render_writer_to_vec() {
298 let doc = Doc::text("io").append(Doc::text(" sink"));
299 let mut buf: Vec<u8> = Vec::new();
300 doc.render_writer(80, &mut buf).unwrap();
301 assert_eq!(buf, b"io sink");
302 }
303
304 #[cfg(feature = "std")]
305 #[test]
306 fn test_render_writer_propagates_io_error() {
307 struct Failing;
310 impl std::io::Write for Failing {
311 fn write(&mut self, _: &[u8]) -> std::io::Result<usize> {
312 Err(std::io::Error::new(std::io::ErrorKind::BrokenPipe, "nope"))
313 }
314 fn flush(&mut self) -> std::io::Result<()> {
315 Ok(())
316 }
317 }
318 let doc = Doc::text("data");
319 let err = doc.render_writer(80, &mut Failing).unwrap_err();
320 assert_eq!(err.kind(), std::io::ErrorKind::BrokenPipe);
321 }
322}