1use crate::comments::COMMENT;
20use crate::quotes::{DelimitedReferences, QUOTES};
21use crate::reference_literal::{decode_reference_literal, prefix_end};
22use crate::{ParseError, ParserConfig, check_document};
23use std::fmt;
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
27pub enum CstKind {
28 Document,
30 Line,
32 Group,
34 Whitespace,
36 Newline,
38 Comment,
40 OpenParen,
42 CloseParen,
44 Colon,
46 Reference,
48}
49
50impl CstKind {
51 pub fn as_str(self) -> &'static str {
56 match self {
57 CstKind::Document => "document",
58 CstKind::Line => "line",
59 CstKind::Group => "group",
60 CstKind::Whitespace => "whitespace",
61 CstKind::Newline => "newline",
62 CstKind::Comment => "comment",
63 CstKind::OpenParen => "open_paren",
64 CstKind::CloseParen => "close_paren",
65 CstKind::Colon => "colon",
66 CstKind::Reference => "reference",
67 }
68 }
69
70 pub fn is_token(self) -> bool {
72 !matches!(self, CstKind::Document | CstKind::Line | CstKind::Group)
73 }
74}
75
76impl fmt::Display for CstKind {
77 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
78 f.write_str(self.as_str())
79 }
80}
81
82#[derive(Debug, Clone, PartialEq, Eq)]
86pub struct CstNode {
87 pub kind: CstKind,
89 pub children: Vec<CstNode>,
91 pub value: Option<String>,
95 pub quote: Option<char>,
98 token_text: String,
100}
101
102impl CstNode {
103 fn node(kind: CstKind, children: Vec<CstNode>) -> Self {
104 CstNode {
105 kind,
106 children,
107 value: None,
108 quote: None,
109 token_text: String::new(),
110 }
111 }
112
113 fn token(kind: CstKind, text: &str) -> Self {
114 CstNode {
115 kind,
116 children: Vec::new(),
117 value: None,
118 quote: None,
119 token_text: text.to_owned(),
120 }
121 }
122
123 pub fn is_token(&self) -> bool {
125 self.kind.is_token()
126 }
127
128 pub fn text(&self) -> String {
131 self.to_string()
132 }
133
134 pub fn tokens(&self) -> Vec<&CstNode> {
136 let mut tokens = Vec::new();
137 self.collect_tokens(&mut tokens);
138 tokens
139 }
140
141 fn collect_tokens<'a>(&'a self, tokens: &mut Vec<&'a CstNode>) {
142 if self.is_token() {
143 tokens.push(self);
144 } else {
145 for child in &self.children {
146 child.collect_tokens(tokens);
147 }
148 }
149 }
150
151 pub fn to_s_expression(&self) -> String {
166 let mut out = String::new();
167 self.write_s_expression(&mut out);
168 out
169 }
170
171 fn write_s_expression(&self, out: &mut String) {
172 if self.is_token() {
173 out.push_str(self.kind.as_str());
174 out.push_str(":\"");
175 for c in self.token_text.chars() {
176 match c {
177 '\\' => out.push_str("\\\\"),
178 '"' => out.push_str("\\\""),
179 '\n' => out.push_str("\\n"),
180 '\r' => out.push_str("\\r"),
181 '\t' => out.push_str("\\t"),
182 _ => out.push(c),
183 }
184 }
185 out.push('"');
186 return;
187 }
188 out.push('(');
189 out.push_str(self.kind.as_str());
190 for child in &self.children {
191 out.push(' ');
192 child.write_s_expression(out);
193 }
194 out.push(')');
195 }
196}
197
198impl fmt::Display for CstNode {
199 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
200 if self.is_token() {
201 return f.write_str(&self.token_text);
202 }
203 for child in &self.children {
204 fmt::Display::fmt(child, f)?;
205 }
206 Ok(())
207 }
208}
209
210pub fn parse_lino_to_cst(document: &str) -> Result<CstNode, ParseError> {
238 parse_lino_to_cst_with_config(document, &ParserConfig::default())
239}
240
241pub fn parse_lino_to_cst_with_config(
257 document: &str,
258 config: &ParserConfig,
259) -> Result<CstNode, ParseError> {
260 let prepared = check_document(document, config)?;
261 build_cst(document, &prepared, config.comments)
262}
263
264struct Container {
267 kind: CstKind,
268 children: Vec<CstNode>,
269 line: Option<Vec<CstNode>>,
270}
271
272impl Container {
273 fn new(kind: CstKind, children: Vec<CstNode>) -> Self {
274 Container {
275 kind,
276 children,
277 line: None,
278 }
279 }
280
281 fn line(&mut self) -> &mut Vec<CstNode> {
283 self.line.get_or_insert_with(Vec::new)
284 }
285
286 fn close_line(&mut self) {
288 if let Some(line) = self.line.take() {
289 self.children.push(CstNode::node(CstKind::Line, line));
290 }
291 }
292
293 fn finish(mut self) -> CstNode {
294 self.close_line();
295 CstNode::node(self.kind, self.children)
296 }
297}
298
299const BARE_REFERENCE_END: &[u8] = b" \t\r\n(:)";
301
302fn build_cst(document: &str, prepared: &str, comments: bool) -> Result<CstNode, ParseError> {
306 debug_assert_eq!(document.len(), prepared.len());
309 let input = document.as_bytes();
310 let blanked = prepared.as_bytes();
311 let references = DelimitedReferences::new(prepared);
312 let mut containers = vec![Container::new(CstKind::Document, Vec::new())];
316 let mut position = 0;
317
318 while position < input.len() {
319 let byte = input[position];
320 let mut end = position + 1;
321 let innermost = containers.len() - 1;
322
323 if comments && byte == COMMENT as u8 && blanked[position] != byte {
324 while end < input.len() && input[end] != b'\n' && input[end] != b'\r' {
326 end += 1;
327 }
328 let token = CstNode::token(CstKind::Comment, &document[position..end]);
329 containers[innermost].line().push(token);
330 } else if byte == b'\r' || byte == b'\n' {
331 if byte == b'\r' && input.get(end) == Some(&b'\n') {
332 end += 1;
333 }
334 let token = CstNode::token(CstKind::Newline, &document[position..end]);
335 containers[innermost].line().push(token);
336 containers[innermost].close_line();
337 } else if byte == b' ' || byte == b'\t' {
338 while end < input.len() && (input[end] == b' ' || input[end] == b'\t') {
339 end += 1;
340 }
341 let token = CstNode::token(CstKind::Whitespace, &document[position..end]);
342 containers[innermost].line().push(token);
343 } else if byte == b'(' {
344 containers[innermost].line();
346 let open = CstNode::token(CstKind::OpenParen, &document[position..end]);
347 containers.push(Container::new(CstKind::Group, vec![open]));
348 } else if byte == b')' {
349 if containers.len() == 1 {
350 return Err(ParseError::InternalError(format!(
351 "Unbalanced \")\" at offset {position}"
352 )));
353 }
354 let mut group = containers.pop().expect("a group is open");
355 group.close_line();
356 group.children.push(CstNode::token(
357 CstKind::CloseParen,
358 &document[position..end],
359 ));
360 let group = CstNode::node(CstKind::Group, group.children);
361 containers
362 .last_mut()
363 .expect("the document is open")
364 .line()
365 .push(group);
366 } else if byte == b':' {
367 let token = CstNode::token(CstKind::Colon, &document[position..end]);
368 containers[innermost].line().push(token);
369 } else {
370 let (reference_end, value, quote) =
371 read_reference(document, prepared, position, &references);
372 end = reference_end;
373 let mut token = CstNode::token(CstKind::Reference, &document[position..end]);
374 token.value = Some(value);
375 token.quote = quote;
376 containers[innermost].line().push(token);
377 }
378 position = end;
379 }
380
381 if containers.len() != 1 {
382 return Err(ParseError::InternalError(
383 "Unbalanced \"(\" at the end of the document".to_string(),
384 ));
385 }
386 Ok(containers.pop().expect("the document is open").finish())
387}
388
389fn read_reference(
392 document: &str,
393 prepared: &str,
394 start: usize,
395 references: &DelimitedReferences,
396) -> (usize, String, Option<char>) {
397 let blanked = prepared.as_bytes();
398 if let Some(end) = literal_end(prepared, start) {
399 let text = &document[start..end];
400 let value = decode_reference_literal(text).unwrap_or_else(|_| text.to_owned());
401 return (end, value, None);
402 }
403 let first = blanked[start];
404 if QUOTES.contains(&first) {
405 if let Some(reading) = references.read(&prepared[start..]) {
406 return (start + reading.length, reading.value, Some(first as char));
407 }
408 }
409 let mut end = start + 1;
410 while end < blanked.len() && !BARE_REFERENCE_END.contains(&blanked[end]) {
411 end += 1;
412 }
413 (end, document[start..end].to_owned(), None)
414}
415
416fn literal_end(prepared: &str, start: usize) -> Option<usize> {
419 let rest = &prepared[start..];
420 let body = prefix_end(rest)?;
421 let bytes = rest.as_bytes();
422 let mut end = body;
423 while end < bytes.len() && !b"} \t\r\n()".contains(&bytes[end]) {
424 end += 1;
425 }
426 (bytes.get(end) == Some(&b'}')).then_some(start + end + 1)
427}