use std::collections::BTreeSet;
use super::error::{Error, ErrorKind, Position};
use super::scalar::{self, BlockHeader, Chomping, Inline};
use super::source::{Line, leading_spaces};
use super::{Entry, Node, Scalar, Style, Value};
pub(super) fn document(text: &str) -> Result<Option<Node>, Error> {
let mut parser = Parser {
lines: Line::split(text),
next: 0,
};
let Some(first) = parser.peek()? else {
return Ok(None);
};
if first.indent != 0 {
return Err(Error::at(first.start(), ErrorKind::UnexpectedIndentation));
}
let node = parser.block(0)?;
match parser.peek()? {
None => Ok(Some(node)),
Some(line) if line.indent > 0 => {
Err(Error::at(line.start(), ErrorKind::UnexpectedIndentation))
}
Some(line) if is_entry(line.text) => {
Err(Error::at(line.start(), ErrorKind::UnexpectedSequence))
}
Some(line) => Err(Error::at(line.start(), ErrorKind::UnexpectedMapping)),
}
}
struct Parser<'a> {
lines: Vec<Line<'a>>,
next: usize,
}
fn is_entry(text: &str) -> bool {
text == "-" || text.starts_with("- ") || text.starts_with("-\t")
}
impl<'a> Parser<'a> {
fn peek(&mut self) -> Result<Option<Line<'a>>, Error> {
while self.lines.get(self.next).is_some_and(Line::is_blank) {
self.next += 1;
}
let Some(line) = self.lines.get(self.next).copied() else {
return Ok(None);
};
let marker = |prefix: &str| {
line.text
.strip_prefix(prefix)
.is_some_and(|rest| rest.is_empty() || rest.starts_with([' ', '\t']))
};
let kind = if line.text.starts_with('\t') {
ErrorKind::TabInIndentation
} else if line.text.starts_with('#') {
ErrorKind::Comment
} else if line.column == 1 && (marker("---") || marker("...") || line.text.starts_with('%'))
{
ErrorKind::DocumentMarker
} else {
return Ok(Some(line));
};
Err(Error::at(line.start(), kind))
}
fn block(&mut self, indent: usize) -> Result<Node, Error> {
match self.peek()? {
Some(line) if is_entry(line.text) => self.sequence(indent),
_ => self.mapping(indent),
}
}
fn mapping(&mut self, indent: usize) -> Result<Node, Error> {
let mut entries = Vec::new();
let mut seen = BTreeSet::new();
let mut position = None;
while let Some(line) = self.peek()? {
if line.indent < indent {
break;
}
if line.indent > indent {
return Err(Error::at(line.start(), ErrorKind::UnexpectedIndentation));
}
if is_entry(line.text) {
return Err(Error::at(line.start(), ErrorKind::UnexpectedSequence));
}
let (key, offset) = field(&line)?;
if !seen.insert(key) {
return Err(Error::at(
line.start(),
ErrorKind::DuplicateKey(key.to_owned()),
));
}
position.get_or_insert(line.start());
self.next += 1;
let value = self.value(&line, offset, indent, true)?;
entries.push(Entry {
key: key.to_owned(),
key_position: line.start(),
value,
});
}
Ok(Node {
position: position.unwrap_or(Position::new(1, 1)),
value: Value::Mapping(entries),
})
}
fn sequence(&mut self, indent: usize) -> Result<Node, Error> {
let mut items = Vec::new();
let mut position = None;
while let Some(line) = self.peek()? {
if line.indent > indent {
return Err(Error::at(line.start(), ErrorKind::UnexpectedIndentation));
}
if line.indent < indent || !is_entry(line.text) {
break;
}
position.get_or_insert(line.start());
self.next += 1;
items.push(self.entry(line, indent)?);
}
Ok(Node {
position: position.unwrap_or(Position::new(1, 1)),
value: Value::Sequence(items),
})
}
fn entry(&mut self, line: Line<'a>, indent: usize) -> Result<Node, Error> {
let rest = &line.text[1..];
if rest.starts_with('\t') {
return Err(Error::at(line.position(1), ErrorKind::TabSeparator));
}
let offset = 1 + leading_spaces(rest);
let content = &line.text[offset..];
if content.is_empty() {
return self.value(&line, offset, indent, false);
}
if is_entry(content) {
return Err(Error::at(line.position(offset), ErrorKind::CompactSequence));
}
if !starts_field(content) {
return self.value(&line, offset, indent, false);
}
self.next -= 1;
self.lines[self.next] = Line {
column: line.column + offset,
indent: indent + offset,
text: content,
..line
};
self.mapping(indent + offset)
}
fn value(
&mut self,
line: &Line<'a>,
offset: usize,
indent: usize,
under_key: bool,
) -> Result<Node, Error> {
let position = line.position(offset);
let value = match scalar::inline(line, offset)? {
Inline::Scalar(scalar) => {
self.no_continuation(indent, scalar.style)?;
Value::Scalar(scalar)
}
Inline::Literal(header) => Value::Scalar(self.literal(indent, header)?),
Inline::Nothing => match self.peek()? {
Some(next) if next.indent > indent => return self.block(next.indent),
Some(next) if under_key && next.indent == indent && is_entry(next.text) => {
return self.sequence(indent);
}
_ => Value::Empty,
},
};
Ok(Node { position, value })
}
fn no_continuation(&mut self, indent: usize, style: Style) -> Result<(), Error> {
match self.peek()? {
Some(next) if next.indent > indent => {
let kind = if style == Style::Plain {
ErrorKind::MultiLinePlainScalar
} else {
ErrorKind::UnexpectedIndentation
};
Err(Error::at(next.start(), kind))
}
_ => Ok(()),
}
}
fn literal(&mut self, indent: usize, header: BlockHeader) -> Result<Scalar, Error> {
let content_indent = match header.indentation {
Some(relative) => indent + relative,
None => self.detect_indentation(indent)?,
};
let mut lines: Vec<&str> = Vec::new();
while let Some(line) = self.lines.get(self.next) {
let raw = line.raw;
let spaces = leading_spaces(raw);
if spaces == raw.len() {
lines.push(raw.get(content_indent..).unwrap_or(""));
} else if spaces >= content_indent {
lines.push(&raw[content_indent..]);
} else {
break;
}
self.next += 1;
}
Ok(Scalar {
style: Style::Literal,
text: chomp(&lines, header.chomping),
})
}
fn detect_indentation(&self, indent: usize) -> Result<usize, Error> {
let leading = |line: &Line<'_>| leading_spaces(line.raw);
let rest = &self.lines[self.next..];
let Some(first) = rest.iter().position(|line| leading(line) < line.raw.len()) else {
return Ok(indent + 1);
};
let spaces = leading(&rest[first]);
if spaces <= indent {
return Ok(indent + 1);
}
match rest[..first].iter().find(|line| line.raw.len() > spaces) {
Some(empty) => Err(Error::at(
Position::new(empty.number, spaces + 1),
ErrorKind::OverIndentedEmptyLine,
)),
None => Ok(spaces),
}
}
}
fn chomp(lines: &[&str], chomping: Chomping) -> String {
let last = lines.iter().rposition(|line| !line.is_empty());
let (body, trailing) = match last {
Some(index) => (&lines[..=index], lines.len() - index - 1),
None => (&lines[..0], lines.len()),
};
let mut text = body.join("\n");
let final_feeds = match (chomping, body.is_empty()) {
(Chomping::Strip, _) | (Chomping::Clip, true) => 0,
(Chomping::Clip, false) => 1,
(Chomping::Keep, false) => 1 + trailing,
(Chomping::Keep, true) => trailing,
};
text.extend(std::iter::repeat_n('\n', final_feeds));
text
}
fn starts_field(text: &str) -> bool {
let name = text.trim_start_matches(|character: char| character.is_ascii_alphanumeric());
name.len() < text.len()
&& name.starts_with(':')
&& (name.len() == 1 || name[1..].starts_with([' ', '\t']))
}
fn field<'t>(line: &Line<'t>) -> Result<(&'t str, usize), Error> {
let text = line.text;
let error = |at: usize, kind| Err(Error::at(line.position(at), kind));
if text.starts_with("? ") || text == "?" {
return error(0, ErrorKind::ComplexKey);
}
let Some(colon) = text.find(':') else {
return error(0, ErrorKind::InvalidKey(text.to_owned()));
};
let key = &text[..colon];
let valid = key.starts_with(|character: char| character.is_ascii_alphabetic())
&& key
.chars()
.all(|character| character.is_ascii_alphanumeric());
let rest = &text[colon + 1..];
if !valid || !(rest.is_empty() || rest.starts_with([' ', '\t'])) {
return error(0, ErrorKind::InvalidKey(key.to_owned()));
}
if rest.starts_with('\t') {
return error(colon + 1, ErrorKind::TabSeparator);
}
Ok((key, colon + 1 + leading_spaces(rest)))
}