convert_to_spaces/lib.rs
1/*
2Convert tabs to spaces
3implementation by Radim Kolar <hsn@sendmail.cz> 2025
4https://gitlab.com/hsn10/convert-to-spaces
5
6This is free and unencumbered software released into the public domain.
7SPDX-License-Identifier: Unlicense OR CC0-1.0
8
9For more information, please refer to <http://unlicense.org/>
10*/
11
12#![forbid(unsafe_code)]
13#![forbid(missing_docs)]
14
15//! # convert-to-spaces
16//!
17//! [](https://crates.io/crates/convert-to-spaces)
18//! [](https://unlicense.org)
19//! [](https://blog.rust-lang.org/2021/10/21/Rust-1.56.0.html)
20//! [](https://docs.rs/convert-to-spaces)
21//! [](https://crates.io/crates/convert-to-spaces/versions)
22//!
23//! Convert tabs to spaces in a string, respecting tab stop logic.
24//!
25//! ## Overview
26//!
27//! `convert-to-spaces` is a lightweight Rust crate designed to precisely transform tab characters (`\t`)
28//! into the appropriate number of spaces. Unlike simple find-and-replace, this crate correctly
29//! applies **tab stop logic**, ensuring that text aligns to the next multiple of your specified tab size.
30//! This is crucial for maintaining consistent indentation and code readability, mimicking the behavior
31//! of modern text editors.
32//!
33//! Whether you're processing code, cleaning up text files, or preparing content for display,
34//! `convert-to-spaces` helps standardize your whitespace.
35//!
36//! ## Features
37//!
38//! * **Accurate Tab Stop Conversion:** Converts `\t` characters into the correct number of spaces
39//! based on the current column position and a defined tab size.
40//! * **Flexible Input:** Accepts any type that can be referenced as a string (`&str`, `String`, `&String`, etc.)
41//! using [`AsRef<str>`].
42//! * **Configurable Tab Size:** Easily specify how many spaces each tab stop represents.
43//! * **Lightweight:** Minimal dependencies, focusing solely on efficient whitespace conversion.
44//!
45//! ## Installation
46//!
47//! Add `convert-to-spaces` to your `Cargo.toml` file:
48//!
49//! ```toml
50//! [dependencies]
51//! convert-to-spaces = "0.1" # Use the latest version from crates.io
52//! ```
53//!
54//! Then, run `cargo build` or `cargo update`.
55//!
56//! ## Usage
57//!
58//! The primary function provided by this crate is [`convert_to_spaces`].
59//!
60//! Here's how to use it:
61//!
62//! ```rust
63//! use convert_to_spaces::convert_to_spaces;
64//!
65//! fn main() {
66//! // Example 1: Basic conversion with a tab size of 4
67//! let input1 = "fn main() {\n\tprintln!(\"Hello, world!\");\n}";
68//! let output1 = convert_to_spaces(input1, 4);
69//! println!("Input:\n{}", input1);
70//! println!("Output (tab size 4):\n{}", output1);
71//! // Expected output:
72//! // fn main() {
73//! // println!("Hello, world!");
74//! // }
75//!
76//! println!("--------------------");
77//!
78//! // Example 2: Text that doesn't start at column 0
79//! let input2 = " Some text\taligned"; // 4 spaces before "Some text"
80//! let output2 = convert_to_spaces(input2, 8);
81//! println!("Input:\n{}", input2);
82//! println!("Output (tab size 8):\n{}", output2);
83//! // Expected output:
84//! // Some text aligned
85//! // ("Some text" is 9 chars. current_column=4+9=13. Next multiple of 8 after 13 is 16. Need 16-13=3 spaces)
86//!
87//! println!("--------------------");
88//!
89//! // Example 3: Zero tab size (effectively removes tabs)
90//! let input3 = "Tab\tremoved";
91//! let output3 = convert_to_spaces(input3, 0);
92//! println!("Input:\n{}", input3);
93//! println!("Output (tab size 0):\n{}", output3);
94//! // Expected output:
95//! // Tabremoved
96//! }
97//! ```
98//!
99//! ## License
100//!
101//! This is free and unencumbered software released into the **public domain**.
102//!
103//! You may use, modify, distribute, and contribute to this code without restriction. To the extent possible under law, the author(s) of this work waive all copyright and related rights.
104//!
105//! Licensed under [CC0-1.0](https://creativecommons.org/publicdomain/zero/1.0/) OR [Unlicense](http://unlicense.org/).
106//!
107//! 
108
109/// Converts tabs in a string to a specified number of spaces, respecting tab stop logic.
110///
111/// Each tab character (`\t`) will advance the text to the next multiple of the
112/// `tab_size`. The number of spaces inserted will vary depending on the
113/// current column position.
114///
115/// # Arguments
116///
117/// * `input` - The string-like reference (`AsRef<str>`) to convert. This allows
118/// passing `&str`, `String`, `&String`, etc.
119/// * `tab_size` - The size of a tab stop. If 0, tabs are effectively removed.
120///
121/// # Returns
122///
123/// A `String` with all tab characters (`\t`) correctly replaced by spaces
124/// according to tab stop rules.
125///
126/// # Examples
127///
128/// ```
129/// use convert_to_spaces::convert_to_spaces;
130///
131/// assert_eq!(convert_to_spaces("SPACE\tTAB", 4), "SPACE TAB");
132/// assert_eq!(convert_to_spaces("\tHello", 4), " Hello");
133/// assert_eq!(convert_to_spaces("Line 1\n\tLine 2", 2), "Line 1\n Line 2");
134/// assert_eq!(convert_to_spaces(String::from("MyString\t"), 8), "MyString ");
135/// assert_eq!(convert_to_spaces("No tabs here", 4), "No tabs here");
136/// ```
137pub fn convert_to_spaces<S: AsRef<str>>(input: S, tab_size: usize) -> String {
138 let input_str = input.as_ref();
139
140 // If tab_size is 0, a tab will insert 0 spaces, effectively removing tabs.
141 if tab_size == 0 {
142 return input_str.replace('\t', "");
143 }
144
145 let mut result = String::new();
146 let mut current_column = 0;
147
148 for char_code in input_str.chars() {
149 match char_code {
150 '\t' => {
151 // Calculate how many spaces are needed to reach the next tab stop
152 let spaces_to_add = tab_size - (current_column % tab_size);
153 for _ in 0..spaces_to_add {
154 result.push(' ');
155 }
156 current_column += spaces_to_add;
157 }
158 '\n' => {
159 // Reset column count for a new line
160 result.push('\n');
161 current_column = 0;
162 }
163 c => {
164 // For other characters, simply append and increment column count
165 result.push(c);
166 // NOTE: This assumes character width is 1. For actual wide/emoji characters,
167 // `current_column` would need to be based on grapheme clusters
168 // and their display widths (e.g., using `unicode-width` crate).
169 current_column += 1;
170 }
171 }
172 }
173
174 result
175}
176
177#[cfg(test)]
178#[path = "lib_tests.rs"]
179mod tests;