1use std::fmt;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
14pub enum PasteTransform {
15 ShellSingleQuotes,
17 ShellDoubleQuotes,
18 ShellBackslash,
19
20 CaseUppercase,
22 CaseLowercase,
23 CaseTitleCase,
24 CaseCamelCase,
25 CasePascalCase,
26 CaseSnakeCase,
27 CaseScreamingSnake,
28 CaseKebabCase,
29
30 NewlineSingleLine,
32 NewlineAddNewlines,
33 NewlineRemoveNewlines,
34
35 WhitespaceTrim,
37 WhitespaceTrimLines,
38 WhitespaceCollapseSpaces,
39 WhitespaceTabsToSpaces,
40 WhitespaceSpacesToTabs,
41 WhitespaceRemoveEmptyLines,
42 WhitespaceNormalizeLineEndings,
43
44 EncodeBase64,
46 DecodeBase64,
47 EncodeUrl,
48 DecodeUrl,
49 EncodeHex,
50 DecodeHex,
51 EncodeJsonEscape,
52 DecodeJsonUnescape,
53}
54
55impl PasteTransform {
56 pub fn display_name(&self) -> &'static str {
58 match self {
59 Self::ShellSingleQuotes => "Shell: Single Quotes",
61 Self::ShellDoubleQuotes => "Shell: Double Quotes",
62 Self::ShellBackslash => "Shell: Backslash Escape",
63
64 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 Self::NewlineSingleLine => "Newline: Paste as Single Line",
76 Self::NewlineAddNewlines => "Newline: Add Newlines",
77 Self::NewlineRemoveNewlines => "Newline: Remove Newlines",
78
79 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 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 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 pub fn all() -> &'static [PasteTransform] {
141 &[
142 Self::ShellSingleQuotes,
144 Self::ShellDoubleQuotes,
145 Self::ShellBackslash,
146 Self::CaseUppercase,
148 Self::CaseLowercase,
149 Self::CaseTitleCase,
150 Self::CaseCamelCase,
151 Self::CasePascalCase,
152 Self::CaseSnakeCase,
153 Self::CaseScreamingSnake,
154 Self::CaseKebabCase,
155 Self::NewlineSingleLine,
157 Self::NewlineAddNewlines,
158 Self::NewlineRemoveNewlines,
159 Self::WhitespaceTrim,
161 Self::WhitespaceTrimLines,
162 Self::WhitespaceCollapseSpaces,
163 Self::WhitespaceTabsToSpaces,
164 Self::WhitespaceSpacesToTabs,
165 Self::WhitespaceRemoveEmptyLines,
166 Self::WhitespaceNormalizeLineEndings,
167 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 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 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
197pub fn transform(input: &str, transform: PasteTransform) -> Result<String, String> {
202 match transform {
203 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 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 PasteTransform::NewlineSingleLine => Ok(paste_as_single_line(input)),
220 PasteTransform::NewlineAddNewlines => Ok(add_newlines(input)),
221 PasteTransform::NewlineRemoveNewlines => Ok(remove_newlines(input)),
222
223 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 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
244const SHELL_SPECIAL_CHARS: &[char] = &[
250 ' ', '\t', '\n', '\r', '\'', '"', '`', '$', '!', '&', '|', ';', '(', ')', '{', '}', '[', ']', '<', '>', '*', '?', '\\', '#', '~', '^', ];
258
259fn shell_single_quote(input: &str) -> String {
260 let escaped = input.replace('\'', "'\\''");
262 format!("'{}'", escaped)
263}
264
265fn shell_double_quote(input: &str) -> String {
266 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
293fn 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
317fn 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 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
418fn paste_as_single_line(input: &str) -> String {
424 input.lines().collect::<Vec<_>>().join(" ")
425}
426
427fn 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
439fn remove_newlines(input: &str) -> String {
441 input.replace(['\n', '\r'], "")
442}
443
444fn 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
486const 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 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 let input = input
625 .strip_prefix("0x")
626 .or_else(|| input.strip_prefix("0X"))
627 .unwrap_or(input);
628
629 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"), '\x0C' => result.push_str("\\f"), 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 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#[cfg(test)]
716mod tests {
717 use super::*;
718
719 #[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 #[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 #[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 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 #[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 #[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 #[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 assert_eq!(
1082 transform("Hello! ", PasteTransform::CaseUppercase).unwrap(),
1083 "HELLO! "
1084 );
1085 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 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("")); assert!(!PasteTransform::CaseUppercase.matches_query("xyz"));
1102 }
1103
1104 #[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); 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}