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::{Ident, 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/// Unescapes `\"` and `\\` sequences in a quoted property key.
60fn unescape_property_key(s: &str) -> String {
61    let mut result = String::with_capacity(s.len());
62    let mut chars = s.chars();
63    while let Some(c) = chars.next() {
64        if c == '\\' {
65            if let Some(next) = chars.next() {
66                result.push(next);
67            }
68        } else {
69            result.push(c);
70        }
71    }
72    result
73}
74
75/// Escapes `"` and `\` characters in a property key for display as `."key"`.
76fn escape_property_key(key: &str) -> String {
77    let mut result = String::with_capacity(key.len() + 2);
78    for c in key.chars() {
79        match c {
80            '"' => result.push_str("\\\""),
81            '\\' => result.push_str("\\\\"),
82            _ => result.push(c),
83        }
84    }
85    result
86}
87
88/// A selector for matching specific types of markdown nodes.
89///
90/// Selectors are used to query and filter markdown documents, similar to CSS selectors
91/// for HTML. Each variant matches a specific type of markdown element.
92#[cfg_attr(feature = "ast-json", derive(Serialize, Deserialize))]
93#[derive(PartialEq, PartialOrd, Debug, Eq, Clone)]
94pub enum Selector {
95    /// Matches blockquote elements (e.g., `> quoted text`).
96    Blockquote,
97    /// Matches footnote definitions.
98    Footnote,
99    /// Matches list elements.
100    ///
101    /// The first `Option<usize>` specifies an item index, the second `Option<bool>` indicates ordered/unordered.
102    List(Option<usize>, Option<bool>),
103    /// Matches TOML frontmatter blocks.
104    Toml,
105    /// Matches YAML frontmatter blocks.
106    Yaml,
107    /// Matches line break elements.
108    Break,
109    /// Matches inline code elements (e.g., `` `code` ``).
110    InlineCode,
111    /// Matches inline math elements (e.g., `$math$`).
112    InlineMath,
113    /// Matches strikethrough/delete elements (e.g., `~~text~~`).
114    Delete,
115    /// Matches emphasis elements (e.g., `*text*` or `_text_`).
116    Emphasis,
117    /// Matches footnote references (e.g., `[^1]`).
118    FootnoteRef,
119    /// Matches raw HTML elements.
120    Html,
121    /// Matches image elements (e.g., `![alt](url)`).
122    Image,
123    /// Matches image reference elements (e.g., `![alt][ref]`).
124    ImageRef,
125    /// Matches MDX JSX text elements.
126    MdxJsxTextElement,
127    /// Matches link elements (e.g., `[text](url)`).
128    Link,
129    /// Matches link reference elements (e.g., `[text][ref]`).
130    LinkRef,
131    /// Matches strong/bold elements (e.g., `**text**`).
132    Strong,
133    /// Matches code block elements.
134    Code,
135    /// Matches math block elements (e.g., `$$math$$`).
136    Math,
137    /// Matches heading elements.
138    ///
139    /// The `Option<u8>` specifies the heading level (1-6). If `None`, matches any heading level.
140    Heading(Option<u8>),
141    /// Matches table elements.
142    ///
143    /// The first `Option<usize>` specifies row index, the second specifies column index.
144    Table(Option<usize>, Option<usize>),
145    /// Matches table alignment elements.
146    TableAlign,
147    /// Matches text nodes.
148    Text,
149    /// Matches horizontal rule elements (e.g., `---`, `***`, `___`).
150    HorizontalRule,
151    /// Matches link/image definition elements.
152    Definition,
153    /// Matches MDX flow expression elements.
154    MdxFlowExpression,
155    /// Matches MDX text expression elements.
156    MdxTextExpression,
157    /// Matches MDX ES module import/export elements.
158    MdxJsEsm,
159    /// Matches MDX JSX flow elements.
160    MdxJsxFlowElement,
161    /// Matches recursively all child nodes.
162    Recursive,
163    /// Matches a task list markdown node.
164    Task,
165    /// Matches a task list markdown node with an unchecked status.
166    Todo,
167    /// Matches a task list markdown node with a checked status.
168    Done,
169    /// Matches a specific attribute of a markdown node.
170    Attr(AttrKind),
171    /// Matches a specific property of a dict or array.
172    Property(Ident),
173}
174
175/// Represents an attribute that can be accessed from markdown nodes.
176///
177/// Attributes allow extracting specific properties from markdown elements,
178/// such as the URL from a link or the language from a code block.
179#[cfg_attr(feature = "ast-json", derive(Serialize, Deserialize))]
180#[derive(PartialEq, PartialOrd, Debug, Eq, Clone)]
181pub enum AttrKind {
182    /// The text value or content of a node.
183    Value,
184    /// Collection of values (used for certain node types).
185    Values,
186    /// The children nodes of an element.
187    Children,
188
189    /// The programming language identifier for code blocks.
190    Lang,
191    /// Additional metadata for code blocks.
192    Meta,
193    /// The fence character used for code blocks (e.g., `` ` `` or `~`).
194    Fence,
195
196    /// The URL for links and images.
197    Url,
198    /// The alt text for images.
199    Alt,
200    /// The title attribute for links and images.
201    Title,
202
203    /// The identifier for references (LinkRef, ImageRef, FootnoteRef, Definition, Footnote).
204    Ident,
205    /// The label for references.
206    Label,
207
208    /// The depth level of a heading (1-6).
209    Depth,
210    /// Alias for `Depth` - the level of a heading.
211    Level,
212
213    /// The index of a list item within its parent list.
214    Index,
215    /// Whether a list is ordered (numbered) or unordered.
216    Ordered,
217    /// The checked status of a task list item.
218    Checked,
219
220    /// The column index of a table cell.
221    Column,
222    /// The row index of a table cell.
223    Row,
224
225    /// The alignment of a table header (left, right, center, none).
226    Align,
227
228    /// The name attribute for MDX JSX elements.
229    Name,
230}
231
232impl Display for AttrKind {
233    fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), fmt::Error> {
234        match self {
235            AttrKind::Value => write!(f, ".value"),
236            AttrKind::Values => write!(f, ".values"),
237            AttrKind::Children => write!(f, ".children"),
238            AttrKind::Lang => write!(f, ".lang"),
239            AttrKind::Meta => write!(f, ".meta"),
240            AttrKind::Fence => write!(f, ".fence"),
241            AttrKind::Url => write!(f, ".url"),
242            AttrKind::Alt => write!(f, ".alt"),
243            AttrKind::Title => write!(f, ".title"),
244            AttrKind::Ident => write!(f, ".ident"),
245            AttrKind::Label => write!(f, ".label"),
246            AttrKind::Depth => write!(f, ".depth"),
247            AttrKind::Level => write!(f, ".level"),
248            AttrKind::Index => write!(f, ".index"),
249            AttrKind::Ordered => write!(f, ".ordered"),
250            AttrKind::Checked => write!(f, ".checked"),
251            AttrKind::Column => write!(f, ".column"),
252            AttrKind::Row => write!(f, ".row"),
253            AttrKind::Align => write!(f, ".align"),
254            AttrKind::Name => write!(f, ".name"),
255        }
256    }
257}
258
259impl Selector {
260    /// Converts a dot-prefixed selector string (e.g. `".text"`, `".h"`) to a `Selector`.
261    ///
262    /// Returns `None` for unknown or non-simple selectors (bracket forms, quoted keys).
263    pub fn from_selector_str(s: &str) -> Option<Self> {
264        match s {
265            ".h" | ".heading" => Some(Selector::Heading(None)),
266            ".h1" => Some(Selector::Heading(Some(1))),
267            ".h2" => Some(Selector::Heading(Some(2))),
268            ".h3" => Some(Selector::Heading(Some(3))),
269            ".h4" => Some(Selector::Heading(Some(4))),
270            ".h5" => Some(Selector::Heading(Some(5))),
271            ".h6" => Some(Selector::Heading(Some(6))),
272            ".>" | ".blockquote" => Some(Selector::Blockquote),
273            ".^" | ".footnote" => Some(Selector::Footnote),
274            ".<" | ".mdx_jsx_flow_element" => Some(Selector::MdxJsxFlowElement),
275            ".**" | ".emphasis" => Some(Selector::Emphasis),
276            ".$$" | ".math" => Some(Selector::Math),
277            ".horizontal_rule" | ".hr" | ".---" | ".***" | ".___" => Some(Selector::HorizontalRule),
278            ".{}" | ".mdx_text_expression" => Some(Selector::MdxTextExpression),
279            ".[^]" | ".footnote_ref" => Some(Selector::FootnoteRef),
280            ".definition" => Some(Selector::Definition),
281            ".break" | ".br" => Some(Selector::Break),
282            ".delete" => Some(Selector::Delete),
283            ".<>" | ".html" => Some(Selector::Html),
284            ".image" => Some(Selector::Image),
285            ".image_ref" => Some(Selector::ImageRef),
286            ".code_inline" | ".inline_code" => Some(Selector::InlineCode),
287            ".math_inline" | ".inline_math" => Some(Selector::InlineMath),
288            ".link" => Some(Selector::Link),
289            ".link_ref" => Some(Selector::LinkRef),
290            ".[]" | ".list" | ".li" => Some(Selector::List(None, None)),
291            ".task" => Some(Selector::Task),
292            ".todo" => Some(Selector::Todo),
293            ".done" => Some(Selector::Done),
294            ".toml" => Some(Selector::Toml),
295            ".strong" => Some(Selector::Strong),
296            ".yaml" => Some(Selector::Yaml),
297            ".code" | ".code_block" => Some(Selector::Code),
298            ".mdx_js_esm" => Some(Selector::MdxJsEsm),
299            ".mdx_jsx_text_element" => Some(Selector::MdxJsxTextElement),
300            ".mdx_flow_expression" => Some(Selector::MdxFlowExpression),
301            ".text" | ".p" | ".paragraph" => Some(Selector::Text),
302            ".[][]" | ".table" => Some(Selector::Table(None, None)),
303            ".table_align" => Some(Selector::TableAlign),
304            ".." => Some(Selector::Recursive),
305            ".value" => Some(Selector::Attr(AttrKind::Value)),
306            ".values" => Some(Selector::Attr(AttrKind::Values)),
307            ".children" | ".cn" => Some(Selector::Attr(AttrKind::Children)),
308            ".lang" => Some(Selector::Attr(AttrKind::Lang)),
309            ".meta" => Some(Selector::Attr(AttrKind::Meta)),
310            ".fence" => Some(Selector::Attr(AttrKind::Fence)),
311            ".url" => Some(Selector::Attr(AttrKind::Url)),
312            ".alt" => Some(Selector::Attr(AttrKind::Alt)),
313            ".title" => Some(Selector::Attr(AttrKind::Title)),
314            ".ident" => Some(Selector::Attr(AttrKind::Ident)),
315            ".label" => Some(Selector::Attr(AttrKind::Label)),
316            ".depth" => Some(Selector::Attr(AttrKind::Depth)),
317            ".level" => Some(Selector::Attr(AttrKind::Level)),
318            ".index" => Some(Selector::Attr(AttrKind::Index)),
319            ".ordered" => Some(Selector::Attr(AttrKind::Ordered)),
320            ".checked" => Some(Selector::Attr(AttrKind::Checked)),
321            ".column" => Some(Selector::Attr(AttrKind::Column)),
322            ".row" => Some(Selector::Attr(AttrKind::Row)),
323            ".align" => Some(Selector::Attr(AttrKind::Align)),
324            ".name" => Some(Selector::Attr(AttrKind::Name)),
325            _ => None,
326        }
327    }
328}
329
330impl TryFrom<&Token> for Selector {
331    type Error = UnknownSelector;
332
333    fn try_from(token: &Token) -> Result<Self, Self::Error> {
334        if let TokenKind::Selector(s) = &token.kind {
335            if let Some(sel) = Self::from_selector_str(s.as_str()) {
336                return Ok(sel);
337            }
338            if let Some(sel) = parse_bracket_selector(s.as_str()) {
339                return Ok(sel);
340            }
341            // Quoted property selector: ."key" is the only way to access dict keys
342            if let Some(quoted) = s.strip_prefix(".\"").and_then(|r| r.strip_suffix('"')) {
343                return Ok(Selector::Property(Ident::new(&unescape_property_key(quoted))));
344            }
345            Err(UnknownSelector(token.clone()))
346        } else {
347            Err(UnknownSelector(token.clone()))
348        }
349    }
350}
351
352impl Display for Selector {
353    fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), fmt::Error> {
354        match self {
355            Selector::Heading(None) => write!(f, ".h"),
356            Selector::Heading(Some(1)) => write!(f, ".h1"),
357            Selector::Heading(Some(2)) => write!(f, ".h2"),
358            Selector::Heading(Some(3)) => write!(f, ".h3"),
359            Selector::Heading(Some(4)) => write!(f, ".h4"),
360            Selector::Heading(Some(5)) => write!(f, ".h5"),
361            Selector::Heading(Some(6)) => write!(f, ".h6"),
362            Selector::Heading(Some(n)) => write!(f, ".h{}", n),
363            Selector::Blockquote => write!(f, ".blockquote"),
364            Selector::Footnote => write!(f, ".footnote"),
365            Selector::List(None, None) => write!(f, ".list"),
366            Selector::List(Some(idx), None) => write!(f, ".[{}]", idx),
367            Selector::List(Some(idx), _) => write!(f, ".[{}]", idx),
368            Selector::List(None, _) => write!(f, ".[]"),
369            Selector::Toml => write!(f, ".toml"),
370            Selector::Yaml => write!(f, ".yaml"),
371            Selector::Break => write!(f, ".break"),
372            Selector::InlineCode => write!(f, ".code_inline"),
373            Selector::InlineMath => write!(f, ".math_inline"),
374            Selector::Delete => write!(f, ".delete"),
375            Selector::Emphasis => write!(f, ".emphasis"),
376            Selector::FootnoteRef => write!(f, ".footnote_ref"),
377            Selector::Html => write!(f, ".html"),
378            Selector::Image => write!(f, ".image"),
379            Selector::ImageRef => write!(f, ".image_ref"),
380            Selector::MdxJsxTextElement => write!(f, ".mdx_jsx_text_element"),
381            Selector::Link => write!(f, ".link"),
382            Selector::LinkRef => write!(f, ".link_ref"),
383            Selector::Strong => write!(f, ".strong"),
384            Selector::Code => write!(f, ".code"),
385            Selector::Math => write!(f, ".math"),
386            Selector::Table(None, None) => write!(f, ".table"),
387            Selector::Table(Some(row), None) => write!(f, ".[{}][]", row),
388            Selector::Table(Some(row), Some(col)) => write!(f, ".[{}][{}]", row, col),
389            Selector::Table(None, Some(col)) => write!(f, ".[][{}]", col),
390            Selector::TableAlign => write!(f, ".table_align"),
391            Selector::Text => write!(f, ".text"),
392            Selector::HorizontalRule => write!(f, ".horizontal_rule"),
393            Selector::Definition => write!(f, ".definition"),
394            Selector::MdxFlowExpression => write!(f, ".mdx_flow_expression"),
395            Selector::MdxTextExpression => write!(f, ".mdx_text_expression"),
396            Selector::MdxJsEsm => write!(f, ".mdx_js_esm"),
397            Selector::MdxJsxFlowElement => write!(f, ".mdx_jsx_flow_element"),
398            Selector::Recursive => write!(f, ".."),
399            Selector::Task => write!(f, ".task"),
400            Selector::Todo => write!(f, ".todo"),
401            Selector::Done => write!(f, ".done"),
402            Selector::Attr(attr) => write!(f, "{}", attr),
403            Selector::Property(property) => write!(f, ".\"{}\"", escape_property_key(&property.as_str())),
404        }
405    }
406}
407
408impl Selector {
409    /// Returns `true` if this is an attribute selector.
410    pub fn is_attribute_selector(&self) -> bool {
411        matches!(self, Selector::Attr(_))
412    }
413
414    /// Returns the selector as a string without a leading dot.
415    pub fn name(&self) -> String {
416        self.to_string().trim_start_matches('.').to_string()
417    }
418}
419
420#[cfg(test)]
421mod tests {
422    use crate::{
423        ArenaId, Position, Range, Token, TokenKind,
424        selector::{AttrKind, Selector, UnknownSelector},
425    };
426    use rstest::rstest;
427    use smol_str::SmolStr;
428
429    #[rstest]
430    // Heading selectors
431    #[case::heading(".h", Selector::Heading(None), ".h")]
432    #[case::heading_h1(".h1", Selector::Heading(Some(1)), ".h1")]
433    #[case::heading_h2(".h2", Selector::Heading(Some(2)), ".h2")]
434    #[case::heading_h3(".h3", Selector::Heading(Some(3)), ".h3")]
435    #[case::heading_h4(".h4", Selector::Heading(Some(4)), ".h4")]
436    #[case::heading_h5(".h5", Selector::Heading(Some(5)), ".h5")]
437    #[case::heading_h6(".h6", Selector::Heading(Some(6)), ".h6")]
438    // Blockquote
439    #[case::blockquote(".blockquote", Selector::Blockquote, ".blockquote")]
440    #[case::blockquote_alias(".>", Selector::Blockquote, ".blockquote")]
441    // Footnote
442    #[case::footnote(".footnote", Selector::Footnote, ".footnote")]
443    #[case::footnote_alias(".^", Selector::Footnote, ".footnote")]
444    // MDX JSX Flow Element
445    #[case::mdx_jsx_flow_element(".mdx_jsx_flow_element", Selector::MdxJsxFlowElement, ".mdx_jsx_flow_element")]
446    #[case::mdx_jsx_flow_element_alias(".<", Selector::MdxJsxFlowElement, ".mdx_jsx_flow_element")]
447    // Emphasis
448    #[case::emphasis(".emphasis", Selector::Emphasis, ".emphasis")]
449    #[case::emphasis_alias(".**", Selector::Emphasis, ".emphasis")]
450    // Math
451    #[case::math(".math", Selector::Math, ".math")]
452    #[case::math_alias(".$$", Selector::Math, ".math")]
453    // Horizontal Rule
454    #[case::horizontal_rule(".horizontal_rule", Selector::HorizontalRule, ".horizontal_rule")]
455    #[case::horizontal_rule_alias_hr(".hr", Selector::HorizontalRule, ".horizontal_rule")]
456    #[case::horizontal_rule_alias_dash(".---", Selector::HorizontalRule, ".horizontal_rule")]
457    #[case::horizontal_rule_alias_star(".***", Selector::HorizontalRule, ".horizontal_rule")]
458    #[case::horizontal_rule_alias_underscore(".___", Selector::HorizontalRule, ".horizontal_rule")]
459    // MDX Text Expression
460    #[case::mdx_text_expression(".mdx_text_expression", Selector::MdxTextExpression, ".mdx_text_expression")]
461    #[case::mdx_text_expression_alias(".{}", Selector::MdxTextExpression, ".mdx_text_expression")]
462    // Footnote Reference
463    #[case::footnote_ref(".footnote_ref", Selector::FootnoteRef, ".footnote_ref")]
464    #[case::footnote_ref_alias(".[^]", Selector::FootnoteRef, ".footnote_ref")]
465    // Definition
466    #[case::definition(".definition", Selector::Definition, ".definition")]
467    // Break
468    #[case::break_selector(".break", Selector::Break, ".break")]
469    #[case::break_alias_br(".br", Selector::Break, ".break")]
470    // Delete
471    #[case::delete(".delete", Selector::Delete, ".delete")]
472    // HTML
473    #[case::html(".html", Selector::Html, ".html")]
474    #[case::html_alias(".<>", Selector::Html, ".html")]
475    // Image
476    #[case::image(".image", Selector::Image, ".image")]
477    // Image Reference
478    #[case::image_ref(".image_ref", Selector::ImageRef, ".image_ref")]
479    // Inline Code
480    #[case::code_inline(".code_inline", Selector::InlineCode, ".code_inline")]
481    #[case::inline_code_alias(".inline_code", Selector::InlineCode, ".code_inline")]
482    // Inline Math
483    #[case::math_inline(".math_inline", Selector::InlineMath, ".math_inline")]
484    #[case::inline_math_alias(".inline_math", Selector::InlineMath, ".math_inline")]
485    // Link
486    #[case::link(".link", Selector::Link, ".link")]
487    // Link Reference
488    #[case::link_ref(".link_ref", Selector::LinkRef, ".link_ref")]
489    // List
490    #[case::list(".list", Selector::List(None, None), ".list")]
491    #[case::list_bracket(".[]", Selector::List(None, None), ".list")]
492    #[case::list_alias_li(".li", Selector::List(None, None), ".list")]
493    #[case::list_with_index(".[1]", Selector::List(Some(1), None), ".[1]")]
494    // Task List
495    #[case::task(".task", Selector::Task, ".task")]
496    #[case::task(".todo", Selector::Todo, ".todo")]
497    #[case::task(".done", Selector::Done, ".done")]
498    // TOML
499    #[case::toml(".toml", Selector::Toml, ".toml")]
500    // Strong
501    #[case::strong(".strong", Selector::Strong, ".strong")]
502    // YAML
503    #[case::yaml(".yaml", Selector::Yaml, ".yaml")]
504    // Code
505    #[case::code(".code", Selector::Code, ".code")]
506    #[case::code_block_alias(".code_block", Selector::Code, ".code")]
507    // MDX JS ESM
508    #[case::mdx_js_esm(".mdx_js_esm", Selector::MdxJsEsm, ".mdx_js_esm")]
509    // MDX JSX Text Element
510    #[case::mdx_jsx_text_element(".mdx_jsx_text_element", Selector::MdxJsxTextElement, ".mdx_jsx_text_element")]
511    // MDX Flow Expression
512    #[case::mdx_flow_expression(".mdx_flow_expression", Selector::MdxFlowExpression, ".mdx_flow_expression")]
513    // Text
514    #[case::text(".text", Selector::Text, ".text")]
515    #[case::text_alias_p(".p", Selector::Text, ".text")]
516    #[case::text_alias_paragraph(".paragraph", 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    // Property selectors: quoted form (."key") – the only way to access dict keys
557    #[case::property_quoted_h1(".\"h1\"", Selector::Property("h1".into()), ".\"h1\"")]
558    #[case::property_quoted_url(".\"url\"", Selector::Property("url".into()), ".\"url\"")]
559    #[case::property_quoted_with_space(".\"my key\"", Selector::Property("my key".into()), ".\"my key\"")]
560    #[case::property_quoted_escaped_quote(".\"my\\\"key\"", Selector::Property("my\"key".into()), ".\"my\\\"key\"")]
561    #[case::property_quoted_escaped_backslash(".\"my\\\\key\"", Selector::Property("my\\key".into()), ".\"my\\\\key\"")]
562    #[case::property_quoted_empty(".\"\"", Selector::Property("".into()), ".\"\"")]
563    fn test_selector_try_from_and_display(
564        #[case] input: &str,
565        #[case] expected_selector: Selector,
566        #[case] expected_display: &str,
567    ) {
568        // Test TryFrom
569        let selector = Selector::try_from(&Token {
570            kind: TokenKind::Selector(SmolStr::new(input)),
571            range: Range {
572                start: Position::new(0, 0),
573                end: Position::new(0, 0),
574            },
575            module_id: ArenaId::new(0),
576        })
577        .expect("Should parse valid selector");
578        assert_eq!(selector, expected_selector);
579
580        // Test Display
581        assert_eq!(selector.to_string(), expected_display);
582    }
583
584    #[rstest]
585    #[case(".")]
586    #[case(".mykey")]
587    #[case(".my_key")]
588    #[case(".unknown")]
589    #[case(".hedaing")]
590    fn test_selector_try_from_invalid(#[case] input: &str) {
591        let token = Token {
592            kind: TokenKind::Selector(SmolStr::new(input)),
593            range: Range {
594                start: Position::new(0, 0),
595                end: Position::new(0, 0),
596            },
597            module_id: ArenaId::new(0),
598        };
599        let result = Selector::try_from(&token);
600        assert!(result.is_err());
601        if let Err(e) = result {
602            assert_eq!(e, UnknownSelector(token));
603        }
604    }
605}