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 => {
74 // Python single-quoted strings: escape single quotes and backslashes
75 s.replace('\\', "\\\\").replace('\'', "\\'")
76 }
77 EscapeContext::JavaScript => {
78 // JavaScript single-quoted strings: escape single quotes and backslashes
79 s.replace('\\', "\\\\").replace('\'', "\\'")
80 }
81 EscapeContext::Ruby => {
82 // Ruby single-quoted strings: escape single quotes and backslashes
83 s.replace('\\', "\\\\").replace('\'', "\\'")
84 }
85 EscapeContext::Php => {
86 // PHP single-quoted strings: escape single quotes and backslashes
87 s.replace('\\', "\\\\").replace('\'', "\\'")
88 }
89 EscapeContext::Rust => {
90 // Rust string literals: escape single quotes and backslashes
91 s.replace('\\', "\\\\").replace('\'', "\\'")
92 }
93 }
94}
95
96/// Escape a string for use in double-quoted string literals.
97///
98/// This handles double-quote escaping, backslash escaping, and any language-specific
99/// special characters (like dollar signs in template strings).
100///
101/// # Arguments
102///
103/// * `s` - The string to escape
104/// * `context` - The target language context
105///
106/// # Returns
107///
108/// A string with appropriate escape sequences for the target language's double-quoted strings
109///
110/// # Examples
111///
112/// ```no_run
113/// use spikard_cli::codegen::common::escaping::{EscapeContext, escape_double_quotes};
114///
115/// // Python and Ruby double-quoted strings need standard escaping
116/// assert_eq!(escape_double_quotes("say \"hi\"", EscapeContext::Python), "say \\\"hi\\\"");
117/// ```
118#[must_use]
119pub fn escape_double_quotes(s: &str, context: EscapeContext) -> String {
120 match context {
121 EscapeContext::Python | EscapeContext::Rust => {
122 // Python and Rust: escape backslashes first, then double quotes
123 s.replace('\\', "\\\\").replace('"', "\\\"")
124 }
125 EscapeContext::JavaScript => {
126 // JavaScript: same as Python but also escape backticks
127 s.replace('\\', "\\\\").replace('"', "\\\"")
128 }
129 EscapeContext::Ruby => {
130 // Ruby: same escaping as Python
131 s.replace('\\', "\\\\").replace('"', "\\\"")
132 }
133 EscapeContext::Php => {
134 // PHP: same escaping as Python
135 s.replace('\\', "\\\\").replace('"', "\\\"")
136 }
137 }
138}
139
140/// Escape a string for use in template literals or backtick-delimited strings.
141///
142/// Template literals are used in JavaScript/TypeScript and some other languages.
143/// They require escaping of backticks and dollar signs (for interpolation).
144///
145/// # Arguments
146///
147/// * `s` - The string to escape
148/// * `context` - The target language context
149///
150/// # Returns
151///
152/// A string with appropriate escape sequences for template literal context
153///
154/// # Examples
155///
156/// ```no_run
157/// use spikard_cli::codegen::common::escaping::{EscapeContext, escape_template_literal};
158///
159/// // JavaScript template literals need backtick and dollar-sign escaping
160/// assert_eq!(escape_template_literal("hello $name", EscapeContext::JavaScript), "hello \\$name");
161/// assert_eq!(escape_template_literal("say `hi`", EscapeContext::JavaScript), "say \\`hi\\`");
162/// ```
163#[must_use]
164pub fn escape_template_literal(s: &str, context: EscapeContext) -> String {
165 match context {
166 EscapeContext::JavaScript => {
167 // Escape backticks, dollar signs, and backslashes
168 s.replace('\\', "\\\\").replace('`', "\\`").replace('$', "\\$")
169 }
170 EscapeContext::Python | EscapeContext::Ruby | EscapeContext::Php | EscapeContext::Rust => {
171 // Other languages don't use template literals, just escape standard sequences
172 s.replace('\\', "\\\\").replace('"', "\\\"")
173 }
174 }
175}
176
177/// Escape a string for use in docstrings or documentation comments.
178///
179/// Docstrings have language-specific delimiters and rules:
180/// - **Python**: Triple-quoted strings (`"""`) with different escape patterns
181/// - **JavaScript/TypeScript**: `JSDoc` comments with `/**` and `*/`
182/// - **Ruby**: YARD documentation with special comment markers
183/// - **PHP**: `PHPDoc` comments with special markers
184/// - **Rust**: rustdoc with `///` or `//!`
185///
186/// # Arguments
187///
188/// * `s` - The string to escape
189/// * `context` - The target language context
190///
191/// # Returns
192///
193/// A string escaped for safe embedding in docstrings of the target language
194///
195/// # Examples
196///
197/// ```no_run
198/// use spikard_cli::codegen::common::escaping::{EscapeContext, escape_for_docstring};
199///
200/// // Python docstrings use triple quotes, which must be escaped
201/// let result = escape_for_docstring("Description with \"\"\" in it", EscapeContext::Python);
202/// assert!(!result.contains("\"\"\""));
203/// assert!(result.contains("\\\""));
204/// ```
205#[must_use]
206pub fn escape_for_docstring(s: &str, context: EscapeContext) -> String {
207 match context {
208 EscapeContext::Python => {
209 // Python triple-quoted docstrings:
210 // Replace """ with \" \" \" to break up the delimiter sequence
211 // This is safer than trying to escape within the string since Python
212 // doesn't support escaping triple quotes with backslashes
213 s.replace("\"\"\"", "\" \" \"")
214 }
215 EscapeContext::JavaScript => {
216 // JSDoc comments: standard escape for double quotes and special comment markers
217 // Avoid */ and /** within the docstring
218 s.replace("*/", "*\\/").replace('\\', "\\\\").replace('"', "\\\"")
219 }
220 EscapeContext::Ruby => {
221 // YARD documentation: standard escape for quotes
222 // Ruby uses # for comments, so avoid breaking those
223 s.replace('\\', "\\\\").replace('"', "\\\"")
224 }
225 EscapeContext::Php => {
226 // PHPDoc comments: standard escape for quotes and special markers
227 // Avoid */ and /** within the docstring
228 s.replace("*/", "*\\/").replace('\\', "\\\\").replace('"', "\\\"")
229 }
230 EscapeContext::Rust => {
231 // Rustdoc: standard escape for quotes
232 // Avoid breaking /// or //! comment markers
233 s.replace('\\', "\\\\").replace('"', "\\\"")
234 }
235 }
236}
237
238/// Escape a string for use in GraphQL SDL (Schema Definition Language) descriptions.
239///
240/// GraphQL SDL uses triple-quoted strings for descriptions, similar to Python.
241/// However, the escape rules are different - we need to escape triple quotes
242/// with backslashes.
243///
244/// # Arguments
245///
246/// * `s` - The string to escape
247/// * `context` - The target language context (mostly for consistency in codegen)
248///
249/// # Returns
250///
251/// A string escaped for safe embedding in GraphQL SDL descriptions
252///
253/// # Examples
254///
255/// ```no_run
256/// use spikard_cli::codegen::common::escaping::{EscapeContext, escape_graphql_sdl_description};
257///
258/// // GraphQL SDL descriptions use triple quotes
259/// let result = escape_graphql_sdl_description("Has \"\"\" in description", EscapeContext::Python);
260/// assert!(result.contains("\\\\\\\""));
261/// ```
262#[must_use]
263pub fn escape_graphql_sdl_description(s: &str, _context: EscapeContext) -> String {
264 // GraphQL SDL descriptions use triple-quoted strings.
265 // Escape """ with \"\"\", similar to GraphQL string escaping
266 // This produces valid GraphQL SDL that can be parsed
267 s.replace("\"\"\"", "\\\"\\\"\\\"")
268}
269
270/// Escape a string for use in GraphQL SDL (Schema Definition Language) as a complete quoted string.
271///
272/// This handles escaping for string values within GraphQL SDL (like argument defaults,
273/// directive values, etc.), which use standard GraphQL string escaping rules.
274///
275/// # Arguments
276///
277/// * `s` - The string to escape
278/// * `context` - The target language context
279///
280/// # Returns
281///
282/// A string escaped for safe embedding in GraphQL SDL quoted strings
283///
284/// # Examples
285///
286/// ```no_run
287/// use spikard_cli::codegen::common::escaping::{EscapeContext, escape_graphql_string};
288///
289/// // Standard GraphQL string escaping
290/// assert_eq!(escape_graphql_string("hello \"world\"", EscapeContext::Rust), "hello \\\"world\\\"");
291/// ```
292#[must_use]
293pub fn escape_graphql_string(s: &str, _context: EscapeContext) -> String {
294 // GraphQL string escaping: escape quotes and backslashes
295 s.replace('\\', "\\\\").replace('"', "\\\"")
296}
297
298/// Escape a string for embedding in JSON strings.
299///
300/// JSON has strict escaping rules for special characters including quotes,
301/// backslashes, newlines, tabs, and control characters.
302///
303/// # Arguments
304///
305/// * `s` - The string to escape
306/// * `context` - The target language context
307///
308/// # Returns
309///
310/// A string with JSON-safe escape sequences
311///
312/// # Examples
313///
314/// ```no_run
315/// use spikard_cli::codegen::common::escaping::{EscapeContext, escape_json_string};
316///
317/// let result = escape_json_string("line1\nline2\t\"quoted\"", EscapeContext::Rust);
318/// assert!(result.contains("\\n"));
319/// assert!(result.contains("\\t"));
320/// assert!(result.contains("\\\""));
321/// ```
322#[must_use]
323pub fn escape_json_string(s: &str, _context: EscapeContext) -> String {
324 let mut result = String::new();
325 for ch in s.chars() {
326 match ch {
327 '"' => result.push_str("\\\""),
328 '\\' => result.push_str("\\\\"),
329 '\n' => result.push_str("\\n"),
330 '\r' => result.push_str("\\r"),
331 '\t' => result.push_str("\\t"),
332 '\x08' => result.push_str("\\b"), // backspace
333 '\x0C' => result.push_str("\\f"), // form feed
334 c if c.is_control() => {
335 result.push_str(&format!("\\u{:04x}", c as u32));
336 }
337 c => result.push(c),
338 }
339 }
340 result
341}
342
343#[cfg(test)]
344mod tests {
345 use super::*;
346
347 mod python {
348 use super::*;
349
350 #[test]
351 fn test_escape_quotes_simple() {
352 assert_eq!(escape_quotes("hello", EscapeContext::Python), "hello");
353 }
354
355 #[test]
356 fn test_escape_quotes_single_quote() {
357 assert_eq!(escape_quotes("it's a string", EscapeContext::Python), "it\\'s a string");
358 }
359
360 #[test]
361 fn test_escape_quotes_backslash() {
362 assert_eq!(
363 escape_quotes("path\\to\\file", EscapeContext::Python),
364 "path\\\\to\\\\file"
365 );
366 }
367
368 #[test]
369 fn test_escape_double_quotes() {
370 assert_eq!(
371 escape_double_quotes("say \"hello\"", EscapeContext::Python),
372 "say \\\"hello\\\""
373 );
374 }
375
376 #[test]
377 fn test_escape_for_docstring_triple_quotes() {
378 let result = escape_for_docstring("Description with \"\"\" in it", EscapeContext::Python);
379 assert!(!result.contains("\"\"\""));
380 assert_eq!(result, "Description with \" \" \" in it");
381 }
382
383 #[test]
384 fn test_escape_graphql_sdl_description() {
385 let result = escape_graphql_sdl_description("Has \"\"\" in description", EscapeContext::Python);
386 assert!(result.contains("\\\"\\\"\\\""));
387 assert_eq!(result, "Has \\\"\\\"\\\" in description");
388 }
389
390 #[test]
391 fn test_escape_docstring_multiple_triple_quotes() {
392 let result = escape_for_docstring("First \"\"\" and second \"\"\"", EscapeContext::Python);
393 assert!(!result.contains("\"\"\""));
394 }
395 }
396
397 mod php {
398 use super::*;
399
400 #[test]
401 fn test_escape_quotes_simple() {
402 assert_eq!(escape_quotes("hello", EscapeContext::Php), "hello");
403 }
404
405 #[test]
406 fn test_escape_quotes_single_quote() {
407 assert_eq!(escape_quotes("it's a string", EscapeContext::Php), "it\\'s a string");
408 }
409
410 #[test]
411 fn test_escape_quotes_backslash() {
412 assert_eq!(
413 escape_quotes("path\\to\\file", EscapeContext::Php),
414 "path\\\\to\\\\file"
415 );
416 }
417
418 #[test]
419 fn test_escape_double_quotes() {
420 assert_eq!(
421 escape_double_quotes("say \"hello\"", EscapeContext::Php),
422 "say \\\"hello\\\""
423 );
424 }
425
426 #[test]
427 fn test_escape_quotes_combined() {
428 assert_eq!(escape_quotes("path\\it's", EscapeContext::Php), "path\\\\it\\'s");
429 }
430 }
431
432 mod javascript {
433 use super::*;
434
435 #[test]
436 fn test_escape_quotes_simple() {
437 assert_eq!(escape_quotes("hello", EscapeContext::JavaScript), "hello");
438 }
439
440 #[test]
441 fn test_escape_quotes_single_quote() {
442 assert_eq!(
443 escape_quotes("it's a string", EscapeContext::JavaScript),
444 "it\\'s a string"
445 );
446 }
447
448 #[test]
449 fn test_escape_template_literal_backtick() {
450 assert_eq!(
451 escape_template_literal("say `hi`", EscapeContext::JavaScript),
452 "say \\`hi\\`"
453 );
454 }
455
456 #[test]
457 fn test_escape_template_literal_dollar_sign() {
458 assert_eq!(
459 escape_template_literal("hello $name", EscapeContext::JavaScript),
460 "hello \\$name"
461 );
462 }
463
464 #[test]
465 fn test_escape_template_literal_combined() {
466 assert_eq!(
467 escape_template_literal("say `$name`", EscapeContext::JavaScript),
468 "say \\`\\$name\\`"
469 );
470 }
471
472 #[test]
473 fn test_escape_for_docstring_jsdoc() {
474 let result = escape_for_docstring("Some text */", EscapeContext::JavaScript);
475 assert!(!result.contains("*/"));
476 }
477 }
478
479 mod ruby {
480 use super::*;
481
482 #[test]
483 fn test_escape_quotes_simple() {
484 assert_eq!(escape_quotes("hello", EscapeContext::Ruby), "hello");
485 }
486
487 #[test]
488 fn test_escape_quotes_single_quote() {
489 assert_eq!(escape_quotes("it's a string", EscapeContext::Ruby), "it\\'s a string");
490 }
491
492 #[test]
493 fn test_escape_quotes_backslash() {
494 assert_eq!(
495 escape_quotes("path\\to\\file", EscapeContext::Ruby),
496 "path\\\\to\\\\file"
497 );
498 }
499
500 #[test]
501 fn test_escape_double_quotes() {
502 assert_eq!(
503 escape_double_quotes("say \"hello\"", EscapeContext::Ruby),
504 "say \\\"hello\\\""
505 );
506 }
507 }
508
509 mod json {
510 use super::*;
511
512 #[test]
513 fn test_escape_json_simple() {
514 assert_eq!(escape_json_string("hello", EscapeContext::Rust), "hello");
515 }
516
517 #[test]
518 fn test_escape_json_double_quote() {
519 assert_eq!(
520 escape_json_string("say \"hello\"", EscapeContext::Rust),
521 "say \\\"hello\\\""
522 );
523 }
524
525 #[test]
526 fn test_escape_json_newline() {
527 assert_eq!(escape_json_string("line1\nline2", EscapeContext::Rust), "line1\\nline2");
528 }
529
530 #[test]
531 fn test_escape_json_tab() {
532 assert_eq!(escape_json_string("col1\tcol2", EscapeContext::Rust), "col1\\tcol2");
533 }
534
535 #[test]
536 fn test_escape_json_combined() {
537 let result = escape_json_string("path\\to\\file with \"quotes\"\nand\ttabs", EscapeContext::Rust);
538 assert!(result.contains("\\\\"));
539 assert!(result.contains("\\\""));
540 assert!(result.contains("\\n"));
541 assert!(result.contains("\\t"));
542 }
543 }
544
545 mod graphql {
546 use super::*;
547
548 #[test]
549 fn test_escape_graphql_string() {
550 assert_eq!(
551 escape_graphql_string("hello \"world\"", EscapeContext::Rust),
552 "hello \\\"world\\\""
553 );
554 }
555
556 #[test]
557 fn test_escape_graphql_backslash() {
558 assert_eq!(
559 escape_graphql_string("path\\to\\file", EscapeContext::Rust),
560 "path\\\\to\\\\file"
561 );
562 }
563 }
564
565 mod consistency {
566 use super::*;
567
568 #[test]
569 fn test_all_contexts_handle_empty_string() {
570 for context in &[
571 EscapeContext::Python,
572 EscapeContext::JavaScript,
573 EscapeContext::Ruby,
574 EscapeContext::Php,
575 EscapeContext::Rust,
576 ] {
577 assert_eq!(escape_quotes("", *context), "");
578 assert_eq!(escape_double_quotes("", *context), "");
579 }
580 }
581
582 #[test]
583 fn test_all_contexts_escape_backslash() {
584 for context in &[
585 EscapeContext::Python,
586 EscapeContext::JavaScript,
587 EscapeContext::Ruby,
588 EscapeContext::Php,
589 EscapeContext::Rust,
590 ] {
591 assert!(escape_quotes("\\", *context).contains("\\\\"));
592 }
593 }
594
595 #[test]
596 fn test_docstring_all_contexts() {
597 // All contexts should handle basic docstring escaping
598 for context in &[
599 EscapeContext::Python,
600 EscapeContext::JavaScript,
601 EscapeContext::Ruby,
602 EscapeContext::Php,
603 EscapeContext::Rust,
604 ] {
605 let result = escape_for_docstring("test string", *context);
606 assert!(!result.is_empty());
607 }
608 }
609 }
610}