Skip to main content

altium_format/query/
parser.rs

1//! Parser for the ergonomic selector syntax.
2//!
3//! # Syntax Overview
4//!
5//! - `U1` - Component by designator
6//! - `R*` - Components matching designator pattern
7//! - `U1:3` - Pin 3 of component U1
8//! - `U1:VCC` - VCC pin of U1
9//! - `$LM358` - Component by part number
10//! - `~VCC` - Net by name
11//! - `@10K` - Component by value
12//! - `#Power` - Sheet by name
13//! - `R*@10K` - Resistors with value 10K
14//! - `component[x>1000]` - Type with property filter
15//! - `U*:output:unconnected` - Output pins on ICs that are unconnected
16
17use crate::error::{AltiumError, Result};
18
19use super::pattern::Pattern;
20use super::selector::{
21    Combinator, FilterOperator, FilterValue, NetConnectedTarget, PropertyFilter, PseudoSelector,
22    RecordMatcher, RecordType, Selector, SelectorChain, SelectorSegment,
23};
24
25/// Parser for selector strings.
26pub struct SelectorParser<'a> {
27    input: &'a str,
28    pos: usize,
29}
30
31impl<'a> SelectorParser<'a> {
32    /// Create a new parser for the given input.
33    pub fn new(input: &'a str) -> Self {
34        Self {
35            input: input.trim(),
36            pos: 0,
37        }
38    }
39
40    /// Parse the input into a Selector.
41    pub fn parse(mut self) -> Result<Selector> {
42        if self.input.is_empty() {
43            return Ok(Selector::any());
44        }
45
46        let mut alternatives = vec![];
47
48        loop {
49            self.skip_whitespace();
50            if self.is_at_end() {
51                break;
52            }
53
54            let chain = self.parse_chain()?;
55            alternatives.push(chain);
56
57            self.skip_whitespace();
58            if self.peek() == Some(',') {
59                self.advance();
60                self.skip_whitespace();
61            } else {
62                break;
63            }
64        }
65
66        if alternatives.is_empty() {
67            Ok(Selector::any())
68        } else {
69            Ok(Selector { alternatives })
70        }
71    }
72
73    /// Parse a selector chain (segments connected by combinators).
74    fn parse_chain(&mut self) -> Result<SelectorChain> {
75        let mut segments = vec![];
76
77        loop {
78            self.skip_whitespace();
79            if self.is_at_end() || self.peek() == Some(',') {
80                break;
81            }
82
83            let mut segment = self.parse_segment()?;
84
85            // Check for combinator
86            self.skip_whitespace();
87            if let Some(combinator) = self.try_parse_combinator() {
88                segment.combinator = Some(combinator);
89                self.skip_whitespace();
90            }
91
92            segments.push(segment);
93
94            // If no combinator was found and we're not at a comma/end, we're done
95            if segments.last().unwrap().combinator.is_none() {
96                // Check if next char could start a new segment
97                let next = self.peek();
98                let could_continue = next
99                    .map(|c| {
100                        c.is_alphanumeric()
101                            || c == '$'
102                            || c == '~'
103                            || c == '@'
104                            || c == '#'
105                            || c == '*'
106                            || c == '['
107                            || c == ':'
108                    })
109                    .unwrap_or(false);
110
111                if could_continue {
112                    // Implicit descendant combinator
113                    segments.last_mut().unwrap().combinator = Some(Combinator::Descendant);
114                } else {
115                    break;
116                }
117            }
118        }
119
120        Ok(SelectorChain { segments })
121    }
122
123    /// Parse a single selector segment.
124    fn parse_segment(&mut self) -> Result<SelectorSegment> {
125        self.skip_whitespace();
126
127        // Determine matcher type from prefix
128        let matcher = self.parse_matcher()?;
129
130        // Parse property filters [prop op value]
131        let mut filters = vec![];
132        while self.peek() == Some('[') {
133            filters.push(self.parse_property_filter()?);
134        }
135
136        // Parse pseudo-selectors :name
137        let mut pseudo = vec![];
138        while self.peek() == Some(':') {
139            // Look ahead to see if this is a pseudo-selector or part of matcher
140            let saved_pos = self.pos;
141            self.advance(); // skip ':'
142
143            // Check if this looks like a pseudo-selector
144            if let Some(c) = self.peek() {
145                if c.is_alphabetic() {
146                    let name = self.parse_identifier_chars();
147                    if let Some(ps) = PseudoSelector::from_name(&name) {
148                        pseudo.push(ps);
149                        continue;
150                    } else if name == "not" || name == "has" || name == "is" {
151                        // Parse nested selector
152                        self.expect('(')?;
153                        let inner = self.parse_until_char(')')?;
154                        self.expect(')')?;
155                        let inner_selector = SelectorParser::new(&inner).parse()?;
156                        let ps = match name.as_str() {
157                            "not" => PseudoSelector::Not(Box::new(inner_selector)),
158                            "has" => PseudoSelector::Has(Box::new(inner_selector)),
159                            "is" => PseudoSelector::Is(Box::new(inner_selector)),
160                            _ => unreachable!(),
161                        };
162                        pseudo.push(ps);
163                        continue;
164                    } else if name == "nth-child" || name == "nthchild" {
165                        self.expect('(')?;
166                        let n_str = self.parse_until_char(')')?;
167                        self.expect(')')?;
168                        let n: usize = n_str.trim().parse().map_err(|_| {
169                            AltiumError::Parse(format!("Invalid nth-child value: {}", n_str))
170                        })?;
171                        pseudo.push(PseudoSelector::NthChild(n));
172                        continue;
173                    }
174                }
175            }
176
177            // Not a pseudo-selector, restore position
178            self.pos = saved_pos;
179            break;
180        }
181
182        Ok(SelectorSegment {
183            matcher,
184            filters,
185            pseudo,
186            combinator: None,
187        })
188    }
189
190    /// Parse the record matcher part of a segment.
191    fn parse_matcher(&mut self) -> Result<RecordMatcher> {
192        match self.peek() {
193            Some('$') => {
194                // Part number: $LM358
195                self.advance();
196                let pattern = self.parse_pattern()?;
197                Ok(RecordMatcher::PartNumber(pattern))
198            }
199            Some('~') => {
200                // Net reference: ~VCC or ~VCC:pins
201                self.advance();
202                let net_pattern = self.parse_pattern()?;
203
204                // Check for :pins or :components suffix
205                if self.peek() == Some(':') {
206                    let saved_pos = self.pos;
207                    self.advance();
208                    let target_name = self.parse_identifier_chars();
209
210                    match target_name.to_lowercase().as_str() {
211                        "pins" => Ok(RecordMatcher::NetConnected {
212                            net: net_pattern,
213                            target: NetConnectedTarget::Pins,
214                        }),
215                        "components" => Ok(RecordMatcher::NetConnected {
216                            net: net_pattern,
217                            target: NetConnectedTarget::Components,
218                        }),
219                        _ => {
220                            // Not a net target, restore and return plain net
221                            self.pos = saved_pos;
222                            Ok(RecordMatcher::Net(net_pattern))
223                        }
224                    }
225                } else {
226                    Ok(RecordMatcher::Net(net_pattern))
227                }
228            }
229            Some('@') => {
230                // Value: @10K
231                self.advance();
232                let pattern = self.parse_pattern()?;
233                Ok(RecordMatcher::Value(pattern))
234            }
235            Some('#') => {
236                // Sheet: #Power
237                self.advance();
238                let pattern = self.parse_pattern()?;
239                Ok(RecordMatcher::Sheet(pattern))
240            }
241            Some('*') => {
242                // Any record
243                self.advance();
244                Ok(RecordMatcher::Any)
245            }
246            Some('[') | Some(':') => {
247                // Property filter or pseudo-selector without type specifier
248                Ok(RecordMatcher::Any)
249            }
250            _ => {
251                // Could be: designator, type name, or designator:pin
252                let ident = self.parse_identifier()?;
253
254                // Check if it's a type keyword
255                if let Some(record_type) = RecordType::try_parse(&ident) {
256                    return Ok(RecordMatcher::Type(record_type));
257                }
258
259                // It's a designator pattern
260                let designator_pattern = Pattern::new(&ident)?;
261
262                // Check for :pin suffix
263                if self.peek() == Some(':') {
264                    let saved_pos = self.pos;
265                    self.advance();
266
267                    // Check if this is a pin pattern or a pseudo-selector
268                    if let Some(c) = self.peek() {
269                        if c.is_alphanumeric() || c == '*' || c == '?' || c == '[' {
270                            let pin_ident = self.parse_identifier()?;
271
272                            // Check if it's actually a pseudo-selector
273                            if PseudoSelector::from_name(&pin_ident).is_some()
274                                || pin_ident == "not"
275                                || pin_ident == "has"
276                                || pin_ident == "is"
277                                || pin_ident.starts_with("nth")
278                            {
279                                // Restore and let pseudo-selector parsing handle it
280                                self.pos = saved_pos;
281                                return Ok(RecordMatcher::Designator(designator_pattern));
282                            }
283
284                            let pin_pattern = Pattern::new(&pin_ident)?;
285                            return Ok(RecordMatcher::Pin {
286                                component: designator_pattern,
287                                pin: pin_pattern,
288                            });
289                        }
290                    }
291                    // Restore position if not a pin pattern
292                    self.pos = saved_pos;
293                }
294
295                // Check for @value suffix (R*@10K)
296                if self.peek() == Some('@') {
297                    self.advance();
298                    let value_pattern = self.parse_pattern()?;
299                    return Ok(RecordMatcher::DesignatorWithValue {
300                        designator: designator_pattern,
301                        value: value_pattern,
302                    });
303                }
304
305                Ok(RecordMatcher::Designator(designator_pattern))
306            }
307        }
308    }
309
310    /// Parse a property filter like [rotation=90].
311    fn parse_property_filter(&mut self) -> Result<PropertyFilter> {
312        self.expect('[')?;
313        self.skip_whitespace();
314
315        // Parse property name
316        let property = self.parse_identifier()?;
317        self.skip_whitespace();
318
319        // Parse operator
320        let operator = self.parse_operator()?;
321        self.skip_whitespace();
322
323        // Parse value
324        let value = self.parse_filter_value()?;
325        self.skip_whitespace();
326
327        self.expect(']')?;
328
329        Ok(PropertyFilter {
330            property,
331            operator,
332            value,
333        })
334    }
335
336    /// Parse a filter operator.
337    fn parse_operator(&mut self) -> Result<FilterOperator> {
338        // Try two-character operators first
339        let two_char = self.peek_n(2);
340        if let Some(op) = two_char.and_then(|s| FilterOperator::try_parse(&s)) {
341            self.advance();
342            self.advance();
343            return Ok(op);
344        }
345
346        // Try single-character operators
347        if let Some(c) = self.peek() {
348            let s = c.to_string();
349            if let Some(op) = FilterOperator::try_parse(&s) {
350                self.advance();
351                return Ok(op);
352            }
353        }
354
355        Err(AltiumError::Parse(format!(
356            "Expected operator at position {}",
357            self.pos
358        )))
359    }
360
361    /// Parse a filter value.
362    fn parse_filter_value(&mut self) -> Result<FilterValue> {
363        match self.peek() {
364            Some('"') | Some('\'') => {
365                // Quoted string
366                let quote = self.advance().unwrap();
367                let value = self.parse_until_char(quote)?;
368                self.expect(quote)?;
369                Ok(FilterValue::String(value))
370            }
371            Some(c) if c.is_ascii_digit() || c == '-' || c == '.' => {
372                // Number
373                let num_str = self.parse_number_str();
374                let num: f64 = num_str
375                    .parse()
376                    .map_err(|_| AltiumError::Parse(format!("Invalid number: {}", num_str)))?;
377                Ok(FilterValue::Number(num))
378            }
379            _ => {
380                // Unquoted string or pattern
381                let value = self.parse_until_chars(&[']', ' ', '\t']);
382                if value.contains(['*', '?', '[']) {
383                    Ok(FilterValue::Pattern(Pattern::new(&value)?))
384                } else if value.eq_ignore_ascii_case("true") {
385                    Ok(FilterValue::Bool(true))
386                } else if value.eq_ignore_ascii_case("false") {
387                    Ok(FilterValue::Bool(false))
388                } else {
389                    Ok(FilterValue::String(value))
390                }
391            }
392        }
393    }
394
395    /// Try to parse a combinator.
396    fn try_parse_combinator(&mut self) -> Option<Combinator> {
397        match self.peek() {
398            Some('>') => {
399                self.advance();
400                Some(Combinator::DirectChild)
401            }
402            Some('/') => {
403                self.advance();
404                Some(Combinator::DirectChild)
405            }
406            _ => None,
407        }
408    }
409
410    /// Parse a pattern (identifier with wildcards).
411    fn parse_pattern(&mut self) -> Result<Pattern> {
412        let ident = self.parse_identifier()?;
413        Pattern::new(&ident)
414    }
415
416    /// Parse an identifier (alphanumeric + wildcards).
417    fn parse_identifier(&mut self) -> Result<String> {
418        let ident = self.parse_identifier_chars();
419        if ident.is_empty() {
420            Err(AltiumError::Parse(format!(
421                "Expected identifier at position {}",
422                self.pos
423            )))
424        } else {
425            Ok(ident)
426        }
427    }
428
429    /// Parse identifier characters.
430    /// Handles glob-style character classes [abc] properly.
431    fn parse_identifier_chars(&mut self) -> String {
432        let start = self.pos;
433        while let Some(c) = self.peek() {
434            if c.is_alphanumeric() || c == '_' || c == '-' || c == '*' || c == '?' || c == '.' {
435                self.advance();
436            } else if c == '[' {
437                // Check if this is a character class [abc] or a property filter [prop=val]
438                // Look ahead to see if we find ] before any = or other filter operators
439                let saved_pos = self.pos;
440                self.advance(); // consume '['
441
442                let mut found_close = false;
443                let mut looks_like_filter = false;
444
445                while let Some(inner_c) = self.peek() {
446                    if inner_c == ']' {
447                        self.advance();
448                        found_close = true;
449                        break;
450                    } else if inner_c == '=' || inner_c == '>' || inner_c == '<' {
451                        // Looks like a property filter, not a character class
452                        looks_like_filter = true;
453                        break;
454                    } else if inner_c == '[' || inner_c == ' ' || inner_c == '\t' {
455                        // Nested bracket or whitespace - not a character class
456                        looks_like_filter = true;
457                        break;
458                    }
459                    self.advance();
460                }
461
462                if !found_close || looks_like_filter {
463                    // Not a character class, restore position and stop
464                    self.pos = saved_pos;
465                    break;
466                }
467                // Otherwise we consumed a valid character class, continue
468            } else {
469                break;
470            }
471        }
472        self.input[start..self.pos].to_string()
473    }
474
475    /// Parse a number string.
476    fn parse_number_str(&mut self) -> String {
477        let start = self.pos;
478        while let Some(c) = self.peek() {
479            if c.is_ascii_digit() || c == '.' || c == '-' || c == '+' || c == 'e' || c == 'E' {
480                self.advance();
481            } else {
482                break;
483            }
484        }
485        self.input[start..self.pos].to_string()
486    }
487
488    /// Parse until a specific character.
489    fn parse_until_char(&mut self, end: char) -> Result<String> {
490        let start = self.pos;
491        let mut depth = 0;
492        while let Some(c) = self.peek() {
493            if c == '(' {
494                depth += 1;
495            } else if c == ')' {
496                if depth == 0 && end == ')' {
497                    break;
498                }
499                depth -= 1;
500            } else if c == end && depth == 0 {
501                break;
502            }
503            self.advance();
504        }
505        Ok(self.input[start..self.pos].to_string())
506    }
507
508    /// Parse until any of the specified characters.
509    fn parse_until_chars(&mut self, ends: &[char]) -> String {
510        let start = self.pos;
511        while let Some(c) = self.peek() {
512            if ends.contains(&c) {
513                break;
514            }
515            self.advance();
516        }
517        self.input[start..self.pos].to_string()
518    }
519
520    /// Expect a specific character.
521    fn expect(&mut self, expected: char) -> Result<()> {
522        match self.peek() {
523            Some(c) if c == expected => {
524                self.advance();
525                Ok(())
526            }
527            Some(c) => Err(AltiumError::Parse(format!(
528                "Expected '{}' but found '{}' at position {}",
529                expected, c, self.pos
530            ))),
531            None => Err(AltiumError::Parse(format!(
532                "Expected '{}' but reached end of input",
533                expected
534            ))),
535        }
536    }
537
538    /// Skip whitespace characters.
539    fn skip_whitespace(&mut self) {
540        while let Some(c) = self.peek() {
541            if c.is_whitespace() {
542                self.advance();
543            } else {
544                break;
545            }
546        }
547    }
548
549    /// Peek at the current character.
550    fn peek(&self) -> Option<char> {
551        self.input[self.pos..].chars().next()
552    }
553
554    /// Peek at the next n characters.
555    fn peek_n(&self, n: usize) -> Option<String> {
556        let remaining = &self.input[self.pos..];
557        if remaining.len() >= n {
558            Some(remaining[..n].to_string())
559        } else {
560            None
561        }
562    }
563
564    /// Advance to the next character.
565    fn advance(&mut self) -> Option<char> {
566        let c = self.peek();
567        if let Some(ch) = c {
568            self.pos += ch.len_utf8();
569        }
570        c
571    }
572
573    /// Check if at end of input.
574    fn is_at_end(&self) -> bool {
575        self.pos >= self.input.len()
576    }
577}
578
579/// Parse a selector string.
580pub fn parse(input: &str) -> Result<Selector> {
581    SelectorParser::new(input).parse()
582}
583
584#[cfg(test)]
585mod tests {
586    use super::*;
587
588    #[test]
589    fn test_parse_designator() {
590        let sel = parse("U1").unwrap();
591        assert_eq!(sel.alternatives.len(), 1);
592        let chain = &sel.alternatives[0];
593        assert_eq!(chain.segments.len(), 1);
594        match &chain.segments[0].matcher {
595            RecordMatcher::Designator(p) => assert_eq!(p.as_str(), "U1"),
596            _ => panic!("Expected Designator"),
597        }
598    }
599
600    #[test]
601    fn test_parse_designator_wildcard() {
602        let sel = parse("R*").unwrap();
603        match &sel.alternatives[0].segments[0].matcher {
604            RecordMatcher::Designator(p) => {
605                assert_eq!(p.as_str(), "R*");
606                assert!(!p.is_literal());
607            }
608            _ => panic!("Expected Designator"),
609        }
610    }
611
612    #[test]
613    fn test_parse_pin() {
614        let sel = parse("U1:3").unwrap();
615        match &sel.alternatives[0].segments[0].matcher {
616            RecordMatcher::Pin { component, pin } => {
617                assert_eq!(component.as_str(), "U1");
618                assert_eq!(pin.as_str(), "3");
619            }
620            _ => panic!("Expected Pin"),
621        }
622    }
623
624    #[test]
625    fn test_parse_pin_by_name() {
626        let sel = parse("U1:VCC").unwrap();
627        match &sel.alternatives[0].segments[0].matcher {
628            RecordMatcher::Pin { component, pin } => {
629                assert_eq!(component.as_str(), "U1");
630                assert_eq!(pin.as_str(), "VCC");
631            }
632            _ => panic!("Expected Pin"),
633        }
634    }
635
636    #[test]
637    fn test_parse_part_number() {
638        let sel = parse("$LM358").unwrap();
639        match &sel.alternatives[0].segments[0].matcher {
640            RecordMatcher::PartNumber(p) => assert_eq!(p.as_str(), "LM358"),
641            _ => panic!("Expected PartNumber"),
642        }
643    }
644
645    #[test]
646    fn test_parse_net() {
647        let sel = parse("~VCC").unwrap();
648        match &sel.alternatives[0].segments[0].matcher {
649            RecordMatcher::Net(p) => assert_eq!(p.as_str(), "VCC"),
650            _ => panic!("Expected Net"),
651        }
652    }
653
654    #[test]
655    fn test_parse_net_pins() {
656        let sel = parse("~VCC:pins").unwrap();
657        match &sel.alternatives[0].segments[0].matcher {
658            RecordMatcher::NetConnected { net, target } => {
659                assert_eq!(net.as_str(), "VCC");
660                assert_eq!(*target, NetConnectedTarget::Pins);
661            }
662            _ => panic!("Expected NetConnected"),
663        }
664    }
665
666    #[test]
667    fn test_parse_value() {
668        let sel = parse("@10K").unwrap();
669        match &sel.alternatives[0].segments[0].matcher {
670            RecordMatcher::Value(p) => assert_eq!(p.as_str(), "10K"),
671            _ => panic!("Expected Value"),
672        }
673    }
674
675    #[test]
676    fn test_parse_sheet() {
677        let sel = parse("#Power").unwrap();
678        match &sel.alternatives[0].segments[0].matcher {
679            RecordMatcher::Sheet(p) => assert_eq!(p.as_str(), "Power"),
680            _ => panic!("Expected Sheet"),
681        }
682    }
683
684    #[test]
685    fn test_parse_designator_with_value() {
686        let sel = parse("R*@10K").unwrap();
687        match &sel.alternatives[0].segments[0].matcher {
688            RecordMatcher::DesignatorWithValue { designator, value } => {
689                assert_eq!(designator.as_str(), "R*");
690                assert_eq!(value.as_str(), "10K");
691            }
692            _ => panic!("Expected DesignatorWithValue"),
693        }
694    }
695
696    #[test]
697    fn test_parse_type() {
698        let sel = parse("component").unwrap();
699        match &sel.alternatives[0].segments[0].matcher {
700            RecordMatcher::Type(t) => assert_eq!(*t, RecordType::Component),
701            _ => panic!("Expected Type"),
702        }
703    }
704
705    #[test]
706    fn test_parse_property_filter() {
707        let sel = parse("U*[rotation=90]").unwrap();
708        let segment = &sel.alternatives[0].segments[0];
709        assert_eq!(segment.filters.len(), 1);
710        assert_eq!(segment.filters[0].property, "rotation");
711        assert!(matches!(segment.filters[0].operator, FilterOperator::Equal));
712    }
713
714    #[test]
715    fn test_parse_pseudo_selector() {
716        let sel = parse("U*:unconnected").unwrap();
717        let segment = &sel.alternatives[0].segments[0];
718        assert_eq!(segment.pseudo.len(), 1);
719        assert!(matches!(segment.pseudo[0], PseudoSelector::Unconnected));
720    }
721
722    #[test]
723    fn test_parse_union() {
724        let sel = parse("R*, C*").unwrap();
725        assert_eq!(sel.alternatives.len(), 2);
726    }
727
728    #[test]
729    fn test_parse_child_combinator() {
730        let sel = parse("U1 > pin").unwrap();
731        let chain = &sel.alternatives[0];
732        assert_eq!(chain.segments.len(), 2);
733        assert!(matches!(
734            chain.segments[0].combinator,
735            Some(Combinator::DirectChild)
736        ));
737    }
738
739    #[test]
740    fn test_parse_slash_combinator() {
741        let sel = parse("U1/pin").unwrap();
742        let chain = &sel.alternatives[0];
743        assert_eq!(chain.segments.len(), 2);
744        assert!(matches!(
745            chain.segments[0].combinator,
746            Some(Combinator::DirectChild)
747        ));
748    }
749
750    #[test]
751    fn test_parse_descendant_combinator() {
752        let sel = parse("U1 pin").unwrap();
753        let chain = &sel.alternatives[0];
754        assert_eq!(chain.segments.len(), 2);
755        assert!(matches!(
756            chain.segments[0].combinator,
757            Some(Combinator::Descendant)
758        ));
759    }
760
761    #[test]
762    fn test_parse_complex() {
763        let sel = parse("U*:output:unconnected").unwrap();
764        let segment = &sel.alternatives[0].segments[0];
765        match &segment.matcher {
766            RecordMatcher::Designator(p) => assert_eq!(p.as_str(), "U*"),
767            _ => panic!("Expected Designator"),
768        }
769        assert_eq!(segment.pseudo.len(), 2);
770    }
771}