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 task list markdown node.
135    Task,
136    /// Matches a task list markdown node with an unchecked status.
137    Todo,
138    /// Matches a task list markdown node with a checked status.
139    Done,
140    /// Matches a specific attribute of a markdown node.
141    Attr(AttrKind),
142}
143
144/// Represents an attribute that can be accessed from markdown nodes.
145///
146/// Attributes allow extracting specific properties from markdown elements,
147/// such as the URL from a link or the language from a code block.
148#[cfg_attr(feature = "ast-json", derive(Serialize, Deserialize))]
149#[derive(PartialEq, PartialOrd, Debug, Eq, Clone)]
150pub enum AttrKind {
151    /// The text value or content of a node.
152    Value,
153    /// Collection of values (used for certain node types).
154    Values,
155    /// The children nodes of an element.
156    Children,
157
158    /// The programming language identifier for code blocks.
159    Lang,
160    /// Additional metadata for code blocks.
161    Meta,
162    /// The fence character used for code blocks (e.g., `` ` `` or `~`).
163    Fence,
164
165    /// The URL for links and images.
166    Url,
167    /// The alt text for images.
168    Alt,
169    /// The title attribute for links and images.
170    Title,
171
172    /// The identifier for references (LinkRef, ImageRef, FootnoteRef, Definition, Footnote).
173    Ident,
174    /// The label for references.
175    Label,
176
177    /// The depth level of a heading (1-6).
178    Depth,
179    /// Alias for `Depth` - the level of a heading.
180    Level,
181
182    /// The index of a list item within its parent list.
183    Index,
184    /// Whether a list is ordered (numbered) or unordered.
185    Ordered,
186    /// The checked status of a task list item.
187    Checked,
188
189    /// The column index of a table cell.
190    Column,
191    /// The row index of a table cell.
192    Row,
193
194    /// The alignment of a table header (left, right, center, none).
195    Align,
196
197    /// The name attribute for MDX JSX elements.
198    Name,
199}
200
201impl Display for AttrKind {
202    fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), fmt::Error> {
203        match self {
204            AttrKind::Value => write!(f, ".value"),
205            AttrKind::Values => write!(f, ".values"),
206            AttrKind::Children => write!(f, ".children"),
207            AttrKind::Lang => write!(f, ".lang"),
208            AttrKind::Meta => write!(f, ".meta"),
209            AttrKind::Fence => write!(f, ".fence"),
210            AttrKind::Url => write!(f, ".url"),
211            AttrKind::Alt => write!(f, ".alt"),
212            AttrKind::Title => write!(f, ".title"),
213            AttrKind::Ident => write!(f, ".ident"),
214            AttrKind::Label => write!(f, ".label"),
215            AttrKind::Depth => write!(f, ".depth"),
216            AttrKind::Level => write!(f, ".level"),
217            AttrKind::Index => write!(f, ".index"),
218            AttrKind::Ordered => write!(f, ".ordered"),
219            AttrKind::Checked => write!(f, ".checked"),
220            AttrKind::Column => write!(f, ".column"),
221            AttrKind::Row => write!(f, ".row"),
222            AttrKind::Align => write!(f, ".align"),
223            AttrKind::Name => write!(f, ".name"),
224        }
225    }
226}
227
228impl TryFrom<&Token> for Selector {
229    type Error = UnknownSelector;
230
231    fn try_from(token: &Token) -> Result<Self, Self::Error> {
232        if let TokenKind::Selector(s) = &token.kind {
233            match s.as_str() {
234                // Heading selectors
235                ".h" | ".heading" => Ok(Selector::Heading(None)),
236                ".h1" => Ok(Selector::Heading(Some(1))),
237                ".h2" => Ok(Selector::Heading(Some(2))),
238                ".h3" => Ok(Selector::Heading(Some(3))),
239                ".h4" => Ok(Selector::Heading(Some(4))),
240                ".h5" => Ok(Selector::Heading(Some(5))),
241                ".h6" => Ok(Selector::Heading(Some(6))),
242
243                // Blockquote
244                ".>" | ".blockquote" => Ok(Selector::Blockquote),
245
246                // Footnote
247                ".^" | ".footnote" => Ok(Selector::Footnote),
248
249                // MDX JSX Flow Element
250                ".<" | ".mdx_jsx_flow_element" => Ok(Selector::MdxJsxFlowElement),
251
252                // Emphasis
253                ".**" | ".emphasis" => Ok(Selector::Emphasis),
254
255                // Math
256                ".$$" | ".math" => Ok(Selector::Math),
257
258                // Horizontal Rule
259                ".horizontal_rule" | ".---" | ".***" | ".___" => Ok(Selector::HorizontalRule),
260
261                // MDX Text Expression
262                ".{}" | ".mdx_text_expression" => Ok(Selector::MdxTextExpression),
263
264                // Footnote Reference
265                ".[^]" | ".footnote_ref" => Ok(Selector::FootnoteRef),
266
267                // Definition
268                ".definition" => Ok(Selector::Definition),
269
270                // Break
271                ".break" => Ok(Selector::Break),
272
273                // Delete
274                ".delete" => Ok(Selector::Delete),
275
276                // HTML
277                ".<>" | ".html" => Ok(Selector::Html),
278
279                // Image
280                ".image" => Ok(Selector::Image),
281
282                // Image Reference
283                ".image_ref" => Ok(Selector::ImageRef),
284
285                // Inline Code
286                ".code_inline" => Ok(Selector::InlineCode),
287
288                // Inline Math
289                ".math_inline" => Ok(Selector::InlineMath),
290
291                // Link
292                ".link" => Ok(Selector::Link),
293
294                // Link Reference
295                ".link_ref" => Ok(Selector::LinkRef),
296
297                // List
298                ".[]" | ".list" => Ok(Selector::List(None, None)),
299
300                // Task List
301                ".task" => Ok(Selector::Task),
302
303                // Todo List
304                ".todo" => Ok(Selector::Todo),
305
306                // Done List
307                ".done" => Ok(Selector::Done),
308
309                // TOML
310                ".toml" => Ok(Selector::Toml),
311
312                // Strong
313                ".strong" => Ok(Selector::Strong),
314
315                // YAML
316                ".yaml" => Ok(Selector::Yaml),
317
318                // Code
319                ".code" => Ok(Selector::Code),
320
321                // MDX JS ESM
322                ".mdx_js_esm" => Ok(Selector::MdxJsEsm),
323
324                // MDX JSX Text Element
325                ".mdx_jsx_text_element" => Ok(Selector::MdxJsxTextElement),
326
327                // MDX Flow Expression
328                ".mdx_flow_expression" => Ok(Selector::MdxFlowExpression),
329
330                // Text
331                ".text" => Ok(Selector::Text),
332
333                // Table
334                ".[][]" | ".table" => Ok(Selector::Table(None, None)),
335
336                // Table Align
337                ".table_align" => Ok(Selector::TableAlign),
338
339                // Recursive
340                ".." => Ok(Selector::Recursive),
341
342                // Attribute selectors - Common
343                ".value" => Ok(Selector::Attr(AttrKind::Value)),
344                ".values" => Ok(Selector::Attr(AttrKind::Values)),
345                ".children" | ".cn" => Ok(Selector::Attr(AttrKind::Children)),
346
347                // Attribute selectors - Code
348                ".lang" => Ok(Selector::Attr(AttrKind::Lang)),
349                ".meta" => Ok(Selector::Attr(AttrKind::Meta)),
350                ".fence" => Ok(Selector::Attr(AttrKind::Fence)),
351
352                // Attribute selectors - Link/Image
353                ".url" => Ok(Selector::Attr(AttrKind::Url)),
354                ".alt" => Ok(Selector::Attr(AttrKind::Alt)),
355                ".title" => Ok(Selector::Attr(AttrKind::Title)),
356
357                // Attribute selectors - Reference
358                ".ident" => Ok(Selector::Attr(AttrKind::Ident)),
359                ".label" => Ok(Selector::Attr(AttrKind::Label)),
360
361                // Attribute selectors - Heading
362                ".depth" => Ok(Selector::Attr(AttrKind::Depth)),
363                ".level" => Ok(Selector::Attr(AttrKind::Level)),
364
365                // Attribute selectors - List
366                ".index" => Ok(Selector::Attr(AttrKind::Index)),
367                ".ordered" => Ok(Selector::Attr(AttrKind::Ordered)),
368                ".checked" => Ok(Selector::Attr(AttrKind::Checked)),
369
370                // Attribute selectors - TableCell
371                ".column" => Ok(Selector::Attr(AttrKind::Column)),
372                ".row" => Ok(Selector::Attr(AttrKind::Row)),
373
374                // Attribute selectors - TableHeader
375                ".align" => Ok(Selector::Attr(AttrKind::Align)),
376
377                // Attribute selectors - MDX
378                ".name" => Ok(Selector::Attr(AttrKind::Name)),
379
380                _ => parse_bracket_selector(s).ok_or_else(|| UnknownSelector(token.clone())),
381            }
382        } else {
383            Err(UnknownSelector(token.clone()))
384        }
385    }
386}
387
388impl Display for Selector {
389    fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), fmt::Error> {
390        match self {
391            Selector::Heading(None) => write!(f, ".h"),
392            Selector::Heading(Some(1)) => write!(f, ".h1"),
393            Selector::Heading(Some(2)) => write!(f, ".h2"),
394            Selector::Heading(Some(3)) => write!(f, ".h3"),
395            Selector::Heading(Some(4)) => write!(f, ".h4"),
396            Selector::Heading(Some(5)) => write!(f, ".h5"),
397            Selector::Heading(Some(6)) => write!(f, ".h6"),
398            Selector::Heading(Some(n)) => write!(f, ".h{}", n),
399            Selector::Blockquote => write!(f, ".blockquote"),
400            Selector::Footnote => write!(f, ".footnote"),
401            Selector::List(None, None) => write!(f, ".list"),
402            Selector::List(Some(idx), None) => write!(f, ".[{}]", idx),
403            Selector::List(Some(idx), _) => write!(f, ".[{}]", idx),
404            Selector::List(None, _) => write!(f, ".[]"),
405            Selector::Toml => write!(f, ".toml"),
406            Selector::Yaml => write!(f, ".yaml"),
407            Selector::Break => write!(f, ".break"),
408            Selector::InlineCode => write!(f, ".code_inline"),
409            Selector::InlineMath => write!(f, ".math_inline"),
410            Selector::Delete => write!(f, ".delete"),
411            Selector::Emphasis => write!(f, ".emphasis"),
412            Selector::FootnoteRef => write!(f, ".footnote_ref"),
413            Selector::Html => write!(f, ".html"),
414            Selector::Image => write!(f, ".image"),
415            Selector::ImageRef => write!(f, ".image_ref"),
416            Selector::MdxJsxTextElement => write!(f, ".mdx_jsx_text_element"),
417            Selector::Link => write!(f, ".link"),
418            Selector::LinkRef => write!(f, ".link_ref"),
419            Selector::Strong => write!(f, ".strong"),
420            Selector::Code => write!(f, ".code"),
421            Selector::Math => write!(f, ".math"),
422            Selector::Table(None, None) => write!(f, ".table"),
423            Selector::Table(Some(row), None) => write!(f, ".[{}][]", row),
424            Selector::Table(Some(row), Some(col)) => write!(f, ".[{}][{}]", row, col),
425            Selector::Table(None, Some(col)) => write!(f, ".[][{}]", col),
426            Selector::TableAlign => write!(f, ".table_align"),
427            Selector::Text => write!(f, ".text"),
428            Selector::HorizontalRule => write!(f, ".horizontal_rule"),
429            Selector::Definition => write!(f, ".definition"),
430            Selector::MdxFlowExpression => write!(f, ".mdx_flow_expression"),
431            Selector::MdxTextExpression => write!(f, ".mdx_text_expression"),
432            Selector::MdxJsEsm => write!(f, ".mdx_js_esm"),
433            Selector::MdxJsxFlowElement => write!(f, ".mdx_jsx_flow_element"),
434            Selector::Recursive => write!(f, ".."),
435            Selector::Task => write!(f, ".task"),
436            Selector::Todo => write!(f, ".todo"),
437            Selector::Done => write!(f, ".done"),
438            Selector::Attr(attr) => write!(f, "{}", attr),
439        }
440    }
441}
442
443impl Selector {
444    /// Returns `true` if this is an attribute selector.
445    pub fn is_attribute_selector(&self) -> bool {
446        matches!(self, Selector::Attr(_))
447    }
448}
449
450#[cfg(test)]
451mod tests {
452    use crate::{
453        ArenaId, Position, Range, Token, TokenKind,
454        selector::{AttrKind, Selector, UnknownSelector},
455    };
456    use rstest::rstest;
457    use smol_str::SmolStr;
458
459    #[rstest]
460    // Heading selectors
461    #[case::heading(".h", Selector::Heading(None), ".h")]
462    #[case::heading_h1(".h1", Selector::Heading(Some(1)), ".h1")]
463    #[case::heading_h2(".h2", Selector::Heading(Some(2)), ".h2")]
464    #[case::heading_h3(".h3", Selector::Heading(Some(3)), ".h3")]
465    #[case::heading_h4(".h4", Selector::Heading(Some(4)), ".h4")]
466    #[case::heading_h5(".h5", Selector::Heading(Some(5)), ".h5")]
467    #[case::heading_h6(".h6", Selector::Heading(Some(6)), ".h6")]
468    // Blockquote
469    #[case::blockquote(".blockquote", Selector::Blockquote, ".blockquote")]
470    #[case::blockquote_alias(".>", Selector::Blockquote, ".blockquote")]
471    // Footnote
472    #[case::footnote(".footnote", Selector::Footnote, ".footnote")]
473    #[case::footnote_alias(".^", Selector::Footnote, ".footnote")]
474    // MDX JSX Flow Element
475    #[case::mdx_jsx_flow_element(".mdx_jsx_flow_element", Selector::MdxJsxFlowElement, ".mdx_jsx_flow_element")]
476    #[case::mdx_jsx_flow_element_alias(".<", Selector::MdxJsxFlowElement, ".mdx_jsx_flow_element")]
477    // Emphasis
478    #[case::emphasis(".emphasis", Selector::Emphasis, ".emphasis")]
479    #[case::emphasis_alias(".**", Selector::Emphasis, ".emphasis")]
480    // Math
481    #[case::math(".math", Selector::Math, ".math")]
482    #[case::math_alias(".$$", Selector::Math, ".math")]
483    // Horizontal Rule
484    #[case::horizontal_rule(".horizontal_rule", Selector::HorizontalRule, ".horizontal_rule")]
485    #[case::horizontal_rule_alias_dash(".---", Selector::HorizontalRule, ".horizontal_rule")]
486    #[case::horizontal_rule_alias_star(".***", Selector::HorizontalRule, ".horizontal_rule")]
487    #[case::horizontal_rule_alias_underscore(".___", Selector::HorizontalRule, ".horizontal_rule")]
488    // MDX Text Expression
489    #[case::mdx_text_expression(".mdx_text_expression", Selector::MdxTextExpression, ".mdx_text_expression")]
490    #[case::mdx_text_expression_alias(".{}", Selector::MdxTextExpression, ".mdx_text_expression")]
491    // Footnote Reference
492    #[case::footnote_ref(".footnote_ref", Selector::FootnoteRef, ".footnote_ref")]
493    #[case::footnote_ref_alias(".[^]", Selector::FootnoteRef, ".footnote_ref")]
494    // Definition
495    #[case::definition(".definition", Selector::Definition, ".definition")]
496    // Break
497    #[case::break_selector(".break", Selector::Break, ".break")]
498    // Delete
499    #[case::delete(".delete", Selector::Delete, ".delete")]
500    // HTML
501    #[case::html(".html", Selector::Html, ".html")]
502    #[case::html_alias(".<>", Selector::Html, ".html")]
503    // Image
504    #[case::image(".image", Selector::Image, ".image")]
505    // Image Reference
506    #[case::image_ref(".image_ref", Selector::ImageRef, ".image_ref")]
507    // Inline Code
508    #[case::code_inline(".code_inline", Selector::InlineCode, ".code_inline")]
509    // Inline Math
510    #[case::math_inline(".math_inline", Selector::InlineMath, ".math_inline")]
511    // Link
512    #[case::link(".link", Selector::Link, ".link")]
513    // Link Reference
514    #[case::link_ref(".link_ref", Selector::LinkRef, ".link_ref")]
515    // List
516    #[case::list(".list", Selector::List(None, None), ".list")]
517    #[case::list_bracket(".[]", Selector::List(None, None), ".list")]
518    #[case::list_with_index(".[1]", Selector::List(Some(1), None), ".[1]")]
519    // Task List
520    #[case::task(".task", Selector::Task, ".task")]
521    #[case::task(".todo", Selector::Todo, ".todo")]
522    #[case::task(".done", Selector::Done, ".done")]
523    // TOML
524    #[case::toml(".toml", Selector::Toml, ".toml")]
525    // Strong
526    #[case::strong(".strong", Selector::Strong, ".strong")]
527    // YAML
528    #[case::yaml(".yaml", Selector::Yaml, ".yaml")]
529    // Code
530    #[case::code(".code", Selector::Code, ".code")]
531    // MDX JS ESM
532    #[case::mdx_js_esm(".mdx_js_esm", Selector::MdxJsEsm, ".mdx_js_esm")]
533    // MDX JSX Text Element
534    #[case::mdx_jsx_text_element(".mdx_jsx_text_element", Selector::MdxJsxTextElement, ".mdx_jsx_text_element")]
535    // MDX Flow Expression
536    #[case::mdx_flow_expression(".mdx_flow_expression", Selector::MdxFlowExpression, ".mdx_flow_expression")]
537    // Text
538    #[case::text(".text", Selector::Text, ".text")]
539    // Table
540    #[case::table(".table", Selector::Table(None, None), ".table")]
541    #[case::table_bracket(".[][]", Selector::Table(None, None), ".table")]
542    #[case::table_row_any(".[1][]", Selector::Table(Some(1), None), ".[1][]")]
543    #[case::table_row_col(".[1][2]", Selector::Table(Some(1), Some(2)), ".[1][2]")]
544    #[case::table_any_col(".[][2]", Selector::Table(None, Some(2)), ".[][2]")]
545    // Table Align
546    #[case::table_align(".table_align", Selector::TableAlign, ".table_align")]
547    // Recursive
548    #[case::recursive("..", Selector::Recursive, "..")]
549    // Attribute selectors - Common
550    #[case::attr_value(".value", Selector::Attr(AttrKind::Value), ".value")]
551    #[case::attr_values(".values", Selector::Attr(AttrKind::Values), ".values")]
552    #[case::attr_children(".children", Selector::Attr(AttrKind::Children), ".children")]
553    // Attribute selectors - Code
554    #[case::attr_lang(".lang", Selector::Attr(AttrKind::Lang), ".lang")]
555    #[case::attr_meta(".meta", Selector::Attr(AttrKind::Meta), ".meta")]
556    #[case::attr_fence(".fence", Selector::Attr(AttrKind::Fence), ".fence")]
557    // Attribute selectors - Link/Image
558    #[case::attr_url(".url", Selector::Attr(AttrKind::Url), ".url")]
559    #[case::attr_alt(".alt", Selector::Attr(AttrKind::Alt), ".alt")]
560    #[case::attr_title(".title", Selector::Attr(AttrKind::Title), ".title")]
561    // Attribute selectors - Reference
562    #[case::attr_ident(".ident", Selector::Attr(AttrKind::Ident), ".ident")]
563    #[case::attr_label(".label", Selector::Attr(AttrKind::Label), ".label")]
564    // Attribute selectors - Heading
565    #[case::attr_depth(".depth", Selector::Attr(AttrKind::Depth), ".depth")]
566    #[case::attr_level(".level", Selector::Attr(AttrKind::Level), ".level")]
567    // Attribute selectors - List
568    #[case::attr_index(".index", Selector::Attr(AttrKind::Index), ".index")]
569    #[case::attr_ordered(".ordered", Selector::Attr(AttrKind::Ordered), ".ordered")]
570    #[case::attr_checked(".checked", Selector::Attr(AttrKind::Checked), ".checked")]
571    // Attribute selectors - TableCell
572    #[case::attr_column(".column", Selector::Attr(AttrKind::Column), ".column")]
573    #[case::attr_row(".row", Selector::Attr(AttrKind::Row), ".row")]
574    // Attribute selectors - TableHeader
575    #[case::attr_align(".align", Selector::Attr(AttrKind::Align), ".align")]
576    // Attribute selectors - MDX
577    #[case::attr_name(".name", Selector::Attr(AttrKind::Name), ".name")]
578    fn test_selector_try_from_and_display(
579        #[case] input: &str,
580        #[case] expected_selector: Selector,
581        #[case] expected_display: &str,
582    ) {
583        // Test TryFrom
584        let selector = Selector::try_from(&Token {
585            kind: TokenKind::Selector(SmolStr::new(input)),
586            range: Range {
587                start: Position::new(0, 0),
588                end: Position::new(0, 0),
589            },
590            module_id: ArenaId::new(0),
591        })
592        .expect("Should parse valid selector");
593        assert_eq!(selector, expected_selector);
594
595        // Test Display
596        assert_eq!(selector.to_string(), expected_display);
597    }
598
599    #[test]
600    fn test_selector_try_from_unknown() {
601        let token = Token {
602            kind: TokenKind::Selector(SmolStr::new(".unknown")),
603            range: Range {
604                start: Position::new(0, 0),
605                end: Position::new(0, 0),
606            },
607            module_id: ArenaId::new(0),
608        };
609        let result = Selector::try_from(&token);
610        assert!(result.is_err());
611        if let Err(e) = result {
612            assert_eq!(e, UnknownSelector(token));
613        }
614    }
615}