Skip to main content

yaml_edit/scalar/
mod.rs

1//! Scalar value wrapper with proper escaping and style support.
2
3#[cfg(feature = "base64")]
4mod binary;
5mod decimal;
6mod quoting;
7mod render;
8mod resolve;
9
10pub(crate) use decimal::{digits_to_decimal, places_to_decimal, signed_digits_to_decimal};
11
12use crate::as_yaml::FlowQuoting;
13use std::fmt;
14
15#[cfg(feature = "base64")]
16use binary::base64_decode;
17
18/// Style of scalar representation in YAML
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub enum ScalarStyle {
21    /// Plain scalar (no quotes)
22    Plain,
23    /// Single-quoted scalar
24    SingleQuoted,
25    /// Double-quoted scalar
26    DoubleQuoted,
27    /// Literal scalar (|)
28    Literal,
29    /// Folded scalar (>)
30    Folded,
31}
32
33/// The five scalar types recognised by the YAML 1.2 core schema.
34///
35/// Returned by [`ScalarValue::classify_plain`], which is the single
36/// source of truth for plain-scalar tag resolution. This is a strict
37/// subset of [`ScalarType`], which additionally covers yaml-edit's
38/// out-of-band types (`Timestamp`, `Binary`, `Regex`) used when
39/// constructing new values.
40#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
41pub enum CoreScalarType {
42    /// `!!null`
43    Null,
44    /// `!!bool`
45    Boolean,
46    /// `!!int`
47    Integer,
48    /// `!!float`
49    Float,
50    /// `!!str` (the fallback when no other tag matches)
51    String,
52}
53
54impl From<CoreScalarType> for ScalarType {
55    fn from(t: CoreScalarType) -> Self {
56        match t {
57            CoreScalarType::Null => ScalarType::Null,
58            CoreScalarType::Boolean => ScalarType::Boolean,
59            CoreScalarType::Integer => ScalarType::Integer,
60            CoreScalarType::Float => ScalarType::Float,
61            CoreScalarType::String => ScalarType::String,
62        }
63    }
64}
65
66/// The c-indicator characters (YAML 1.2 5.3), which cannot start a plain
67/// scalar, except for `-`, `?` and `:` followed by a safe character.
68pub(crate) const INDICATORS: &[char] = &[
69    '-', '?', ':', ',', '[', ']', '{', '}', '#', '&', '*', '!', '|', '>', '\'', '"', '%', '@', '`',
70];
71
72/// Type of a scalar value
73#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
74pub enum ScalarType {
75    /// String value
76    String,
77    /// Integer value
78    Integer,
79    /// Float value
80    Float,
81    /// Boolean value
82    Boolean,
83    /// Null value
84    Null,
85    /// Binary data (base64 encoded)
86    #[cfg(feature = "base64")]
87    Binary,
88    /// Timestamp value
89    Timestamp,
90    /// Regular expression
91    Regex,
92}
93
94/// A scalar value with metadata about its style and content
95#[derive(Debug, Clone, PartialEq, Eq)]
96pub struct ScalarValue {
97    /// The actual value
98    value: String,
99    /// The style to use when rendering
100    style: ScalarStyle,
101    /// The type of the scalar
102    scalar_type: ScalarType,
103    /// The explicit tag written on the node, e.g. `!!float` or `!local`
104    tag: Option<String>,
105}
106
107impl ScalarValue {
108    /// Create a scalar value explicitly treating it as a string (no type auto-detection)
109    ///
110    /// This method always creates a `String` type scalar. The value will be properly
111    /// quoted if needed when rendering to YAML, but no type detection is performed.
112    ///
113    /// # Examples
114    ///
115    /// ```
116    /// use yaml_edit::ScalarValue;
117    ///
118    /// let scalar = ScalarValue::string("123");
119    /// assert_eq!(scalar.scalar_type(), yaml_edit::ScalarType::String);
120    /// // Renders with quotes to distinguish from integer
121    /// assert_eq!(scalar.to_yaml_string(), "'123'");
122    ///
123    /// let scalar = ScalarValue::string("true");
124    /// assert_eq!(scalar.scalar_type(), yaml_edit::ScalarType::String);
125    /// // Renders with quotes to distinguish from boolean
126    /// assert_eq!(scalar.to_yaml_string(), "'true'");
127    ///
128    /// let scalar = ScalarValue::string("hello");
129    /// assert_eq!(scalar.scalar_type(), yaml_edit::ScalarType::String);
130    /// // Plain strings don't need quotes
131    /// assert_eq!(scalar.to_yaml_string(), "hello");
132    /// ```
133    ///
134    /// For YAML-style type detection (parsing "123" as Integer, "true" as Boolean),
135    /// use [`ScalarValue::parse()`] instead.
136    pub fn string(value: impl Into<String>) -> Self {
137        let value = value.into();
138        let style = Self::detect_style(&value);
139        // Detect the type - default to String for user-provided values
140        let scalar_type = ScalarType::String;
141        Self {
142            value,
143            style,
144            scalar_type,
145            tag: None,
146        }
147    }
148
149    /// Create a new scalar with a specific style
150    pub fn with_style(value: impl Into<String>, style: ScalarStyle) -> Self {
151        Self {
152            value: value.into(),
153            style,
154            scalar_type: ScalarType::String,
155            tag: None,
156        }
157    }
158
159    /// Create a plain scalar
160    pub fn plain(value: impl Into<String>) -> Self {
161        Self::with_style(value, ScalarStyle::Plain)
162    }
163
164    /// Create a single-quoted scalar
165    pub fn single_quoted(value: impl Into<String>) -> Self {
166        Self::with_style(value, ScalarStyle::SingleQuoted)
167    }
168
169    /// Create a double-quoted scalar
170    pub fn double_quoted(value: impl Into<String>) -> Self {
171        Self::with_style(value, ScalarStyle::DoubleQuoted)
172    }
173
174    /// Create a literal scalar
175    pub fn literal(value: impl Into<String>) -> Self {
176        Self::with_style(value, ScalarStyle::Literal)
177    }
178
179    /// Create a folded scalar
180    pub fn folded(value: impl Into<String>) -> Self {
181        Self::with_style(value, ScalarStyle::Folded)
182    }
183
184    /// Create a null scalar
185    pub fn null() -> Self {
186        Self {
187            value: "null".to_string(),
188            style: ScalarStyle::Plain,
189            scalar_type: ScalarType::Null,
190            tag: None,
191        }
192    }
193
194    /// Create a timestamp scalar
195    pub fn timestamp(value: impl Into<String>) -> Self {
196        Self {
197            value: value.into(),
198            style: ScalarStyle::Plain,
199            scalar_type: ScalarType::Timestamp,
200            tag: None,
201        }
202    }
203
204    /// Create a regex scalar
205    pub fn regex(pattern: impl Into<String>) -> Self {
206        Self {
207            value: pattern.into(),
208            style: ScalarStyle::Plain,
209            scalar_type: ScalarType::Regex,
210            tag: None,
211        }
212    }
213
214    /// Return this scalar with an explicit tag, such as `!!str` or `!local`.
215    ///
216    /// The tag is rendered in front of the content. It does not change the
217    /// scalar's [`ScalarType`].
218    ///
219    /// ```
220    /// use yaml_edit::ScalarValue;
221    ///
222    /// let scalar = ScalarValue::from(1.0).with_tag("!!float");
223    /// assert_eq!(scalar.tag(), Some("!!float"));
224    /// assert_eq!(scalar.to_yaml_string(), "!!float 1.0");
225    /// ```
226    pub fn with_tag(mut self, tag: impl Into<String>) -> Self {
227        self.tag = Some(tag.into());
228        self
229    }
230
231    /// The explicit tag on this scalar, if any.
232    pub fn tag(&self) -> Option<&str> {
233        self.tag.as_deref()
234    }
235
236    /// Get the raw value
237    pub fn value(&self) -> &str {
238        &self.value
239    }
240
241    /// Get the style
242    pub fn style(&self) -> ScalarStyle {
243        self.style
244    }
245
246    /// Get the scalar type
247    pub fn scalar_type(&self) -> ScalarType {
248        self.scalar_type
249    }
250
251    /// Try to parse this scalar as an `i64`.
252    ///
253    /// Returns `None` if the scalar type is not `Integer` or the value does
254    /// not fit in an `i64`. Integers are read as the YAML 1.2 core schema
255    /// defines them, so `0755` is seven hundred and fifty five; only
256    /// `0o755` is octal.
257    pub fn to_i64(&self) -> Option<i64> {
258        if self.scalar_type == ScalarType::Integer {
259            Self::parse_integer(self.value.trim())
260        } else {
261            None
262        }
263    }
264
265    /// Same as [`to_i64`](Self::to_i64), which now follows YAML 1.2.
266    #[deprecated(since = "0.3.3", note = "to_i64 follows YAML 1.2; use it instead")]
267    pub fn to_i64_yaml_1_2(&self) -> Option<i64> {
268        self.to_i64()
269    }
270
271    /// Try to parse this scalar as an `f64`.
272    ///
273    /// Returns `None` if the scalar type is not `Float`.
274    pub fn to_f64(&self) -> Option<f64> {
275        if self.scalar_type == ScalarType::Float {
276            Self::parse_float(self.value.trim())
277        } else {
278            None
279        }
280    }
281
282    /// Try to parse this scalar as a `bool`.
283    ///
284    /// Returns `None` if the scalar type is not `Boolean`.
285    /// Recognizes the YAML 1.2 core schema spellings: `true`, `True`,
286    /// `TRUE`, `false`, `False` and `FALSE`.
287    pub fn to_bool(&self) -> Option<bool> {
288        if self.scalar_type == ScalarType::Boolean {
289            Self::parse_bool(self.value.trim())
290        } else {
291            None
292        }
293    }
294
295    /// Check if this is a timestamp scalar
296    pub fn is_timestamp(&self) -> bool {
297        self.scalar_type == ScalarType::Timestamp
298    }
299
300    /// Check if this is a regex scalar
301    pub fn is_regex(&self) -> bool {
302        self.scalar_type == ScalarType::Regex
303    }
304
305    /// Compile and return a Regex object if this is a regex scalar
306    ///
307    /// This method is only available when the `regex` feature is enabled.
308    /// Returns None if this is not a regex scalar or if the pattern is invalid.
309    ///
310    /// # Example
311    /// ```
312    /// # #[cfg(feature = "regex")]
313    /// # {
314    /// use yaml_edit::ScalarValue;
315    ///
316    /// let scalar = ScalarValue::regex(r"\d{3}-\d{4}");
317    /// let regex = scalar.as_regex().unwrap();
318    /// assert!(regex.is_match("555-1234"));
319    /// # }
320    /// ```
321    #[cfg(feature = "regex")]
322    pub fn as_regex(&self) -> Option<regex::Regex> {
323        if self.scalar_type == ScalarType::Regex {
324            regex::Regex::new(&self.value).ok()
325        } else {
326            None
327        }
328    }
329
330    /// Try to compile this scalar as a regex, regardless of its type
331    ///
332    /// This method is only available when the `regex` feature is enabled.
333    /// This will attempt to compile the scalar value as a regex pattern,
334    /// even if it's not marked with the !!regex tag.
335    ///
336    /// # Example
337    /// ```
338    /// # #[cfg(feature = "regex")]
339    /// # {
340    /// use yaml_edit::ScalarValue;
341    ///
342    /// let scalar = ScalarValue::string(r"\d+");  // Plain string scalar
343    /// let regex = scalar.try_as_regex().unwrap();
344    /// assert!(regex.is_match("123"));
345    /// # }
346    /// ```
347    #[cfg(feature = "regex")]
348    pub fn try_as_regex(&self) -> Result<regex::Regex, regex::Error> {
349        regex::Regex::new(&self.value)
350    }
351
352    /// Try to coerce this scalar to the specified type
353    pub fn coerce_to_type(&self, target_type: ScalarType) -> Option<ScalarValue> {
354        if self.scalar_type == target_type {
355            return Some(self.clone());
356        }
357
358        match target_type {
359            ScalarType::String => Some(ScalarValue {
360                value: self.value.clone(),
361                style: ScalarStyle::Plain,
362                scalar_type: ScalarType::String,
363                tag: None,
364            }),
365            ScalarType::Integer => Self::parse_integer(self.value.trim()).map(ScalarValue::from),
366            ScalarType::Float => Self::parse_float(self.value.trim()).map(ScalarValue::from),
367            ScalarType::Boolean => match self.value.to_lowercase().as_str() {
368                "true" | "yes" | "on" | "1" => Some(ScalarValue::from(true)),
369                "false" | "no" | "off" | "0" => Some(ScalarValue::from(false)),
370                _ => None,
371            },
372            ScalarType::Null => match self.value.to_lowercase().as_str() {
373                "null" | "~" | "" => Some(ScalarValue::null()),
374                _ => None,
375            },
376            #[cfg(feature = "base64")]
377            ScalarType::Binary => {
378                // Try to decode as base64 to verify it's valid binary data
379                if base64_decode(&self.value).is_ok() {
380                    Some(ScalarValue {
381                        value: self.value.clone(),
382                        style: ScalarStyle::Plain,
383                        scalar_type: ScalarType::Binary,
384                        tag: None,
385                    })
386                } else {
387                    None
388                }
389            }
390            ScalarType::Timestamp => {
391                // Basic timestamp format validation
392                if Self::is_valid_timestamp(&self.value) {
393                    Some(ScalarValue::timestamp(&self.value))
394                } else {
395                    None
396                }
397            }
398            ScalarType::Regex => {
399                // For regex, just convert the value
400                Some(ScalarValue::regex(&self.value))
401            }
402        }
403    }
404
405    /// Parse a YAML scalar value with automatic type detection
406    ///
407    /// This method automatically detects the YAML type based on the content:
408    /// - "123" → Integer
409    /// - "3.14" → Float
410    /// - "true" / "false" → Boolean
411    /// - "null" / "~" → Null
412    /// - etc.
413    ///
414    /// # Examples
415    ///
416    /// ```
417    /// use yaml_edit::{ScalarValue, ScalarType};
418    ///
419    /// let scalar = ScalarValue::parse("123");
420    /// assert_eq!(scalar.scalar_type(), ScalarType::Integer);
421    /// assert_eq!(scalar.to_yaml_string(), "123");
422    ///
423    /// let scalar = ScalarValue::parse("true");
424    /// assert_eq!(scalar.scalar_type(), ScalarType::Boolean);
425    /// assert_eq!(scalar.to_yaml_string(), "true");
426    ///
427    /// let scalar = ScalarValue::parse("hello");
428    /// assert_eq!(scalar.scalar_type(), ScalarType::String);
429    /// assert_eq!(scalar.to_yaml_string(), "hello");
430    /// ```
431    ///
432    /// To create a String-type scalar without auto-detection (e.g., to represent
433    /// the string "123" rather than the integer 123), use [`ScalarValue::string()`] instead.
434    pub fn parse(value: impl Into<String>) -> Self {
435        let value = value.into();
436        let scalar_type = Self::auto_detect_type(&value);
437        // For non-string types, use Plain style (no quotes)
438        // For string types, detect appropriate style
439        let style = match scalar_type {
440            ScalarType::String => Self::detect_style(&value),
441            // All other types use plain style
442            _ => ScalarStyle::Plain,
443        };
444
445        Self {
446            value,
447            style,
448            scalar_type,
449            tag: None,
450        }
451    }
452
453    /// Create a ScalarValue from a Scalar syntax node, preserving the type from the lexer
454    ///
455    /// This extracts type information directly from the token kind (INT, BOOL, FLOAT, etc.)
456    /// rather than guessing based on heuristics. This is the correct way to convert
457    /// parsed YAML into ScalarValue.
458    pub fn from_scalar(scalar: &crate::yaml::Scalar) -> Self {
459        let value = scalar.as_string();
460        let raw_text = scalar.value();
461
462        let style = if raw_text.starts_with('"') && raw_text.ends_with('"') {
463            ScalarStyle::DoubleQuoted
464        } else if raw_text.starts_with('\'') && raw_text.ends_with('\'') {
465            ScalarStyle::SingleQuoted
466        } else if scalar.is_block() && raw_text.starts_with('|') {
467            ScalarStyle::Literal
468        } else if scalar.is_block() {
469            ScalarStyle::Folded
470        } else {
471            ScalarStyle::Plain
472        };
473
474        // Quoted and block scalars are always !!str per YAML 1.2 tag
475        // resolution. Plain scalars go through the core-schema
476        // classifier, which stays correct even when the plain-scalar
477        // text spans multiple lexer tokens (e.g. multi-line plain
478        // scalars, which yield STRING + NEWLINE + INDENT + STRING).
479        // `trim_end` because the lexer may absorb trailing whitespace
480        // into the scalar text; leading whitespace can't appear because
481        // it would have been emitted as INDENT.
482        let scalar_type = if style != ScalarStyle::Plain {
483            ScalarType::String
484        } else {
485            Self::classify_plain(raw_text.trim_end()).into()
486        };
487
488        Self {
489            value,
490            style,
491            scalar_type,
492            tag: None,
493        }
494    }
495}
496
497impl fmt::Display for ScalarValue {
498    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
499        write!(f, "{}", self.to_yaml_string())
500    }
501}
502
503impl From<String> for ScalarValue {
504    fn from(value: String) -> Self {
505        Self::string(value)
506    }
507}
508
509impl From<&str> for ScalarValue {
510    fn from(value: &str) -> Self {
511        Self::string(value)
512    }
513}
514
515/// `From<$t>` for the numeric primitives, all of which render through
516/// `to_string()` and differ only in the YAML type they carry.
517macro_rules! impl_from_number {
518    ($($t:ty => $ty:expr),* $(,)?) => {
519        $(
520            impl From<$t> for ScalarValue {
521                fn from(value: $t) -> Self {
522                    Self {
523                        value: value.to_string(),
524                        style: ScalarStyle::Plain,
525                        scalar_type: $ty,
526                        tag: None,
527                    }
528                }
529            }
530        )*
531    };
532}
533
534impl_from_number! {
535    i32 => ScalarType::Integer,
536    i64 => ScalarType::Integer,
537}
538
539macro_rules! impl_from_float {
540    ($($t:ty),* $(,)?) => {
541        $(
542            impl From<$t> for ScalarValue {
543                fn from(value: $t) -> Self {
544                    Self {
545                        value: Self::format_float(value.to_string()),
546                        style: ScalarStyle::Plain,
547                        scalar_type: ScalarType::Float,
548                        tag: None,
549                    }
550                }
551            }
552        )*
553    };
554}
555
556impl_from_float!(f32, f64);
557
558impl From<bool> for ScalarValue {
559    fn from(value: bool) -> Self {
560        Self {
561            value: if value { "true" } else { "false" }.to_string(),
562            style: ScalarStyle::Plain,
563            scalar_type: ScalarType::Boolean,
564            tag: None,
565        }
566    }
567}
568
569/// Same as [`ScalarValue::from_scalar`].
570impl From<crate::yaml::Scalar> for ScalarValue {
571    fn from(scalar: crate::yaml::Scalar) -> Self {
572        Self::from_scalar(&scalar)
573    }
574}
575
576impl crate::AsYaml for ScalarValue {
577    fn as_node(&self) -> Option<&crate::yaml::SyntaxNode> {
578        None
579    }
580
581    fn kind(&self) -> crate::as_yaml::YamlKind {
582        match self.rendered_tag() {
583            Some(tag) => crate::as_yaml::YamlKind::Tagged(std::borrow::Cow::Owned(tag.to_string())),
584            None => crate::as_yaml::YamlKind::Scalar,
585        }
586    }
587
588    fn build_content(
589        &self,
590        builder: &mut rowan::GreenNodeBuilder,
591        indent: usize,
592        flow_context: bool,
593    ) -> bool {
594        self.build(
595            builder,
596            indent,
597            flow_context.then_some(FlowQuoting::Minimal),
598        )
599    }
600
601    fn build_flow_content(
602        &self,
603        builder: &mut rowan::GreenNodeBuilder,
604        indent: usize,
605        quoting: FlowQuoting,
606    ) -> bool {
607        self.build(builder, indent, Some(quoting))
608    }
609
610    fn is_inline(&self) -> bool {
611        true
612    }
613}
614
615#[cfg(test)]
616mod tests {
617    use super::*;
618
619    #[test]
620    fn test_plain_scalars() {
621        let scalar = ScalarValue::string("simple");
622        assert_eq!(scalar.to_yaml_string(), "simple");
623
624        let scalar = ScalarValue::string("hello world");
625        assert_eq!(scalar.to_yaml_string(), "hello world");
626    }
627
628    #[test]
629    fn test_values_needing_quotes() {
630        // Boolean-like values
631        let scalar = ScalarValue::string("true");
632        assert_eq!(scalar.to_yaml_string(), "'true'");
633
634        let scalar = ScalarValue::string("false");
635        assert_eq!(scalar.to_yaml_string(), "'false'");
636
637        let scalar = ScalarValue::string("yes");
638        assert_eq!(scalar.to_yaml_string(), "'yes'");
639
640        let scalar = ScalarValue::string("no");
641        assert_eq!(scalar.to_yaml_string(), "'no'");
642
643        // Null-like values
644        let scalar = ScalarValue::string("null");
645        assert_eq!(scalar.to_yaml_string(), "'null'");
646
647        let scalar = ScalarValue::string("~");
648        assert_eq!(scalar.to_yaml_string(), "'~'");
649
650        // Numbers
651        let scalar = ScalarValue::string("123");
652        assert_eq!(scalar.to_yaml_string(), "'123'");
653
654        let scalar = ScalarValue::string("3.14");
655        assert_eq!(scalar.to_yaml_string(), "'3.14'");
656
657        // Special characters
658        let scalar = ScalarValue::string("value: something");
659        assert_eq!(scalar.to_yaml_string(), "'value: something'");
660
661        let scalar = ScalarValue::string("# comment");
662        assert_eq!(scalar.to_yaml_string(), "'# comment'");
663
664        // Leading/trailing whitespace
665        let scalar = ScalarValue::string("  spaces  ");
666        assert_eq!(scalar.to_yaml_string(), "'  spaces  '");
667    }
668
669    #[test]
670    fn test_single_quoted() {
671        let scalar = ScalarValue::single_quoted("value with 'quotes'");
672        assert_eq!(scalar.to_yaml_string(), "'value with ''quotes'''");
673    }
674
675    #[test]
676    fn test_double_quoted() {
677        let scalar = ScalarValue::double_quoted("value with \"quotes\" and \\backslash");
678        assert_eq!(
679            scalar.to_yaml_string(),
680            "\"value with \\\"quotes\\\" and \\\\backslash\""
681        );
682
683        let scalar = ScalarValue::double_quoted("line1\nline2\ttab");
684        assert_eq!(scalar.to_yaml_string(), "\"line1\\nline2\\ttab\"");
685    }
686
687    #[test]
688    fn test_carriage_return_is_escaped() {
689        assert_eq!(ScalarValue::plain("a\rb").to_yaml_string(), "\"a\\rb\"");
690        assert_eq!(
691            ScalarValue::single_quoted("a\rb").to_yaml_string(),
692            "\"a\\rb\""
693        );
694    }
695
696    #[test]
697    fn test_multiline() {
698        let scalar = ScalarValue::string("line1\nline2\nline3");
699        // Should auto-detect literal style for multiline
700        assert_eq!(scalar.style(), ScalarStyle::Literal);
701    }
702
703    #[test]
704    fn test_from_types() {
705        let scalar = ScalarValue::from(42);
706        assert_eq!(scalar.to_yaml_string(), "42");
707
708        let scalar = ScalarValue::from(1.234);
709        assert_eq!(scalar.to_yaml_string(), "1.234");
710
711        let scalar = ScalarValue::from(true);
712        assert_eq!(scalar.to_yaml_string(), "true");
713
714        let scalar = ScalarValue::from(false);
715        assert_eq!(scalar.to_yaml_string(), "false");
716    }
717
718    #[test]
719    fn test_empty_string() {
720        let scalar = ScalarValue::string("");
721        assert_eq!(scalar.to_yaml_string(), "''");
722    }
723
724    #[test]
725    fn test_special_start_chars() {
726        let scalar = ScalarValue::string("-item");
727        assert_eq!(scalar.to_yaml_string(), "'-item'");
728
729        let scalar = ScalarValue::string("?key");
730        assert_eq!(scalar.to_yaml_string(), "'?key'");
731
732        let scalar = ScalarValue::string("[array]");
733        assert_eq!(scalar.to_yaml_string(), "'[array]'");
734    }
735
736    #[test]
737    fn test_null_scalar() {
738        let scalar = ScalarValue::null();
739        assert_eq!(scalar.to_yaml_string(), "null");
740        assert_eq!(scalar.scalar_type, ScalarType::Null);
741    }
742
743    #[test]
744    fn test_indentation_preservation() {
745        // Test preserving exact indentation in block scalars
746        let content_with_indent = "  Line 1\n    Line 2 more indented\n  Line 3";
747        let scalar = ScalarValue::literal(content_with_indent);
748
749        // The indicator keeps the leading spaces from being read as the
750        // block's own indentation.
751        let yaml_output = scalar.to_literal_with_indent(0, 2);
752        assert_eq!(
753            yaml_output,
754            "|2-\n    Line 1\n      Line 2 more indented\n    Line 3"
755        );
756    }
757
758    #[test]
759    fn test_literal_with_custom_indent() {
760        // Test applying custom indentation to unindented content
761        let content = "Line 1\nLine 2\nLine 3";
762        let scalar = ScalarValue::literal(content);
763
764        let yaml_4_spaces = scalar.to_literal_with_indent(0, 4);
765        assert_eq!(yaml_4_spaces, "|-\n    Line 1\n    Line 2\n    Line 3");
766
767        let yaml_1_space = scalar.to_literal_with_indent(0, 1);
768        assert_eq!(yaml_1_space, "|-\n Line 1\n Line 2\n Line 3");
769    }
770
771    #[test]
772    fn test_block_indent_counts_from_parent_column() {
773        let scalar = ScalarValue::literal("  x\n  y");
774        assert_eq!(scalar.to_yaml_string(), "|2-\n    x\n    y");
775        assert_eq!(
776            scalar.to_yaml_string_with_indent(2, 2),
777            "|2-\n      x\n      y"
778        );
779        assert_eq!(
780            scalar.to_literal_with_indent(4, 3),
781            "|3-\n         x\n         y"
782        );
783        assert_eq!(
784            ScalarValue::folded("  x\n").to_folded_with_indent(2, 2),
785            ">2\n      x"
786        );
787        assert_eq!(
788            ScalarValue::literal("x\ny\n").to_yaml_string_with_indent(2, 2),
789            "|\n    x\n    y"
790        );
791    }
792
793    #[test]
794    fn test_block_indicator_when_first_line_is_indented() {
795        // Only the first line decides the indentation a reader detects, so
796        // it alone decides whether an indicator is needed.
797        assert_eq!(
798            ScalarValue::literal(" a\nb").to_yaml_string(),
799            "|2-\n   a\n  b"
800        );
801        assert_eq!(
802            ScalarValue::literal("  L1\n    L2\n  L3").to_yaml_string_with_indent(2, 2),
803            "|2-\n      L1\n        L2\n      L3"
804        );
805        assert_eq!(
806            ScalarValue::literal("  \nx").to_yaml_string(),
807            "|2-\n    \n  x"
808        );
809        assert_eq!(
810            ScalarValue::literal("a\n  b").to_yaml_string(),
811            "|-\n  a\n    b"
812        );
813    }
814
815    #[test]
816    fn test_block_keeps_whitespace_only_lines() {
817        assert_eq!(
818            ScalarValue::literal("a\n  \nb").to_yaml_string(),
819            "|-\n  a\n    \n  b"
820        );
821    }
822
823    #[test]
824    #[should_panic(expected = "does not fit in one digit")]
825    fn test_block_indent_indicator_too_large() {
826        ScalarValue::literal("  x").to_literal_with_indent(0, 10);
827    }
828
829    #[test]
830    #[should_panic(expected = "must be indented")]
831    fn test_block_indent_zero() {
832        ScalarValue::literal("x").to_literal_with_indent(2, 0);
833    }
834
835    #[test]
836    fn test_folded_with_custom_indent() {
837        // Test applying custom indentation to folded scalars
838        let content = "Line 1\nLine 2\nLine 3";
839        let scalar = ScalarValue::folded(content);
840
841        let yaml_3_spaces = scalar.to_folded_with_indent(0, 3);
842        assert_eq!(yaml_3_spaces, ">-\n   Line 1\n\n   Line 2\n\n   Line 3");
843    }
844
845    #[test]
846    fn test_block_scalar_first_line_more_indented() {
847        // The first line's extra spaces would be taken as the block's own
848        // indentation, leaving the later line outside it.
849        assert_eq!(
850            ScalarValue::string("  a\nb").to_yaml_string(),
851            "|2-\n    a\n  b"
852        );
853        assert_eq!(
854            ScalarValue::string("  x\n y").to_yaml_string_with_indent(2, 2),
855            "|2-\n      x\n     y"
856        );
857        assert_eq!(
858            ScalarValue::string("\n x").to_yaml_string_with_indent(2, 2),
859            "|2-\n\n     x"
860        );
861    }
862
863    #[test]
864    fn test_block_scalar_whitespace_only_lines() {
865        assert_eq!(
866            ScalarValue::literal("a\n ").to_yaml_string(),
867            "|-\n  a\n   "
868        );
869        assert_eq!(
870            ScalarValue::literal("a\n   \nb").to_yaml_string(),
871            "|-\n  a\n     \n  b"
872        );
873        assert_eq!(ScalarValue::literal(" ").to_yaml_string(), "|2-\n   ");
874        assert_eq!(ScalarValue::literal(" \n").to_yaml_string(), "|2\n   ");
875        // Left to choose, such a value is double-quoted.
876        assert_eq!(ScalarValue::string("a\n ").to_yaml_string(), "\"a\\n \"");
877        assert_eq!(ScalarValue::string(" \n").to_yaml_string(), "\" \\n\"");
878    }
879
880    #[test]
881    fn test_folded_preserves_line_breaks() {
882        assert_eq!(
883            ScalarValue::folded("a\nb").to_yaml_string(),
884            ">-\n  a\n\n  b"
885        );
886        // Breaks next to a more-indented line are not folded, so they need
887        // no extra empty line.
888        assert_eq!(
889            ScalarValue::folded("a\n\nb\n  c\nd\n").to_yaml_string(),
890            ">\n  a\n\n\n  b\n    c\n  d"
891        );
892    }
893
894    #[test]
895    fn test_single_quoted_line_breaks() {
896        // Continuation lines are indented to stay inside the parent block.
897        assert_eq!(
898            ScalarValue::single_quoted("a\nb").to_yaml_string(),
899            "'a\n\n  b'"
900        );
901        assert_eq!(
902            ScalarValue::single_quoted("a\n\nb").to_yaml_string(),
903            "'a\n\n\n  b'"
904        );
905        assert_eq!(
906            ScalarValue::single_quoted("\nb\n").to_yaml_string(),
907            "'\n\n  b\n\n'"
908        );
909        assert_eq!(
910            ScalarValue::single_quoted("a\nb").to_yaml_string_with_indent(2, 2),
911            "'a\n\n    b'"
912        );
913        // Whitespace around a line break is trimmed when folding, so only
914        // double quotes can carry it.
915        assert_eq!(
916            ScalarValue::single_quoted("a \nb").to_yaml_string(),
917            "\"a \\nb\""
918        );
919        assert_eq!(
920            ScalarValue::single_quoted("a\n b").to_yaml_string(),
921            "\"a\\n b\""
922        );
923    }
924
925    #[test]
926    fn test_mixed_empty_lines_preservation() {
927        // Test handling of empty lines in block scalars
928        let content_with_empty_lines = "Line 1\n\nLine 3\n\n\nLine 6";
929        let scalar = ScalarValue::literal(content_with_empty_lines);
930
931        let yaml_output = scalar.to_literal_with_indent(0, 2);
932        assert_eq!(yaml_output, "|-\n  Line 1\n\n  Line 3\n\n\n  Line 6");
933
934        // Empty lines should remain empty (no indentation added)
935        // Input has 3 empty lines; they should appear unchanged in the output
936        let lines: Vec<&str> = yaml_output.lines().collect();
937        let empty_line_count = lines.iter().filter(|line| line.is_empty()).count();
938        assert_eq!(empty_line_count, 3);
939    }
940
941    #[test]
942    fn test_double_quoted_with_escapes() {
943        // Test that double-quoted scalars properly escape and unescape
944        let original = "Hello\nWorld\t😃";
945        let scalar = ScalarValue::double_quoted(original);
946        let yaml_string = scalar.to_yaml_string();
947
948        // Should contain escaped sequences
949        assert_eq!(yaml_string, "\"Hello\\nWorld\\t\\U0001F603\"");
950
951        // Parse it back
952        let parsed = ScalarValue::parse_escape_sequences(&yaml_string[1..yaml_string.len() - 1]);
953        assert_eq!(parsed, original);
954    }
955
956    #[test]
957    fn test_unicode_output_formatting() {
958        // Test that Unicode characters are properly formatted in output
959        let scalar = ScalarValue::double_quoted("Hello 世界 🌍");
960        let yaml_string = scalar.to_yaml_string();
961
962        // Should escape non-ASCII characters
963        assert_eq!(yaml_string, "\"Hello \\u4E16\\u754C \\U0001F30D\"");
964
965        // But the internal value should remain unchanged
966        assert_eq!(scalar.value(), "Hello 世界 🌍");
967    }
968
969    #[test]
970    fn test_timestamp_creation_and_validation() {
971        // Test various timestamp formats
972        let valid_timestamps = [
973            "2023-12-25",
974            "2023-12-25T10:30:45",
975            "2023-12-25 10:30:45",
976            "2023-12-25T10:30:45Z",
977            "2001-12-14 21:59:43.10 -5", // Space-separated with timezone
978            "2001-12-15T02:59:43.1Z",    // ISO 8601
979            "2001-12-14t21:59:43.10-05:00", // Lowercase t
980        ];
981
982        for ts in valid_timestamps {
983            let scalar = ScalarValue::timestamp(ts);
984            assert!(scalar.is_timestamp());
985            assert_eq!(scalar.scalar_type(), ScalarType::Timestamp);
986            assert_eq!(scalar.value(), ts);
987
988            let yaml_output = scalar.to_yaml_string();
989            assert_eq!(yaml_output, format!("!!timestamp {}", ts));
990
991            // Test auto-detection recognizes it as timestamp
992            let auto_scalar = ScalarValue::parse(ts);
993            assert_eq!(
994                auto_scalar.scalar_type(),
995                ScalarType::Timestamp,
996                "Failed to auto-detect '{}' as timestamp",
997                ts
998            );
999        }
1000
1001        // Test invalid timestamps are not recognized
1002        let invalid_timestamps = [
1003            "not-a-date",
1004            "2023-13-01", // Invalid month
1005            "2023-12-32", // Invalid day
1006            "12:34:56",   // Time only (should be String)
1007            "2023/12/25", // Wrong separator
1008        ];
1009
1010        for ts in invalid_timestamps {
1011            let auto_scalar = ScalarValue::parse(ts);
1012            assert_ne!(
1013                auto_scalar.scalar_type(),
1014                ScalarType::Timestamp,
1015                "'{}' should not be detected as timestamp",
1016                ts
1017            );
1018        }
1019    }
1020
1021    #[test]
1022    fn test_regex_creation() {
1023        let pattern = r"^\d{3}-\d{2}-\d{4}$";
1024        let scalar = ScalarValue::regex(pattern);
1025
1026        assert!(scalar.is_regex());
1027        assert_eq!(scalar.scalar_type(), ScalarType::Regex);
1028        assert_eq!(scalar.value(), pattern);
1029
1030        let yaml_output = scalar.to_yaml_string();
1031        assert_eq!(yaml_output, format!("!!regex {}", pattern));
1032    }
1033
1034    #[test]
1035    fn test_regex_edge_cases() {
1036        // Test empty pattern
1037        let empty_regex = ScalarValue::regex("");
1038        assert!(empty_regex.is_regex());
1039        assert_eq!(empty_regex.value(), "");
1040        assert_eq!(empty_regex.to_yaml_string(), "!!regex");
1041
1042        // Test pattern with special characters
1043        let special_chars = ScalarValue::regex(r"[.*+?^${}()|[\]\\]");
1044        assert!(special_chars.is_regex());
1045        assert_eq!(special_chars.value(), r"[.*+?^${}()|[\]\\]");
1046
1047        // Test unicode patterns
1048        let unicode_regex = ScalarValue::regex(r"\p{L}+");
1049        assert!(unicode_regex.is_regex());
1050        assert_eq!(unicode_regex.value(), r"\p{L}+");
1051
1052        // Test very long pattern
1053        let long_pattern = "a".repeat(1000);
1054        let long_regex = ScalarValue::regex(&long_pattern);
1055        assert!(long_regex.is_regex());
1056        assert_eq!(long_regex.value(), long_pattern);
1057
1058        // Test pattern with quotes and escapes
1059        let quoted_regex = ScalarValue::regex(r#"'quoted' and "double quoted" with \\ backslash"#);
1060        assert!(quoted_regex.is_regex());
1061        assert_eq!(
1062            quoted_regex.value(),
1063            r#"'quoted' and "double quoted" with \\ backslash"#
1064        );
1065    }
1066
1067    #[test]
1068    fn test_regex_type_coercion() {
1069        let regex_scalar = ScalarValue::regex(r"\d+");
1070
1071        // Test coercing regex to string
1072        let string_scalar = regex_scalar.coerce_to_type(ScalarType::String).unwrap();
1073        assert_eq!(string_scalar.scalar_type(), ScalarType::String);
1074        assert_eq!(string_scalar.value(), r"\d+");
1075        assert!(!string_scalar.is_regex());
1076
1077        // Test coercing string to regex
1078        let str_scalar = ScalarValue::string("test.*");
1079        let regex_from_string = str_scalar.coerce_to_type(ScalarType::Regex).unwrap();
1080        assert_eq!(regex_from_string.scalar_type(), ScalarType::Regex);
1081        assert_eq!(regex_from_string.value(), "test.*");
1082        assert!(regex_from_string.is_regex());
1083
1084        // Test that regex cannot be coerced to number types
1085        assert!(regex_scalar.coerce_to_type(ScalarType::Integer).is_none());
1086        assert!(regex_scalar.coerce_to_type(ScalarType::Float).is_none());
1087        assert!(regex_scalar.coerce_to_type(ScalarType::Boolean).is_none());
1088    }
1089
1090    #[test]
1091    #[cfg(feature = "regex")]
1092    fn test_regex_compilation() {
1093        // Test as_regex() with a regex scalar
1094        let regex_scalar = ScalarValue::regex(r"\d{3}-\d{4}");
1095        let compiled = regex_scalar.as_regex().unwrap();
1096        assert!(compiled.is_match("555-1234"));
1097        assert!(!compiled.is_match("not-a-phone"));
1098
1099        // Test as_regex() with a non-regex scalar returns None
1100        let string_scalar = ScalarValue::string("not a regex");
1101        assert!(string_scalar.as_regex().is_none());
1102
1103        // Test try_as_regex() with any scalar type
1104        let pattern_scalar = ScalarValue::string(r"^\w+@\w+\.\w+$");
1105        let email_regex = pattern_scalar.try_as_regex().unwrap();
1106        assert!(email_regex.is_match("test@example.com"));
1107        assert!(!email_regex.is_match("not-an-email"));
1108
1109        // Test with invalid regex pattern
1110        let invalid_scalar = ScalarValue::regex(r"[invalid(");
1111        assert!(invalid_scalar.as_regex().is_none());
1112
1113        // Test try_as_regex() with invalid pattern returns error
1114        let invalid_pattern = ScalarValue::string(r"[invalid(");
1115        assert!(invalid_pattern.try_as_regex().is_err());
1116    }
1117
1118    #[test]
1119    #[cfg(feature = "regex")]
1120    fn test_regex_extraction_use_cases() {
1121        // Test extracting and using regex for validation
1122        let validation_rules = [
1123            ScalarValue::regex(r"^\d{5}$"),                 // ZIP code
1124            ScalarValue::regex(r"^[A-Z]{2}$"),              // State code
1125            ScalarValue::regex(r"^\(\d{3}\) \d{3}-\d{4}$"), // Phone number
1126        ];
1127
1128        let test_values = ["12345", "CA", "(555) 123-4567"];
1129
1130        for (rule, value) in validation_rules.iter().zip(test_values.iter()) {
1131            let regex = rule.as_regex().unwrap();
1132            assert!(regex.is_match(value), "Pattern should match {}", value);
1133        }
1134
1135        // Test with complex regex patterns
1136        let email_regex = ScalarValue::regex(r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$");
1137        let compiled = email_regex.as_regex().unwrap();
1138        assert!(compiled.is_match("user@example.com"));
1139        assert!(compiled.is_match("test.user+tag@sub.domain.org"));
1140        assert!(!compiled.is_match("invalid.email"));
1141
1142        // Test extracting capture groups
1143        let version_regex = ScalarValue::regex(r"^v(\d+)\.(\d+)\.(\d+)$");
1144        let compiled = version_regex.as_regex().unwrap();
1145        if let Some(captures) = compiled.captures("v1.2.3") {
1146            assert_eq!(captures.get(1).unwrap().as_str(), "1");
1147            assert_eq!(captures.get(2).unwrap().as_str(), "2");
1148            assert_eq!(captures.get(3).unwrap().as_str(), "3");
1149        } else {
1150            panic!("Should have matched version string");
1151        }
1152    }
1153
1154    #[test]
1155    fn test_type_coercion() {
1156        // Test coercing string to integer
1157        let str_scalar = ScalarValue::string("42");
1158        let int_scalar = str_scalar.coerce_to_type(ScalarType::Integer).unwrap();
1159        assert_eq!(int_scalar.scalar_type(), ScalarType::Integer);
1160        assert_eq!(int_scalar.value(), "42");
1161
1162        // Test coercing string to boolean
1163        let bool_scalar = ScalarValue::string("true")
1164            .coerce_to_type(ScalarType::Boolean)
1165            .unwrap();
1166        assert_eq!(bool_scalar.scalar_type(), ScalarType::Boolean);
1167        assert_eq!(bool_scalar.value(), "true");
1168
1169        // Test coercing boolean string variations
1170        let yes_scalar = ScalarValue::string("yes")
1171            .coerce_to_type(ScalarType::Boolean)
1172            .unwrap();
1173        assert_eq!(yes_scalar.value(), "true");
1174
1175        let no_scalar = ScalarValue::string("no")
1176            .coerce_to_type(ScalarType::Boolean)
1177            .unwrap();
1178        assert_eq!(no_scalar.value(), "false");
1179
1180        // Test failed coercion
1181        let str_scalar = ScalarValue::string("not_a_number");
1182        assert!(str_scalar.coerce_to_type(ScalarType::Integer).is_none());
1183    }
1184
1185    #[test]
1186    fn test_auto_type_detection() {
1187        // Test various automatic type detections
1188        assert_eq!(ScalarValue::auto_detect_type("42"), ScalarType::Integer);
1189        assert_eq!(ScalarValue::auto_detect_type("3.14"), ScalarType::Float);
1190        assert_eq!(ScalarValue::auto_detect_type("true"), ScalarType::Boolean);
1191        assert_eq!(ScalarValue::auto_detect_type("false"), ScalarType::Boolean);
1192        assert_eq!(ScalarValue::auto_detect_type("yes"), ScalarType::String);
1193        assert_eq!(ScalarValue::auto_detect_type("NuLl"), ScalarType::String);
1194        assert_eq!(ScalarValue::auto_detect_type("null"), ScalarType::Null);
1195        assert_eq!(ScalarValue::auto_detect_type("~"), ScalarType::Null);
1196        assert_eq!(ScalarValue::auto_detect_type(""), ScalarType::Null);
1197        assert_eq!(
1198            ScalarValue::auto_detect_type("2023-12-25"),
1199            ScalarType::Timestamp
1200        );
1201        assert_eq!(
1202            ScalarValue::auto_detect_type("2023-12-25T10:30:45"),
1203            ScalarType::Timestamp
1204        );
1205        // Base64-looking text is still a string unless tagged !!binary.
1206        assert_eq!(
1207            ScalarValue::auto_detect_type("SGVsbG8gV29ybGQ="),
1208            ScalarType::String
1209        );
1210        assert_eq!(
1211            ScalarValue::auto_detect_type("hello world"),
1212            ScalarType::String
1213        );
1214    }
1215
1216    #[test]
1217    fn test_from_yaml_scalar_creation() {
1218        let int_scalar = ScalarValue::parse("123");
1219        assert_eq!(int_scalar.scalar_type(), ScalarType::Integer);
1220
1221        let bool_scalar = ScalarValue::parse("true");
1222        assert_eq!(bool_scalar.scalar_type(), ScalarType::Boolean);
1223
1224        let timestamp_scalar = ScalarValue::parse("2023-12-25");
1225        assert_eq!(timestamp_scalar.scalar_type(), ScalarType::Timestamp);
1226
1227        let string_scalar = ScalarValue::parse("hello world");
1228        assert_eq!(string_scalar.scalar_type(), ScalarType::String);
1229    }
1230
1231    #[test]
1232    fn test_yaml_1_1_timestamp_examples() {
1233        // The examples of https://yaml.org/type/timestamp.html, and the
1234        // one-digit and white space forms its regular expression allows.
1235        for text in [
1236            "2001-12-15T02:59:43.1Z",
1237            "2001-12-14t21:59:43.10-05:00",
1238            "2001-12-14 21:59:43.10 -5",
1239            "2001-12-15 2:59:43.10",
1240            "2002-12-14",
1241            "2001-1-5T1:02:03Z",
1242            "2001-12-14  21:59:43",
1243            "2001-12-14\t21:59:43",
1244        ] {
1245            assert_eq!(
1246                ScalarValue::parse(text).scalar_type(),
1247                ScalarType::Timestamp,
1248                "{text:?}"
1249            );
1250        }
1251        for text in [
1252            "2001-1-5",
1253            "2001-12-5",
1254            "2001-1-15",
1255            "2001-12-14T",
1256            "2001-12-14 123:59:43",
1257            "2001-12-14T21:5:43",
1258            "2001-12-14T21:59:4",
1259            "2001-12-14x21:59:43",
1260            "2001-12-14T24:00:00",
1261            "2001-12-32",
1262        ] {
1263            assert_eq!(
1264                ScalarValue::parse(text).scalar_type(),
1265                ScalarType::String,
1266                "{text:?}"
1267            );
1268        }
1269    }
1270
1271    #[test]
1272    #[cfg(feature = "base64")]
1273    fn test_binary_yaml_output_with_tags() {
1274        let data = b"Binary data here";
1275        let scalar = ScalarValue::binary(data);
1276        let yaml_output = scalar.to_yaml_string();
1277
1278        assert!(yaml_output.starts_with("!!binary "));
1279
1280        // Extract just the base64 part
1281        let base64_part = &yaml_output[9..]; // Remove "!!binary "
1282        let decoded = base64_decode(base64_part).expect("Should decode");
1283        assert_eq!(decoded, data);
1284    }
1285
1286    #[test]
1287    #[cfg(feature = "base64")]
1288    fn test_special_data_types_with_different_styles() {
1289        // Binary with different styles should still include tag
1290        let data = b"test";
1291        let binary_scalar = ScalarValue::binary(data);
1292
1293        // Even if we change style, binary type should maintain tag
1294        let mut styled_binary = binary_scalar;
1295        styled_binary.style = ScalarStyle::DoubleQuoted;
1296
1297        // The to_yaml_string should still respect the scalar type for tagging
1298        assert_eq!(styled_binary.to_yaml_string(), "!!binary \"dGVzdA==\"");
1299    }
1300
1301    #[test]
1302    fn test_type_checking_methods() {
1303        #[cfg(feature = "base64")]
1304        let binary_scalar = ScalarValue::binary(b"test");
1305        let timestamp_scalar = ScalarValue::timestamp("2023-12-25");
1306        let regex_scalar = ScalarValue::regex(r"\d+");
1307        let string_scalar = ScalarValue::string("hello");
1308
1309        // Test type checking methods
1310        #[cfg(feature = "base64")]
1311        assert!(binary_scalar.is_binary());
1312        #[cfg(feature = "base64")]
1313        assert!(!binary_scalar.is_timestamp());
1314        #[cfg(feature = "base64")]
1315        assert!(!binary_scalar.is_regex());
1316
1317        #[cfg(feature = "base64")]
1318        assert!(!timestamp_scalar.is_binary());
1319        assert!(timestamp_scalar.is_timestamp());
1320        assert!(!timestamp_scalar.is_regex());
1321
1322        #[cfg(feature = "base64")]
1323        assert!(!regex_scalar.is_binary());
1324        assert!(!regex_scalar.is_timestamp());
1325        assert!(regex_scalar.is_regex());
1326
1327        #[cfg(feature = "base64")]
1328        assert!(!string_scalar.is_binary());
1329        assert!(!string_scalar.is_timestamp());
1330        assert!(!string_scalar.is_regex());
1331    }
1332
1333    #[test]
1334    fn test_binary_is_not_an_integer() {
1335        // YAML 1.2 has no binary integer form.
1336        for text in ["0b1010", "0B101", "-0b1010", "+0b101"] {
1337            assert_eq!(ScalarValue::parse_integer(text), None, "{text}");
1338            assert_eq!(ScalarValue::auto_detect_type(text), ScalarType::String);
1339        }
1340    }
1341
1342    #[test]
1343    fn test_modern_octal_number_parsing() {
1344        // Test modern octal number parsing (0o prefix)
1345        assert_eq!(ScalarValue::parse_integer("0o755"), Some(493)); // 7*64 + 5*8 + 5
1346        assert_eq!(ScalarValue::parse_integer("0o644"), Some(420)); // 6*64 + 4*8 + 4
1347        assert_eq!(ScalarValue::auto_detect_type("0o755"), ScalarType::Integer);
1348
1349        // The core schema allows neither a sign nor an uppercase prefix.
1350        for text in ["0O777", "-0o755", "+0o644"] {
1351            assert_eq!(ScalarValue::parse_integer(text), None, "{text}");
1352            assert_eq!(ScalarValue::auto_detect_type(text), ScalarType::String);
1353        }
1354
1355        // Test invalid octal
1356        assert_eq!(ScalarValue::parse_integer("0o789"), None); // Contains invalid digit
1357        assert_eq!(ScalarValue::parse_integer("0o"), None); // Empty after prefix
1358    }
1359
1360    #[test]
1361    fn test_leading_zero_is_decimal() {
1362        // YAML 1.2 dropped 1.1's bare-octal form.
1363        assert_eq!(ScalarValue::parse_integer("0755"), Some(755));
1364        assert_eq!(ScalarValue::parse_integer("0644"), Some(644));
1365        assert_eq!(ScalarValue::parse_integer("-0777"), Some(-777));
1366
1367        // Test auto-detection
1368        assert_eq!(ScalarValue::auto_detect_type("0755"), ScalarType::Integer);
1369        assert_eq!(ScalarValue::auto_detect_type("0644"), ScalarType::Integer);
1370
1371        // Test edge cases
1372        assert_eq!(ScalarValue::parse_integer("0"), Some(0)); // Single zero
1373        assert_eq!(ScalarValue::parse_integer("00"), Some(0)); // Double zero
1374
1375        assert_eq!(ScalarValue::parse_integer("0789"), Some(789));
1376        assert_eq!(ScalarValue::parse_integer("08"), Some(8));
1377    }
1378
1379    #[test]
1380    fn test_hexadecimal_number_parsing() {
1381        // Test hexadecimal number parsing (0x prefix) - should still work
1382        assert_eq!(ScalarValue::parse_integer("0xFF"), Some(255));
1383        assert_eq!(ScalarValue::parse_integer("0x1A"), Some(26));
1384        assert_eq!(ScalarValue::parse_integer("0xff"), Some(255));
1385        assert_eq!(ScalarValue::auto_detect_type("0xFF"), ScalarType::Integer);
1386
1387        // The core schema allows neither a sign nor an uppercase prefix.
1388        for text in ["0XFF", "-0xFF", "+0x1A", "+0x1F", "-0o7", "0X1F", "0O7"] {
1389            assert_eq!(ScalarValue::parse_integer(text), None, "{text}");
1390            assert_eq!(ScalarValue::auto_detect_type(text), ScalarType::String);
1391            assert_eq!(
1392                ScalarValue::classify_plain(text),
1393                CoreScalarType::String,
1394                "{text}"
1395            );
1396        }
1397    }
1398
1399    #[test]
1400    fn test_decimal_number_parsing() {
1401        // Test decimal number parsing (no prefix) - should still work
1402        assert_eq!(ScalarValue::parse_integer("42"), Some(42));
1403        assert_eq!(ScalarValue::parse_integer("123"), Some(123));
1404        assert_eq!(ScalarValue::parse_integer("-42"), Some(-42));
1405        assert_eq!(ScalarValue::parse_integer("+123"), Some(123));
1406
1407        // Test auto-detection
1408        assert_eq!(ScalarValue::auto_detect_type("42"), ScalarType::Integer);
1409        assert_eq!(ScalarValue::auto_detect_type("-123"), ScalarType::Integer);
1410    }
1411
1412    #[test]
1413    fn test_number_format_yaml_output() {
1414        // Test that different number formats are properly detected and output
1415        let binary_scalar = ScalarValue::parse("0b1010");
1416        assert_eq!(binary_scalar.scalar_type(), ScalarType::String);
1417        assert_eq!(binary_scalar.to_yaml_string(), "'0b1010'");
1418
1419        let octal_scalar = ScalarValue::parse("0o755");
1420        assert_eq!(octal_scalar.scalar_type(), ScalarType::Integer);
1421        assert_eq!(octal_scalar.value(), "0o755");
1422
1423        let hex_scalar = ScalarValue::parse("0xFF");
1424        assert_eq!(hex_scalar.scalar_type(), ScalarType::Integer);
1425        assert_eq!(hex_scalar.value(), "0xFF");
1426
1427        let legacy_octal_scalar = ScalarValue::parse("0755");
1428        assert_eq!(legacy_octal_scalar.scalar_type(), ScalarType::Integer);
1429        assert_eq!(legacy_octal_scalar.value(), "0755");
1430        assert_eq!(legacy_octal_scalar.to_i64(), Some(755));
1431    }
1432
1433    #[test]
1434    fn test_coerce_to_null() {
1435        // Only the recognized null spellings coerce to Null.
1436        let s = ScalarValue::string("null");
1437        assert_eq!(
1438            s.coerce_to_type(ScalarType::Null).map(|v| v.scalar_type()),
1439            Some(ScalarType::Null)
1440        );
1441        assert!(ScalarValue::string("~")
1442            .coerce_to_type(ScalarType::Null)
1443            .is_some());
1444        assert!(ScalarValue::string("")
1445            .coerce_to_type(ScalarType::Null)
1446            .is_some());
1447        assert!(ScalarValue::string("notnull")
1448            .coerce_to_type(ScalarType::Null)
1449            .is_none());
1450    }
1451
1452    #[test]
1453    fn test_parse_preserves_string_style() {
1454        // The String arm of parse() runs detect_style; a plain word stays Plain.
1455        let plain = ScalarValue::parse("hello");
1456        assert_eq!(plain.scalar_type(), ScalarType::String);
1457        assert_eq!(plain.style, ScalarStyle::Plain);
1458
1459        // A multi-line string is given Literal style by detect_style, which only
1460        // runs in the String arm of parse().
1461        let multiline = ScalarValue::parse("multi line\nvalue");
1462        assert_eq!(multiline.scalar_type(), ScalarType::String);
1463        assert_eq!(multiline.style, ScalarStyle::Literal);
1464    }
1465
1466    #[test]
1467    fn test_from_scalar_type_and_style() {
1468        use crate::yaml::Document;
1469        use rowan::ast::AstNode;
1470        use std::str::FromStr;
1471
1472        // NULL token maps to Null type.
1473        let doc = Document::from_str("k: null\n").unwrap();
1474        let mapping = doc.as_mapping().unwrap();
1475        let null_node = mapping.get("k").unwrap();
1476        let null_scalar = crate::yaml::Scalar::cast(null_node.syntax().clone()).unwrap();
1477        let sv = ScalarValue::from_scalar(&null_scalar);
1478        assert_eq!(sv.scalar_type(), ScalarType::Null);
1479        assert_eq!(sv.style, ScalarStyle::Plain);
1480
1481        // STRING token in double quotes maps to String type, DoubleQuoted style.
1482        let doc = Document::from_str("k: \"hi\"\n").unwrap();
1483        let node = doc.as_mapping().unwrap().get("k").unwrap();
1484        let scalar = crate::yaml::Scalar::cast(node.syntax().clone()).unwrap();
1485        let sv = ScalarValue::from_scalar(&scalar);
1486        assert_eq!(sv.scalar_type(), ScalarType::String);
1487        assert_eq!(sv.style, ScalarStyle::DoubleQuoted);
1488
1489        // Single quoted string keeps SingleQuoted style.
1490        let doc = Document::from_str("k: 'hi'\n").unwrap();
1491        let node = doc.as_mapping().unwrap().get("k").unwrap();
1492        let scalar = crate::yaml::Scalar::cast(node.syntax().clone()).unwrap();
1493        let sv = ScalarValue::from_scalar(&scalar);
1494        assert_eq!(sv.style, ScalarStyle::SingleQuoted);
1495    }
1496
1497    #[test]
1498    fn test_from_scalar_ampersand_in_plain_is_string() {
1499        // `3.1&1` is a single plain scalar with literal content `3.1&1`.
1500        // Per YAML 1.2 core-schema tag resolution it must resolve to
1501        // !!str, not !!float.
1502        use crate::yaml::Document;
1503        use rowan::ast::AstNode;
1504        use std::str::FromStr;
1505
1506        let doc = Document::from_str("k: 3.1&1\n").unwrap();
1507        let node = doc.as_mapping().unwrap().get("k").unwrap();
1508        let scalar = crate::yaml::Scalar::cast(node.syntax().clone()).unwrap();
1509        let sv = ScalarValue::from_scalar(&scalar);
1510        assert_eq!(sv.scalar_type(), ScalarType::String);
1511        assert_eq!(sv.style, ScalarStyle::Plain);
1512    }
1513
1514    #[test]
1515    fn test_from_scalar_signed_yaml_infinity_is_float() {
1516        // `+.INF` is one !!float per YAML 1.2 even though Rust's
1517        // `f64::from_str` wouldn't accept the bare token on its own.
1518        use crate::yaml::Document;
1519        use rowan::ast::AstNode;
1520        use std::str::FromStr;
1521
1522        let doc = Document::from_str("k: +.INF\n").unwrap();
1523        let node = doc.as_mapping().unwrap().get("k").unwrap();
1524        let scalar = crate::yaml::Scalar::cast(node.syntax().clone()).unwrap();
1525        let sv = ScalarValue::from_scalar(&scalar);
1526        assert_eq!(sv.scalar_type(), ScalarType::Float);
1527    }
1528
1529    #[test]
1530    fn test_classify_plain_bare_inf_nan_is_string() {
1531        // Per YAML 1.2 only the dotted spellings are !!float; bare
1532        // `inf`, `nan`, `infinity` in any casing are strings even
1533        // though Rust's `f64::from_str` accepts them.
1534        for input in [
1535            "inf", "Inf", "InF", "INF", "iNf", "nan", "NaN", "NAN", "nAn", "infinity", "Infinity",
1536            "INFINITY", "+inf", "-Inf", "+nan",
1537        ] {
1538            assert_eq!(
1539                ScalarValue::classify_plain(input),
1540                CoreScalarType::String,
1541                "bare {input:?} should classify as String"
1542            );
1543        }
1544        // But the dotted spellings still resolve to Float.
1545        for input in [".inf", ".Inf", ".INF", "+.inf", "-.INF", ".nan"] {
1546            assert_eq!(
1547                ScalarValue::classify_plain(input),
1548                CoreScalarType::Float,
1549                "{input:?} should classify as Float"
1550            );
1551        }
1552    }
1553
1554    #[test]
1555    fn test_canonical_integer() {
1556        for (text, expected) in [
1557            ("0", "0"),
1558            ("-0", "0"),
1559            ("+000", "0"),
1560            ("0x0", "0"),
1561            ("0o0", "0"),
1562            ("42", "42"),
1563            ("+0042", "42"),
1564            ("-42", "-42"),
1565            ("0x2A", "42"),
1566            ("0o52", "42"),
1567            ("-9223372036854775808", "-9223372036854775808"),
1568            ("9223372036854775808", "9223372036854775808"),
1569            ("+9223372036854775808", "9223372036854775808"),
1570            ("0x10000000000000000", "18446744073709551616"),
1571            ("0o2000000000000000000000", "18446744073709551616"),
1572            ("0x1000000000", "68719476736"),
1573            (
1574                "0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF",
1575                "340282366920938463463374607431768211455",
1576            ),
1577            (
1578                "0x100000000000000000000000000000000",
1579                "340282366920938463463374607431768211456",
1580            ),
1581            (
1582                "-000100000000000000000000000000",
1583                "-100000000000000000000000000",
1584            ),
1585        ] {
1586            assert_eq!(
1587                ScalarValue::canonical_integer(text).as_deref(),
1588                Some(expected),
1589                "{text}"
1590            );
1591        }
1592        for text in [
1593            "", "+", "-", "0x", "0o8", "-0x1", "+0o1", "1_000", "1.0", " 1",
1594        ] {
1595            assert_eq!(ScalarValue::canonical_integer(text), None, "{text:?}");
1596        }
1597    }
1598
1599    #[test]
1600    fn test_from_scalar_block_style_is_string() {
1601        // Block scalars (| and >) always resolve to !!str regardless of
1602        // content, and their style must be reported as Literal or Folded.
1603        use crate::yaml::Document;
1604        use rowan::ast::AstNode;
1605        use std::str::FromStr;
1606
1607        let doc = Document::from_str("k: |\n  1\n").unwrap();
1608        let node = doc.as_mapping().unwrap().get("k").unwrap();
1609        let scalar = crate::yaml::Scalar::cast(node.syntax().clone()).unwrap();
1610        let sv = ScalarValue::from_scalar(&scalar);
1611        assert_eq!(sv.style, ScalarStyle::Literal);
1612        assert_eq!(sv.scalar_type(), ScalarType::String);
1613
1614        let doc = Document::from_str("k: >\n  x\n").unwrap();
1615        let node = doc.as_mapping().unwrap().get("k").unwrap();
1616        let scalar = crate::yaml::Scalar::cast(node.syntax().clone()).unwrap();
1617        let sv = ScalarValue::from_scalar(&scalar);
1618        assert_eq!(sv.style, ScalarStyle::Folded);
1619        assert_eq!(sv.scalar_type(), ScalarType::String);
1620    }
1621
1622    #[test]
1623    fn test_needs_quoting_special_chars() {
1624        // Leading-character based quoting and special embedded characters, plus
1625        // each boolean/null keyword spelling (so dropping any one keyword arm is
1626        // detected). Single quotes are preferred unless the value already
1627        // contains one, in which case double quotes are used.
1628        let cases = [
1629            ("&anchor", "'&anchor'"),
1630            ("*alias", "'*alias'"),
1631            ("!tag", "'!tag'"),
1632            ("a|b", "'a|b'"),
1633            ("it's", "\"it's\""),
1634            ("a\"b", "'a\"b'"),
1635            ("50%", "'50%'"),
1636            ("on", "'on'"),
1637            ("off", "'off'"),
1638            ("On", "'On'"),
1639            ("OFF", "'OFF'"),
1640            ("yes", "'yes'"),
1641            ("no", "'no'"),
1642            ("true", "'true'"),
1643            ("false", "'false'"),
1644            ("null", "'null'"),
1645        ];
1646        for (input, expected) in cases {
1647            assert_eq!(ScalarValue::string(input).to_yaml_string(), expected);
1648        }
1649        // A plain word that needs no quoting round-trips unchanged.
1650        assert_eq!(ScalarValue::string("plain").to_yaml_string(), "plain");
1651    }
1652
1653    #[test]
1654    fn test_plain_string_with_control_character_is_double_quoted() {
1655        assert_eq!(ScalarValue::plain("\u{7}").to_yaml_string(), "\"\\a\"");
1656        assert_eq!(ScalarValue::plain("a\rb").to_yaml_string(), "\"a\\rb\"");
1657        assert_eq!(
1658            ScalarValue::plain("a\rb").with_tag("!x").to_yaml_string(),
1659            "!x \"a\\rb\""
1660        );
1661    }
1662
1663    #[test]
1664    fn test_literal_and_folded_nonempty() {
1665        // Literal and folded styles render the block indicator on its own line
1666        // followed by the content indented by two spaces. A folded block
1667        // needs an empty line to keep a line break.
1668        let s = ScalarValue::with_style("line one\nline two", ScalarStyle::Literal);
1669        assert_eq!(s.to_yaml_string(), "|-\n  line one\n  line two");
1670
1671        let s = ScalarValue::with_style("line one\nline two", ScalarStyle::Folded);
1672        assert_eq!(s.to_yaml_string(), ">-\n  line one\n\n  line two");
1673    }
1674
1675    #[test]
1676    fn test_display_matches_yaml_string() {
1677        let s = ScalarValue::string("true");
1678        assert_eq!(format!("{}", s), s.to_yaml_string());
1679        assert_eq!(format!("{}", s), "'true'");
1680    }
1681
1682    #[test]
1683    fn test_as_yaml_token_kind_per_type() {
1684        use crate::lex::SyntaxKind;
1685        use rowan::ast::AstNode;
1686
1687        let cases = [
1688            (ScalarValue::parse("42"), SyntaxKind::INT),
1689            (ScalarValue::parse("3.14"), SyntaxKind::FLOAT),
1690            (ScalarValue::parse("true"), SyntaxKind::BOOL),
1691            (ScalarValue::parse("null"), SyntaxKind::NULL),
1692            (ScalarValue::string("hello"), SyntaxKind::STRING),
1693        ];
1694        for (value, expected) in cases {
1695            let mut builder = rowan::GreenNodeBuilder::new();
1696            crate::AsYaml::build_content(&value, &mut builder, 0, false);
1697            let green = builder.finish();
1698            let node = rowan::SyntaxNode::<crate::yaml::Lang>::new_root(green);
1699            let scalar = crate::yaml::Scalar::cast(node).unwrap();
1700            let kind = scalar
1701                .syntax()
1702                .children_with_tokens()
1703                .filter_map(|c| c.into_token())
1704                .next()
1705                .unwrap()
1706                .kind();
1707            assert_eq!(kind, expected, "value {:?}", value.value());
1708        }
1709    }
1710
1711    #[test]
1712    #[cfg(feature = "regex")]
1713    fn test_as_regex_type_gated() {
1714        // as_regex only compiles when the scalar is tagged as a regex.
1715        let re = ScalarValue::regex(r"\d+");
1716        assert!(re.as_regex().is_some());
1717
1718        let not_re = ScalarValue::string(r"\d+");
1719        assert!(not_re.as_regex().is_none());
1720
1721        // try_as_regex ignores the type and compiles the value directly.
1722        assert!(not_re.try_as_regex().is_ok());
1723        assert!(ScalarValue::string("(").try_as_regex().is_err());
1724    }
1725
1726    #[test]
1727    fn test_from_f64_renders_yaml_float() {
1728        let cases: [(f64, &str); 6] = [
1729            (f64::NAN, ".nan"),
1730            (f64::INFINITY, ".inf"),
1731            (f64::NEG_INFINITY, "-.inf"),
1732            (1.0, "1.0"),
1733            (-0.0, "-0.0"),
1734            (2.5, "2.5"),
1735        ];
1736        for (value, expected) in cases {
1737            let sv = ScalarValue::from(value);
1738            assert_eq!(sv.to_yaml_string(), expected);
1739            assert_eq!(ScalarValue::classify_plain(expected), CoreScalarType::Float);
1740        }
1741        assert_eq!(ScalarValue::from(1.0f32).to_yaml_string(), "1.0");
1742        assert_eq!(ScalarValue::from(f32::NAN).to_yaml_string(), ".nan");
1743    }
1744
1745    #[test]
1746    fn test_to_f64_special_values() {
1747        let float = |s: &str| ScalarValue {
1748            value: s.to_string(),
1749            style: ScalarStyle::Plain,
1750            scalar_type: ScalarType::Float,
1751            tag: None,
1752        };
1753        assert_eq!(float(".inf").to_f64(), Some(f64::INFINITY));
1754        assert_eq!(float("+.INF").to_f64(), Some(f64::INFINITY));
1755        assert_eq!(float("-.Inf").to_f64(), Some(f64::NEG_INFINITY));
1756        assert!(float(".NaN").to_f64().unwrap().is_nan());
1757        assert_eq!(float("inf").to_f64(), None);
1758        assert_eq!(float("1.5").to_f64(), Some(1.5));
1759    }
1760
1761    #[test]
1762    fn test_parse_does_not_infer_binary() {
1763        for text in ["test", "abcd", "noon", "SGVsbG8="] {
1764            let scalar = ScalarValue::parse(text);
1765            assert_eq!(scalar.scalar_type(), ScalarType::String, "{text}");
1766            assert_eq!(scalar.to_yaml_string(), text);
1767        }
1768    }
1769
1770    #[test]
1771    fn test_sign_after_radix_prefix_is_not_integer() {
1772        for text in ["0x-5", "0x+5", "0o-7", "0o+7", "0b-1", "+-5", "-+5"] {
1773            assert_eq!(ScalarValue::parse_integer(text), None, "{text}");
1774            assert_eq!(
1775                ScalarValue::classify_plain(text),
1776                CoreScalarType::String,
1777                "{text}"
1778            );
1779            assert_eq!(
1780                ScalarValue::auto_detect_type(text),
1781                ScalarType::String,
1782                "{text}"
1783            );
1784            let doc: crate::yaml::Document = format!("k: {text}\n").parse().unwrap();
1785            let node = doc.as_mapping().unwrap().get("k").unwrap();
1786            assert_eq!(node.as_scalar().unwrap().as_i64(), None, "{text}");
1787        }
1788    }
1789
1790    #[test]
1791    fn test_integers_outside_i64() {
1792        assert_eq!(
1793            ScalarValue::parse_integer("-9223372036854775808"),
1794            Some(i64::MIN)
1795        );
1796        assert_eq!(ScalarValue::parse_integer("9223372036854775808"), None);
1797        for text in [
1798            "9223372036854775808",
1799            "-9223372036854775808",
1800            "0xFFFFFFFFFFFFFFFF",
1801            "123456789012345678901234567890",
1802        ] {
1803            assert_eq!(
1804                ScalarValue::classify_plain(text),
1805                CoreScalarType::Integer,
1806                "{text}"
1807            );
1808            assert_eq!(
1809                ScalarValue::auto_detect_type(text),
1810                ScalarType::Integer,
1811                "{text}"
1812            );
1813        }
1814        assert_eq!(
1815            ScalarValue::parse("-9223372036854775808").to_i64(),
1816            Some(i64::MIN)
1817        );
1818        assert_eq!(ScalarValue::parse("9223372036854775808").to_i64(), None);
1819    }
1820
1821    #[test]
1822    fn test_coerce_to_special_types() {
1823        let coerce = |value: &str, target: ScalarType| {
1824            ScalarValue::string(value)
1825                .coerce_to_type(target)
1826                .map(|s| (s.scalar_type(), s.value().to_string()))
1827        };
1828        #[cfg(feature = "base64")]
1829        {
1830            assert_eq!(
1831                coerce("aGk=", ScalarType::Binary),
1832                Some((ScalarType::Binary, "aGk=".to_string()))
1833            );
1834            assert_eq!(coerce("!!", ScalarType::Binary), None);
1835            assert_eq!(ScalarValue::string("aGk=").as_binary(), None);
1836            assert_eq!(
1837                ScalarValue::binary(b"hi").as_binary(),
1838                Some(Ok(b"hi".to_vec()))
1839            );
1840        }
1841        assert_eq!(
1842            coerce("2001-12-14", ScalarType::Timestamp),
1843            Some((ScalarType::Timestamp, "2001-12-14".to_string()))
1844        );
1845        assert_eq!(coerce("x", ScalarType::Timestamp), None);
1846        assert_eq!(
1847            coerce("a+", ScalarType::Regex),
1848            Some((ScalarType::Regex, "a+".to_string()))
1849        );
1850        assert_eq!(
1851            coerce("~", ScalarType::Null),
1852            Some((ScalarType::Null, "null".to_string()))
1853        );
1854        assert_eq!(coerce("x", ScalarType::Null), None);
1855    }
1856
1857    #[test]
1858    #[should_panic(expected = "does not fit in one digit")]
1859    fn test_block_indent_indicator_from_first_line_only() {
1860        ScalarValue::literal("\n  x\ny").to_literal_with_indent(0, 10);
1861    }
1862}