ansi_align/
lib.rs

1//! # ansi-align
2//!
3//! A Rust library for aligning text with proper support for ANSI escape sequences and Unicode characters.
4//!
5//! This crate provides functions to align text in various ways (left, center, right) while correctly
6//! handling ANSI escape sequences (like color codes) and Unicode characters with varying display widths.
7//!
8//! ## Features
9//!
10//! - **ANSI-aware alignment**: Correctly handles text containing ANSI escape sequences
11//! - **Unicode support**: Properly calculates display width for Unicode characters including CJK
12//! - **Multiple alignment options**: Left, center, and right alignment
13//! - **Customizable**: Configure split strings and padding characters
14//! - **Performance optimized**: Single-pass processing with efficient memory usage
15//! - **Type-safe**: Uses a [`Width`] type for display width values
16//!
17//! ## Quick Start
18//!
19//! ```rust
20//! use ansi_align::{ansi_align, center, left, right};
21//!
22//! // Basic alignment (defaults to center)
23//! let text = "hello\nworld";
24//! let centered = ansi_align(text);
25//!
26//! // Specific alignment functions
27//! let left_aligned = left("short\nlonger line");
28//! let centered = center("short\nlonger line");
29//! let right_aligned = right("short\nlonger line");
30//! ```
31//!
32//! ## Advanced Usage
33//!
34//! ```rust
35//! use ansi_align::{ansi_align_with_options, Alignment, AlignOptions};
36//!
37//! let text = "line1|line2|line3";
38//! let options = AlignOptions::new(Alignment::Right)
39//!     .with_split("|")
40//!     .with_pad('.');
41//!
42//! let result = ansi_align_with_options(text, &options);
43//! ```
44
45#![deny(missing_docs)]
46#![warn(clippy::all)]
47#![warn(clippy::pedantic)]
48#![warn(clippy::nursery)]
49
50use std::fmt;
51use std::ops::{Add, Div, Mul, Sub};
52use string_width::string_width;
53
54/// Specifies the alignment direction for text.
55///
56/// This enum defines the three possible alignment options that can be used
57/// with the alignment functions.
58///
59/// # Examples
60///
61/// ```rust
62/// use ansi_align::{Alignment, AlignOptions, ansi_align_with_options};
63///
64/// let text = "hello\nworld";
65///
66/// // Center alignment
67/// let opts = AlignOptions::new(Alignment::Center);
68/// let result = ansi_align_with_options(text, &opts);
69///
70/// // Right alignment
71/// let opts = AlignOptions::new(Alignment::Right);
72/// let result = ansi_align_with_options(text, &opts);
73/// ```
74#[derive(Debug, Clone, Copy, PartialEq, Eq)]
75pub enum Alignment {
76    /// Align text to the left (no padding added to the left side)
77    Left,
78    /// Align text to the center (padding distributed evenly on both sides)
79    Center,
80    /// Align text to the right (padding added to the left side)
81    Right,
82}
83
84/// A type-safe wrapper for display width values.
85///
86/// This type represents the visual width of text as it would appear on screen,
87/// taking into account ANSI escape sequences and Unicode character widths.
88/// It provides type safety to prevent confusion between byte lengths and display widths.
89///
90/// # Examples
91///
92/// ```rust
93/// use ansi_align::Width;
94///
95/// let width = Width::new(42);
96/// assert_eq!(width.get(), 42);
97///
98/// // Convert from usize
99/// let width: Width = 24.into();
100/// assert_eq!(width.get(), 24);
101///
102/// // Ordering works as expected
103/// assert!(Width::new(10) < Width::new(20));
104/// ```
105#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
106pub struct Width(usize);
107
108impl Width {
109    /// Creates a new `Width` value from a `usize`.
110    ///
111    /// # Arguments
112    ///
113    /// * `value` - The display width value
114    ///
115    /// # Examples
116    ///
117    /// ```rust
118    /// use ansi_align::Width;
119    ///
120    /// let width = Width::new(10);
121    /// assert_eq!(width.get(), 10);
122    /// ```
123    #[must_use]
124    pub const fn new(value: usize) -> Self {
125        Self(value)
126    }
127
128    /// Returns the underlying `usize` value.
129    ///
130    /// # Examples
131    ///
132    /// ```rust
133    /// use ansi_align::Width;
134    ///
135    /// let width = Width::new(42);
136    /// assert_eq!(width.get(), 42);
137    /// ```
138    #[must_use]
139    pub const fn get(self) -> usize {
140        self.0
141    }
142
143    /// Saturating subtraction that doesn't underflow
144    ///
145    /// # Examples
146    ///
147    /// ```rust
148    /// use ansi_align::Width;
149    ///
150    /// let width = Width::new(5);
151    /// let result = width.saturating_sub(Width::new(10));
152    /// assert_eq!(result.get(), 0); // Would underflow, but saturates to 0
153    /// ```
154    #[must_use]
155    pub const fn saturating_sub(self, other: Self) -> Self {
156        Self(self.0.saturating_sub(other.0))
157    }
158
159    /// Check if width is zero
160    ///
161    /// # Examples
162    ///
163    /// ```rust
164    /// use ansi_align::Width;
165    ///
166    /// assert!(Width::new(0).is_zero());
167    /// assert!(!Width::new(5).is_zero());
168    /// ```
169    #[must_use]
170    pub const fn is_zero(self) -> bool {
171        self.0 == 0
172    }
173}
174
175impl From<usize> for Width {
176    fn from(value: usize) -> Self {
177        Self(value)
178    }
179}
180
181impl Add for Width {
182    type Output = Self;
183    fn add(self, other: Self) -> Self {
184        Self(self.0 + other.0)
185    }
186}
187
188impl Sub for Width {
189    type Output = Self;
190    fn sub(self, other: Self) -> Self {
191        Self(self.0 - other.0)
192    }
193}
194
195impl Mul for Width {
196    type Output = Self;
197    fn mul(self, other: Self) -> Self {
198        Self(self.0 * other.0)
199    }
200}
201
202impl Div for Width {
203    type Output = Self;
204    fn div(self, other: Self) -> Self {
205        Self(self.0 / other.0)
206    }
207}
208
209impl fmt::Display for Width {
210    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
211        write!(f, "{}", self.0)
212    }
213}
214
215/// Configuration options for text alignment operations.
216///
217/// This struct allows you to customize how text alignment is performed,
218/// including the alignment direction, line separator, and padding character.
219///
220/// # Examples
221///
222/// ```rust
223/// use ansi_align::{AlignOptions, Alignment, ansi_align_with_options};
224///
225/// // Basic usage with default settings
226/// let opts = AlignOptions::new(Alignment::Center);
227///
228/// // Customized options
229/// let opts = AlignOptions::new(Alignment::Right)
230///     .with_split("|")
231///     .with_pad('.');
232///
233/// let text = "short|longer text";
234/// let result = ansi_align_with_options(text, &opts);
235/// ```
236#[derive(Debug, Clone)]
237pub struct AlignOptions {
238    /// The alignment type (left, center, right)
239    pub align: Alignment,
240    /// The string to split lines on (default: "\n")
241    pub split: String,
242    /// The padding character to use (default: " ")
243    pub pad: char,
244}
245
246impl Default for AlignOptions {
247    fn default() -> Self {
248        Self {
249            align: Alignment::Center,
250            split: "\n".to_string(),
251            pad: ' ',
252        }
253    }
254}
255
256impl AlignOptions {
257    /// Creates new alignment options with the specified alignment direction.
258    ///
259    /// Uses default values for split string (`"\n"`) and padding character (`' '`).
260    ///
261    /// # Arguments
262    ///
263    /// * `align` - The alignment direction to use
264    ///
265    /// # Examples
266    ///
267    /// ```rust
268    /// use ansi_align::{AlignOptions, Alignment};
269    ///
270    /// let opts = AlignOptions::new(Alignment::Center);
271    /// ```
272    #[must_use]
273    pub fn new(align: Alignment) -> Self {
274        Self {
275            align,
276            ..Default::default()
277        }
278    }
279
280    /// Sets the string used to split lines using the builder pattern.
281    ///
282    /// By default, lines are split on `"\n"`, but you can specify any string
283    /// as a line separator.
284    ///
285    /// # Arguments
286    ///
287    /// * `split` - The string to use as a line separator
288    ///
289    /// # Examples
290    ///
291    /// ```rust
292    /// use ansi_align::{AlignOptions, Alignment};
293    ///
294    /// let opts = AlignOptions::new(Alignment::Center)
295    ///     .with_split("|")
296    ///     .with_split("<->"); // Multi-character separators work too
297    /// ```
298    #[must_use]
299    pub fn with_split<S: Into<String>>(mut self, split: S) -> Self {
300        self.split = split.into();
301        self
302    }
303
304    /// Sets the character used for padding using the builder pattern.
305    ///
306    /// By default, spaces (`' '`) are used for padding, but you can specify
307    /// any character.
308    ///
309    /// # Arguments
310    ///
311    /// * `pad` - The character to use for padding
312    ///
313    /// # Examples
314    ///
315    /// ```rust
316    /// use ansi_align::{AlignOptions, Alignment};
317    ///
318    /// let opts = AlignOptions::new(Alignment::Right)
319    ///     .with_pad('.');
320    /// ```
321    #[must_use]
322    pub const fn with_pad(mut self, pad: char) -> Self {
323        self.pad = pad;
324        self
325    }
326}
327
328/// Efficiently create padding string for alignment
329fn create_padding(pad_char: char, count: usize) -> String {
330    match count {
331        0 => String::new(),
332        1 => pad_char.to_string(),
333        // Use repeat for small counts (more efficient)
334        2..=16 => pad_char.to_string().repeat(count),
335        // Use with_capacity + push for larger counts to avoid reallocations
336        _ => {
337            let mut padding = String::with_capacity(count * pad_char.len_utf8());
338            for _ in 0..count {
339                padding.push(pad_char);
340            }
341            padding
342        }
343    }
344}
345
346/// Align text with center alignment (default behavior)
347#[must_use]
348pub fn ansi_align(text: &str) -> String {
349    ansi_align_with_options(text, &AlignOptions::default())
350}
351
352/// Calculate display widths for lines
353fn calculate_line_widths<'a>(text: &'a str, split: &str) -> Vec<(&'a str, Width)> {
354    text.split(split)
355        .map(|line| (line, Width::from(string_width(line))))
356        .collect()
357}
358
359/// Find the maximum width among lines
360fn find_max_width(line_data: &[(&str, Width)]) -> Width {
361    line_data
362        .iter()
363        .map(|(_, width)| *width)
364        .max()
365        .unwrap_or(Width::new(0))
366}
367
368/// Apply alignment to a single line
369fn align_line(
370    line: &str,
371    line_width: Width,
372    max_width: Width,
373    alignment: Alignment,
374    pad_char: char,
375) -> String {
376    let padding_needed = match alignment {
377        Alignment::Left => 0,
378        Alignment::Center => (max_width.get() - line_width.get()) / 2,
379        Alignment::Right => max_width.get() - line_width.get(),
380    };
381
382    if padding_needed == 0 {
383        line.to_string()
384    } else {
385        let mut result = create_padding(pad_char, padding_needed);
386        result.push_str(line);
387        result
388    }
389}
390
391/// Align text with support for ANSI escape sequences
392///
393/// This function handles text containing ANSI escape sequences (like color codes)
394/// by calculating display width correctly, ignoring the escape sequences.
395///
396/// # Examples
397///
398/// ```
399/// use ansi_align::{ansi_align, ansi_align_with_options, Alignment, AlignOptions};
400///
401/// // Basic center alignment
402/// let result = ansi_align("hello\nworld");
403///
404/// // Right alignment with custom padding
405/// let opts = AlignOptions::new(Alignment::Right).with_pad('.');
406/// let result = ansi_align_with_options("hi\nhello", &opts);
407///
408/// // Works with ANSI escape sequences
409/// let colored = "\x1b[31mred\x1b[0m\n\x1b[32mgreen\x1b[0m";
410/// let result = ansi_align_with_options(colored, &AlignOptions::new(Alignment::Center));
411/// ```
412///
413/// # Performance
414///
415/// This function makes a single pass through the input text for optimal performance.
416/// For left alignment, it returns the input unchanged as an optimization.
417#[must_use]
418pub fn ansi_align_with_options(text: &str, opts: &AlignOptions) -> String {
419    if text.is_empty() || opts.align == Alignment::Left {
420        return text.to_string();
421    }
422
423    let line_data = calculate_line_widths(text, &opts.split);
424    let max_width = find_max_width(&line_data);
425
426    let aligned_lines: Vec<String> = line_data
427        .into_iter()
428        .map(|(line, width)| align_line(line, width, max_width, opts.align, opts.pad))
429        .collect();
430
431    aligned_lines.join(&opts.split)
432}
433
434/// Align text to the left (no-op, returns original text)
435#[must_use]
436pub fn left(text: &str) -> String {
437    ansi_align_with_options(text, &AlignOptions::new(Alignment::Left))
438}
439
440/// Align text to the center
441#[must_use]
442pub fn center(text: &str) -> String {
443    ansi_align_with_options(text, &AlignOptions::new(Alignment::Center))
444}
445
446/// Align text to the right
447#[must_use]
448pub fn right(text: &str) -> String {
449    ansi_align_with_options(text, &AlignOptions::new(Alignment::Right))
450}
451
452#[cfg(test)]
453mod tests {
454    use super::*;
455
456    // Basic alignment tests
457    #[test]
458    fn test_left_alignment() {
459        let text = "hello\nworld";
460        let result = left(text);
461        assert_eq!(result, text); // Left alignment should be no-op
462    }
463
464    #[test]
465    fn test_center_alignment() {
466        let text = "hi\nhello";
467        let result = center(text);
468        let lines: Vec<&str> = result.split('\n').collect();
469        assert_eq!(lines[0], " hi"); // 1 space padding for "hi"
470        assert_eq!(lines[1], "hello"); // No padding for "hello"
471    }
472
473    #[test]
474    fn test_right_alignment() {
475        let text = "hi\nhello";
476        let result = right(text);
477        let lines: Vec<&str> = result.split('\n').collect();
478        assert_eq!(lines[0], "   hi"); // 3 spaces padding for "hi"
479        assert_eq!(lines[1], "hello"); // No padding for "hello"
480    }
481
482    // Unicode and ANSI tests
483    #[test]
484    fn test_unicode_characters() {
485        let text = "古\n古古古";
486        let result = center(text);
487        let lines: Vec<&str> = result.split('\n').collect();
488        assert_eq!(lines[0], "  古"); // 2 spaces padding (CJK char is width 2)
489        assert_eq!(lines[1], "古古古"); // No padding
490    }
491
492    #[test]
493    fn test_ansi_escape_sequences() {
494        let text = "hello\n\u{001B}[1mworld\u{001B}[0m";
495        let result = center(text);
496        let lines: Vec<&str> = result.split('\n').collect();
497        assert_eq!(lines[0], "hello");
498        assert_eq!(lines[1], "\u{001B}[1mworld\u{001B}[0m"); // ANSI codes preserved
499    }
500
501    #[test]
502    fn test_complex_ansi_sequences() {
503        // Test with multiple ANSI codes and colors
504        let text = "\x1b[31m\x1b[1mred\x1b[0m\n\x1b[32mgreen text\x1b[0m";
505        let result = right(text);
506        let lines: Vec<&str> = result.split('\n').collect();
507        // "red" has display width 3, "green text" has width 10
508        assert_eq!(lines[0], "       \x1b[31m\x1b[1mred\x1b[0m"); // 7 spaces padding
509        assert_eq!(lines[1], "\x1b[32mgreen text\x1b[0m"); // No padding
510    }
511
512    // Edge cases
513    #[test]
514    fn test_empty_string() {
515        assert_eq!(ansi_align_with_options("", &AlignOptions::default()), "");
516        assert_eq!(left(""), "");
517        assert_eq!(center(""), "");
518        assert_eq!(right(""), "");
519    }
520
521    #[test]
522    fn test_single_line() {
523        let text = "hello";
524        assert_eq!(left(text), "hello");
525        assert_eq!(center(text), "hello");
526        assert_eq!(right(text), "hello");
527    }
528
529    #[test]
530    fn test_single_character() {
531        let text = "a\nb";
532        let result = center(text);
533        assert_eq!(result, "a\nb"); // Both lines same width, no padding needed
534    }
535
536    #[test]
537    fn test_whitespace_only() {
538        let text = "   \n ";
539        let result = center(text);
540        let lines: Vec<&str> = result.split('\n').collect();
541        assert_eq!(lines[0], "   "); // 3 spaces, no padding needed
542        assert_eq!(lines[1], "  "); // 1 space + 1 padding space
543    }
544
545    // Custom options tests
546    #[test]
547    fn test_custom_split_and_pad() {
548        let text = "a|bb";
549        let opts = AlignOptions::new(Alignment::Right)
550            .with_split("|")
551            .with_pad('.');
552        let result = ansi_align_with_options(text, &opts);
553        assert_eq!(result, ".a|bb");
554    }
555
556    #[test]
557    fn test_custom_split_multichar() {
558        let text = "short<->very long line";
559        let opts = AlignOptions::new(Alignment::Center).with_split("<->");
560        let result = ansi_align_with_options(text, &opts);
561        assert_eq!(result, "    short<->very long line");
562    }
563
564    #[test]
565    fn test_different_padding_chars() {
566        let text = "hi\nhello";
567
568        // Test dot padding
569        let opts = AlignOptions::new(Alignment::Right).with_pad('.');
570        let result = ansi_align_with_options(text, &opts);
571        assert_eq!(result, "...hi\nhello");
572
573        // Test underscore padding
574        let opts = AlignOptions::new(Alignment::Center).with_pad('_');
575        let result = ansi_align_with_options(text, &opts);
576        assert_eq!(result, "_hi\nhello");
577
578        // Test zero padding
579        let opts = AlignOptions::new(Alignment::Right).with_pad('0');
580        let result = ansi_align_with_options(text, &opts);
581        assert_eq!(result, "000hi\nhello");
582    }
583
584    // Performance and memory optimization tests
585    #[test]
586    fn test_large_padding() {
587        let text = format!("a\n{}", "b".repeat(100));
588        let result = right(&text);
589        let lines: Vec<&str> = result.split('\n').collect();
590        assert_eq!(lines[0].len(), 100); // 99 spaces + "a"
591        assert!(lines[0].starts_with(&" ".repeat(99)));
592        assert!(lines[0].ends_with('a'));
593        assert_eq!(lines[1], "b".repeat(100));
594    }
595
596    #[test]
597    fn test_no_padding_optimization() {
598        // Test that lines requiring no padding are handled efficiently
599        let text = "same\nsame\nsame";
600        let result = center(text);
601        assert_eq!(result, text); // Should be unchanged
602    }
603
604    // Width type tests
605    #[test]
606    fn test_width_type() {
607        let width = Width::new(42);
608        assert_eq!(width.get(), 42);
609
610        let width_from_usize: Width = 24.into();
611        assert_eq!(width_from_usize.get(), 24);
612
613        // Test ordering
614        assert!(Width::new(10) < Width::new(20));
615        assert_eq!(Width::new(15), Width::new(15));
616    }
617
618    #[test]
619    fn test_width_arithmetic() {
620        let w1 = Width::new(10);
621        let w2 = Width::new(5);
622
623        // Test addition
624        assert_eq!((w1 + w2).get(), 15);
625
626        // Test subtraction
627        assert_eq!((w1 - w2).get(), 5);
628
629        // Test multiplication
630        assert_eq!((w1 * w2).get(), 50);
631
632        // Test division
633        assert_eq!((w1 / w2).get(), 2);
634
635        // Test saturating subtraction
636        assert_eq!(w2.saturating_sub(w1).get(), 0); // 5 - 10 = 0 (saturated)
637        assert_eq!(w1.saturating_sub(w2).get(), 5); // 10 - 5 = 5
638
639        // Test is_zero
640        assert!(Width::new(0).is_zero());
641        assert!(!w1.is_zero());
642    }
643
644    #[test]
645    fn test_width_display() {
646        let width = Width::new(42);
647        assert_eq!(format!("{width}"), "42");
648    }
649
650    // Comprehensive alignment scenarios
651    #[test]
652    fn test_mixed_width_lines() {
653        let text = "a\nbb\nccc\ndddd\neeeee";
654
655        // Center alignment
656        let result = center(text);
657        let lines: Vec<&str> = result.split('\n').collect();
658
659        // The longest line is "eeeee" with 5 chars
660        // So padding for center should be: (5-1)/2=2, (5-2)/2=1, (5-3)/2=1, (5-4)/2=0, (5-5)/2=0
661        assert_eq!(lines[0], "  a"); // 2 spaces + "a"
662        assert_eq!(lines[1], " bb"); // 1 space + "bb"
663        assert_eq!(lines[2], " ccc"); // 1 space + "ccc" (corrected)
664        assert_eq!(lines[3], "dddd"); // no padding (corrected)
665        assert_eq!(lines[4], "eeeee"); // no padding
666
667        // Right alignment
668        let result = right(text);
669        let lines: Vec<&str> = result.split('\n').collect();
670        assert_eq!(lines[0], "    a"); // 4 spaces + "a"
671        assert_eq!(lines[1], "   bb"); // 3 spaces + "bb"
672        assert_eq!(lines[2], "  ccc"); // 2 spaces + "ccc"
673        assert_eq!(lines[3], " dddd"); // 1 space + "dddd"
674        assert_eq!(lines[4], "eeeee"); // no padding
675    }
676
677    #[test]
678    fn test_center_odd_padding() {
679        // Test center alignment with odd padding amounts
680        let text = "a\nbbbb";
681        let result = center(text);
682        let lines: Vec<&str> = result.split('\n').collect();
683        assert_eq!(lines[0], " a"); // (4-1)/2 = 1 space
684        assert_eq!(lines[1], "bbbb"); // no padding
685    }
686
687    #[test]
688    fn test_multiline_with_empty_lines() {
689        let text = "hello\n\nworld";
690        let result = center(text);
691        let lines: Vec<&str> = result.split('\n').collect();
692        assert_eq!(lines[0], "hello");
693        assert_eq!(lines[1], "  "); // 2 spaces for empty line (center of 5-char width)
694        assert_eq!(lines[2], "world");
695    }
696
697    // Regression tests for performance improvements
698    #[test]
699    fn test_no_unnecessary_allocations() {
700        // This test ensures we don't regress on the performance improvements
701        let text = "line1\nline2\nline3";
702        let result = left(text);
703        // Left alignment should return original string (no allocations for processing)
704        assert_eq!(result, text);
705    }
706
707    #[test]
708    fn test_padding_efficiency() {
709        // Test the efficient padding creation for different sizes
710        let text = format!("a\n{}", "b".repeat(20));
711
712        // Small padding (should use repeat)
713        let opts = AlignOptions::new(Alignment::Right);
714        let result = ansi_align_with_options("a\nbb", &opts);
715        assert_eq!(result, " a\nbb");
716
717        // Large padding (should use with_capacity)
718        let result = ansi_align_with_options(&text, &opts);
719        let lines: Vec<&str> = result.split('\n').collect();
720        assert_eq!(lines[0].len(), 20); // 19 spaces + "a"
721        assert!(lines[0].ends_with('a'));
722    }
723
724    #[test]
725    fn test_create_padding_unicode() {
726        // Test padding with Unicode characters
727        let text = "a\nbb";
728        let opts = AlignOptions::new(Alignment::Right).with_pad('•');
729        let result = ansi_align_with_options(text, &opts);
730        assert_eq!(result, "•a\nbb");
731
732        // Test with multi-byte Unicode character
733        let opts = AlignOptions::new(Alignment::Right).with_pad('🎯');
734        let result = ansi_align_with_options(text, &opts);
735        assert_eq!(result, "🎯a\nbb");
736    }
737
738    // Integration tests
739    #[test]
740    fn test_real_world_scenario() {
741        // Simulate aligning a simple table or menu
742        let menu = "Home\nAbout Us\nContact\nServices";
743        let result = center(menu);
744        let lines: Vec<&str> = result.split('\n').collect();
745
746        // "About Us" and "Services" are both 8 chars (longest)
747        assert_eq!(lines[0], "  Home"); // 2 spaces (8-4)/2 = 2
748        assert_eq!(lines[1], "About Us"); // no padding (8 chars)
749        assert_eq!(lines[2], "Contact"); // no padding - "Contact" is 7 chars, (8-7)/2 = 0
750        assert_eq!(lines[3], "Services"); // no padding (8 chars)
751    }
752
753    #[test]
754    fn test_code_alignment() {
755        // Test aligning code-like content
756        let code = "if x:\n    return y\nelse:\n    return z";
757        let result = right(code);
758        let lines: Vec<&str> = result.split('\n').collect();
759
760        // "    return y" and "    return z" are longest at 12 chars
761        assert_eq!(lines[0], "       if x:"); // 7 spaces + "if x:" (12-5=7)
762        assert_eq!(lines[1], "    return y"); // no padding (12 chars)
763        assert_eq!(lines[2], "       else:"); // 7 spaces + "else:" (12-5=7)
764        assert_eq!(lines[3], "    return z"); // no padding (12 chars)
765    }
766}