granit_parser/lib.rs
1// Copyright 2015, Yuheng Chen.
2// Copyright 2023, Ethiraric.
3// See the LICENSE file at the top-level directory of this distribution.
4
5//! YAML 1.2 parser implementation in pure Rust.
6//!
7//! `granit-parser` is a low-level event parser. It reads YAML input and yields a stream of
8//! [`Event`] values paired with their source [`Span`].
9//! With the default `comments` feature, comments are emitted as `Event::Comment`. They
10//! are presentation metadata, not YAML data nodes, so consumers building YAML value trees should
11//! ignore them.
12//!
13//! Add it to your project:
14//!
15//! ```sh
16//! cargo add granit-parser
17//! ```
18//!
19//! # Usage
20//!
21//! ```rust
22//! # fn main() -> Result<(), granit_parser::ScanError> {
23//! # #[cfg(feature = "comments")]
24//! # {
25//! use granit_parser::{Event, Parser, Placement};
26//!
27//! let yaml = r#"# header
28//! items: # inline
29//! - milk
30//! - bread
31//! "#;
32//! let mut comments = Vec::new();
33//!
34//! for next in Parser::new_from_str(yaml) {
35//! let (event, span) = next?;
36//! if let Event::Comment(text, placement) = event {
37//! comments.push((
38//! text.into_owned(),
39//! placement,
40//! span.slice(yaml).unwrap().to_owned(),
41//! ));
42//! }
43//! }
44//!
45//! assert_eq!(
46//! comments,
47//! [
48//! (" header".to_owned(), Placement::Above, "# header".to_owned()),
49//! (" inline".to_owned(), Placement::Right, "# inline".to_owned()),
50//! ]
51//! );
52//! # }
53//! # Ok(())
54//! # }
55//! ```
56//!
57//! For comment events, the companion [`Span`] covers the whole source comment, including `#` and
58//! excluding the line break. With [`Parser::new_from_str`], [`Span::slice`] returns that source
59//! comment text.
60//!
61//! # Anchor and tag source metadata
62//!
63//! Set [`Options::emit_anchor_events`] to receive [`Event::Anchor`] with the assigned ID and
64//! original name, without `&`. Its companion span covers the complete `&name` token. This event
65//! immediately follows the anchored scalar or collection start, before any collection children.
66//! It is presentation metadata, so [`Event::is_node`] returns `false`. Its source range precedes
67//! the node content even though the event follows the node. Emission defaults to `false`, leaving
68//! existing event streams unchanged.
69//!
70//! The adjacency guarantee applies within each [`Parser`]. With [`ParserStack`], consume pending
71//! anchor metadata before pushing another source if relying on adjacency in the combined stream.
72//!
73//! Resolved [`Event::Alias`] events carry the target ID and a complete `*name` token span. An
74//! unresolved alias can instead be exposed through [`Options::emit_unresolved_aliases`]. Explicit
75//! tags remain on node events; [`Span::tag_span`] preserves their complete raw token, including
76//! verbatim notation and percent escapes that normalized tag helpers may not reproduce.
77//!
78//! ```rust
79//! use std::collections::BTreeMap;
80//! use granit_parser::{Event, Parser};
81//!
82//! let yaml = "message: &msg !note hello\ncopy: *msg\nexample: \"&msg is just text\"\n";
83//! let options = granit_parser::options! { emit_anchor_events: true };
84//! let mut anchors = BTreeMap::new();
85//! for result in Parser::new_from_str_with_options(yaml, options) {
86//! let (event, span) = result?;
87//! if let Some(tag_span) = span.tag_span() {
88//! assert_eq!(tag_span.slice(yaml), Some("!note"));
89//! }
90//! match event {
91//! Event::Anchor(id, name) => {
92//! assert_eq!(span.slice(yaml), Some("&msg"));
93//! anchors.insert(id, (name, span));
94//! }
95//! Event::Alias(id) => {
96//! let (name, definition_span) = &anchors[&id];
97//! assert_eq!(name, "msg");
98//! assert_eq!(definition_span.slice(yaml), Some("&msg"));
99//! assert_eq!(span.slice(yaml), Some("*msg"));
100//! }
101//! _ => {}
102//! }
103//! }
104//! assert_eq!(anchors.len(), 1); // The quoted text does not define an anchor.
105//! # Ok::<(), granit_parser::ScanError>(())
106//! ```
107//!
108//! # Limits
109//!
110//! [`Options::strict_indentation`] enables strict YAML indentation for flow collections. It
111//! defaults to `false` for compatibility with `PyYAML` and ruamel.yaml, accepting under-indented flow entries
112//! and delimiters. [`Options`] also controls comment emission and limits on buffered comments,
113//! simple-key lookahead, directive retention, and flow- and block-collection nesting. With the
114//! `comments` feature, comment tokens and events are emitted by default; setting
115//! `Options::emit_comments` to `false`
116//! recognizes and validates comments without capturing their text or emitting them. The defaults
117//! allow 96 buffered comment events, 1024 characters of simple-key lookahead, 1024 bytes of retained
118//! directive data, 16 reserved-directive parameters, 255 nested flow collections, and 255 nested
119//! block collections.
120//! Existing constructors use these defaults.
121//! Without the `comments` feature, comment capture and emission are compiled out of scanners,
122//! parsers, and parser stacks. YAML comments are still skipped and validated.
123//! [`Options::emit_comments`] remains available and defaults to `true` with the feature enabled
124//! and `false` without it. Setting it to `true` has no effect without the feature.
125//! [`Parser::new_from_str_with_options`], [`Parser::new_from_iter_with_options`],
126//! [`Parser::new_from_fallible_iter_with_options`], [`Parser::with_options`],
127//! [`Scanner::with_options`], and [`ParserStack::with_options`] accept customized options created
128//! with [`options!`].
129//!
130//! Tooling can set [`Options::emit_unresolved_aliases`] to `true` to receive
131//! [`Event::UnresolvedAlias`] nodes and continue structural parsing when an anchor is missing.
132//! These references remain invalid YAML and must be handled by the consumer. The option defaults
133//! to `false`, preserving the [`ErrorKind::UnknownAnchor`] error and termination.
134//!
135//! # Features
136//! **Note:** This crate's MSRV is `1.81.0`.
137//!
138//! #### `comments` (enabled by default)
139//! Enables scanner comment tokens, parser comment events, and their capture, placement, buffering,
140//! and continuation state. Without it, scanners skip comments without retaining their text, and
141//! comment-specific state is compiled out. YAML comment syntax and source positions are still
142//! validated and tracked.
143//!
144//! `Comment`, `Placement`, `TokenType::Comment`, `Event::Comment`, `ErrorKind::TooManyComments`,
145//! `Options::max_buffered_comment_events`, and `Input::may_contain_comments` are available only
146//! with this feature.
147//!
148//! #### `error_messages` (enabled by default)
149//! Provides human-readable text through [`ErrorKind`]'s `Display` implementation and
150//! [`ScanError::info`]. Disabling this feature makes both render an empty string while retaining
151//! machine-readable error kinds and source markers.
152//!
153//! #### `std`
154//! Retains the original `std::io::Error` inside [`InputIoError`] when constructed through
155//! `InputIoError::from_io` or its `From<std::io::Error>` implementation. Without this feature,
156//! [`InputIoError::from_message`] remains available for portable `no_std` error reporting.
157//!
158//! #### `debug_prints`
159//! Enables the `debug` module and usage of debug prints in the scanner and the parser. Do not
160//! enable if you are consuming the crate rather than working on it as this can significantly
161//! decrease performance. Output remains opt-in behind a local compile-time toggle in
162//! `src/debug.rs`.
163//!
164//! This feature does not raise the MSRV further.
165//!
166//! This feature enables `std` and is _not_ `no_std` compatible.
167
168#![forbid(unsafe_code)]
169#![warn(missing_docs, clippy::pedantic)]
170#![no_std]
171
172extern crate alloc;
173
174#[cfg(feature = "std")]
175extern crate std;
176
177mod char_traits;
178#[macro_use]
179mod debug;
180mod error;
181pub mod input;
182mod macros;
183mod options;
184mod parser;
185mod parser_stack;
186mod scanner;
187
188pub use crate::error::{ErrorKind, InputIoError, ScanError};
189pub use crate::input::{str::StrInput, BorrowedInput, BufferedInput, FallibleBufferedInput, Input};
190pub use crate::options::Options;
191pub use crate::parser::{
192 Event, EventReceiver, ParseResult, Parser, ParserTrait, SpannedEventReceiver, StructureStyle,
193 Tag, TryEventReceiver, TryLoadError, TrySpannedEventReceiver, YamlVersion,
194};
195pub use crate::parser_stack::{ParserStack, ReplayParser};
196#[cfg(feature = "comments")]
197pub use crate::scanner::{Comment, Placement};
198pub use crate::scanner::{Marker, ScalarStyle, Scanner, Span, Token, TokenType};
199
200// Keep every Rust example in the package README covered by `cargo test --doc` without duplicating
201// the README in the rendered crate-level documentation.
202#[cfg(doctest)]
203#[doc = include_str!("../README.md")]
204mod readme_doctests {}
205
206// Verify the feature-disabled public API without adding these checks to rendered documentation.
207#[cfg(all(doctest, not(feature = "comments")))]
208/// ```compile_fail
209/// use granit_parser::Comment;
210/// ```
211/// ```compile_fail
212/// use granit_parser::Placement;
213/// ```
214/// ```compile_fail
215/// let _ = granit_parser::Event::Comment;
216/// ```
217/// ```compile_fail
218/// let _ = granit_parser::TokenType::Comment;
219/// ```
220/// ```compile_fail
221/// let _ = granit_parser::Options::default().max_buffered_comment_events;
222/// ```
223/// ```compile_fail
224/// let _ = granit_parser::ErrorKind::TooManyComments;
225/// ```
226/// ```compile_fail
227/// use granit_parser::{Input, StrInput};
228/// let _ = <StrInput<'_> as Input>::may_contain_comments(&StrInput::new(""));
229/// ```
230mod comments_disabled_doctests {}