Skip to main content

par_term/
paste_transform.rs

1//! Paste transformation utilities.
2//!
3//! Provides text transformations for the "Paste Special" feature, allowing users
4//! to transform clipboard content before pasting (shell escaping, case conversion,
5//! encoding, whitespace normalization, etc.).
6
7use std::fmt;
8
9/// Available paste transformations.
10///
11/// Each variant represents a text transformation that can be applied to clipboard
12/// content before pasting. Organized into categories for UI display.
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
14pub enum PasteTransform {
15    // Shell category
16    ShellSingleQuotes,
17    ShellDoubleQuotes,
18    ShellBackslash,
19
20    // Case category
21    CaseUppercase,
22    CaseLowercase,
23    CaseTitleCase,
24    CaseCamelCase,
25    CasePascalCase,
26    CaseSnakeCase,
27    CaseScreamingSnake,
28    CaseKebabCase,
29
30    // Newline category
31    NewlineSingleLine,
32    NewlineAddNewlines,
33    NewlineRemoveNewlines,
34
35    // Whitespace category
36    WhitespaceTrim,
37    WhitespaceTrimLines,
38    WhitespaceCollapseSpaces,
39    WhitespaceTabsToSpaces,
40    WhitespaceSpacesToTabs,
41    WhitespaceRemoveEmptyLines,
42    WhitespaceNormalizeLineEndings,
43
44    // Encode category
45    EncodeBase64,
46    DecodeBase64,
47    EncodeUrl,
48    DecodeUrl,
49    EncodeHex,
50    DecodeHex,
51    EncodeJsonEscape,
52    DecodeJsonUnescape,
53}
54
55impl PasteTransform {
56    /// Display name for the UI (with category prefix for searchability).
57    pub fn display_name(&self) -> &'static str {
58        match self {
59            // Shell
60            Self::ShellSingleQuotes => "Shell: Single Quotes",
61            Self::ShellDoubleQuotes => "Shell: Double Quotes",
62            Self::ShellBackslash => "Shell: Backslash Escape",
63
64            // Case
65            Self::CaseUppercase => "Case: UPPERCASE",
66            Self::CaseLowercase => "Case: lowercase",
67            Self::CaseTitleCase => "Case: Title Case",
68            Self::CaseCamelCase => "Case: camelCase",
69            Self::CasePascalCase => "Case: PascalCase",
70            Self::CaseSnakeCase => "Case: snake_case",
71            Self::CaseScreamingSnake => "Case: SCREAMING_SNAKE",
72            Self::CaseKebabCase => "Case: kebab-case",
73
74            // Newline
75            Self::NewlineSingleLine => "Newline: Paste as Single Line",
76            Self::NewlineAddNewlines => "Newline: Add Newlines",
77            Self::NewlineRemoveNewlines => "Newline: Remove Newlines",
78
79            // Whitespace
80            Self::WhitespaceTrim => "Whitespace: Trim",
81            Self::WhitespaceTrimLines => "Whitespace: Trim Lines",
82            Self::WhitespaceCollapseSpaces => "Whitespace: Collapse Spaces",
83            Self::WhitespaceTabsToSpaces => "Whitespace: Tabs to Spaces",
84            Self::WhitespaceSpacesToTabs => "Whitespace: Spaces to Tabs",
85            Self::WhitespaceRemoveEmptyLines => "Whitespace: Remove Empty Lines",
86            Self::WhitespaceNormalizeLineEndings => "Whitespace: Normalize Line Endings",
87
88            // Encode
89            Self::EncodeBase64 => "Encode: Base64",
90            Self::DecodeBase64 => "Decode: Base64",
91            Self::EncodeUrl => "Encode: URL",
92            Self::DecodeUrl => "Decode: URL",
93            Self::EncodeHex => "Encode: Hex",
94            Self::DecodeHex => "Decode: Hex",
95            Self::EncodeJsonEscape => "Encode: JSON Escape",
96            Self::DecodeJsonUnescape => "Decode: JSON Unescape",
97        }
98    }
99
100    /// Short description of what the transform does.
101    pub fn description(&self) -> &'static str {
102        match self {
103            Self::ShellSingleQuotes => "Wrap in single quotes, escape internal quotes",
104            Self::ShellDoubleQuotes => "Wrap in double quotes, escape special chars",
105            Self::ShellBackslash => "Escape special characters with backslash",
106
107            Self::CaseUppercase => "Convert all characters to uppercase",
108            Self::CaseLowercase => "Convert all characters to lowercase",
109            Self::CaseTitleCase => "Capitalize first letter of each word",
110            Self::CaseCamelCase => "Convert to camelCase (firstWordLower)",
111            Self::CasePascalCase => "Convert to PascalCase (AllWordsCapitalized)",
112            Self::CaseSnakeCase => "Convert to snake_case (lowercase_with_underscores)",
113            Self::CaseScreamingSnake => "Convert to SCREAMING_SNAKE_CASE",
114            Self::CaseKebabCase => "Convert to kebab-case (lowercase-with-hyphens)",
115
116            Self::NewlineSingleLine => "Strip all newlines, join into a single line",
117            Self::NewlineAddNewlines => "Ensure text ends with a newline after each line",
118            Self::NewlineRemoveNewlines => "Remove all newline characters",
119
120            Self::WhitespaceTrim => "Remove leading and trailing whitespace",
121            Self::WhitespaceTrimLines => "Trim whitespace from each line",
122            Self::WhitespaceCollapseSpaces => "Replace multiple spaces with single space",
123            Self::WhitespaceTabsToSpaces => "Convert tabs to 4 spaces",
124            Self::WhitespaceSpacesToTabs => "Convert 4 spaces to tabs",
125            Self::WhitespaceRemoveEmptyLines => "Remove blank lines",
126            Self::WhitespaceNormalizeLineEndings => "Convert line endings to LF (\\n)",
127
128            Self::EncodeBase64 => "Encode text as Base64",
129            Self::DecodeBase64 => "Decode Base64 to text",
130            Self::EncodeUrl => "URL/percent-encode special characters",
131            Self::DecodeUrl => "Decode URL/percent-encoded text",
132            Self::EncodeHex => "Encode text as hexadecimal",
133            Self::DecodeHex => "Decode hexadecimal to text",
134            Self::EncodeJsonEscape => "Escape text for JSON string",
135            Self::DecodeJsonUnescape => "Unescape JSON string escapes",
136        }
137    }
138
139    /// All available transformations in display order.
140    pub fn all() -> &'static [PasteTransform] {
141        &[
142            // Shell
143            Self::ShellSingleQuotes,
144            Self::ShellDoubleQuotes,
145            Self::ShellBackslash,
146            // Case
147            Self::CaseUppercase,
148            Self::CaseLowercase,
149            Self::CaseTitleCase,
150            Self::CaseCamelCase,
151            Self::CasePascalCase,
152            Self::CaseSnakeCase,
153            Self::CaseScreamingSnake,
154            Self::CaseKebabCase,
155            // Newline
156            Self::NewlineSingleLine,
157            Self::NewlineAddNewlines,
158            Self::NewlineRemoveNewlines,
159            // Whitespace
160            Self::WhitespaceTrim,
161            Self::WhitespaceTrimLines,
162            Self::WhitespaceCollapseSpaces,
163            Self::WhitespaceTabsToSpaces,
164            Self::WhitespaceSpacesToTabs,
165            Self::WhitespaceRemoveEmptyLines,
166            Self::WhitespaceNormalizeLineEndings,
167            // Encode
168            Self::EncodeBase64,
169            Self::DecodeBase64,
170            Self::EncodeUrl,
171            Self::DecodeUrl,
172            Self::EncodeHex,
173            Self::DecodeHex,
174            Self::EncodeJsonEscape,
175            Self::DecodeJsonUnescape,
176        ]
177    }
178
179    /// Check if the display name matches a fuzzy search query.
180    pub fn matches_query(&self, query: &str) -> bool {
181        if query.is_empty() {
182            return true;
183        }
184        let name = self.display_name().to_lowercase();
185        let query = query.to_lowercase();
186        // Simple substring matching - supports "b64", "shell", "upper", etc.
187        name.contains(&query)
188    }
189}
190
191impl fmt::Display for PasteTransform {
192    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
193        write!(f, "{}", self.display_name())
194    }
195}
196
197/// Apply a transformation to the input text.
198///
199/// Returns `Ok(transformed_text)` on success, or `Err(error_message)` if the
200/// transformation fails (e.g., invalid Base64 input for decode).
201pub fn transform(input: &str, transform: PasteTransform) -> Result<String, String> {
202    match transform {
203        // Shell transformations
204        PasteTransform::ShellSingleQuotes => Ok(shell_single_quote(input)),
205        PasteTransform::ShellDoubleQuotes => Ok(shell_double_quote(input)),
206        PasteTransform::ShellBackslash => Ok(shell_backslash_escape(input)),
207
208        // Case transformations
209        PasteTransform::CaseUppercase => Ok(input.to_uppercase()),
210        PasteTransform::CaseLowercase => Ok(input.to_lowercase()),
211        PasteTransform::CaseTitleCase => Ok(title_case(input)),
212        PasteTransform::CaseCamelCase => Ok(camel_case(input)),
213        PasteTransform::CasePascalCase => Ok(pascal_case(input)),
214        PasteTransform::CaseSnakeCase => Ok(snake_case(input)),
215        PasteTransform::CaseScreamingSnake => Ok(screaming_snake_case(input)),
216        PasteTransform::CaseKebabCase => Ok(kebab_case(input)),
217
218        // Newline transformations
219        PasteTransform::NewlineSingleLine => Ok(paste_as_single_line(input)),
220        PasteTransform::NewlineAddNewlines => Ok(add_newlines(input)),
221        PasteTransform::NewlineRemoveNewlines => Ok(remove_newlines(input)),
222
223        // Whitespace transformations
224        PasteTransform::WhitespaceTrim => Ok(input.trim().to_string()),
225        PasteTransform::WhitespaceTrimLines => Ok(trim_lines(input)),
226        PasteTransform::WhitespaceCollapseSpaces => Ok(collapse_spaces(input)),
227        PasteTransform::WhitespaceTabsToSpaces => Ok(input.replace('\t', "    ")),
228        PasteTransform::WhitespaceSpacesToTabs => Ok(input.replace("    ", "\t")),
229        PasteTransform::WhitespaceRemoveEmptyLines => Ok(remove_empty_lines(input)),
230        PasteTransform::WhitespaceNormalizeLineEndings => Ok(normalize_line_endings(input)),
231
232        // Encoding transformations
233        PasteTransform::EncodeBase64 => Ok(base64_encode(input)),
234        PasteTransform::DecodeBase64 => base64_decode(input),
235        PasteTransform::EncodeUrl => Ok(url_encode(input)),
236        PasteTransform::DecodeUrl => url_decode(input),
237        PasteTransform::EncodeHex => Ok(hex_encode(input)),
238        PasteTransform::DecodeHex => hex_decode(input),
239        PasteTransform::EncodeJsonEscape => Ok(json_escape(input)),
240        PasteTransform::DecodeJsonUnescape => json_unescape(input),
241    }
242}
243
244// ============================================================================
245// Shell transformations
246// ============================================================================
247
248/// Characters that require quoting/escaping in shell contexts.
249const SHELL_SPECIAL_CHARS: &[char] = &[
250    ' ', '\t', '\n', '\r', // Whitespace
251    '\'', '"', '`', // Quotes and backticks
252    '$', '!', '&', '|', // Variable expansion and control operators
253    ';', '(', ')', '{', '}', '[', ']', // Grouping and subshell
254    '<', '>', // Redirection
255    '*', '?', // Glob patterns
256    '\\', '#', '~', '^', // Escape, comments, home, history
257];
258
259fn shell_single_quote(input: &str) -> String {
260    // Single quotes: escape internal ' as '\''
261    let escaped = input.replace('\'', "'\\''");
262    format!("'{}'", escaped)
263}
264
265fn shell_double_quote(input: &str) -> String {
266    // Double quotes: escape $, `, \, ", !
267    let mut result = String::with_capacity(input.len() + 10);
268    result.push('"');
269    for c in input.chars() {
270        match c {
271            '$' | '`' | '\\' | '"' | '!' => {
272                result.push('\\');
273                result.push(c);
274            }
275            _ => result.push(c),
276        }
277    }
278    result.push('"');
279    result
280}
281
282fn shell_backslash_escape(input: &str) -> String {
283    let mut result = String::with_capacity(input.len() * 2);
284    for c in input.chars() {
285        if SHELL_SPECIAL_CHARS.contains(&c) {
286            result.push('\\');
287        }
288        result.push(c);
289    }
290    result
291}
292
293// ============================================================================
294// Case transformations
295// ============================================================================
296
297fn title_case(input: &str) -> String {
298    let mut result = String::with_capacity(input.len());
299    let mut capitalize_next = true;
300
301    for c in input.chars() {
302        if c.is_whitespace() || c == '-' || c == '_' {
303            result.push(c);
304            capitalize_next = true;
305        } else if capitalize_next {
306            for upper in c.to_uppercase() {
307                result.push(upper);
308            }
309            capitalize_next = false;
310        } else {
311            result.push(c);
312        }
313    }
314    result
315}
316
317/// Split input into words (by whitespace, hyphens, underscores, or camelCase boundaries).
318fn split_into_words(input: &str) -> Vec<String> {
319    let mut words = Vec::new();
320    let mut current_word = String::new();
321    let mut prev_was_lowercase = false;
322
323    for c in input.chars() {
324        if c.is_whitespace() || c == '-' || c == '_' {
325            if !current_word.is_empty() {
326                words.push(current_word);
327                current_word = String::new();
328            }
329            prev_was_lowercase = false;
330        } else if c.is_uppercase() && prev_was_lowercase {
331            // camelCase boundary
332            if !current_word.is_empty() {
333                words.push(current_word);
334                current_word = String::new();
335            }
336            current_word.push(c);
337            prev_was_lowercase = false;
338        } else {
339            current_word.push(c);
340            prev_was_lowercase = c.is_lowercase();
341        }
342    }
343
344    if !current_word.is_empty() {
345        words.push(current_word);
346    }
347
348    words
349}
350
351fn camel_case(input: &str) -> String {
352    let words = split_into_words(input);
353    let mut result = String::new();
354
355    for (i, word) in words.iter().enumerate() {
356        if i == 0 {
357            result.push_str(&word.to_lowercase());
358        } else {
359            let mut chars = word.chars();
360            if let Some(first) = chars.next() {
361                for upper in first.to_uppercase() {
362                    result.push(upper);
363                }
364                for c in chars {
365                    result.push(c.to_ascii_lowercase());
366                }
367            }
368        }
369    }
370    result
371}
372
373fn pascal_case(input: &str) -> String {
374    let words = split_into_words(input);
375    let mut result = String::new();
376
377    for word in &words {
378        let mut chars = word.chars();
379        if let Some(first) = chars.next() {
380            for upper in first.to_uppercase() {
381                result.push(upper);
382            }
383            for c in chars {
384                result.push(c.to_ascii_lowercase());
385            }
386        }
387    }
388    result
389}
390
391fn snake_case(input: &str) -> String {
392    let words = split_into_words(input);
393    words
394        .iter()
395        .map(|w| w.to_lowercase())
396        .collect::<Vec<_>>()
397        .join("_")
398}
399
400fn screaming_snake_case(input: &str) -> String {
401    let words = split_into_words(input);
402    words
403        .iter()
404        .map(|w| w.to_uppercase())
405        .collect::<Vec<_>>()
406        .join("_")
407}
408
409fn kebab_case(input: &str) -> String {
410    let words = split_into_words(input);
411    words
412        .iter()
413        .map(|w| w.to_lowercase())
414        .collect::<Vec<_>>()
415        .join("-")
416}
417
418// ============================================================================
419// Newline transformations
420// ============================================================================
421
422/// Strip all newlines and join into a single line, replacing newlines with spaces.
423fn paste_as_single_line(input: &str) -> String {
424    input.lines().collect::<Vec<_>>().join(" ")
425}
426
427/// Ensure each line ends with a newline character.
428fn add_newlines(input: &str) -> String {
429    if input.is_empty() {
430        return String::new();
431    }
432    let mut result: String = input.lines().collect::<Vec<_>>().join("\n");
433    if !result.ends_with('\n') {
434        result.push('\n');
435    }
436    result
437}
438
439/// Remove all newline characters from the text.
440fn remove_newlines(input: &str) -> String {
441    input.replace(['\n', '\r'], "")
442}
443
444// ============================================================================
445// Whitespace transformations
446// ============================================================================
447
448fn trim_lines(input: &str) -> String {
449    input
450        .lines()
451        .map(|line| line.trim())
452        .collect::<Vec<_>>()
453        .join("\n")
454}
455
456fn collapse_spaces(input: &str) -> String {
457    let mut result = String::with_capacity(input.len());
458    let mut prev_was_space = false;
459
460    for c in input.chars() {
461        if c == ' ' {
462            if !prev_was_space {
463                result.push(c);
464                prev_was_space = true;
465            }
466        } else {
467            result.push(c);
468            prev_was_space = false;
469        }
470    }
471    result
472}
473
474fn remove_empty_lines(input: &str) -> String {
475    input
476        .lines()
477        .filter(|line| !line.trim().is_empty())
478        .collect::<Vec<_>>()
479        .join("\n")
480}
481
482fn normalize_line_endings(input: &str) -> String {
483    input.replace("\r\n", "\n").replace('\r', "\n")
484}
485
486// ============================================================================
487// Encoding transformations
488// ============================================================================
489
490const BASE64_CHARS: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
491
492fn base64_encode(input: &str) -> String {
493    let bytes = input.as_bytes();
494    let mut result = String::with_capacity(bytes.len().div_ceil(3) * 4);
495
496    for chunk in bytes.chunks(3) {
497        let b0 = chunk[0] as u32;
498        let b1 = chunk.get(1).map(|&b| b as u32).unwrap_or(0);
499        let b2 = chunk.get(2).map(|&b| b as u32).unwrap_or(0);
500
501        let n = (b0 << 16) | (b1 << 8) | b2;
502
503        result.push(BASE64_CHARS[(n >> 18) as usize & 0x3F] as char);
504        result.push(BASE64_CHARS[(n >> 12) as usize & 0x3F] as char);
505
506        if chunk.len() > 1 {
507            result.push(BASE64_CHARS[(n >> 6) as usize & 0x3F] as char);
508        } else {
509            result.push('=');
510        }
511
512        if chunk.len() > 2 {
513            result.push(BASE64_CHARS[n as usize & 0x3F] as char);
514        } else {
515            result.push('=');
516        }
517    }
518
519    result
520}
521
522fn base64_decode(input: &str) -> Result<String, String> {
523    let input = input.trim();
524    if input.is_empty() {
525        return Ok(String::new());
526    }
527
528    // Build reverse lookup table
529    let mut decode_table = [255u8; 256];
530    for (i, &c) in BASE64_CHARS.iter().enumerate() {
531        decode_table[c as usize] = i as u8;
532    }
533
534    let mut bytes = Vec::with_capacity(input.len() * 3 / 4);
535    let mut buffer = 0u32;
536    let mut bits_collected = 0;
537
538    for c in input.chars() {
539        if c == '=' {
540            break;
541        }
542        if c.is_whitespace() {
543            continue;
544        }
545
546        let value = decode_table[c as usize];
547        if value == 255 {
548            return Err(format!("Invalid Base64 character: '{}'", c));
549        }
550
551        buffer = (buffer << 6) | (value as u32);
552        bits_collected += 6;
553
554        if bits_collected >= 8 {
555            bits_collected -= 8;
556            bytes.push((buffer >> bits_collected) as u8);
557            buffer &= (1 << bits_collected) - 1;
558        }
559    }
560
561    String::from_utf8(bytes).map_err(|e| format!("Invalid UTF-8 in decoded data: {}", e))
562}
563
564fn url_encode(input: &str) -> String {
565    let mut result = String::with_capacity(input.len() * 3);
566
567    for c in input.chars() {
568        match c {
569            'A'..='Z' | 'a'..='z' | '0'..='9' | '-' | '_' | '.' | '~' => {
570                result.push(c);
571            }
572            _ => {
573                for byte in c.to_string().as_bytes() {
574                    result.push('%');
575                    result.push_str(&format!("{:02X}", byte));
576                }
577            }
578        }
579    }
580    result
581}
582
583fn url_decode(input: &str) -> Result<String, String> {
584    let mut bytes = Vec::with_capacity(input.len());
585    let mut chars = input.chars().peekable();
586
587    while let Some(c) = chars.next() {
588        if c == '%' {
589            let hex: String = chars.by_ref().take(2).collect();
590            if hex.len() != 2 {
591                return Err("Incomplete percent-encoding".to_string());
592            }
593            match u8::from_str_radix(&hex, 16) {
594                Ok(byte) => bytes.push(byte),
595                Err(_) => return Err(format!("Invalid hex in URL encoding: %{}", hex)),
596            }
597        } else if c == '+' {
598            bytes.push(b' ');
599        } else {
600            for byte in c.to_string().as_bytes() {
601                bytes.push(*byte);
602            }
603        }
604    }
605
606    String::from_utf8(bytes).map_err(|e| format!("Invalid UTF-8 in decoded data: {}", e))
607}
608
609fn hex_encode(input: &str) -> String {
610    input
611        .as_bytes()
612        .iter()
613        .map(|b| format!("{:02x}", b))
614        .collect()
615}
616
617fn hex_decode(input: &str) -> Result<String, String> {
618    let input = input.trim();
619    if input.is_empty() {
620        return Ok(String::new());
621    }
622
623    // Remove common hex prefixes
624    let input = input
625        .strip_prefix("0x")
626        .or_else(|| input.strip_prefix("0X"))
627        .unwrap_or(input);
628
629    // Filter out whitespace and collect hex chars
630    let hex_chars: String = input.chars().filter(|c| !c.is_whitespace()).collect();
631
632    if !hex_chars.len().is_multiple_of(2) {
633        return Err("Hex string must have even length".to_string());
634    }
635
636    let bytes: Result<Vec<u8>, _> = (0..hex_chars.len())
637        .step_by(2)
638        .map(|i| {
639            u8::from_str_radix(&hex_chars[i..i + 2], 16)
640                .map_err(|_| format!("Invalid hex: {}", &hex_chars[i..i + 2]))
641        })
642        .collect();
643
644    let bytes = bytes?;
645    String::from_utf8(bytes).map_err(|e| format!("Invalid UTF-8 in decoded data: {}", e))
646}
647
648fn json_escape(input: &str) -> String {
649    let mut result = String::with_capacity(input.len() + 10);
650
651    for c in input.chars() {
652        match c {
653            '"' => result.push_str("\\\""),
654            '\\' => result.push_str("\\\\"),
655            '\n' => result.push_str("\\n"),
656            '\r' => result.push_str("\\r"),
657            '\t' => result.push_str("\\t"),
658            '\x08' => result.push_str("\\b"), // backspace
659            '\x0C' => result.push_str("\\f"), // form feed
660            c if c.is_control() => {
661                result.push_str(&format!("\\u{:04x}", c as u32));
662            }
663            _ => result.push(c),
664        }
665    }
666    result
667}
668
669fn json_unescape(input: &str) -> Result<String, String> {
670    let mut result = String::with_capacity(input.len());
671    let mut chars = input.chars().peekable();
672
673    while let Some(c) = chars.next() {
674        if c == '\\' {
675            match chars.next() {
676                Some('"') => result.push('"'),
677                Some('\\') => result.push('\\'),
678                Some('/') => result.push('/'),
679                Some('n') => result.push('\n'),
680                Some('r') => result.push('\r'),
681                Some('t') => result.push('\t'),
682                Some('b') => result.push('\x08'),
683                Some('f') => result.push('\x0C'),
684                Some('u') => {
685                    let hex: String = chars.by_ref().take(4).collect();
686                    if hex.len() != 4 {
687                        return Err("Incomplete \\u escape sequence".to_string());
688                    }
689                    match u32::from_str_radix(&hex, 16) {
690                        Ok(code) => match char::from_u32(code) {
691                            Some(ch) => result.push(ch),
692                            None => return Err(format!("Invalid Unicode code point: \\u{}", hex)),
693                        },
694                        Err(_) => return Err(format!("Invalid hex in \\u escape: {}", hex)),
695                    }
696                }
697                Some(other) => {
698                    // Unknown escape, keep as-is
699                    result.push('\\');
700                    result.push(other);
701                }
702                None => result.push('\\'),
703            }
704        } else {
705            result.push(c);
706        }
707    }
708    Ok(result)
709}
710
711// ============================================================================
712// Tests
713// ============================================================================
714
715#[cfg(test)]
716mod tests {
717    use super::*;
718
719    // Shell transformations
720    #[test]
721    fn test_shell_single_quotes() {
722        assert_eq!(
723            transform("hello world", PasteTransform::ShellSingleQuotes).unwrap(),
724            "'hello world'"
725        );
726        assert_eq!(
727            transform("it's a test", PasteTransform::ShellSingleQuotes).unwrap(),
728            "'it'\\''s a test'"
729        );
730    }
731
732    #[test]
733    fn test_shell_double_quotes() {
734        assert_eq!(
735            transform("hello world", PasteTransform::ShellDoubleQuotes).unwrap(),
736            "\"hello world\""
737        );
738        assert_eq!(
739            transform("$HOME/file", PasteTransform::ShellDoubleQuotes).unwrap(),
740            "\"\\$HOME/file\""
741        );
742    }
743
744    #[test]
745    fn test_shell_backslash() {
746        assert_eq!(
747            transform("hello world", PasteTransform::ShellBackslash).unwrap(),
748            "hello\\ world"
749        );
750        assert_eq!(
751            transform("$var", PasteTransform::ShellBackslash).unwrap(),
752            "\\$var"
753        );
754    }
755
756    // Case transformations
757    #[test]
758    fn test_case_uppercase() {
759        assert_eq!(
760            transform("Hello World", PasteTransform::CaseUppercase).unwrap(),
761            "HELLO WORLD"
762        );
763    }
764
765    #[test]
766    fn test_case_lowercase() {
767        assert_eq!(
768            transform("Hello World", PasteTransform::CaseLowercase).unwrap(),
769            "hello world"
770        );
771    }
772
773    #[test]
774    fn test_case_title_case() {
775        assert_eq!(
776            transform("hello world", PasteTransform::CaseTitleCase).unwrap(),
777            "Hello World"
778        );
779        assert_eq!(
780            transform("hello-world", PasteTransform::CaseTitleCase).unwrap(),
781            "Hello-World"
782        );
783    }
784
785    #[test]
786    fn test_case_camel_case() {
787        assert_eq!(
788            transform("hello world", PasteTransform::CaseCamelCase).unwrap(),
789            "helloWorld"
790        );
791        assert_eq!(
792            transform("Hello World", PasteTransform::CaseCamelCase).unwrap(),
793            "helloWorld"
794        );
795        assert_eq!(
796            transform("hello_world", PasteTransform::CaseCamelCase).unwrap(),
797            "helloWorld"
798        );
799    }
800
801    #[test]
802    fn test_case_pascal_case() {
803        assert_eq!(
804            transform("hello world", PasteTransform::CasePascalCase).unwrap(),
805            "HelloWorld"
806        );
807    }
808
809    #[test]
810    fn test_case_snake_case() {
811        assert_eq!(
812            transform("Hello World", PasteTransform::CaseSnakeCase).unwrap(),
813            "hello_world"
814        );
815        assert_eq!(
816            transform("helloWorld", PasteTransform::CaseSnakeCase).unwrap(),
817            "hello_world"
818        );
819    }
820
821    #[test]
822    fn test_case_screaming_snake() {
823        assert_eq!(
824            transform("Hello World", PasteTransform::CaseScreamingSnake).unwrap(),
825            "HELLO_WORLD"
826        );
827    }
828
829    #[test]
830    fn test_case_kebab_case() {
831        assert_eq!(
832            transform("Hello World", PasteTransform::CaseKebabCase).unwrap(),
833            "hello-world"
834        );
835    }
836
837    // Newline transformations
838    #[test]
839    fn test_newline_single_line() {
840        assert_eq!(
841            transform("line1\nline2\nline3", PasteTransform::NewlineSingleLine).unwrap(),
842            "line1 line2 line3"
843        );
844        assert_eq!(
845            transform("single line", PasteTransform::NewlineSingleLine).unwrap(),
846            "single line"
847        );
848    }
849
850    #[test]
851    fn test_newline_add_newlines() {
852        assert_eq!(
853            transform("line1\nline2", PasteTransform::NewlineAddNewlines).unwrap(),
854            "line1\nline2\n"
855        );
856        // Already has trailing newline
857        assert_eq!(
858            transform("line1\nline2\n", PasteTransform::NewlineAddNewlines).unwrap(),
859            "line1\nline2\n"
860        );
861    }
862
863    #[test]
864    fn test_newline_remove_newlines() {
865        assert_eq!(
866            transform("line1\nline2\nline3", PasteTransform::NewlineRemoveNewlines).unwrap(),
867            "line1line2line3"
868        );
869        assert_eq!(
870            transform("line1\r\nline2", PasteTransform::NewlineRemoveNewlines).unwrap(),
871            "line1line2"
872        );
873    }
874
875    // Whitespace transformations
876    #[test]
877    fn test_whitespace_trim() {
878        assert_eq!(
879            transform("  hello  ", PasteTransform::WhitespaceTrim).unwrap(),
880            "hello"
881        );
882    }
883
884    #[test]
885    fn test_whitespace_trim_lines() {
886        assert_eq!(
887            transform("  line1  \n  line2  ", PasteTransform::WhitespaceTrimLines).unwrap(),
888            "line1\nline2"
889        );
890    }
891
892    #[test]
893    fn test_whitespace_collapse_spaces() {
894        assert_eq!(
895            transform("hello    world", PasteTransform::WhitespaceCollapseSpaces).unwrap(),
896            "hello world"
897        );
898    }
899
900    #[test]
901    fn test_whitespace_tabs_to_spaces() {
902        assert_eq!(
903            transform("hello\tworld", PasteTransform::WhitespaceTabsToSpaces).unwrap(),
904            "hello    world"
905        );
906    }
907
908    #[test]
909    fn test_whitespace_spaces_to_tabs() {
910        assert_eq!(
911            transform("hello    world", PasteTransform::WhitespaceSpacesToTabs).unwrap(),
912            "hello\tworld"
913        );
914    }
915
916    #[test]
917    fn test_whitespace_remove_empty_lines() {
918        assert_eq!(
919            transform(
920                "line1\n\nline2\n  \nline3",
921                PasteTransform::WhitespaceRemoveEmptyLines
922            )
923            .unwrap(),
924            "line1\nline2\nline3"
925        );
926    }
927
928    #[test]
929    fn test_whitespace_normalize_line_endings() {
930        assert_eq!(
931            transform(
932                "line1\r\nline2\rline3",
933                PasteTransform::WhitespaceNormalizeLineEndings
934            )
935            .unwrap(),
936            "line1\nline2\nline3"
937        );
938    }
939
940    // Encoding transformations
941    #[test]
942    fn test_encode_base64() {
943        assert_eq!(
944            transform("hello", PasteTransform::EncodeBase64).unwrap(),
945            "aGVsbG8="
946        );
947        assert_eq!(
948            transform("Hello World!", PasteTransform::EncodeBase64).unwrap(),
949            "SGVsbG8gV29ybGQh"
950        );
951    }
952
953    #[test]
954    fn test_decode_base64() {
955        assert_eq!(
956            transform("aGVsbG8=", PasteTransform::DecodeBase64).unwrap(),
957            "hello"
958        );
959        assert_eq!(
960            transform("SGVsbG8gV29ybGQh", PasteTransform::DecodeBase64).unwrap(),
961            "Hello World!"
962        );
963    }
964
965    #[test]
966    fn test_base64_roundtrip() {
967        let original = "The quick brown fox jumps over the lazy dog!";
968        let encoded = transform(original, PasteTransform::EncodeBase64).unwrap();
969        let decoded = transform(&encoded, PasteTransform::DecodeBase64).unwrap();
970        assert_eq!(decoded, original);
971    }
972
973    #[test]
974    fn test_encode_url() {
975        assert_eq!(
976            transform("hello world", PasteTransform::EncodeUrl).unwrap(),
977            "hello%20world"
978        );
979        assert_eq!(
980            transform("a=b&c=d", PasteTransform::EncodeUrl).unwrap(),
981            "a%3Db%26c%3Dd"
982        );
983    }
984
985    #[test]
986    fn test_decode_url() {
987        assert_eq!(
988            transform("hello%20world", PasteTransform::DecodeUrl).unwrap(),
989            "hello world"
990        );
991        assert_eq!(
992            transform("hello+world", PasteTransform::DecodeUrl).unwrap(),
993            "hello world"
994        );
995    }
996
997    #[test]
998    fn test_url_roundtrip() {
999        let original = "hello world! & goodbye=yes";
1000        let encoded = transform(original, PasteTransform::EncodeUrl).unwrap();
1001        let decoded = transform(&encoded, PasteTransform::DecodeUrl).unwrap();
1002        assert_eq!(decoded, original);
1003    }
1004
1005    #[test]
1006    fn test_encode_hex() {
1007        assert_eq!(
1008            transform("hello", PasteTransform::EncodeHex).unwrap(),
1009            "68656c6c6f"
1010        );
1011    }
1012
1013    #[test]
1014    fn test_decode_hex() {
1015        assert_eq!(
1016            transform("68656c6c6f", PasteTransform::DecodeHex).unwrap(),
1017            "hello"
1018        );
1019        assert_eq!(
1020            transform("0x68656c6c6f", PasteTransform::DecodeHex).unwrap(),
1021            "hello"
1022        );
1023    }
1024
1025    #[test]
1026    fn test_hex_roundtrip() {
1027        let original = "Hello World!";
1028        let encoded = transform(original, PasteTransform::EncodeHex).unwrap();
1029        let decoded = transform(&encoded, PasteTransform::DecodeHex).unwrap();
1030        assert_eq!(decoded, original);
1031    }
1032
1033    #[test]
1034    fn test_encode_json_escape() {
1035        assert_eq!(
1036            transform("hello\nworld", PasteTransform::EncodeJsonEscape).unwrap(),
1037            "hello\\nworld"
1038        );
1039        assert_eq!(
1040            transform("say \"hi\"", PasteTransform::EncodeJsonEscape).unwrap(),
1041            "say \\\"hi\\\""
1042        );
1043    }
1044
1045    #[test]
1046    fn test_decode_json_unescape() {
1047        assert_eq!(
1048            transform("hello\\nworld", PasteTransform::DecodeJsonUnescape).unwrap(),
1049            "hello\nworld"
1050        );
1051        assert_eq!(
1052            transform("say \\\"hi\\\"", PasteTransform::DecodeJsonUnescape).unwrap(),
1053            "say \"hi\""
1054        );
1055    }
1056
1057    #[test]
1058    fn test_json_roundtrip() {
1059        let original = "Line1\nLine2\tTabbed \"quoted\"";
1060        let encoded = transform(original, PasteTransform::EncodeJsonEscape).unwrap();
1061        let decoded = transform(&encoded, PasteTransform::DecodeJsonUnescape).unwrap();
1062        assert_eq!(decoded, original);
1063    }
1064
1065    // Edge cases
1066    #[test]
1067    fn test_empty_string() {
1068        for transform_type in PasteTransform::all() {
1069            let result = transform("", *transform_type);
1070            assert!(
1071                result.is_ok(),
1072                "Transform {:?} failed on empty string",
1073                transform_type
1074            );
1075        }
1076    }
1077
1078    #[test]
1079    fn test_unicode() {
1080        // Uppercase preserves emojis
1081        assert_eq!(
1082            transform("Hello! ", PasteTransform::CaseUppercase).unwrap(),
1083            "HELLO! "
1084        );
1085        // Base64 encoding of emoji (rocket is F0 9F 9A 81 in UTF-8)
1086        let encoded = transform("", PasteTransform::EncodeBase64).unwrap();
1087        let decoded = transform(&encoded, PasteTransform::DecodeBase64).unwrap();
1088        assert_eq!(decoded, "");
1089    }
1090
1091    #[test]
1092    fn test_fuzzy_match() {
1093        // Substring matching on display name
1094        assert!(PasteTransform::EncodeBase64.matches_query("base"));
1095        assert!(PasteTransform::EncodeBase64.matches_query("Base64"));
1096        assert!(PasteTransform::ShellSingleQuotes.matches_query("shell"));
1097        assert!(PasteTransform::ShellSingleQuotes.matches_query("single"));
1098        assert!(PasteTransform::CaseUppercase.matches_query("upper"));
1099        assert!(PasteTransform::CaseUppercase.matches_query("CASE"));
1100        assert!(PasteTransform::CaseUppercase.matches_query("")); // empty matches all
1101        assert!(!PasteTransform::CaseUppercase.matches_query("xyz"));
1102    }
1103
1104    // Error cases
1105    #[test]
1106    fn test_invalid_base64() {
1107        let result = transform("not valid base64!!!", PasteTransform::DecodeBase64);
1108        assert!(result.is_err());
1109    }
1110
1111    #[test]
1112    fn test_invalid_hex() {
1113        let result = transform("xyz", PasteTransform::DecodeHex);
1114        assert!(result.is_err());
1115
1116        let result = transform("abc", PasteTransform::DecodeHex); // odd length
1117        assert!(result.is_err());
1118    }
1119
1120    #[test]
1121    fn test_invalid_url_encoding() {
1122        let result = transform("%ZZ", PasteTransform::DecodeUrl);
1123        assert!(result.is_err());
1124    }
1125}