yaml_edit/nodes/
scalar_node.rs1use super::{fresh_token, Lang, SyntaxNode};
2use crate::as_yaml::{AsYaml, YamlKind};
3use crate::lex::SyntaxKind;
4use crate::scalar::ScalarValue;
5use crate::yaml::ValueNode;
6use rowan::ast::AstNode;
7use std::fmt;
8
9ast_node!(Scalar, SCALAR, "A YAML scalar value");
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13enum Chomping {
14 Strip,
16 Keep,
18 Clip,
20}
21
22#[derive(Debug, Clone, PartialEq, Eq)]
24pub enum ScalarConversionError {
25 QuotedValue,
27 ParseError(String),
29}
30
31impl fmt::Display for ScalarConversionError {
32 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
33 match self {
34 ScalarConversionError::QuotedValue => {
35 write!(f, "Cannot convert quoted scalar to numeric/boolean type")
36 }
37 ScalarConversionError::ParseError(msg) => {
38 write!(f, "Failed to parse scalar: {msg}")
39 }
40 }
41 }
42}
43
44impl std::error::Error for ScalarConversionError {}
45
46impl Scalar {
47 pub fn value(&self) -> String {
49 self.0.text().to_string()
50 }
51
52 pub fn as_string(&self) -> String {
54 let text = self.value();
55
56 if text.len() >= 2 && text.starts_with('"') && text.ends_with('"') {
58 ScalarValue::parse_escape_sequences(&text[1..text.len() - 1])
60 } else if text.len() >= 2 && text.starts_with('\'') && text.ends_with('\'') {
61 let content = &text[1..text.len() - 1];
63 let unescaped = content.replace("''", "'");
64 if unescaped.contains('\n') {
66 let mut result = String::new();
69 for (i, line) in unescaped.lines().enumerate() {
70 if i > 0 {
71 result.push(' ');
72 }
73 result.push_str(line.trim());
74 }
75 result
76 } else {
77 unescaped
78 }
79 } else if text.starts_with('|') || text.starts_with('>') {
80 Self::parse_block_scalar(&text)
82 } else {
83 if text.contains('\n') {
85 let mut result = String::new();
89 let mut pending_breaks = 0usize;
90 for line in text.lines() {
91 let trimmed = line.trim();
92 if trimmed.is_empty() {
93 if !result.is_empty() {
96 pending_breaks += 1;
97 }
98 continue;
99 }
100 if !result.is_empty() {
101 if pending_breaks == 0 {
102 result.push(' ');
103 } else {
104 for _ in 0..pending_breaks {
105 result.push('\n');
106 }
107 }
108 }
109 pending_breaks = 0;
110 result.push_str(trimmed);
111 }
112 result
113 } else {
114 text
115 }
116 }
117 }
118
119 fn parse_block_scalar(text: &str) -> String {
121 let mut lines = text.lines();
122 let first_line = match lines.next() {
123 Some(line) => line,
124 None => return String::new(),
125 };
126
127 let is_literal = first_line.starts_with('|');
128
129 let header = first_line.trim();
131 let chomping = if header.contains('-') {
132 Chomping::Strip
133 } else if header.contains('+') {
134 Chomping::Keep
135 } else {
136 Chomping::Clip
137 };
138
139 let content_lines: Vec<&str> = lines.collect();
141 if content_lines.is_empty() {
142 return String::new();
143 }
144
145 let explicit_indent = header
151 .chars()
152 .find(|c| c.is_ascii_digit() && *c != '0')
153 .and_then(|c| c.to_digit(10))
154 .map(|d| d as usize);
155 let base_indent = match explicit_indent {
156 Some(explicit) => explicit,
157 None => content_lines
158 .iter()
159 .find(|line| !line.trim().is_empty())
160 .map_or(0, |line| line.chars().take_while(|c| *c == ' ').count()),
161 };
162
163 let trailing_empty_count = content_lines
165 .iter()
166 .rev()
167 .take_while(|line| line.trim().is_empty())
168 .count();
169
170 let mut result = String::new();
172 let mut prev_was_empty = false;
173 let mut prev_was_more_indented = false;
174
175 for (i, line) in content_lines.iter().enumerate() {
176 if line.trim().is_empty() {
177 if is_literal {
179 result.push('\n');
181 } else {
182 if !prev_was_empty && i > 0 {
184 result.push('\n');
186 }
187 }
188 prev_was_empty = true;
189 prev_was_more_indented = false;
190 } else {
191 let leading_spaces = line.chars().take_while(|c| *c == ' ').count();
197 let strip = leading_spaces.min(base_indent);
198 let strip_bytes = line
199 .char_indices()
200 .nth(strip)
201 .map_or(line.len(), |(i, _)| i);
202 let stripped = &line[strip_bytes..];
203
204 if is_literal {
205 result.push_str(stripped);
207 result.push('\n');
208 prev_was_more_indented = false;
209 } else {
210 let line_indent = line.chars().take_while(|c| *c == ' ').count();
212 let is_more_indented = line_indent > base_indent;
213
214 if is_more_indented {
215 if i > 0 && !prev_was_empty && !prev_was_more_indented {
217 result.push('\n');
219 }
220 result.push_str(stripped);
221 result.push('\n');
222 prev_was_more_indented = true;
223 } else {
224 if i > 0 {
226 if prev_was_empty || prev_was_more_indented {
227 result.push_str(stripped);
229 } else {
230 result.push(' ');
232 result.push_str(stripped);
233 }
234 } else {
235 result.push_str(stripped);
237 }
238 prev_was_more_indented = false;
239 }
240 }
241 prev_was_empty = false;
242 }
243 }
244
245 match chomping {
247 Chomping::Strip => {
248 result = result.trim_end_matches('\n').to_string();
250 }
251 Chomping::Clip => {
252 result = result.trim_end_matches('\n').to_string();
254 result.push('\n');
255 }
256 Chomping::Keep => {
257 result = result.trim_end_matches('\n').to_string();
260 let has_content = content_lines.len() > trailing_empty_count;
266 let newlines = trailing_empty_count + usize::from(has_content);
267 for _ in 0..newlines {
268 result.push('\n');
269 }
270 }
271 }
272
273 result
274 }
275
276 pub fn is_quoted(&self) -> bool {
278 let text = self.value();
279 text.len() >= 2
280 && ((text.starts_with('"') && text.ends_with('"'))
281 || (text.starts_with('\'') && text.ends_with('\'')))
282 }
283
284 pub fn unquoted_value(&self) -> String {
292 let text = self.value();
293 if self.is_quoted() {
294 text[1..text.len() - 1].to_string()
295 } else {
296 text
297 }
298 }
299}
300
301impl Scalar {
302 pub fn set_value(&self, value: &str) {
309 let children_count = self.0.children_with_tokens().count();
310 self.0.splice_children(
311 0..children_count,
312 vec![fresh_token(SyntaxKind::STRING, value).into()],
313 );
314 }
315
316 crate::nodes::ast_node_spans!("scalar");
317
318 pub fn as_i64(&self) -> Option<i64> {
323 TryInto::<i64>::try_into(self).ok()
324 }
325
326 pub fn as_f64(&self) -> Option<f64> {
330 TryInto::<f64>::try_into(self).ok()
331 }
332
333 pub fn as_bool(&self) -> Option<bool> {
338 TryInto::<bool>::try_into(self).ok()
339 }
340
341 pub fn is_null(&self) -> bool {
345 if self.is_quoted() {
346 return false;
347 }
348 let val = self.as_string();
349 matches!(val.as_str(), "null" | "Null" | "NULL" | "~" | "")
350 }
351}
352
353impl AsYaml for Scalar {
354 fn as_node(&self) -> Option<&SyntaxNode> {
355 Some(&self.0)
356 }
357
358 fn kind(&self) -> YamlKind {
359 YamlKind::Scalar
360 }
361
362 fn build_content(
363 &self,
364 builder: &mut rowan::GreenNodeBuilder,
365 _indent: usize,
366 _flow_context: bool,
367 ) -> bool {
368 crate::as_yaml::copy_node_content(builder, &self.0);
369 false
371 }
372
373 fn is_inline(&self) -> bool {
374 ValueNode::is_inline(self)
375 }
376}
377
378impl TryFrom<&Scalar> for i64 {
380 type Error = ScalarConversionError;
381
382 fn try_from(scalar: &Scalar) -> Result<Self, Self::Error> {
383 if scalar.is_quoted() {
384 return Err(ScalarConversionError::QuotedValue);
385 }
386
387 let value = scalar.as_string();
388
389 if let Some(hex) = value
391 .strip_prefix("0x")
392 .or_else(|| value.strip_prefix("0X"))
393 {
394 i64::from_str_radix(hex, 16)
395 .map_err(|e| ScalarConversionError::ParseError(e.to_string()))
396 } else if let Some(octal) = value
397 .strip_prefix("0o")
398 .or_else(|| value.strip_prefix("0O"))
399 {
400 i64::from_str_radix(octal, 8)
401 .map_err(|e| ScalarConversionError::ParseError(e.to_string()))
402 } else if let Some(binary) = value
403 .strip_prefix("0b")
404 .or_else(|| value.strip_prefix("0B"))
405 {
406 i64::from_str_radix(binary, 2)
407 .map_err(|e| ScalarConversionError::ParseError(e.to_string()))
408 } else {
409 value
410 .parse::<i64>()
411 .map_err(|e| ScalarConversionError::ParseError(e.to_string()))
412 }
413 }
414}
415
416impl TryFrom<&Scalar> for f64 {
417 type Error = ScalarConversionError;
418
419 fn try_from(scalar: &Scalar) -> Result<Self, Self::Error> {
420 if scalar.is_quoted() {
421 return Err(ScalarConversionError::QuotedValue);
422 }
423
424 let value = scalar.as_string();
425
426 match value.as_str() {
428 ".inf" | ".Inf" | ".INF" | "+.inf" | "+.Inf" | "+.INF" => Ok(f64::INFINITY),
429 "-.inf" | "-.Inf" | "-.INF" => Ok(f64::NEG_INFINITY),
430 ".nan" | ".NaN" | ".NAN" => Ok(f64::NAN),
431 _ => value
432 .parse::<f64>()
433 .map_err(|e| ScalarConversionError::ParseError(e.to_string())),
434 }
435 }
436}
437
438impl TryFrom<&Scalar> for bool {
439 type Error = ScalarConversionError;
440
441 fn try_from(scalar: &Scalar) -> Result<Self, Self::Error> {
442 if scalar.is_quoted() {
443 return Err(ScalarConversionError::QuotedValue);
444 }
445
446 let value = scalar.as_string();
447
448 match value.as_str() {
450 "true" | "True" | "TRUE" => Ok(true),
451 "false" | "False" | "FALSE" => Ok(false),
452 "yes" | "Yes" | "YES" | "on" | "On" | "ON" => Ok(true),
454 "no" | "No" | "NO" | "off" | "Off" | "OFF" => Ok(false),
455 _ => Err(ScalarConversionError::ParseError(format!(
456 "'{value}' is not a recognized boolean value"
457 ))),
458 }
459 }
460}
461
462#[cfg(test)]
463mod tests {
464 use crate::Document;
465 use std::str::FromStr;
466
467 #[test]
468 fn test_unterminated_quote_as_string_does_not_panic() {
469 let (doc, errors) = Document::from_str_relaxed("\"");
470 assert!(!errors.is_empty());
471 let scalar = doc.as_scalar().expect("relaxed parse keeps a scalar");
472 assert_eq!(scalar.as_string(), "\"");
473 assert_eq!(scalar.unquoted_value(), "\"");
474
475 let (doc, _) = Document::from_str_relaxed("'");
476 let scalar = doc.as_scalar().expect("relaxed parse keeps a scalar");
477 assert_eq!(scalar.as_string(), "'");
478 assert_eq!(scalar.unquoted_value(), "'");
479 }
480
481 #[test]
482 fn test_json_array_quoted_strings_cst_structure() {
483 let json = r#"{
490 "items": [
491 "first",
492 "second"
493 ]
494}"#;
495
496 let doc = Document::from_str(json).unwrap();
497 let mapping = doc.as_mapping().unwrap();
498 let items = mapping.get("items").unwrap();
499 let sequence = items.as_sequence().unwrap();
500
501 let values: Vec<_> = sequence
503 .values()
504 .filter_map(|node| {
505 if let crate::YamlNode::Scalar(scalar) = node {
506 Some(scalar)
507 } else {
508 None
509 }
510 })
511 .collect();
512
513 assert_eq!(values.len(), 2);
514
515 assert_eq!(
517 values[0].value(),
518 r#""first""#,
519 "first item should not have trailing whitespace"
520 );
521 assert_eq!(
522 values[1].value(),
523 r#""second""#,
524 "second item should not have trailing whitespace"
525 );
526
527 assert_eq!(values[0].as_string(), "first");
529 assert_eq!(values[1].as_string(), "second");
530 }
531
532 #[test]
533 fn test_compact_json_array() {
534 let json = r#"{"items": ["first", "second"]}"#;
536
537 let doc = Document::from_str(json).unwrap();
538 let mapping = doc.as_mapping().unwrap();
539 let items = mapping.get("items").unwrap();
540 let sequence = items.as_sequence().unwrap();
541
542 let values: Vec<_> = sequence
543 .values()
544 .filter_map(|node| {
545 if let crate::YamlNode::Scalar(scalar) = node {
546 Some(scalar)
547 } else {
548 None
549 }
550 })
551 .collect();
552
553 assert_eq!(values.len(), 2);
554 assert_eq!(values[0].value(), r#""first""#);
555 assert_eq!(values[1].value(), r#""second""#);
556 assert_eq!(values[0].as_string(), "first");
557 assert_eq!(values[1].as_string(), "second");
558 }
559
560 #[test]
561 fn test_yaml_flow_arrays_quoted_strings() {
562 let yaml = r#"
564items: ["first", "second"]
565"#;
566
567 let doc = Document::from_str(yaml).unwrap();
568 let mapping = doc.as_mapping().unwrap();
569 let items = mapping.get("items").unwrap();
570 let sequence = items.as_sequence().unwrap();
571
572 let values: Vec<_> = sequence
573 .values()
574 .filter_map(|node| {
575 if let crate::YamlNode::Scalar(scalar) = node {
576 Some(scalar)
577 } else {
578 None
579 }
580 })
581 .collect();
582
583 assert_eq!(values.len(), 2);
584 assert_eq!(values[0].value(), r#""first""#);
585 assert_eq!(values[1].value(), r#""second""#);
586 assert_eq!(values[0].as_string(), "first");
587 assert_eq!(values[1].as_string(), "second");
588 }
589
590 #[test]
591 fn test_parse_block_scalar_multibyte_after_dedent() {
592 let yaml = ">\n a\n\u{4f1}b\n";
596 let _ = super::Scalar::parse_block_scalar(yaml);
598 }
599}