1use crate::{Diagnostic, DiagnosticKind, YamlError};
2
3pub const TARGET_YAML_VERSION: &str = "1.2.2";
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
8pub struct NodeId(pub u32);
9
10impl NodeId {
11 #[must_use]
17 pub fn from_usize(index: usize) -> Self {
18 Self(u32::try_from(index).expect("node arena is too large for u32-based node IDs"))
19 }
20
21 #[must_use]
23 pub const fn as_usize(self) -> usize {
24 self.0 as usize
25 }
26}
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
30pub struct Span {
31 pub start: u32,
33 pub end: u32,
35}
36
37impl Span {
38 #[must_use]
40 pub const fn new(start: u32, end: u32) -> Self {
41 Self { start, end }
42 }
43
44 #[must_use]
50 pub fn from_usize(start: usize, end: usize) -> Self {
51 Self::try_from((start, end)).expect("YAML source is too large for u32-based spans")
52 }
53
54 #[must_use]
56 pub const fn empty(offset: u32) -> Self {
57 Self {
58 start: offset,
59 end: offset,
60 }
61 }
62
63 pub(crate) fn usize_to_u32(offset: usize) -> u32 {
64 u32::try_from(offset).expect("YAML source is too large for u32-based spans")
65 }
66
67 pub(crate) fn offset_from_usize(base: u32, offset: usize) -> u32 {
68 base.checked_add(Self::usize_to_u32(offset))
69 .expect("YAML source is too large for u32-based spans")
70 }
71
72 #[must_use]
74 pub fn empty_from_usize(offset: usize) -> Self {
75 Self::empty(Self::usize_to_u32(offset))
76 }
77
78 #[must_use]
80 pub const fn len(self) -> u32 {
81 self.end.saturating_sub(self.start)
82 }
83
84 #[must_use]
86 pub const fn is_empty(self) -> bool {
87 self.start == self.end
88 }
89
90 #[must_use]
92 pub const fn contains(self, offset: u32) -> bool {
93 self.start <= offset && offset < self.end
94 }
95}
96
97impl TryFrom<(usize, usize)> for Span {
98 type Error = std::num::TryFromIntError;
99
100 fn try_from((start, end): (usize, usize)) -> Result<Self, Self::Error> {
101 Ok(Self {
102 start: Self::usize_to_u32(start),
103 end: Self::usize_to_u32(end),
104 })
105 }
106}
107#[derive(Debug, Clone, Copy, PartialEq, Eq)]
109pub struct LineCol {
110 pub line: usize,
112 pub column: usize,
114}
115
116#[derive(Debug, Clone, PartialEq, Eq)]
118pub struct Source {
119 text: String,
120 line_starts: Vec<u32>,
121}
122
123impl Source {
124 pub fn new(text: String) -> Result<Self, YamlError> {
132 let mut line_starts = Vec::with_capacity(text.len() / 32 + 1);
133 line_starts.push(0);
134 if text.is_ascii() {
135 for (offset, byte) in text.bytes().enumerate() {
136 if !matches!(byte, b'\t' | b'\n' | b'\r' | b' '..=b'~') {
137 return Err(invalid_yaml_character(offset, char::from(byte)));
138 }
139 if byte == b'\n' {
140 line_starts.push(Span::usize_to_u32(offset + 1));
141 }
142 }
143 } else {
144 for (offset, character) in text.char_indices() {
145 if !is_yaml_printable(character) {
146 return Err(invalid_yaml_character(offset, character));
147 }
148 if character == '\n' {
149 line_starts.push(Span::usize_to_u32(offset + 1));
150 }
151 }
152 }
153
154 Ok(Self { text, line_starts })
155 }
156
157 #[must_use]
159 pub fn as_str(&self) -> &str {
160 &self.text
161 }
162
163 #[must_use]
165 pub fn len(&self) -> usize {
166 self.text.len()
167 }
168
169 #[must_use]
171 pub fn is_empty(&self) -> bool {
172 self.text.is_empty()
173 }
174
175 #[must_use]
177 pub fn line_starts(&self) -> &[u32] {
178 &self.line_starts
179 }
180
181 #[must_use]
188 pub fn slice(&self, span: Span) -> &str {
189 self.try_slice(span)
190 .expect("span must be in bounds and on UTF-8 boundaries")
191 }
192
193 pub fn try_slice(&self, span: Span) -> Result<&str, YamlError> {
200 let start = span.start as usize;
201 let end = span.end as usize;
202
203 if start > end || end > self.text.len() {
204 return Err(YamlError::new(Diagnostic::new(
205 DiagnosticKind::Source,
206 "span is outside the source text",
207 span,
208 )));
209 }
210
211 self.text.get(start..end).ok_or_else(|| {
212 YamlError::new(Diagnostic::new(
213 DiagnosticKind::Source,
214 "span does not align with UTF-8 character boundaries",
215 span,
216 ))
217 })
218 }
219
220 #[must_use]
222 pub fn line_col(&self, offset: usize) -> LineCol {
223 let offset = Span::usize_to_u32(offset.min(self.text.len()));
224 let line_index = match self.line_starts.binary_search(&offset) {
225 Ok(index) => index,
226 Err(index) => index.saturating_sub(1),
227 };
228 let line_start = self.line_starts[line_index];
229
230 LineCol {
231 line: line_index + 1,
232 column: (offset - line_start) as usize + 1,
233 }
234 }
235
236 #[must_use]
238 pub fn diagnostic_position(&self, diagnostic: &Diagnostic) -> LineCol {
239 self.line_col(diagnostic.span.start as usize)
240 }
241}
242
243fn invalid_yaml_character(offset: usize, character: char) -> YamlError {
244 let span = Span::from_usize(offset, offset + character.len_utf8());
245 YamlError::new(
246 Diagnostic::new(
247 DiagnosticKind::Source,
248 format!("invalid YAML 1.2.2 character U+{:04X}", character as u32),
249 span,
250 )
251 .with_note(
252 "YAML streams may contain tab, line feeds, carriage returns, printable Unicode characters, and non-breaking spaces",
253 ),
254 )
255}
256
257pub(crate) fn validate_yaml_chars(text: &str) -> Result<(), YamlError> {
258 for (offset, character) in text.char_indices() {
259 if !is_yaml_printable(character) {
260 let span = Span::from_usize(offset, offset + character.len_utf8());
261 return Err(YamlError::new(
262 Diagnostic::new(
263 DiagnosticKind::Source,
264 format!(
265 "invalid YAML 1.2.2 character U+{:04X}",
266 character as u32
267 ),
268 span,
269 )
270 .with_note(
271 "YAML streams may contain tab, line feeds, carriage returns, printable Unicode characters, and non-breaking spaces",
272 ),
273 ));
274 }
275 }
276
277 Ok(())
278}
279
280const fn is_yaml_printable(character: char) -> bool {
281 matches!(
282 character as u32,
283 0x09 | 0x0A | 0x0D | 0x20..=0x7E | 0x85 | 0xA0..=0xD7FF | 0xE000..=0xFFFD | 0x001_0000..=0x0010_FFFF
284 )
285}