diff --git a/.cargo/deny.toml b/.cargo/deny.toml new file mode 100644 index 0000000..04dad67 --- /dev/null +++ b/.cargo/deny.toml @@ -0,0 +1,4 @@ +[licenses] +allow = [ + "MIT", +] diff --git a/.github/workflows/rust-lint.yml b/.github/workflows/rust-lint.yml new file mode 100644 index 0000000..1d6f4a8 --- /dev/null +++ b/.github/workflows/rust-lint.yml @@ -0,0 +1,27 @@ +name: Rust Lint +on: [push, pull_request] + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: dtolnay/rust-toolchain@stable + + # Check formatting + - run: cargo fmt --all -- --check + + # Run Clippy with warnings treated as errors + - run: cargo clippy --all-targets --all-features -- -D warnings + + # Fail on documentation warnings + - run: RUSTDOCFLAGS="-Dwarnings" cargo doc --no-deps --document-private-items + + # Install cargo-deny and check for security issues + - name: Install cargo-deny + run: cargo install cargo-deny + - run: cargo deny check + + # Install and run cargo-audit for security vulnerabilities + - run: cargo install cargo-audit + - run: cargo audit diff --git a/Cargo.lock b/Cargo.lock index 2ff6893..46072bb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,9 +8,19 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" +[[package]] +name = "line-ending" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "510949e2bbe320b6e4db39883314e170fde67ecda62d8844bea64e3cb1be11d8" +dependencies = [ + "doc-comment", +] + [[package]] name = "string-auto-indent" -version = "0.1.0-alpha1" +version = "0.1.0" dependencies = [ "doc-comment", + "line-ending", ] diff --git a/Cargo.toml b/Cargo.toml index 0d88688..362fa90 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "string-auto-indent" -version = "0.1.0-alpha1" +version = "0.1.0" authors = ["Jeremy Harris "] edition = "2021" description = "Normalizes multi-line string indentation while preserving platform-specific line endings." @@ -9,3 +9,4 @@ license = "MIT" [dependencies] doc-comment = "0.3.3" +line-ending = "1.2.0" diff --git a/README.md b/README.md index b7da419..9d536e0 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Multi-line String Auto Indent +# Multi-line String Auto-Indent [![made-with-rust][rust-logo]][rust-src-page] [![crates.io][crates-badge]][crates-page] @@ -29,6 +29,10 @@ cargo add string-auto-indent ## Usage +## Example 1: Basic Indentation + +This example removes unnecessary leading spaces while preserving the relative indentation of nested lines. + ```rust use string_auto_indent::{auto_indent, LineEnding}; @@ -38,29 +42,36 @@ let text = r#" Level 1 Level 2 Level 3 -"#; + "#; -// For cross-platform testing -let line_ending = LineEnding::detect(text); +// Expected output after applying auto indentation +let expected = r#" +String Auto Indent + +Level 1 + Level 2 + Level 3 +"#; -// With auto-indent +// Verify that `auto_indent` correctly normalizes indentation assert_eq!( auto_indent(text), - // For cross-platform testing: Restore platform-specific line endings - line_ending.restore("String Auto Indent\n\nLevel 1\n Level 2\n Level 3\n") + expected, + "The auto_indent function should normalize leading whitespace." ); -// Without auto-indent -assert_eq!( +// Ensure the original text is not identical to the expected output +// This confirms that `auto_indent` actually modifies the string. +assert_ne!( text, - // For cross-platform testing: Restore platform-specific line endings - line_ending.restore("\n String Auto Indent\n\n Level 1\n Level 2\n Level 3\n"), + expected, + "The original text should *not* be identical to the expected output before normalization." ); ``` ### Example Output -**With `auto-indent` enabled.** +#### With `auto-indent` ```text String Auto Indent @@ -70,7 +81,7 @@ Level 1 Level 3 ``` -**With `auto-intent` disabled.** +#### Without `auto-intent` ```text String Auto Indent @@ -80,6 +91,54 @@ Level 1 Level 3 ``` +## Example 2: Mixed Indentation + +This example demonstrates how `auto_indent` normalizes inconsistent indentation while preserving the relative structure of nested content. + +```rust +use string_auto_indent::{auto_indent, LineEnding}; + +let text = r#" + String Auto Indent + + 1. Point 1 + a. Sub point a + b. Sub point b + 2. Point 2 + a. Sub point a + b. Sub piont b + 1b. Sub piont 1b + "#; + +// Expected output after applying auto indentation +let expected = r#" +String Auto Indent + + 1. Point 1 + a. Sub point a + b. Sub point b + 2. Point 2 + a. Sub point a + b. Sub piont b + 1b. Sub piont 1b +"#; + +// Verify that `auto_indent` correctly normalizes indentation +assert_eq!( + auto_indent(text), + expected, + "The auto_indent function should normalize leading whitespace." +); + +// Ensure the original text is not identical to the expected output +// This confirms that `auto_indent` actually modifies the string. +assert_ne!( + text, + expected, + "The original text should *not* be identical to the expected output before normalization." +); +``` + ## How It Works 1. Detects the platform’s line endings (`\n`, `\r\n`, `\r`) and normalizes input for processing. @@ -93,6 +152,8 @@ Level 1 - Formatting log messages or CLI output while ensuring alignment. - Cleaning up documentation strings or multi-line literals in indented Rust code. - Processing structured text while ensuring consistent indentation. +- Declaring multi-line variables in code where the indentation should match the codebase for readability, but the actual string content should not retain unnecessary leading spaces. +- Ensuring consistent formatting in generated strings for use in templates, serialization, or output rendering. ## License Licensed under **MIT**. See [`LICENSE`][license-page] for details. diff --git a/examples/basic.rs b/examples/basic.rs index c1df09b..6925779 100644 --- a/examples/basic.rs +++ b/examples/basic.rs @@ -1,7 +1,7 @@ use string_auto_indent::auto_indent; fn main() { - println!(""); + println!(); let text = r#"Example: A B @@ -15,5 +15,5 @@ fn main() { println!("Without auto-indent:"); print!("{}", text); - println!(""); + println!(); } diff --git a/src/lib.rs b/src/lib.rs index 9dfc67c..3817325 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,7 +1,6 @@ #[cfg(doctest)] doc_comment::doctest!("../README.md"); -pub mod line_ending; pub use line_ending::LineEnding; /// Struct that encapsulates auto-indentation logic. @@ -13,7 +12,7 @@ impl AutoIndent { /// Creates a new instance by detecting the line ending from the input. fn new(input: &str) -> Self { Self { - line_ending: LineEnding::detect(input), + line_ending: LineEnding::from(input), } } @@ -23,20 +22,10 @@ impl AutoIndent { return String::new(); } - // Normalize to `\n` for consistent processing - let input = LineEnding::normalize(input); - let mut lines: Vec<&str> = input.lines().collect(); + let mut lines: Vec = LineEnding::split(input); - // Track whether the original input ended with a newline - let ends_with_newline = input.ends_with('\n'); - - // Remove the first line if it's empty - let first_line = if lines.first().map(|s| s.trim()).unwrap_or("").is_empty() { - lines.remove(0); - None - } else { - Some(lines.remove(0)) // Take first line exactly as is - }; + // Take first line exactly as is + let first_line = Some(lines.remove(0)); // Find the minimum indentation for all remaining lines let min_indent = lines @@ -67,12 +56,7 @@ impl AutoIndent { } // Preserve the original trailing newline behavior - let mut output = self.line_ending.restore_from_lines(result); - if ends_with_newline { - output.push_str(self.line_ending.as_str()); - } - - output + self.line_ending.join(result) } } @@ -86,6 +70,40 @@ mod tests { use super::*; use line_ending::LineEnding; + fn get_readme_contents() -> String { + use std::fs::File; + use std::io::Read; + + let readme_file = "README.md"; + + // Read file contents + let mut read_content = String::new(); + File::open(readme_file) + .unwrap_or_else(|_| panic!("Failed to open {}", readme_file)) + .read_to_string(&mut read_content) + .unwrap_or_else(|_| panic!("Failed to read {}", readme_file)); + + read_content + } + + #[test] + fn test_preserves_formatting() { + let readme_contents = get_readme_contents(); + + assert_eq!(auto_indent(&readme_contents), readme_contents); + + // Validate the content was actually read + let lines = LineEnding::split(&readme_contents); + assert_eq!(lines.first().unwrap(), "# Multi-line String Auto-Indent"); + + // Ensure the README has more than 5 lines + assert!( + lines.len() > 5, + "Expected README to have more than 5 lines, but got {}", + lines.len() + ); + } + #[test] fn test_basic_implementation() { let input = r#"Basic Test @@ -94,20 +112,20 @@ mod tests { 3 "#; - let line_ending = LineEnding::detect(input); + let line_ending = LineEnding::from(input); // With auto-indent assert_eq!( auto_indent(input), // string_replace_all("Basic Test\n1\n 2\n 3\n", "\n", e.as_str()) - line_ending.restore("Basic Test\n1\n 2\n 3\n") + line_ending.denormalize("Basic Test\n1\n 2\n 3\n") ); // Without auto-indent assert_eq!( input, line_ending - .restore("Basic Test\n 1\n 2\n 3\n ") + .denormalize("Basic Test\n 1\n 2\n 3\n ") ); } @@ -119,18 +137,18 @@ mod tests { 3 "#; - let line_ending = LineEnding::detect(input); + let line_ending = LineEnding::from(input); // With auto-indent assert_eq!( auto_indent(input), - line_ending.restore("1\n 2\n 3\n") + line_ending.denormalize("\n1\n 2\n 3\n") ); // Without auto-indent assert_eq!( input, - line_ending.restore("\n 1\n 2\n 3\n "), + line_ending.denormalize("\n 1\n 2\n 3\n "), ); } @@ -140,18 +158,18 @@ mod tests { Second Line "#; - let line_ending = LineEnding::detect(input); + let line_ending = LineEnding::from(input); // With auto-indent assert_eq!( auto_indent(input), - line_ending.restore(" <- First Line\nSecond Line\n") + line_ending.denormalize(" <- First Line\nSecond Line\n") ); // Without auto-indent assert_eq!( input, - line_ending.restore(" <- First Line\n Second Line\n "), + line_ending.denormalize(" <- First Line\n Second Line\n "), ); } @@ -162,18 +180,18 @@ mod tests { Third Line "#; - let line_ending = LineEnding::detect(input); + let line_ending = LineEnding::from(input); // With auto-indent assert_eq!( auto_indent(input), - line_ending.restore("First Line\n Second Line\nThird Line\n",) + line_ending.denormalize("First Line\n Second Line\nThird Line\n",) ); // Without auto-indent assert_eq!( input, - line_ending.restore("First Line\n Second Line\nThird Line\n "), + line_ending.denormalize("First Line\n Second Line\nThird Line\n "), ); } @@ -181,16 +199,16 @@ Third Line fn test_single_line_no_change() { let input = "Single line no change"; - let line_ending = LineEnding::detect(input); + let line_ending = LineEnding::from(input); // With auto-indent assert_eq!( auto_indent(input), - line_ending.restore("Single line no change") + line_ending.denormalize("Single line no change") ); // Without auto-indent - assert_eq!(input, line_ending.restore("Single line no change")); + assert_eq!(input, line_ending.denormalize("Single line no change")); } #[test] @@ -208,18 +226,18 @@ Third Line E "#; - let line_ending = LineEnding::detect(input); + let line_ending = LineEnding::from(input); // With auto-indent assert_eq!( auto_indent(input), - line_ending.restore("First Line\n\n A\n\n B\n\n C\n\n D\n\nE\n") + line_ending.denormalize("First Line\n\n A\n\n B\n\n C\n\n D\n\nE\n") ); // Without auto-indent assert_eq!( input, - line_ending.restore( + line_ending.denormalize( "First Line\n \n A\n\n B\n\n C\n\n D\n\n E\n " ), ); diff --git a/src/line_ending.rs b/src/line_ending.rs deleted file mode 100644 index 1c6ada8..0000000 --- a/src/line_ending.rs +++ /dev/null @@ -1,46 +0,0 @@ -/// Enum representing the detected line ending style. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -#[allow(clippy::upper_case_acronyms)] -pub enum LineEnding { - LF, // "\n" (Unix, Linux, macOS) - CRLF, // "\r\n" (Windows) - CR, // "\r" (old macOS) -} - -impl LineEnding { - /// Detects the line ending style used in the input string. - pub fn detect(s: &str) -> Self { - if s.contains("\r\n") { - Self::CRLF - } else if s.contains("\r") { - Self::CR - } else { - Self::LF - } - } - - /// Returns the string representation of the line ending. - pub fn as_str(&self) -> &'static str { - match self { - Self::LF => "\n", - Self::CRLF => "\r\n", - Self::CR => "\r", - } - } - - /// Normalize to `\n` for consistent processing. - pub fn normalize(s: &str) -> String { - s.replace("\r\n", "\n").replace("\r", "\n") - } - - /// Restores line endings back to their original value. - #[allow(dead_code)] - pub fn restore(&self, s: &str) -> String { - s.replace("\n", self.as_str()) - } - - /// Applies the line endiing to the given lines. - pub fn restore_from_lines(&self, lines: Vec) -> String { - lines.join(self.as_str()) - } -}