spikard_cli/codegen/common/escaping.rs
1//! Language-specific string escaping utilities for code generation.
2//!
3//! This module provides unified escaping functions for strings that will be embedded
4//! in generated code across multiple languages. Each language has specific quote characters,
5//! escape sequences, and special cases that must be handled correctly to produce valid,
6//! compilable code.
7//!
8//! # Examples
9//!
10//! ```no_run
11//! use spikard_cli::codegen::common::escaping::{EscapeContext, escape_quotes, escape_for_docstring};
12//!
13//! // Escape quotes for a Python single-quoted string
14//! let python_str = escape_quotes("it's a string", EscapeContext::Python);
15//! assert!(python_str.contains("\\'"));
16//!
17//! // Escape for a Python docstring (triple-quoted)
18//! let docstring = escape_for_docstring("The description says \"\"\"", EscapeContext::Python);
19//! assert!(!docstring.contains("\"\"\""));
20//! ```
21
22/// Language context for determining escape sequences and quote characters.
23///
24/// Different languages have different conventions for string literals:
25/// - **Python**: Uses both single/double quotes and triple-quotes for docstrings
26/// - **JavaScript/TypeScript**: Supports template literals with backticks, and single/double quotes
27/// - **Ruby**: Uses both single and double quotes with different escape rules
28/// - **PHP**: Requires strict escaping of backslashes and quotes
29/// - **Rust**: Raw strings and standard escape sequences
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub enum EscapeContext {
32 /// Python (3.10+): Handles single/double quotes and triple-quoted strings
33 Python,
34 /// JavaScript/TypeScript: Template literals with backtick support
35 JavaScript,
36 /// Ruby: Single and double quoted strings with different escape sequences
37 Ruby,
38 /// PHP: Single and double quoted strings with strict escaping rules
39 Php,
40 /// Rust: Standard escape sequences and raw strings
41 Rust,
42}
43
44/// Escape a string for use in a single-quoted string literal in the target language.
45///
46/// This handles language-specific quote character escaping and necessary backslash escaping.
47/// The result can be safely embedded in a single-quoted string of the target language.
48///
49/// # Arguments
50///
51/// * `s` - The string to escape
52/// * `context` - The target language context
53///
54/// # Returns
55///
56/// A string with appropriate escape sequences for the target language's single-quoted strings
57///
58/// # Examples
59///
60/// ```no_run
61/// use spikard_cli::codegen::common::escaping::{EscapeContext, escape_quotes};
62///
63/// // Escape for PHP single-quoted strings
64/// assert_eq!(escape_quotes("path\\to\\file", EscapeContext::Php), "path\\\\to\\\\file");
65/// assert_eq!(escape_quotes("it's", EscapeContext::Php), "it\\'s");
66///
67/// // Escape for Ruby single-quoted strings
68/// assert_eq!(escape_quotes("it's", EscapeContext::Ruby), "it\\'s");
69/// ```
70#[must_use]
71pub fn escape_quotes(s: &str, context: EscapeContext) -> String {
72 match context {
73 EscapeContext::Python => s.replace('\\', "\\\\").replace('\'', "\\'"),
74 EscapeContext::JavaScript => s.replace('\\', "\\\\").replace('\'', "\\'"),
75 EscapeContext::Ruby => s.replace('\\', "\\\\").replace('\'', "\\'"),
76 EscapeContext::Php => s.replace('\\', "\\\\").replace('\'', "\\'"),
77 EscapeContext::Rust => s.replace('\\', "\\\\").replace('\'', "\\'"),
78 }
79}
80
81/// Escape a string for use in double-quoted string literals.
82///
83/// This handles double-quote escaping, backslash escaping, and any language-specific
84/// special characters (like dollar signs in template strings).
85///
86/// # Arguments
87///
88/// * `s` - The string to escape
89/// * `context` - The target language context
90///
91/// # Returns
92///
93/// A string with appropriate escape sequences for the target language's double-quoted strings
94///
95/// # Examples
96///
97/// ```no_run
98/// use spikard_cli::codegen::common::escaping::{EscapeContext, escape_double_quotes};
99///
100/// // Python and Ruby double-quoted strings need standard escaping
101/// assert_eq!(escape_double_quotes("say \"hi\"", EscapeContext::Python), "say \\\"hi\\\"");
102/// ```
103#[must_use]
104pub fn escape_double_quotes(s: &str, context: EscapeContext) -> String {
105 match context {
106 EscapeContext::Python | EscapeContext::Rust => s.replace('\\', "\\\\").replace('"', "\\\""),
107 EscapeContext::JavaScript => s.replace('\\', "\\\\").replace('"', "\\\""),
108 EscapeContext::Ruby => s.replace('\\', "\\\\").replace('"', "\\\""),
109 EscapeContext::Php => s.replace('\\', "\\\\").replace('"', "\\\""),
110 }
111}
112
113/// Escape a string for use in template literals or backtick-delimited strings.
114///
115/// Template literals are used in JavaScript/TypeScript and some other languages.
116/// They require escaping of backticks and dollar signs (for interpolation).
117///
118/// # Arguments
119///
120/// * `s` - The string to escape
121/// * `context` - The target language context
122///
123/// # Returns
124///
125/// A string with appropriate escape sequences for template literal context
126///
127/// # Examples
128///
129/// ```no_run
130/// use spikard_cli::codegen::common::escaping::{EscapeContext, escape_template_literal};
131///
132/// // JavaScript template literals need backtick and dollar-sign escaping
133/// assert_eq!(escape_template_literal("hello $name", EscapeContext::JavaScript), "hello \\$name");
134/// assert_eq!(escape_template_literal("say `hi`", EscapeContext::JavaScript), "say \\`hi\\`");
135/// ```
136#[must_use]
137pub fn escape_template_literal(s: &str, context: EscapeContext) -> String {
138 match context {
139 EscapeContext::JavaScript => s.replace('\\', "\\\\").replace('`', "\\`").replace('$', "\\$"),
140 EscapeContext::Python | EscapeContext::Ruby | EscapeContext::Php | EscapeContext::Rust => {
141 s.replace('\\', "\\\\").replace('"', "\\\"")
142 }
143 }
144}
145
146/// Escape a string for use in docstrings or documentation comments.
147///
148/// Docstrings have language-specific delimiters and rules:
149/// - **Python**: Triple-quoted strings (`"""`) with different escape patterns
150/// - **JavaScript/TypeScript**: `JSDoc` comments with `/**` and `*/`
151/// - **Ruby**: YARD documentation with special comment markers
152/// - **PHP**: `PHPDoc` comments with special markers
153/// - **Rust**: rustdoc with `///` or `//!`
154///
155/// # Arguments
156///
157/// * `s` - The string to escape
158/// * `context` - The target language context
159///
160/// # Returns
161///
162/// A string escaped for safe embedding in docstrings of the target language
163///
164/// # Examples
165///
166/// ```no_run
167/// use spikard_cli::codegen::common::escaping::{EscapeContext, escape_for_docstring};
168///
169/// // Python docstrings use triple quotes, which must be escaped
170/// let result = escape_for_docstring("Description with \"\"\" in it", EscapeContext::Python);
171/// assert!(!result.contains("\"\"\""));
172/// assert!(result.contains("\\\""));
173/// ```
174#[must_use]
175pub fn escape_for_docstring(s: &str, context: EscapeContext) -> String {
176 match context {
177 EscapeContext::Python => s.replace("\"\"\"", "\" \" \""),
178 EscapeContext::JavaScript => s.replace("*/", "*\\/").replace('\\', "\\\\").replace('"', "\\\""),
179 EscapeContext::Ruby => s.replace('\\', "\\\\").replace('"', "\\\""),
180 EscapeContext::Php => s.replace("*/", "*\\/").replace('\\', "\\\\").replace('"', "\\\""),
181 EscapeContext::Rust => s.replace('\\', "\\\\").replace('"', "\\\""),
182 }
183}
184
185/// Escape a string for use in GraphQL SDL (Schema Definition Language) descriptions.
186///
187/// GraphQL SDL uses triple-quoted strings for descriptions, similar to Python.
188/// However, the escape rules are different - we need to escape triple quotes
189/// with backslashes.
190///
191/// # Arguments
192///
193/// * `s` - The string to escape
194/// * `context` - The target language context (mostly for consistency in codegen)
195///
196/// # Returns
197///
198/// A string escaped for safe embedding in GraphQL SDL descriptions
199///
200/// # Examples
201///
202/// ```no_run
203/// use spikard_cli::codegen::common::escaping::{EscapeContext, escape_graphql_sdl_description};
204///
205/// // GraphQL SDL descriptions use triple quotes
206/// let result = escape_graphql_sdl_description("Has \"\"\" in description", EscapeContext::Python);
207/// assert!(result.contains("\\\\\\\""));
208/// ```
209#[must_use]
210pub fn escape_graphql_sdl_description(s: &str, _context: EscapeContext) -> String {
211 s.replace("\"\"\"", "\\\"\\\"\\\"")
212}
213
214/// Escape a string for use in GraphQL SDL (Schema Definition Language) as a complete quoted string.
215///
216/// This handles escaping for string values within GraphQL SDL (like argument defaults,
217/// directive values, etc.), which use standard GraphQL string escaping rules.
218///
219/// # Arguments
220///
221/// * `s` - The string to escape
222/// * `context` - The target language context
223///
224/// # Returns
225///
226/// A string escaped for safe embedding in GraphQL SDL quoted strings
227///
228/// # Examples
229///
230/// ```no_run
231/// use spikard_cli::codegen::common::escaping::{EscapeContext, escape_graphql_string};
232///
233/// // Standard GraphQL string escaping
234/// assert_eq!(escape_graphql_string("hello \"world\"", EscapeContext::Rust), "hello \\\"world\\\"");
235/// ```
236#[must_use]
237pub fn escape_graphql_string(s: &str, _context: EscapeContext) -> String {
238 s.replace('\\', "\\\\").replace('"', "\\\"")
239}
240
241/// Escape a string for embedding in JSON strings.
242///
243/// JSON has strict escaping rules for special characters including quotes,
244/// backslashes, newlines, tabs, and control characters.
245///
246/// # Arguments
247///
248/// * `s` - The string to escape
249/// * `context` - The target language context
250///
251/// # Returns
252///
253/// A string with JSON-safe escape sequences
254///
255/// # Examples
256///
257/// ```no_run
258/// use spikard_cli::codegen::common::escaping::{EscapeContext, escape_json_string};
259///
260/// let result = escape_json_string("line1\nline2\t\"quoted\"", EscapeContext::Rust);
261/// assert!(result.contains("\\n"));
262/// assert!(result.contains("\\t"));
263/// assert!(result.contains("\\\""));
264/// ```
265#[must_use]
266pub fn escape_json_string(s: &str, _context: EscapeContext) -> String {
267 let mut result = String::new();
268 for ch in s.chars() {
269 match ch {
270 '"' => result.push_str("\\\""),
271 '\\' => result.push_str("\\\\"),
272 '\n' => result.push_str("\\n"),
273 '\r' => result.push_str("\\r"),
274 '\t' => result.push_str("\\t"),
275 '\x08' => result.push_str("\\b"),
276 '\x0C' => result.push_str("\\f"),
277 c if c.is_control() => {
278 result.push_str(&format!("\\u{:04x}", c as u32));
279 }
280 c => result.push(c),
281 }
282 }
283 result
284}
285
286#[cfg(test)]
287mod tests {
288 use super::*;
289
290 mod python {
291 use super::*;
292
293 #[test]
294 fn test_escape_quotes_simple() {
295 assert_eq!(escape_quotes("hello", EscapeContext::Python), "hello");
296 }
297
298 #[test]
299 fn test_escape_quotes_single_quote() {
300 assert_eq!(escape_quotes("it's a string", EscapeContext::Python), "it\\'s a string");
301 }
302
303 #[test]
304 fn test_escape_quotes_backslash() {
305 assert_eq!(
306 escape_quotes("path\\to\\file", EscapeContext::Python),
307 "path\\\\to\\\\file"
308 );
309 }
310
311 #[test]
312 fn test_escape_double_quotes() {
313 assert_eq!(
314 escape_double_quotes("say \"hello\"", EscapeContext::Python),
315 "say \\\"hello\\\""
316 );
317 }
318
319 #[test]
320 fn test_escape_for_docstring_triple_quotes() {
321 let result = escape_for_docstring("Description with \"\"\" in it", EscapeContext::Python);
322 assert!(!result.contains("\"\"\""));
323 assert_eq!(result, "Description with \" \" \" in it");
324 }
325
326 #[test]
327 fn test_escape_graphql_sdl_description() {
328 let result = escape_graphql_sdl_description("Has \"\"\" in description", EscapeContext::Python);
329 assert!(result.contains("\\\"\\\"\\\""));
330 assert_eq!(result, "Has \\\"\\\"\\\" in description");
331 }
332
333 #[test]
334 fn test_escape_docstring_multiple_triple_quotes() {
335 let result = escape_for_docstring("First \"\"\" and second \"\"\"", EscapeContext::Python);
336 assert!(!result.contains("\"\"\""));
337 }
338 }
339
340 mod php {
341 use super::*;
342
343 #[test]
344 fn test_escape_quotes_simple() {
345 assert_eq!(escape_quotes("hello", EscapeContext::Php), "hello");
346 }
347
348 #[test]
349 fn test_escape_quotes_single_quote() {
350 assert_eq!(escape_quotes("it's a string", EscapeContext::Php), "it\\'s a string");
351 }
352
353 #[test]
354 fn test_escape_quotes_backslash() {
355 assert_eq!(
356 escape_quotes("path\\to\\file", EscapeContext::Php),
357 "path\\\\to\\\\file"
358 );
359 }
360
361 #[test]
362 fn test_escape_double_quotes() {
363 assert_eq!(
364 escape_double_quotes("say \"hello\"", EscapeContext::Php),
365 "say \\\"hello\\\""
366 );
367 }
368
369 #[test]
370 fn test_escape_quotes_combined() {
371 assert_eq!(escape_quotes("path\\it's", EscapeContext::Php), "path\\\\it\\'s");
372 }
373 }
374
375 mod javascript {
376 use super::*;
377
378 #[test]
379 fn test_escape_quotes_simple() {
380 assert_eq!(escape_quotes("hello", EscapeContext::JavaScript), "hello");
381 }
382
383 #[test]
384 fn test_escape_quotes_single_quote() {
385 assert_eq!(
386 escape_quotes("it's a string", EscapeContext::JavaScript),
387 "it\\'s a string"
388 );
389 }
390
391 #[test]
392 fn test_escape_template_literal_backtick() {
393 assert_eq!(
394 escape_template_literal("say `hi`", EscapeContext::JavaScript),
395 "say \\`hi\\`"
396 );
397 }
398
399 #[test]
400 fn test_escape_template_literal_dollar_sign() {
401 assert_eq!(
402 escape_template_literal("hello $name", EscapeContext::JavaScript),
403 "hello \\$name"
404 );
405 }
406
407 #[test]
408 fn test_escape_template_literal_combined() {
409 assert_eq!(
410 escape_template_literal("say `$name`", EscapeContext::JavaScript),
411 "say \\`\\$name\\`"
412 );
413 }
414
415 #[test]
416 fn test_escape_for_docstring_jsdoc() {
417 let result = escape_for_docstring("Some text */", EscapeContext::JavaScript);
418 assert!(!result.contains("*/"));
419 }
420 }
421
422 mod ruby {
423 use super::*;
424
425 #[test]
426 fn test_escape_quotes_simple() {
427 assert_eq!(escape_quotes("hello", EscapeContext::Ruby), "hello");
428 }
429
430 #[test]
431 fn test_escape_quotes_single_quote() {
432 assert_eq!(escape_quotes("it's a string", EscapeContext::Ruby), "it\\'s a string");
433 }
434
435 #[test]
436 fn test_escape_quotes_backslash() {
437 assert_eq!(
438 escape_quotes("path\\to\\file", EscapeContext::Ruby),
439 "path\\\\to\\\\file"
440 );
441 }
442
443 #[test]
444 fn test_escape_double_quotes() {
445 assert_eq!(
446 escape_double_quotes("say \"hello\"", EscapeContext::Ruby),
447 "say \\\"hello\\\""
448 );
449 }
450 }
451
452 mod json {
453 use super::*;
454
455 #[test]
456 fn test_escape_json_simple() {
457 assert_eq!(escape_json_string("hello", EscapeContext::Rust), "hello");
458 }
459
460 #[test]
461 fn test_escape_json_double_quote() {
462 assert_eq!(
463 escape_json_string("say \"hello\"", EscapeContext::Rust),
464 "say \\\"hello\\\""
465 );
466 }
467
468 #[test]
469 fn test_escape_json_newline() {
470 assert_eq!(escape_json_string("line1\nline2", EscapeContext::Rust), "line1\\nline2");
471 }
472
473 #[test]
474 fn test_escape_json_tab() {
475 assert_eq!(escape_json_string("col1\tcol2", EscapeContext::Rust), "col1\\tcol2");
476 }
477
478 #[test]
479 fn test_escape_json_combined() {
480 let result = escape_json_string("path\\to\\file with \"quotes\"\nand\ttabs", EscapeContext::Rust);
481 assert!(result.contains("\\\\"));
482 assert!(result.contains("\\\""));
483 assert!(result.contains("\\n"));
484 assert!(result.contains("\\t"));
485 }
486 }
487
488 mod graphql {
489 use super::*;
490
491 #[test]
492 fn test_escape_graphql_string() {
493 assert_eq!(
494 escape_graphql_string("hello \"world\"", EscapeContext::Rust),
495 "hello \\\"world\\\""
496 );
497 }
498
499 #[test]
500 fn test_escape_graphql_backslash() {
501 assert_eq!(
502 escape_graphql_string("path\\to\\file", EscapeContext::Rust),
503 "path\\\\to\\\\file"
504 );
505 }
506 }
507
508 mod consistency {
509 use super::*;
510
511 #[test]
512 fn test_all_contexts_handle_empty_string() {
513 for context in &[
514 EscapeContext::Python,
515 EscapeContext::JavaScript,
516 EscapeContext::Ruby,
517 EscapeContext::Php,
518 EscapeContext::Rust,
519 ] {
520 assert_eq!(escape_quotes("", *context), "");
521 assert_eq!(escape_double_quotes("", *context), "");
522 }
523 }
524
525 #[test]
526 fn test_all_contexts_escape_backslash() {
527 for context in &[
528 EscapeContext::Python,
529 EscapeContext::JavaScript,
530 EscapeContext::Ruby,
531 EscapeContext::Php,
532 EscapeContext::Rust,
533 ] {
534 assert!(escape_quotes("\\", *context).contains("\\\\"));
535 }
536 }
537
538 #[test]
539 fn test_docstring_all_contexts() {
540 for context in &[
541 EscapeContext::Python,
542 EscapeContext::JavaScript,
543 EscapeContext::Ruby,
544 EscapeContext::Php,
545 EscapeContext::Rust,
546 ] {
547 let result = escape_for_docstring("test string", *context);
548 assert!(!result.is_empty());
549 }
550 }
551 }
552}