#![allow(clippy::items_after_test_module)]
#[cfg(not(test))]
use alloc::{borrow::Cow, string::String, string::ToString, vec::Vec};
#[cfg(test)]
use std::borrow::Cow;
#[cfg(test)]
use std::string::ToString;
use super::index::YamlIndex;
use super::simd::find_json_escape;
#[derive(Debug)]
pub struct YamlCursor<'a, W = Vec<u64>> {
text: &'a [u8],
index: &'a YamlIndex<W>,
bp_pos: usize,
}
impl<'a, W> Clone for YamlCursor<'a, W> {
fn clone(&self) -> Self {
*self
}
}
impl<'a, W> Copy for YamlCursor<'a, W> {}
impl<'a, W: AsRef<[u64]>> YamlCursor<'a, W> {
#[inline]
pub fn new(index: &'a YamlIndex<W>, text: &'a [u8], bp_pos: usize) -> Self {
Self {
text,
index,
bp_pos,
}
}
#[inline]
pub fn bp_position(&self) -> usize {
self.bp_pos
}
#[inline]
pub fn is_container(&self) -> bool {
self.index.is_container(self.bp_pos)
}
pub fn text_position(&self) -> Option<usize> {
self.index.bp_to_text_pos(self.bp_pos)
}
#[inline]
pub fn text_end_position(&self) -> Option<usize> {
self.index.bp_to_text_end_pos(self.bp_pos)
}
#[inline]
pub fn first_child(&self) -> Option<YamlCursor<'a, W>> {
let new_pos = self.index.bp().first_child(self.bp_pos)?;
Some(YamlCursor {
text: self.text,
index: self.index,
bp_pos: new_pos,
})
}
#[inline]
pub fn next_sibling(&self) -> Option<YamlCursor<'a, W>> {
let new_pos = self.index.bp().next_sibling(self.bp_pos)?;
Some(YamlCursor {
text: self.text,
index: self.index,
bp_pos: new_pos,
})
}
#[inline]
pub fn parent(&self) -> Option<YamlCursor<'a, W>> {
let new_pos = self.index.bp().parent(self.bp_pos)?;
Some(YamlCursor {
text: self.text,
index: self.index,
bp_pos: new_pos,
})
}
pub fn value(&self) -> YamlValue<'a, W> {
if self.bp_pos == 0 {
return YamlValue::Sequence(YamlElements::from_sequence_cursor(*self));
}
if self.index.is_seq_item(self.bp_pos) {
if let Some(child) = self.first_child() {
return child.value();
}
return YamlValue::Null;
}
let open_idx = self.index.bp_to_open_idx(self.bp_pos);
let Some(text_pos) = self.index.text_pos_by_open_idx(open_idx) else {
return YamlValue::Error("invalid cursor position");
};
if text_pos >= self.text.len() {
return YamlValue::Null;
}
if self.is_container() {
if self.index.is_sequence_at_bp(self.bp_pos) {
return YamlValue::Sequence(YamlElements::from_sequence_cursor(*self));
} else {
return YamlValue::Mapping(YamlFields::from_mapping_cursor(*self));
}
}
let byte = self.text[text_pos];
if byte == b'*' {
return self.parse_alias_value(text_pos);
}
let effective_text_pos = if byte == b'&' {
self.skip_anchor_and_whitespace(text_pos)
} else {
text_pos
};
if effective_text_pos >= self.text.len() {
return YamlValue::Null;
}
let byte = self.text[effective_text_pos];
if byte == b'[' {
return YamlValue::Sequence(YamlElements::from_sequence_cursor(*self));
}
if byte == b'{' {
return YamlValue::Mapping(YamlFields::from_mapping_cursor(*self));
}
if byte == b'-'
&& effective_text_pos + 1 < self.text.len()
&& self.text[effective_text_pos + 1] == b' '
{
if let Some(first_child) = self.first_child() {
if let Some(child_text_pos) = first_child.text_position() {
if child_text_pos < self.text.len()
&& self.text[child_text_pos] == b'-'
&& child_text_pos + 1 < self.text.len()
&& self.text[child_text_pos + 1] == b' '
{
return YamlValue::Sequence(YamlElements::from_sequence_cursor(*self));
}
}
}
}
if let Some(first_child) = self.first_child() {
if first_child.next_sibling().is_some() {
if let Some(first_child_text_pos) = first_child.text_position() {
if first_child_text_pos < self.text.len() {
let first_byte = self.text[first_child_text_pos];
if first_byte != b'-' {
return YamlValue::Mapping(YamlFields::from_mapping_cursor(*self));
}
}
}
}
}
match self.text[effective_text_pos] {
b'"' => YamlValue::String(YamlString::DoubleQuoted {
text: self.text,
start: effective_text_pos,
}),
b'\'' => YamlValue::String(YamlString::SingleQuoted {
text: self.text,
start: effective_text_pos,
}),
b'|' => {
let (chomping, explicit_indent) = self.parse_block_header(effective_text_pos);
YamlValue::String(YamlString::BlockLiteral {
text: self.text,
indicator_pos: effective_text_pos,
chomping,
explicit_indent,
})
}
b'>' => {
let (chomping, explicit_indent) = self.parse_block_header(effective_text_pos);
YamlValue::String(YamlString::BlockFolded {
text: self.text,
indicator_pos: effective_text_pos,
chomping,
explicit_indent,
})
}
_ => {
let end = self
.index
.text_end_pos_by_open_idx(open_idx)
.filter(|&e| e >= effective_text_pos)
.unwrap_or(effective_text_pos);
let base_indent = if end > effective_text_pos
&& self.text[effective_text_pos..end].contains(&b'\n')
{
self.compute_scalar_base_indent(effective_text_pos)
} else {
0
};
YamlValue::String(YamlString::Unquoted {
text: self.text,
start: effective_text_pos,
end,
base_indent,
})
}
}
}
fn parse_alias_value(&self, text_pos: usize) -> YamlValue<'a, W> {
let start = text_pos + 1;
let mut end = start;
while end < self.text.len() {
match self.text[end] {
b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9' | b'_' | b'-' => end += 1,
_ => break,
}
}
let anchor_name = match core::str::from_utf8(&self.text[start..end]) {
Ok(s) => s,
Err(_) => return YamlValue::Error("invalid UTF-8 in anchor name"),
};
let target = self.index.resolve_alias(self.bp_pos, self.text);
YamlValue::Alias {
anchor_name,
target,
}
}
fn skip_anchor_and_whitespace(&self, start: usize) -> usize {
let mut pos = start + 1;
while pos < self.text.len() {
match self.text[pos] {
b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9' | b'_' | b'-' | b':' => pos += 1,
_ => break,
}
}
while pos < self.text.len() && (self.text[pos] == b' ' || self.text[pos] == b'\t') {
pos += 1;
}
pos
}
fn parse_block_header(&self, indicator_pos: usize) -> (ChompingIndicator, Option<u8>) {
let mut pos = indicator_pos + 1;
let mut chomping = ChompingIndicator::Clip;
let mut explicit_indent = None;
for _ in 0..2 {
if pos >= self.text.len() {
break;
}
match self.text[pos] {
b'-' => {
chomping = ChompingIndicator::Strip;
pos += 1;
}
b'+' => {
chomping = ChompingIndicator::Keep;
pos += 1;
}
b'1'..=b'9' => {
explicit_indent = Some(self.text[pos] - b'0');
pos += 1;
}
_ => break,
}
}
(chomping, explicit_indent)
}
fn compute_line_indent_static(text: &[u8], pos: usize) -> usize {
let mut line_start = pos;
while line_start > 0 && text[line_start - 1] != b'\n' {
line_start -= 1;
}
let mut indent = 0;
while line_start + indent < text.len() && text[line_start + indent] == b' ' {
indent += 1;
}
indent
}
fn compute_scalar_base_indent(&self, value_pos: usize) -> usize {
let mut line_start = value_pos;
while line_start > 0 && self.text[line_start - 1] != b'\n' {
line_start -= 1;
}
Self::compute_line_indent_static(self.text, line_start)
}
#[allow(dead_code)]
fn compute_base_indent_and_root_flag(&self, value_pos: usize) -> (usize, bool) {
let mut line_start = value_pos;
while line_start > 0 && self.text[line_start - 1] != b'\n' {
line_start -= 1;
}
let line_indent = Self::compute_line_indent_static(self.text, line_start);
let mut last_seq_col = None;
let mut explicit_indicator_col = None;
let mut last_key_separator_pos = None;
let mut scan = line_start + line_indent; while scan < value_pos {
if self.text[scan] == b'?' {
if scan + 1 < self.text.len()
&& matches!(self.text[scan + 1], b' ' | b'\t' | b'\n' | b'\r')
{
explicit_indicator_col = Some(scan - line_start);
}
}
if self.text[scan] == b':' {
if scan + 1 < self.text.len() && matches!(self.text[scan + 1], b' ' | b'\t' | b'\n')
{
if scan == line_start + line_indent {
explicit_indicator_col = Some(scan - line_start);
} else {
last_key_separator_pos = Some(scan);
}
}
}
if self.text[scan] == b'-' {
if scan + 1 < self.text.len() && matches!(self.text[scan + 1], b' ' | b'\t') {
last_seq_col = Some(scan - line_start);
}
}
scan += 1;
}
let seq_col_before_key = match (last_seq_col, last_key_separator_pos) {
(Some(seq_col), Some(key_sep_pos)) => {
let seq_pos = line_start + seq_col;
if seq_pos < key_sep_pos {
None
} else {
Some(seq_col)
}
}
(Some(seq_col), None) => Some(seq_col),
_ => None,
};
if let Some(seq_col) = seq_col_before_key {
return (seq_col, false);
}
if let Some(exp_col) = explicit_indicator_col {
return (exp_col, false);
}
let mut has_colon_before = false;
let mut colon_pos = 0;
scan = line_start;
while scan < value_pos {
if self.text[scan] == b':' {
if scan + 1 < self.text.len() {
let next = self.text[scan + 1];
if next == b' ' || next == b'\t' || next == b'\n' {
has_colon_before = true;
colon_pos = scan;
}
}
}
scan += 1;
}
if has_colon_before {
let mut key_start = colon_pos;
while key_start > line_start {
let prev = self.text[key_start - 1];
if prev == b' ' || prev == b'\t' {
break;
}
if prev == b'"' || prev == b'\'' {
return (
Self::compute_line_indent_static(self.text, value_pos),
false,
);
}
key_start -= 1;
}
(key_start - line_start, false)
} else {
if line_start == 0 {
return (0, true);
}
let mut prev_line_start = line_start - 1;
while prev_line_start > 0 && self.text[prev_line_start - 1] != b'\n' {
prev_line_start -= 1;
}
let prev_line = &self.text[prev_line_start..line_start.saturating_sub(1)];
let trimmed = prev_line
.iter()
.copied()
.skip_while(|&b| b == b' ')
.collect::<alloc::vec::Vec<u8>>();
if trimmed.starts_with(b"---") {
let rest = &trimmed[3..];
if rest.is_empty() || rest.iter().all(|&b| b == b' ' || b == b'\t') {
return (0, true);
}
}
(
Self::compute_line_indent_static(self.text, prev_line_start),
false,
)
}
}
#[allow(dead_code)]
fn is_in_flow_context(&self, pos: usize) -> bool {
let mut line_start = pos;
while line_start > 0 && self.text[line_start - 1] != b'\n' {
line_start -= 1;
}
let mut has_flow_marker_on_line = false;
for i in line_start..pos {
match self.text[i] {
b'[' | b'{' => {
has_flow_marker_on_line = true;
break;
}
_ => {}
}
}
if !has_flow_marker_on_line {
let quick_start = line_start.saturating_sub(256);
let mut found_opener = false;
for i in quick_start..line_start {
if matches!(self.text[i], b'[' | b'{') {
found_opener = true;
break;
}
}
if !found_opener {
return false;
}
}
let scan_start = if line_start > 4096 {
let mut start = line_start.saturating_sub(4096);
while start > 0 && self.text[start - 1] != b'\n' {
start -= 1;
}
start
} else {
0
};
let mut depth = 0i32;
let mut in_double_quote = false;
let mut in_single_quote = false;
let mut i = scan_start;
while i < pos {
let byte = self.text[i];
if in_double_quote {
if byte == b'\\' && i + 1 < pos {
i += 2;
continue;
}
if byte == b'"' {
in_double_quote = false;
}
} else if in_single_quote {
if byte == b'\'' {
if i + 1 < pos && self.text[i + 1] == b'\'' {
i += 2;
continue;
}
in_single_quote = false;
}
} else {
match byte {
b'"' => in_double_quote = true,
b'\'' => in_single_quote = true,
b'[' | b'{' => depth += 1,
b']' | b'}' => depth -= 1,
_ => {}
}
}
i += 1;
}
depth > 0
}
fn find_scalar_end(&self, start: usize) -> usize {
let mut end = start;
while end < self.text.len() {
match self.text[end] {
b'\n' | b'#' => break,
b',' | b']' | b'}' => break,
b':' => {
if end + 1 >= self.text.len() {
break;
}
if self.text[end + 1] == b' ' || self.text[end + 1] == b'\n' {
break;
}
end += 1;
}
_ => end += 1,
}
}
while end > start && self.text[end - 1] == b' ' {
end -= 1;
}
end
}
#[allow(dead_code)]
fn find_plain_scalar_end(&self, start: usize, base_indent: usize, is_doc_root: bool) -> usize {
let in_flow = self.is_in_flow_context(start);
let mut end = start;
loop {
while end < self.text.len() {
match self.text[end] {
b'\n' => break,
b'#' => {
if end > start && matches!(self.text[end - 1], b' ' | b'\t') {
break;
}
end += 1;
}
b',' | b']' | b'}' if in_flow => break,
b':' => {
if end + 1 >= self.text.len() {
break;
}
if self.text[end + 1] == b' '
|| self.text[end + 1] == b'\n'
|| self.text[end + 1] == b'\t'
{
break;
}
end += 1;
}
_ => end += 1,
}
}
let mut line_end = end;
while line_end > start && matches!(self.text[line_end - 1], b' ' | b'\t') {
line_end -= 1;
}
if end >= self.text.len() || self.text[end] != b'\n' {
return line_end;
}
let newline_pos = end;
end += 1;
let mut empty_lines_end = end;
while empty_lines_end < self.text.len() {
let mut line_indent = 0;
while empty_lines_end + line_indent < self.text.len()
&& self.text[empty_lines_end + line_indent] == b' '
{
line_indent += 1;
}
let after_indent = empty_lines_end + line_indent;
if after_indent >= self.text.len() {
return line_end;
}
match self.text[after_indent] {
b'\n' => {
empty_lines_end = after_indent + 1;
}
b'\r' => {
empty_lines_end = after_indent + 1;
if empty_lines_end < self.text.len() && self.text[empty_lines_end] == b'\n'
{
empty_lines_end += 1;
}
}
b'\t' => {
let mut check_pos = after_indent;
while check_pos < self.text.len()
&& matches!(self.text[check_pos], b'\t' | b' ')
{
check_pos += 1;
}
if check_pos >= self.text.len()
|| matches!(self.text[check_pos], b'\n' | b'\r')
{
empty_lines_end = check_pos;
if check_pos < self.text.len() && self.text[check_pos] == b'\r' {
empty_lines_end += 1;
if empty_lines_end < self.text.len()
&& self.text[empty_lines_end] == b'\n'
{
empty_lines_end += 1;
}
} else if check_pos < self.text.len() && self.text[check_pos] == b'\n' {
empty_lines_end += 1;
}
} else if in_flow
|| line_indent > base_indent
|| (is_doc_root && line_indent == 0)
{
end = empty_lines_end;
break;
} else {
return line_end;
}
}
b'#' => {
return line_end;
}
b'-' => {
if line_indent == 0
&& after_indent + 2 < self.text.len()
&& self.text[after_indent + 1] == b'-'
&& self.text[after_indent + 2] == b'-'
{
let after_marker = after_indent + 3;
if after_marker >= self.text.len()
|| matches!(self.text[after_marker], b' ' | b'\t' | b'\n' | b'\r')
{
return line_end;
}
}
if after_indent + 1 < self.text.len()
&& (self.text[after_indent + 1] == b' '
|| self.text[after_indent + 1] == b'\t'
|| self.text[after_indent + 1] == b'\n')
{
if line_indent == base_indent || (is_doc_root && line_indent == 0) {
return line_end;
}
}
if in_flow || line_indent > base_indent || (is_doc_root && line_indent == 0)
{
end = empty_lines_end;
break;
}
return line_end;
}
b']' | b'}' | b',' => {
return line_end;
}
b'?' => {
if after_indent + 1 >= self.text.len()
|| matches!(self.text[after_indent + 1], b' ' | b'\t' | b'\n' | b'\r')
{
return line_end;
}
if in_flow || line_indent > base_indent || (is_doc_root && line_indent == 0)
{
end = empty_lines_end;
break;
}
return line_end;
}
b':' => {
if after_indent + 1 >= self.text.len()
|| matches!(self.text[after_indent + 1], b' ' | b'\t' | b'\n' | b'\r')
{
return line_end;
}
if in_flow || line_indent > base_indent || (is_doc_root && line_indent == 0)
{
end = empty_lines_end;
break;
}
return line_end;
}
b'.' => {
if line_indent == 0
&& after_indent + 2 < self.text.len()
&& self.text[after_indent + 1] == b'.'
&& self.text[after_indent + 2] == b'.'
{
let after_marker = after_indent + 3;
if after_marker >= self.text.len()
|| matches!(self.text[after_marker], b' ' | b'\t' | b'\n' | b'\r')
{
return line_end;
}
}
if in_flow || line_indent > base_indent || (is_doc_root && line_indent == 0)
{
end = empty_lines_end;
break;
}
return line_end;
}
_ => {
if in_flow || line_indent > base_indent || (is_doc_root && line_indent == 0)
{
end = empty_lines_end;
break;
} else {
return line_end;
}
}
}
}
if empty_lines_end >= self.text.len() {
return line_end;
}
let _ = newline_pos; }
}
#[inline]
pub fn children(&self) -> YamlChildren<'a, W> {
YamlChildren {
current: self.first_child(),
}
}
pub fn raw_bytes(&self) -> Option<&'a [u8]> {
let start = self.text_position()?;
let end = if self.is_container() {
let close_bp = self.index.bp().find_close(self.bp_pos)?;
let close_rank = self.index.bp().rank1(close_bp);
self.index.ib_select1_from(close_rank, close_rank / 8)? + 1
} else {
match self.text.get(start)? {
b'"' => self.find_double_quote_end(start),
b'\'' => self.find_single_quote_end(start),
_ => self.find_scalar_end(start),
}
};
Some(&self.text[start..end.min(self.text.len())])
}
fn find_double_quote_end(&self, start: usize) -> usize {
let mut i = start + 1;
while i < self.text.len() {
match self.text[i] {
b'"' => return i + 1,
b'\\' => i += 2,
_ => i += 1,
}
}
self.text.len()
}
fn find_single_quote_end(&self, start: usize) -> usize {
let mut i = start + 1;
while i < self.text.len() {
if self.text[i] == b'\'' {
if i + 1 < self.text.len() && self.text[i + 1] == b'\'' {
i += 2; } else {
return i + 1;
}
} else {
i += 1;
}
}
self.text.len()
}
pub fn to_json(&self) -> String {
let mut output = String::new();
self.write_json_to(&mut output);
output
}
pub fn to_json_document(&self) -> String {
let mut output = String::new();
if self.bp_pos == 0 {
if let YamlValue::Sequence(elements) = self.value() {
let mut iter = elements.into_iter();
if let Some(first) = iter.next() {
if iter.next().is_none() {
write_yaml_value_as_json(&mut output, first);
return output;
}
}
}
}
self.write_json_to(&mut output);
output
}
pub fn stream_json<Out: core::fmt::Write>(&self, out: &mut Out) -> core::fmt::Result {
self.stream_json_value(out)
}
pub fn stream_json_document<Out: core::fmt::Write>(&self, out: &mut Out) -> core::fmt::Result {
if self.bp_pos == 0 {
if let YamlValue::Sequence(elements) = self.value() {
let mut iter = elements.into_iter();
if let Some(first) = iter.next() {
if iter.next().is_none() {
return stream_yaml_value_as_json(out, first);
}
}
}
}
self.stream_json_value(out)
}
pub fn stream_yaml<Out: core::fmt::Write>(
&self,
out: &mut Out,
indent_spaces: usize,
) -> core::fmt::Result {
self.stream_yaml_value(out, 0, indent_spaces)
}
pub fn stream_yaml_document<Out: core::fmt::Write>(
&self,
out: &mut Out,
indent_spaces: usize,
) -> core::fmt::Result {
if self.bp_pos == 0 {
if let YamlValue::Sequence(elements) = self.value() {
if let Some((cursor, rest)) = elements.uncons_cursor() {
if rest.is_empty() {
return cursor.stream_yaml_value(out, 0, indent_spaces);
}
}
}
}
self.stream_yaml_value(out, 0, indent_spaces)
}
fn stream_yaml_value<Out: core::fmt::Write>(
&self,
out: &mut Out,
current_indent: usize,
indent_spaces: usize,
) -> core::fmt::Result {
match self.value() {
YamlValue::Null => out.write_str("null"),
YamlValue::String(s) => stream_yaml_string_value(out, &s),
YamlValue::Mapping(fields) => {
if fields.is_empty() {
return out.write_str("{}");
}
if indent_spaces == 0 {
out.write_char('{')?;
let mut first = true;
for field in fields {
if !first {
out.write_str(", ")?;
}
first = false;
if let YamlValue::String(s) = field.key() {
stream_yaml_string_value(out, &s)?;
} else {
out.write_str("\"\"")?;
}
out.write_str(": ")?;
field.value_cursor().stream_yaml_value(out, 0, 0)?;
}
out.write_char('}')
} else {
let mut first = true;
for field in fields {
if !first {
out.write_char('\n')?;
write_yaml_indent(out, current_indent)?;
}
first = false;
if let YamlValue::String(s) = field.key() {
stream_yaml_string_value(out, &s)?;
} else {
out.write_str("\"\"")?;
}
out.write_char(':')?;
let value = field.value_cursor();
if is_yaml_cursor_container(&value) {
out.write_char('\n')?;
write_yaml_indent(out, current_indent + indent_spaces)?;
value.stream_yaml_value(
out,
current_indent + indent_spaces,
indent_spaces,
)?;
} else {
out.write_char(' ')?;
value.stream_yaml_value(
out,
current_indent + indent_spaces,
indent_spaces,
)?;
}
}
Ok(())
}
}
YamlValue::Sequence(elements) => {
if elements.is_empty() {
return out.write_str("[]");
}
if indent_spaces == 0 {
out.write_char('[')?;
let mut first = true;
let mut elems = elements;
while let Some((cursor, rest)) = elems.uncons_cursor() {
if !first {
out.write_str(", ")?;
}
first = false;
cursor.stream_yaml_value(out, 0, 0)?;
elems = rest;
}
out.write_char(']')
} else {
let mut first = true;
let mut elems = elements;
while let Some((cursor, rest)) = elems.uncons_cursor() {
if !first {
out.write_char('\n')?;
write_yaml_indent(out, current_indent)?;
}
first = false;
out.write_str("- ")?;
if is_yaml_cursor_container(&cursor) {
out.write_char('\n')?;
write_yaml_indent(out, current_indent + indent_spaces)?;
cursor.stream_yaml_value(
out,
current_indent + indent_spaces,
indent_spaces,
)?;
} else {
cursor.stream_yaml_value(
out,
current_indent + indent_spaces,
indent_spaces,
)?;
}
elems = rest;
}
Ok(())
}
}
YamlValue::Alias { target, .. } => {
if let Some(target_cursor) = target {
target_cursor.stream_yaml_value(out, current_indent, indent_spaces)
} else {
out.write_str("null")
}
}
YamlValue::Error(_) => out.write_str("null"),
}
}
fn stream_json_value<Out: core::fmt::Write>(&self, out: &mut Out) -> core::fmt::Result {
match self.value() {
YamlValue::Null => out.write_str("null"),
YamlValue::String(s) => {
match stream_yaml_string_to_json(out, &s) {
Ok(true) => Ok(()), Ok(false) => {
if let Ok(str_val) = s.as_str() {
stream_yaml_scalar_as_json(out, &str_val)
} else {
out.write_str("null")
}
}
Err(_) => out.write_str("null"),
}
}
YamlValue::Mapping(fields) => {
out.write_char('{')?;
let mut first = true;
for field in fields {
if !first {
out.write_char(',')?;
}
first = false;
if let YamlValue::String(s) = field.key() {
match stream_yaml_string_to_json(out, &s) {
Ok(true) => {} Ok(false) | Err(_) => {
if let Ok(key_str) = s.as_str() {
stream_json_string(out, &key_str)?;
} else {
out.write_str("\"\"")?;
}
}
}
} else {
out.write_str("\"\"")?;
}
out.write_char(':')?;
field.value_cursor().stream_json_value(out)?;
}
out.write_char('}')
}
YamlValue::Sequence(elements) => {
out.write_char('[')?;
let mut first = true;
for elem in elements {
if !first {
out.write_char(',')?;
}
first = false;
stream_yaml_value_as_json(out, elem)?;
}
out.write_char(']')
}
YamlValue::Alias { target, .. } => {
if let Some(target_cursor) = target {
target_cursor.stream_json_value(out)
} else {
out.write_str("null")
}
}
YamlValue::Error(_) => out.write_str("null"),
}
}
fn write_json_to(&self, output: &mut String) {
match self.value() {
YamlValue::Null => output.push_str("null"),
YamlValue::String(s) => {
match write_yaml_string_to_json(output, &s) {
Ok(true) => {} Ok(false) => {
if let Ok(str_val) = s.as_str() {
write_yaml_scalar_as_json(output, &str_val);
} else {
output.push_str("null");
}
}
Err(_) => output.push_str("null"),
}
}
YamlValue::Mapping(fields) => {
output.push('{');
let mut first = true;
for field in fields {
if !first {
output.push(',');
}
first = false;
if let YamlValue::String(s) = field.key() {
match write_yaml_string_to_json(output, &s) {
Ok(true) => {} Ok(false) | Err(_) => {
if let Ok(key_str) = s.as_str() {
write_json_string(output, &key_str);
} else {
output.push_str("\"\"");
}
}
}
} else {
output.push_str("\"\"");
}
output.push(':');
field.value_cursor().write_json_to(output);
}
output.push('}');
}
YamlValue::Sequence(elements) => {
output.push('[');
let mut first = true;
for elem in elements {
if !first {
output.push(',');
}
first = false;
write_yaml_value_as_json(output, elem);
}
output.push(']');
}
YamlValue::Alias { target, .. } => {
if let Some(target_cursor) = target {
target_cursor.write_json_to(output);
} else {
output.push_str("null");
}
}
YamlValue::Error(_) => output.push_str("null"),
}
}
#[inline]
pub fn anchor(&self) -> Option<&str> {
self.index.get_anchor_name(self.bp_pos)
}
#[inline]
pub fn alias(&self) -> Option<&str> {
self.index.get_alias_anchor_name(self.bp_pos)
}
#[inline]
pub fn is_alias(&self) -> bool {
self.index.is_alias(self.bp_pos)
}
#[inline]
pub fn line(&self) -> usize {
let offset = self.text_position().unwrap_or(0);
let (line, _column) = self.index.to_line_column(offset, self.text);
line
}
#[inline]
pub fn column(&self) -> usize {
let offset = self.text_position().unwrap_or(0);
let (_line, column) = self.index.to_line_column(offset, self.text);
column
}
pub fn document_index(&self) -> Option<usize> {
if self.bp_pos == 0 {
return None;
}
let mut doc_ancestor = *self;
while let Some(parent) = doc_ancestor.parent() {
if parent.bp_pos == 0 {
break;
}
doc_ancestor = parent;
}
let root = YamlCursor::new(self.index, self.text, 0);
let mut current = root.first_child()?;
let mut index = 0;
loop {
if current.bp_pos == doc_ancestor.bp_pos {
return Some(index);
}
match current.next_sibling() {
Some(next) => {
current = next;
index += 1;
}
None => {
return Some(0);
}
}
}
}
pub fn style(&self) -> &'static str {
let Some(text_pos) = self.text_position() else {
return "";
};
if text_pos >= self.text.len() {
return "";
}
let effective_pos = if self.text[text_pos] == b'&' {
self.skip_anchor_and_whitespace(text_pos)
} else {
text_pos
};
if effective_pos >= self.text.len() {
return "";
}
match self.text[effective_pos] {
b'"' => "double",
b'\'' => "single",
b'|' => "literal",
b'>' => "folded",
b'{' | b'[' => "flow",
_ => "",
}
}
pub fn tag(&self) -> &'static str {
match self.value() {
YamlValue::Null => "!!null",
YamlValue::String(s) => {
if s.is_unquoted() {
if let Ok(str_val) = s.as_str() {
let s = str_val.as_ref();
if matches!(s, "null" | "~" | "") {
return "!!null";
}
if matches!(s, "true" | "True" | "TRUE" | "false" | "False" | "FALSE") {
return "!!bool";
}
if s.parse::<i64>().is_ok() {
return "!!int";
}
if s.parse::<f64>().is_ok()
|| matches!(
s,
".inf"
| ".Inf"
| ".INF"
| "-.inf"
| "-.Inf"
| "-.INF"
| ".nan"
| ".NaN"
| ".NAN"
)
{
return "!!float";
}
}
}
"!!str"
}
YamlValue::Mapping(_) => "!!map",
YamlValue::Sequence(_) => "!!seq",
YamlValue::Alias { target, .. } => {
if let Some(t) = target {
t.tag()
} else {
"!!null"
}
}
YamlValue::Error(_) => "!!null",
}
}
pub fn kind(&self) -> &'static str {
if self.is_alias() {
return "alias";
}
match self.value() {
YamlValue::Null | YamlValue::String(_) => "scalar",
YamlValue::Sequence(_) => "seq",
YamlValue::Mapping(_) => "map",
YamlValue::Alias { .. } => "alias",
YamlValue::Error(_) => "scalar",
}
}
#[inline]
pub fn index(&self) -> &YamlIndex<W> {
self.index
}
#[inline]
pub fn text(&self) -> &'a [u8] {
self.text
}
pub fn cursor_at_offset(&self, offset: usize) -> Option<YamlCursor<'a, W>> {
if offset >= self.text.len() {
return None;
}
let rank = self.index.ib_rank1(offset);
let struct_text_pos = if let Some(struct_pos) = self.index.ib_select1(rank) {
if struct_pos == offset {
struct_pos
} else if rank > 0 {
self.index.ib_select1(rank - 1)?
} else {
return None;
}
} else if rank > 0 {
self.index.ib_select1(rank - 1)?
} else {
return None;
};
let bp_pos = self.index.find_bp_at_text_pos(struct_text_pos)?;
let cursor = YamlCursor {
text: self.text,
index: self.index,
bp_pos,
};
if bp_pos == 0 && self.index.is_sequence_at_bp(0) {
cursor.first_child()
} else {
Some(cursor)
}
}
pub fn cursor_at_position(&self, line: usize, col: usize) -> Option<YamlCursor<'a, W>> {
let offset = self.index.to_offset(line, col, self.text)?;
self.cursor_at_offset(offset)
}
}
fn write_yaml_value_as_json<W: AsRef<[u64]>>(output: &mut String, value: YamlValue<'_, W>) {
match value {
YamlValue::Null => output.push_str("null"),
YamlValue::String(s) => {
match write_yaml_string_to_json(output, &s) {
Ok(true) => {} Ok(false) => {
if let Ok(str_val) = s.as_str() {
write_yaml_scalar_as_json(output, &str_val);
} else {
output.push_str("null");
}
}
Err(_) => output.push_str("null"),
}
}
YamlValue::Mapping(fields) => {
output.push('{');
let mut first = true;
for field in fields {
if !first {
output.push(',');
}
first = false;
if let YamlValue::String(s) = field.key() {
match write_yaml_string_to_json(output, &s) {
Ok(true) => {} Ok(false) | Err(_) => {
if let Ok(key_str) = s.as_str() {
write_json_string(output, &key_str);
} else {
output.push_str("\"\"");
}
}
}
} else {
output.push_str("\"\"");
}
output.push(':');
field.value_cursor().write_json_to(output);
}
output.push('}');
}
YamlValue::Sequence(elements) => {
output.push('[');
let mut first = true;
for elem in elements {
if !first {
output.push(',');
}
first = false;
write_yaml_value_as_json(output, elem);
}
output.push(']');
}
YamlValue::Alias { target, .. } => {
if let Some(target_cursor) = target {
target_cursor.write_json_to(output);
} else {
output.push_str("null");
}
}
YamlValue::Error(_) => output.push_str("null"),
}
}
fn write_json_string(output: &mut String, s: &str) {
output.push('"');
let bytes = s.as_bytes();
let len = bytes.len();
let mut i = 0;
while i < len {
let escape_pos = find_json_escape(bytes, i);
if i < escape_pos {
output.push_str(&s[i..escape_pos]);
}
i = escape_pos;
if i < len {
let b = bytes[i];
match b {
b'"' => output.push_str("\\\""),
b'\\' => output.push_str("\\\\"),
b'\n' => output.push_str("\\n"),
b'\r' => output.push_str("\\r"),
b'\t' => output.push_str("\\t"),
b if b < 0x20 => {
output.push_str("\\u00");
const HEX: &[u8; 16] = b"0123456789abcdef";
output.push(HEX[(b >> 4) as usize] as char);
output.push(HEX[(b & 0xf) as usize] as char);
}
_ => {} }
i += 1;
}
}
output.push('"');
}
#[inline]
fn write_json_escape(output: &mut String, ch: char) {
match ch {
'"' => output.push_str("\\\""),
'\\' => output.push_str("\\\\"),
'\n' => output.push_str("\\n"),
'\r' => output.push_str("\\r"),
'\t' => output.push_str("\\t"),
c if (c as u32) < 0x20 => {
output.push_str("\\u00");
const HEX: &[u8; 16] = b"0123456789abcdef";
let b = c as u8;
output.push(HEX[(b >> 4) as usize] as char);
output.push(HEX[(b & 0xf) as usize] as char);
}
c if (c as u32) >= 0x80 && (c as u32) < 0x100 => {
output.push_str("\\u00");
const HEX: &[u8; 16] = b"0123456789abcdef";
let b = c as u8;
output.push(HEX[(b >> 4) as usize] as char);
output.push(HEX[(b & 0xf) as usize] as char);
}
c if (c as u32) >= 0x100 => {
let cp = c as u32;
if cp <= 0xFFFF {
output.push_str("\\u");
const HEX: &[u8; 16] = b"0123456789abcdef";
output.push(HEX[((cp >> 12) & 0xF) as usize] as char);
output.push(HEX[((cp >> 8) & 0xF) as usize] as char);
output.push(HEX[((cp >> 4) & 0xF) as usize] as char);
output.push(HEX[(cp & 0xF) as usize] as char);
} else {
let adjusted = cp - 0x10000;
let high = 0xD800 + (adjusted >> 10);
let low = 0xDC00 + (adjusted & 0x3FF);
output.push_str("\\u");
const HEX: &[u8; 16] = b"0123456789abcdef";
output.push(HEX[((high >> 12) & 0xF) as usize] as char);
output.push(HEX[((high >> 8) & 0xF) as usize] as char);
output.push(HEX[((high >> 4) & 0xF) as usize] as char);
output.push(HEX[(high & 0xF) as usize] as char);
output.push_str("\\u");
output.push(HEX[((low >> 12) & 0xF) as usize] as char);
output.push(HEX[((low >> 8) & 0xF) as usize] as char);
output.push(HEX[((low >> 4) & 0xF) as usize] as char);
output.push(HEX[(low & 0xF) as usize] as char);
}
}
c => output.push(c),
}
}
fn transcode_double_quoted_to_json(
output: &mut String,
bytes: &[u8],
) -> Result<(), YamlStringError> {
output.push('"');
let mut i = 0;
while i < bytes.len() {
match bytes[i] {
b'\\' => {
if i + 1 >= bytes.len() {
return Err(YamlStringError::InvalidEscape);
}
i += 1;
match bytes[i] {
b'n' => output.push_str("\\n"),
b'r' => output.push_str("\\r"),
b't' | b'\t' => output.push_str("\\t"),
b'"' => output.push_str("\\\""),
b'\\' => output.push_str("\\\\"),
b'/' => output.push('/'),
b' ' => output.push(' '),
b'0' => output.push_str("\\u0000"),
b'a' => output.push_str("\\u0007"), b'b' => output.push_str("\\u0008"), b'v' => output.push_str("\\u000b"), b'f' => output.push_str("\\u000c"), b'e' => output.push_str("\\u001b"), b'N' => output.push_str("\\u0085"), b'_' => output.push_str("\\u00a0"), b'L' => output.push_str("\\u2028"), b'P' => output.push_str("\\u2029"), b'\n' => {
i += 1;
while i < bytes.len() && matches!(bytes[i], b' ' | b'\t') {
i += 1;
}
continue;
}
b'\r' => {
i += 1;
if i < bytes.len() && bytes[i] == b'\n' {
i += 1;
}
while i < bytes.len() && matches!(bytes[i], b' ' | b'\t') {
i += 1;
}
continue;
}
b'x' => {
if i + 2 >= bytes.len() {
return Err(YamlStringError::InvalidEscape);
}
let hex = &bytes[i + 1..i + 3];
let val = parse_hex(hex)?;
if val < 0x20 || val == 0x22 || val == 0x5C {
write_json_escape(
output,
char::from_u32(val as u32).unwrap_or('\u{FFFD}'),
);
} else if val <= 0x7F {
output.push(val as u8 as char);
} else {
write_json_escape(
output,
char::from_u32(val as u32).unwrap_or('\u{FFFD}'),
);
}
i += 2;
}
b'u' => {
if i + 4 >= bytes.len() {
return Err(YamlStringError::InvalidEscape);
}
let hex = &bytes[i + 1..i + 5];
let codepoint = parse_hex(hex)? as u32;
let ch = char::from_u32(codepoint).ok_or(YamlStringError::InvalidEscape)?;
write_json_escape(output, ch);
i += 4;
}
b'U' => {
if i + 8 >= bytes.len() {
return Err(YamlStringError::InvalidEscape);
}
let hex = &bytes[i + 1..i + 9];
let codepoint = parse_hex(hex)?;
let ch = char::from_u32(codepoint).ok_or(YamlStringError::InvalidEscape)?;
write_json_escape(output, ch);
i += 8;
}
_ => return Err(YamlStringError::InvalidEscape),
}
i += 1;
}
b'\r' | b'\n' => {
i = transcode_fold_line_break_to_json(bytes, i, output);
}
b'"' => {
output.push_str("\\\"");
i += 1;
}
b if b < 0x20 => {
write_json_escape(output, b as char);
i += 1;
}
_ => {
let start = i;
while i < bytes.len() {
let b = bytes[i];
if matches!(b, b'\\' | b'\n' | b'\r' | b'"') || b < 0x20 {
break;
}
i += 1;
}
let chunk = core::str::from_utf8(&bytes[start..i])
.map_err(|_| YamlStringError::InvalidUtf8)?;
output.push_str(chunk);
}
}
}
output.push('"');
Ok(())
}
fn transcode_single_quoted_to_json(
output: &mut String,
bytes: &[u8],
) -> Result<(), YamlStringError> {
output.push('"');
let mut i = 0;
while i < bytes.len() {
match bytes[i] {
b'\'' if i + 1 < bytes.len() && bytes[i + 1] == b'\'' => {
output.push('\'');
i += 2;
}
b'\r' | b'\n' => {
i = transcode_fold_line_break_to_json(bytes, i, output);
}
b'"' => {
output.push_str("\\\"");
i += 1;
}
b'\\' => {
output.push_str("\\\\");
i += 1;
}
b if b < 0x20 => {
write_json_escape(output, b as char);
i += 1;
}
_ => {
let start = i;
while i < bytes.len() {
let b = bytes[i];
if matches!(b, b'\n' | b'\r' | b'"' | b'\\') || b < 0x20 {
break;
}
if b == b'\'' && i + 1 < bytes.len() && bytes[i + 1] == b'\'' {
break;
}
i += 1;
}
let chunk = core::str::from_utf8(&bytes[start..i])
.map_err(|_| YamlStringError::InvalidUtf8)?;
output.push_str(chunk);
}
}
}
output.push('"');
Ok(())
}
fn transcode_fold_line_break_to_json(bytes: &[u8], mut i: usize, output: &mut String) -> usize {
while output.ends_with(' ') || output.ends_with('\t') {
output.pop();
}
if bytes[i] == b'\r' {
i += 1;
if i < bytes.len() && bytes[i] == b'\n' {
i += 1;
}
} else if bytes[i] == b'\n' {
i += 1;
}
let mut empty_lines = 0;
loop {
while i < bytes.len() && matches!(bytes[i], b' ' | b'\t') {
i += 1;
}
if i < bytes.len() && (bytes[i] == b'\n' || bytes[i] == b'\r') {
empty_lines += 1;
if bytes[i] == b'\r' {
i += 1;
if i < bytes.len() && bytes[i] == b'\n' {
i += 1;
}
} else {
i += 1;
}
} else {
break;
}
}
if empty_lines == 0 {
output.push(' ');
} else {
for _ in 0..empty_lines {
output.push_str("\\n");
}
}
i
}
fn write_yaml_string_to_json(
output: &mut String,
s: &YamlString<'_>,
) -> Result<bool, YamlStringError> {
match s {
YamlString::DoubleQuoted { text, start } => {
let end = YamlString::find_double_quote_end(text, *start);
let bytes = &text[*start + 1..end - 1];
if !bytes.contains(&b'\\') && !bytes.contains(&b'\n') && !bytes.contains(&b'\r') {
let s = core::str::from_utf8(bytes).map_err(|_| YamlStringError::InvalidUtf8)?;
write_json_string(output, s);
} else {
transcode_double_quoted_to_json(output, bytes)?;
}
Ok(true) }
YamlString::SingleQuoted { text, start } => {
let end = YamlString::find_single_quote_end(text, *start);
let bytes = &text[*start + 1..end - 1];
if !bytes.contains(&b'\'') && !bytes.contains(&b'\n') && !bytes.contains(&b'\r') {
let s = core::str::from_utf8(bytes).map_err(|_| YamlStringError::InvalidUtf8)?;
write_json_string(output, s);
} else {
transcode_single_quoted_to_json(output, bytes)?;
}
Ok(true) }
YamlString::Unquoted { .. }
| YamlString::BlockLiteral { .. }
| YamlString::BlockFolded { .. } => {
Ok(false)
}
}
}
#[inline]
fn write_i64(output: &mut String, mut n: i64) {
if n == 0 {
output.push('0');
return;
}
if n < 0 {
output.push('-');
if n == i64::MIN {
output.push_str("9223372036854775808");
return;
}
n = -n;
}
let mut buf = [0u8; 20];
let mut i = buf.len();
while n > 0 {
i -= 1;
buf[i] = b'0' + (n % 10) as u8;
n /= 10;
}
output.push_str(unsafe { core::str::from_utf8_unchecked(&buf[i..]) });
}
#[inline]
fn write_f64(output: &mut String, f: f64) {
if f.fract() == 0.0 && f.abs() < 9007199254740992.0 {
write_i64(output, f as i64);
} else {
output.push_str(&f.to_string());
}
}
#[inline]
fn write_yaml_scalar_as_json(output: &mut String, str_val: &str) {
let bytes = str_val.as_bytes();
if bytes.is_empty() {
output.push_str("null");
return;
}
let first = bytes[0];
match first {
b'n' => {
if str_val == "null" {
output.push_str("null");
return;
}
}
b'~' => {
if bytes.len() == 1 {
output.push_str("null");
return;
}
}
b't' => {
if str_val == "true" {
output.push_str("true");
return;
}
}
b'T' => {
if str_val == "True" || str_val == "TRUE" {
output.push_str("true");
return;
}
}
b'f' => {
if str_val == "false" {
output.push_str("false");
return;
}
}
b'F' => {
if str_val == "False" || str_val == "FALSE" {
output.push_str("false");
return;
}
}
b'.' => match str_val {
".inf" | ".Inf" | ".INF" | ".nan" | ".NaN" | ".NAN" => {
output.push_str("null");
return;
}
_ => {}
},
b'-' => {
if bytes.len() > 1 {
match bytes[1] {
b'.' => {
if str_val == "-.inf" || str_val == "-.Inf" || str_val == "-.INF" {
output.push_str("null");
return;
}
}
b'0'..=b'9' => {
if let Ok(n) = str_val.parse::<i64>() {
write_i64(output, n);
return;
}
if let Ok(f) = str_val.parse::<f64>() {
if !f.is_nan() && !f.is_infinite() {
write_f64(output, f);
return;
}
}
}
_ => {}
}
}
}
b'0'..=b'9' => {
if let Ok(n) = str_val.parse::<i64>() {
write_i64(output, n);
return;
}
if let Ok(f) = str_val.parse::<f64>() {
if !f.is_nan() && !f.is_infinite() {
write_f64(output, f);
return;
}
}
}
b'+' => {
if bytes.len() > 1 && bytes[1].is_ascii_digit() {
if let Ok(n) = str_val.parse::<i64>() {
write_i64(output, n);
return;
}
if let Ok(f) = str_val.parse::<f64>() {
if !f.is_nan() && !f.is_infinite() {
write_f64(output, f);
return;
}
}
}
}
_ => {}
}
write_json_string(output, str_val);
}
fn stream_yaml_value_as_json<W: AsRef<[u64]>, Out: core::fmt::Write>(
out: &mut Out,
value: YamlValue<'_, W>,
) -> core::fmt::Result {
match value {
YamlValue::Null => out.write_str("null"),
YamlValue::String(s) => match stream_yaml_string_to_json(out, &s) {
Ok(true) => Ok(()),
Ok(false) => {
if let Ok(str_val) = s.as_str() {
stream_yaml_scalar_as_json(out, &str_val)
} else {
out.write_str("null")
}
}
Err(_) => out.write_str("null"),
},
YamlValue::Mapping(fields) => {
out.write_char('{')?;
let mut first = true;
for field in fields {
if !first {
out.write_char(',')?;
}
first = false;
if let YamlValue::String(s) = field.key() {
match stream_yaml_string_to_json(out, &s) {
Ok(true) => {}
Ok(false) | Err(_) => {
if let Ok(key_str) = s.as_str() {
stream_json_string(out, &key_str)?;
} else {
out.write_str("\"\"")?;
}
}
}
} else {
out.write_str("\"\"")?;
}
out.write_char(':')?;
field.value_cursor().stream_json_value(out)?;
}
out.write_char('}')
}
YamlValue::Sequence(elements) => {
out.write_char('[')?;
let mut first = true;
for elem in elements {
if !first {
out.write_char(',')?;
}
first = false;
stream_yaml_value_as_json(out, elem)?;
}
out.write_char(']')
}
YamlValue::Alias { target, .. } => {
if let Some(target_cursor) = target {
target_cursor.stream_json_value(out)
} else {
out.write_str("null")
}
}
YamlValue::Error(_) => out.write_str("null"),
}
}
fn stream_json_string<Out: core::fmt::Write>(out: &mut Out, s: &str) -> core::fmt::Result {
out.write_char('"')?;
let bytes = s.as_bytes();
let mut i = 0;
while i < bytes.len() {
let start = i;
while i < bytes.len() {
let b = bytes[i];
if b == b'"' || b == b'\\' || b < 0x20 {
break;
}
i += 1;
}
if start < i {
out.write_str(&s[start..i])?;
}
if i < bytes.len() {
let b = bytes[i];
match b {
b'"' => out.write_str("\\\"")?,
b'\\' => out.write_str("\\\\")?,
b'\n' => out.write_str("\\n")?,
b'\r' => out.write_str("\\r")?,
b'\t' => out.write_str("\\t")?,
b if b < 0x20 => {
out.write_str("\\u00")?;
const HEX: &[u8; 16] = b"0123456789abcdef";
out.write_char(HEX[(b >> 4) as usize] as char)?;
out.write_char(HEX[(b & 0xf) as usize] as char)?;
}
_ => unreachable!(),
}
i += 1;
}
}
out.write_char('"')
}
#[inline]
fn stream_json_escape<Out: core::fmt::Write>(out: &mut Out, ch: char) -> core::fmt::Result {
match ch {
'"' => out.write_str("\\\""),
'\\' => out.write_str("\\\\"),
'\n' => out.write_str("\\n"),
'\r' => out.write_str("\\r"),
'\t' => out.write_str("\\t"),
c if (c as u32) < 0x20 => {
out.write_str("\\u00")?;
const HEX: &[u8; 16] = b"0123456789abcdef";
let b = c as u8;
out.write_char(HEX[(b >> 4) as usize] as char)?;
out.write_char(HEX[(b & 0xf) as usize] as char)
}
c if (c as u32) >= 0x80 && (c as u32) < 0x100 => {
out.write_str("\\u00")?;
const HEX: &[u8; 16] = b"0123456789abcdef";
let b = c as u8;
out.write_char(HEX[(b >> 4) as usize] as char)?;
out.write_char(HEX[(b & 0xf) as usize] as char)
}
c if (c as u32) >= 0x100 => {
let cp = c as u32;
if cp <= 0xFFFF {
out.write_str("\\u")?;
const HEX: &[u8; 16] = b"0123456789abcdef";
out.write_char(HEX[((cp >> 12) & 0xF) as usize] as char)?;
out.write_char(HEX[((cp >> 8) & 0xF) as usize] as char)?;
out.write_char(HEX[((cp >> 4) & 0xF) as usize] as char)?;
out.write_char(HEX[(cp & 0xF) as usize] as char)
} else {
let adjusted = cp - 0x10000;
let high = 0xD800 + (adjusted >> 10);
let low = 0xDC00 + (adjusted & 0x3FF);
out.write_str("\\u")?;
const HEX: &[u8; 16] = b"0123456789abcdef";
out.write_char(HEX[((high >> 12) & 0xF) as usize] as char)?;
out.write_char(HEX[((high >> 8) & 0xF) as usize] as char)?;
out.write_char(HEX[((high >> 4) & 0xF) as usize] as char)?;
out.write_char(HEX[(high & 0xF) as usize] as char)?;
out.write_str("\\u")?;
out.write_char(HEX[((low >> 12) & 0xF) as usize] as char)?;
out.write_char(HEX[((low >> 8) & 0xF) as usize] as char)?;
out.write_char(HEX[((low >> 4) & 0xF) as usize] as char)?;
out.write_char(HEX[(low & 0xF) as usize] as char)
}
}
c => out.write_char(c),
}
}
fn stream_transcode_double_quoted_to_json<Out: core::fmt::Write>(
out: &mut Out,
bytes: &[u8],
) -> Result<(), YamlStringError> {
out.write_char('"')
.map_err(|_| YamlStringError::InvalidUtf8)?;
let mut i = 0;
while i < bytes.len() {
match bytes[i] {
b'\\' => {
if i + 1 >= bytes.len() {
return Err(YamlStringError::InvalidEscape);
}
i += 1;
match bytes[i] {
b'n' => out
.write_str("\\n")
.map_err(|_| YamlStringError::InvalidUtf8)?,
b'r' => out
.write_str("\\r")
.map_err(|_| YamlStringError::InvalidUtf8)?,
b't' | b'\t' => out
.write_str("\\t")
.map_err(|_| YamlStringError::InvalidUtf8)?,
b'"' => out
.write_str("\\\"")
.map_err(|_| YamlStringError::InvalidUtf8)?,
b'\\' => out
.write_str("\\\\")
.map_err(|_| YamlStringError::InvalidUtf8)?,
b'/' => out
.write_char('/')
.map_err(|_| YamlStringError::InvalidUtf8)?,
b' ' => out
.write_char(' ')
.map_err(|_| YamlStringError::InvalidUtf8)?,
b'0' => out
.write_str("\\u0000")
.map_err(|_| YamlStringError::InvalidUtf8)?,
b'a' => out
.write_str("\\u0007")
.map_err(|_| YamlStringError::InvalidUtf8)?,
b'b' => out
.write_str("\\u0008")
.map_err(|_| YamlStringError::InvalidUtf8)?,
b'v' => out
.write_str("\\u000b")
.map_err(|_| YamlStringError::InvalidUtf8)?,
b'f' => out
.write_str("\\u000c")
.map_err(|_| YamlStringError::InvalidUtf8)?,
b'e' => out
.write_str("\\u001b")
.map_err(|_| YamlStringError::InvalidUtf8)?,
b'N' => out
.write_str("\\u0085")
.map_err(|_| YamlStringError::InvalidUtf8)?,
b'_' => out
.write_str("\\u00a0")
.map_err(|_| YamlStringError::InvalidUtf8)?,
b'L' => out
.write_str("\\u2028")
.map_err(|_| YamlStringError::InvalidUtf8)?,
b'P' => out
.write_str("\\u2029")
.map_err(|_| YamlStringError::InvalidUtf8)?,
b'\n' => {
i += 1;
while i < bytes.len() && matches!(bytes[i], b' ' | b'\t') {
i += 1;
}
continue;
}
b'\r' => {
i += 1;
if i < bytes.len() && bytes[i] == b'\n' {
i += 1;
}
while i < bytes.len() && matches!(bytes[i], b' ' | b'\t') {
i += 1;
}
continue;
}
b'x' => {
if i + 2 >= bytes.len() {
return Err(YamlStringError::InvalidEscape);
}
let hex = &bytes[i + 1..i + 3];
let val = parse_hex(hex)?;
let ch = char::from_u32(val as u32).unwrap_or('\u{FFFD}');
if val < 0x20 || val == 0x22 || val == 0x5C {
stream_json_escape(out, ch)
.map_err(|_| YamlStringError::InvalidUtf8)?;
} else if val <= 0x7F {
out.write_char(val as u8 as char)
.map_err(|_| YamlStringError::InvalidUtf8)?;
} else {
stream_json_escape(out, ch)
.map_err(|_| YamlStringError::InvalidUtf8)?;
}
i += 2;
}
b'u' => {
if i + 4 >= bytes.len() {
return Err(YamlStringError::InvalidEscape);
}
let hex = &bytes[i + 1..i + 5];
let codepoint = parse_hex(hex)? as u32;
let ch = char::from_u32(codepoint).ok_or(YamlStringError::InvalidEscape)?;
stream_json_escape(out, ch).map_err(|_| YamlStringError::InvalidUtf8)?;
i += 4;
}
b'U' => {
if i + 8 >= bytes.len() {
return Err(YamlStringError::InvalidEscape);
}
let hex = &bytes[i + 1..i + 9];
let codepoint = parse_hex(hex)?;
let ch = char::from_u32(codepoint).ok_or(YamlStringError::InvalidEscape)?;
stream_json_escape(out, ch).map_err(|_| YamlStringError::InvalidUtf8)?;
i += 8;
}
_ => return Err(YamlStringError::InvalidEscape),
}
i += 1;
}
b'\r' | b'\n' => {
i = stream_transcode_fold_line_break(bytes, i, out)?;
}
b'"' => {
out.write_str("\\\"")
.map_err(|_| YamlStringError::InvalidUtf8)?;
i += 1;
}
b if b < 0x20 => {
stream_json_escape(out, b as char).map_err(|_| YamlStringError::InvalidUtf8)?;
i += 1;
}
_ => {
let start = i;
while i < bytes.len() {
let b = bytes[i];
if matches!(b, b'\\' | b'\n' | b'\r' | b'"') || b < 0x20 {
break;
}
i += 1;
}
let chunk = core::str::from_utf8(&bytes[start..i])
.map_err(|_| YamlStringError::InvalidUtf8)?;
out.write_str(chunk)
.map_err(|_| YamlStringError::InvalidUtf8)?;
}
}
}
out.write_char('"')
.map_err(|_| YamlStringError::InvalidUtf8)?;
Ok(())
}
fn stream_transcode_single_quoted_to_json<Out: core::fmt::Write>(
out: &mut Out,
bytes: &[u8],
) -> Result<(), YamlStringError> {
out.write_char('"')
.map_err(|_| YamlStringError::InvalidUtf8)?;
let mut i = 0;
while i < bytes.len() {
match bytes[i] {
b'\'' if i + 1 < bytes.len() && bytes[i + 1] == b'\'' => {
out.write_char('\'')
.map_err(|_| YamlStringError::InvalidUtf8)?;
i += 2;
}
b'\r' | b'\n' => {
i = stream_transcode_fold_line_break(bytes, i, out)?;
}
b'"' => {
out.write_str("\\\"")
.map_err(|_| YamlStringError::InvalidUtf8)?;
i += 1;
}
b'\\' => {
out.write_str("\\\\")
.map_err(|_| YamlStringError::InvalidUtf8)?;
i += 1;
}
b if b < 0x20 => {
stream_json_escape(out, b as char).map_err(|_| YamlStringError::InvalidUtf8)?;
i += 1;
}
_ => {
let start = i;
while i < bytes.len() {
let b = bytes[i];
if matches!(b, b'\'' | b'\n' | b'\r' | b'"' | b'\\') || b < 0x20 {
break;
}
i += 1;
}
let chunk = core::str::from_utf8(&bytes[start..i])
.map_err(|_| YamlStringError::InvalidUtf8)?;
out.write_str(chunk)
.map_err(|_| YamlStringError::InvalidUtf8)?;
}
}
}
out.write_char('"')
.map_err(|_| YamlStringError::InvalidUtf8)?;
Ok(())
}
fn stream_transcode_fold_line_break<Out: core::fmt::Write>(
bytes: &[u8],
mut i: usize,
out: &mut Out,
) -> Result<usize, YamlStringError> {
let mut newlines = 0;
while i < bytes.len() && matches!(bytes[i], b'\r' | b'\n') {
if bytes[i] == b'\r' {
i += 1;
if i < bytes.len() && bytes[i] == b'\n' {
i += 1;
}
} else {
i += 1;
}
newlines += 1;
while i < bytes.len() && matches!(bytes[i], b' ' | b'\t') {
i += 1;
}
}
if newlines == 1 {
out.write_char(' ')
.map_err(|_| YamlStringError::InvalidUtf8)?;
} else {
for _ in 1..newlines {
out.write_str("\\n")
.map_err(|_| YamlStringError::InvalidUtf8)?;
}
}
Ok(i)
}
fn stream_yaml_string_to_json<Out: core::fmt::Write>(
out: &mut Out,
s: &YamlString<'_>,
) -> Result<bool, YamlStringError> {
match s {
YamlString::DoubleQuoted { text, start } => {
let end = YamlString::find_double_quote_end(text, *start);
let bytes = &text[*start + 1..end - 1];
if !bytes.contains(&b'\\') && !bytes.contains(&b'\n') && !bytes.contains(&b'\r') {
let s = core::str::from_utf8(bytes).map_err(|_| YamlStringError::InvalidUtf8)?;
stream_json_string(out, s).map_err(|_| YamlStringError::InvalidUtf8)?;
} else {
stream_transcode_double_quoted_to_json(out, bytes)?;
}
Ok(true)
}
YamlString::SingleQuoted { text, start } => {
let end = YamlString::find_single_quote_end(text, *start);
let bytes = &text[*start + 1..end - 1];
if !bytes.contains(&b'\'') && !bytes.contains(&b'\n') && !bytes.contains(&b'\r') {
let s = core::str::from_utf8(bytes).map_err(|_| YamlStringError::InvalidUtf8)?;
stream_json_string(out, s).map_err(|_| YamlStringError::InvalidUtf8)?;
} else {
stream_transcode_single_quoted_to_json(out, bytes)?;
}
Ok(true)
}
YamlString::Unquoted { .. }
| YamlString::BlockLiteral { .. }
| YamlString::BlockFolded { .. } => Ok(false),
}
}
fn stream_yaml_scalar_as_json<Out: core::fmt::Write>(
out: &mut Out,
str_val: &str,
) -> core::fmt::Result {
let bytes = str_val.as_bytes();
if bytes.is_empty() {
return out.write_str("null");
}
let first = bytes[0];
match first {
b'n' => {
if str_val == "null" {
return out.write_str("null");
}
}
b'~' => {
if bytes.len() == 1 {
return out.write_str("null");
}
}
b't' => {
if str_val == "true" {
return out.write_str("true");
}
}
b'T' => {
if str_val == "True" || str_val == "TRUE" {
return out.write_str("true");
}
}
b'f' => {
if str_val == "false" {
return out.write_str("false");
}
}
b'F' => {
if str_val == "False" || str_val == "FALSE" {
return out.write_str("false");
}
}
b'.' => match str_val {
".inf" | ".Inf" | ".INF" | ".nan" | ".NaN" | ".NAN" => {
return out.write_str("null");
}
_ => {}
},
b'-' => {
if bytes.len() > 1 {
match bytes[1] {
b'.' => {
if str_val == "-.inf" || str_val == "-.Inf" || str_val == "-.INF" {
return out.write_str("null");
}
}
b'0'..=b'9' => {
if let Ok(n) = str_val.parse::<i64>() {
return write!(out, "{}", n);
}
if let Ok(f) = str_val.parse::<f64>() {
if !f.is_nan() && !f.is_infinite() {
return write!(out, "{}", f);
}
}
}
_ => {}
}
}
}
b'0'..=b'9' => {
if let Ok(n) = str_val.parse::<i64>() {
return write!(out, "{}", n);
}
if let Ok(f) = str_val.parse::<f64>() {
if !f.is_nan() && !f.is_infinite() {
return write!(out, "{}", f);
}
}
}
b'+' => {
if bytes.len() > 1 && bytes[1].is_ascii_digit() {
if let Ok(n) = str_val.parse::<i64>() {
return write!(out, "{}", n);
}
if let Ok(f) = str_val.parse::<f64>() {
if !f.is_nan() && !f.is_infinite() {
return write!(out, "{}", f);
}
}
}
}
_ => {}
}
stream_json_string(out, str_val)
}
#[derive(Debug)]
pub struct YamlChildren<'a, W = Vec<u64>> {
current: Option<YamlCursor<'a, W>>,
}
impl<'a, W> Clone for YamlChildren<'a, W> {
fn clone(&self) -> Self {
*self
}
}
impl<'a, W> Copy for YamlChildren<'a, W> {}
impl<'a, W: AsRef<[u64]>> Iterator for YamlChildren<'a, W> {
type Item = YamlCursor<'a, W>;
#[inline]
fn next(&mut self) -> Option<Self::Item> {
let cursor = self.current?;
self.current = cursor.next_sibling();
Some(cursor)
}
}
#[derive(Clone, Debug)]
pub enum YamlValue<'a, W = Vec<u64>> {
Null,
String(YamlString<'a>),
Mapping(YamlFields<'a, W>),
Sequence(YamlElements<'a, W>),
Alias {
anchor_name: &'a str,
target: Option<YamlCursor<'a, W>>,
},
Error(&'static str),
}
#[derive(Debug)]
pub struct YamlFields<'a, W = Vec<u64>> {
key_cursor: Option<YamlCursor<'a, W>>,
}
impl<'a, W> Clone for YamlFields<'a, W> {
fn clone(&self) -> Self {
*self
}
}
impl<'a, W> Copy for YamlFields<'a, W> {}
impl<'a, W: AsRef<[u64]>> YamlFields<'a, W> {
pub fn from_mapping_cursor(mapping_cursor: YamlCursor<'a, W>) -> Self {
Self {
key_cursor: mapping_cursor.first_child(),
}
}
#[inline]
pub fn is_empty(&self) -> bool {
self.key_cursor.is_none()
}
pub fn uncons(&self) -> Option<(YamlField<'a, W>, YamlFields<'a, W>)> {
let key_cursor = self.key_cursor?;
let value_cursor = key_cursor.next_sibling()?;
let rest = YamlFields {
key_cursor: value_cursor.next_sibling(),
};
let field = YamlField {
key_cursor,
value_cursor,
};
Some((field, rest))
}
pub fn find(&self, name: &str) -> Option<YamlValue<'a, W>> {
let mut fields = *self;
while let Some((field, rest)) = fields.uncons() {
if let YamlValue::String(key) = field.key() {
if key.as_str().ok()? == name {
return Some(field.value());
}
}
fields = rest;
}
None
}
}
impl<'a, W: AsRef<[u64]>> Iterator for YamlFields<'a, W> {
type Item = YamlField<'a, W>;
fn next(&mut self) -> Option<Self::Item> {
let (field, rest) = self.uncons()?;
*self = rest;
Some(field)
}
}
#[derive(Debug)]
pub struct YamlField<'a, W = Vec<u64>> {
key_cursor: YamlCursor<'a, W>,
value_cursor: YamlCursor<'a, W>,
}
impl<'a, W> Clone for YamlField<'a, W> {
fn clone(&self) -> Self {
*self
}
}
impl<'a, W> Copy for YamlField<'a, W> {}
impl<'a, W: AsRef<[u64]>> YamlField<'a, W> {
#[inline]
pub fn key(&self) -> YamlValue<'a, W> {
self.key_cursor.value()
}
#[inline]
pub fn value(&self) -> YamlValue<'a, W> {
self.value_cursor.value()
}
#[inline]
pub fn value_cursor(&self) -> YamlCursor<'a, W> {
self.value_cursor
}
}
#[derive(Debug)]
pub struct YamlElements<'a, W = Vec<u64>> {
element_cursor: Option<YamlCursor<'a, W>>,
}
impl<'a, W> Clone for YamlElements<'a, W> {
fn clone(&self) -> Self {
*self
}
}
impl<'a, W> Copy for YamlElements<'a, W> {}
impl<'a, W: AsRef<[u64]>> YamlElements<'a, W> {
pub fn from_sequence_cursor(sequence_cursor: YamlCursor<'a, W>) -> Self {
Self {
element_cursor: sequence_cursor.first_child(),
}
}
#[inline]
pub fn is_empty(&self) -> bool {
self.element_cursor.is_none()
}
pub fn uncons_cursor(&self) -> Option<(YamlCursor<'a, W>, YamlElements<'a, W>)> {
let element_cursor = self.element_cursor?;
let rest = YamlElements {
element_cursor: element_cursor.next_sibling(),
};
if element_cursor.is_container() {
Some((element_cursor, rest))
} else if let Some(text_pos) = element_cursor.text_position() {
if text_pos < element_cursor.text.len()
&& element_cursor.text[text_pos] == b'-'
&& text_pos + 1 < element_cursor.text.len()
&& (element_cursor.text[text_pos + 1] == b' '
|| element_cursor.text[text_pos + 1] == b'\t')
{
if let Some(child) = element_cursor.first_child() {
Some((child, rest))
} else {
Some((element_cursor, rest))
}
} else {
Some((element_cursor, rest))
}
} else {
Some((element_cursor, rest))
}
}
pub fn uncons(&self) -> Option<(YamlValue<'a, W>, YamlElements<'a, W>)> {
let element_cursor = self.element_cursor?;
let rest = YamlElements {
element_cursor: element_cursor.next_sibling(),
};
let value = if element_cursor.is_container() {
element_cursor.value()
} else if let Some(text_pos) = element_cursor.text_position() {
if text_pos < element_cursor.text.len()
&& element_cursor.text[text_pos] == b'-'
&& text_pos + 1 < element_cursor.text.len()
&& (element_cursor.text[text_pos + 1] == b' '
|| element_cursor.text[text_pos + 1] == b'\t')
{
if let Some(child) = element_cursor.first_child() {
child.value()
} else {
YamlValue::Null
}
} else {
element_cursor.value()
}
} else {
element_cursor.value()
};
Some((value, rest))
}
pub fn get(&self, index: usize) -> Option<YamlValue<'a, W>> {
let mut cursor = self.element_cursor?;
for _ in 0..index {
cursor = cursor.next_sibling()?;
}
let value_cursor = if cursor.is_container() {
cursor
} else if let Some(text_pos) = cursor.text_position() {
if text_pos < cursor.text.len()
&& cursor.text[text_pos] == b'-'
&& cursor.first_child().is_some()
{
cursor.first_child().unwrap()
} else {
cursor
}
} else {
cursor
};
Some(value_cursor.value())
}
}
impl<'a, W: AsRef<[u64]>> Iterator for YamlElements<'a, W> {
type Item = YamlValue<'a, W>;
fn next(&mut self) -> Option<Self::Item> {
let (elem, rest) = self.uncons()?;
*self = rest;
Some(elem)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ChompingIndicator {
Clip,
Strip,
Keep,
}
#[derive(Clone, Debug)]
pub enum YamlString<'a> {
DoubleQuoted { text: &'a [u8], start: usize },
SingleQuoted { text: &'a [u8], start: usize },
Unquoted {
text: &'a [u8],
start: usize,
end: usize,
base_indent: usize,
},
BlockLiteral {
text: &'a [u8],
indicator_pos: usize,
chomping: ChompingIndicator,
explicit_indent: Option<u8>,
},
BlockFolded {
text: &'a [u8],
indicator_pos: usize,
chomping: ChompingIndicator,
explicit_indent: Option<u8>,
},
}
impl<'a> YamlString<'a> {
pub fn is_unquoted(&self) -> bool {
matches!(self, YamlString::Unquoted { .. })
}
pub fn raw_bytes(&self) -> &'a [u8] {
match self {
YamlString::DoubleQuoted { text, start } => {
let end = Self::find_double_quote_end(text, *start);
&text[*start..end]
}
YamlString::SingleQuoted { text, start } => {
let end = Self::find_single_quote_end(text, *start);
&text[*start..end]
}
YamlString::Unquoted {
text, start, end, ..
} => &text[*start..*end],
YamlString::BlockLiteral {
text,
indicator_pos,
chomping,
explicit_indent,
}
| YamlString::BlockFolded {
text,
indicator_pos,
chomping,
explicit_indent,
} => {
let (_, content_end) = Self::find_block_content_range(
text,
*indicator_pos,
*chomping,
*explicit_indent,
);
&text[*indicator_pos..content_end]
}
}
}
pub fn as_str(&self) -> Result<Cow<'a, str>, YamlStringError> {
match self {
YamlString::DoubleQuoted { text, start } => {
let end = Self::find_double_quote_end(text, *start);
let bytes = &text[*start + 1..end - 1]; if !bytes.contains(&b'\\') && !bytes.contains(&b'\n') && !bytes.contains(&b'\r') {
let s =
core::str::from_utf8(bytes).map_err(|_| YamlStringError::InvalidUtf8)?;
Ok(Cow::Borrowed(s))
} else {
decode_double_quoted(bytes).map(Cow::Owned)
}
}
YamlString::SingleQuoted { text, start } => {
let end = Self::find_single_quote_end(text, *start);
let bytes = &text[*start + 1..end - 1]; if !bytes.contains(&b'\'') && !bytes.contains(&b'\n') && !bytes.contains(&b'\r') {
let s =
core::str::from_utf8(bytes).map_err(|_| YamlStringError::InvalidUtf8)?;
Ok(Cow::Borrowed(s))
} else {
decode_single_quoted(bytes).map(Cow::Owned)
}
}
YamlString::Unquoted {
text,
start,
end,
base_indent,
} => {
let bytes = &text[*start..*end];
if !bytes.contains(&b'\n') && !bytes.contains(&b'\r') {
let s =
core::str::from_utf8(bytes).map_err(|_| YamlStringError::InvalidUtf8)?;
Ok(Cow::Borrowed(s))
} else {
decode_plain_scalar(bytes, *base_indent).map(Cow::Owned)
}
}
YamlString::BlockLiteral {
text,
indicator_pos,
chomping,
explicit_indent,
} => decode_block_literal(text, *indicator_pos, *chomping, *explicit_indent),
YamlString::BlockFolded {
text,
indicator_pos,
chomping,
explicit_indent,
} => decode_block_folded(text, *indicator_pos, *chomping, *explicit_indent),
}
}
fn find_double_quote_end(text: &[u8], start: usize) -> usize {
let mut i = start + 1;
while i < text.len() {
match text[i] {
b'"' => return i + 1,
b'\\' => i += 2,
_ => i += 1,
}
}
text.len()
}
fn find_single_quote_end(text: &[u8], start: usize) -> usize {
let mut i = start + 1;
while i < text.len() {
if text[i] == b'\'' {
if i + 1 < text.len() && text[i + 1] == b'\'' {
i += 2;
} else {
return i + 1;
}
} else {
i += 1;
}
}
text.len()
}
fn find_block_content_range(
text: &[u8],
indicator_pos: usize,
chomping: ChompingIndicator,
explicit_indent: Option<u8>,
) -> (usize, usize) {
let base_indent = Self::compute_key_indent(text, indicator_pos);
let on_document_start_line = Self::is_document_start_line(text, indicator_pos);
let mut pos = indicator_pos + 1;
while pos < text.len() && text[pos] != b'\n' {
pos += 1;
}
if pos >= text.len() {
return (pos, pos); }
pos += 1;
let content_start = pos;
let content_indent = if let Some(indent) = explicit_indent {
base_indent + indent as usize
} else {
match Self::detect_block_indent(text, pos) {
Some(indent) if indent > base_indent || (on_document_start_line && indent == 0) => {
indent
}
Some(_) | None => {
if chomping == ChompingIndicator::Keep {
let mut end = content_start;
while end < text.len() {
let mut spaces = 0;
while end + spaces < text.len() && text[end + spaces] == b' ' {
spaces += 1;
}
if end + spaces >= text.len() {
break;
}
match text[end + spaces] {
b'\n' => {
end += spaces + 1;
}
b'\r' => {
end += spaces + 1;
if end < text.len() && text[end] == b'\n' {
end += 1;
}
}
_ => {
break;
}
}
}
if end == content_start && content_start > 0 {
let prev = content_start - 1;
if text[prev] == b'\n'
|| (text[prev] == b'\r'
&& (content_start >= text.len()
|| text[content_start] != b'\n'))
{
return (prev, content_start);
}
}
return (content_start, end);
} else {
return (content_start, content_start);
}
}
}
};
let mut last_content_end = pos;
let mut has_content = false;
while pos < text.len() {
let line_start = pos;
let mut line_indent = 0;
while pos < text.len() && text[pos] == b' ' {
line_indent += 1;
pos += 1;
}
if pos >= text.len() {
if line_indent >= content_indent {
last_content_end = pos;
has_content = true;
}
break;
}
match text[pos] {
b'\n' => {
pos += 1;
}
b'\r' => {
pos += 1;
if pos < text.len() && text[pos] == b'\n' {
pos += 1;
}
}
_ => {
if line_indent < content_indent {
pos = line_start;
break;
}
while pos < text.len() && text[pos] != b'\n' && text[pos] != b'\r' {
pos += 1;
}
last_content_end = pos;
has_content = true;
if pos < text.len() {
if text[pos] == b'\r' {
pos += 1;
if pos < text.len() && text[pos] == b'\n' {
pos += 1;
}
} else if text[pos] == b'\n' {
pos += 1;
}
}
}
}
}
let content_end = match chomping {
ChompingIndicator::Strip => last_content_end,
ChompingIndicator::Clip => {
if has_content {
let mut end = last_content_end;
if end < text.len() && text[end] == b'\n' {
end += 1;
} else if end < text.len() && text[end] == b'\r' {
end += 1;
if end < text.len() && text[end] == b'\n' {
end += 1;
}
}
end
} else {
last_content_end
}
}
ChompingIndicator::Keep => pos, };
(content_start, content_end)
}
fn compute_key_indent(text: &[u8], indicator_pos: usize) -> usize {
let mut line_start = indicator_pos;
while line_start > 0 && text[line_start - 1] != b'\n' {
line_start -= 1;
}
let mut pos = line_start;
while pos < text.len() && text[pos] == b' ' {
pos += 1;
}
let line_indent = pos - line_start;
if pos < text.len() && text[pos] == b'-' {
if pos + 1 < text.len()
&& (text[pos + 1] == b' ' || text[pos + 1] == b'\t' || text[pos + 1] == b'\n')
{
let has_colon = text
.get((pos + 2)..indicator_pos)
.is_some_and(|slice| slice.contains(&b':'));
if has_colon {
return line_indent + 2;
} else {
return line_indent;
}
}
}
line_indent
}
fn is_document_start_line(text: &[u8], pos: usize) -> bool {
let mut line_start = pos;
while line_start > 0 && text[line_start - 1] != b'\n' {
line_start -= 1;
}
let mut check_pos = line_start;
while check_pos < text.len() && text[check_pos] == b' ' {
check_pos += 1;
}
if check_pos + 2 < text.len()
&& text[check_pos] == b'-'
&& text[check_pos + 1] == b'-'
&& text[check_pos + 2] == b'-'
{
return true;
}
false
}
fn detect_block_indent(text: &[u8], start: usize) -> Option<usize> {
let mut pos = start;
loop {
if pos >= text.len() {
return None;
}
let mut indent = 0;
while pos < text.len() && text[pos] == b' ' {
indent += 1;
pos += 1;
}
if pos >= text.len() {
return None;
}
match text[pos] {
b'\n' => {
pos += 1;
}
b'\r' => {
pos += 1;
if pos < text.len() && text[pos] == b'\n' {
pos += 1;
}
}
_ => {
return Some(indent);
}
}
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum YamlStringError {
InvalidUtf8,
InvalidEscape,
}
impl core::fmt::Display for YamlStringError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
YamlStringError::InvalidUtf8 => write!(f, "invalid UTF-8 in string"),
YamlStringError::InvalidEscape => write!(f, "invalid escape sequence"),
}
}
}
fn decode_double_quoted(bytes: &[u8]) -> Result<String, YamlStringError> {
let mut result = String::with_capacity(bytes.len());
let mut i = 0;
while i < bytes.len() {
match bytes[i] {
b'\\' => {
if i + 1 >= bytes.len() {
return Err(YamlStringError::InvalidEscape);
}
i += 1;
match bytes[i] {
b'0' => result.push('\0'),
b'a' => result.push('\x07'), b'b' => result.push('\x08'), b't' | b'\t' => result.push('\t'),
b'n' => result.push('\n'),
b'v' => result.push('\x0B'), b'f' => result.push('\x0C'), b'r' => result.push('\r'),
b'e' => result.push('\x1B'), b' ' => result.push(' '),
b'"' => result.push('"'),
b'/' => result.push('/'),
b'\\' => result.push('\\'),
b'N' => result.push('\u{0085}'), b'_' => result.push('\u{00A0}'), b'L' => result.push('\u{2028}'), b'P' => result.push('\u{2029}'), b'\n' => {
i += 1;
while i < bytes.len() && matches!(bytes[i], b' ' | b'\t') {
i += 1;
}
continue; }
b'\r' => {
i += 1;
if i < bytes.len() && bytes[i] == b'\n' {
i += 1;
}
while i < bytes.len() && matches!(bytes[i], b' ' | b'\t') {
i += 1;
}
continue;
}
b'x' => {
if i + 2 >= bytes.len() {
return Err(YamlStringError::InvalidEscape);
}
let hex = &bytes[i + 1..i + 3];
let val = parse_hex(hex)?;
if val <= 0x7F {
result.push(val as u8 as char);
} else {
result.push(
char::from_u32(val as u32).ok_or(YamlStringError::InvalidEscape)?,
);
}
i += 2;
}
b'u' => {
if i + 4 >= bytes.len() {
return Err(YamlStringError::InvalidEscape);
}
let hex = &bytes[i + 1..i + 5];
let codepoint = parse_hex(hex)? as u32;
result
.push(char::from_u32(codepoint).ok_or(YamlStringError::InvalidEscape)?);
i += 4;
}
b'U' => {
if i + 8 >= bytes.len() {
return Err(YamlStringError::InvalidEscape);
}
let hex = &bytes[i + 1..i + 9];
let codepoint = parse_hex(hex)?;
result
.push(char::from_u32(codepoint).ok_or(YamlStringError::InvalidEscape)?);
i += 8;
}
_ => return Err(YamlStringError::InvalidEscape),
}
i += 1;
}
b'\r' | b'\n' => {
i = fold_quoted_line_break(bytes, i, &mut result);
}
_ => {
let start = i;
while i < bytes.len() && !matches!(bytes[i], b'\\' | b'\n' | b'\r') {
i += 1;
}
let chunk = core::str::from_utf8(&bytes[start..i])
.map_err(|_| YamlStringError::InvalidUtf8)?;
result.push_str(chunk);
}
}
}
Ok(result)
}
fn decode_single_quoted(bytes: &[u8]) -> Result<String, YamlStringError> {
let mut result = String::with_capacity(bytes.len());
let mut i = 0;
while i < bytes.len() {
match bytes[i] {
b'\'' if i + 1 < bytes.len() && bytes[i + 1] == b'\'' => {
result.push('\'');
i += 2;
}
b'\r' | b'\n' => {
i = fold_quoted_line_break(bytes, i, &mut result);
}
_ => {
let start = i;
while i < bytes.len()
&& !matches!(bytes[i], b'\n' | b'\r')
&& !(bytes[i] == b'\'' && i + 1 < bytes.len() && bytes[i + 1] == b'\'')
{
i += 1;
}
let chunk = core::str::from_utf8(&bytes[start..i])
.map_err(|_| YamlStringError::InvalidUtf8)?;
result.push_str(chunk);
}
}
}
Ok(result)
}
fn fold_quoted_line_break(bytes: &[u8], mut i: usize, result: &mut String) -> usize {
while result.ends_with(' ') || result.ends_with('\t') {
result.pop();
}
if bytes[i] == b'\r' {
i += 1;
if i < bytes.len() && bytes[i] == b'\n' {
i += 1;
}
} else if bytes[i] == b'\n' {
i += 1;
}
let mut empty_lines = 0;
loop {
while i < bytes.len() && matches!(bytes[i], b' ' | b'\t') {
i += 1;
}
if i < bytes.len() && (bytes[i] == b'\n' || bytes[i] == b'\r') {
empty_lines += 1;
if bytes[i] == b'\r' {
i += 1;
if i < bytes.len() && bytes[i] == b'\n' {
i += 1;
}
} else {
i += 1;
}
} else {
break;
}
}
if empty_lines == 0 {
result.push(' ');
} else {
for _ in 0..empty_lines {
result.push('\n');
}
}
i
}
fn decode_plain_scalar(bytes: &[u8], base_indent: usize) -> Result<String, YamlStringError> {
let mut result = String::with_capacity(bytes.len());
let mut i = 0;
while i < bytes.len() {
match bytes[i] {
b'\r' | b'\n' => {
i = fold_plain_line_break(bytes, i, base_indent, &mut result);
}
_ => {
let start = i;
while i < bytes.len() && !matches!(bytes[i], b'\n' | b'\r') {
i += 1;
}
let mut end = i;
while end > start && matches!(bytes[end - 1], b' ' | b'\t') {
end -= 1;
}
let chunk = core::str::from_utf8(&bytes[start..end])
.map_err(|_| YamlStringError::InvalidUtf8)?;
result.push_str(chunk);
}
}
}
Ok(result)
}
fn fold_plain_line_break(
bytes: &[u8],
mut i: usize,
base_indent: usize,
result: &mut String,
) -> usize {
if bytes[i] == b'\r' {
i += 1;
if i < bytes.len() && bytes[i] == b'\n' {
i += 1;
}
} else if bytes[i] == b'\n' {
i += 1;
}
let mut empty_lines = 0;
loop {
let line_start = i;
while i < bytes.len() && bytes[i] == b' ' {
i += 1;
}
let line_indent = i - line_start;
if i < bytes.len() && (bytes[i] == b'\n' || bytes[i] == b'\r') {
empty_lines += 1;
if bytes[i] == b'\r' {
i += 1;
if i < bytes.len() && bytes[i] == b'\n' {
i += 1;
}
} else {
i += 1;
}
} else if i >= bytes.len() {
break;
} else if bytes[i] == b'\t' {
let mut check = i;
while check < bytes.len() && matches!(bytes[check], b'\t' | b' ') {
check += 1;
}
if check >= bytes.len() || matches!(bytes[check], b'\n' | b'\r') {
empty_lines += 1;
i = check;
if i < bytes.len() && bytes[i] == b'\r' {
i += 1;
if i < bytes.len() && bytes[i] == b'\n' {
i += 1;
}
} else if i < bytes.len() && bytes[i] == b'\n' {
i += 1;
}
} else {
while i < bytes.len() && bytes[i] == b'\t' {
i += 1;
}
break;
}
} else {
if line_indent > base_indent {
break;
} else {
break;
}
}
}
if empty_lines == 0 {
if !result.is_empty() {
result.push(' ');
}
} else {
for _ in 0..empty_lines {
result.push('\n');
}
}
i
}
fn parse_hex(hex: &[u8]) -> Result<u32, YamlStringError> {
let mut value = 0u32;
for &b in hex {
let digit = match b {
b'0'..=b'9' => b - b'0',
b'a'..=b'f' => b - b'a' + 10,
b'A'..=b'F' => b - b'A' + 10,
_ => return Err(YamlStringError::InvalidEscape),
};
value = value * 16 + digit as u32;
}
Ok(value)
}
fn decode_block_literal<'a>(
text: &'a [u8],
indicator_pos: usize,
chomping: ChompingIndicator,
explicit_indent: Option<u8>,
) -> Result<Cow<'a, str>, YamlStringError> {
let (content_start, content_end) =
YamlString::find_block_content_range(text, indicator_pos, chomping, explicit_indent);
if content_start >= content_end {
if chomping == ChompingIndicator::Keep {
return Ok(Cow::Borrowed("\n"));
}
return Ok(Cow::Borrowed(""));
}
let content = &text[content_start..content_end];
let base_indent = YamlString::compute_key_indent(text, indicator_pos);
let indent = if let Some(ei) = explicit_indent {
base_indent + ei as usize
} else {
YamlString::detect_block_indent(text, content_start).unwrap_or(0)
};
if indent == 0 {
let s = core::str::from_utf8(content).map_err(|_| YamlStringError::InvalidUtf8)?;
return Ok(Cow::Borrowed(s));
}
let mut result = String::with_capacity(content.len());
let mut pos = 0;
while pos < content.len() {
let mut line_indent = 0;
while pos + line_indent < content.len() && content[pos + line_indent] == b' ' {
line_indent += 1;
}
let skip = line_indent.min(indent);
pos += skip;
let line_start = pos;
while pos < content.len() && content[pos] != b'\n' && content[pos] != b'\r' {
pos += 1;
}
let line = core::str::from_utf8(&content[line_start..pos])
.map_err(|_| YamlStringError::InvalidUtf8)?;
result.push_str(line);
if pos < content.len() {
if content[pos] == b'\r' {
pos += 1;
result.push('\n');
if pos < content.len() && content[pos] == b'\n' {
pos += 1;
}
} else if content[pos] == b'\n' {
pos += 1;
result.push('\n');
}
}
}
match chomping {
ChompingIndicator::Strip => {
while result.ends_with('\n') {
result.pop();
}
}
ChompingIndicator::Clip => {
while result.ends_with("\n\n") {
result.pop();
}
if !result.is_empty() && !result.ends_with('\n') {
result.push('\n');
}
}
ChompingIndicator::Keep => {
}
}
Ok(Cow::Owned(result))
}
fn decode_block_folded<'a>(
text: &'a [u8],
indicator_pos: usize,
chomping: ChompingIndicator,
explicit_indent: Option<u8>,
) -> Result<Cow<'a, str>, YamlStringError> {
let (content_start, content_end) =
YamlString::find_block_content_range(text, indicator_pos, chomping, explicit_indent);
if content_start >= content_end {
if chomping == ChompingIndicator::Keep {
return Ok(Cow::Borrowed("\n"));
}
return Ok(Cow::Borrowed(""));
}
let content = &text[content_start..content_end];
let base_indent = YamlString::compute_key_indent(text, indicator_pos);
let indent = if let Some(ei) = explicit_indent {
base_indent + ei as usize
} else {
YamlString::detect_block_indent(text, content_start).unwrap_or(0)
};
let mut result = String::with_capacity(content.len());
let mut pos = 0;
let mut prev_was_blank = false;
let mut prev_was_more_indented = false;
let mut first_line = true;
while pos < content.len() {
let mut line_indent = 0;
while pos + line_indent < content.len() && content[pos + line_indent] == b' ' {
line_indent += 1;
}
let is_blank = pos + line_indent >= content.len()
|| content[pos + line_indent] == b'\n'
|| content[pos + line_indent] == b'\r';
if is_blank {
if !result.is_empty() && !result.ends_with('\n') {
result.push('\n');
}
result.push('\n');
prev_was_blank = true;
prev_was_more_indented = false;
pos += line_indent;
if pos < content.len() {
if content[pos] == b'\r' {
pos += 1;
if pos < content.len() && content[pos] == b'\n' {
pos += 1;
}
} else if content[pos] == b'\n' {
pos += 1;
}
}
} else {
let skip = line_indent.min(indent);
pos += skip;
let is_more_indented =
line_indent > indent || (pos < content.len() && content[pos] == b'\t');
let line_start = pos;
while pos < content.len() && content[pos] != b'\n' && content[pos] != b'\r' {
pos += 1;
}
if !first_line && !result.is_empty() && !result.ends_with('\n') {
if is_more_indented || prev_was_more_indented || prev_was_blank {
result.push('\n');
} else {
result.push(' ');
}
}
let line = core::str::from_utf8(&content[line_start..pos])
.map_err(|_| YamlStringError::InvalidUtf8)?;
result.push_str(line);
prev_was_blank = false;
prev_was_more_indented = is_more_indented;
first_line = false;
if pos < content.len() {
if content[pos] == b'\r' {
pos += 1;
if pos < content.len() && content[pos] == b'\n' {
pos += 1;
}
} else if content[pos] == b'\n' {
pos += 1;
}
}
}
}
match chomping {
ChompingIndicator::Strip => {
while result.ends_with('\n') {
result.pop();
}
}
ChompingIndicator::Clip => {
while result.ends_with("\n\n") {
result.pop();
}
if !result.is_empty() && !result.ends_with('\n') {
result.push('\n');
}
}
ChompingIndicator::Keep => {
}
}
Ok(Cow::Owned(result))
}
#[derive(Clone, Copy, Debug)]
pub struct YamlNumber<'a> {
text: &'a [u8],
start: usize,
end: usize,
}
impl<'a> YamlNumber<'a> {
pub fn new(text: &'a [u8], start: usize, end: usize) -> Self {
Self { text, start, end }
}
pub fn raw_bytes(&self) -> &'a [u8] {
&self.text[self.start..self.end]
}
pub fn as_i64(&self) -> Result<i64, YamlNumberError> {
let bytes = self.raw_bytes();
let s = core::str::from_utf8(bytes).map_err(|_| YamlNumberError::InvalidUtf8)?;
s.parse().map_err(|_| YamlNumberError::InvalidNumber)
}
pub fn as_f64(&self) -> Result<f64, YamlNumberError> {
let bytes = self.raw_bytes();
let s = core::str::from_utf8(bytes).map_err(|_| YamlNumberError::InvalidUtf8)?;
s.parse().map_err(|_| YamlNumberError::InvalidNumber)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum YamlNumberError {
InvalidUtf8,
InvalidNumber,
}
impl core::fmt::Display for YamlNumberError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
YamlNumberError::InvalidUtf8 => write!(f, "invalid UTF-8 in number"),
YamlNumberError::InvalidNumber => write!(f, "invalid number format"),
}
}
}
use crate::jq::document::{
DocumentCursor, DocumentElements, DocumentField, DocumentFields, DocumentValue,
};
impl<'a, W: AsRef<[u64]> + Clone> DocumentCursor for YamlCursor<'a, W> {
type Value = YamlValue<'a, W>;
#[inline]
fn value(&self) -> Self::Value {
YamlCursor::value(self)
}
#[inline]
fn first_child(&self) -> Option<Self> {
YamlCursor::first_child(self)
}
#[inline]
fn next_sibling(&self) -> Option<Self> {
YamlCursor::next_sibling(self)
}
#[inline]
fn parent(&self) -> Option<Self> {
YamlCursor::parent(self)
}
#[inline]
fn is_container(&self) -> bool {
YamlCursor::is_container(self)
}
#[inline]
fn text_position(&self) -> Option<usize> {
YamlCursor::text_position(self)
}
#[inline]
fn line(&self) -> usize {
YamlCursor::line(self)
}
#[inline]
fn column(&self) -> usize {
YamlCursor::column(self)
}
#[inline]
fn document_index(&self) -> Option<usize> {
YamlCursor::document_index(self)
}
#[inline]
fn cursor_at_offset(&self, offset: usize) -> Option<Self> {
YamlCursor::cursor_at_offset(self, offset)
}
#[inline]
fn cursor_at_position(&self, line: usize, col: usize) -> Option<Self> {
YamlCursor::cursor_at_position(self, line, col)
}
#[inline]
fn stream_json<Out: core::fmt::Write>(&self, out: &mut Out) -> core::fmt::Result {
YamlCursor::stream_json(self, out)
}
#[inline]
fn stream_yaml<Out: core::fmt::Write>(
&self,
out: &mut Out,
indent_spaces: usize,
) -> core::fmt::Result {
YamlCursor::stream_yaml(self, out, indent_spaces)
}
#[inline]
fn is_falsy(&self) -> bool {
match self.value() {
YamlValue::Null => true,
YamlValue::String(s) if s.is_unquoted() => {
if let Ok(str_val) = s.as_str() {
matches!(
str_val.as_ref(),
"null" | "~" | "" | "false" | "False" | "FALSE"
)
} else {
false
}
}
_ => false,
}
}
}
impl<'a, W: AsRef<[u64]> + Clone> DocumentValue for YamlValue<'a, W> {
type Cursor = YamlCursor<'a, W>;
type Fields = YamlFields<'a, W>;
type Elements = YamlElements<'a, W>;
fn is_null(&self) -> bool {
match self {
YamlValue::Null => true,
YamlValue::String(s) if s.is_unquoted() => {
if let Ok(str_val) = s.as_str() {
matches!(str_val.as_ref(), "null" | "~" | "")
} else {
false
}
}
YamlValue::Alias { target, .. } => {
target.map(|t| t.value().is_null()).unwrap_or(true) }
_ => false,
}
}
fn as_bool(&self) -> Option<bool> {
match self {
YamlValue::String(s) if s.is_unquoted() => {
let str_val = s.as_str().ok()?;
match str_val.as_ref() {
"true" | "True" | "TRUE" => Some(true),
"false" | "False" | "FALSE" => Some(false),
_ => None,
}
}
YamlValue::Alias { target, .. } => target.and_then(|t| t.value().as_bool()),
_ => None,
}
}
fn as_i64(&self) -> Option<i64> {
match self {
YamlValue::String(s) if s.is_unquoted() => {
let str_val = s.as_str().ok()?;
str_val.parse().ok()
}
YamlValue::Alias { target, .. } => target.and_then(|t| t.value().as_i64()),
_ => None,
}
}
fn as_f64(&self) -> Option<f64> {
match self {
YamlValue::String(s) if s.is_unquoted() => {
let str_val = s.as_str().ok()?;
match str_val.as_ref() {
".inf" | ".Inf" | ".INF" => Some(f64::INFINITY),
"-.inf" | "-.Inf" | "-.INF" => Some(f64::NEG_INFINITY),
".nan" | ".NaN" | ".NAN" => Some(f64::NAN),
_ => str_val.parse().ok(),
}
}
YamlValue::Alias { target, .. } => target.and_then(|t| t.value().as_f64()),
_ => None,
}
}
fn as_str(&self) -> Option<Cow<'_, str>> {
match self {
YamlValue::String(s) => s.as_str().ok(),
YamlValue::Alias { target, .. } => {
target.and_then(|t| {
if let YamlValue::String(s) = t.value() {
s.as_str().ok().map(|cow| Cow::Owned(cow.into_owned()))
} else {
None
}
})
}
_ => None,
}
}
fn as_object(&self) -> Option<Self::Fields> {
match self {
YamlValue::Mapping(fields) => Some(*fields),
YamlValue::Alias { target, .. } => target.and_then(|t| t.value().as_object()),
_ => None,
}
}
fn as_array(&self) -> Option<Self::Elements> {
match self {
YamlValue::Sequence(elements) => Some(*elements),
YamlValue::Alias { target, .. } => target.and_then(|t| t.value().as_array()),
_ => None,
}
}
fn type_name(&self) -> &'static str {
match self {
YamlValue::Null => "null",
YamlValue::String(s) => {
if s.is_unquoted() {
if let Ok(str_val) = s.as_str() {
match str_val.as_ref() {
"null" | "~" | "" => return "null",
"true" | "True" | "TRUE" | "false" | "False" | "FALSE" => {
return "boolean"
}
_ => {
if str_val.parse::<i64>().is_ok() || str_val.parse::<f64>().is_ok()
{
return "number";
}
}
}
}
}
"string"
}
YamlValue::Mapping(_) => "object",
YamlValue::Sequence(_) => "array",
YamlValue::Alias { target, .. } => {
target.map(|t| t.value().type_name()).unwrap_or("null")
}
YamlValue::Error(_) => "error",
}
}
fn is_error(&self) -> bool {
matches!(self, YamlValue::Error(_))
}
fn error_message(&self) -> Option<&'static str> {
match self {
YamlValue::Error(msg) => Some(msg),
_ => None,
}
}
}
impl<'a, W: AsRef<[u64]> + Clone> DocumentFields for YamlFields<'a, W> {
type Value = YamlValue<'a, W>;
type Cursor = YamlCursor<'a, W>;
fn uncons(&self) -> Option<(DocumentField<Self::Value, Self::Cursor>, Self)> {
let (field, rest) = YamlFields::uncons(self)?;
Some((
DocumentField {
key: field.key(),
value: field.value(),
value_cursor: field.value_cursor(),
},
rest,
))
}
fn find(&self, name: &str) -> Option<Self::Value> {
YamlFields::find(self, name)
}
fn is_empty(&self) -> bool {
YamlFields::is_empty(self)
}
}
impl<'a, W: AsRef<[u64]> + Clone> DocumentElements for YamlElements<'a, W> {
type Value = YamlValue<'a, W>;
type Cursor = YamlCursor<'a, W>;
fn uncons(&self) -> Option<(Self::Value, Self)> {
YamlElements::uncons(self)
}
fn uncons_cursor(&self) -> Option<(Self::Cursor, Self)> {
YamlElements::uncons_cursor(self)
}
fn get(&self, index: usize) -> Option<Self::Value> {
YamlElements::get(self, index)
}
fn is_empty(&self) -> bool {
YamlElements::is_empty(self)
}
}
fn is_yaml_cursor_container<W: AsRef<[u64]>>(cursor: &YamlCursor<'_, W>) -> bool {
match cursor.value() {
YamlValue::Mapping(fields) => !fields.is_empty(),
YamlValue::Sequence(elements) => !elements.is_empty(),
_ => false,
}
}
fn write_yaml_indent<Out: core::fmt::Write>(out: &mut Out, spaces: usize) -> core::fmt::Result {
for _ in 0..spaces {
out.write_char(' ')?;
}
Ok(())
}
fn stream_yaml_string_value<Out: core::fmt::Write>(
out: &mut Out,
s: &YamlString<'_>,
) -> core::fmt::Result {
let str_val = match s.as_str() {
Ok(v) => v,
Err(_) => return out.write_str("\"\""),
};
match s {
YamlString::DoubleQuoted { .. } => stream_yaml_double_quoted(out, &str_val),
YamlString::SingleQuoted { .. } => stream_yaml_single_quoted(out, &str_val),
_ => {
if needs_yaml_quoting(&str_val) {
stream_yaml_double_quoted(out, &str_val)
} else {
out.write_str(&str_val)
}
}
}
}
fn needs_yaml_quoting(s: &str) -> bool {
if s.is_empty() {
return true;
}
let bytes = s.as_bytes();
let first = bytes[0];
if matches!(
first,
b'-' | b'?'
| b':'
| b','
| b'['
| b']'
| b'{'
| b'}'
| b'#'
| b'&'
| b'*'
| b'!'
| b'|'
| b'>'
| b'\''
| b'"'
| b'%'
| b'@'
| b'`'
) {
return true;
}
if bytes[0] == b' ' || bytes[bytes.len() - 1] == b' ' {
return true;
}
let lower = s.to_lowercase();
if matches!(
lower.as_str(),
"null" | "~" | "true" | "false" | "yes" | "no" | "on" | "off" | ".inf" | "-.inf" | ".nan"
) {
return true;
}
if looks_like_yaml_number(s) {
return true;
}
for b in bytes {
if *b < 0x20 || *b == b':' || *b == b'#' {
return true;
}
}
false
}
fn looks_like_yaml_number(s: &str) -> bool {
if s.is_empty() {
return false;
}
let bytes = s.as_bytes();
let mut i = 0;
if bytes[i] == b'-' || bytes[i] == b'+' {
i += 1;
if i >= bytes.len() {
return false;
}
}
if !bytes[i].is_ascii_digit() {
return false;
}
let mut has_dot = false;
let mut has_exp = false;
while i < bytes.len() {
match bytes[i] {
b'0'..=b'9' => {}
b'.' if !has_dot && !has_exp => has_dot = true,
b'e' | b'E' if !has_exp => {
has_exp = true;
if i + 1 < bytes.len() && (bytes[i + 1] == b'-' || bytes[i + 1] == b'+') {
i += 1;
}
}
_ => return false,
}
i += 1;
}
true
}
fn stream_yaml_double_quoted<Out: core::fmt::Write>(out: &mut Out, s: &str) -> core::fmt::Result {
out.write_char('"')?;
for ch in s.chars() {
match ch {
'"' => out.write_str("\\\"")?,
'\\' => out.write_str("\\\\")?,
'\n' => out.write_str("\\n")?,
'\r' => out.write_str("\\r")?,
'\t' => out.write_str("\\t")?,
c if (c as u32) < 0x20 => {
let b = c as u8;
out.write_str("\\x")?;
const HEX: &[u8; 16] = b"0123456789abcdef";
out.write_char(HEX[(b >> 4) as usize] as char)?;
out.write_char(HEX[(b & 0xf) as usize] as char)?;
}
c => out.write_char(c)?,
}
}
out.write_char('"')
}
fn stream_yaml_single_quoted<Out: core::fmt::Write>(out: &mut Out, s: &str) -> core::fmt::Result {
out.write_char('\'')?;
for ch in s.chars() {
if ch == '\'' {
out.write_str("''")?;
} else {
out.write_char(ch)?;
}
}
out.write_char('\'')
}
#[cfg(test)]
mod tests {
use super::*;
use crate::yaml::YamlIndex;
fn first_doc<'a, W: AsRef<[u64]> + core::fmt::Debug>(
root: YamlCursor<'a, W>,
) -> YamlValue<'a, W> {
match root.value() {
YamlValue::Sequence(elements) => elements
.into_iter()
.next()
.expect("expected at least one document"),
other => panic!(
"expected root to be document array (sequence), got {:?}",
other
),
}
}
#[test]
fn test_simple_mapping_navigation() {
let yaml = b"name: Alice";
let index = YamlIndex::build(yaml).unwrap();
let root = index.root(yaml);
match first_doc(root) {
YamlValue::Mapping(fields) => {
assert!(!fields.is_empty());
}
other => panic!("expected mapping, got {:?}", other),
}
}
#[test]
fn test_double_quoted_string() {
let yaml = b"name: \"Alice\"";
let index = YamlIndex::build(yaml).unwrap();
let root = index.root(yaml);
assert_eq!(root.text_position(), Some(0));
if let YamlValue::Mapping(fields) = first_doc(root) {
assert!(!fields.is_empty());
if let Some((field, _rest)) = fields.uncons() {
if let YamlValue::String(k) = field.key() {
assert_eq!(&*k.as_str().unwrap(), "name");
} else {
panic!("expected string key");
}
if let YamlValue::String(v) = field.value() {
assert_eq!(&*v.as_str().unwrap(), "Alice");
} else {
panic!("expected string value");
}
}
} else {
panic!("expected mapping");
}
}
#[test]
fn test_single_quoted_string() {
let yaml = b"name: 'Alice'";
let index = YamlIndex::build(yaml).unwrap();
let root = index.root(yaml);
if let YamlValue::Mapping(fields) = first_doc(root) {
if let Some(YamlValue::String(s)) = fields.find("name") {
assert_eq!(&*s.as_str().unwrap(), "Alice");
}
}
}
#[test]
fn test_unquoted_string() {
let yaml = b"name: Alice";
let index = YamlIndex::build(yaml).unwrap();
let root = index.root(yaml);
if let YamlValue::Mapping(fields) = first_doc(root) {
if let Some(YamlValue::String(s)) = fields.find("name") {
assert_eq!(&*s.as_str().unwrap(), "Alice");
}
}
}
#[test]
fn test_escape_double_quote() {
let s = YamlString::DoubleQuoted {
text: b"\"hello\\nworld\"",
start: 0,
};
assert_eq!(&*s.as_str().unwrap(), "hello\nworld");
}
#[test]
fn test_escape_single_quote() {
let s = YamlString::SingleQuoted {
text: b"'it''s'",
start: 0,
};
assert_eq!(&*s.as_str().unwrap(), "it's");
}
#[test]
fn test_decode_double_quoted_tab_escape() {
let s = YamlString::DoubleQuoted {
text: b"\"hello\\tworld\"",
start: 0,
};
assert_eq!(&*s.as_str().unwrap(), "hello\tworld");
}
#[test]
fn test_decode_double_quoted_carriage_return_escape() {
let s = YamlString::DoubleQuoted {
text: b"\"line\\rbreak\"",
start: 0,
};
assert_eq!(&*s.as_str().unwrap(), "line\rbreak");
}
#[test]
fn test_decode_double_quoted_backslash_escape() {
let s = YamlString::DoubleQuoted {
text: b"\"path\\\\to\\\\file\"",
start: 0,
};
assert_eq!(&*s.as_str().unwrap(), "path\\to\\file");
}
#[test]
fn test_decode_double_quoted_quote_escape() {
let s = YamlString::DoubleQuoted {
text: b"\"say \\\"hello\\\"\"",
start: 0,
};
assert_eq!(&*s.as_str().unwrap(), "say \"hello\"");
}
#[test]
fn test_decode_double_quoted_null_escape() {
let s = YamlString::DoubleQuoted {
text: b"\"null\\0char\"",
start: 0,
};
assert_eq!(&*s.as_str().unwrap(), "null\0char");
}
#[test]
fn test_decode_double_quoted_bell_escape() {
let s = YamlString::DoubleQuoted {
text: b"\"bell\\achar\"",
start: 0,
};
assert_eq!(&*s.as_str().unwrap(), "bell\x07char");
}
#[test]
fn test_decode_double_quoted_backspace_escape() {
let s = YamlString::DoubleQuoted {
text: b"\"back\\bspace\"",
start: 0,
};
assert_eq!(&*s.as_str().unwrap(), "back\x08space");
}
#[test]
fn test_decode_double_quoted_formfeed_escape() {
let s = YamlString::DoubleQuoted {
text: b"\"form\\ffeed\"",
start: 0,
};
assert_eq!(&*s.as_str().unwrap(), "form\x0Cfeed");
}
#[test]
fn test_decode_double_quoted_vertical_tab_escape() {
let s = YamlString::DoubleQuoted {
text: b"\"vert\\vtab\"",
start: 0,
};
assert_eq!(&*s.as_str().unwrap(), "vert\x0Btab");
}
#[test]
fn test_decode_double_quoted_escape_char_escape() {
let s = YamlString::DoubleQuoted {
text: b"\"esc\\echar\"",
start: 0,
};
assert_eq!(&*s.as_str().unwrap(), "esc\x1Bchar");
}
#[test]
fn test_decode_double_quoted_space_escape() {
let s = YamlString::DoubleQuoted {
text: b"\"spaced\\ word\"",
start: 0,
};
assert_eq!(&*s.as_str().unwrap(), "spaced word");
}
#[test]
fn test_decode_double_quoted_slash_escape() {
let s = YamlString::DoubleQuoted {
text: b"\"path\\/to\\/file\"",
start: 0,
};
assert_eq!(&*s.as_str().unwrap(), "path/to/file");
}
#[test]
fn test_decode_double_quoted_next_line_escape() {
let s = YamlString::DoubleQuoted {
text: b"\"next\\Nline\"",
start: 0,
};
assert_eq!(&*s.as_str().unwrap(), "next\u{0085}line");
}
#[test]
fn test_decode_double_quoted_nbsp_escape() {
let s = YamlString::DoubleQuoted {
text: b"\"non\\_break\"",
start: 0,
};
assert_eq!(&*s.as_str().unwrap(), "non\u{00A0}break");
}
#[test]
fn test_decode_double_quoted_line_separator_escape() {
let s = YamlString::DoubleQuoted {
text: b"\"line\\Lsep\"",
start: 0,
};
assert_eq!(&*s.as_str().unwrap(), "line\u{2028}sep");
}
#[test]
fn test_decode_double_quoted_paragraph_separator_escape() {
let s = YamlString::DoubleQuoted {
text: b"\"para\\Psep\"",
start: 0,
};
assert_eq!(&*s.as_str().unwrap(), "para\u{2029}sep");
}
#[test]
fn test_decode_double_quoted_hex_escape() {
let s = YamlString::DoubleQuoted {
text: b"\"hex\\x41char\"", start: 0,
};
assert_eq!(&*s.as_str().unwrap(), "hexAchar");
}
#[test]
fn test_decode_double_quoted_hex_control_escape() {
let s = YamlString::DoubleQuoted {
text: b"\"ctrl\\x07char\"", start: 0,
};
assert_eq!(&*s.as_str().unwrap(), "ctrl\x07char");
}
#[test]
fn test_decode_double_quoted_unicode_4digit_escape() {
let s = YamlString::DoubleQuoted {
text: b"\"euro\\u20ACsign\"", start: 0,
};
assert_eq!(&*s.as_str().unwrap(), "euro€sign");
}
#[test]
fn test_decode_double_quoted_unicode_8digit_escape() {
let s = YamlString::DoubleQuoted {
text: b"\"emoji\\U0001F600face\"", start: 0,
};
assert_eq!(&*s.as_str().unwrap(), "emoji😀face");
}
#[test]
fn test_decode_double_quoted_multiple_escapes() {
let s = YamlString::DoubleQuoted {
text: b"\"line1\\nline2\\ttabbed\\\\slash\"",
start: 0,
};
assert_eq!(&*s.as_str().unwrap(), "line1\nline2\ttabbed\\slash");
}
#[test]
fn test_decode_double_quoted_escaped_newline_continuation() {
let s = YamlString::DoubleQuoted {
text: b"\"line one\\\n line two\"",
start: 0,
};
assert_eq!(&*s.as_str().unwrap(), "line oneline two");
}
#[test]
fn test_decode_double_quoted_escaped_crlf_continuation() {
let s = YamlString::DoubleQuoted {
text: b"\"line one\\\r\n line two\"",
start: 0,
};
assert_eq!(&*s.as_str().unwrap(), "line oneline two");
}
#[test]
fn test_decode_single_quoted_double_quote() {
let s = YamlString::SingleQuoted {
text: b"'say \"hello\"'",
start: 0,
};
assert_eq!(&*s.as_str().unwrap(), "say \"hello\"");
}
#[test]
fn test_decode_single_quoted_backslash() {
let s = YamlString::SingleQuoted {
text: b"'path\\to\\file'",
start: 0,
};
assert_eq!(&*s.as_str().unwrap(), "path\\to\\file");
}
#[test]
fn test_flow_sequence_navigation() {
let yaml = b"items: [1, 2, 3]";
let index = YamlIndex::build(yaml).unwrap();
let root = index.root(yaml);
if let YamlValue::Mapping(fields) = first_doc(root) {
if let Some(YamlValue::Sequence(elements)) = fields.find("items") {
let items: Vec<_> = elements.collect();
assert_eq!(items.len(), 3);
if let YamlValue::String(s) = &items[0] {
assert_eq!(&*s.as_str().unwrap(), "1");
} else {
panic!("expected string value for item");
}
} else {
panic!("expected sequence for items");
}
} else {
panic!("expected mapping");
}
}
#[test]
fn test_flow_mapping_navigation() {
let yaml = b"person: {name: Alice, age: 30}";
let index = YamlIndex::build(yaml).unwrap();
let root = index.root(yaml);
if let YamlValue::Mapping(fields) = first_doc(root) {
if let Some(YamlValue::Mapping(person_fields)) = fields.find("person") {
if let Some(YamlValue::String(s)) = person_fields.find("name") {
assert_eq!(&*s.as_str().unwrap(), "Alice");
} else {
panic!("expected name field");
}
if let Some(YamlValue::String(s)) = person_fields.find("age") {
assert_eq!(&*s.as_str().unwrap(), "30");
} else {
panic!("expected age field");
}
} else {
panic!("expected mapping for person");
}
} else {
panic!("expected mapping");
}
}
#[test]
fn test_flow_nested_navigation() {
let yaml = b"data: {users: [{name: Alice}, {name: Bob}]}";
let index = YamlIndex::build(yaml).unwrap();
let root = index.root(yaml);
if let YamlValue::Mapping(fields) = first_doc(root) {
if let Some(YamlValue::Mapping(data_fields)) = fields.find("data") {
if let Some(YamlValue::Sequence(users)) = data_fields.find("users") {
let items: Vec<_> = users.collect();
assert_eq!(items.len(), 2, "expected 2 users");
if let YamlValue::Mapping(user_fields) = &items[0] {
if let Some(YamlValue::String(s)) = user_fields.find("name") {
assert_eq!(&*s.as_str().unwrap(), "Alice");
}
}
if let YamlValue::Mapping(user_fields) = &items[1] {
if let Some(YamlValue::String(s)) = user_fields.find("name") {
assert_eq!(&*s.as_str().unwrap(), "Bob");
}
}
} else {
panic!("expected users sequence");
}
} else {
panic!("expected data mapping");
}
} else {
panic!("expected mapping");
}
}
#[test]
fn test_flow_empty_sequence_navigation() {
let yaml = b"items: []";
let index = YamlIndex::build(yaml).unwrap();
let root = index.root(yaml);
if let YamlValue::Mapping(fields) = first_doc(root) {
if let Some(YamlValue::Sequence(elements)) = fields.find("items") {
let items: Vec<_> = elements.collect();
assert_eq!(items.len(), 0, "expected empty sequence");
} else {
panic!("expected sequence for items");
}
} else {
panic!("expected mapping");
}
}
#[test]
fn test_flow_empty_mapping_navigation() {
let yaml = b"data: {}";
let index = YamlIndex::build(yaml).unwrap();
let root = index.root(yaml);
if let YamlValue::Mapping(fields) = first_doc(root) {
if let Some(YamlValue::Mapping(data_fields)) = fields.find("data") {
assert!(data_fields.is_empty(), "expected empty mapping");
} else {
panic!("expected mapping for data");
}
} else {
panic!("expected mapping");
}
}
#[test]
fn test_block_literal_navigation() {
let yaml = b"text: |\n Line 1\n Line 2\n";
let index = YamlIndex::build(yaml).unwrap();
let root = index.root(yaml);
if let YamlValue::Mapping(fields) = first_doc(root) {
if let Some(YamlValue::String(s)) = fields.find("text") {
let decoded = s.as_str().unwrap();
assert!(
decoded.contains("Line 1") && decoded.contains("Line 2"),
"block literal should contain both lines, got: {:?}",
decoded
);
} else {
panic!("expected string for text");
}
} else {
panic!("expected mapping");
}
}
#[test]
fn test_block_folded_navigation() {
let yaml = b"text: >\n First part\n second part\n";
let index = YamlIndex::build(yaml).unwrap();
let root = index.root(yaml);
if let YamlValue::Mapping(fields) = first_doc(root) {
if let Some(YamlValue::String(s)) = fields.find("text") {
let decoded = s.as_str().unwrap();
assert!(
decoded.contains("First part") && decoded.contains("second part"),
"block folded should contain both parts, got: {:?}",
decoded
);
} else {
panic!("expected string for text");
}
} else {
panic!("expected mapping");
}
}
#[test]
fn test_block_scalar_in_sequence() {
let yaml_simple = b"- item1\n- item2\n";
let index_simple = YamlIndex::build(yaml_simple).unwrap();
let root_simple = index_simple.root(yaml_simple);
if let YamlValue::Sequence(elements) = first_doc(root_simple) {
let items: Vec<_> = elements.collect();
assert_eq!(items.len(), 2, "simple: expected 2 items");
if let YamlValue::String(s) = &items[0] {
assert_eq!(&*s.as_str().unwrap(), "item1");
} else {
panic!("simple: expected string for item1, got: {:?}", items[0]);
}
} else {
panic!("simple: expected sequence");
}
let yaml = b"- |\n item\n- value\n";
let index = YamlIndex::build(yaml).unwrap();
let root = index.root(yaml);
if let YamlValue::Sequence(elements) = first_doc(root) {
let items: Vec<_> = elements.collect();
assert_eq!(items.len(), 2, "expected 2 items, got items: {:?}", items);
if let YamlValue::String(s) = &items[0] {
let decoded = s.as_str().unwrap();
assert!(
decoded.contains("item"),
"first item should contain 'item', got: {:?}",
decoded
);
} else {
panic!("expected string for first item, got: {:?}", items[0]);
}
if let YamlValue::String(s) = &items[1] {
assert_eq!(&*s.as_str().unwrap(), "value");
} else {
panic!("expected string for second item");
}
} else {
panic!("expected sequence, got: {:?}", first_doc(root));
}
}
#[test]
fn test_anchor_and_alias_basic() {
let yaml = b"anchor: &name value\nalias: *name";
let index = YamlIndex::build(yaml).unwrap();
let root = index.root(yaml);
let doc_cursor = root.first_child().expect("expected document");
if let YamlValue::Mapping(fields) = doc_cursor.value() {
if let Some(YamlValue::String(s)) = fields.find("anchor") {
assert_eq!(&*s.as_str().unwrap(), "value");
} else {
panic!("expected string for anchor");
}
let fields = YamlFields::from_mapping_cursor(doc_cursor);
if let Some((_field, _rest)) = fields.uncons() {
let fields = _rest;
if let Some((field, _)) = fields.uncons() {
if let YamlValue::Alias {
anchor_name,
target,
} = field.value()
{
assert_eq!(anchor_name, "name");
assert!(target.is_some(), "alias should resolve to target");
if let Some(target_cursor) = target {
if let YamlValue::String(s) = target_cursor.value() {
assert_eq!(&*s.as_str().unwrap(), "value");
} else {
panic!("expected string for resolved alias");
}
}
} else {
panic!("expected alias for second field value");
}
}
}
} else {
panic!("expected mapping");
}
}
#[test]
fn test_anchor_on_flow_mapping() {
let yaml = b"defaults: &defaults {key: value}\nother: *defaults";
let index = YamlIndex::build(yaml).unwrap();
let root = index.root(yaml);
let doc_cursor = root.first_child().expect("expected document");
if let YamlValue::Mapping(fields) = doc_cursor.value() {
if let Some(YamlValue::Mapping(default_fields)) = fields.find("defaults") {
if let Some(YamlValue::String(s)) = default_fields.find("key") {
assert_eq!(&*s.as_str().unwrap(), "value");
} else {
panic!("expected key in defaults");
}
} else {
panic!("expected mapping for defaults");
}
let fields = YamlFields::from_mapping_cursor(doc_cursor);
for field in fields {
if let YamlValue::String(key) = field.key() {
if key.as_str().unwrap() == "other" {
if let YamlValue::Alias {
anchor_name,
target,
} = field.value()
{
assert_eq!(anchor_name, "defaults");
assert!(target.is_some());
} else {
panic!("expected alias for 'other'");
}
return;
}
}
}
panic!("did not find 'other' field");
} else {
panic!("expected mapping");
}
}
#[test]
fn test_anchor_in_flow_sequence() {
let yaml = b"items: [&first one, &second two, *first]";
let index = YamlIndex::build(yaml).unwrap();
let root = index.root(yaml);
if let YamlValue::Mapping(fields) = first_doc(root) {
if let Some(YamlValue::Sequence(elements)) = fields.find("items") {
let items: Vec<_> = elements.collect();
assert_eq!(items.len(), 3, "expected 3 items");
if let YamlValue::String(s) = &items[0] {
assert_eq!(&*s.as_str().unwrap(), "one");
} else {
panic!("expected string for first item");
}
if let YamlValue::String(s) = &items[1] {
assert_eq!(&*s.as_str().unwrap(), "two");
} else {
panic!("expected string for second item");
}
if let YamlValue::Alias {
anchor_name,
target,
} = &items[2]
{
assert_eq!(*anchor_name, "first");
assert!(target.is_some());
} else {
panic!("expected alias for third item, got: {:?}", items[2]);
}
} else {
panic!("expected sequence for items");
}
} else {
panic!("expected mapping");
}
}
#[test]
fn test_anchor_in_flow_mapping() {
let yaml = b"data: {name: &n Alice, greeting: *n}";
let index = YamlIndex::build(yaml).unwrap();
let root = index.root(yaml);
let doc_cursor = root.first_child().expect("expected document");
if let YamlValue::Mapping(fields) = doc_cursor.value() {
if let Some(YamlValue::Mapping(data_fields)) = fields.find("data") {
if let Some(YamlValue::String(s)) = data_fields.find("name") {
assert_eq!(&*s.as_str().unwrap(), "Alice");
} else {
panic!("expected string for name");
}
let data_fields = YamlFields::from_mapping_cursor(
doc_cursor.first_child().unwrap().next_sibling().unwrap(),
);
for field in data_fields {
if let YamlValue::String(key) = field.key() {
if key.as_str().unwrap() == "greeting" {
if let YamlValue::Alias {
anchor_name,
target,
} = field.value()
{
assert_eq!(anchor_name, "n");
assert!(target.is_some());
return;
} else {
panic!("expected alias for greeting");
}
}
}
}
panic!("did not find greeting field");
} else {
panic!("expected mapping for data");
}
} else {
panic!("expected mapping");
}
}
#[test]
fn test_inline_anchor_in_sequence() {
let yaml = b"- &item value\n- *item";
let index = YamlIndex::build(yaml).unwrap();
let root = index.root(yaml);
if let YamlValue::Sequence(elements) = first_doc(root) {
let items: Vec<_> = elements.collect();
assert_eq!(items.len(), 2, "expected 2 items");
if let YamlValue::String(s) = &items[0] {
assert_eq!(&*s.as_str().unwrap(), "value");
} else {
panic!("expected string for first item");
}
if let YamlValue::Alias {
anchor_name,
target,
} = &items[1]
{
assert_eq!(*anchor_name, "item");
assert!(target.is_some());
} else {
panic!("expected alias for second item");
}
} else {
panic!("expected sequence");
}
}
#[test]
fn test_multiple_anchors() {
let yaml = b"a: &x 1\nb: &y 2\nc: [*x, *y]";
let index = YamlIndex::build(yaml).unwrap();
let root = index.root(yaml);
if let YamlValue::Mapping(fields) = first_doc(root) {
if let Some(YamlValue::String(s)) = fields.find("a") {
assert_eq!(&*s.as_str().unwrap(), "1");
}
if let Some(YamlValue::String(s)) = fields.find("b") {
assert_eq!(&*s.as_str().unwrap(), "2");
}
if let Some(YamlValue::Sequence(elements)) = fields.find("c") {
let items: Vec<_> = elements.collect();
assert_eq!(items.len(), 2);
if let YamlValue::Alias { anchor_name, .. } = &items[0] {
assert_eq!(*anchor_name, "x");
} else {
panic!("expected alias for first element");
}
if let YamlValue::Alias { anchor_name, .. } = &items[1] {
assert_eq!(*anchor_name, "y");
} else {
panic!("expected alias for second element");
}
} else {
panic!("expected sequence for c");
}
} else {
panic!("expected mapping");
}
}
#[test]
fn test_undefined_alias() {
let yaml = b"bad: *undefined";
let index = YamlIndex::build(yaml).unwrap();
let root = index.root(yaml);
if let YamlValue::Mapping(fields) = first_doc(root) {
for field in fields {
if let YamlValue::String(key) = field.key() {
if key.as_str().unwrap() == "bad" {
if let YamlValue::Alias {
anchor_name,
target,
} = field.value()
{
assert_eq!(anchor_name, "undefined");
assert!(target.is_none(), "undefined alias should not resolve");
return;
} else {
panic!("expected alias for bad");
}
}
}
}
panic!("did not find bad field");
} else {
panic!("expected mapping");
}
}
#[test]
fn test_block_nested_sequence() {
let yaml = b"items:\n - one\n - two";
let index = YamlIndex::build(yaml).unwrap();
let root = index.root(yaml);
if let YamlValue::Mapping(fields) = first_doc(root) {
for field in fields {
let key = field.key();
let val = field.value();
if let YamlValue::String(k) = key {
if k.as_str().unwrap() == "items" {
if let YamlValue::Sequence(elements) = val {
let items: Vec<_> = elements.collect();
assert_eq!(items.len(), 2, "expected 2 items, got: {:?}", items);
if let YamlValue::String(s) = &items[0] {
assert_eq!(&*s.as_str().unwrap(), "one");
}
return;
} else {
panic!("expected sequence for items, got: {:?}", val);
}
}
}
}
panic!("did not find items field");
} else {
panic!("expected mapping, got: {:?}", first_doc(root));
}
}
#[test]
fn test_block_nested_mapping() {
let yaml = b"person:\n name: Alice\n age: 30";
let index = YamlIndex::build(yaml).unwrap();
let root = index.root(yaml);
if let YamlValue::Mapping(fields) = first_doc(root) {
if let Some(YamlValue::Mapping(person_fields)) = fields.find("person") {
if let Some(YamlValue::String(s)) = person_fields.find("name") {
assert_eq!(&*s.as_str().unwrap(), "Alice");
} else {
panic!("expected name field");
}
if let Some(YamlValue::String(s)) = person_fields.find("age") {
assert_eq!(&*s.as_str().unwrap(), "30");
} else {
panic!("expected age field");
}
} else {
panic!("expected mapping for person");
}
} else {
panic!("expected mapping");
}
}
#[test]
fn test_multiple_toplevel_nested_mappings() {
let yaml = b"server:\n host: localhost\ndatabase:\n host: db.example.com";
let index = YamlIndex::build(yaml).unwrap();
let root = index.root(yaml);
if let YamlValue::Mapping(fields) = first_doc(root) {
let all_fields: Vec<_> = fields.into_iter().collect();
assert_eq!(
all_fields.len(),
2,
"expected 2 fields (server, database), got {} fields",
all_fields.len()
);
if let YamlValue::String(k) = all_fields[0].key() {
assert_eq!(&*k.as_str().unwrap(), "server");
} else {
panic!("expected string key for first field");
}
if let YamlValue::String(k) = all_fields[1].key() {
assert_eq!(&*k.as_str().unwrap(), "database");
} else {
panic!("expected string key for second field");
}
if let YamlValue::Mapping(server_fields) = all_fields[0].value() {
if let Some(YamlValue::String(s)) = server_fields.find("host") {
assert_eq!(&*s.as_str().unwrap(), "localhost");
} else {
panic!("expected host field in server");
}
} else {
panic!("expected mapping for server value");
}
if let YamlValue::Mapping(db_fields) = all_fields[1].value() {
if let Some(YamlValue::String(s)) = db_fields.find("host") {
assert_eq!(&*s.as_str().unwrap(), "db.example.com");
} else {
panic!("expected host field in database");
}
} else {
panic!("expected mapping for database value");
}
} else {
panic!("expected mapping");
}
}
#[test]
fn test_multiline_double_quoted_simple() {
let yaml = b"key: \"line one\n line two\"";
let index = YamlIndex::build(yaml).unwrap();
let root = index.root(yaml);
if let YamlValue::Sequence(docs) = root.value() {
let doc = docs.into_iter().next().unwrap();
if let YamlValue::Mapping(fields) = doc {
let field = fields.into_iter().next().unwrap();
let value = field.value();
if let YamlValue::String(s) = value {
assert_eq!(&*s.as_str().unwrap(), "line one line two");
} else {
panic!("expected string value");
}
} else {
panic!("expected mapping");
}
} else {
panic!("expected sequence");
}
}
#[test]
fn test_multiline_double_quoted_empty_line() {
let yaml = b"key: \"line one\n\n line two\"";
let index = YamlIndex::build(yaml).unwrap();
let root = index.root(yaml);
if let YamlValue::Sequence(docs) = root.value() {
let doc = docs.into_iter().next().unwrap();
if let YamlValue::Mapping(fields) = doc {
let field = fields.into_iter().next().unwrap();
let value = field.value();
if let YamlValue::String(s) = value {
assert_eq!(&*s.as_str().unwrap(), "line one\nline two");
} else {
panic!("expected string value");
}
} else {
panic!("expected mapping");
}
} else {
panic!("expected sequence");
}
}
#[test]
fn test_multiline_double_quoted_escaped_newline() {
let yaml = b"key: \"line one\\\n line two\"";
let index = YamlIndex::build(yaml).unwrap();
let root = index.root(yaml);
if let YamlValue::Sequence(docs) = root.value() {
let doc = docs.into_iter().next().unwrap();
if let YamlValue::Mapping(fields) = doc {
let field = fields.into_iter().next().unwrap();
let value = field.value();
if let YamlValue::String(s) = value {
assert_eq!(&*s.as_str().unwrap(), "line oneline two");
} else {
panic!("expected string value");
}
} else {
panic!("expected mapping");
}
} else {
panic!("expected sequence");
}
}
#[test]
fn test_multiline_single_quoted_simple() {
let yaml = b"key: 'line one\n line two'";
let index = YamlIndex::build(yaml).unwrap();
let root = index.root(yaml);
if let YamlValue::Sequence(docs) = root.value() {
let doc = docs.into_iter().next().unwrap();
if let YamlValue::Mapping(fields) = doc {
let field = fields.into_iter().next().unwrap();
let value = field.value();
if let YamlValue::String(s) = value {
assert_eq!(&*s.as_str().unwrap(), "line one line two");
} else {
panic!("expected string value");
}
} else {
panic!("expected mapping");
}
} else {
panic!("expected sequence");
}
}
#[test]
fn test_multiline_single_quoted_with_escaped_quote() {
let yaml = b"key: 'it''s\n working'";
let index = YamlIndex::build(yaml).unwrap();
let root = index.root(yaml);
if let YamlValue::Sequence(docs) = root.value() {
let doc = docs.into_iter().next().unwrap();
if let YamlValue::Mapping(fields) = doc {
let field = fields.into_iter().next().unwrap();
let value = field.value();
if let YamlValue::String(s) = value {
assert_eq!(&*s.as_str().unwrap(), "it's working");
} else {
panic!("expected string value");
}
} else {
panic!("expected mapping");
}
} else {
panic!("expected sequence");
}
}
#[test]
fn test_sequence_entry_content_on_next_line() {
let yaml = b"-\n name: Mark McGwire\n hr: 65";
let result = YamlIndex::build(yaml);
assert!(result.is_ok(), "Failed to parse: {:?}", result.err());
}
#[test]
fn test_sequence_entry_nested_sequence_on_next_line() {
let yaml = b"-\n - inner1\n - inner2";
let result = YamlIndex::build(yaml);
assert!(result.is_ok(), "Failed to parse: {:?}", result.err());
}
fn get_json_via_transcode(yaml: &[u8]) -> String {
let index = YamlIndex::build(yaml).unwrap();
let root = index.root(yaml);
root.to_json_document()
}
fn get_json_via_decode(yaml: &[u8]) -> String {
let index = YamlIndex::build(yaml).unwrap();
let root = index.root(yaml);
match first_doc(root) {
YamlValue::String(s) => {
let decoded = s.as_str().unwrap();
let mut out = String::from("\"");
for ch in decoded.chars() {
match ch {
'"' => out.push_str("\\\""),
'\\' => out.push_str("\\\\"),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
'\x08' => out.push_str("\\b"),
'\x0c' => out.push_str("\\f"),
c if (c as u32) < 0x20 => {
out.push_str(&format!("\\u{:04x}", c as u32));
}
c => out.push(c),
}
}
out.push('"');
out
}
YamlValue::Mapping(fields) => {
let mut pairs = Vec::new();
for field in fields.into_iter() {
if let YamlValue::String(k) = field.key() {
if let YamlValue::String(v) = field.value() {
let key_str = k.as_str().unwrap();
let val_str = v.as_str().unwrap();
pairs.push(format!("\"{}\":\"{}\"", key_str, val_str));
}
}
}
format!("{{{}}}", pairs.join(","))
}
_ => panic!("expected string or mapping"),
}
}
fn json_string_to_rust_string(json: &str) -> String {
assert!(
json.starts_with('"') && json.ends_with('"'),
"not a JSON string: {}",
json
);
let inner = &json[1..json.len() - 1];
let mut result = String::new();
let mut chars = inner.chars().peekable();
while let Some(c) = chars.next() {
if c == '\\' {
match chars.next() {
Some('n') => result.push('\n'),
Some('r') => result.push('\r'),
Some('t') => result.push('\t'),
Some('b') => result.push('\x08'),
Some('f') => result.push('\x0c'),
Some('"') => result.push('"'),
Some('\\') => result.push('\\'),
Some('/') => result.push('/'),
Some('u') => {
let hex: String = chars.by_ref().take(4).collect();
let codepoint = u32::from_str_radix(&hex, 16).unwrap();
if (0xD800..=0xDBFF).contains(&codepoint) {
if chars.next() == Some('\\') && chars.next() == Some('u') {
let low_hex: String = chars.by_ref().take(4).collect();
let low = u32::from_str_radix(&low_hex, 16).unwrap();
let full = 0x10000 + ((codepoint - 0xD800) << 10) + (low - 0xDC00);
result.push(char::from_u32(full).unwrap());
}
} else {
result.push(char::from_u32(codepoint).unwrap());
}
}
Some(c) => panic!("unknown escape: \\{}", c),
None => panic!("trailing backslash"),
}
} else {
result.push(c);
}
}
result
}
fn assert_json_semantically_equal(transcoded: &str, decoded: &str, context: &str) {
let transcoded_value = json_string_to_rust_string(transcoded);
let decoded_value = json_string_to_rust_string(decoded);
assert_eq!(
transcoded_value, decoded_value,
"{}: JSON strings decode to different values.\n transcoded JSON: {}\n decoded JSON: {}\n transcoded value: {:?}\n decoded value: {:?}",
context, transcoded, decoded, transcoded_value, decoded_value
);
}
#[test]
fn test_transcode_double_quoted_simple() {
let yaml = b"\"hello world\"";
let json = get_json_via_transcode(yaml);
assert_eq!(json, "\"hello world\"");
}
#[test]
fn test_transcode_double_quoted_newline_escape() {
let yaml = b"\"hello\\nworld\"";
let transcoded = get_json_via_transcode(yaml);
let decoded = get_json_via_decode(yaml);
assert_eq!(transcoded, decoded, "transcoded vs decoded mismatch");
assert_eq!(transcoded, "\"hello\\nworld\"");
}
#[test]
fn test_transcode_double_quoted_tab_escape() {
let yaml = b"\"hello\\tworld\"";
let transcoded = get_json_via_transcode(yaml);
let decoded = get_json_via_decode(yaml);
assert_eq!(transcoded, decoded);
assert_eq!(transcoded, "\"hello\\tworld\"");
}
#[test]
fn test_transcode_double_quoted_backslash_escape() {
let yaml = b"\"path\\\\to\\\\file\"";
let transcoded = get_json_via_transcode(yaml);
let decoded = get_json_via_decode(yaml);
assert_eq!(transcoded, decoded);
assert_eq!(transcoded, "\"path\\\\to\\\\file\"");
}
#[test]
fn test_transcode_double_quoted_quote_escape() {
let yaml = b"\"say \\\"hello\\\"\"";
let transcoded = get_json_via_transcode(yaml);
let decoded = get_json_via_decode(yaml);
assert_eq!(transcoded, decoded);
assert_eq!(transcoded, "\"say \\\"hello\\\"\"");
}
#[test]
fn test_transcode_double_quoted_null_escape() {
let yaml = b"\"null\\0char\"";
let transcoded = get_json_via_transcode(yaml);
let decoded = get_json_via_decode(yaml);
assert_eq!(transcoded, decoded);
assert_eq!(transcoded, "\"null\\u0000char\"");
}
#[test]
fn test_transcode_double_quoted_bell_escape() {
let yaml = b"\"bell\\achar\"";
let transcoded = get_json_via_transcode(yaml);
let decoded = get_json_via_decode(yaml);
assert_eq!(transcoded, decoded);
assert_eq!(transcoded, "\"bell\\u0007char\"");
}
#[test]
fn test_transcode_double_quoted_backspace_escape() {
let yaml = b"\"back\\bspace\"";
let transcoded = get_json_via_transcode(yaml);
let decoded = get_json_via_decode(yaml);
assert_json_semantically_equal(&transcoded, &decoded, "backspace escape");
}
#[test]
fn test_transcode_double_quoted_formfeed_escape() {
let yaml = b"\"form\\ffeed\"";
let transcoded = get_json_via_transcode(yaml);
let decoded = get_json_via_decode(yaml);
assert_json_semantically_equal(&transcoded, &decoded, "formfeed escape");
}
#[test]
fn test_transcode_double_quoted_vertical_tab_escape() {
let yaml = b"\"vert\\vtab\"";
let transcoded = get_json_via_transcode(yaml);
let decoded = get_json_via_decode(yaml);
assert_eq!(transcoded, decoded);
assert_eq!(transcoded, "\"vert\\u000btab\"");
}
#[test]
fn test_transcode_double_quoted_escape_escape() {
let yaml = b"\"esc\\echar\"";
let transcoded = get_json_via_transcode(yaml);
let decoded = get_json_via_decode(yaml);
assert_eq!(transcoded, decoded);
assert_eq!(transcoded, "\"esc\\u001bchar\"");
}
#[test]
fn test_transcode_double_quoted_next_line_escape() {
let yaml = b"\"next\\Nline\"";
let transcoded = get_json_via_transcode(yaml);
let decoded = get_json_via_decode(yaml);
assert_json_semantically_equal(&transcoded, &decoded, "next line escape");
assert_eq!(transcoded, "\"next\\u0085line\"");
}
#[test]
fn test_transcode_double_quoted_nbsp_escape() {
let yaml = b"\"non\\_break\"";
let transcoded = get_json_via_transcode(yaml);
let decoded = get_json_via_decode(yaml);
assert_json_semantically_equal(&transcoded, &decoded, "nbsp escape");
assert_eq!(transcoded, "\"non\\u00a0break\"");
}
#[test]
fn test_transcode_double_quoted_line_separator_escape() {
let yaml = b"\"line\\Lsep\"";
let transcoded = get_json_via_transcode(yaml);
let decoded = get_json_via_decode(yaml);
assert_json_semantically_equal(&transcoded, &decoded, "line separator escape");
assert_eq!(transcoded, "\"line\\u2028sep\"");
}
#[test]
fn test_transcode_double_quoted_paragraph_separator_escape() {
let yaml = b"\"para\\Psep\"";
let transcoded = get_json_via_transcode(yaml);
let decoded = get_json_via_decode(yaml);
assert_json_semantically_equal(&transcoded, &decoded, "paragraph separator escape");
assert_eq!(transcoded, "\"para\\u2029sep\"");
}
#[test]
fn test_transcode_double_quoted_hex_escape() {
let yaml = b"\"hex\\x41char\""; let transcoded = get_json_via_transcode(yaml);
let decoded = get_json_via_decode(yaml);
assert_eq!(transcoded, decoded);
assert_eq!(transcoded, "\"hexAchar\"");
}
#[test]
fn test_transcode_double_quoted_hex_control_escape() {
let yaml = b"\"ctrl\\x07char\""; let transcoded = get_json_via_transcode(yaml);
let decoded = get_json_via_decode(yaml);
assert_eq!(transcoded, decoded);
assert_eq!(transcoded, "\"ctrl\\u0007char\"");
}
#[test]
fn test_transcode_double_quoted_unicode_4digit_escape() {
let yaml = b"\"euro\\u20ACsign\""; let transcoded = get_json_via_transcode(yaml);
let decoded = get_json_via_decode(yaml);
assert_json_semantically_equal(&transcoded, &decoded, "unicode 4-digit escape");
assert_eq!(transcoded, "\"euro\\u20acsign\"");
}
#[test]
fn test_transcode_double_quoted_unicode_8digit_escape() {
let yaml = b"\"emoji\\U0001F600face\""; let transcoded = get_json_via_transcode(yaml);
let decoded = get_json_via_decode(yaml);
assert_json_semantically_equal(&transcoded, &decoded, "unicode 8-digit escape");
assert_eq!(transcoded, "\"emoji\\ud83d\\ude00face\"");
}
#[test]
fn test_transcode_double_quoted_multiple_escapes() {
let yaml = b"\"line1\\nline2\\ttabbed\\\\slash\"";
let transcoded = get_json_via_transcode(yaml);
let decoded = get_json_via_decode(yaml);
assert_eq!(transcoded, decoded);
assert_eq!(transcoded, "\"line1\\nline2\\ttabbed\\\\slash\"");
}
#[test]
fn test_transcode_double_quoted_multiline_folding() {
let yaml = b"\"hello\nworld\"";
let transcoded = get_json_via_transcode(yaml);
let decoded = get_json_via_decode(yaml);
assert_eq!(transcoded, decoded);
assert_eq!(transcoded, "\"hello world\"");
}
#[test]
fn test_transcode_double_quoted_multiline_empty_lines() {
let yaml = b"\"hello\n\nworld\"";
let transcoded = get_json_via_transcode(yaml);
let decoded = get_json_via_decode(yaml);
assert_eq!(transcoded, decoded);
assert_eq!(transcoded, "\"hello\\nworld\"");
}
#[test]
fn test_transcode_single_quoted_simple() {
let yaml = b"'hello world'";
let json = get_json_via_transcode(yaml);
assert_eq!(json, "\"hello world\"");
}
#[test]
fn test_transcode_single_quoted_escape() {
let yaml = b"'it''s working'";
let transcoded = get_json_via_transcode(yaml);
let decoded = get_json_via_decode(yaml);
assert_eq!(transcoded, decoded);
assert_eq!(transcoded, "\"it's working\"");
}
#[test]
fn test_transcode_single_quoted_double_quote() {
let yaml = b"'say \"hello\"'";
let transcoded = get_json_via_transcode(yaml);
let decoded = get_json_via_decode(yaml);
assert_eq!(transcoded, decoded);
assert_eq!(transcoded, "\"say \\\"hello\\\"\"");
}
#[test]
fn test_transcode_single_quoted_backslash() {
let yaml = b"'path\\to\\file'";
let transcoded = get_json_via_transcode(yaml);
let decoded = get_json_via_decode(yaml);
assert_eq!(transcoded, decoded);
assert_eq!(transcoded, "\"path\\\\to\\\\file\"");
}
#[test]
fn test_transcode_single_quoted_multiline_folding() {
let yaml = b"'hello\nworld'";
let transcoded = get_json_via_transcode(yaml);
let decoded = get_json_via_decode(yaml);
assert_eq!(transcoded, decoded);
assert_eq!(transcoded, "\"hello world\"");
}
#[test]
fn test_transcode_single_quoted_multiline_empty_lines() {
let yaml = b"'hello\n\nworld'";
let transcoded = get_json_via_transcode(yaml);
let decoded = get_json_via_decode(yaml);
assert_eq!(transcoded, decoded);
assert_eq!(transcoded, "\"hello\\nworld\"");
}
#[test]
fn test_transcode_in_mapping_context() {
let yaml = b"key: \"value\\nwith\\nnewlines\"";
let transcoded = get_json_via_transcode(yaml);
assert!(transcoded.contains("value\\nwith\\nnewlines"));
}
#[test]
fn test_transcode_slash_escape() {
let yaml = b"\"path\\/to\\/file\"";
let transcoded = get_json_via_transcode(yaml);
let decoded = get_json_via_decode(yaml);
assert_eq!(transcoded, decoded);
assert_eq!(transcoded, "\"path/to/file\"");
}
#[test]
fn test_transcode_space_escape() {
let yaml = b"\"spaced\\ word\"";
let transcoded = get_json_via_transcode(yaml);
let decoded = get_json_via_decode(yaml);
assert_eq!(transcoded, decoded);
assert_eq!(transcoded, "\"spaced word\"");
}
#[test]
fn test_transcode_carriage_return_escape() {
let yaml = b"\"line\\rbreak\"";
let transcoded = get_json_via_transcode(yaml);
let decoded = get_json_via_decode(yaml);
assert_eq!(transcoded, decoded);
assert_eq!(transcoded, "\"line\\rbreak\"");
}
#[test]
fn test_transcode_crlf_multiline() {
let yaml = b"\"hello\r\nworld\"";
let transcoded = get_json_via_transcode(yaml);
let decoded = get_json_via_decode(yaml);
assert_eq!(transcoded, decoded);
assert_eq!(transcoded, "\"hello world\"");
}
#[test]
fn test_transcode_escaped_newline_continuation() {
let yaml = b"\"line one\\\n line two\"";
let transcoded = get_json_via_transcode(yaml);
let decoded = get_json_via_decode(yaml);
assert_eq!(transcoded, decoded);
assert_eq!(transcoded, "\"line oneline two\"");
}
#[test]
fn test_transcode_escaped_crlf_continuation() {
let yaml = b"\"line one\\\r\n line two\"";
let transcoded = get_json_via_transcode(yaml);
let decoded = get_json_via_decode(yaml);
assert_eq!(transcoded, decoded);
assert_eq!(transcoded, "\"line oneline two\"");
}
#[test]
fn test_transcode_tab_escape_literal() {
let yaml = b"\"tab\\\there\"";
let transcoded = get_json_via_transcode(yaml);
let decoded = get_json_via_decode(yaml);
assert_eq!(transcoded, decoded);
assert_eq!(transcoded, "\"tab\\there\"");
}
#[test]
fn test_transcode_matches_old_multiline_double_quoted_simple() {
let yaml = b"key: \"line one\n line two\"";
let transcoded = get_json_via_transcode(yaml);
assert!(
transcoded.contains("line one line two"),
"got: {}",
transcoded
);
}
#[test]
fn test_transcode_matches_old_multiline_double_quoted_empty_line() {
let yaml = b"key: \"line one\n\n line two\"";
let transcoded = get_json_via_transcode(yaml);
assert!(
transcoded.contains("line one\\nline two"),
"got: {}",
transcoded
);
}
#[test]
fn test_transcode_matches_old_multiline_single_quoted_simple() {
let yaml = b"key: 'line one\n line two'";
let transcoded = get_json_via_transcode(yaml);
assert!(
transcoded.contains("line one line two"),
"got: {}",
transcoded
);
}
#[test]
fn test_transcode_matches_old_multiline_single_quoted_with_escaped_quote() {
let yaml = b"key: 'it''s\n working'";
let transcoded = get_json_via_transcode(yaml);
assert!(transcoded.contains("it's working"), "got: {}", transcoded);
}
#[test]
fn test_anchor_metadata() {
let yaml = b"default: &myanchor value\nref: *myanchor";
let index = YamlIndex::build(yaml).unwrap();
let root = index.root(yaml);
if let YamlValue::Mapping(fields) = first_doc(root) {
for field in fields {
if let YamlValue::String(k) = field.key() {
let key = k.as_str().unwrap();
if key.as_ref() == "default" {
let cursor = field.value_cursor();
assert_eq!(cursor.anchor(), Some("myanchor"));
return;
}
}
}
}
panic!("Could not find anchored value");
}
#[test]
fn test_style_double_quoted() {
let yaml = b"key: \"value\"";
let index = YamlIndex::build(yaml).unwrap();
let root = index.root(yaml);
if let YamlValue::Mapping(fields) = first_doc(root) {
if let Some(field) = fields.into_iter().next() {
let cursor = field.value_cursor();
assert_eq!(cursor.style(), "double");
return;
}
}
panic!("Could not find value");
}
#[test]
fn test_style_single_quoted() {
let yaml = b"key: 'value'";
let index = YamlIndex::build(yaml).unwrap();
let root = index.root(yaml);
if let YamlValue::Mapping(fields) = first_doc(root) {
if let Some(field) = fields.into_iter().next() {
let cursor = field.value_cursor();
assert_eq!(cursor.style(), "single");
return;
}
}
panic!("Could not find value");
}
#[test]
fn test_style_literal_block() {
let yaml = b"key: |\n line1\n line2";
let index = YamlIndex::build(yaml).unwrap();
let root = index.root(yaml);
if let YamlValue::Mapping(fields) = first_doc(root) {
if let Some(field) = fields.into_iter().next() {
let cursor = field.value_cursor();
assert_eq!(cursor.style(), "literal");
return;
}
}
panic!("Could not find value");
}
#[test]
fn test_style_folded_block() {
let yaml = b"key: >\n line1\n line2";
let index = YamlIndex::build(yaml).unwrap();
let root = index.root(yaml);
if let YamlValue::Mapping(fields) = first_doc(root) {
if let Some(field) = fields.into_iter().next() {
let cursor = field.value_cursor();
assert_eq!(cursor.style(), "folded");
return;
}
}
panic!("Could not find value");
}
#[test]
fn test_style_flow_sequence() {
let yaml = b"key: [a, b, c]";
let index = YamlIndex::build(yaml).unwrap();
let root = index.root(yaml);
if let YamlValue::Mapping(fields) = first_doc(root) {
if let Some(field) = fields.into_iter().next() {
let cursor = field.value_cursor();
assert_eq!(cursor.style(), "flow");
return;
}
}
panic!("Could not find value");
}
#[test]
fn test_style_flow_mapping() {
let yaml = b"key: {a: 1, b: 2}";
let index = YamlIndex::build(yaml).unwrap();
let root = index.root(yaml);
if let YamlValue::Mapping(fields) = first_doc(root) {
if let Some(field) = fields.into_iter().next() {
let cursor = field.value_cursor();
assert_eq!(cursor.style(), "flow");
return;
}
}
panic!("Could not find value");
}
#[test]
fn test_style_plain() {
let yaml = b"key: plain_value";
let index = YamlIndex::build(yaml).unwrap();
let root = index.root(yaml);
if let YamlValue::Mapping(fields) = first_doc(root) {
if let Some(field) = fields.into_iter().next() {
let cursor = field.value_cursor();
assert_eq!(cursor.style(), "");
return;
}
}
panic!("Could not find value");
}
#[test]
fn test_tag_string() {
let yaml = b"key: hello";
let index = YamlIndex::build(yaml).unwrap();
let root = index.root(yaml);
if let YamlValue::Mapping(fields) = first_doc(root) {
if let Some(field) = fields.into_iter().next() {
let cursor = field.value_cursor();
assert_eq!(cursor.tag(), "!!str");
return;
}
}
panic!("Could not find value");
}
#[test]
fn test_tag_int() {
let yaml = b"key: 42";
let index = YamlIndex::build(yaml).unwrap();
let root = index.root(yaml);
if let YamlValue::Mapping(fields) = first_doc(root) {
if let Some(field) = fields.into_iter().next() {
let cursor = field.value_cursor();
assert_eq!(cursor.tag(), "!!int");
return;
}
}
panic!("Could not find value");
}
#[test]
fn test_tag_bool() {
let yaml = b"key: true";
let index = YamlIndex::build(yaml).unwrap();
let root = index.root(yaml);
if let YamlValue::Mapping(fields) = first_doc(root) {
if let Some(field) = fields.into_iter().next() {
let cursor = field.value_cursor();
assert_eq!(cursor.tag(), "!!bool");
return;
}
}
panic!("Could not find value");
}
#[test]
fn test_tag_null() {
let yaml = b"key: null";
let index = YamlIndex::build(yaml).unwrap();
let root = index.root(yaml);
if let YamlValue::Mapping(fields) = first_doc(root) {
if let Some(field) = fields.into_iter().next() {
let cursor = field.value_cursor();
assert_eq!(cursor.tag(), "!!null");
return;
}
}
panic!("Could not find value");
}
#[test]
fn test_tag_float() {
let yaml = b"key: 3.14";
let index = YamlIndex::build(yaml).unwrap();
let root = index.root(yaml);
if let YamlValue::Mapping(fields) = first_doc(root) {
if let Some(field) = fields.into_iter().next() {
let cursor = field.value_cursor();
assert_eq!(cursor.tag(), "!!float");
return;
}
}
panic!("Could not find value");
}
#[test]
fn test_tag_seq() {
let yaml = b"key:\n - a\n - b";
let index = YamlIndex::build(yaml).unwrap();
let root = index.root(yaml);
if let YamlValue::Mapping(fields) = first_doc(root) {
if let Some(field) = fields.into_iter().next() {
let cursor = field.value_cursor();
assert_eq!(cursor.tag(), "!!seq");
return;
}
}
panic!("Could not find sequence");
}
#[test]
fn test_tag_map() {
let yaml = b"outer:\n inner: value";
let index = YamlIndex::build(yaml).unwrap();
let root = index.root(yaml);
if let YamlValue::Mapping(fields) = first_doc(root) {
if let Some(field) = fields.into_iter().next() {
let cursor = field.value_cursor();
assert_eq!(cursor.tag(), "!!map");
return;
}
}
panic!("Could not find nested mapping");
}
#[test]
fn test_kind_scalar() {
let yaml = b"key: value";
let index = YamlIndex::build(yaml).unwrap();
let root = index.root(yaml);
if let YamlValue::Mapping(fields) = first_doc(root) {
if let Some(field) = fields.into_iter().next() {
let cursor = field.value_cursor();
assert_eq!(cursor.kind(), "scalar");
return;
}
}
panic!("Could not find value");
}
#[test]
fn test_kind_seq() {
let yaml = b"key:\n - a\n - b";
let index = YamlIndex::build(yaml).unwrap();
let root = index.root(yaml);
if let YamlValue::Mapping(fields) = first_doc(root) {
if let Some(field) = fields.into_iter().next() {
let cursor = field.value_cursor();
assert_eq!(cursor.kind(), "seq");
return;
}
}
panic!("Could not find sequence");
}
#[test]
fn test_kind_map() {
let yaml = b"outer:\n inner: value";
let index = YamlIndex::build(yaml).unwrap();
let root = index.root(yaml);
if let YamlValue::Mapping(fields) = first_doc(root) {
if let Some(field) = fields.into_iter().next() {
let cursor = field.value_cursor();
assert_eq!(cursor.kind(), "map");
return;
}
}
panic!("Could not find nested mapping");
}
#[test]
fn test_no_anchor() {
let yaml = b"key: value";
let index = YamlIndex::build(yaml).unwrap();
let root = index.root(yaml);
if let YamlValue::Mapping(fields) = first_doc(root) {
if let Some(field) = fields.into_iter().next() {
let cursor = field.value_cursor();
assert_eq!(cursor.anchor(), None);
return;
}
}
panic!("Could not find value");
}
#[test]
fn test_anchor_on_anchored_value() {
let yaml = b"default: &myanchor value";
let index = YamlIndex::build(yaml).unwrap();
let root = index.root(yaml);
if let YamlValue::Mapping(fields) = first_doc(root) {
if let Some(field) = fields.into_iter().next() {
let cursor = field.value_cursor();
assert_eq!(cursor.anchor(), Some("myanchor"));
assert!(!cursor.is_alias());
assert_eq!(cursor.alias(), None);
return;
}
}
panic!("Could not find value");
}
#[test]
fn test_alias_returns_anchor_name() {
let yaml = b"default: &myanchor value\nref: *myanchor";
let index = YamlIndex::build(yaml).unwrap();
let root = index.root(yaml);
if let YamlValue::Mapping(mut fields) = first_doc(root) {
fields.next();
if let Some(field) = fields.next() {
let cursor = field.value_cursor();
assert!(cursor.is_alias());
assert_eq!(cursor.alias(), Some("myanchor"));
assert_eq!(cursor.anchor(), None);
return;
}
}
panic!("Could not find alias");
}
#[test]
fn test_kind_alias() {
let yaml = b"default: &myanchor value\nref: *myanchor";
let index = YamlIndex::build(yaml).unwrap();
let root = index.root(yaml);
if let YamlValue::Mapping(mut fields) = first_doc(root) {
fields.next();
if let Some(field) = fields.next() {
let cursor = field.value_cursor();
assert_eq!(cursor.kind(), "alias");
return;
}
}
panic!("Could not find alias");
}
#[test]
fn test_alias_on_non_alias() {
let yaml = b"key: value";
let index = YamlIndex::build(yaml).unwrap();
let root = index.root(yaml);
if let YamlValue::Mapping(fields) = first_doc(root) {
if let Some(field) = fields.into_iter().next() {
let cursor = field.value_cursor();
assert!(!cursor.is_alias());
assert_eq!(cursor.alias(), None);
return;
}
}
panic!("Could not find value");
}
#[test]
fn test_alias_to_sequence() {
let yaml = b"items: &list\n - a\n - b\nref: *list";
let index = YamlIndex::build(yaml).unwrap();
let root = index.root(yaml);
if let YamlValue::Mapping(mut fields) = first_doc(root) {
if let Some(field) = fields.next() {
let cursor = field.value_cursor();
assert_eq!(cursor.anchor(), Some("list"));
assert_eq!(cursor.kind(), "seq");
}
if let Some(field) = fields.next() {
let cursor = field.value_cursor();
assert!(cursor.is_alias());
assert_eq!(cursor.alias(), Some("list"));
assert_eq!(cursor.kind(), "alias");
return;
}
}
panic!("Could not find alias");
}
#[test]
fn test_alias_to_mapping() {
let yaml = b"defaults: &defaults\n host: localhost\nproduction: *defaults";
let index = YamlIndex::build(yaml).unwrap();
let root = index.root(yaml);
if let YamlValue::Mapping(mut fields) = first_doc(root) {
if let Some(field) = fields.next() {
let cursor = field.value_cursor();
assert_eq!(cursor.anchor(), Some("defaults"));
assert_eq!(cursor.kind(), "map");
}
if let Some(field) = fields.next() {
let cursor = field.value_cursor();
assert!(cursor.is_alias());
assert_eq!(cursor.alias(), Some("defaults"));
assert_eq!(cursor.kind(), "alias");
return;
}
}
panic!("Could not find alias");
}
#[test]
fn test_line_basic() {
let yaml = b"name: Alice\nage: 30";
let index = YamlIndex::build(yaml).unwrap();
let root = index.root(yaml);
assert_eq!(root.line(), 1);
}
#[test]
fn test_line_multiline() {
let yaml = b"first: one\nsecond: two\nthird: three";
let index = YamlIndex::build(yaml).unwrap();
let root = index.root(yaml);
if let YamlValue::Mapping(mut fields) = first_doc(root) {
if let Some(field) = fields.next() {
let cursor = field.value_cursor();
assert_eq!(cursor.line(), 1);
}
if let Some(field) = fields.next() {
let cursor = field.value_cursor();
assert_eq!(cursor.line(), 2);
}
if let Some(field) = fields.next() {
let cursor = field.value_cursor();
assert_eq!(cursor.line(), 3);
}
}
}
#[test]
fn test_column_basic() {
let yaml = b"name: Alice";
let index = YamlIndex::build(yaml).unwrap();
let root = index.root(yaml);
assert_eq!(root.column(), 1);
if let YamlValue::Mapping(mut fields) = first_doc(root) {
if let Some(field) = fields.next() {
let cursor = field.value_cursor();
assert_eq!(cursor.column(), 7);
}
}
}
#[test]
fn test_column_sequence() {
let yaml = b"- first\n- second";
let index = YamlIndex::build(yaml).unwrap();
let root = index.root(yaml);
if let YamlValue::Sequence(elements) = first_doc(root) {
let mut cursor = elements.element_cursor.unwrap();
assert_eq!(cursor.column(), 1);
assert_eq!(cursor.line(), 1);
cursor = cursor.next_sibling().unwrap();
assert_eq!(cursor.column(), 1);
assert_eq!(cursor.line(), 2);
}
}
#[test]
fn test_line_and_column_nested() {
let yaml = b"outer:\n inner: value";
let index = YamlIndex::build(yaml).unwrap();
let root = index.root(yaml);
if let YamlValue::Mapping(mut fields) = first_doc(root) {
if let Some(outer_field) = fields.next() {
let outer_cursor = outer_field.value_cursor();
assert_eq!(outer_cursor.line(), 2);
assert_eq!(outer_cursor.column(), 3);
if let YamlValue::Mapping(mut inner_fields) = outer_field.value() {
if let Some(inner_field) = inner_fields.next() {
let inner_cursor = inner_field.value_cursor();
assert_eq!(inner_cursor.line(), 2);
assert_eq!(inner_cursor.column(), 10);
}
}
}
}
}
#[test]
fn test_line_flow_collection() {
let yaml = b"items: [a, b, c]";
let index = YamlIndex::build(yaml).unwrap();
let root = index.root(yaml);
if let YamlValue::Mapping(mut fields) = first_doc(root) {
if let Some(field) = fields.next() {
let cursor = field.value_cursor();
assert_eq!(cursor.line(), 1);
assert_eq!(cursor.column(), 8);
}
}
}
#[test]
fn test_line_block_scalar() {
let yaml = b"text: |\n line one\n line two";
let index = YamlIndex::build(yaml).unwrap();
let root = index.root(yaml);
if let YamlValue::Mapping(mut fields) = first_doc(root) {
if let Some(field) = fields.next() {
let cursor = field.value_cursor();
assert_eq!(cursor.line(), 1);
assert_eq!(cursor.column(), 7);
}
}
}
}