Skip to main content

par_term/paste_transform/
mod.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//!
7//! # Sub-modules
8//!
9//! - `case` — case conversion (title, camel, pascal, snake, screaming snake, kebab)
10//! - `encoding` — Base64, URL, Hex, and JSON escape/unescape
11//! - `sanitize` — clipboard content sanitization (strip dangerous control chars)
12//! - `shell` — shell quoting and backslash escaping
13//! - `whitespace` — whitespace and newline normalization
14
15mod case;
16mod encoding;
17mod sanitize;
18mod shell;
19mod whitespace;
20
21#[cfg(test)]
22mod tests;
23
24use std::fmt;
25
26// Re-export the public API
27pub use sanitize::{paste_contains_control_chars, sanitize_paste_content};
28
29use case::{camel_case, kebab_case, pascal_case, screaming_snake_case, snake_case, title_case};
30use encoding::{
31    base64_decode, base64_encode, hex_decode, hex_encode, json_escape, json_unescape, url_decode,
32    url_encode,
33};
34use shell::{shell_backslash_escape, shell_double_quote, shell_single_quote};
35use whitespace::{
36    add_newlines, collapse_spaces, normalize_line_endings, paste_as_single_line,
37    remove_empty_lines, remove_newlines, trim_lines,
38};
39
40/// Available paste transformations.
41///
42/// Each variant represents a text transformation that can be applied to clipboard
43/// content before pasting. Organized into categories for UI display.
44#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
45pub enum PasteTransform {
46    // Shell category
47    ShellSingleQuotes,
48    ShellDoubleQuotes,
49    ShellBackslash,
50
51    // Case category
52    CaseUppercase,
53    CaseLowercase,
54    CaseTitleCase,
55    CaseCamelCase,
56    CasePascalCase,
57    CaseSnakeCase,
58    CaseScreamingSnake,
59    CaseKebabCase,
60
61    // Newline category
62    NewlineSingleLine,
63    NewlineAddNewlines,
64    NewlineRemoveNewlines,
65
66    // Whitespace category
67    WhitespaceTrim,
68    WhitespaceTrimLines,
69    WhitespaceCollapseSpaces,
70    WhitespaceTabsToSpaces,
71    WhitespaceSpacesToTabs,
72    WhitespaceRemoveEmptyLines,
73    WhitespaceNormalizeLineEndings,
74
75    // Encode category
76    EncodeBase64,
77    DecodeBase64,
78    EncodeUrl,
79    DecodeUrl,
80    EncodeHex,
81    DecodeHex,
82    EncodeJsonEscape,
83    DecodeJsonUnescape,
84}
85
86impl PasteTransform {
87    /// Display name for the UI (with category prefix for searchability).
88    pub fn display_name(&self) -> &'static str {
89        match self {
90            // Shell
91            Self::ShellSingleQuotes => "Shell: Single Quotes",
92            Self::ShellDoubleQuotes => "Shell: Double Quotes",
93            Self::ShellBackslash => "Shell: Backslash Escape",
94
95            // Case
96            Self::CaseUppercase => "Case: UPPERCASE",
97            Self::CaseLowercase => "Case: lowercase",
98            Self::CaseTitleCase => "Case: Title Case",
99            Self::CaseCamelCase => "Case: camelCase",
100            Self::CasePascalCase => "Case: PascalCase",
101            Self::CaseSnakeCase => "Case: snake_case",
102            Self::CaseScreamingSnake => "Case: SCREAMING_SNAKE",
103            Self::CaseKebabCase => "Case: kebab-case",
104
105            // Newline
106            Self::NewlineSingleLine => "Newline: Paste as Single Line",
107            Self::NewlineAddNewlines => "Newline: Add Newlines",
108            Self::NewlineRemoveNewlines => "Newline: Remove Newlines",
109
110            // Whitespace
111            Self::WhitespaceTrim => "Whitespace: Trim",
112            Self::WhitespaceTrimLines => "Whitespace: Trim Lines",
113            Self::WhitespaceCollapseSpaces => "Whitespace: Collapse Spaces",
114            Self::WhitespaceTabsToSpaces => "Whitespace: Tabs to Spaces",
115            Self::WhitespaceSpacesToTabs => "Whitespace: Spaces to Tabs",
116            Self::WhitespaceRemoveEmptyLines => "Whitespace: Remove Empty Lines",
117            Self::WhitespaceNormalizeLineEndings => "Whitespace: Normalize Line Endings",
118
119            // Encode
120            Self::EncodeBase64 => "Encode: Base64",
121            Self::DecodeBase64 => "Decode: Base64",
122            Self::EncodeUrl => "Encode: URL",
123            Self::DecodeUrl => "Decode: URL",
124            Self::EncodeHex => "Encode: Hex",
125            Self::DecodeHex => "Decode: Hex",
126            Self::EncodeJsonEscape => "Encode: JSON Escape",
127            Self::DecodeJsonUnescape => "Decode: JSON Unescape",
128        }
129    }
130
131    /// Short description of what the transform does.
132    pub fn description(&self) -> &'static str {
133        match self {
134            Self::ShellSingleQuotes => "Wrap in single quotes, escape internal quotes",
135            Self::ShellDoubleQuotes => "Wrap in double quotes, escape special chars",
136            Self::ShellBackslash => "Escape special characters with backslash",
137
138            Self::CaseUppercase => "Convert all characters to uppercase",
139            Self::CaseLowercase => "Convert all characters to lowercase",
140            Self::CaseTitleCase => "Capitalize first letter of each word",
141            Self::CaseCamelCase => "Convert to camelCase (firstWordLower)",
142            Self::CasePascalCase => "Convert to PascalCase (AllWordsCapitalized)",
143            Self::CaseSnakeCase => "Convert to snake_case (lowercase_with_underscores)",
144            Self::CaseScreamingSnake => "Convert to SCREAMING_SNAKE_CASE",
145            Self::CaseKebabCase => "Convert to kebab-case (lowercase-with-hyphens)",
146
147            Self::NewlineSingleLine => "Strip all newlines, join into a single line",
148            Self::NewlineAddNewlines => "Ensure text ends with a newline after each line",
149            Self::NewlineRemoveNewlines => "Remove all newline characters",
150
151            Self::WhitespaceTrim => "Remove leading and trailing whitespace",
152            Self::WhitespaceTrimLines => "Trim whitespace from each line",
153            Self::WhitespaceCollapseSpaces => "Replace multiple spaces with single space",
154            Self::WhitespaceTabsToSpaces => "Convert tabs to 4 spaces",
155            Self::WhitespaceSpacesToTabs => "Convert 4 spaces to tabs",
156            Self::WhitespaceRemoveEmptyLines => "Remove blank lines",
157            Self::WhitespaceNormalizeLineEndings => "Convert line endings to LF (\\n)",
158
159            Self::EncodeBase64 => "Encode text as Base64",
160            Self::DecodeBase64 => "Decode Base64 to text",
161            Self::EncodeUrl => "URL/percent-encode special characters",
162            Self::DecodeUrl => "Decode URL/percent-encoded text",
163            Self::EncodeHex => "Encode text as hexadecimal",
164            Self::DecodeHex => "Decode hexadecimal to text",
165            Self::EncodeJsonEscape => "Escape text for JSON string",
166            Self::DecodeJsonUnescape => "Unescape JSON string escapes",
167        }
168    }
169
170    /// All available transformations in display order.
171    pub fn all() -> &'static [PasteTransform] {
172        &[
173            // Shell
174            Self::ShellSingleQuotes,
175            Self::ShellDoubleQuotes,
176            Self::ShellBackslash,
177            // Case
178            Self::CaseUppercase,
179            Self::CaseLowercase,
180            Self::CaseTitleCase,
181            Self::CaseCamelCase,
182            Self::CasePascalCase,
183            Self::CaseSnakeCase,
184            Self::CaseScreamingSnake,
185            Self::CaseKebabCase,
186            // Newline
187            Self::NewlineSingleLine,
188            Self::NewlineAddNewlines,
189            Self::NewlineRemoveNewlines,
190            // Whitespace
191            Self::WhitespaceTrim,
192            Self::WhitespaceTrimLines,
193            Self::WhitespaceCollapseSpaces,
194            Self::WhitespaceTabsToSpaces,
195            Self::WhitespaceSpacesToTabs,
196            Self::WhitespaceRemoveEmptyLines,
197            Self::WhitespaceNormalizeLineEndings,
198            // Encode
199            Self::EncodeBase64,
200            Self::DecodeBase64,
201            Self::EncodeUrl,
202            Self::DecodeUrl,
203            Self::EncodeHex,
204            Self::DecodeHex,
205            Self::EncodeJsonEscape,
206            Self::DecodeJsonUnescape,
207        ]
208    }
209
210    /// Check if the display name matches a fuzzy search query.
211    pub fn matches_query(&self, query: &str) -> bool {
212        if query.is_empty() {
213            return true;
214        }
215        let name = self.display_name().to_lowercase();
216        let query = query.to_lowercase();
217        // Simple substring matching - supports "b64", "shell", "upper", etc.
218        name.contains(&query)
219    }
220}
221
222impl fmt::Display for PasteTransform {
223    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
224        write!(f, "{}", self.display_name())
225    }
226}
227
228/// Apply a transformation to the input text.
229///
230/// Returns `Ok(transformed_text)` on success, or `Err(error_message)` if the
231/// transformation fails (e.g., invalid Base64 input for decode).
232pub fn transform(input: &str, transform: PasteTransform) -> Result<String, String> {
233    match transform {
234        // Shell transformations
235        PasteTransform::ShellSingleQuotes => Ok(shell_single_quote(input)),
236        PasteTransform::ShellDoubleQuotes => Ok(shell_double_quote(input)),
237        PasteTransform::ShellBackslash => Ok(shell_backslash_escape(input)),
238
239        // Case transformations
240        PasteTransform::CaseUppercase => Ok(input.to_uppercase()),
241        PasteTransform::CaseLowercase => Ok(input.to_lowercase()),
242        PasteTransform::CaseTitleCase => Ok(title_case(input)),
243        PasteTransform::CaseCamelCase => Ok(camel_case(input)),
244        PasteTransform::CasePascalCase => Ok(pascal_case(input)),
245        PasteTransform::CaseSnakeCase => Ok(snake_case(input)),
246        PasteTransform::CaseScreamingSnake => Ok(screaming_snake_case(input)),
247        PasteTransform::CaseKebabCase => Ok(kebab_case(input)),
248
249        // Newline transformations
250        PasteTransform::NewlineSingleLine => Ok(paste_as_single_line(input)),
251        PasteTransform::NewlineAddNewlines => Ok(add_newlines(input)),
252        PasteTransform::NewlineRemoveNewlines => Ok(remove_newlines(input)),
253
254        // Whitespace transformations
255        PasteTransform::WhitespaceTrim => Ok(input.trim().to_string()),
256        PasteTransform::WhitespaceTrimLines => Ok(trim_lines(input)),
257        PasteTransform::WhitespaceCollapseSpaces => Ok(collapse_spaces(input)),
258        PasteTransform::WhitespaceTabsToSpaces => Ok(input.replace('\t', "    ")),
259        PasteTransform::WhitespaceSpacesToTabs => Ok(input.replace("    ", "\t")),
260        PasteTransform::WhitespaceRemoveEmptyLines => Ok(remove_empty_lines(input)),
261        PasteTransform::WhitespaceNormalizeLineEndings => Ok(normalize_line_endings(input)),
262
263        // Encoding transformations
264        PasteTransform::EncodeBase64 => Ok(base64_encode(input)),
265        PasteTransform::DecodeBase64 => base64_decode(input),
266        PasteTransform::EncodeUrl => Ok(url_encode(input)),
267        PasteTransform::DecodeUrl => url_decode(input),
268        PasteTransform::EncodeHex => Ok(hex_encode(input)),
269        PasteTransform::DecodeHex => hex_decode(input),
270        PasteTransform::EncodeJsonEscape => Ok(json_escape(input)),
271        PasteTransform::DecodeJsonUnescape => json_unescape(input),
272    }
273}