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