amarok_parser/builders/
helpers.rs

1use amarok_syntax::{Diagnostic, Span};
2use pest::iterators::Pair;
3
4use crate::grammar::Rule;
5
6pub(crate) fn span_of(pair: &Pair<Rule>) -> Span {
7    let span = pair.as_span();
8    Span::new(span.start(), span.end())
9}
10
11pub(crate) fn expect_single_inner<'input>(
12    pair: Pair<'input, Rule>,
13    context: &str,
14) -> Result<Pair<'input, Rule>, Diagnostic> {
15    let span = span_of(&pair);
16    let mut inner = pair.into_inner();
17    let first = inner.next().ok_or_else(|| {
18        Diagnostic::new(format!("{context} had no inner content.")).with_span(span)
19    })?;
20    if inner.next().is_some() {
21        return Err(
22            Diagnostic::new(format!("{context} had more than one inner element.")).with_span(span),
23        );
24    }
25    Ok(first)
26}
27
28/// Find the first inner child of `pair` matching `rule`.
29///
30/// `context` is included in the error message to identify which builder
31/// asked for it (e.g. "If statement missing condition expression.").
32pub(crate) fn find_child<'input>(
33    pair: Pair<'input, Rule>,
34    rule: Rule,
35    context: &str,
36) -> Result<Pair<'input, Rule>, Diagnostic> {
37    let span = span_of(&pair);
38    pair.into_inner()
39        .find(|p| p.as_rule() == rule)
40        .ok_or_else(|| Diagnostic::new(format!("{context} missing {rule:?}.")).with_span(span))
41}
42
43/// Convert a `path` parse node into its identifier segments, validating
44/// each child is an `identifier` and rejecting empty paths.
45pub(crate) fn collect_path_segments(
46    path_pair: Pair<Rule>,
47    context: &str,
48) -> Result<Vec<String>, Diagnostic> {
49    let path_span = span_of(&path_pair);
50    let mut path: Vec<String> = Vec::new();
51    for segment in path_pair.into_inner() {
52        if segment.as_rule() != Rule::identifier {
53            return Err(Diagnostic::new(format!(
54                "{context} expected identifier, got {:?}",
55                segment.as_rule()
56            ))
57            .with_span(span_of(&segment)));
58        }
59        path.push(segment.as_str().to_string());
60    }
61
62    if path.is_empty() {
63        return Err(Diagnostic::new(format!("{context} path was empty.")).with_span(path_span));
64    }
65
66    Ok(path)
67}
68
69pub(crate) fn unquote_string(text: &str, span: Span) -> Result<String, Diagnostic> {
70    if !text.starts_with('"') || !text.ends_with('"') || text.len() < 2 {
71        return Err(Diagnostic::new(format!("Invalid string literal: {text}")).with_span(span));
72    }
73
74    let content = &text[1..text.len() - 1];
75
76    // Minimal unescaping: support \" and \\ only.
77    let mut result = String::with_capacity(content.len());
78    let mut chars = content.chars();
79    while let Some(character) = chars.next() {
80        if character == '\\' {
81            let next = chars
82                .next()
83                .ok_or_else(|| Diagnostic::new("String ends with a backslash.").with_span(span))?;
84            match next {
85                '"' => result.push('"'),
86                '\\' => result.push('\\'),
87                other => {
88                    return Err(Diagnostic::new(format!(
89                        "Unsupported escape sequence: \\{other} (only \\\" and \\\\ supported)"
90                    ))
91                    .with_span(span));
92                }
93            }
94        } else {
95            result.push(character);
96        }
97    }
98
99    Ok(result)
100}