#![deny(missing_docs)]
#![allow(clippy::type_complexity)]
#![warn(clippy::unnecessary_to_owned)]
#![warn(clippy::redundant_clone)]
#![warn(clippy::inefficient_to_string)]
#![warn(clippy::manual_string_new)]
#![doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/README.md"))]
pub mod anchor_resolution;
mod as_yaml;
mod builder;
pub mod custom_tags;
pub mod debug;
mod error;
pub mod error_recovery;
mod lex;
mod nodes;
mod parse;
pub mod path;
mod scalar;
mod schema;
pub mod validator;
mod value;
pub mod visitor;
mod yaml;
pub use as_yaml::{yaml_eq, AsYaml, YamlKind, YamlNode};
pub use builder::{MappingBuilder, SequenceBuilder, YamlBuilder};
pub use error::{YamlError, YamlResult};
pub use lex::{
lex, lex_with_validation, lex_with_validation_config, SyntaxKind, ValidationConfig,
WhitespaceError, WhitespaceErrorCategory,
};
pub use parse::Parse;
pub use scalar::{ScalarStyle, ScalarType, ScalarValue};
pub use schema::{
CustomSchema, CustomValidationResult, Schema, SchemaValidator, ValidationError,
ValidationErrorKind, ValidationResult,
};
pub use yaml::{
Alias, Directive, Document, Lang, Mapping, MappingEntry, Scalar, ScalarConversionError,
Sequence, Set, TaggedNode, YamlFile,
};
pub mod advanced {
pub use rowan::TextRange;
use crate::yaml::SyntaxNode;
use crate::TextPosition;
pub fn syntax_node_range(node: &SyntaxNode) -> TextRange {
node.text_range()
}
pub fn text_position_to_range(pos: TextPosition) -> TextRange {
pos.into()
}
pub fn text_range_to_position(range: TextRange) -> TextPosition {
range.into()
}
}
pub use custom_tags::{
CompressedBinaryHandler,
CustomTagError,
CustomTagHandler,
CustomTagParser,
CustomTagRegistry,
EnvVarHandler,
JsonHandler,
TimestampHandler,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct TextPosition {
pub start: u32,
pub end: u32,
}
impl TextPosition {
pub fn new(start: u32, end: u32) -> Self {
Self { start, end }
}
pub fn len(&self) -> u32 {
self.end - self.start
}
pub fn is_empty(&self) -> bool {
self.start == self.end
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct LineColumn {
pub line: usize,
pub column: usize,
}
impl LineColumn {
pub fn new(line: usize, column: usize) -> Self {
Self { line, column }
}
}
impl std::fmt::Display for LineColumn {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}:{}", self.line, self.column)
}
}
pub fn byte_offset_to_line_column(text: &str, byte_offset: usize) -> LineColumn {
let mut line = 1;
let mut column = 1;
for (i, ch) in text.char_indices() {
if i >= byte_offset {
break;
}
if ch == '\n' {
line += 1;
column = 1;
} else {
column += 1;
}
}
LineColumn { line, column }
}
impl From<rowan::TextRange> for TextPosition {
fn from(range: rowan::TextRange) -> Self {
Self {
start: u32::from(range.start()),
end: u32::from(range.end()),
}
}
}
impl From<TextPosition> for rowan::TextRange {
fn from(pos: TextPosition) -> Self {
rowan::TextRange::new(pos.start.into(), pos.end.into())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum ParseErrorKind {
UnclosedFlowSequence,
UnclosedFlowMapping,
UnterminatedString,
Other,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct PositionedParseError {
pub message: String,
pub range: TextPosition,
pub code: Option<String>,
pub kind: ParseErrorKind,
}
impl PositionedParseError {
pub fn start_position(&self, source_text: &str) -> LineColumn {
byte_offset_to_line_column(source_text, self.range.start as usize)
}
pub fn end_position(&self, source_text: &str) -> LineColumn {
byte_offset_to_line_column(source_text, self.range.end as usize)
}
}
impl std::fmt::Display for PositionedParseError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.message)
}
}
impl std::error::Error for PositionedParseError {}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Indentation {
FieldNameLength,
Spaces(u32),
}
impl Default for Indentation {
fn default() -> Self {
Indentation::Spaces(2)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::str::FromStr;
#[test]
fn test_byte_offset_to_line_column_basic() {
let text = "line 1\nline 2\nline 3";
let pos = byte_offset_to_line_column(text, 0);
assert_eq!(pos.line, 1);
assert_eq!(pos.column, 1);
let pos = byte_offset_to_line_column(text, 7);
assert_eq!(pos.line, 2);
assert_eq!(pos.column, 1);
let pos = byte_offset_to_line_column(text, 10);
assert_eq!(pos.line, 2);
assert_eq!(pos.column, 4);
let pos = byte_offset_to_line_column(text, 14);
assert_eq!(pos.line, 3);
assert_eq!(pos.column, 1);
}
#[test]
fn test_byte_offset_to_line_column_unicode() {
let text = "hello\n世界\nworld";
let pos = byte_offset_to_line_column(text, 0);
assert_eq!(pos.line, 1);
assert_eq!(pos.column, 1);
let pos = byte_offset_to_line_column(text, 6);
assert_eq!(pos.line, 2);
assert_eq!(pos.column, 1);
let pos = byte_offset_to_line_column(text, 9);
assert_eq!(pos.line, 2);
assert_eq!(pos.column, 2);
}
#[test]
fn test_line_column_display() {
let pos = LineColumn::new(42, 17);
assert_eq!(format!("{}", pos), "42:17");
let pos2 = LineColumn::new(1, 1);
assert_eq!(format!("{}", pos2), "1:1");
}
#[test]
fn test_document_position() {
let text = "name: Alice\nage: 30";
let doc = Document::from_str(text).unwrap();
let start = doc.start_position(text);
assert_eq!(start.line, 1);
assert_eq!(start.column, 1);
let range = doc.byte_range();
assert_eq!(range.start, 0);
assert!(range.end > 0);
}
#[test]
fn test_mapping_position() {
let text = "server:\n host: localhost\n port: 8080";
let doc = Document::from_str(text).unwrap();
let mapping = doc.as_mapping().unwrap();
let start = mapping.start_position(text);
assert_eq!(start.line, 1);
assert_eq!(start.column, 1);
let server_mapping = mapping.get_mapping("server").unwrap();
let server_start = server_mapping.start_position(text);
assert_eq!(server_start.line, 2);
}
#[test]
fn test_scalar_position_via_nodes() {
let text = "name: Alice\nage: 30";
let doc = Document::from_str(text).unwrap();
let mapping = doc.as_mapping().unwrap();
let entries: Vec<_> = mapping.entries().collect();
assert!(entries.len() >= 2);
let first_entry = &entries[0];
let key_node = first_entry.key_node().unwrap();
assert_eq!(key_node.to_string().trim(), "name");
let value_node = first_entry.value_node().unwrap();
assert_eq!(value_node.to_string().trim(), "Alice");
}
#[test]
fn test_sequence_position() {
let text = "items:\n - apple\n - banana";
let doc = Document::from_str(text).unwrap();
let mapping = doc.as_mapping().unwrap();
let items_node = mapping.get("items").unwrap();
assert!(items_node.as_sequence().is_some());
}
#[test]
fn test_positioned_parse_error() {
let text = "invalid:\n - [unclosed";
let parse = Parse::parse_yaml(text);
let errors = parse.positioned_errors();
if errors.is_empty() {
return;
}
let err = &errors[0];
let start = err.start_position(text);
assert_eq!(start.line, 2);
}
#[test]
fn test_multiline_document_byte_offsets() {
let text = "# Comment\nname: Alice\n\nage: 30";
let doc = Document::from_str(text).unwrap();
let range = doc.byte_range();
assert_eq!(range.start, 10);
assert_eq!(range.end, 30);
let start = doc.start_position(text);
assert_eq!(start.line, 2);
assert_eq!(start.column, 1);
}
#[test]
fn test_nested_mapping_byte_ranges() {
let text = "server:\n database:\n host: localhost";
let doc = Document::from_str(text).unwrap();
let mapping = doc.as_mapping().unwrap();
let server_mapping = mapping.get_mapping("server").unwrap();
let server_range = server_mapping.byte_range();
assert!(server_range.end > server_range.start);
let server_pos = server_mapping.start_position(text);
assert!(server_pos.line > 0);
}
#[test]
fn test_empty_lines_positions() {
let text = "a: 1\n\n\nb: 2";
let pos1 = byte_offset_to_line_column(text, 0);
assert_eq!(pos1.line, 1);
let pos2 = byte_offset_to_line_column(text, 7);
assert_eq!(pos2.line, 4);
}
#[test]
fn test_document_end_position() {
let text = "key: value";
let doc = Document::from_str(text).unwrap();
let start = doc.start_position(text);
let end = doc.end_position(text);
assert_eq!(start.line, 1);
assert!(end.column >= start.column);
}
#[test]
fn test_mapping_end_position() {
let text = "a: 1\nb: 2";
let doc = Document::from_str(text).unwrap();
let mapping = doc.as_mapping().unwrap();
let start = mapping.start_position(text);
let end = mapping.end_position(text);
assert_eq!(start.line, 1);
assert_eq!(end.line, 2);
}
}