Skip to main content

mq_lang/
selector.rs

1use std::fmt::{self, Display, Formatter};
2
3#[cfg(feature = "ast-json")]
4use serde::{Deserialize, Serialize};
5use thiserror::Error;
6
7use crate::{Token, TokenKind};
8
9/// Error type returned when an unknown selector is encountered during parsing.
10#[derive(Error, Clone, Debug, PartialOrd, Eq, Ord, PartialEq)]
11#[error("Unknown selector `{0}`")]
12pub struct UnknownSelector(pub Token);
13
14/// Parses a bracket-based selector string like `.[n]` (List) or `.[n][m]` (Table).
15///
16/// Returns `Some(Selector)` if the string matches, `None` otherwise.
17fn parse_bracket_selector(s: &str) -> Option<Selector> {
18    let inner = s.strip_prefix(".[")?;
19    let (first, rest) = inner.split_once(']')?;
20
21    if !first.is_empty() && !first.bytes().all(|b| b.is_ascii_digit()) {
22        return None;
23    }
24    let first_idx: Option<usize> = if first.is_empty() {
25        None
26    } else {
27        Some(first.parse().ok()?)
28    };
29
30    if rest.is_empty() {
31        // ".[n]" → List
32        return Some(Selector::List(first_idx, None));
33    }
34
35    let inner2 = rest.strip_prefix('[')?;
36    let (second, tail) = inner2.split_once(']')?;
37    if !tail.is_empty() {
38        return None;
39    }
40    if !second.is_empty() && !second.bytes().all(|b| b.is_ascii_digit()) {
41        return None;
42    }
43    let second_idx: Option<usize> = if second.is_empty() {
44        None
45    } else {
46        Some(second.parse().ok()?)
47    };
48    // ".[n][m]" → Table
49    Some(Selector::Table(first_idx, second_idx))
50}
51
52impl UnknownSelector {
53    /// Creates a new `UnknownSelector` error with the given token.
54    pub fn new(token: Token) -> Self {
55        Self(token)
56    }
57}
58
59/// A selector for matching specific types of markdown nodes.
60///
61/// Selectors are used to query and filter markdown documents, similar to CSS selectors
62/// for HTML. Each variant matches a specific type of markdown element.
63#[cfg_attr(feature = "ast-json", derive(Serialize, Deserialize))]
64#[derive(PartialEq, PartialOrd, Debug, Eq, Clone)]
65pub enum Selector {
66    /// Matches blockquote elements (e.g., `> quoted text`).
67    Blockquote,
68    /// Matches footnote definitions.
69    Footnote,
70    /// Matches list elements.
71    ///
72    /// The first `Option<usize>` specifies an item index, the second `Option<bool>` indicates ordered/unordered.
73    List(Option<usize>, Option<bool>),
74    /// Matches TOML frontmatter blocks.
75    Toml,
76    /// Matches YAML frontmatter blocks.
77    Yaml,
78    /// Matches line break elements.
79    Break,
80    /// Matches inline code elements (e.g., `` `code` ``).
81    InlineCode,
82    /// Matches inline math elements (e.g., `$math$`).
83    InlineMath,
84    /// Matches strikethrough/delete elements (e.g., `~~text~~`).
85    Delete,
86    /// Matches emphasis elements (e.g., `*text*` or `_text_`).
87    Emphasis,
88    /// Matches footnote references (e.g., `[^1]`).
89    FootnoteRef,
90    /// Matches raw HTML elements.
91    Html,
92    /// Matches image elements (e.g., `![alt](url)`).
93    Image,
94    /// Matches image reference elements (e.g., `![alt][ref]`).
95    ImageRef,
96    /// Matches MDX JSX text elements.
97    MdxJsxTextElement,
98    /// Matches link elements (e.g., `[text](url)`).
99    Link,
100    /// Matches link reference elements (e.g., `[text][ref]`).
101    LinkRef,
102    /// Matches strong/bold elements (e.g., `**text**`).
103    Strong,
104    /// Matches code block elements.
105    Code,
106    /// Matches math block elements (e.g., `$$math$$`).
107    Math,
108    /// Matches heading elements.
109    ///
110    /// The `Option<u8>` specifies the heading level (1-6). If `None`, matches any heading level.
111    Heading(Option<u8>),
112    /// Matches table elements.
113    ///
114    /// The first `Option<usize>` specifies row index, the second specifies column index.
115    Table(Option<usize>, Option<usize>),
116    /// Matches table alignment elements.
117    TableAlign,
118    /// Matches text nodes.
119    Text,
120    /// Matches horizontal rule elements (e.g., `---`, `***`, `___`).
121    HorizontalRule,
122    /// Matches link/image definition elements.
123    Definition,
124    /// Matches MDX flow expression elements.
125    MdxFlowExpression,
126    /// Matches MDX text expression elements.
127    MdxTextExpression,
128    /// Matches MDX ES module import/export elements.
129    MdxJsEsm,
130    /// Matches MDX JSX flow elements.
131    MdxJsxFlowElement,
132    /// Matches recursively all child nodes.
133    Recursive,
134    /// Matches a specific attribute of a markdown node.
135    Attr(AttrKind),
136}
137
138/// Represents an attribute that can be accessed from markdown nodes.
139///
140/// Attributes allow extracting specific properties from markdown elements,
141/// such as the URL from a link or the language from a code block.
142#[cfg_attr(feature = "ast-json", derive(Serialize, Deserialize))]
143#[derive(PartialEq, PartialOrd, Debug, Eq, Clone)]
144pub enum AttrKind {
145    /// The text value or content of a node.
146    Value,
147    /// Collection of values (used for certain node types).
148    Values,
149    /// The children nodes of an element.
150    Children,
151
152    /// The programming language identifier for code blocks.
153    Lang,
154    /// Additional metadata for code blocks.
155    Meta,
156    /// The fence character used for code blocks (e.g., `` ` `` or `~`).
157    Fence,
158
159    /// The URL for links and images.
160    Url,
161    /// The alt text for images.
162    Alt,
163    /// The title attribute for links and images.
164    Title,
165
166    /// The identifier for references (LinkRef, ImageRef, FootnoteRef, Definition, Footnote).
167    Ident,
168    /// The label for references.
169    Label,
170
171    /// The depth level of a heading (1-6).
172    Depth,
173    /// Alias for `Depth` - the level of a heading.
174    Level,
175
176    /// The index of a list item within its parent list.
177    Index,
178    /// Whether a list is ordered (numbered) or unordered.
179    Ordered,
180    /// The checked status of a task list item.
181    Checked,
182
183    /// The column index of a table cell.
184    Column,
185    /// The row index of a table cell.
186    Row,
187
188    /// The alignment of a table header (left, right, center, none).
189    Align,
190
191    /// The name attribute for MDX JSX elements.
192    Name,
193}
194
195impl Display for AttrKind {
196    fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), fmt::Error> {
197        match self {
198            AttrKind::Value => write!(f, ".value"),
199            AttrKind::Values => write!(f, ".values"),
200            AttrKind::Children => write!(f, ".children"),
201            AttrKind::Lang => write!(f, ".lang"),
202            AttrKind::Meta => write!(f, ".meta"),
203            AttrKind::Fence => write!(f, ".fence"),
204            AttrKind::Url => write!(f, ".url"),
205            AttrKind::Alt => write!(f, ".alt"),
206            AttrKind::Title => write!(f, ".title"),
207            AttrKind::Ident => write!(f, ".ident"),
208            AttrKind::Label => write!(f, ".label"),
209            AttrKind::Depth => write!(f, ".depth"),
210            AttrKind::Level => write!(f, ".level"),
211            AttrKind::Index => write!(f, ".index"),
212            AttrKind::Ordered => write!(f, ".ordered"),
213            AttrKind::Checked => write!(f, ".checked"),
214            AttrKind::Column => write!(f, ".column"),
215            AttrKind::Row => write!(f, ".row"),
216            AttrKind::Align => write!(f, ".align"),
217            AttrKind::Name => write!(f, ".name"),
218        }
219    }
220}
221
222impl TryFrom<&Token> for Selector {
223    type Error = UnknownSelector;
224
225    fn try_from(token: &Token) -> Result<Self, Self::Error> {
226        if let TokenKind::Selector(s) = &token.kind {
227            match s.as_str() {
228                // Heading selectors
229                ".h" | ".heading" => Ok(Selector::Heading(None)),
230                ".h1" => Ok(Selector::Heading(Some(1))),
231                ".h2" => Ok(Selector::Heading(Some(2))),
232                ".h3" => Ok(Selector::Heading(Some(3))),
233                ".h4" => Ok(Selector::Heading(Some(4))),
234                ".h5" => Ok(Selector::Heading(Some(5))),
235                ".h6" => Ok(Selector::Heading(Some(6))),
236
237                // Blockquote
238                ".>" | ".blockquote" => Ok(Selector::Blockquote),
239
240                // Footnote
241                ".^" | ".footnote" => Ok(Selector::Footnote),
242
243                // MDX JSX Flow Element
244                ".<" | ".mdx_jsx_flow_element" => Ok(Selector::MdxJsxFlowElement),
245
246                // Emphasis
247                ".**" | ".emphasis" => Ok(Selector::Emphasis),
248
249                // Math
250                ".$$" | ".math" => Ok(Selector::Math),
251
252                // Horizontal Rule
253                ".horizontal_rule" | ".---" | ".***" | ".___" => Ok(Selector::HorizontalRule),
254
255                // MDX Text Expression
256                ".{}" | ".mdx_text_expression" => Ok(Selector::MdxTextExpression),
257
258                // Footnote Reference
259                ".[^]" | ".footnote_ref" => Ok(Selector::FootnoteRef),
260
261                // Definition
262                ".definition" => Ok(Selector::Definition),
263
264                // Break
265                ".break" => Ok(Selector::Break),
266
267                // Delete
268                ".delete" => Ok(Selector::Delete),
269
270                // HTML
271                ".<>" | ".html" => Ok(Selector::Html),
272
273                // Image
274                ".image" => Ok(Selector::Image),
275
276                // Image Reference
277                ".image_ref" => Ok(Selector::ImageRef),
278
279                // Inline Code
280                ".code_inline" => Ok(Selector::InlineCode),
281
282                // Inline Math
283                ".math_inline" => Ok(Selector::InlineMath),
284
285                // Link
286                ".link" => Ok(Selector::Link),
287
288                // Link Reference
289                ".link_ref" => Ok(Selector::LinkRef),
290
291                // List
292                ".[]" | ".list" => Ok(Selector::List(None, None)),
293
294                // TOML
295                ".toml" => Ok(Selector::Toml),
296
297                // Strong
298                ".strong" => Ok(Selector::Strong),
299
300                // YAML
301                ".yaml" => Ok(Selector::Yaml),
302
303                // Code
304                ".code" => Ok(Selector::Code),
305
306                // MDX JS ESM
307                ".mdx_js_esm" => Ok(Selector::MdxJsEsm),
308
309                // MDX JSX Text Element
310                ".mdx_jsx_text_element" => Ok(Selector::MdxJsxTextElement),
311
312                // MDX Flow Expression
313                ".mdx_flow_expression" => Ok(Selector::MdxFlowExpression),
314
315                // Text
316                ".text" => Ok(Selector::Text),
317
318                // Table
319                ".[][]" | ".table" => Ok(Selector::Table(None, None)),
320
321                // Table Align
322                ".table_align" => Ok(Selector::TableAlign),
323
324                // Recursive
325                ".." => Ok(Selector::Recursive),
326
327                // Attribute selectors - Common
328                ".value" => Ok(Selector::Attr(AttrKind::Value)),
329                ".values" => Ok(Selector::Attr(AttrKind::Values)),
330                ".children" | ".cn" => Ok(Selector::Attr(AttrKind::Children)),
331
332                // Attribute selectors - Code
333                ".lang" => Ok(Selector::Attr(AttrKind::Lang)),
334                ".meta" => Ok(Selector::Attr(AttrKind::Meta)),
335                ".fence" => Ok(Selector::Attr(AttrKind::Fence)),
336
337                // Attribute selectors - Link/Image
338                ".url" => Ok(Selector::Attr(AttrKind::Url)),
339                ".alt" => Ok(Selector::Attr(AttrKind::Alt)),
340                ".title" => Ok(Selector::Attr(AttrKind::Title)),
341
342                // Attribute selectors - Reference
343                ".ident" => Ok(Selector::Attr(AttrKind::Ident)),
344                ".label" => Ok(Selector::Attr(AttrKind::Label)),
345
346                // Attribute selectors - Heading
347                ".depth" => Ok(Selector::Attr(AttrKind::Depth)),
348                ".level" => Ok(Selector::Attr(AttrKind::Level)),
349
350                // Attribute selectors - List
351                ".index" => Ok(Selector::Attr(AttrKind::Index)),
352                ".ordered" => Ok(Selector::Attr(AttrKind::Ordered)),
353                ".checked" => Ok(Selector::Attr(AttrKind::Checked)),
354
355                // Attribute selectors - TableCell
356                ".column" => Ok(Selector::Attr(AttrKind::Column)),
357                ".row" => Ok(Selector::Attr(AttrKind::Row)),
358
359                // Attribute selectors - TableHeader
360                ".align" => Ok(Selector::Attr(AttrKind::Align)),
361
362                // Attribute selectors - MDX
363                ".name" => Ok(Selector::Attr(AttrKind::Name)),
364
365                _ => parse_bracket_selector(s).ok_or_else(|| UnknownSelector(token.clone())),
366            }
367        } else {
368            Err(UnknownSelector(token.clone()))
369        }
370    }
371}
372
373impl Display for Selector {
374    fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), fmt::Error> {
375        match self {
376            Selector::Heading(None) => write!(f, ".h"),
377            Selector::Heading(Some(1)) => write!(f, ".h1"),
378            Selector::Heading(Some(2)) => write!(f, ".h2"),
379            Selector::Heading(Some(3)) => write!(f, ".h3"),
380            Selector::Heading(Some(4)) => write!(f, ".h4"),
381            Selector::Heading(Some(5)) => write!(f, ".h5"),
382            Selector::Heading(Some(6)) => write!(f, ".h6"),
383            Selector::Heading(Some(n)) => write!(f, ".h{}", n),
384            Selector::Blockquote => write!(f, ".blockquote"),
385            Selector::Footnote => write!(f, ".footnote"),
386            Selector::List(None, None) => write!(f, ".list"),
387            Selector::List(Some(idx), None) => write!(f, ".[{}]", idx),
388            Selector::List(Some(idx), _) => write!(f, ".[{}]", idx),
389            Selector::List(None, _) => write!(f, ".[]"),
390            Selector::Toml => write!(f, ".toml"),
391            Selector::Yaml => write!(f, ".yaml"),
392            Selector::Break => write!(f, ".break"),
393            Selector::InlineCode => write!(f, ".code_inline"),
394            Selector::InlineMath => write!(f, ".math_inline"),
395            Selector::Delete => write!(f, ".delete"),
396            Selector::Emphasis => write!(f, ".emphasis"),
397            Selector::FootnoteRef => write!(f, ".footnote_ref"),
398            Selector::Html => write!(f, ".html"),
399            Selector::Image => write!(f, ".image"),
400            Selector::ImageRef => write!(f, ".image_ref"),
401            Selector::MdxJsxTextElement => write!(f, ".mdx_jsx_text_element"),
402            Selector::Link => write!(f, ".link"),
403            Selector::LinkRef => write!(f, ".link_ref"),
404            Selector::Strong => write!(f, ".strong"),
405            Selector::Code => write!(f, ".code"),
406            Selector::Math => write!(f, ".math"),
407            Selector::Table(None, None) => write!(f, ".table"),
408            Selector::Table(Some(row), None) => write!(f, ".[{}][]", row),
409            Selector::Table(Some(row), Some(col)) => write!(f, ".[{}][{}]", row, col),
410            Selector::Table(None, Some(col)) => write!(f, ".[][{}]", col),
411            Selector::TableAlign => write!(f, ".table_align"),
412            Selector::Text => write!(f, ".text"),
413            Selector::HorizontalRule => write!(f, ".horizontal_rule"),
414            Selector::Definition => write!(f, ".definition"),
415            Selector::MdxFlowExpression => write!(f, ".mdx_flow_expression"),
416            Selector::MdxTextExpression => write!(f, ".mdx_text_expression"),
417            Selector::MdxJsEsm => write!(f, ".mdx_js_esm"),
418            Selector::MdxJsxFlowElement => write!(f, ".mdx_jsx_flow_element"),
419            Selector::Recursive => write!(f, ".."),
420            Selector::Attr(attr) => write!(f, "{}", attr),
421        }
422    }
423}
424
425impl Selector {
426    /// Returns `true` if this is an attribute selector.
427    pub fn is_attribute_selector(&self) -> bool {
428        matches!(self, Selector::Attr(_))
429    }
430}
431
432#[cfg(test)]
433mod tests {
434    use crate::{
435        ArenaId, Position, Range, Token, TokenKind,
436        selector::{AttrKind, Selector, UnknownSelector},
437    };
438    use rstest::rstest;
439    use smol_str::SmolStr;
440
441    #[rstest]
442    // Heading selectors
443    #[case::heading(".h", Selector::Heading(None), ".h")]
444    #[case::heading_h1(".h1", Selector::Heading(Some(1)), ".h1")]
445    #[case::heading_h2(".h2", Selector::Heading(Some(2)), ".h2")]
446    #[case::heading_h3(".h3", Selector::Heading(Some(3)), ".h3")]
447    #[case::heading_h4(".h4", Selector::Heading(Some(4)), ".h4")]
448    #[case::heading_h5(".h5", Selector::Heading(Some(5)), ".h5")]
449    #[case::heading_h6(".h6", Selector::Heading(Some(6)), ".h6")]
450    // Blockquote
451    #[case::blockquote(".blockquote", Selector::Blockquote, ".blockquote")]
452    #[case::blockquote_alias(".>", Selector::Blockquote, ".blockquote")]
453    // Footnote
454    #[case::footnote(".footnote", Selector::Footnote, ".footnote")]
455    #[case::footnote_alias(".^", Selector::Footnote, ".footnote")]
456    // MDX JSX Flow Element
457    #[case::mdx_jsx_flow_element(".mdx_jsx_flow_element", Selector::MdxJsxFlowElement, ".mdx_jsx_flow_element")]
458    #[case::mdx_jsx_flow_element_alias(".<", Selector::MdxJsxFlowElement, ".mdx_jsx_flow_element")]
459    // Emphasis
460    #[case::emphasis(".emphasis", Selector::Emphasis, ".emphasis")]
461    #[case::emphasis_alias(".**", Selector::Emphasis, ".emphasis")]
462    // Math
463    #[case::math(".math", Selector::Math, ".math")]
464    #[case::math_alias(".$$", Selector::Math, ".math")]
465    // Horizontal Rule
466    #[case::horizontal_rule(".horizontal_rule", Selector::HorizontalRule, ".horizontal_rule")]
467    #[case::horizontal_rule_alias_dash(".---", Selector::HorizontalRule, ".horizontal_rule")]
468    #[case::horizontal_rule_alias_star(".***", Selector::HorizontalRule, ".horizontal_rule")]
469    #[case::horizontal_rule_alias_underscore(".___", Selector::HorizontalRule, ".horizontal_rule")]
470    // MDX Text Expression
471    #[case::mdx_text_expression(".mdx_text_expression", Selector::MdxTextExpression, ".mdx_text_expression")]
472    #[case::mdx_text_expression_alias(".{}", Selector::MdxTextExpression, ".mdx_text_expression")]
473    // Footnote Reference
474    #[case::footnote_ref(".footnote_ref", Selector::FootnoteRef, ".footnote_ref")]
475    #[case::footnote_ref_alias(".[^]", Selector::FootnoteRef, ".footnote_ref")]
476    // Definition
477    #[case::definition(".definition", Selector::Definition, ".definition")]
478    // Break
479    #[case::break_selector(".break", Selector::Break, ".break")]
480    // Delete
481    #[case::delete(".delete", Selector::Delete, ".delete")]
482    // HTML
483    #[case::html(".html", Selector::Html, ".html")]
484    #[case::html_alias(".<>", Selector::Html, ".html")]
485    // Image
486    #[case::image(".image", Selector::Image, ".image")]
487    // Image Reference
488    #[case::image_ref(".image_ref", Selector::ImageRef, ".image_ref")]
489    // Inline Code
490    #[case::code_inline(".code_inline", Selector::InlineCode, ".code_inline")]
491    // Inline Math
492    #[case::math_inline(".math_inline", Selector::InlineMath, ".math_inline")]
493    // Link
494    #[case::link(".link", Selector::Link, ".link")]
495    // Link Reference
496    #[case::link_ref(".link_ref", Selector::LinkRef, ".link_ref")]
497    // List
498    #[case::list(".list", Selector::List(None, None), ".list")]
499    #[case::list_bracket(".[]", Selector::List(None, None), ".list")]
500    #[case::list_with_index(".[1]", Selector::List(Some(1), None), ".[1]")]
501    // TOML
502    #[case::toml(".toml", Selector::Toml, ".toml")]
503    // Strong
504    #[case::strong(".strong", Selector::Strong, ".strong")]
505    // YAML
506    #[case::yaml(".yaml", Selector::Yaml, ".yaml")]
507    // Code
508    #[case::code(".code", Selector::Code, ".code")]
509    // MDX JS ESM
510    #[case::mdx_js_esm(".mdx_js_esm", Selector::MdxJsEsm, ".mdx_js_esm")]
511    // MDX JSX Text Element
512    #[case::mdx_jsx_text_element(".mdx_jsx_text_element", Selector::MdxJsxTextElement, ".mdx_jsx_text_element")]
513    // MDX Flow Expression
514    #[case::mdx_flow_expression(".mdx_flow_expression", Selector::MdxFlowExpression, ".mdx_flow_expression")]
515    // Text
516    #[case::text(".text", Selector::Text, ".text")]
517    // Table
518    #[case::table(".table", Selector::Table(None, None), ".table")]
519    #[case::table_bracket(".[][]", Selector::Table(None, None), ".table")]
520    #[case::table_row_any(".[1][]", Selector::Table(Some(1), None), ".[1][]")]
521    #[case::table_row_col(".[1][2]", Selector::Table(Some(1), Some(2)), ".[1][2]")]
522    #[case::table_any_col(".[][2]", Selector::Table(None, Some(2)), ".[][2]")]
523    // Table Align
524    #[case::table_align(".table_align", Selector::TableAlign, ".table_align")]
525    // Recursive
526    #[case::recursive("..", Selector::Recursive, "..")]
527    // Attribute selectors - Common
528    #[case::attr_value(".value", Selector::Attr(AttrKind::Value), ".value")]
529    #[case::attr_values(".values", Selector::Attr(AttrKind::Values), ".values")]
530    #[case::attr_children(".children", Selector::Attr(AttrKind::Children), ".children")]
531    // Attribute selectors - Code
532    #[case::attr_lang(".lang", Selector::Attr(AttrKind::Lang), ".lang")]
533    #[case::attr_meta(".meta", Selector::Attr(AttrKind::Meta), ".meta")]
534    #[case::attr_fence(".fence", Selector::Attr(AttrKind::Fence), ".fence")]
535    // Attribute selectors - Link/Image
536    #[case::attr_url(".url", Selector::Attr(AttrKind::Url), ".url")]
537    #[case::attr_alt(".alt", Selector::Attr(AttrKind::Alt), ".alt")]
538    #[case::attr_title(".title", Selector::Attr(AttrKind::Title), ".title")]
539    // Attribute selectors - Reference
540    #[case::attr_ident(".ident", Selector::Attr(AttrKind::Ident), ".ident")]
541    #[case::attr_label(".label", Selector::Attr(AttrKind::Label), ".label")]
542    // Attribute selectors - Heading
543    #[case::attr_depth(".depth", Selector::Attr(AttrKind::Depth), ".depth")]
544    #[case::attr_level(".level", Selector::Attr(AttrKind::Level), ".level")]
545    // Attribute selectors - List
546    #[case::attr_index(".index", Selector::Attr(AttrKind::Index), ".index")]
547    #[case::attr_ordered(".ordered", Selector::Attr(AttrKind::Ordered), ".ordered")]
548    #[case::attr_checked(".checked", Selector::Attr(AttrKind::Checked), ".checked")]
549    // Attribute selectors - TableCell
550    #[case::attr_column(".column", Selector::Attr(AttrKind::Column), ".column")]
551    #[case::attr_row(".row", Selector::Attr(AttrKind::Row), ".row")]
552    // Attribute selectors - TableHeader
553    #[case::attr_align(".align", Selector::Attr(AttrKind::Align), ".align")]
554    // Attribute selectors - MDX
555    #[case::attr_name(".name", Selector::Attr(AttrKind::Name), ".name")]
556    fn test_selector_try_from_and_display(
557        #[case] input: &str,
558        #[case] expected_selector: Selector,
559        #[case] expected_display: &str,
560    ) {
561        // Test TryFrom
562        let selector = Selector::try_from(&Token {
563            kind: TokenKind::Selector(SmolStr::new(input)),
564            range: Range {
565                start: Position::new(0, 0),
566                end: Position::new(0, 0),
567            },
568            module_id: ArenaId::new(0),
569        })
570        .expect("Should parse valid selector");
571        assert_eq!(selector, expected_selector);
572
573        // Test Display
574        assert_eq!(selector.to_string(), expected_display);
575    }
576
577    #[test]
578    fn test_selector_try_from_unknown() {
579        let token = Token {
580            kind: TokenKind::Selector(SmolStr::new(".unknown")),
581            range: Range {
582                start: Position::new(0, 0),
583                end: Position::new(0, 0),
584            },
585            module_id: ArenaId::new(0),
586        };
587        let result = Selector::try_from(&token);
588        assert!(result.is_err());
589        if let Err(e) = result {
590            assert_eq!(e, UnknownSelector(token));
591        }
592    }
593}