noyalib/cst/mod.rs
1// SPDX-License-Identifier: MIT OR Apache-2.0
2// Copyright (c) 2026 Noyalib. All rights reserved.
3
4//! Side-table CST (concrete syntax tree) for lossless round-tripping.
5//!
6//! This module is the implementation of the design described in
7//! `docs/design/green-tree.md`. It exposes a `Document` type that
8//! parses YAML byte-faithfully — every byte of the input is retained
9//! as a green-tree leaf — so that
10//! `parse_document(s).unwrap().to_string()` is byte-identical to `s`
11//! for any input the parser accepts.
12//!
13//! The `Document` API depends on the parser's `SpanTree`, which lives
14//! under the `std` feature.
15//!
16//! The `Value` API (`from_str`, `to_string`, `StreamingDeserializer`)
17//! is unchanged. Trivia capture is enabled only on this path; the
18//! fast path pays no extra cost.
19//!
20//! # Current scope
21//!
22//! - **Read access.** [`Document::as_value`]
23//! for a typed view, [`Document::span_at`]
24//! / [`Document::get`] for byte-range
25//! lookups by `path`, and
26//! [`Document::syntax`] for the green
27//! tree itself.
28//! - **Mutation.** [`Document::replace_span`]
29//! (primitive byte replacement) and
30//! [`Document::set`] (path-targeted, the
31//! wrapper most callers want). Both re-parse on edit and reject
32//! the change if the spliced source is invalid YAML, leaving the
33//! document untouched.
34//! [`Document::edit`] creates an
35//! [`EditSession`] that batches independent
36//! byte-range replacements and validates the assembled result once.
37//!
38//! - **Comments.** [`Document::comments_at`]
39//! classifies the comments decorating a node into a
40//! [`CommentBundle`], and
41//! [`Document::set_comment`] /
42//! [`Document::remove_comment`]
43//! write them back, addressed by
44//! [`CommentPosition`]. A leading block
45//! is written at the node's own indentation. Both go through
46//! `replace_span`, so they inherit its guard.
47//!
48//! - **Auto-formatting.** [`Emit`] turns a typed
49//! value into the YAML spelling that re-parses to exactly that
50//! value at a given site, so
51//! [`Document::insert_entry_value`],
52//! [`Document::push_back_value`]
53//! and
54//! [`Document::insert_after_value`]
55//! quote and escape what the fragment-taking mutators splice
56//! verbatim.
57//!
58//! The green tree itself is still a flat sequence of leaves under a
59//! single `Document` parent — sufficient for byte-faithful
60//! round-tripping and for the span-based edit primitive. Hierarchical
61//! nesting (per-mapping / per-sequence parent nodes) is tracked as a
62//! follow-up in `docs/design/green-tree.md`.
63//!
64//! # Examples
65//!
66//! ```
67//! use noyalib::cst::parse_document;
68//!
69//! let src = "name: noyalib # the project\nversion: 0.0.1\n";
70//! let doc = parse_document(src).unwrap();
71//! assert_eq!(doc.to_string(), src);
72//! ```
73//!
74//! # Parser configuration
75//!
76//! [`parse_document`] and
77//! [`parse_stream`] run under the default
78//! [`ParserConfig`](crate::ParserConfig), the same limits as
79//! [`from_str`](crate::from_str).
80//! [`parse_document_with_config`]
81//! and [`parse_stream_with_config`]
82//! take one, mirroring [`from_str_with_config`](crate::from_str_with_config);
83//! the returned [`Document`] keeps it for every
84//! later re-parse of its own source, so an edit never falls back to
85//! the defaults.
86//!
87//! # Multi-document streams
88//!
89//! Use [`parse_stream`] for inputs
90//! containing `---` / `...` separators — one
91//! [`Document`] per logical YAML document,
92//! with each slice covering the exact bytes of that document so
93//! concatenation reproduces the input verbatim:
94//!
95//! ```
96//! use noyalib::cst::{parse_stream, Document};
97//!
98//! let src = "---\nfoo: 1\n...\n---\nbar: 2\n";
99//! let docs = parse_stream(src).unwrap();
100//! assert_eq!(docs.len(), 2);
101//! assert_eq!(docs[0].as_value()["foo"].as_i64(), Some(1));
102//! assert_eq!(docs[1].as_value()["bar"].as_i64(), Some(2));
103//! let joined: String = docs.iter().map(Document::source).collect();
104//! assert_eq!(joined, src);
105//! ```
106
107mod anchor;
108mod annotated;
109mod builder;
110#[cfg(feature = "validate-schema")]
111mod coerce;
112mod document;
113mod emit;
114mod entry;
115mod format;
116mod green;
117mod syntax;
118
119pub use anchor::{AliasInfo, AnchorInfo};
120pub use annotated::{CommentBundle, CommentPosition};
121#[cfg(feature = "validate-schema")]
122#[cfg_attr(docsrs, doc(cfg(feature = "validate-schema")))]
123pub use coerce::coerce_to_schema;
124pub use document::{
125 Document, EditSession, RepairScope, parse_document, parse_document_with_config, parse_stream,
126 parse_stream_with_config,
127};
128pub use emit::{Emit, EmitCtx};
129pub use entry::Entry;
130pub use format::{FormatConfig, format, format_with_config, format_with_parser_config};
131pub use green::{GreenChild, GreenNode};
132pub use syntax::SyntaxKind;
133
134/// YAML's white space and line breaks (YAML 1.2.2 §5.4, §5.5). Rust's
135/// `str::trim` and `char::is_whitespace` also match Unicode spaces such
136/// as NEL (U+0085), NBSP and U+3000, which YAML reads as content, so
137/// CST code trims and tests source text with this set instead.
138pub(crate) const YAML_BLANK: [char; 4] = [' ', '\t', '\n', '\r'];
139
140/// `s` holds nothing but [`YAML_BLANK`] characters.
141pub(crate) fn is_yaml_blank(s: &str) -> bool {
142 s.trim_matches(YAML_BLANK).is_empty()
143}