11use std:: borrow:: Cow ;
22
3- use fancy_regex:: Regex ;
4-
53pub fn single_to_double_quoted < ' src > (
64 content : & ' src str ,
75 start_delim : & ' src str ,
@@ -15,38 +13,70 @@ pub fn single_to_double_quoted<'src>(
1513 )
1614 . into ( )
1715 } else {
18- // For percent literals, we only care about the delimiter
19- // e.g. for `%<` we're looking for the `<`
20- let start_delim = if let Some ( stripped) = start_delim. strip_prefix ( '%' ) {
21- stripped
22- } else {
23- start_delim
24- } ;
16+ let start_delim_char = start_delim. chars ( ) . last ( ) . unwrap ( ) ;
17+ let end_delim_char = end_delim. chars ( ) . last ( ) . unwrap ( ) ;
18+ convert_percent_literal_escapes ( content, start_delim_char, end_delim_char)
19+ }
20+ }
21+
22+ /// Converts escape sequences in a percent-literal string's content so they are
23+ /// valid inside a double-quoted string.
24+ fn convert_percent_literal_escapes (
25+ content : & str ,
26+ start_delim : char ,
27+ end_delim : char ,
28+ ) -> Cow < ' _ , str > {
29+ let first_change_pos = {
30+ let mut pos = None ;
31+ let mut chars = content. char_indices ( ) . peekable ( ) ;
32+ while let Some ( ( i, c) ) = chars. next ( ) {
33+ if c == '"' {
34+ pos = Some ( i) ;
35+ break ;
36+ } else if c == '\\'
37+ && let Some ( & ( _, next) ) = chars. peek ( )
38+ {
39+ if next == start_delim || next == end_delim {
40+ pos = Some ( i) ;
41+ break ;
42+ }
43+ chars. next ( ) ; // skip next char — treat \X as a unit
44+ }
45+ }
46+ pos
47+ } ;
48+
49+ let Some ( start) = first_change_pos else {
50+ return Cow :: Borrowed ( content) ; // No changes
51+ } ;
2552
26- let regexp = Regex :: new ( & format ! (
27- r#"(?<!\\)(\\\\)*(\"|\\{}|\\{})"# ,
28- fancy_regex:: escape( start_delim) ,
29- fancy_regex:: escape( end_delim)
30- ) )
31- . unwrap ( ) ;
53+ let mut output = content[ ..start] . to_string ( ) ;
54+ let mut chars = content[ start..] . chars ( ) . peekable ( ) ;
3255
33- regexp. replace_all ( content, |captures : & fancy_regex:: Captures | {
34- // first capture is the entire match
35- let val = captures. get ( 0 ) . unwrap ( ) ;
36- let val_str = val. as_str ( ) ;
37- if val_str. ends_with ( "\" " ) {
38- // Ends with a quote, which we transform to `\"`
39- format ! ( "{}\\ \" " , & val_str[ 0 ..( val_str. len( ) - 1 ) ] )
56+ while let Some ( c) = chars. next ( ) {
57+ if c == '\\' {
58+ if let Some ( & next) = chars. peek ( ) {
59+ if next == start_delim || next == end_delim {
60+ // Drop the delimiter escape: \( → (
61+ output. push ( next) ;
62+ chars. next ( ) ;
63+ } else {
64+ // Write back the original with no changes
65+ output. push ( '\\' ) ;
66+ output. push ( next) ;
67+ chars. next ( ) ;
68+ }
4069 } else {
41- // drop unnecessary escape
42- format ! (
43- "{}{}" ,
44- & val_str[ 0 ..( val_str. len( ) - 2 ) ] ,
45- val_str. chars( ) . last( ) . unwrap( )
46- )
70+ output. push ( '\\' ) ;
4771 }
48- } )
72+ } else if c == '"' {
73+ output. push_str ( "\\ \" " ) ;
74+ } else {
75+ output. push ( c) ;
76+ }
4977 }
78+
79+ Cow :: Owned ( output)
5080}
5181
5282/// Escapes content for word arrays when converting to bracket delimiters.
0 commit comments