1use 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::borrow::Cow;
8use std::fmt;
9
10ast_node!(Scalar, SCALAR, "A YAML scalar value");
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub(crate) enum Chomping {
15 Strip,
17 Keep,
19 Clip,
21}
22
23#[derive(Debug, Clone, PartialEq, Eq)]
25pub enum ScalarConversionError {
26 QuotedValue,
28 ParseError(String),
30}
31
32impl fmt::Display for ScalarConversionError {
33 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
34 match self {
35 ScalarConversionError::QuotedValue => {
36 write!(f, "Cannot convert quoted scalar to numeric/boolean type")
37 }
38 ScalarConversionError::ParseError(msg) => {
39 write!(f, "Failed to parse scalar: {msg}")
40 }
41 }
42 }
43}
44
45impl std::error::Error for ScalarConversionError {}
46
47fn header_indicators(header: &str) -> (Chomping, Option<usize>) {
52 let indicators: String = header
53 .chars()
54 .skip(1)
55 .take_while(|c| c.is_ascii_digit() || *c == '+' || *c == '-')
56 .collect();
57 let chomping = if indicators.contains('-') {
58 Chomping::Strip
59 } else if indicators.contains('+') {
60 Chomping::Keep
61 } else {
62 Chomping::Clip
63 };
64 let indent = indicators
65 .chars()
66 .find(|c| c.is_ascii_digit() && *c != '0')
67 .and_then(|c| c.to_digit(10))
68 .map(|d| d as usize);
69 (chomping, indent)
70}
71
72pub(crate) fn block_scalar_indicators(scalar: &SyntaxNode) -> (Chomping, Option<usize>) {
75 let header: String = scalar
76 .children_with_tokens()
77 .filter_map(|c| c.into_token())
78 .take_while(|t| t.kind() != SyntaxKind::NEWLINE)
79 .map(|t| t.text().to_string())
80 .collect();
81 header_indicators(&header)
82}
83
84pub(crate) fn block_parent_column(scalar: &SyntaxNode) -> usize {
88 scalar
89 .ancestors()
90 .find(|n| {
91 matches!(
92 n.kind(),
93 SyntaxKind::MAPPING_ENTRY | SyntaxKind::SEQUENCE_ENTRY
94 )
95 })
96 .map_or(0, |entry| crate::column::node_column(&entry))
97}
98
99fn normalize_line_breaks(text: &str) -> Cow<'_, str> {
102 if text.contains('\r') {
103 Cow::Owned(text.replace("\r\n", "\n").replace('\r', "\n"))
104 } else {
105 Cow::Borrowed(text)
106 }
107}
108
109impl Scalar {
110 pub fn value(&self) -> String {
112 self.0.text().to_string()
113 }
114
115 pub fn as_string(&self) -> String {
117 let text = self.value();
118
119 if text.len() >= 2 && text.starts_with('"') && text.ends_with('"') {
121 ScalarValue::parse_escape_sequences(&text[1..text.len() - 1])
123 } else if text.len() >= 2 && text.starts_with('\'') && text.ends_with('\'') {
124 let content = &text[1..text.len() - 1];
126 let unescaped = content.replace("''", "'");
127 if unescaped.contains(['\n', '\r']) {
129 let unescaped = normalize_line_breaks(&unescaped);
132 let lines: Vec<&str> = unescaped.split('\n').collect();
133 let last = lines.len() - 1;
134 let mut result = lines[0].trim_end_matches([' ', '\t']).to_string();
135 let mut empty_lines = 0;
136 for (i, line) in lines.iter().enumerate().skip(1) {
137 let line = line.trim_start_matches([' ', '\t']);
138 let line = if i == last {
139 line
140 } else {
141 line.trim_end_matches([' ', '\t'])
142 };
143 if line.is_empty() && i != last {
144 empty_lines += 1;
145 continue;
146 }
147 match empty_lines {
148 0 => result.push(' '),
149 n => result.push_str(&"\n".repeat(n)),
150 }
151 empty_lines = 0;
152 result.push_str(line);
153 }
154 result
155 } else {
156 unescaped
157 }
158 } else if self.is_block() {
159 Self::parse_block_scalar(&text, block_parent_column(&self.0))
160 } else {
161 if text.contains(['\n', '\r']) {
163 let mut result = String::new();
167 let mut pending_breaks = 0usize;
168 for line in normalize_line_breaks(&text).lines() {
169 let trimmed = crate::scalar::trim_white(line);
170 if trimmed.is_empty() {
171 if !result.is_empty() {
174 pending_breaks += 1;
175 }
176 continue;
177 }
178 if !result.is_empty() {
179 if pending_breaks == 0 {
180 result.push(' ');
181 } else {
182 for _ in 0..pending_breaks {
183 result.push('\n');
184 }
185 }
186 }
187 pending_breaks = 0;
188 result.push_str(trimmed);
189 }
190 result
191 } else {
192 text
193 }
194 }
195 }
196
197 fn parse_block_scalar(text: &str, parent_column: usize) -> String {
202 let text = normalize_line_breaks(text);
203 let mut lines = text.lines();
204 let first_line = match lines.next() {
205 Some(line) => line,
206 None => return String::new(),
207 };
208
209 let is_literal = first_line.starts_with('|');
210 let (chomping, explicit_indent) = header_indicators(first_line);
211
212 let content_lines: Vec<&str> = lines.collect();
214 if content_lines.is_empty() {
215 return String::new();
216 }
217
218 let base_indent = match explicit_indent {
224 Some(explicit) => parent_column + explicit,
225 None => content_lines
229 .iter()
230 .find(|line| !line.trim_start_matches(' ').is_empty())
231 .map_or(usize::MAX, |line| crate::column::space_indent(line)),
232 };
233
234 let mut result = String::new();
237 let mut empty_lines = 0;
238 let mut prev_spaced: Option<bool> = None;
240
241 for line in &content_lines {
242 let leading_spaces = crate::column::space_indent(line);
247 let strip = leading_spaces.min(base_indent);
248 let strip_bytes = line
249 .char_indices()
250 .nth(strip)
251 .map_or(line.len(), |(i, _)| i);
252 let stripped = &line[strip_bytes..];
253 let less_indented = leading_spaces < base_indent;
256 if stripped.is_empty()
257 || (less_indented && stripped.trim_start_matches([' ', '\t']).is_empty())
258 {
259 empty_lines += 1;
260 continue;
261 }
262
263 let spaced = stripped.starts_with([' ', '\t']);
267 let breaks = match prev_spaced {
268 Some(prev) if is_literal || prev || spaced => empty_lines + 1,
269 Some(_) if empty_lines == 0 => {
270 result.push(' ');
271 0
272 }
273 _ => empty_lines,
274 };
275 result.push_str(&"\n".repeat(breaks));
276 result.push_str(stripped);
277 prev_spaced = Some(spaced);
278 empty_lines = 0;
279 }
280
281 let has_content = prev_spaced.is_some();
283 match chomping {
284 Chomping::Strip => {}
285 Chomping::Clip => {
286 if has_content {
287 result.push('\n');
288 }
289 }
290 Chomping::Keep => {
291 let newlines = empty_lines + usize::from(has_content);
297 result.push_str(&"\n".repeat(newlines));
298 }
299 }
300
301 result
302 }
303
304 pub fn is_quoted(&self) -> bool {
306 let text = self.value();
307 text.len() >= 2
308 && ((text.starts_with('"') && text.ends_with('"'))
309 || (text.starts_with('\'') && text.ends_with('\'')))
310 }
311
312 pub fn unquoted_value(&self) -> String {
320 let text = self.value();
321 if self.is_quoted() {
322 text[1..text.len() - 1].to_string()
323 } else {
324 text
325 }
326 }
327}
328
329impl Scalar {
330 pub fn set_value(&self, value: &str) {
337 let children: Vec<_> = self.0.children_with_tokens().collect();
338 let mut tail = Vec::new();
341 if crate::nodes::is_block_scalar(&self.0) {
342 let is_layout = |k| {
343 matches!(
344 k,
345 SyntaxKind::NEWLINE | SyntaxKind::INDENT | SyntaxKind::WHITESPACE
346 )
347 };
348 let content_end = children
349 .iter()
350 .rposition(|c| !is_layout(c.kind()))
351 .map_or(0, |i| i + 1);
352 tail = children[content_end..]
353 .iter()
354 .skip_while(|c| c.kind() != SyntaxKind::NEWLINE)
355 .filter_map(|c| c.as_token().cloned())
356 .collect();
357 }
358 crate::nodes::replace_children(
359 &self.0,
360 0..children.len(),
361 vec![fresh_token(SyntaxKind::STRING, value).into()],
362 );
363 if let Some((line_break, blank_lines)) = tail.split_first() {
364 crate::nodes::trivia::end_former_block_scalar(&self.0, line_break, blank_lines);
365 }
366 }
367
368 crate::nodes::ast_node_spans!("scalar");
369
370 pub fn as_i64(&self) -> Option<i64> {
375 TryInto::<i64>::try_into(self).ok()
376 }
377
378 pub fn as_f64(&self) -> Option<f64> {
383 TryInto::<f64>::try_into(self).ok()
384 }
385
386 pub fn as_bool(&self) -> Option<bool> {
391 TryInto::<bool>::try_into(self).ok()
392 }
393
394 pub fn is_block(&self) -> bool {
399 crate::nodes::is_block_scalar(&self.0)
400 }
401
402 pub fn is_null(&self) -> bool {
407 if self.is_quoted() || self.is_block() {
408 return false;
409 }
410 let val = self.as_string();
411 matches!(val.as_str(), "null" | "Null" | "NULL" | "~" | "")
412 }
413
414 pub(crate) fn needs_flow_quotes(&self) -> bool {
417 !self.is_quoted() && !self.is_block() && self.value().contains([',', '[', ']', '{', '}'])
418 }
419}
420
421impl AsYaml for Scalar {
422 fn as_node(&self) -> Option<&SyntaxNode> {
423 Some(&self.0)
424 }
425
426 fn kind(&self) -> YamlKind {
427 YamlKind::Scalar
428 }
429
430 fn build_content(
437 &self,
438 builder: &mut rowan::GreenNodeBuilder,
439 indent: usize,
440 flow_context: bool,
441 ) -> bool {
442 if flow_context && self.needs_flow_quotes() {
443 return ScalarValue::double_quoted(self.as_string())
444 .build_content(builder, indent, true);
445 }
446 if !ValueNode::is_inline(self) {
447 crate::yaml::copy_node_to_builder(builder, &self.0);
448 return false;
449 }
450 let delta = indent as isize - crate::column::line_indent(&self.0) as isize;
451 let delta = crate::as_yaml::clear_parent_column(&self.0, indent, delta);
452 builder.start_node(SyntaxKind::SCALAR.into());
453 copy_shifting_continuation_lines(builder, &self.0, delta, false);
454 builder.finish_node();
455 false
457 }
458
459 fn is_inline(&self) -> bool {
460 ValueNode::is_inline(self)
461 }
462}
463
464fn copy_shifting_continuation_lines(
469 builder: &mut rowan::GreenNodeBuilder,
470 node: &SyntaxNode,
471 delta: isize,
472 mut after_newline: bool,
473) -> bool {
474 for child in node.children_with_tokens() {
475 let token = match child {
476 rowan::NodeOrToken::Node(n) => {
477 builder.start_node(n.kind().into());
478 after_newline = copy_shifting_continuation_lines(builder, &n, delta, after_newline);
479 builder.finish_node();
480 continue;
481 }
482 rowan::NodeOrToken::Token(t) => t,
483 };
484 match token.kind() {
485 SyntaxKind::NEWLINE => {
486 builder.token(token.kind().into(), token.text());
487 after_newline = true;
488 continue;
489 }
490 SyntaxKind::INDENT | SyntaxKind::WHITESPACE if after_newline => {
491 let shifted = crate::as_yaml::shift_line_start(token.text(), delta);
492 if !shifted.is_empty() {
493 builder.token(SyntaxKind::INDENT.into(), &shifted);
494 }
495 }
496 kind => {
497 if after_newline && delta > 0 {
498 builder.token(SyntaxKind::INDENT.into(), &" ".repeat(delta as usize));
499 }
500 builder.token(
501 kind.into(),
502 &crate::as_yaml::shift_inner_lines(token.text(), delta),
503 );
504 }
505 }
506 after_newline = false;
507 }
508 after_newline
509}
510
511fn typed_value(scalar: &Scalar) -> Result<ScalarValue, ScalarConversionError> {
515 if scalar.is_quoted() {
516 return Err(ScalarConversionError::QuotedValue);
517 }
518 Ok(ScalarValue::from_scalar(scalar))
519}
520
521impl TryFrom<&Scalar> for i64 {
522 type Error = ScalarConversionError;
523
524 fn try_from(scalar: &Scalar) -> Result<Self, Self::Error> {
525 let value = typed_value(scalar)?;
526 value.to_i64().ok_or_else(|| {
527 ScalarConversionError::ParseError(format!(
528 "'{}' is not an integer that fits in an i64",
529 value.value()
530 ))
531 })
532 }
533}
534
535impl TryFrom<&Scalar> for f64 {
536 type Error = ScalarConversionError;
537
538 fn try_from(scalar: &Scalar) -> Result<Self, Self::Error> {
539 let value = typed_value(scalar)?;
540 value
541 .to_f64()
542 .or_else(|| value.to_i64().map(|i| i as f64))
543 .ok_or_else(|| {
544 ScalarConversionError::ParseError(format!("'{}' is not a float", value.value()))
545 })
546 }
547}
548
549impl TryFrom<&Scalar> for bool {
550 type Error = ScalarConversionError;
551
552 fn try_from(scalar: &Scalar) -> Result<Self, Self::Error> {
553 let value = typed_value(scalar)?;
554 value.to_bool().ok_or_else(|| {
555 ScalarConversionError::ParseError(format!(
556 "'{}' is not a recognized boolean value",
557 value.value()
558 ))
559 })
560 }
561}
562
563#[cfg(test)]
564mod tests {
565 #[test]
566 fn test_parse_block_scalar_multibyte_after_dedent() {
567 let yaml = ">\n a\n\u{4f1}b\n";
571 let _ = super::Scalar::parse_block_scalar(yaml, 0);
573 }
574}