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 WikiLink,
133 Callout,
135 Embed,
137 Strong,
139 Code,
141 Math,
143 Heading(Option<u8>),
147 Table(Option<usize>, Option<usize>),
151 TableAlign,
153 Text,
155 HorizontalRule,
157 Definition,
159 MdxFlowExpression,
161 MdxTextExpression,
163 MdxJsEsm,
165 MdxJsxFlowElement,
167 Recursive,
169 Task,
171 Todo,
173 Done,
175 Attr(AttrKind),
177 Property(Ident),
179}
180
181#[cfg_attr(feature = "ast-json", derive(Serialize, Deserialize))]
186#[derive(PartialEq, PartialOrd, Debug, Eq, Clone)]
187pub enum AttrKind {
188 Value,
190 Values,
192 Children,
194
195 Lang,
197 Meta,
199 Fence,
201
202 Url,
204 Alt,
206 Title,
208
209 Ident,
211 Label,
213
214 Depth,
216 Level,
218
219 Index,
221 Ordered,
223 Checked,
225
226 Column,
228 Row,
230
231 Align,
233
234 Name,
236 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 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 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 pub fn is_attribute_selector(&self) -> bool {
427 matches!(self, Selector::Attr(_))
428 }
429
430 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 #[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 #[case::blockquote(".blockquote", Selector::Blockquote, ".blockquote")]
456 #[case::blockquote_alias(".>", Selector::Blockquote, ".blockquote")]
457 #[case::footnote(".footnote", Selector::Footnote, ".footnote")]
459 #[case::footnote_alias(".^", Selector::Footnote, ".footnote")]
460 #[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 #[case::emphasis(".emphasis", Selector::Emphasis, ".emphasis")]
465 #[case::emphasis_alias(".**", Selector::Emphasis, ".emphasis")]
466 #[case::math(".math", Selector::Math, ".math")]
468 #[case::math_alias(".$$", Selector::Math, ".math")]
469 #[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 #[case::mdx_text_expression(".mdx_text_expression", Selector::MdxTextExpression, ".mdx_text_expression")]
477 #[case::mdx_text_expression_alias(".{}", Selector::MdxTextExpression, ".mdx_text_expression")]
478 #[case::footnote_ref(".footnote_ref", Selector::FootnoteRef, ".footnote_ref")]
480 #[case::footnote_ref_alias(".[^]", Selector::FootnoteRef, ".footnote_ref")]
481 #[case::definition(".definition", Selector::Definition, ".definition")]
483 #[case::break_selector(".break", Selector::Break, ".break")]
485 #[case::break_alias_br(".br", Selector::Break, ".break")]
486 #[case::delete(".delete", Selector::Delete, ".delete")]
488 #[case::html(".html", Selector::Html, ".html")]
490 #[case::html_alias(".<>", Selector::Html, ".html")]
491 #[case::image(".image", Selector::Image, ".image")]
493 #[case::image_ref(".image_ref", Selector::ImageRef, ".image_ref")]
495 #[case::code_inline(".code_inline", Selector::InlineCode, ".code_inline")]
497 #[case::inline_code_alias(".inline_code", Selector::InlineCode, ".code_inline")]
498 #[case::math_inline(".math_inline", Selector::InlineMath, ".math_inline")]
500 #[case::inline_math_alias(".inline_math", Selector::InlineMath, ".math_inline")]
501 #[case::link(".link", Selector::Link, ".link")]
503 #[case::link_ref(".link_ref", Selector::LinkRef, ".link_ref")]
505 #[case::wikilink(".wikilink", Selector::WikiLink, ".wikilink")]
507 #[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 #[case::task(".task", Selector::Task, ".task")]
514 #[case::task(".todo", Selector::Todo, ".todo")]
515 #[case::task(".done", Selector::Done, ".done")]
516 #[case::toml(".toml", Selector::Toml, ".toml")]
518 #[case::strong(".strong", Selector::Strong, ".strong")]
520 #[case::yaml(".yaml", Selector::Yaml, ".yaml")]
522 #[case::code(".code", Selector::Code, ".code")]
524 #[case::code_block_alias(".code_block", Selector::Code, ".code")]
525 #[case::mdx_js_esm(".mdx_js_esm", Selector::MdxJsEsm, ".mdx_js_esm")]
527 #[case::mdx_jsx_text_element(".mdx_jsx_text_element", Selector::MdxJsxTextElement, ".mdx_jsx_text_element")]
529 #[case::mdx_flow_expression(".mdx_flow_expression", Selector::MdxFlowExpression, ".mdx_flow_expression")]
531 #[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 #[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 #[case::table_align(".table_align", Selector::TableAlign, ".table_align")]
543 #[case::recursive("..", Selector::Recursive, "..")]
545 #[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 #[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 #[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 #[case::attr_ident(".ident", Selector::Attr(AttrKind::Ident), ".ident")]
559 #[case::attr_label(".label", Selector::Attr(AttrKind::Label), ".label")]
560 #[case::attr_depth(".depth", Selector::Attr(AttrKind::Depth), ".depth")]
562 #[case::attr_level(".level", Selector::Attr(AttrKind::Level), ".level")]
563 #[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 #[case::attr_column(".column", Selector::Attr(AttrKind::Column), ".column")]
569 #[case::attr_row(".row", Selector::Attr(AttrKind::Row), ".row")]
570 #[case::attr_align(".align", Selector::Attr(AttrKind::Align), ".align")]
572 #[case::attr_name(".name", Selector::Attr(AttrKind::Name), ".name")]
574 #[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 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 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}