use crate::parser::tokens::mapping::parse_mapping_with_tokens;
macro_rules! parse_err {
($msg:expr) => {
YamlError::from($msg)
};
($stream:expr, $msg:expr) => {
YamlError::from(helpers::parse_error_token($stream, $msg))
};
}
use crate::error::YamlError;
use crate::io::traits::ISource;
use crate::nodes::node::Node;
use crate::parser::ParseResult;
use crate::parser::directives::DirectiveContext;
use crate::parser::document::explicit_key::parse_multiple_explicit_keys;
use crate::parser::document::indentation::{
ensure_indent_at_least, ensure_indent_at_least_no_source,
};
use crate::parser::document::mapping::parse_mapping;
use crate::parser::document::value::parse_value;
use crate::parser::utils::context::{CollectionType, ParsingContext};
use crate::parser::utils::helpers;
use crate::parser::utils::helpers::{BlockHeadKind, DocMarkerKind, classify_doc_marker};
fn parse_scalar_or_value(
source: &mut dyn ISource,
directives: &DirectiveContext,
indent_level: usize,
ctx: &ParsingContext,
) -> ParseResult<Node> {
if matches!(ctx.collection_type, CollectionType::None)
&& matches!(source.current(), Some(c) if c.is_alphanumeric())
{
parse_plain_multiline_scalar(source, indent_level)
} else {
parse_value(source, directives)
}
}
fn line_looks_like_mapping_entry(line: &str) -> bool {
line.contains(": ") || line.ends_with(':')
}
fn parse_plain_multiline_scalar(source: &mut dyn ISource, base_indent: usize) -> ParseResult<Node> {
let mut paragraphs: Vec<Vec<String>> = Vec::new();
let mut current_paragraph: Vec<String> = Vec::new();
let mut is_first_line = true;
let mut first_line_is_plain_scalar = false;
let mut first_line_indent: usize = 0;
loop {
if !source.more() {
break;
}
let line_start_state = source.save_state();
let line_indent = source.get_current_indent_level();
let had_inline_comment = {
let st = source.save_state();
let raw_s = crate::utils::collect_until(source, |c| c == '\n' || c == '\r');
let has_comment = raw_s.contains(" #") || raw_s.contains("\t#");
source.restore_state(st);
has_comment
};
let line = crate::utils::read_line_trimmed_into_string(source);
if (line == "..." || line == "---") && line_indent == base_indent {
source.restore_state(line_start_state);
break;
}
if is_first_line && !line.is_empty() {
first_line_is_plain_scalar = !line_looks_like_mapping_entry(&line);
first_line_indent = line_indent;
}
if !is_first_line
&& !line.is_empty()
&& first_line_is_plain_scalar
&& line_indent >= first_line_indent
&& line_looks_like_mapping_entry(&line)
{
return Err(crate::error::YamlError::from(
"Invalid mapping entry in plain multiline scalar: \
continuation line contains a mapping value indicator (\": \") \
which is forbidden in plain block scalars",
));
}
match source.current() {
Some('\r') => {
source.next();
if matches!(source.current(), Some('\n')) {
source.next();
}
}
Some('\n') => {
source.next();
}
_ => {}
}
if line.is_empty() {
if !current_paragraph.is_empty() {
paragraphs.push(current_paragraph);
current_paragraph = Vec::new();
}
} else {
current_paragraph.push(line.clone());
}
is_first_line = false;
if had_inline_comment && !line.is_empty() {
break;
}
}
if !current_paragraph.is_empty() {
paragraphs.push(current_paragraph);
}
let mut parts: Vec<String> = Vec::new();
for para in paragraphs {
parts.push(para.join(" "));
}
let combined = parts.join("\n");
Ok(Node::Str(
combined,
crate::nodes::node::QuoteType::Unquoted,
crate::nodes::node::BlockStyle::None,
))
}
fn token_dispatch(
source: &mut dyn ISource,
directives: &DirectiveContext,
ctx: &ParsingContext,
) -> Option<ParseResult<Node>> {
let st = source.save_state();
if let Ok(ts) = crate::parser::token_stream::TokenStream::new(source, directives, false) {
match ts.current() {
Some(crate::parser::lexer::Token::Indent(lvl)) => {
let level_val = *lvl;
source.restore_state(st);
let mut stream =
crate::parser::token_stream::TokenStream::new(source, directives, false)
.ok()?;
let result = parse_mapping_with_tokens(&mut stream, level_val, directives, 0);
return Some(result.map_err(YamlError::from));
}
Some(crate::parser::lexer::Token::Dash) => {
if ctx.in_flow || matches!(ctx.collection_type, CollectionType::BlockMapping) {
source.restore_state(st);
return None;
}
source.restore_state(st);
let seq_indent = source.get_current_indent_level();
let mut stream =
crate::parser::token_stream::TokenStream::new(source, directives, false)
.ok()?;
let result = crate::parser::tokens::sequence::parse_block_sequence_at(
&mut stream,
seq_indent,
ctx.indent_level,
directives,
0,
);
return Some(result.map_err(YamlError::from));
}
_ => {
source.restore_state(st);
}
}
} else {
source.restore_state(st);
}
None
}
fn is_doc_end(source: &mut dyn ISource, directives: &DirectiveContext) -> ParseResult<bool> {
let st = source.save_state();
let ts = crate::parser::token_stream::TokenStream::new(source, directives, false)?;
let res = matches!(classify_doc_marker(&ts), Some(DocMarkerKind::End));
source.restore_state(st);
Ok(res)
}
fn handle_multiple_explicit_keys(
source: &mut dyn ISource,
current_indent: usize,
) -> ParseResult<Node> {
Ok(parse_multiple_explicit_keys(source, current_indent).map_err(YamlError::from)?)
}
fn skip_trivia_with_ctx(
source: &mut dyn ISource,
ctx: &ParsingContext,
) -> crate::parser::ParseResult<()> {
if !ctx.in_flow {
match crate::utils::skip_whitespace_and_comments_validate_tabs(source) {
Ok(()) => Ok(()),
Err(_e) => Err(crate::parser::errors::indentation_errors::IndentationErrors::tabs_not_allowed_yaml_block(source)),
}
} else {
crate::utils::skip_whitespace_and_comments(source);
Ok(())
}
}
pub fn parse_document_contents(
source: &mut dyn ISource,
indent_level: usize,
directives: &DirectiveContext,
ctx: &ParsingContext,
) -> ParseResult<Node> {
skip_trivia_with_ctx(source, ctx)?;
crate::parser::utils::helpers::validate_indentation_and_whitespace(source, directives, ctx)?;
let head_kind = helpers::classify_block_head(source, directives, ctx);
if matches!(source.current(), Some('?')) {
let current_indent = source.get_current_indent_level();
return handle_multiple_explicit_keys(source, current_indent);
}
if matches!(
head_kind,
BlockHeadKind::BlockMapping | BlockHeadKind::BlockSequence
) {
if let Some(result) = token_dispatch(source, directives, ctx) {
return result;
}
}
match source.current() {
Some(c) if c == crate::constants::CHAR_DASH => {
if ctx.in_flow || matches!(ctx.collection_type, CollectionType::BlockMapping) {
return parse_scalar_or_value(source, directives, indent_level, ctx);
}
let seq_indent = source.get_current_indent_level();
ensure_indent_at_least_no_source(seq_indent, indent_level, "Sequence item")?;
let mut stream =
crate::parser::token_stream::TokenStream::new(source, directives, false)?;
match stream.current() {
Some(crate::parser::lexer::Token::DocumentStart) => {
return Ok(Node::None);
}
Some(crate::parser::lexer::Token::Dash) => {
let seq = crate::parser::tokens::sequence::parse_block_sequence_at(
&mut stream,
seq_indent,
indent_level,
directives,
0,
)?;
if let Node::None = seq {
return Ok(Node::None);
}
Ok(seq)
}
_ => parse_scalar_or_value(source, directives, indent_level, ctx),
}
}
Some(c) if c == crate::constants::CHAR_DOT => {
let map_indent = source.get_current_indent_level();
if is_doc_end(source, directives)? {
return Ok(Node::None);
}
ensure_indent_at_least(source, map_indent, indent_level, "Mapping key")?;
if matches!(head_kind, BlockHeadKind::BlockMapping) {
Ok(parse_mapping(source, map_indent, directives)?)
} else {
parse_scalar_or_value(source, directives, indent_level, ctx)
}
}
Some(c) if c == crate::constants::CHAR_HASH => {
skip_trivia_with_ctx(source, ctx)?;
parse_document_contents(source, indent_level, directives, ctx)
}
Some(c) if c == crate::constants::CHAR_LBRACE => {
let mut stream =
crate::parser::token_stream::TokenStream::new(source, directives, true)?;
Ok(
crate::parser::document::inline_tokens::parse_inline_mapping_with_tokens(
&mut stream,
directives,
0,
false,
None,
)?,
)
}
Some(c) if c == crate::constants::CHAR_LBRACKET => {
let mut stream =
crate::parser::token_stream::TokenStream::new(source, directives, true)?;
let start_line = stream.source_line();
let result = crate::parser::document::inline_tokens::parse_inline_sequence_with_tokens(
&mut stream,
directives,
0,
None,
)?;
if stream.source_line() > start_line {
if matches!(stream.current(), Some(crate::parser::lexer::Token::Colon)) {
return Err(
"Parse error: implicit block mapping key spans multiple lines \
(multiline flow collection cannot be an implicit block mapping key)"
.to_string()
.into(),
);
}
}
{
let _ = stream.skip_trivia();
use crate::parser::lexer::Token;
let is_invalid = matches!(
stream.current(),
Some(Token::Plain(_))
| Some(Token::DoubleQuoted(..))
| Some(Token::SingleQuoted(_))
| Some(Token::Anchor(_))
| Some(Token::Tag(_))
| Some(Token::FlowMappingStart)
| Some(Token::FlowSequenceStart)
| Some(Token::QuestionMark)
);
if is_invalid {
return Err(crate::parser::utils::error_builder::syntax_error(
stream.source_mut(),
"Invalid item after end of flow sequence: \
unexpected content after closing ']'",
));
}
}
Ok(result)
}
Some(c) if c == crate::constants::CHAR_EXCLAMATION => {
let mut stream =
crate::parser::token_stream::TokenStream::new(source, directives, false)?;
match stream.current() {
Some(crate::parser::lexer::Token::Tag(_)) => {
let is_mapping_key = match stream.peek() {
Ok(Some(crate::parser::lexer::Token::Colon)) => true,
_ => false,
};
if is_mapping_key {
Ok(parse_mapping(source, indent_level, directives)?)
} else {
Ok(crate::parser::tokens::value::parse_value_with_tokens(
&mut stream,
directives,
0,
)?)
}
}
_ => Ok(crate::parser::tokens::value::parse_value_with_tokens(
&mut stream,
directives,
0,
)?),
}
}
Some(c) if c == '&' => {
let mut stream =
crate::parser::token_stream::TokenStream::new(source, directives, false)?;
match stream.current() {
Some(crate::parser::lexer::Token::Anchor(_)) => {
stream.next()?;
match stream.current() {
Some(crate::parser::lexer::Token::Colon) => {
Ok(parse_mapping(source, indent_level, directives)?)
}
Some(crate::parser::lexer::Token::Dash) => {
let mut ts_err = crate::parser::token_stream::TokenStream::new(
source, directives, false,
)?;
Err(crate::parser::errors::anchor_errors::AnchorErrors::anchor_cannot_precede_dash_same_line(&mut ts_err))
}
Some(crate::parser::lexer::Token::Plain(s)) => {
if s.trim_start().starts_with('-') {
let mut ts_err = crate::parser::token_stream::TokenStream::new(
source, directives, false,
)?;
Err(crate::parser::errors::anchor_errors::AnchorErrors::anchor_cannot_precede_dash_same_line(&mut ts_err))
} else {
Ok(crate::parser::tokens::value::parse_value_with_tokens(
&mut stream,
directives,
0,
)?)
}
}
_ => Ok(crate::parser::tokens::value::parse_value_with_tokens(
&mut stream,
directives,
0,
)?),
}
}
_ => Ok(crate::parser::tokens::value::parse_value_with_tokens(
&mut stream,
directives,
0,
)?),
}
}
Some(c) if c == '*' => parse_scalar_or_value(source, directives, indent_level, ctx),
Some(c) if c == ':' => {
let current_indent = source.get_current_indent_level();
let mut pairs: Vec<(Node, Node)> = Vec::new();
loop {
if source.get_current_indent_level() != current_indent
|| source.current() != Some(':')
{
break;
}
source.next();
if source.current() == Some('\t') {
let stream =
crate::parser::token_stream::TokenStream::new(source, directives, false)?;
return Err(parse_err!(
&stream,
"Tabs cannot be used as separation after explicit value indicator"
));
}
crate::utils::skip_whitespace_and_comments(source);
let value_node = {
let mut stream =
crate::parser::token_stream::TokenStream::new(source, directives, false)?;
match stream.current() {
Some(crate::parser::lexer::Token::Newline)
| Some(crate::parser::lexer::Token::DocumentStart)
| Some(crate::parser::lexer::Token::DocumentEnd)
| None => Node::None,
_ => crate::parser::tokens::value::parse_value_with_tokens(
&mut stream,
directives,
0,
)?,
}
};
pairs.push((Node::None, value_node));
let at_doc_marker = {
let st = source.save_state();
let c0 = source.current();
let c1 = if c0.is_some() {
source.next();
source.current()
} else {
None
};
let c2 = if c1.is_some() {
source.next();
source.current()
} else {
None
};
let c3 = if c2.is_some() {
source.next();
source.current()
} else {
None
};
source.restore_state(st);
let is_triple_dash = c0 == Some('-') && c1 == Some('-') && c2 == Some('-');
let is_triple_dot = c0 == Some('.') && c1 == Some('.') && c2 == Some('.');
let sep_ok = c3.map_or(true, |c| {
crate::utils::is_horizontal_space(c)
|| crate::utils::is_line_terminator(c)
|| c == '#'
});
(is_triple_dash || is_triple_dot) && sep_ok
};
if !at_doc_marker {
crate::utils::skip_until_newline(source);
if source
.current()
.map_or(false, crate::utils::is_line_terminator)
{
source.next();
}
crate::utils::skip_whitespace_and_comments(source);
}
}
Ok(crate::parser::utils::node_utils::make_mapping_node(pairs))
}
Some(c) if c == '?' => unreachable!(),
Some(c) if c.is_alphanumeric() => {
if matches!(head_kind, BlockHeadKind::BlockMapping) {
let base_indent = source.get_current_indent_level();
let mut stream =
crate::parser::token_stream::TokenStream::new(source, directives, false)?;
Ok(parse_mapping_with_tokens(
&mut stream,
base_indent,
directives,
0,
)?)
} else {
parse_scalar_or_value(source, directives, indent_level, ctx)
}
}
Some(c) if c.is_whitespace() => {
source.next();
Ok(parse_document_contents(
source,
indent_level,
directives,
ctx,
)?)
}
Some('\0') => {
source.next();
Ok(parse_document_contents(
source,
indent_level,
directives,
ctx,
)?)
}
Some(c) if matches!(c, '<' | '>' | '"' | '\'' | '|') => {
if matches!(head_kind, BlockHeadKind::BlockMapping) {
Ok(parse_mapping(source, indent_level, directives)?)
} else {
parse_scalar_or_value(source, directives, indent_level, ctx)
}
}
Some('%') => Ok(Node::None),
Some(c) => {
let stream = crate::parser::token_stream::TokenStream::new(source, directives, false)?;
Err(parse_err!(
&stream,
&format!(
"{}{}",
crate::error::messages::ERR_UNEXPECTED_CHAR_PREFIX,
c
)
))
}
None => Ok(Node::None),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::io::sources::buffer::Buffer;
use crate::nodes::node::Node;
use crate::parser::directives::DirectiveContext;
use crate::parser::utils::context::CollectionType;
use crate::parser::utils::context::ParsingContext;
fn make_ctx_none() -> ParsingContext {
ParsingContext {
collection_type: CollectionType::None,
..Default::default()
}
}
#[test]
fn test_parse_scalar_or_value_plain_multiline() {
let mut buf = Buffer::new(b"abc\ndef\nghi");
let directives = DirectiveContext::default();
let ctx = make_ctx_none();
let node = parse_scalar_or_value(&mut buf, &directives, 0, &ctx).unwrap();
assert_eq!(node, Node::from("abc def ghi"));
}
#[test]
fn test_parse_scalar_or_value_value_branch() {
let mut buf = Buffer::new(b"42");
let directives = DirectiveContext::default();
let ctx = ParsingContext {
collection_type: CollectionType::BlockSequence,
..Default::default()
};
let node = parse_scalar_or_value(&mut buf, &directives, 0, &ctx).unwrap();
assert!(node != Node::None);
}
#[test]
fn test_parse_document_contents_unexpected_char() {
let mut buf = Buffer::new(b"$");
let directives = DirectiveContext::default();
let ctx = make_ctx_none();
let result = parse_document_contents(&mut buf, 0, &directives, &ctx);
assert!(result.is_err());
}
#[test]
fn test_parse_document_contents_none_on_eof() {
let mut buf = Buffer::new(b"");
let directives = DirectiveContext::default();
let ctx = make_ctx_none();
let node = parse_document_contents(&mut buf, 0, &directives, &ctx).unwrap();
assert_eq!(node, Node::None);
}
#[test]
fn test_2cms_regression_az63_should_succeed() {
let config = crate::parser::config::ParserConfig::strict();
let result = crate::parse_with_config("one:\n- 2\n- 3\nfour: 5\n", config);
assert!(
result.is_ok(),
"AZ63 should parse successfully: {:?}",
result.err()
);
}
#[test]
fn test_2cms_fix_invalid_key_value_in_continuation() {
let config = crate::parser::config::ParserConfig::strict();
let result = crate::parse_with_config("this\n is\n invalid: x\n", config);
assert!(
result.is_err(),
"2CMS should fail: continuation line 'invalid: x' is a mapping entry"
);
}
#[test]
fn test_c2sp_multiline_flow_seq_implicit_key_errors() {
let config = crate::parser::config::ParserConfig::strict();
let result = crate::parse_with_config("[23\n]: 42\n", config);
assert!(
result.is_err(),
"C2SP: multiline flow sequence as implicit mapping key should error"
);
}
}