Skip to main content

yaml_edit/
scalar.rs

1//! Scalar value wrapper with proper escaping and style support.
2
3use std::fmt;
4
5#[cfg(feature = "base64")]
6use base64::{engine::general_purpose, Engine as _};
7
8/// Base64 encode bytes for binary data
9#[cfg(feature = "base64")]
10fn base64_encode(input: &[u8]) -> String {
11    general_purpose::STANDARD.encode(input)
12}
13
14/// Base64 decode string back to bytes
15#[cfg(feature = "base64")]
16fn base64_decode(input: &str) -> Result<Vec<u8>, String> {
17    general_purpose::STANDARD
18        .decode(input.trim())
19        .map_err(|e| format!("Base64 decode error: {e}"))
20}
21
22/// Style of scalar representation in YAML
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub enum ScalarStyle {
25    /// Plain scalar (no quotes)
26    Plain,
27    /// Single-quoted scalar
28    SingleQuoted,
29    /// Double-quoted scalar
30    DoubleQuoted,
31    /// Literal scalar (|)
32    Literal,
33    /// Folded scalar (>)
34    Folded,
35}
36
37/// The five scalar types recognised by the YAML 1.2 core schema.
38///
39/// Returned by [`ScalarValue::classify_plain`], which is the single
40/// source of truth for plain-scalar tag resolution. This is a strict
41/// subset of [`ScalarType`], which additionally covers yaml-edit's
42/// out-of-band types (`Timestamp`, `Binary`, `Regex`) used when
43/// constructing new values.
44#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
45pub enum CoreScalarType {
46    /// `!!null`
47    Null,
48    /// `!!bool`
49    Boolean,
50    /// `!!int`
51    Integer,
52    /// `!!float`
53    Float,
54    /// `!!str` (the fallback when no other tag matches)
55    String,
56}
57
58impl From<CoreScalarType> for ScalarType {
59    fn from(t: CoreScalarType) -> Self {
60        match t {
61            CoreScalarType::Null => ScalarType::Null,
62            CoreScalarType::Boolean => ScalarType::Boolean,
63            CoreScalarType::Integer => ScalarType::Integer,
64            CoreScalarType::Float => ScalarType::Float,
65            CoreScalarType::String => ScalarType::String,
66        }
67    }
68}
69
70/// Type of a scalar value
71#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
72pub enum ScalarType {
73    /// String value
74    String,
75    /// Integer value
76    Integer,
77    /// Float value
78    Float,
79    /// Boolean value
80    Boolean,
81    /// Null value
82    Null,
83    /// Binary data (base64 encoded)
84    #[cfg(feature = "base64")]
85    Binary,
86    /// Timestamp value
87    Timestamp,
88    /// Regular expression
89    Regex,
90}
91
92/// A scalar value with metadata about its style and content
93#[derive(Debug, Clone, PartialEq, Eq)]
94pub struct ScalarValue {
95    /// The actual value
96    value: String,
97    /// The style to use when rendering
98    style: ScalarStyle,
99    /// The type of the scalar
100    scalar_type: ScalarType,
101}
102
103/// Parse a `\uNNNN` or `\UNNNNNNNN` unicode escape from `chars` (with
104/// the leading `\u`/`\U` already consumed) and append the resulting
105/// character to `out`, falling back to the literal `\<prefix><digits>`
106/// when the digits are missing, non-hex, or outside the valid unicode
107/// range.
108fn push_unicode_escape(
109    out: &mut String,
110    chars: &mut std::iter::Peekable<std::str::Chars<'_>>,
111    prefix: char,
112    width: usize,
113) {
114    let hex_digits: String = chars.by_ref().take(width).collect();
115    let decoded = if hex_digits.len() == width {
116        u32::from_str_radix(&hex_digits, 16)
117            .ok()
118            .and_then(char::from_u32)
119    } else {
120        None
121    };
122    match decoded {
123        Some(c) => out.push(c),
124        None => {
125            out.push('\\');
126            out.push(prefix);
127            out.push_str(&hex_digits);
128        }
129    }
130}
131
132impl ScalarValue {
133    /// Create a scalar value explicitly treating it as a string (no type auto-detection)
134    ///
135    /// This method always creates a `String` type scalar. The value will be properly
136    /// quoted if needed when rendering to YAML, but no type detection is performed.
137    ///
138    /// # Examples
139    ///
140    /// ```
141    /// use yaml_edit::ScalarValue;
142    ///
143    /// let scalar = ScalarValue::string("123");
144    /// assert_eq!(scalar.scalar_type(), yaml_edit::ScalarType::String);
145    /// // Renders with quotes to distinguish from integer
146    /// assert_eq!(scalar.to_yaml_string(), "'123'");
147    ///
148    /// let scalar = ScalarValue::string("true");
149    /// assert_eq!(scalar.scalar_type(), yaml_edit::ScalarType::String);
150    /// // Renders with quotes to distinguish from boolean
151    /// assert_eq!(scalar.to_yaml_string(), "'true'");
152    ///
153    /// let scalar = ScalarValue::string("hello");
154    /// assert_eq!(scalar.scalar_type(), yaml_edit::ScalarType::String);
155    /// // Plain strings don't need quotes
156    /// assert_eq!(scalar.to_yaml_string(), "hello");
157    /// ```
158    ///
159    /// For YAML-style type detection (parsing "123" as Integer, "true" as Boolean),
160    /// use [`ScalarValue::parse()`] instead.
161    pub fn string(value: impl Into<String>) -> Self {
162        let value = value.into();
163        let style = Self::detect_style(&value);
164        // Detect the type - default to String for user-provided values
165        let scalar_type = ScalarType::String;
166        Self {
167            value,
168            style,
169            scalar_type,
170        }
171    }
172
173    /// Parse escape sequences in a double-quoted string
174    pub fn parse_escape_sequences(text: &str) -> String {
175        let mut result = String::with_capacity(text.len()); // Pre-allocate
176        let mut chars = text.chars().peekable();
177
178        while let Some(ch) = chars.next() {
179            if ch == '\\' {
180                if let Some(&escaped) = chars.peek() {
181                    chars.next(); // consume the escaped character
182                    match escaped {
183                        // Standard escape sequences
184                        'n' => result.push('\n'),
185                        't' => result.push('\t'),
186                        'r' => result.push('\r'),
187                        'b' => result.push('\x08'),
188                        'f' => result.push('\x0C'),
189                        'a' => result.push('\x07'), // bell
190                        'e' => result.push('\x1B'), // escape
191                        'v' => result.push('\x0B'), // vertical tab
192                        '0' => result.push('\0'),   // null
193                        '\\' => result.push('\\'),
194                        '"' => result.push('"'),
195                        '\'' => result.push('\''),
196                        '/' => result.push('/'),
197                        // Line break escape (YAML specific)
198                        ' ' => {
199                            // Escaped space followed by line break - line folding
200                            if let Some(&'\n') = chars.peek() {
201                                chars.next(); // consume the newline
202                                              // In YAML, escaped line breaks are folded to nothing
203                                continue;
204                            }
205                            result.push(' ');
206                        }
207                        '\n' => {
208                            // Escaped line break - removes the line break
209                            continue;
210                        }
211                        // Unicode escapes
212                        'x' => {
213                            // \xNN - 2-digit hex
214                            let mut hex_chars = [0u8; 2];
215                            let mut count = 0;
216                            for (i, ch) in chars.by_ref().take(2).enumerate() {
217                                if let Some(digit) = ch.to_digit(16) {
218                                    hex_chars[i] = digit as u8;
219                                    count += 1;
220                                } else {
221                                    // Put back invalid char
222                                    result.push('\\');
223                                    result.push('x');
224                                    for &hex_char in hex_chars.iter().take(count) {
225                                        result.push(char::from_digit(hex_char as u32, 16).unwrap());
226                                    }
227                                    result.push(ch);
228                                    break;
229                                }
230                            }
231                            if count == 2 {
232                                let code = hex_chars[0] * 16 + hex_chars[1];
233                                result.push(code as char);
234                            } else if count > 0 {
235                                // Incomplete hex escape
236                                result.push('\\');
237                                result.push('x');
238                                for &hex_char in hex_chars.iter().take(count) {
239                                    result.push(char::from_digit(hex_char as u32, 16).unwrap());
240                                }
241                            }
242                        }
243                        'u' => push_unicode_escape(&mut result, &mut chars, 'u', 4),
244                        'U' => push_unicode_escape(&mut result, &mut chars, 'U', 8),
245                        // Unknown escape sequence - preserve as literal
246                        _ => {
247                            result.push('\\');
248                            result.push(escaped);
249                        }
250                    }
251                } else {
252                    // Backslash at end of string
253                    result.push('\\');
254                }
255            } else {
256                result.push(ch);
257            }
258        }
259
260        result
261    }
262
263    /// Create a new scalar with a specific style
264    pub fn with_style(value: impl Into<String>, style: ScalarStyle) -> Self {
265        Self {
266            value: value.into(),
267            style,
268            scalar_type: ScalarType::String,
269        }
270    }
271
272    /// Create a plain scalar
273    pub fn plain(value: impl Into<String>) -> Self {
274        Self::with_style(value, ScalarStyle::Plain)
275    }
276
277    /// Create a single-quoted scalar
278    pub fn single_quoted(value: impl Into<String>) -> Self {
279        Self::with_style(value, ScalarStyle::SingleQuoted)
280    }
281
282    /// Create a double-quoted scalar
283    pub fn double_quoted(value: impl Into<String>) -> Self {
284        Self::with_style(value, ScalarStyle::DoubleQuoted)
285    }
286
287    /// Create a literal scalar
288    pub fn literal(value: impl Into<String>) -> Self {
289        Self::with_style(value, ScalarStyle::Literal)
290    }
291
292    /// Create a folded scalar
293    pub fn folded(value: impl Into<String>) -> Self {
294        Self::with_style(value, ScalarStyle::Folded)
295    }
296
297    /// Create a null scalar
298    pub fn null() -> Self {
299        Self {
300            value: "null".to_string(),
301            style: ScalarStyle::Plain,
302            scalar_type: ScalarType::Null,
303        }
304    }
305
306    /// Create a binary scalar from raw bytes
307    #[cfg(feature = "base64")]
308    pub fn binary(data: &[u8]) -> Self {
309        let encoded = base64_encode(data);
310        Self {
311            value: encoded,
312            style: ScalarStyle::Plain,
313            scalar_type: ScalarType::Binary,
314        }
315    }
316
317    /// Create a timestamp scalar
318    pub fn timestamp(value: impl Into<String>) -> Self {
319        Self {
320            value: value.into(),
321            style: ScalarStyle::Plain,
322            scalar_type: ScalarType::Timestamp,
323        }
324    }
325
326    /// Create a regex scalar
327    pub fn regex(pattern: impl Into<String>) -> Self {
328        Self {
329            value: pattern.into(),
330            style: ScalarStyle::Plain,
331            scalar_type: ScalarType::Regex,
332        }
333    }
334
335    /// Get the raw value
336    pub fn value(&self) -> &str {
337        &self.value
338    }
339
340    /// Get the style
341    pub fn style(&self) -> ScalarStyle {
342        self.style
343    }
344
345    /// Get the scalar type
346    pub fn scalar_type(&self) -> ScalarType {
347        self.scalar_type
348    }
349
350    /// Try to parse this scalar as an `i64`.
351    ///
352    /// Returns `None` if the scalar type is not `Integer`.
353    ///
354    /// A bare leading zero is read as YAML 1.1 octal, so `0755` gives 493.
355    /// Use [`to_i64_yaml_1_2`](Self::to_i64_yaml_1_2) for the 1.2 reading,
356    /// where the same text is 755.
357    pub fn to_i64(&self) -> Option<i64> {
358        if self.scalar_type == ScalarType::Integer {
359            Self::parse_integer(&self.value)
360        } else {
361            None
362        }
363    }
364
365    /// As [`to_i64`](Self::to_i64), under YAML 1.2's `!!int` rules.
366    ///
367    /// YAML 1.2 dropped 1.1's bare-octal form, so `0755` is seven hundred
368    /// and fifty five here where [`to_i64`](Self::to_i64) gives 493. Every
369    /// other spelling, `0o755` included, reads the same both ways.
370    pub fn to_i64_yaml_1_2(&self) -> Option<i64> {
371        if self.scalar_type != ScalarType::Integer {
372            return None;
373        }
374        if Self::is_legacy_octal(&self.value) {
375            let body = self.value.strip_prefix(['+', '-']).unwrap_or(&self.value);
376            let magnitude = body.parse::<i64>().ok()?;
377            return Some(if self.value.starts_with('-') {
378                -magnitude
379            } else {
380                magnitude
381            });
382        }
383        Self::parse_integer(&self.value)
384    }
385
386    /// Try to parse this scalar as an `f64`.
387    ///
388    /// Returns `None` if the scalar type is not `Float`.
389    pub fn to_f64(&self) -> Option<f64> {
390        if self.scalar_type == ScalarType::Float {
391            self.value.trim().parse::<f64>().ok()
392        } else {
393            None
394        }
395    }
396
397    /// Try to parse this scalar as a `bool`.
398    ///
399    /// Returns `None` if the scalar type is not `Boolean`.
400    /// Recognizes: `true`, `false`, `yes`, `no`, `on`, `off` (case-insensitive).
401    pub fn to_bool(&self) -> Option<bool> {
402        if self.scalar_type == ScalarType::Boolean {
403            match self.value.to_lowercase().as_str() {
404                "true" | "yes" | "on" => Some(true),
405                "false" | "no" | "off" => Some(false),
406                _ => None,
407            }
408        } else {
409            None
410        }
411    }
412
413    /// Extract binary data if this is a binary scalar
414    #[cfg(feature = "base64")]
415    pub fn as_binary(&self) -> Option<Result<Vec<u8>, String>> {
416        match self.scalar_type {
417            ScalarType::Binary => Some(base64_decode(&self.value)),
418            _ => None,
419        }
420    }
421
422    /// Check if this is a binary scalar
423    #[cfg(feature = "base64")]
424    pub fn is_binary(&self) -> bool {
425        self.scalar_type == ScalarType::Binary
426    }
427
428    /// Check if this is a timestamp scalar
429    pub fn is_timestamp(&self) -> bool {
430        self.scalar_type == ScalarType::Timestamp
431    }
432
433    /// Check if this is a regex scalar
434    pub fn is_regex(&self) -> bool {
435        self.scalar_type == ScalarType::Regex
436    }
437
438    /// Compile and return a Regex object if this is a regex scalar
439    ///
440    /// This method is only available when the `regex` feature is enabled.
441    /// Returns None if this is not a regex scalar or if the pattern is invalid.
442    ///
443    /// # Example
444    /// ```
445    /// # #[cfg(feature = "regex")]
446    /// # {
447    /// use yaml_edit::ScalarValue;
448    ///
449    /// let scalar = ScalarValue::regex(r"\d{3}-\d{4}");
450    /// let regex = scalar.as_regex().unwrap();
451    /// assert!(regex.is_match("555-1234"));
452    /// # }
453    /// ```
454    #[cfg(feature = "regex")]
455    pub fn as_regex(&self) -> Option<regex::Regex> {
456        if self.scalar_type == ScalarType::Regex {
457            regex::Regex::new(&self.value).ok()
458        } else {
459            None
460        }
461    }
462
463    /// Try to compile this scalar as a regex, regardless of its type
464    ///
465    /// This method is only available when the `regex` feature is enabled.
466    /// This will attempt to compile the scalar value as a regex pattern,
467    /// even if it's not marked with the !!regex tag.
468    ///
469    /// # Example
470    /// ```
471    /// # #[cfg(feature = "regex")]
472    /// # {
473    /// use yaml_edit::ScalarValue;
474    ///
475    /// let scalar = ScalarValue::string(r"\d+");  // Plain string scalar
476    /// let regex = scalar.try_as_regex().unwrap();
477    /// assert!(regex.is_match("123"));
478    /// # }
479    /// ```
480    #[cfg(feature = "regex")]
481    pub fn try_as_regex(&self) -> Result<regex::Regex, regex::Error> {
482        regex::Regex::new(&self.value)
483    }
484
485    /// Try to coerce this scalar to the specified type
486    pub fn coerce_to_type(&self, target_type: ScalarType) -> Option<ScalarValue> {
487        if self.scalar_type == target_type {
488            return Some(self.clone());
489        }
490
491        match target_type {
492            ScalarType::String => Some(ScalarValue {
493                value: self.value.clone(),
494                style: ScalarStyle::Plain,
495                scalar_type: ScalarType::String,
496            }),
497            ScalarType::Integer => Self::parse_integer(&self.value).map(ScalarValue::from),
498            ScalarType::Float => self.value.parse::<f64>().ok().map(ScalarValue::from),
499            ScalarType::Boolean => match self.value.to_lowercase().as_str() {
500                "true" | "yes" | "on" | "1" => Some(ScalarValue::from(true)),
501                "false" | "no" | "off" | "0" => Some(ScalarValue::from(false)),
502                _ => None,
503            },
504            ScalarType::Null => match self.value.to_lowercase().as_str() {
505                "null" | "~" | "" => Some(ScalarValue::null()),
506                _ => None,
507            },
508            #[cfg(feature = "base64")]
509            ScalarType::Binary => {
510                // Try to decode as base64 to verify it's valid binary data
511                if base64_decode(&self.value).is_ok() {
512                    Some(ScalarValue {
513                        value: self.value.clone(),
514                        style: ScalarStyle::Plain,
515                        scalar_type: ScalarType::Binary,
516                    })
517                } else {
518                    None
519                }
520            }
521            ScalarType::Timestamp => {
522                // Basic timestamp format validation
523                if self.is_valid_timestamp(&self.value) {
524                    Some(ScalarValue::timestamp(&self.value))
525                } else {
526                    None
527                }
528            }
529            ScalarType::Regex => {
530                // For regex, just convert the value
531                Some(ScalarValue::regex(&self.value))
532            }
533        }
534    }
535
536    /// Parse an integer with support for various formats
537    /// Supports: decimal, hexadecimal (0x), binary (0b), octal (0o and legacy 0)
538    pub(crate) fn parse_integer(value: &str) -> Option<i64> {
539        let value = value.trim();
540
541        // Handle negative numbers
542        let (is_negative, value) = if let Some(stripped) = value.strip_prefix('-') {
543            (true, stripped)
544        } else if let Some(stripped) = value.strip_prefix('+') {
545            (false, stripped)
546        } else {
547            (false, value)
548        };
549
550        let parsed = if let Some(hex_part) = value
551            .strip_prefix("0x")
552            .or_else(|| value.strip_prefix("0X"))
553        {
554            // Hexadecimal
555            i64::from_str_radix(hex_part, 16).ok()
556        } else if let Some(bin_part) = value
557            .strip_prefix("0b")
558            .or_else(|| value.strip_prefix("0B"))
559        {
560            // Binary
561            i64::from_str_radix(bin_part, 2).ok()
562        } else if let Some(oct_part) = value
563            .strip_prefix("0o")
564            .or_else(|| value.strip_prefix("0O"))
565        {
566            // Modern octal
567            i64::from_str_radix(oct_part, 8).ok()
568        } else if value.starts_with('0')
569            && value.len() > 1
570            && value.chars().all(|c| c.is_ascii_digit())
571        {
572            // Legacy octal (starts with 0 but not 0x, 0b, 0o)
573            i64::from_str_radix(value, 8).ok()
574        } else {
575            // Decimal
576            value.parse::<i64>().ok()
577        };
578
579        parsed.map(|n| if is_negative { -n } else { n })
580    }
581
582    /// Whether `value` is written in YAML 1.1's bare-octal form (a leading
583    /// `0` before more digits, as in `0755`).
584    ///
585    /// YAML 1.2's `!!int` has no such form: `0755` is seven hundred and
586    /// fifty five, where 1.1 read it as 493. `0o755` is the 1.2 spelling.
587    pub(crate) fn is_legacy_octal(value: &str) -> bool {
588        let body = value.strip_prefix(['+', '-']).unwrap_or(value);
589        body.len() > 1 && body.starts_with('0') && body.bytes().all(|b| b.is_ascii_digit())
590    }
591
592    /// Classify a plain (unquoted) scalar's text per the YAML 1.2 core
593    /// schema tag-resolution rules.
594    ///
595    /// This is the single source of truth for what a plain scalar
596    /// resolves to; the lexer and [`from_scalar`](Self::from_scalar)
597    /// both delegate here so token-emit-time and CST-inspection-time
598    /// classification cannot drift.
599    ///
600    /// Note that [`auto_detect_type`](Self::auto_detect_type) is
601    /// deliberately more permissive (recognises YAML 1.1 `yes`/`no`
602    /// booleans, timestamps, base64) and is meant for constructing new
603    /// values from arbitrary strings, not for reading spec-conformant
604    /// YAML back out.
605    pub fn classify_plain(value: &str) -> CoreScalarType {
606        match value {
607            "true" | "false" | "True" | "False" | "TRUE" | "FALSE" => {
608                return CoreScalarType::Boolean
609            }
610            "null" | "Null" | "NULL" | "~" => return CoreScalarType::Null,
611            _ => {}
612        }
613
614        // A bare leading zero is YAML 1.1 octal, which 1.2 dropped: the
615        // value is still an integer, just a decimal one, so the text
616        // classifies the same way either side of the change. A digit
617        // outside 0-7 makes it no octal at all, and `parse_integer` gives
618        // up there by contract, so recognise the run of digits here rather
619        // than letting `08` fall through to the float parser below: no
620        // YAML version reads it as a float.
621        if Self::parse_integer(value).is_some() || Self::is_legacy_octal(value) {
622            return CoreScalarType::Integer;
623        }
624
625        // YAML 1.2 spells infinity/NaN with a leading dot: `.inf`, `.Inf`,
626        // `.INF` (plus optional `+`/`-`), and `.nan`, `.NaN`, `.NAN`.
627        // Rust's `f64::from_str` also accepts bare `inf`, `nan`,
628        // `infinity`, etc. in any casing, but per the spec those are
629        // strings when written without the leading dot.
630        match value {
631            ".inf" | ".Inf" | ".INF" | "+.inf" | "+.Inf" | "+.INF" | "-.inf" | "-.Inf"
632            | "-.INF" | ".nan" | ".NaN" | ".NAN" => return CoreScalarType::Float,
633            _ => {}
634        }
635        let body = value.strip_prefix(['+', '-']).unwrap_or(value);
636        if body.eq_ignore_ascii_case("inf")
637            || body.eq_ignore_ascii_case("infinity")
638            || body.eq_ignore_ascii_case("nan")
639        {
640            return CoreScalarType::String;
641        }
642
643        if value.parse::<f64>().is_ok() {
644            return CoreScalarType::Float;
645        }
646
647        CoreScalarType::String
648    }
649
650    /// Auto-detect the most appropriate scalar type from a string value
651    pub fn auto_detect_type(value: &str) -> ScalarType {
652        // Check for null values first
653        match value.to_lowercase().as_str() {
654            "null" | "~" | "" => return ScalarType::Null,
655            _ => {}
656        }
657
658        // Check for boolean values
659        match value.to_lowercase().as_str() {
660            "true" | "false" | "yes" | "no" | "on" | "off" => return ScalarType::Boolean,
661            _ => {}
662        }
663
664        // Check for numbers with various formats
665        if Self::parse_integer(value).is_some() {
666            return ScalarType::Integer;
667        }
668        if value.parse::<f64>().is_ok() {
669            return ScalarType::Float;
670        }
671
672        // Check for timestamps (basic patterns)
673        if Self::is_valid_timestamp_static(value) {
674            return ScalarType::Timestamp;
675        }
676
677        // Check for binary data (base64)
678        #[cfg(feature = "base64")]
679        if Self::looks_like_base64(value) && base64_decode(value).is_ok() {
680            return ScalarType::Binary;
681        }
682
683        // Default to string
684        ScalarType::String
685    }
686
687    /// Parse a YAML scalar value with automatic type detection
688    ///
689    /// This method automatically detects the YAML type based on the content:
690    /// - "123" → Integer
691    /// - "3.14" → Float
692    /// - "true" / "false" → Boolean
693    /// - "null" / "~" → Null
694    /// - etc.
695    ///
696    /// # Examples
697    ///
698    /// ```
699    /// use yaml_edit::{ScalarValue, ScalarType};
700    ///
701    /// let scalar = ScalarValue::parse("123");
702    /// assert_eq!(scalar.scalar_type(), ScalarType::Integer);
703    /// assert_eq!(scalar.to_yaml_string(), "123");
704    ///
705    /// let scalar = ScalarValue::parse("true");
706    /// assert_eq!(scalar.scalar_type(), ScalarType::Boolean);
707    /// assert_eq!(scalar.to_yaml_string(), "true");
708    ///
709    /// let scalar = ScalarValue::parse("hello");
710    /// assert_eq!(scalar.scalar_type(), ScalarType::String);
711    /// assert_eq!(scalar.to_yaml_string(), "hello");
712    /// ```
713    ///
714    /// To create a String-type scalar without auto-detection (e.g., to represent
715    /// the string "123" rather than the integer 123), use [`ScalarValue::string()`] instead.
716    pub fn parse(value: impl Into<String>) -> Self {
717        let value = value.into();
718        let scalar_type = Self::auto_detect_type(&value);
719        // For non-string types, use Plain style (no quotes)
720        // For string types, detect appropriate style
721        let style = match scalar_type {
722            ScalarType::String => Self::detect_style(&value),
723            // All other types use plain style
724            _ => ScalarStyle::Plain,
725        };
726
727        Self {
728            value,
729            style,
730            scalar_type,
731        }
732    }
733
734    /// Create a ScalarValue from a Scalar syntax node, preserving the type from the lexer
735    ///
736    /// This extracts type information directly from the token kind (INT, BOOL, FLOAT, etc.)
737    /// rather than guessing based on heuristics. This is the correct way to convert
738    /// parsed YAML into ScalarValue.
739    pub fn from_scalar(scalar: &crate::yaml::Scalar) -> Self {
740        let value = scalar.as_string();
741        let raw_text = scalar.value();
742
743        let style = if raw_text.starts_with('"') && raw_text.ends_with('"') {
744            ScalarStyle::DoubleQuoted
745        } else if raw_text.starts_with('\'') && raw_text.ends_with('\'') {
746            ScalarStyle::SingleQuoted
747        } else if raw_text.starts_with('|') {
748            ScalarStyle::Literal
749        } else if raw_text.starts_with('>') {
750            ScalarStyle::Folded
751        } else {
752            ScalarStyle::Plain
753        };
754
755        // Quoted and block scalars are always !!str per YAML 1.2 tag
756        // resolution. Plain scalars go through the core-schema
757        // classifier, which stays correct even when the plain-scalar
758        // text spans multiple lexer tokens (e.g. multi-line plain
759        // scalars, which yield STRING + NEWLINE + INDENT + STRING).
760        // `trim_end` because the lexer may absorb trailing whitespace
761        // into the scalar text; leading whitespace can't appear because
762        // it would have been emitted as INDENT.
763        let scalar_type = if style != ScalarStyle::Plain {
764            ScalarType::String
765        } else {
766            Self::classify_plain(raw_text.trim_end()).into()
767        };
768
769        Self {
770            value,
771            style,
772            scalar_type,
773        }
774    }
775
776    /// Check if a string looks like base64 encoded data
777    #[cfg(feature = "base64")]
778    fn looks_like_base64(value: &str) -> bool {
779        if value.is_empty() {
780            return false;
781        }
782
783        // Must be reasonable length and contain only base64 characters
784        // Also need to check that padding is only at the end
785        if value.len() < 4 || value.len() % 4 != 0 {
786            return false;
787        }
788
789        let padding_count = value.chars().filter(|&c| c == '=').count();
790        if padding_count > 2 {
791            return false;
792        }
793
794        // Check all characters are valid base64
795        if !value
796            .chars()
797            .all(|c| matches!(c, 'A'..='Z' | 'a'..='z' | '0'..='9' | '+' | '/' | '='))
798        {
799            return false;
800        }
801
802        // Check that padding is only at the end
803        if padding_count > 0 {
804            let padding_start = value.len() - padding_count;
805            if !value[padding_start..].chars().all(|c| c == '=') {
806                return false;
807            }
808            // Check that non-padding part doesn't contain '='
809            if value[..padding_start].contains('=') {
810                return false;
811            }
812        }
813
814        // Final validation: try to decode it to ensure it's actually valid base64
815        // This will catch cases like "SGVs" which looks valid but isn't proper base64
816        base64_decode(value).is_ok()
817    }
818
819    /// Basic timestamp format validation
820    fn is_valid_timestamp(&self, value: &str) -> bool {
821        Self::is_valid_timestamp_static(value)
822    }
823
824    /// Static version of timestamp validation
825    fn is_valid_timestamp_static(value: &str) -> bool {
826        // Basic patterns for common timestamp formats
827        // ISO 8601: YYYY-MM-DD or YYYY-MM-DDTHH:MM:SS etc.
828        if Self::matches_iso8601_pattern(value) {
829            return true;
830        }
831
832        // Unix timestamp (seconds since epoch)
833        if let Ok(timestamp) = value.parse::<u64>() {
834            // Reasonable range: between 1970 and 2100
835            return timestamp > 0 && timestamp < 4_102_444_800; // 2100-01-01
836        }
837
838        false
839    }
840
841    /// Simple pattern matching for ISO 8601 timestamps
842    fn matches_iso8601_pattern(value: &str) -> bool {
843        let chars: Vec<char> = value.chars().collect();
844
845        // Must be at least YYYY-MM-DD (10 chars)
846        if chars.len() < 10 {
847            return false;
848        }
849
850        // Check YYYY-MM-DD pattern
851        if !(chars[0..4].iter().all(|c| c.is_ascii_digit())
852            && chars[4] == '-'
853            && chars[5..7].iter().all(|c| c.is_ascii_digit())
854            && chars[7] == '-'
855            && chars[8..10].iter().all(|c| c.is_ascii_digit()))
856        {
857            return false;
858        }
859
860        // Validate month and day ranges (basic validation)
861        let month_str: String = chars[5..7].iter().collect();
862        let day_str: String = chars[8..10].iter().collect();
863
864        if let (Ok(month), Ok(day)) = (month_str.parse::<u8>(), day_str.parse::<u8>()) {
865            if !(1..=12).contains(&month) || !(1..=31).contains(&day) {
866                return false;
867            }
868        } else {
869            return false;
870        }
871
872        // If it's just YYYY-MM-DD, that's valid
873        if chars.len() == 10 {
874            return true;
875        }
876
877        // Check for time part: T or t or space followed by HH:MM:SS
878        if chars.len() >= 19 {
879            let sep = chars[10];
880            if (sep == 'T' || sep == 't' || sep == ' ')
881                && chars[11..13].iter().all(|c| c.is_ascii_digit())
882                && chars[13] == ':'
883                && chars[14..16].iter().all(|c| c.is_ascii_digit())
884                && chars[16] == ':'
885                && chars[17..19].iter().all(|c| c.is_ascii_digit())
886            {
887                // Validate hour, minute, second ranges
888                let hour_str: String = chars[11..13].iter().collect();
889                let minute_str: String = chars[14..16].iter().collect();
890                let second_str: String = chars[17..19].iter().collect();
891
892                if let (Ok(hour), Ok(minute), Ok(second)) = (
893                    hour_str.parse::<u8>(),
894                    minute_str.parse::<u8>(),
895                    second_str.parse::<u8>(),
896                ) {
897                    if hour > 23 || minute > 59 || second > 59 {
898                        return false;
899                    }
900                } else {
901                    return false;
902                }
903
904                return true;
905            }
906        }
907
908        false
909    }
910
911    /// Detect the appropriate style for a value
912    fn detect_style(value: &str) -> ScalarStyle {
913        // A line break has to be decided first. A quoted scalar folds its
914        // breaks into spaces, so `"x\ny\n"` written as `'x\ny\n'` reads back
915        // as `x y`, while a literal block keeps them. A literal also answers
916        // every reason needs_quoting gives, so nothing multi-line needs
917        // quotes.
918        if value.contains('\n') {
919            return ScalarStyle::Literal;
920        }
921        // Check if value needs quoting
922        if Self::needs_quoting(value) {
923            // Prefer single quotes if no single quotes in value
924            if !value.contains('\'') {
925                ScalarStyle::SingleQuoted
926            } else {
927                ScalarStyle::DoubleQuoted
928            }
929        } else {
930            ScalarStyle::Plain
931        }
932    }
933
934    /// Check if a value needs quoting when treated as a string
935    fn needs_quoting(value: &str) -> bool {
936        // Empty string needs quotes
937        if value.is_empty() {
938            return true;
939        }
940
941        // Check for YAML keywords that would be misinterpreted
942        // These need quotes when we want them as strings
943        if value.eq_ignore_ascii_case("true")
944            || value.eq_ignore_ascii_case("false")
945            || value.eq_ignore_ascii_case("yes")
946            || value.eq_ignore_ascii_case("no")
947            || value.eq_ignore_ascii_case("on")
948            || value.eq_ignore_ascii_case("off")
949            || value.eq_ignore_ascii_case("null")
950            || value == "~"
951        {
952            return true;
953        }
954
955        // Also quote things that look like numbers to preserve them as strings
956        if value.parse::<f64>().is_ok() || Self::parse_integer(value).is_some() {
957            return true;
958        }
959
960        // Check if starts with special characters. `#` is included because a
961        // plain scalar starting with `#` reads as a comment: at column 0 the
962        // whole line becomes one, and after whitespace (e.g. `key: #x`) the
963        // `#x` becomes a trailing comment and the value is lost.
964        if value.starts_with(|ch: char| {
965            matches!(
966                ch,
967                '-' | '?'
968                    | ':'
969                    | '['
970                    | ']'
971                    | '{'
972                    | '}'
973                    | ','
974                    | '>'
975                    | '<'
976                    | '!'
977                    | '&'
978                    | '*'
979                    | '#'
980                    | '%'
981                    | '@'
982                    | '`'
983            )
984        }) {
985            return true;
986        }
987
988        // Document terminator (`...`) and start marker (`---`) at
989        // column 0 begin a new stream document, so they must be quoted
990        // when used as a scalar / key.
991        if value.starts_with("...") || value.starts_with("---") {
992            return true;
993        }
994
995        // Check for special characters that require quoting. `:` and `#` are
996        // only ambiguous in context, and the context differs between them:
997        // a `:` ends a key when followed by whitespace or end of input, while
998        // a `#` starts a comment when *preceded* by whitespace (YAML 1.2
999        // 6.6). So `a#b` is a fine plain scalar but `a #b` is not.
1000        let mut prev: Option<char> = None;
1001        let mut chars = value.chars().peekable();
1002        while let Some(ch) = chars.next() {
1003            match ch {
1004                '&' | '*' | '!' | '|' | '\'' | '"' | '%' => return true,
1005                ':' if chars.peek().map_or(true, |next| next.is_whitespace()) => {
1006                    return true;
1007                }
1008                '#' if prev.map_or(true, char::is_whitespace) => {
1009                    return true;
1010                }
1011                _ => {}
1012            }
1013            prev = Some(ch);
1014        }
1015
1016        // Leading/trailing whitespace needs quotes
1017        if value != value.trim() {
1018            return true;
1019        }
1020
1021        // A lone carriage return is a YAML line break, so a plain scalar
1022        // containing one would be read back as two lines. (A `\n` is handled
1023        // earlier, by rendering the value as a literal block.)
1024        if value.contains('\r') {
1025            return true;
1026        }
1027
1028        false
1029    }
1030
1031    /// Render the scalar as a YAML string with proper escaping.
1032    ///
1033    /// A block scalar's body is indented two spaces. Use
1034    /// [`to_yaml_string_with_indent`](Self::to_yaml_string_with_indent) where
1035    /// the value sits inside an indented entry, whose body has to clear that
1036    /// entry's own column.
1037    pub fn to_yaml_string(&self) -> String {
1038        self.to_yaml_string_with_indent(2)
1039    }
1040
1041    /// As [`to_yaml_string`](Self::to_yaml_string), indenting a block
1042    /// scalar's body by `block_indent` spaces.
1043    pub fn to_yaml_string_with_indent(&self, block_indent: usize) -> String {
1044        // For special data types, always include the tag regardless of style
1045        let tag_prefix = match self.scalar_type {
1046            #[cfg(feature = "base64")]
1047            ScalarType::Binary => "!!binary ",
1048            ScalarType::Timestamp => "!!timestamp ",
1049            ScalarType::Regex => "!!regex ",
1050            _ => "",
1051        };
1052
1053        let content = match self.style {
1054            ScalarStyle::Plain => {
1055                // Check if we need to quote based on type vs content
1056                match self.scalar_type {
1057                    ScalarType::String => {
1058                        // For strings, quote if the content looks like a special value
1059                        if Self::needs_quoting(&self.value) {
1060                            self.to_single_quoted()
1061                        } else {
1062                            self.value.clone()
1063                        }
1064                    }
1065                    // For non-strings, output as plain (unquoted)
1066                    ScalarType::Integer
1067                    | ScalarType::Float
1068                    | ScalarType::Boolean
1069                    | ScalarType::Null
1070                    | ScalarType::Timestamp
1071                    | ScalarType::Regex => self.value.clone(),
1072                    #[cfg(feature = "base64")]
1073                    ScalarType::Binary => self.value.clone(),
1074                }
1075            }
1076            ScalarStyle::SingleQuoted => self.to_single_quoted(),
1077            ScalarStyle::DoubleQuoted => self.to_double_quoted(),
1078            ScalarStyle::Literal => self.to_literal_with_indent(block_indent),
1079            ScalarStyle::Folded => self.to_folded_with_indent(block_indent),
1080        };
1081
1082        format!("{tag_prefix}{content}")
1083    }
1084
1085    /// Convert to single-quoted string
1086    fn to_single_quoted(&self) -> String {
1087        // Escape single quotes by doubling them
1088        let escaped = self.value.replace('\'', "''");
1089        format!("'{escaped}'")
1090    }
1091
1092    /// Convert to double-quoted string
1093    fn to_double_quoted(&self) -> String {
1094        let mut result = String::from("\"");
1095        for ch in self.value.chars() {
1096            match ch {
1097                '"' => result.push_str("\\\""),
1098                '\\' => result.push_str("\\\\"),
1099                '\n' => result.push_str("\\n"),
1100                '\r' => result.push_str("\\r"),
1101                '\t' => result.push_str("\\t"),
1102                '\x08' => result.push_str("\\b"),
1103                '\x0C' => result.push_str("\\f"),
1104                '\x07' => result.push_str("\\a"), // bell
1105                '\x1B' => result.push_str("\\e"), // escape
1106                '\x0B' => result.push_str("\\v"), // vertical tab
1107                '\0' => result.push_str("\\0"),   // null
1108                c if c.is_control() || (c as u32) > 0x7F => {
1109                    // Handle Unicode characters and control characters
1110                    let code_point = c as u32;
1111                    if code_point <= 0xFF {
1112                        result.push_str(&format!("\\x{code_point:02X}"));
1113                    } else if code_point <= 0xFFFF {
1114                        result.push_str(&format!("\\u{code_point:04X}"));
1115                    } else {
1116                        result.push_str(&format!("\\U{code_point:08X}"));
1117                    }
1118                }
1119                c => result.push(c),
1120            }
1121        }
1122        result.push('"');
1123        result
1124    }
1125
1126    /// Convert to literal block scalar with specific indentation
1127    pub fn to_literal_with_indent(&self, indent: usize) -> String {
1128        self.to_block_with_indent('|', indent)
1129    }
1130
1131    /// Convert to folded block scalar with specific indentation
1132    pub fn to_folded_with_indent(&self, indent: usize) -> String {
1133        self.to_block_with_indent('>', indent)
1134    }
1135
1136    /// Render as a block scalar introduced by `marker` (`|` literal, `>` folded).
1137    fn to_block_with_indent(&self, marker: char, indent: usize) -> String {
1138        // Pick the chomping indicator that preserves the value's trailing
1139        // line breaks: a bare `|` clips to exactly one, `|-` strips them all
1140        // and `|+` keeps them. Otherwise reading the value back gains or
1141        // loses a newline.
1142        let trailing = self.value.len() - self.value.trim_end_matches('\n').len();
1143        let chomp = match trailing {
1144            0 => "-",
1145            1 => "",
1146            _ => "+",
1147        };
1148        // Content that already carries consistent indentation is preserved.
1149        // That indentation would be read as the block's own and stripped on
1150        // the way back in, though, losing the value's leading spaces, so say
1151        // where the content really starts: `|2` for `"  x\n  y"`.
1152        if let Some(content_indent) = self.detect_content_indentation() {
1153            if content_indent > 0 {
1154                // The indicator counts from the block's own column, so the
1155                // body has to sit that much further right for the value's
1156                // leading spaces to survive.
1157                let body = " ".repeat(indent);
1158                let shifted = self
1159                    .value
1160                    .lines()
1161                    .map(|line| {
1162                        if line.is_empty() {
1163                            String::new()
1164                        } else {
1165                            format!("{body}{line}")
1166                        }
1167                    })
1168                    .collect::<Vec<_>>()
1169                    .join("\n");
1170                return format!("{marker}{indent}{chomp}\n{shifted}");
1171            }
1172            return format!("{marker}{chomp}\n{}", self.value);
1173        }
1174        let indent_str = " ".repeat(indent);
1175        let indented = self
1176            .value
1177            .lines()
1178            .map(|line| {
1179                if line.trim().is_empty() {
1180                    String::new()
1181                } else {
1182                    format!("{indent_str}{line}")
1183                }
1184            })
1185            .collect::<Vec<_>>()
1186            .join("\n");
1187        format!("{marker}{chomp}\n{indented}")
1188    }
1189
1190    /// Detect the minimum indentation level of non-empty lines in the content
1191    fn detect_content_indentation(&self) -> Option<usize> {
1192        let non_empty_lines: Vec<&str> = self
1193            .value
1194            .lines()
1195            .filter(|line| !line.trim().is_empty())
1196            .collect();
1197
1198        if non_empty_lines.is_empty() {
1199            return None;
1200        }
1201
1202        let mut min_indent = None;
1203        let mut all_have_same_indent = true;
1204
1205        for line in non_empty_lines {
1206            let indent = line.len() - line.trim_start().len();
1207            match min_indent {
1208                None => min_indent = Some(indent),
1209                Some(current_min) => {
1210                    if indent != current_min {
1211                        all_have_same_indent = false;
1212                    }
1213                    min_indent = Some(current_min.min(indent));
1214                }
1215            }
1216        }
1217
1218        // Only preserve indentation if all lines have some consistent structure
1219        if all_have_same_indent && min_indent.unwrap_or(0) > 0 {
1220            min_indent
1221        } else {
1222            None
1223        }
1224    }
1225}
1226
1227impl fmt::Display for ScalarValue {
1228    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1229        write!(f, "{}", self.to_yaml_string())
1230    }
1231}
1232
1233impl From<String> for ScalarValue {
1234    fn from(value: String) -> Self {
1235        Self::string(value)
1236    }
1237}
1238
1239impl From<&str> for ScalarValue {
1240    fn from(value: &str) -> Self {
1241        Self::string(value)
1242    }
1243}
1244
1245/// `From<$t>` for the numeric primitives, all of which render through
1246/// `to_string()` and differ only in the YAML type they carry.
1247macro_rules! impl_from_number {
1248    ($($t:ty => $ty:expr),* $(,)?) => {
1249        $(
1250            impl From<$t> for ScalarValue {
1251                fn from(value: $t) -> Self {
1252                    Self {
1253                        value: value.to_string(),
1254                        style: ScalarStyle::Plain,
1255                        scalar_type: $ty,
1256                    }
1257                }
1258            }
1259        )*
1260    };
1261}
1262
1263impl_from_number! {
1264    i32 => ScalarType::Integer,
1265    i64 => ScalarType::Integer,
1266    f32 => ScalarType::Float,
1267    f64 => ScalarType::Float,
1268}
1269
1270impl From<bool> for ScalarValue {
1271    fn from(value: bool) -> Self {
1272        Self {
1273            value: if value { "true" } else { "false" }.to_string(),
1274            style: ScalarStyle::Plain,
1275            scalar_type: ScalarType::Boolean,
1276        }
1277    }
1278}
1279
1280impl From<crate::yaml::Scalar> for ScalarValue {
1281    fn from(scalar: crate::yaml::Scalar) -> Self {
1282        let value = scalar.as_string();
1283        ScalarValue::parse(&value)
1284    }
1285}
1286
1287impl crate::AsYaml for ScalarValue {
1288    fn as_node(&self) -> Option<&crate::yaml::SyntaxNode> {
1289        None
1290    }
1291
1292    fn kind(&self) -> crate::as_yaml::YamlKind {
1293        crate::as_yaml::YamlKind::Scalar
1294    }
1295
1296    fn build_content(
1297        &self,
1298        builder: &mut rowan::GreenNodeBuilder,
1299        indent: usize,
1300        _flow_context: bool,
1301    ) -> bool {
1302        use crate::lex::SyntaxKind;
1303        // Render with proper YAML quoting/escaping. When the value needs
1304        // quotes (e.g. the string "null", or text containing ": "), the
1305        // rendered text starts with a quote and must be tokenized as a
1306        // STRING regardless of the semantic type.
1307        // A block scalar's body has to clear the column its entry sits at,
1308        // or the text it renders to is not the value it stood for:
1309        // `a:\n  b: |-\n  x\n` has an empty scalar and a stray `x`.
1310        let text = self.to_yaml_string_with_indent(indent + 2);
1311        let quoted = text.starts_with('\'') || text.starts_with('"');
1312        let token_kind = if quoted {
1313            SyntaxKind::STRING
1314        } else {
1315            match self.scalar_type() {
1316                ScalarType::Integer => SyntaxKind::INT,
1317                ScalarType::Float => SyntaxKind::FLOAT,
1318                ScalarType::Boolean => SyntaxKind::BOOL,
1319                ScalarType::Null => SyntaxKind::NULL,
1320                _ => SyntaxKind::STRING,
1321            }
1322        };
1323        builder.start_node(SyntaxKind::SCALAR.into());
1324        builder.token(token_kind.into(), &text);
1325        builder.finish_node();
1326        false
1327    }
1328
1329    fn is_inline(&self) -> bool {
1330        true
1331    }
1332}
1333
1334#[cfg(test)]
1335mod tests {
1336    use super::*;
1337
1338    #[test]
1339    fn test_plain_scalars() {
1340        let scalar = ScalarValue::string("simple");
1341        assert_eq!(scalar.to_yaml_string(), "simple");
1342
1343        let scalar = ScalarValue::string("hello world");
1344        assert_eq!(scalar.to_yaml_string(), "hello world");
1345    }
1346
1347    #[test]
1348    fn test_values_needing_quotes() {
1349        // Boolean-like values
1350        let scalar = ScalarValue::string("true");
1351        assert_eq!(scalar.to_yaml_string(), "'true'");
1352
1353        let scalar = ScalarValue::string("false");
1354        assert_eq!(scalar.to_yaml_string(), "'false'");
1355
1356        let scalar = ScalarValue::string("yes");
1357        assert_eq!(scalar.to_yaml_string(), "'yes'");
1358
1359        let scalar = ScalarValue::string("no");
1360        assert_eq!(scalar.to_yaml_string(), "'no'");
1361
1362        // Null-like values
1363        let scalar = ScalarValue::string("null");
1364        assert_eq!(scalar.to_yaml_string(), "'null'");
1365
1366        let scalar = ScalarValue::string("~");
1367        assert_eq!(scalar.to_yaml_string(), "'~'");
1368
1369        // Numbers
1370        let scalar = ScalarValue::string("123");
1371        assert_eq!(scalar.to_yaml_string(), "'123'");
1372
1373        let scalar = ScalarValue::string("3.14");
1374        assert_eq!(scalar.to_yaml_string(), "'3.14'");
1375
1376        // Special characters
1377        let scalar = ScalarValue::string("value: something");
1378        assert_eq!(scalar.to_yaml_string(), "'value: something'");
1379
1380        let scalar = ScalarValue::string("# comment");
1381        assert_eq!(scalar.to_yaml_string(), "'# comment'");
1382
1383        // Leading/trailing whitespace
1384        let scalar = ScalarValue::string("  spaces  ");
1385        assert_eq!(scalar.to_yaml_string(), "'  spaces  '");
1386    }
1387
1388    #[test]
1389    fn test_single_quoted() {
1390        let scalar = ScalarValue::single_quoted("value with 'quotes'");
1391        assert_eq!(scalar.to_yaml_string(), "'value with ''quotes'''");
1392    }
1393
1394    #[test]
1395    fn test_double_quoted() {
1396        let scalar = ScalarValue::double_quoted("value with \"quotes\" and \\backslash");
1397        assert_eq!(
1398            scalar.to_yaml_string(),
1399            "\"value with \\\"quotes\\\" and \\\\backslash\""
1400        );
1401
1402        let scalar = ScalarValue::double_quoted("line1\nline2\ttab");
1403        assert_eq!(scalar.to_yaml_string(), "\"line1\\nline2\\ttab\"");
1404    }
1405
1406    #[test]
1407    fn round_trips_keys_needing_quotes() {
1408        use crate::Document;
1409        use std::str::FromStr;
1410
1411        // Each of these was written unquoted and could not be read back:
1412        // " #" starts a comment (the old rule checked the character *after*
1413        // the `#`, but YAML keys on what precedes it), and a line break
1414        // turned the key into a block scalar.
1415        for key in [
1416            "a. #a", "a #c", "a#b", "a\nb", "a\rb", "x: y", "a:b", "plain", "a b", "#lead", "a:",
1417            "", "true", "123", "- x", "? x",
1418        ] {
1419            let doc = Document::from_str("z: 1\n").unwrap();
1420            doc.as_mapping().unwrap().set(key, "v");
1421            let text = doc.to_string();
1422
1423            let reparsed = Document::from_str(&text)
1424                .unwrap_or_else(|e| panic!("{key:?} produced unparseable {text:?}: {e}"));
1425            let value = reparsed
1426                .as_mapping()
1427                .and_then(|m| m.get(key))
1428                .unwrap_or_else(|| panic!("{key:?} did not survive as {text:?}"));
1429            assert_eq!(value.to_string(), "v", "{key:?} in {text:?}");
1430        }
1431    }
1432
1433    #[test]
1434    fn test_multiline() {
1435        let scalar = ScalarValue::string("line1\nline2\nline3");
1436        // Should auto-detect literal style for multiline
1437        assert_eq!(scalar.style(), ScalarStyle::Literal);
1438    }
1439
1440    #[test]
1441    fn test_from_types() {
1442        let scalar = ScalarValue::from(42);
1443        assert_eq!(scalar.to_yaml_string(), "42");
1444
1445        let scalar = ScalarValue::from(1.234);
1446        assert_eq!(scalar.to_yaml_string(), "1.234");
1447
1448        let scalar = ScalarValue::from(true);
1449        assert_eq!(scalar.to_yaml_string(), "true");
1450
1451        let scalar = ScalarValue::from(false);
1452        assert_eq!(scalar.to_yaml_string(), "false");
1453    }
1454
1455    #[test]
1456    fn test_empty_string() {
1457        let scalar = ScalarValue::string("");
1458        assert_eq!(scalar.to_yaml_string(), "''");
1459    }
1460
1461    #[test]
1462    fn test_special_start_chars() {
1463        let scalar = ScalarValue::string("-item");
1464        assert_eq!(scalar.to_yaml_string(), "'-item'");
1465
1466        let scalar = ScalarValue::string("?key");
1467        assert_eq!(scalar.to_yaml_string(), "'?key'");
1468
1469        let scalar = ScalarValue::string("[array]");
1470        assert_eq!(scalar.to_yaml_string(), "'[array]'");
1471    }
1472
1473    #[test]
1474    fn test_null_scalar() {
1475        let scalar = ScalarValue::null();
1476        assert_eq!(scalar.to_yaml_string(), "null");
1477        assert_eq!(scalar.scalar_type, ScalarType::Null);
1478    }
1479
1480    #[test]
1481    fn test_escape_sequences_basic() {
1482        // Test basic escape sequences
1483        assert_eq!(
1484            ScalarValue::parse_escape_sequences("hello\\nworld"),
1485            "hello\nworld"
1486        );
1487        assert_eq!(
1488            ScalarValue::parse_escape_sequences("tab\\there"),
1489            "tab\there"
1490        );
1491        assert_eq!(
1492            ScalarValue::parse_escape_sequences("quote\\\"test"),
1493            "quote\"test"
1494        );
1495        assert_eq!(
1496            ScalarValue::parse_escape_sequences("back\\\\slash"),
1497            "back\\slash"
1498        );
1499        assert_eq!(
1500            ScalarValue::parse_escape_sequences("return\\rtest"),
1501            "return\rtest"
1502        );
1503    }
1504
1505    #[test]
1506    fn test_escape_sequences_control_chars() {
1507        // Test control character escapes
1508        assert_eq!(ScalarValue::parse_escape_sequences("bell\\a"), "bell\x07");
1509        assert_eq!(
1510            ScalarValue::parse_escape_sequences("backspace\\b"),
1511            "backspace\x08"
1512        );
1513        assert_eq!(
1514            ScalarValue::parse_escape_sequences("formfeed\\f"),
1515            "formfeed\x0C"
1516        );
1517        assert_eq!(
1518            ScalarValue::parse_escape_sequences("escape\\e"),
1519            "escape\x1B"
1520        );
1521        assert_eq!(ScalarValue::parse_escape_sequences("vtab\\v"), "vtab\x0B");
1522        assert_eq!(ScalarValue::parse_escape_sequences("null\\0"), "null\0");
1523        assert_eq!(ScalarValue::parse_escape_sequences("slash\\/"), "slash/");
1524    }
1525
1526    #[test]
1527    fn test_escape_sequences_unicode_x() {
1528        // Test \xNN escape sequences
1529        assert_eq!(ScalarValue::parse_escape_sequences("\\x41"), "A"); // 0x41 = 'A'
1530        assert_eq!(ScalarValue::parse_escape_sequences("\\x7A"), "z"); // 0x7A = 'z'
1531        assert_eq!(ScalarValue::parse_escape_sequences("\\x20"), " "); // 0x20 = space
1532        assert_eq!(ScalarValue::parse_escape_sequences("\\xFF"), "\u{FF}"); // 0xFF = ÿ
1533
1534        // Test invalid hex sequences
1535        assert_eq!(ScalarValue::parse_escape_sequences("\\xGH"), "\\xGH"); // Invalid hex
1536        assert_eq!(ScalarValue::parse_escape_sequences("\\x4"), "\\x4"); // Incomplete
1537    }
1538
1539    #[test]
1540    fn test_escape_sequences_unicode_u() {
1541        // Test \uNNNN escape sequences
1542        assert_eq!(ScalarValue::parse_escape_sequences("\\u0041"), "A"); // 0x0041 = 'A'
1543        assert_eq!(ScalarValue::parse_escape_sequences("\\u03B1"), "α"); // Greek alpha
1544        assert_eq!(ScalarValue::parse_escape_sequences("\\u2603"), "☃"); // Snowman
1545        assert_eq!(ScalarValue::parse_escape_sequences("\\u4E2D"), "中"); // Chinese character
1546
1547        // Test invalid sequences
1548        assert_eq!(ScalarValue::parse_escape_sequences("\\uGHIJ"), "\\uGHIJ"); // Invalid hex
1549        assert_eq!(ScalarValue::parse_escape_sequences("\\u041"), "\\u041"); // Incomplete
1550    }
1551
1552    #[test]
1553    fn test_escape_sequences_unicode_capital_u() {
1554        // Test \UNNNNNNNN escape sequences
1555        assert_eq!(ScalarValue::parse_escape_sequences("\\U00000041"), "A"); // 0x00000041 = 'A'
1556        assert_eq!(ScalarValue::parse_escape_sequences("\\U0001F603"), "😃"); // Smiley emoji
1557        assert_eq!(ScalarValue::parse_escape_sequences("\\U0001F4A9"), "💩"); // Pile of poo emoji
1558
1559        // Test invalid sequences
1560        assert_eq!(
1561            ScalarValue::parse_escape_sequences("\\UGHIJKLMN"),
1562            "\\UGHIJKLMN"
1563        ); // Invalid hex
1564        assert_eq!(
1565            ScalarValue::parse_escape_sequences("\\U0000004"),
1566            "\\U0000004"
1567        ); // Incomplete
1568        assert_eq!(
1569            ScalarValue::parse_escape_sequences("\\UFFFFFFFF"),
1570            "\\UFFFFFFFF"
1571        ); // Invalid code point
1572    }
1573
1574    #[test]
1575    fn test_escape_sequences_line_folding() {
1576        // Test line folding with escaped spaces and newlines
1577        assert_eq!(
1578            ScalarValue::parse_escape_sequences("line\\ \nfolding"),
1579            "linefolding"
1580        );
1581        assert_eq!(
1582            ScalarValue::parse_escape_sequences("escaped\\nline\\nbreak"),
1583            "escaped\nline\nbreak"
1584        );
1585        assert_eq!(
1586            ScalarValue::parse_escape_sequences("remove\\\nline\\nbreak"),
1587            "removeline\nbreak"
1588        );
1589    }
1590
1591    #[test]
1592    fn test_escape_sequences_mixed() {
1593        // Test mixed escape sequences
1594        let input = "Hello\\nWorld\\u0021\\x20\\U0001F44D";
1595        let expected = "Hello\nWorld! 👍";
1596        assert_eq!(ScalarValue::parse_escape_sequences(input), expected);
1597
1598        // Test with quotes and backslashes
1599        let input = "Quote\\\"back\\\\slash\\ttab";
1600        let expected = "Quote\"back\\slash\ttab";
1601        assert_eq!(ScalarValue::parse_escape_sequences(input), expected);
1602    }
1603
1604    #[test]
1605    fn test_escape_sequences_unknown() {
1606        // Test unknown escape sequences are preserved
1607        assert_eq!(ScalarValue::parse_escape_sequences("\\q"), "\\q");
1608        assert_eq!(ScalarValue::parse_escape_sequences("\\z"), "\\z");
1609        assert_eq!(ScalarValue::parse_escape_sequences("\\1"), "\\1");
1610    }
1611
1612    #[test]
1613    fn test_indentation_preservation() {
1614        // Test preserving exact indentation in block scalars
1615        let content_with_indent = "  Line 1\n    Line 2 more indented\n  Line 3";
1616        let scalar = ScalarValue::literal(content_with_indent);
1617
1618        // Should detect that content already has indentation and preserve it
1619        let yaml_output = scalar.to_literal_with_indent(2);
1620        assert_eq!(
1621            yaml_output,
1622            "|-\n    Line 1\n      Line 2 more indented\n    Line 3"
1623        );
1624    }
1625
1626    #[test]
1627    fn test_indentation_detection() {
1628        // Test content with consistent indentation
1629        let consistent_content = "  Line 1\n  Line 2\n  Line 3";
1630        let scalar1 = ScalarValue::literal(consistent_content);
1631        assert_eq!(scalar1.detect_content_indentation(), Some(2));
1632
1633        // Test content with no indentation
1634        let no_indent_content = "Line 1\nLine 2\nLine 3";
1635        let scalar2 = ScalarValue::literal(no_indent_content);
1636        assert_eq!(scalar2.detect_content_indentation(), None);
1637
1638        // Test content with inconsistent indentation
1639        let inconsistent_content = "  Line 1\n    Line 2\n Line 3";
1640        let scalar3 = ScalarValue::literal(inconsistent_content);
1641        assert_eq!(scalar3.detect_content_indentation(), None);
1642
1643        // Test empty content
1644        let empty_content = "";
1645        let scalar4 = ScalarValue::literal(empty_content);
1646        assert_eq!(scalar4.detect_content_indentation(), None);
1647
1648        // Test content with only whitespace lines
1649        let whitespace_content = "  Line 1\n\n  Line 3";
1650        let scalar5 = ScalarValue::literal(whitespace_content);
1651        assert_eq!(scalar5.detect_content_indentation(), Some(2));
1652    }
1653
1654    #[test]
1655    fn test_literal_with_custom_indent() {
1656        // Test applying custom indentation to unindented content
1657        let content = "Line 1\nLine 2\nLine 3";
1658        let scalar = ScalarValue::literal(content);
1659
1660        let yaml_4_spaces = scalar.to_literal_with_indent(4);
1661        assert_eq!(yaml_4_spaces, "|-\n    Line 1\n    Line 2\n    Line 3");
1662
1663        let yaml_1_space = scalar.to_literal_with_indent(1);
1664        assert_eq!(yaml_1_space, "|-\n Line 1\n Line 2\n Line 3");
1665    }
1666
1667    #[test]
1668    fn test_folded_with_custom_indent() {
1669        // Test applying custom indentation to folded scalars
1670        let content = "Line 1\nLine 2\nLine 3";
1671        let scalar = ScalarValue::folded(content);
1672
1673        let yaml_3_spaces = scalar.to_folded_with_indent(3);
1674        assert_eq!(yaml_3_spaces, ">-\n   Line 1\n   Line 2\n   Line 3");
1675    }
1676
1677    #[test]
1678    fn test_mixed_empty_lines_preservation() {
1679        // Test handling of empty lines in block scalars
1680        let content_with_empty_lines = "Line 1\n\nLine 3\n\n\nLine 6";
1681        let scalar = ScalarValue::literal(content_with_empty_lines);
1682
1683        let yaml_output = scalar.to_literal_with_indent(2);
1684        assert_eq!(yaml_output, "|-\n  Line 1\n\n  Line 3\n\n\n  Line 6");
1685
1686        // Empty lines should remain empty (no indentation added)
1687        // Input has 3 empty lines; they should appear unchanged in the output
1688        let lines: Vec<&str> = yaml_output.lines().collect();
1689        let empty_line_count = lines.iter().filter(|line| line.is_empty()).count();
1690        assert_eq!(empty_line_count, 3);
1691    }
1692
1693    #[test]
1694    fn test_escape_sequences_edge_cases() {
1695        // Test edge cases
1696        assert_eq!(ScalarValue::parse_escape_sequences(""), "");
1697        assert_eq!(ScalarValue::parse_escape_sequences("\\"), "\\");
1698        assert_eq!(
1699            ScalarValue::parse_escape_sequences("no escapes"),
1700            "no escapes"
1701        );
1702        assert_eq!(ScalarValue::parse_escape_sequences("\\\\\\\\"), "\\\\");
1703    }
1704
1705    #[test]
1706    fn test_double_quoted_with_escapes() {
1707        // Test that double-quoted scalars properly escape and unescape
1708        let original = "Hello\nWorld\t😃";
1709        let scalar = ScalarValue::double_quoted(original);
1710        let yaml_string = scalar.to_yaml_string();
1711
1712        // Should contain escaped sequences
1713        assert_eq!(yaml_string, "\"Hello\\nWorld\\t\\U0001F603\"");
1714
1715        // Parse it back
1716        let parsed = ScalarValue::parse_escape_sequences(&yaml_string[1..yaml_string.len() - 1]);
1717        assert_eq!(parsed, original);
1718    }
1719
1720    #[test]
1721    fn test_unicode_output_formatting() {
1722        // Test that Unicode characters are properly formatted in output
1723        let scalar = ScalarValue::double_quoted("Hello 世界 🌍");
1724        let yaml_string = scalar.to_yaml_string();
1725
1726        // Should escape non-ASCII characters
1727        assert_eq!(yaml_string, "\"Hello \\u4E16\\u754C \\U0001F30D\"");
1728
1729        // But the internal value should remain unchanged
1730        assert_eq!(scalar.value(), "Hello 世界 🌍");
1731    }
1732
1733    #[test]
1734    #[cfg(feature = "base64")]
1735    fn test_binary_data_encoding() {
1736        // Test creating binary scalar from raw bytes
1737        let data = b"Hello, World!";
1738        let scalar = ScalarValue::binary(data);
1739
1740        assert!(scalar.is_binary());
1741        assert_eq!(scalar.scalar_type(), ScalarType::Binary);
1742
1743        // Should produce valid base64
1744        let yaml_output = scalar.to_yaml_string();
1745        assert!(yaml_output.starts_with("!!binary "));
1746
1747        // Should be able to decode back to original data
1748        if let Some(decoded_result) = scalar.as_binary() {
1749            let decoded = decoded_result.expect("Should decode successfully");
1750            assert_eq!(decoded, data);
1751        } else {
1752            panic!("Should be able to extract binary data");
1753        }
1754    }
1755
1756    #[test]
1757    #[cfg(feature = "base64")]
1758    fn test_base64_encoding_decoding() {
1759        // Test various byte sequences
1760        let test_cases = [
1761            b"".as_slice(),
1762            b"A",
1763            b"AB",
1764            b"ABC",
1765            b"ABCD",
1766            b"Hello, World!",
1767            &[0, 1, 2, 3, 255, 254, 253],
1768        ];
1769
1770        for data in test_cases {
1771            let encoded = base64_encode(data);
1772            let decoded = base64_decode(&encoded).expect("Should decode successfully");
1773            assert_eq!(decoded, data, "Failed for data: {:?}", data);
1774        }
1775    }
1776
1777    #[test]
1778    fn test_timestamp_creation_and_validation() {
1779        // Test various timestamp formats
1780        let valid_timestamps = [
1781            "2023-12-25",
1782            "2023-12-25T10:30:45",
1783            "2023-12-25 10:30:45",
1784            "2023-12-25T10:30:45Z",
1785            "2001-12-14 21:59:43.10 -5", // Space-separated with timezone
1786            "2001-12-15T02:59:43.1Z",    // ISO 8601
1787            "2001-12-14t21:59:43.10-05:00", // Lowercase t
1788        ];
1789
1790        for ts in valid_timestamps {
1791            let scalar = ScalarValue::timestamp(ts);
1792            assert!(scalar.is_timestamp());
1793            assert_eq!(scalar.scalar_type(), ScalarType::Timestamp);
1794            assert_eq!(scalar.value(), ts);
1795
1796            let yaml_output = scalar.to_yaml_string();
1797            assert_eq!(yaml_output, format!("!!timestamp {}", ts));
1798
1799            // Test auto-detection recognizes it as timestamp
1800            let auto_scalar = ScalarValue::parse(ts);
1801            assert_eq!(
1802                auto_scalar.scalar_type(),
1803                ScalarType::Timestamp,
1804                "Failed to auto-detect '{}' as timestamp",
1805                ts
1806            );
1807        }
1808
1809        // Test invalid timestamps are not recognized
1810        let invalid_timestamps = [
1811            "not-a-date",
1812            "2023-13-01", // Invalid month
1813            "2023-12-32", // Invalid day
1814            "12:34:56",   // Time only (should be String)
1815            "2023/12/25", // Wrong separator
1816        ];
1817
1818        for ts in invalid_timestamps {
1819            let auto_scalar = ScalarValue::parse(ts);
1820            assert_ne!(
1821                auto_scalar.scalar_type(),
1822                ScalarType::Timestamp,
1823                "'{}' should not be detected as timestamp",
1824                ts
1825            );
1826        }
1827    }
1828
1829    #[test]
1830    fn test_regex_creation() {
1831        let pattern = r"^\d{3}-\d{2}-\d{4}$";
1832        let scalar = ScalarValue::regex(pattern);
1833
1834        assert!(scalar.is_regex());
1835        assert_eq!(scalar.scalar_type(), ScalarType::Regex);
1836        assert_eq!(scalar.value(), pattern);
1837
1838        let yaml_output = scalar.to_yaml_string();
1839        assert_eq!(yaml_output, format!("!!regex {}", pattern));
1840    }
1841
1842    #[test]
1843    fn test_regex_edge_cases() {
1844        // Test empty pattern
1845        let empty_regex = ScalarValue::regex("");
1846        assert!(empty_regex.is_regex());
1847        assert_eq!(empty_regex.value(), "");
1848        assert_eq!(empty_regex.to_yaml_string(), "!!regex ");
1849
1850        // Test pattern with special characters
1851        let special_chars = ScalarValue::regex(r"[.*+?^${}()|[\]\\]");
1852        assert!(special_chars.is_regex());
1853        assert_eq!(special_chars.value(), r"[.*+?^${}()|[\]\\]");
1854
1855        // Test unicode patterns
1856        let unicode_regex = ScalarValue::regex(r"\p{L}+");
1857        assert!(unicode_regex.is_regex());
1858        assert_eq!(unicode_regex.value(), r"\p{L}+");
1859
1860        // Test very long pattern
1861        let long_pattern = "a".repeat(1000);
1862        let long_regex = ScalarValue::regex(&long_pattern);
1863        assert!(long_regex.is_regex());
1864        assert_eq!(long_regex.value(), long_pattern);
1865
1866        // Test pattern with quotes and escapes
1867        let quoted_regex = ScalarValue::regex(r#"'quoted' and "double quoted" with \\ backslash"#);
1868        assert!(quoted_regex.is_regex());
1869        assert_eq!(
1870            quoted_regex.value(),
1871            r#"'quoted' and "double quoted" with \\ backslash"#
1872        );
1873    }
1874
1875    #[test]
1876    fn test_regex_type_coercion() {
1877        let regex_scalar = ScalarValue::regex(r"\d+");
1878
1879        // Test coercing regex to string
1880        let string_scalar = regex_scalar.coerce_to_type(ScalarType::String).unwrap();
1881        assert_eq!(string_scalar.scalar_type(), ScalarType::String);
1882        assert_eq!(string_scalar.value(), r"\d+");
1883        assert!(!string_scalar.is_regex());
1884
1885        // Test coercing string to regex
1886        let str_scalar = ScalarValue::string("test.*");
1887        let regex_from_string = str_scalar.coerce_to_type(ScalarType::Regex).unwrap();
1888        assert_eq!(regex_from_string.scalar_type(), ScalarType::Regex);
1889        assert_eq!(regex_from_string.value(), "test.*");
1890        assert!(regex_from_string.is_regex());
1891
1892        // Test that regex cannot be coerced to number types
1893        assert!(regex_scalar.coerce_to_type(ScalarType::Integer).is_none());
1894        assert!(regex_scalar.coerce_to_type(ScalarType::Float).is_none());
1895        assert!(regex_scalar.coerce_to_type(ScalarType::Boolean).is_none());
1896    }
1897
1898    #[test]
1899    #[cfg(feature = "regex")]
1900    fn test_regex_compilation() {
1901        // Test as_regex() with a regex scalar
1902        let regex_scalar = ScalarValue::regex(r"\d{3}-\d{4}");
1903        let compiled = regex_scalar.as_regex().unwrap();
1904        assert!(compiled.is_match("555-1234"));
1905        assert!(!compiled.is_match("not-a-phone"));
1906
1907        // Test as_regex() with a non-regex scalar returns None
1908        let string_scalar = ScalarValue::string("not a regex");
1909        assert!(string_scalar.as_regex().is_none());
1910
1911        // Test try_as_regex() with any scalar type
1912        let pattern_scalar = ScalarValue::string(r"^\w+@\w+\.\w+$");
1913        let email_regex = pattern_scalar.try_as_regex().unwrap();
1914        assert!(email_regex.is_match("test@example.com"));
1915        assert!(!email_regex.is_match("not-an-email"));
1916
1917        // Test with invalid regex pattern
1918        let invalid_scalar = ScalarValue::regex(r"[invalid(");
1919        assert!(invalid_scalar.as_regex().is_none());
1920
1921        // Test try_as_regex() with invalid pattern returns error
1922        let invalid_pattern = ScalarValue::string(r"[invalid(");
1923        assert!(invalid_pattern.try_as_regex().is_err());
1924    }
1925
1926    #[test]
1927    #[cfg(feature = "regex")]
1928    fn test_regex_extraction_use_cases() {
1929        // Test extracting and using regex for validation
1930        let validation_rules = [
1931            ScalarValue::regex(r"^\d{5}$"),                 // ZIP code
1932            ScalarValue::regex(r"^[A-Z]{2}$"),              // State code
1933            ScalarValue::regex(r"^\(\d{3}\) \d{3}-\d{4}$"), // Phone number
1934        ];
1935
1936        let test_values = ["12345", "CA", "(555) 123-4567"];
1937
1938        for (rule, value) in validation_rules.iter().zip(test_values.iter()) {
1939            let regex = rule.as_regex().unwrap();
1940            assert!(regex.is_match(value), "Pattern should match {}", value);
1941        }
1942
1943        // Test with complex regex patterns
1944        let email_regex = ScalarValue::regex(r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$");
1945        let compiled = email_regex.as_regex().unwrap();
1946        assert!(compiled.is_match("user@example.com"));
1947        assert!(compiled.is_match("test.user+tag@sub.domain.org"));
1948        assert!(!compiled.is_match("invalid.email"));
1949
1950        // Test extracting capture groups
1951        let version_regex = ScalarValue::regex(r"^v(\d+)\.(\d+)\.(\d+)$");
1952        let compiled = version_regex.as_regex().unwrap();
1953        if let Some(captures) = compiled.captures("v1.2.3") {
1954            assert_eq!(captures.get(1).unwrap().as_str(), "1");
1955            assert_eq!(captures.get(2).unwrap().as_str(), "2");
1956            assert_eq!(captures.get(3).unwrap().as_str(), "3");
1957        } else {
1958            panic!("Should have matched version string");
1959        }
1960    }
1961
1962    #[test]
1963    fn test_type_coercion() {
1964        // Test coercing string to integer
1965        let str_scalar = ScalarValue::string("42");
1966        let int_scalar = str_scalar.coerce_to_type(ScalarType::Integer).unwrap();
1967        assert_eq!(int_scalar.scalar_type(), ScalarType::Integer);
1968        assert_eq!(int_scalar.value(), "42");
1969
1970        // Test coercing string to boolean
1971        let bool_scalar = ScalarValue::string("true")
1972            .coerce_to_type(ScalarType::Boolean)
1973            .unwrap();
1974        assert_eq!(bool_scalar.scalar_type(), ScalarType::Boolean);
1975        assert_eq!(bool_scalar.value(), "true");
1976
1977        // Test coercing boolean string variations
1978        let yes_scalar = ScalarValue::string("yes")
1979            .coerce_to_type(ScalarType::Boolean)
1980            .unwrap();
1981        assert_eq!(yes_scalar.value(), "true");
1982
1983        let no_scalar = ScalarValue::string("no")
1984            .coerce_to_type(ScalarType::Boolean)
1985            .unwrap();
1986        assert_eq!(no_scalar.value(), "false");
1987
1988        // Test failed coercion
1989        let str_scalar = ScalarValue::string("not_a_number");
1990        assert!(str_scalar.coerce_to_type(ScalarType::Integer).is_none());
1991    }
1992
1993    #[test]
1994    fn test_auto_type_detection() {
1995        // Test various automatic type detections
1996        assert_eq!(ScalarValue::auto_detect_type("42"), ScalarType::Integer);
1997        assert_eq!(ScalarValue::auto_detect_type("3.14"), ScalarType::Float);
1998        assert_eq!(ScalarValue::auto_detect_type("true"), ScalarType::Boolean);
1999        assert_eq!(ScalarValue::auto_detect_type("false"), ScalarType::Boolean);
2000        assert_eq!(ScalarValue::auto_detect_type("yes"), ScalarType::Boolean);
2001        assert_eq!(ScalarValue::auto_detect_type("null"), ScalarType::Null);
2002        assert_eq!(ScalarValue::auto_detect_type("~"), ScalarType::Null);
2003        assert_eq!(ScalarValue::auto_detect_type(""), ScalarType::Null);
2004        assert_eq!(
2005            ScalarValue::auto_detect_type("2023-12-25"),
2006            ScalarType::Timestamp
2007        );
2008        assert_eq!(
2009            ScalarValue::auto_detect_type("2023-12-25T10:30:45"),
2010            ScalarType::Timestamp
2011        );
2012        #[cfg(feature = "base64")]
2013        assert_eq!(
2014            ScalarValue::auto_detect_type("SGVsbG8gV29ybGQ="),
2015            ScalarType::Binary
2016        );
2017        #[cfg(not(feature = "base64"))]
2018        assert_eq!(
2019            ScalarValue::auto_detect_type("SGVsbG8gV29ybGQ="),
2020            ScalarType::String
2021        );
2022        assert_eq!(
2023            ScalarValue::auto_detect_type("hello world"),
2024            ScalarType::String
2025        );
2026    }
2027
2028    #[test]
2029    fn test_from_yaml_scalar_creation() {
2030        let int_scalar = ScalarValue::parse("123");
2031        assert_eq!(int_scalar.scalar_type(), ScalarType::Integer);
2032
2033        let bool_scalar = ScalarValue::parse("true");
2034        assert_eq!(bool_scalar.scalar_type(), ScalarType::Boolean);
2035
2036        let timestamp_scalar = ScalarValue::parse("2023-12-25");
2037        assert_eq!(timestamp_scalar.scalar_type(), ScalarType::Timestamp);
2038
2039        let string_scalar = ScalarValue::parse("hello world");
2040        assert_eq!(string_scalar.scalar_type(), ScalarType::String);
2041    }
2042
2043    #[test]
2044    fn test_timestamp_pattern_matching() {
2045        // Valid patterns
2046        assert!(ScalarValue::matches_iso8601_pattern("2023-12-25"));
2047        assert!(ScalarValue::matches_iso8601_pattern("2023-12-25T10:30:45"));
2048        assert!(ScalarValue::matches_iso8601_pattern("2023-12-25t10:30:45")); // Lowercase t
2049        assert!(ScalarValue::matches_iso8601_pattern("2023-12-25 10:30:45"));
2050        assert!(ScalarValue::matches_iso8601_pattern("2023-01-01T00:00:00"));
2051        assert!(ScalarValue::matches_iso8601_pattern(
2052            "2001-12-14t21:59:43.10-05:00"
2053        )); // Complex with lowercase t
2054
2055        // Invalid patterns
2056        assert!(!ScalarValue::matches_iso8601_pattern("2023-13-25")); // Invalid month
2057        assert!(!ScalarValue::matches_iso8601_pattern("23-12-25")); // Wrong year format
2058        assert!(!ScalarValue::matches_iso8601_pattern("2023/12/25")); // Wrong separator
2059        assert!(!ScalarValue::matches_iso8601_pattern("not-a-date"));
2060        assert!(!ScalarValue::matches_iso8601_pattern("2023"));
2061    }
2062
2063    #[test]
2064    #[cfg(feature = "base64")]
2065    fn test_base64_detection() {
2066        // Valid base64 strings
2067        assert!(ScalarValue::looks_like_base64("SGVsbG8=")); // "Hello"
2068        assert!(ScalarValue::looks_like_base64("V29ybGQ=")); // "World"
2069        assert!(ScalarValue::looks_like_base64("SGVsbG8gV29ybGQ=")); // "Hello World"
2070        assert!(ScalarValue::looks_like_base64("AAAA")); // All A's
2071
2072        // Invalid base64 strings
2073        assert!(!ScalarValue::looks_like_base64("Hello")); // No padding, wrong chars
2074        assert!(!ScalarValue::looks_like_base64("SGVsbG8")); // Missing padding (7 chars, should be 8 with padding)
2075        assert!(!ScalarValue::looks_like_base64("")); // Empty
2076        assert!(!ScalarValue::looks_like_base64("SGV@")); // Invalid character
2077        assert!(!ScalarValue::looks_like_base64("SGVsbG8g===")); // Too much padding
2078    }
2079
2080    #[test]
2081    #[cfg(feature = "base64")]
2082    fn test_binary_yaml_output_with_tags() {
2083        let data = b"Binary data here";
2084        let scalar = ScalarValue::binary(data);
2085        let yaml_output = scalar.to_yaml_string();
2086
2087        assert!(yaml_output.starts_with("!!binary "));
2088
2089        // Extract just the base64 part
2090        let base64_part = &yaml_output[9..]; // Remove "!!binary "
2091        let decoded = base64_decode(base64_part).expect("Should decode");
2092        assert_eq!(decoded, data);
2093    }
2094
2095    #[test]
2096    #[cfg(feature = "base64")]
2097    fn test_special_data_types_with_different_styles() {
2098        // Binary with different styles should still include tag
2099        let data = b"test";
2100        let binary_scalar = ScalarValue::binary(data);
2101
2102        // Even if we change style, binary type should maintain tag
2103        let mut styled_binary = binary_scalar;
2104        styled_binary.style = ScalarStyle::DoubleQuoted;
2105
2106        // The to_yaml_string should still respect the scalar type for tagging
2107        assert_eq!(styled_binary.to_yaml_string(), "!!binary \"dGVzdA==\"");
2108    }
2109
2110    #[test]
2111    fn test_type_checking_methods() {
2112        #[cfg(feature = "base64")]
2113        let binary_scalar = ScalarValue::binary(b"test");
2114        let timestamp_scalar = ScalarValue::timestamp("2023-12-25");
2115        let regex_scalar = ScalarValue::regex(r"\d+");
2116        let string_scalar = ScalarValue::string("hello");
2117
2118        // Test type checking methods
2119        #[cfg(feature = "base64")]
2120        assert!(binary_scalar.is_binary());
2121        #[cfg(feature = "base64")]
2122        assert!(!binary_scalar.is_timestamp());
2123        #[cfg(feature = "base64")]
2124        assert!(!binary_scalar.is_regex());
2125
2126        #[cfg(feature = "base64")]
2127        assert!(!timestamp_scalar.is_binary());
2128        assert!(timestamp_scalar.is_timestamp());
2129        assert!(!timestamp_scalar.is_regex());
2130
2131        #[cfg(feature = "base64")]
2132        assert!(!regex_scalar.is_binary());
2133        assert!(!regex_scalar.is_timestamp());
2134        assert!(regex_scalar.is_regex());
2135
2136        #[cfg(feature = "base64")]
2137        assert!(!string_scalar.is_binary());
2138        assert!(!string_scalar.is_timestamp());
2139        assert!(!string_scalar.is_regex());
2140    }
2141
2142    #[test]
2143    fn test_binary_number_parsing() {
2144        // Test binary number parsing (0b prefix)
2145        assert_eq!(ScalarValue::parse_integer("0b1010"), Some(10));
2146        assert_eq!(ScalarValue::parse_integer("0b11111111"), Some(255));
2147        assert_eq!(ScalarValue::parse_integer("0B101"), Some(5)); // Uppercase B
2148        assert_eq!(ScalarValue::parse_integer("-0b1010"), Some(-10));
2149        assert_eq!(ScalarValue::parse_integer("+0b101"), Some(5));
2150
2151        // Test auto-detection
2152        assert_eq!(ScalarValue::auto_detect_type("0b1010"), ScalarType::Integer);
2153        assert_eq!(
2154            ScalarValue::auto_detect_type("0B11111111"),
2155            ScalarType::Integer
2156        );
2157
2158        // Test invalid binary
2159        assert_eq!(ScalarValue::parse_integer("0b1012"), None); // Contains invalid digit
2160        assert_eq!(ScalarValue::parse_integer("0b"), None); // Empty after prefix
2161    }
2162
2163    #[test]
2164    fn test_modern_octal_number_parsing() {
2165        // Test modern octal number parsing (0o prefix)
2166        assert_eq!(ScalarValue::parse_integer("0o755"), Some(493)); // 7*64 + 5*8 + 5
2167        assert_eq!(ScalarValue::parse_integer("0o644"), Some(420)); // 6*64 + 4*8 + 4
2168        assert_eq!(ScalarValue::parse_integer("0O777"), Some(511)); // Uppercase O
2169        assert_eq!(ScalarValue::parse_integer("-0o755"), Some(-493));
2170        assert_eq!(ScalarValue::parse_integer("+0o644"), Some(420));
2171
2172        // Test auto-detection
2173        assert_eq!(ScalarValue::auto_detect_type("0o755"), ScalarType::Integer);
2174        assert_eq!(ScalarValue::auto_detect_type("0O644"), ScalarType::Integer);
2175
2176        // Test invalid octal
2177        assert_eq!(ScalarValue::parse_integer("0o789"), None); // Contains invalid digit
2178        assert_eq!(ScalarValue::parse_integer("0o"), None); // Empty after prefix
2179    }
2180
2181    #[test]
2182    fn test_legacy_octal_number_parsing() {
2183        // Test legacy octal number parsing (0 prefix)
2184        assert_eq!(ScalarValue::parse_integer("0755"), Some(493));
2185        assert_eq!(ScalarValue::parse_integer("0644"), Some(420));
2186        assert_eq!(ScalarValue::parse_integer("0777"), Some(511));
2187
2188        // Test auto-detection
2189        assert_eq!(ScalarValue::auto_detect_type("0755"), ScalarType::Integer);
2190        assert_eq!(ScalarValue::auto_detect_type("0644"), ScalarType::Integer);
2191
2192        // Test edge cases
2193        assert_eq!(ScalarValue::parse_integer("0"), Some(0)); // Single zero
2194        assert_eq!(ScalarValue::parse_integer("00"), Some(0)); // Double zero
2195
2196        // Numbers starting with 0 but containing 8 or 9 should fail as octal
2197        assert_eq!(ScalarValue::parse_integer("0789"), None);
2198        assert_eq!(ScalarValue::parse_integer("0128"), None);
2199    }
2200
2201    #[test]
2202    fn test_hexadecimal_number_parsing() {
2203        // Test hexadecimal number parsing (0x prefix) - should still work
2204        assert_eq!(ScalarValue::parse_integer("0xFF"), Some(255));
2205        assert_eq!(ScalarValue::parse_integer("0x1A"), Some(26));
2206        assert_eq!(ScalarValue::parse_integer("0XFF"), Some(255)); // Uppercase X
2207        assert_eq!(ScalarValue::parse_integer("-0xFF"), Some(-255));
2208        assert_eq!(ScalarValue::parse_integer("+0x1A"), Some(26));
2209
2210        // Test auto-detection
2211        assert_eq!(ScalarValue::auto_detect_type("0xFF"), ScalarType::Integer);
2212        assert_eq!(ScalarValue::auto_detect_type("0X1A"), ScalarType::Integer);
2213    }
2214
2215    #[test]
2216    fn test_decimal_number_parsing() {
2217        // Test decimal number parsing (no prefix) - should still work
2218        assert_eq!(ScalarValue::parse_integer("42"), Some(42));
2219        assert_eq!(ScalarValue::parse_integer("123"), Some(123));
2220        assert_eq!(ScalarValue::parse_integer("-42"), Some(-42));
2221        assert_eq!(ScalarValue::parse_integer("+123"), Some(123));
2222
2223        // Test auto-detection
2224        assert_eq!(ScalarValue::auto_detect_type("42"), ScalarType::Integer);
2225        assert_eq!(ScalarValue::auto_detect_type("-123"), ScalarType::Integer);
2226    }
2227
2228    #[test]
2229    fn test_number_format_yaml_output() {
2230        // Test that different number formats are properly detected and output
2231        let binary_scalar = ScalarValue::parse("0b1010");
2232        assert_eq!(binary_scalar.scalar_type(), ScalarType::Integer);
2233        assert_eq!(binary_scalar.value(), "0b1010");
2234
2235        let octal_scalar = ScalarValue::parse("0o755");
2236        assert_eq!(octal_scalar.scalar_type(), ScalarType::Integer);
2237        assert_eq!(octal_scalar.value(), "0o755");
2238
2239        let hex_scalar = ScalarValue::parse("0xFF");
2240        assert_eq!(hex_scalar.scalar_type(), ScalarType::Integer);
2241        assert_eq!(hex_scalar.value(), "0xFF");
2242
2243        let legacy_octal_scalar = ScalarValue::parse("0755");
2244        assert_eq!(legacy_octal_scalar.scalar_type(), ScalarType::Integer);
2245        assert_eq!(legacy_octal_scalar.value(), "0755");
2246    }
2247
2248    #[test]
2249    fn test_unix_timestamp_bounds() {
2250        // Unix epoch boundary: 0 is rejected, 1 accepted, and the upper bound
2251        // (2100-01-01) is exclusive.
2252        assert!(!ScalarValue::is_valid_timestamp_static("0"));
2253        assert!(ScalarValue::is_valid_timestamp_static("1"));
2254        assert!(ScalarValue::is_valid_timestamp_static("4102444799"));
2255        assert!(!ScalarValue::is_valid_timestamp_static("4102444800"));
2256
2257        // The instance method delegates to the static one.
2258        let s = ScalarValue::string("x");
2259        assert!(s.is_valid_timestamp("1"));
2260        assert!(!s.is_valid_timestamp("0"));
2261    }
2262
2263    #[test]
2264    fn test_iso8601_time_component_bounds() {
2265        // Hour/minute/second upper bounds: 23:59:59 is valid, anything past is not.
2266        assert!(ScalarValue::matches_iso8601_pattern("2023-01-01T23:59:59"));
2267        assert!(!ScalarValue::matches_iso8601_pattern("2023-01-01T24:00:00"));
2268        assert!(!ScalarValue::matches_iso8601_pattern("2023-01-01T10:60:00"));
2269        assert!(!ScalarValue::matches_iso8601_pattern("2023-01-01T10:30:60"));
2270
2271        // Day bounds: 31 is valid, 0 and 32 are not.
2272        assert!(ScalarValue::matches_iso8601_pattern("2023-01-31"));
2273        assert!(!ScalarValue::matches_iso8601_pattern("2023-01-00"));
2274        assert!(!ScalarValue::matches_iso8601_pattern("2023-01-32"));
2275
2276        // A malformed time separator falls through to invalid.
2277        assert!(!ScalarValue::matches_iso8601_pattern("2023-01-01X10:30:45"));
2278        // Time present but seconds field non-numeric.
2279        assert!(!ScalarValue::matches_iso8601_pattern("2023-01-01T10:30:xx"));
2280
2281        // The two date separators must each be '-'. A non-dash in either
2282        // position must reject even though the digit groups parse as valid
2283        // month/day. This pins the separator conjuncts in the format check.
2284        assert!(!ScalarValue::matches_iso8601_pattern("2023X12-25"));
2285        assert!(!ScalarValue::matches_iso8601_pattern("2023-12X25"));
2286    }
2287
2288    #[test]
2289    #[cfg(feature = "base64")]
2290    fn test_base64_padding_bounds() {
2291        // Exactly two padding characters is the maximum allowed and must pass.
2292        assert!(ScalarValue::looks_like_base64("QQ==")); // "A"
2293                                                         // One padding character.
2294        assert!(ScalarValue::looks_like_base64("SGVsbG8=")); // "Hello"
2295                                                             // Padding in the middle is rejected even with a valid count.
2296        assert!(!ScalarValue::looks_like_base64("QQ=Q"));
2297        // Length not a multiple of 4.
2298        assert!(!ScalarValue::looks_like_base64("QQQ"));
2299    }
2300
2301    #[test]
2302    fn test_coerce_to_null() {
2303        // Only the recognized null spellings coerce to Null.
2304        let s = ScalarValue::string("null");
2305        assert_eq!(
2306            s.coerce_to_type(ScalarType::Null).map(|v| v.scalar_type()),
2307            Some(ScalarType::Null)
2308        );
2309        assert!(ScalarValue::string("~")
2310            .coerce_to_type(ScalarType::Null)
2311            .is_some());
2312        assert!(ScalarValue::string("")
2313            .coerce_to_type(ScalarType::Null)
2314            .is_some());
2315        assert!(ScalarValue::string("notnull")
2316            .coerce_to_type(ScalarType::Null)
2317            .is_none());
2318    }
2319
2320    #[test]
2321    fn test_parse_escape_single_quote() {
2322        // The \' escape decodes to a bare single quote.
2323        assert_eq!(ScalarValue::parse_escape_sequences("a\\'b"), "a'b");
2324    }
2325
2326    #[test]
2327    fn test_parse_preserves_string_style() {
2328        // The String arm of parse() runs detect_style; a plain word stays Plain.
2329        let plain = ScalarValue::parse("hello");
2330        assert_eq!(plain.scalar_type(), ScalarType::String);
2331        assert_eq!(plain.style, ScalarStyle::Plain);
2332
2333        // A multi-line string is given Literal style by detect_style, which only
2334        // runs in the String arm of parse().
2335        let multiline = ScalarValue::parse("multi line\nvalue");
2336        assert_eq!(multiline.scalar_type(), ScalarType::String);
2337        assert_eq!(multiline.style, ScalarStyle::Literal);
2338    }
2339
2340    #[test]
2341    fn test_from_scalar_type_and_style() {
2342        use crate::yaml::Document;
2343        use rowan::ast::AstNode;
2344        use std::str::FromStr;
2345
2346        // NULL token maps to Null type.
2347        let doc = Document::from_str("k: null\n").unwrap();
2348        let mapping = doc.as_mapping().unwrap();
2349        let null_node = mapping.get("k").unwrap();
2350        let null_scalar = crate::yaml::Scalar::cast(null_node.syntax().clone()).unwrap();
2351        let sv = ScalarValue::from_scalar(&null_scalar);
2352        assert_eq!(sv.scalar_type(), ScalarType::Null);
2353        assert_eq!(sv.style, ScalarStyle::Plain);
2354
2355        // STRING token in double quotes maps to String type, DoubleQuoted style.
2356        let doc = Document::from_str("k: \"hi\"\n").unwrap();
2357        let node = doc.as_mapping().unwrap().get("k").unwrap();
2358        let scalar = crate::yaml::Scalar::cast(node.syntax().clone()).unwrap();
2359        let sv = ScalarValue::from_scalar(&scalar);
2360        assert_eq!(sv.scalar_type(), ScalarType::String);
2361        assert_eq!(sv.style, ScalarStyle::DoubleQuoted);
2362
2363        // Single quoted string keeps SingleQuoted style.
2364        let doc = Document::from_str("k: 'hi'\n").unwrap();
2365        let node = doc.as_mapping().unwrap().get("k").unwrap();
2366        let scalar = crate::yaml::Scalar::cast(node.syntax().clone()).unwrap();
2367        let sv = ScalarValue::from_scalar(&scalar);
2368        assert_eq!(sv.style, ScalarStyle::SingleQuoted);
2369    }
2370
2371    #[test]
2372    fn test_from_scalar_ampersand_in_plain_is_string() {
2373        // `3.1&1` is a single plain scalar with literal content `3.1&1`.
2374        // Per YAML 1.2 core-schema tag resolution it must resolve to
2375        // !!str, not !!float.
2376        use crate::yaml::Document;
2377        use rowan::ast::AstNode;
2378        use std::str::FromStr;
2379
2380        let doc = Document::from_str("k: 3.1&1\n").unwrap();
2381        let node = doc.as_mapping().unwrap().get("k").unwrap();
2382        let scalar = crate::yaml::Scalar::cast(node.syntax().clone()).unwrap();
2383        let sv = ScalarValue::from_scalar(&scalar);
2384        assert_eq!(sv.scalar_type(), ScalarType::String);
2385        assert_eq!(sv.style, ScalarStyle::Plain);
2386    }
2387
2388    #[test]
2389    fn test_from_scalar_signed_yaml_infinity_is_float() {
2390        // `+.INF` is one !!float per YAML 1.2 even though Rust's
2391        // `f64::from_str` wouldn't accept the bare token on its own.
2392        use crate::yaml::Document;
2393        use rowan::ast::AstNode;
2394        use std::str::FromStr;
2395
2396        let doc = Document::from_str("k: +.INF\n").unwrap();
2397        let node = doc.as_mapping().unwrap().get("k").unwrap();
2398        let scalar = crate::yaml::Scalar::cast(node.syntax().clone()).unwrap();
2399        let sv = ScalarValue::from_scalar(&scalar);
2400        assert_eq!(sv.scalar_type(), ScalarType::Float);
2401    }
2402
2403    #[test]
2404    fn test_classify_plain_bare_inf_nan_is_string() {
2405        // Per YAML 1.2 only the dotted spellings are !!float; bare
2406        // `inf`, `nan`, `infinity` in any casing are strings even
2407        // though Rust's `f64::from_str` accepts them.
2408        for input in [
2409            "inf", "Inf", "InF", "INF", "iNf", "nan", "NaN", "NAN", "nAn", "infinity", "Infinity",
2410            "INFINITY", "+inf", "-Inf", "+nan",
2411        ] {
2412            assert_eq!(
2413                ScalarValue::classify_plain(input),
2414                CoreScalarType::String,
2415                "bare {input:?} should classify as String"
2416            );
2417        }
2418        // But the dotted spellings still resolve to Float.
2419        for input in [".inf", ".Inf", ".INF", "+.inf", "-.INF", ".nan"] {
2420            assert_eq!(
2421                ScalarValue::classify_plain(input),
2422                CoreScalarType::Float,
2423                "{input:?} should classify as Float"
2424            );
2425        }
2426    }
2427
2428    #[test]
2429    fn test_from_scalar_block_style_is_string() {
2430        // Block scalars (| and >) always resolve to !!str regardless of
2431        // content, and their style must be reported as Literal or Folded.
2432        use crate::yaml::Document;
2433        use rowan::ast::AstNode;
2434        use std::str::FromStr;
2435
2436        let doc = Document::from_str("k: |\n  1\n").unwrap();
2437        let node = doc.as_mapping().unwrap().get("k").unwrap();
2438        let scalar = crate::yaml::Scalar::cast(node.syntax().clone()).unwrap();
2439        let sv = ScalarValue::from_scalar(&scalar);
2440        assert_eq!(sv.style, ScalarStyle::Literal);
2441        assert_eq!(sv.scalar_type(), ScalarType::String);
2442
2443        let doc = Document::from_str("k: >\n  x\n").unwrap();
2444        let node = doc.as_mapping().unwrap().get("k").unwrap();
2445        let scalar = crate::yaml::Scalar::cast(node.syntax().clone()).unwrap();
2446        let sv = ScalarValue::from_scalar(&scalar);
2447        assert_eq!(sv.style, ScalarStyle::Folded);
2448        assert_eq!(sv.scalar_type(), ScalarType::String);
2449    }
2450
2451    #[test]
2452    fn test_needs_quoting_special_chars() {
2453        // Leading-character based quoting and special embedded characters, plus
2454        // each boolean/null keyword spelling (so dropping any one keyword arm is
2455        // detected). Single quotes are preferred unless the value already
2456        // contains one, in which case double quotes are used.
2457        let cases = [
2458            ("&anchor", "'&anchor'"),
2459            ("*alias", "'*alias'"),
2460            ("!tag", "'!tag'"),
2461            ("a|b", "'a|b'"),
2462            ("it's", "\"it's\""),
2463            ("a\"b", "'a\"b'"),
2464            ("50%", "'50%'"),
2465            ("on", "'on'"),
2466            ("off", "'off'"),
2467            ("On", "'On'"),
2468            ("OFF", "'OFF'"),
2469            ("yes", "'yes'"),
2470            ("no", "'no'"),
2471            ("true", "'true'"),
2472            ("false", "'false'"),
2473            ("null", "'null'"),
2474        ];
2475        for (input, expected) in cases {
2476            assert_eq!(ScalarValue::string(input).to_yaml_string(), expected);
2477        }
2478        // A plain word that needs no quoting round-trips unchanged.
2479        assert_eq!(ScalarValue::string("plain").to_yaml_string(), "plain");
2480    }
2481
2482    #[test]
2483    fn test_literal_and_folded_nonempty() {
2484        // Literal and folded styles render the block indicator on its own line
2485        // followed by the content indented by two spaces.
2486        let s = ScalarValue::with_style("line one\nline two", ScalarStyle::Literal);
2487        assert_eq!(s.to_yaml_string(), "|-\n  line one\n  line two");
2488
2489        let s = ScalarValue::with_style("line one\nline two", ScalarStyle::Folded);
2490        assert_eq!(s.to_yaml_string(), ">-\n  line one\n  line two");
2491    }
2492
2493    #[test]
2494    fn test_display_matches_yaml_string() {
2495        let s = ScalarValue::string("true");
2496        assert_eq!(format!("{}", s), s.to_yaml_string());
2497        assert_eq!(format!("{}", s), "'true'");
2498    }
2499
2500    #[test]
2501    fn test_as_yaml_token_kind_per_type() {
2502        use crate::lex::SyntaxKind;
2503        use rowan::ast::AstNode;
2504
2505        let cases = [
2506            (ScalarValue::parse("42"), SyntaxKind::INT),
2507            (ScalarValue::parse("3.14"), SyntaxKind::FLOAT),
2508            (ScalarValue::parse("true"), SyntaxKind::BOOL),
2509            (ScalarValue::parse("null"), SyntaxKind::NULL),
2510            (ScalarValue::string("hello"), SyntaxKind::STRING),
2511        ];
2512        for (value, expected) in cases {
2513            let mut builder = rowan::GreenNodeBuilder::new();
2514            crate::AsYaml::build_content(&value, &mut builder, 0, false);
2515            let green = builder.finish();
2516            let node = rowan::SyntaxNode::<crate::yaml::Lang>::new_root(green);
2517            let scalar = crate::yaml::Scalar::cast(node).unwrap();
2518            let kind = scalar
2519                .syntax()
2520                .children_with_tokens()
2521                .filter_map(|c| c.into_token())
2522                .next()
2523                .unwrap()
2524                .kind();
2525            assert_eq!(kind, expected, "value {:?}", value.value());
2526        }
2527    }
2528
2529    #[test]
2530    fn test_as_yaml_quotes_string_that_looks_special() {
2531        use std::str::FromStr;
2532
2533        // A string whose value reads as a null/bool/number must be quoted so it
2534        // round-trips as a string, not the special value.
2535        let doc = crate::yaml::Document::from_str("key: x\n").unwrap();
2536        doc.as_mapping()
2537            .unwrap()
2538            .set("key", ScalarValue::string("null"));
2539        assert_eq!(doc.to_string(), "key: 'null'\n");
2540
2541        // A string containing ": " must be quoted or it produces invalid YAML.
2542        let doc = crate::yaml::Document::from_str("key: x\n").unwrap();
2543        doc.as_mapping()
2544            .unwrap()
2545            .set("key", ScalarValue::string("has: colon"));
2546        assert_eq!(doc.to_string(), "key: 'has: colon'\n");
2547
2548        // An actual null scalar stays bare.
2549        let doc = crate::yaml::Document::from_str("key: x\n").unwrap();
2550        doc.as_mapping().unwrap().set("key", ScalarValue::null());
2551        assert_eq!(doc.to_string(), "key: null\n");
2552    }
2553
2554    #[test]
2555    #[cfg(feature = "regex")]
2556    fn test_as_regex_type_gated() {
2557        // as_regex only compiles when the scalar is tagged as a regex.
2558        let re = ScalarValue::regex(r"\d+");
2559        assert!(re.as_regex().is_some());
2560
2561        let not_re = ScalarValue::string(r"\d+");
2562        assert!(not_re.as_regex().is_none());
2563
2564        // try_as_regex ignores the type and compiles the value directly.
2565        assert!(not_re.try_as_regex().is_ok());
2566        assert!(ScalarValue::string("(").try_as_regex().is_err());
2567    }
2568}