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//! [![Crates.io](https://img.shields.io/crates/v/convert-to-spaces.svg)](https://crates.io/crates/convert-to-spaces)
18//! [![License](https://img.shields.io/crates/l/convert-to-spaces)](https://unlicense.org)
19//! [![MSRV](https://img.shields.io/crates/msrv/convert-to-spaces?logo=rust&label=MSRV&labelColor=orange)](https://blog.rust-lang.org/2021/10/21/Rust-1.56.0.html)
20//! [![Docs.rs](https://docs.rs/convert-to-spaces/badge.svg)](https://docs.rs/convert-to-spaces)
21//! [![Downloads](https://img.shields.io/crates/d/convert-to-spaces)](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//! ![Unlicense logo](https://unlicense.org/pd-icon.png)
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;