44// option. This file may not be copied, modified, or distributed
55// except according to those terms.
66
7- use anyhow:: Result ;
8- use bstr:: ByteSlice ;
9- use libyaml:: { Emitter , Encoding , Event , ScalarStyle } ;
7+ use std:: fmt:: Write ;
108
119/// Serialize a YAML scalar while preserving the caller's quote style.
12- pub ( crate ) fn serialize_yaml_scalar ( value : & str , quote : & str ) -> Result < String > {
13- let style = match quote {
14- "'" => Some ( ScalarStyle :: SingleQuoted ) ,
15- "\" " => Some ( ScalarStyle :: DoubleQuoted ) ,
16- _ => None ,
17- } ;
10+ pub ( crate ) fn serialize_yaml_scalar ( value : & str , quote : & str ) -> anyhow:: Result < String > {
11+ match quote {
12+ "'" => Ok ( format ! ( "'{}'" , escape_single_quoted( value) ) ) ,
13+ "\" " => Ok ( format ! ( "\" {}\" " , escape_double_quoted( value) ) ) ,
14+ _ => {
15+ if is_simple_plain ( value) {
16+ Ok ( value. to_owned ( ) )
17+ } else {
18+ // Defer to serde-saphyr to select quoting/escaping for non-trivial scalars.
19+ let rendered = serde_saphyr:: to_string ( & value) ?;
20+ Ok ( rendered. trim_end_matches ( '\n' ) . to_owned ( ) )
21+ }
22+ }
23+ }
24+ }
1825
19- let mut writer = Vec :: new ( ) ;
20- {
21- let mut emitter = Emitter :: new ( & mut writer) ?;
22- emitter. emit ( Event :: StreamStart {
23- encoding : Some ( Encoding :: Utf8 ) ,
24- } ) ?;
25- emitter. emit ( Event :: DocumentStart {
26- version : None ,
27- tags : vec ! [ ] ,
28- implicit : true ,
29- } ) ?;
30- emitter. emit ( Event :: Scalar {
31- anchor : None ,
32- tag : None ,
33- value : value. to_owned ( ) ,
34- plain_implicit : true ,
35- quoted_implicit : true ,
36- style,
37- } ) ?;
38- emitter. emit ( Event :: DocumentEnd { implicit : true } ) ?;
39- emitter. emit ( Event :: StreamEnd { } ) ?;
40- emitter. flush ( ) ?;
26+ /// Fast-path: allow simple, plain scalars we want to keep unquoted.
27+ fn is_simple_plain ( value : & str ) -> bool {
28+ if value. is_empty ( ) {
29+ return false ;
4130 }
42- let trimmed = writer. trim_end ( ) ;
43- Ok ( str:: from_utf8 ( trimmed) ?. to_owned ( ) )
31+ value
32+ . chars ( )
33+ . all ( |ch| ch. is_ascii_alphanumeric ( ) || matches ! ( ch, '.' | '-' | '_' | '/' | '+' | '@' ) )
34+ }
35+
36+ /// YAML single-quoted strings escape a single quote by doubling it.
37+ fn escape_single_quoted ( value : & str ) -> String {
38+ value. replace ( '\'' , "''" )
39+ }
40+
41+ /// YAML double-quoted strings use backslash escapes for control characters.
42+ fn escape_double_quoted ( value : & str ) -> String {
43+ let mut escaped = String :: with_capacity ( value. len ( ) ) ;
44+ for ch in value. chars ( ) {
45+ match ch {
46+ '\\' => escaped. push_str ( "\\ \\ " ) ,
47+ '"' => escaped. push_str ( "\\ \" " ) ,
48+ '\t' => escaped. push_str ( "\\ t" ) ,
49+ '\n' => escaped. push_str ( "\\ n" ) ,
50+ '\r' => escaped. push_str ( "\\ r" ) ,
51+ c if c. is_control ( ) => {
52+ let _ = write ! ( escaped, "\\ u{:04X}" , c as u32 ) ;
53+ }
54+ c => escaped. push ( c) ,
55+ }
56+ }
57+ escaped
4458}
4559
4660#[ cfg( test) ]
@@ -61,11 +75,32 @@ mod tests {
6175 assert_eq ! ( rendered, "'123'" ) ;
6276 let rendered = serialize_yaml_scalar ( "123" , "\" " ) . unwrap ( ) ;
6377 assert_eq ! ( rendered, "\" 123\" " ) ;
78+ let rendered = serialize_yaml_scalar ( "a:b" , "" ) . unwrap ( ) ;
79+ assert_eq ! ( rendered, "a:b" ) ;
6480 let rendered = serialize_yaml_scalar ( "a:b" , "'" ) . unwrap ( ) ;
6581 assert_eq ! ( rendered, "'a:b'" ) ;
6682 let rendered = serialize_yaml_scalar ( "a\" b" , "\" " ) . unwrap ( ) ;
6783 assert_eq ! ( rendered, "\" a\\ \" b\" " ) ;
6884 let rendered = serialize_yaml_scalar ( "a'b" , "'" ) . unwrap ( ) ;
6985 assert_eq ! ( rendered, "'a''b'" ) ;
86+
87+ let rendered = serialize_yaml_scalar ( "abc def" , "" ) . unwrap ( ) ;
88+ assert_eq ! ( rendered, "abc def" ) ;
89+ let rendered = serialize_yaml_scalar ( "abc def" , "'" ) . unwrap ( ) ;
90+ assert_eq ! ( rendered, "'abc def'" ) ;
91+ let rendered = serialize_yaml_scalar ( "abc def" , "\" " ) . unwrap ( ) ;
92+ assert_eq ! ( rendered, "\" abc def\" " ) ;
93+ }
94+
95+ #[ test]
96+ fn serialize_yaml_scalar_quotes_and_escapes ( ) {
97+ let rendered = serialize_yaml_scalar ( "a\\ b" , "\" " ) . unwrap ( ) ;
98+ assert_eq ! ( rendered, "\" a\\ \\ b\" " ) ;
99+ let rendered = serialize_yaml_scalar ( "a\n b" , "\" " ) . unwrap ( ) ;
100+ assert_eq ! ( rendered, "\" a\\ nb\" " ) ;
101+ let rendered = serialize_yaml_scalar ( "a\t b" , "\" " ) . unwrap ( ) ;
102+ assert_eq ! ( rendered, "\" a\\ tb\" " ) ;
103+ let rendered = serialize_yaml_scalar ( "a\\ b" , "'" ) . unwrap ( ) ;
104+ assert_eq ! ( rendered, "'a\\ b'" ) ;
70105 }
71106}
0 commit comments