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:** No dependencies, focusing solely on efficient whitespace conversion.
44//! * **MSRV 1.56:** Access to entire *Rust 2021 Edition*.
45//! * **Public domain:** No need to give credits. Fully [free software](https://unlicense.org/).
46//!
47//! ## Installation
48//!
49//! Add `convert-to-spaces` to your `Cargo.toml` file:
50//!
51//! ```toml
52//! [dependencies]
53//! convert-to-spaces = "0.1" # Use the latest version from crates.io
54//! ```
55//!
56//! Then, run `cargo build` or `cargo update`.
57//!
58//! ## Usage
59//!
60//! The primary function provided by this crate is [`convert_to_spaces`].
61//!
62//! Here's how to use it:
63//!
64//! ```rust
65//! use convert_to_spaces::convert_to_spaces;
66//!
67//! fn main() {
68//! // Example 1: Basic conversion with a tab size of 4
69//! let input1 = "fn main() {\n\tprintln!(\"Hello, world!\");\n}";
70//! let output1 = convert_to_spaces(input1, 4);
71//! println!("Input:\n{}", input1);
72//! println!("Output (tab size 4):\n{}", output1);
73//! // Expected output:
74//! // fn main() {
75//! // println!("Hello, world!");
76//! // }
77//!
78//! println!("--------------------");
79//!
80//! // Example 2: Text that doesn't start at column 0
81//! let input2 = " Some text\taligned"; // 4 spaces before "Some text"
82//! let output2 = convert_to_spaces(input2, 8);
83//! println!("Input:\n{}", input2);
84//! println!("Output (tab size 8):\n{}", output2);
85//! // Expected output:
86//! // Some text aligned
87//! // ("Some text" is 9 chars. current_column=4+9=13. Next multiple of 8 after 13 is 16. Need 16-13=3 spaces)
88//!
89//! println!("--------------------");
90//!
91//! // Example 3: Zero tab size (effectively removes tabs)
92//! let input3 = "Tab\tremoved";
93//! let output3 = convert_to_spaces(input3, 0);
94//! println!("Input:\n{}", input3);
95//! println!("Output (tab size 0):\n{}", output3);
96//! // Expected output:
97//! // Tabremoved
98//! }
99//! ```
100//!
101//! ## License
102//!
103//! This is free and unencumbered software released into the **public domain**.
104//!
105//! 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.
106//!
107//! Licensed under [CC0-1.0](https://creativecommons.org/publicdomain/zero/1.0/) OR [Unlicense](http://unlicense.org/).
108//!
109//! 
110
111/// Converts tabs in a string to a specified number of spaces, respecting tab stop logic.
112///
113/// Each tab character (`\t`) will advance the text to the next multiple of the
114/// `tab_size`. The number of spaces inserted will vary depending on the
115/// current column position.
116///
117/// # Arguments
118///
119/// * `input` - The string-like reference (`AsRef<str>`) to convert. This allows
120/// passing `&str`, `String`, `&String`, etc.
121/// * `tab_size` - The size of a tab stop. If 0, tabs are effectively removed.
122///
123/// # Returns
124///
125/// A `String` with all tab characters (`\t`) correctly replaced by spaces
126/// according to tab stop rules.
127///
128/// # Examples
129///
130/// ```
131/// use convert_to_spaces::convert_to_spaces;
132///
133/// assert_eq!(convert_to_spaces("SPACE\tTAB", 4), "SPACE TAB");
134/// assert_eq!(convert_to_spaces("\tHello", 4), " Hello");
135/// assert_eq!(convert_to_spaces("Line 1\n\tLine 2", 2), "Line 1\n Line 2");
136/// assert_eq!(convert_to_spaces(String::from("MyString\t"), 8), "MyString ");
137/// assert_eq!(convert_to_spaces("No tabs here", 4), "No tabs here");
138/// ```
139pub fn convert_to_spaces<S: AsRef<str>>(input: S, tab_size: usize) -> String {
140 let input_str = input.as_ref();
141
142 // If tab_size is 0, a tab will insert 0 spaces, effectively removing tabs.
143 if tab_size == 0 {
144 return input_str.replace('\t', "");
145 }
146
147 let mut result = String::new();
148 let mut current_column = 0;
149
150 for char_code in input_str.chars() {
151 match char_code {
152 '\t' => {
153 // Calculate how many spaces are needed to reach the next tab stop
154 let spaces_to_add = tab_size - (current_column % tab_size);
155 for _ in 0..spaces_to_add {
156 result.push(' ');
157 }
158 current_column += spaces_to_add;
159 }
160 '\n' => {
161 // Reset column count for a new line
162 result.push('\n');
163 current_column = 0;
164 }
165 c => {
166 // For other characters, simply append and increment column count
167 result.push(c);
168 // NOTE: This assumes character width is 1. For actual wide/emoji characters,
169 // `current_column` would need to be based on grapheme clusters
170 // and their display widths (e.g., using `unicode-width` crate).
171 current_column += 1;
172 }
173 }
174 }
175
176 result
177}
178
179#[cfg(test)]
180#[path = "lib_tests.rs"]
181mod tests;