use crate::comments::COMMENT;
use crate::quotes::{DelimitedReferences, QUOTES};
use crate::reference_literal::{decode_reference_literal, reference_literal_end};
use crate::{ParseError, ParserConfig, check_document};
use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum CstKind {
Document,
Line,
Group,
Whitespace,
Newline,
Comment,
OpenParen,
CloseParen,
Colon,
Reference,
}
impl CstKind {
pub fn as_str(self) -> &'static str {
match self {
CstKind::Document => "document",
CstKind::Line => "line",
CstKind::Group => "group",
CstKind::Whitespace => "whitespace",
CstKind::Newline => "newline",
CstKind::Comment => "comment",
CstKind::OpenParen => "open_paren",
CstKind::CloseParen => "close_paren",
CstKind::Colon => "colon",
CstKind::Reference => "reference",
}
}
pub fn is_token(self) -> bool {
!matches!(self, CstKind::Document | CstKind::Line | CstKind::Group)
}
}
impl fmt::Display for CstKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CstNode {
pub kind: CstKind,
pub children: Vec<CstNode>,
pub value: Option<String>,
pub quote: Option<char>,
token_text: String,
}
impl CstNode {
fn node(kind: CstKind, children: Vec<CstNode>) -> Self {
CstNode {
kind,
children,
value: None,
quote: None,
token_text: String::new(),
}
}
fn token(kind: CstKind, text: &str) -> Self {
CstNode {
kind,
children: Vec::new(),
value: None,
quote: None,
token_text: text.to_owned(),
}
}
pub fn is_token(&self) -> bool {
self.kind.is_token()
}
pub fn text(&self) -> String {
self.to_string()
}
pub fn tokens(&self) -> Vec<&CstNode> {
let mut tokens = Vec::new();
self.collect_tokens(&mut tokens);
tokens
}
fn collect_tokens<'a>(&'a self, tokens: &mut Vec<&'a CstNode>) {
if self.is_token() {
tokens.push(self);
} else {
for child in &self.children {
child.collect_tokens(tokens);
}
}
}
pub fn to_s_expression(&self) -> String {
let mut out = String::new();
self.write_s_expression(&mut out);
out
}
fn write_s_expression(&self, out: &mut String) {
if self.is_token() {
out.push_str(self.kind.as_str());
out.push_str(":\"");
for c in self.token_text.chars() {
match c {
'\\' => out.push_str("\\\\"),
'"' => out.push_str("\\\""),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
_ => out.push(c),
}
}
out.push('"');
return;
}
out.push('(');
out.push_str(self.kind.as_str());
for child in &self.children {
out.push(' ');
child.write_s_expression(out);
}
out.push(')');
}
}
impl fmt::Display for CstNode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.is_token() {
return f.write_str(&self.token_text);
}
for child in &self.children {
fmt::Display::fmt(child, f)?;
}
Ok(())
}
}
pub fn parse_lino_to_cst(document: &str) -> Result<CstNode, ParseError> {
parse_lino_to_cst_with_config(document, &ParserConfig::default())
}
pub fn parse_lino_to_cst_with_config(
document: &str,
config: &ParserConfig,
) -> Result<CstNode, ParseError> {
let prepared = check_document(document, config)?;
build_cst(document, &prepared, config.comments)
}
struct Container {
kind: CstKind,
children: Vec<CstNode>,
line: Option<Vec<CstNode>>,
}
impl Container {
fn new(kind: CstKind, children: Vec<CstNode>) -> Self {
Container {
kind,
children,
line: None,
}
}
fn line(&mut self) -> &mut Vec<CstNode> {
self.line.get_or_insert_with(Vec::new)
}
fn close_line(&mut self) {
if let Some(line) = self.line.take() {
self.children.push(CstNode::node(CstKind::Line, line));
}
}
fn finish(mut self) -> CstNode {
self.close_line();
CstNode::node(self.kind, self.children)
}
}
const BARE_REFERENCE_END: &[u8] = b" \t\r\n(:)";
fn build_cst(document: &str, prepared: &str, comments: bool) -> Result<CstNode, ParseError> {
debug_assert_eq!(document.len(), prepared.len());
let input = document.as_bytes();
let blanked = prepared.as_bytes();
let references = DelimitedReferences::new(document);
let mut containers = vec![Container::new(CstKind::Document, Vec::new())];
let mut position = 0;
while position < input.len() {
let byte = input[position];
let mut end = position + 1;
let innermost = containers.len() - 1;
if comments && byte == COMMENT as u8 && blanked[position] != byte {
while end < input.len() && input[end] != b'\n' && input[end] != b'\r' {
end += 1;
}
let token = CstNode::token(CstKind::Comment, &document[position..end]);
containers[innermost].line().push(token);
} else if byte == b'\r' || byte == b'\n' {
if byte == b'\r' && input.get(end) == Some(&b'\n') {
end += 1;
}
let token = CstNode::token(CstKind::Newline, &document[position..end]);
containers[innermost].line().push(token);
containers[innermost].close_line();
} else if byte == b' ' || byte == b'\t' {
while end < input.len() && (input[end] == b' ' || input[end] == b'\t') {
end += 1;
}
let token = CstNode::token(CstKind::Whitespace, &document[position..end]);
containers[innermost].line().push(token);
} else if byte == b'(' {
containers[innermost].line();
let open = CstNode::token(CstKind::OpenParen, &document[position..end]);
containers.push(Container::new(CstKind::Group, vec![open]));
} else if byte == b')' {
if containers.len() == 1 {
return Err(ParseError::InternalError(format!(
"Unbalanced \")\" at offset {position}"
)));
}
let mut group = containers.pop().expect("a group is open");
group.close_line();
group.children.push(CstNode::token(
CstKind::CloseParen,
&document[position..end],
));
let group = CstNode::node(CstKind::Group, group.children);
containers
.last_mut()
.expect("the document is open")
.line()
.push(group);
} else if byte == b':' {
let token = CstNode::token(CstKind::Colon, &document[position..end]);
containers[innermost].line().push(token);
} else {
let (reference_end, value, quote) =
read_reference(document, prepared, position, &references);
end = reference_end;
let mut token = CstNode::token(CstKind::Reference, &document[position..end]);
token.value = Some(value);
token.quote = quote;
containers[innermost].line().push(token);
}
position = end;
}
if containers.len() != 1 {
return Err(ParseError::InternalError(
"Unbalanced \"(\" at the end of the document".to_string(),
));
}
Ok(containers.pop().expect("the document is open").finish())
}
fn read_reference(
document: &str,
prepared: &str,
start: usize,
references: &DelimitedReferences,
) -> (usize, String, Option<char>) {
let blanked = prepared.as_bytes();
if let Some(end) = reference_literal_end(document, start) {
let text = &document[start..end];
let value = decode_reference_literal(text).unwrap_or_else(|_| text.to_owned());
return (end, value, None);
}
let first = blanked[start];
if QUOTES.contains(&first) {
if let Some(reading) = references.read(&document[start..]) {
return (start + reading.length, reading.value, Some(first as char));
}
}
let mut end = start + 1;
while end < blanked.len() && !BARE_REFERENCE_END.contains(&blanked[end]) {
end += 1;
}
(end, document[start..end].to_owned(), None)
}