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#[derive(Error, Clone, Debug, PartialOrd, Eq, Ord, PartialEq)]
11#[error("Unknown selector `{0}`")]
12pub struct UnknownSelector(pub Token);
13
14fn 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 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 Some(Selector::Table(first_idx, second_idx))
50}
51
52impl UnknownSelector {
53 pub fn new(token: Token) -> Self {
55 Self(token)
56 }
57}
58
59fn 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
75fn 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#[cfg_attr(feature = "ast-json", derive(Serialize, Deserialize))]
93#[derive(PartialEq, PartialOrd, Debug, Eq, Clone)]
94pub enum Selector {
95 Blockquote,
97 Footnote,
99 List(Option<usize>, Option<bool>),
103 Toml,
105 Yaml,
107 Break,
109 InlineCode,
111 InlineMath,
113 Delete,
115 Emphasis,
117 FootnoteRef,
119 Html,
121 Image,
123 ImageRef,
125 MdxJsxTextElement,
127 Link,
129 LinkRef,
131 Strong,
133 Code,
135 Math,
137 Heading(Option<u8>),
141 Table(Option<usize>, Option<usize>),
145 TableAlign,
147 Text,
149 HorizontalRule,
151 Definition,
153 MdxFlowExpression,
155 MdxTextExpression,
157 MdxJsEsm,
159 MdxJsxFlowElement,
161 Recursive,
163 Task,
165 Todo,
167 Done,
169 Attr(AttrKind),
171 Property(Ident),
173}
174
175#[cfg_attr(feature = "ast-json", derive(Serialize, Deserialize))]
180#[derive(PartialEq, PartialOrd, Debug, Eq, Clone)]
181pub enum AttrKind {
182 Value,
184 Values,
186 Children,
188
189 Lang,
191 Meta,
193 Fence,
195
196 Url,
198 Alt,
200 Title,
202
203 Ident,
205 Label,
207
208 Depth,
210 Level,
212
213 Index,
215 Ordered,
217 Checked,
219
220 Column,
222 Row,
224
225 Align,
227
228 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 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 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 pub fn is_attribute_selector(&self) -> bool {
411 matches!(self, Selector::Attr(_))
412 }
413
414 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 #[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 #[case::blockquote(".blockquote", Selector::Blockquote, ".blockquote")]
440 #[case::blockquote_alias(".>", Selector::Blockquote, ".blockquote")]
441 #[case::footnote(".footnote", Selector::Footnote, ".footnote")]
443 #[case::footnote_alias(".^", Selector::Footnote, ".footnote")]
444 #[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 #[case::emphasis(".emphasis", Selector::Emphasis, ".emphasis")]
449 #[case::emphasis_alias(".**", Selector::Emphasis, ".emphasis")]
450 #[case::math(".math", Selector::Math, ".math")]
452 #[case::math_alias(".$$", Selector::Math, ".math")]
453 #[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 #[case::mdx_text_expression(".mdx_text_expression", Selector::MdxTextExpression, ".mdx_text_expression")]
461 #[case::mdx_text_expression_alias(".{}", Selector::MdxTextExpression, ".mdx_text_expression")]
462 #[case::footnote_ref(".footnote_ref", Selector::FootnoteRef, ".footnote_ref")]
464 #[case::footnote_ref_alias(".[^]", Selector::FootnoteRef, ".footnote_ref")]
465 #[case::definition(".definition", Selector::Definition, ".definition")]
467 #[case::break_selector(".break", Selector::Break, ".break")]
469 #[case::break_alias_br(".br", Selector::Break, ".break")]
470 #[case::delete(".delete", Selector::Delete, ".delete")]
472 #[case::html(".html", Selector::Html, ".html")]
474 #[case::html_alias(".<>", Selector::Html, ".html")]
475 #[case::image(".image", Selector::Image, ".image")]
477 #[case::image_ref(".image_ref", Selector::ImageRef, ".image_ref")]
479 #[case::code_inline(".code_inline", Selector::InlineCode, ".code_inline")]
481 #[case::inline_code_alias(".inline_code", Selector::InlineCode, ".code_inline")]
482 #[case::math_inline(".math_inline", Selector::InlineMath, ".math_inline")]
484 #[case::inline_math_alias(".inline_math", Selector::InlineMath, ".math_inline")]
485 #[case::link(".link", Selector::Link, ".link")]
487 #[case::link_ref(".link_ref", Selector::LinkRef, ".link_ref")]
489 #[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 #[case::task(".task", Selector::Task, ".task")]
496 #[case::task(".todo", Selector::Todo, ".todo")]
497 #[case::task(".done", Selector::Done, ".done")]
498 #[case::toml(".toml", Selector::Toml, ".toml")]
500 #[case::strong(".strong", Selector::Strong, ".strong")]
502 #[case::yaml(".yaml", Selector::Yaml, ".yaml")]
504 #[case::code(".code", Selector::Code, ".code")]
506 #[case::code_block_alias(".code_block", Selector::Code, ".code")]
507 #[case::mdx_js_esm(".mdx_js_esm", Selector::MdxJsEsm, ".mdx_js_esm")]
509 #[case::mdx_jsx_text_element(".mdx_jsx_text_element", Selector::MdxJsxTextElement, ".mdx_jsx_text_element")]
511 #[case::mdx_flow_expression(".mdx_flow_expression", Selector::MdxFlowExpression, ".mdx_flow_expression")]
513 #[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 #[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 #[case::table_align(".table_align", Selector::TableAlign, ".table_align")]
525 #[case::recursive("..", Selector::Recursive, "..")]
527 #[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 #[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 #[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 #[case::attr_ident(".ident", Selector::Attr(AttrKind::Ident), ".ident")]
541 #[case::attr_label(".label", Selector::Attr(AttrKind::Label), ".label")]
542 #[case::attr_depth(".depth", Selector::Attr(AttrKind::Depth), ".depth")]
544 #[case::attr_level(".level", Selector::Attr(AttrKind::Level), ".level")]
545 #[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 #[case::attr_column(".column", Selector::Attr(AttrKind::Column), ".column")]
551 #[case::attr_row(".row", Selector::Attr(AttrKind::Row), ".row")]
552 #[case::attr_align(".align", Selector::Attr(AttrKind::Align), ".align")]
554 #[case::attr_name(".name", Selector::Attr(AttrKind::Name), ".name")]
556 #[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 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 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}