1use crate::doctree::ids::IdRegistry;
15use crate::doctree::{kinds, messages, AttrValue, Node, Span};
16
17use super::punctuation;
18
19const NULL: char = '\u{0}';
20
21pub fn escape2null(text: &str) -> String {
24 let mut out = String::with_capacity(text.len());
25 let mut chars = text.chars();
26 while let Some(c) = chars.next() {
27 if c == '\\' {
28 out.push(NULL);
29 if let Some(next) = chars.next() {
30 out.push(next);
31 }
32 } else {
33 out.push(c);
34 }
35 }
36 out
37}
38
39pub fn unescape(text: &str, restore_backslashes: bool) -> String {
43 if restore_backslashes {
44 return text.replace(NULL, "\\");
45 }
46 let mut out = String::with_capacity(text.len());
47 let mut chars = text.chars().peekable();
48 while let Some(c) = chars.next() {
49 if c == NULL {
50 match chars.peek() {
51 Some(' ') | Some('\n') => {
52 chars.next();
53 }
54 _ => {}
55 }
56 } else {
57 out.push(c);
58 }
59 }
60 out
61}
62
63pub struct InlineResult {
66 pub nodes: Vec<Node>,
67 pub messages: Vec<Node>,
68 pub roles: Vec<super::RoleRecord>,
70}
71
72fn collapse_whitespace(text: &str) -> String {
79 let mut out = String::with_capacity(text.len());
80 let mut in_ws = false;
81 for c in text.chars() {
82 if crate::utils::py_isspace(c) {
83 in_ws = true;
84 } else {
85 if in_ws {
86 out.push(' ');
87 in_ws = false;
88 }
89 out.push(c);
90 }
91 }
92 if in_ws {
93 out.push(' ');
94 }
95 out
96}
97
98fn target_collapses_whitespace(domain: &str, reftype: &str) -> bool {
122 !matches!(
123 (domain, reftype),
124 ("py" | "js" | "c" | "cpp", _) | ("std", "option" | "token")
125 )
126}
127
128fn is_start_prefix_ok(prev: Option<char>) -> bool {
129 match prev {
130 None => true,
131 Some(c) => {
132 crate::utils::py_isspace(c)
135 || punctuation::OPENERS.contains(&c)
136 || punctuation::DELIMITERS.contains(&c)
137 }
138 }
139}
140
141fn is_end_suffix_ok(next: Option<char>) -> bool {
142 match next {
143 None => true,
144 Some(c) => {
145 crate::utils::py_isspace(c)
146 || c == NULL
147 || punctuation::CLOSING_DELIMITERS.contains(&c)
148 || punctuation::DELIMITERS.contains(&c)
149 || punctuation::CLOSERS.contains(&c)
150 }
151 }
152}
153
154fn match_chars(c1: char, c2: char) -> bool {
157 match punctuation::OPENERS_RAW.iter().position(|c| *c == c1) {
158 None => false,
159 Some(i) => {
160 if punctuation::CLOSERS_RAW.get(i) == Some(&c2) {
161 return true;
162 }
163 punctuation::QUOTE_PAIRS
164 .iter()
165 .any(|(k, v)| *k == c1 && v.contains(c2))
166 }
167 }
168}
169
170fn is_word_char(c: char) -> bool {
171 c.is_alphanumeric() || c == '_'
172}
173
174fn match_simplename(chars: &[char], at: usize) -> Option<usize> {
178 let n = chars.len();
179 let mut i = at;
180 let atom = |c: char| is_word_char(c) && c != '_';
181 if i >= n || !atom(chars[i]) {
182 return None;
183 }
184 while i < n && atom(chars[i]) {
185 i += 1;
186 }
187 loop {
188 if i < n && matches!(chars[i], '-' | '.' | '_' | '+' | ':') {
189 let sep_end = i + 1;
190 if sep_end < n && atom(chars[sep_end]) {
191 i = sep_end + 1;
192 while i < n && atom(chars[i]) {
193 i += 1;
194 }
195 continue;
196 }
197 }
198 break;
199 }
200 Some(i - at)
201}
202
203fn is_urilast(c: char) -> bool {
204 matches!(c, '_' | '~' | '*' | '/' | '=' | '+') || c.is_ascii_alphanumeric()
205}
206
207fn is_uric(c: char) -> bool {
208 matches!(
209 c,
210 '-' | '_'
211 | '.'
212 | '!'
213 | '~'
214 | '*'
215 | '\''
216 | '('
217 | ')'
218 | '['
219 | ']'
220 | ';'
221 | '/'
222 | ':'
223 | '@'
224 | '&'
225 | '='
226 | '+'
227 | '$'
228 | ','
229 | '%'
230 | '\u{0}'
231 ) || c.is_ascii_alphanumeric()
232}
233
234fn is_emailc(c: char) -> bool {
235 matches!(
236 c,
237 '-' | '_'
238 | '!'
239 | '~'
240 | '*'
241 | '\''
242 | '{'
243 | '|'
244 | '}'
245 | '/'
246 | '#'
247 | '?'
248 | '^'
249 | '`'
250 | '&'
251 | '='
252 | '+'
253 | '$'
254 | '%'
255 | '\u{0}'
256 ) || c.is_ascii_alphanumeric()
257}
258
259#[derive(Clone, Copy, Default)]
268pub struct RefContext<'a> {
269 pub program: Option<&'a str>,
273 pub py_module: Option<&'a str>,
288 pub py_class: Option<&'a str>,
289 pub py_class_key: bool,
294 pub py_module_key: bool,
299 pub py_classes_key: bool,
304 pub py_modules_key: bool,
308}
309
310struct Inliner<'a> {
311 chars: Vec<char>,
313 failed_end_search: std::collections::HashMap<(char, usize), usize>,
318 span: Span,
319 lineno: u32,
320 source_path: &'a str,
321 registry: &'a mut IdRegistry,
322 sphinx: bool,
325 docname: &'a str,
326 ctx: RefContext<'a>,
328 py: &'a crate::py::PySigConfig,
331 roles: Vec<super::RoleRecord>,
332 nodes: Vec<Node>,
333 messages: Vec<Node>,
334 pending: String,
336}
337
338impl<'a> Inliner<'a> {
339 fn quoted_start(&self, start: usize, len: usize) -> bool {
343 let post = match self.chars.get(start + len) {
344 None => return true, Some(c) => *c,
346 };
347 if start == 0 {
348 return false;
349 }
350 let pre = self.chars[start - 1];
351 match_chars(pre, post)
352 }
353
354 fn start_ok(&self, at: usize, len: usize) -> bool {
355 let prev = at.checked_sub(1).map(|i| self.chars[i]);
356 if !is_start_prefix_ok(prev) {
357 return false;
358 }
359 match self.chars.get(at + len) {
361 Some(c) if crate::utils::py_isspace(*c) => false,
362 _ => !self.quoted_start(at, len),
363 }
364 }
365
366 fn find_end(
372 &mut self,
373 from: usize,
374 end_str: &[char],
375 allow_null_before: bool,
376 ) -> Option<usize> {
377 let key = (end_str[0], end_str.len());
378 if let Some(fail_from) = self.failed_end_search.get(&key) {
379 if from >= *fail_from {
380 return None;
381 }
382 }
383 let n = self.chars.len();
384 let len = end_str.len();
385 let mut i = from + 1;
387 while i + len <= n {
388 if self.chars[i..i + len] == *end_str {
389 let prev = self.chars[i - 1];
390 let ok_behind = if allow_null_before {
391 !crate::utils::py_isspace(prev)
392 } else {
393 !crate::utils::py_isspace(prev) && prev != NULL
394 };
395 let ok_ahead = is_end_suffix_ok(self.chars.get(i + len).copied());
396 if ok_behind && ok_ahead {
397 return Some(i);
398 }
399 }
400 i += 1;
401 }
402 self.failed_end_search.insert(key, from);
403 None
404 }
405
406 fn flush_text(&mut self) {
407 if !self.pending.is_empty() {
408 let pending = std::mem::take(&mut self.pending);
409 self.implicit_inline(&pending);
410 }
411 }
412
413 fn implicit_inline(&mut self, text: &str) {
416 if !text.contains(':') && !text.contains('@') {
420 let out = unescape(text, false);
421 if !out.is_empty() {
422 self.nodes.push(Node::text_node(out, self.span));
423 }
424 return;
425 }
426 let chars: Vec<char> = text.chars().collect();
427 let n = chars.len();
428 let mut emitted_upto = 0usize;
429 let mut i = 0usize;
430 while i < n {
431 let prev = i.checked_sub(1).map(|p| chars[p]);
432 let at_start = is_start_prefix_ok(prev);
433 if at_start {
434 if let Some((len, refuri, display)) = match_standalone_uri(&chars, i) {
435 let before: String = chars[emitted_upto..i].iter().collect();
436 let before = unescape(&before, false);
437 if !before.is_empty() {
438 self.nodes.push(Node::text_node(before, self.span));
439 }
440 let mut r = Node::elem(kinds::REFERENCE, self.span);
441 r.set("refuri", AttrValue::Str(unescape(&refuri, false)));
442 r.children
443 .push(Node::text_node(unescape(&display, false), self.span));
444 self.nodes.push(r);
445 i += len;
446 emitted_upto = i;
447 continue;
448 }
449 }
450 i += 1;
451 }
452 let rest: String = chars[emitted_upto..].iter().collect();
453 let rest = unescape(&rest, false);
454 if !rest.is_empty() {
455 self.nodes.push(Node::text_node(rest, self.span));
456 }
457 }
458
459 fn emit_inline(&mut self, kind: &'static str, content: &str, restore: bool) {
460 self.flush_text();
461 let mut node = Node::elem(kind, self.span);
462 node.children
463 .push(Node::text_node(unescape(content, restore), self.span));
464 self.nodes.push(node);
465 }
466
467 fn emit_problematic(&mut self, start_str: &str, construct: &str) {
470 self.flush_text();
471 let msg_id = self.registry.allocate_auto_id();
472 let prob_id = self.registry.allocate_auto_id();
473 let mut prob = Node::elem(kinds::PROBLEMATIC, self.span);
474 prob.attrs.ids.push(prob_id.clone());
475 prob.set("refid", AttrValue::Str(msg_id.clone()));
476 prob.children
477 .push(Node::text_node(start_str.to_string(), self.span));
478 self.nodes.push(prob);
479 let mut msg = messages::system_message(
480 messages::WARNING,
481 &format!("Inline {construct} start-string without end-string."),
482 self.lineno,
483 self.source_path,
484 );
485 msg.attrs.ids.push(msg_id);
486 msg.attrs.backrefs.push(prob_id);
487 self.messages.push(msg);
488 }
489
490 fn run(&mut self) {
491 let n = self.chars.len();
492 let mut i = 0usize;
493 while i < n {
494 let c = self.chars[i];
495 let simple: Option<(usize, &'static str, &[char], &str, bool)> =
497 if c == '*' && self.chars.get(i + 1) == Some(&'*') {
498 Some((2, kinds::STRONG, &['*', '*'], "strong", false))
499 } else if c == '*' {
500 Some((1, kinds::EMPHASIS, &['*'], "emphasis", false))
501 } else if c == '`' && self.chars.get(i + 1) == Some(&'`') {
502 Some((2, kinds::LITERAL, &['`', '`'], "literal", true))
503 } else {
504 None
505 };
506 if let Some((start_len, kind, end_str, construct, restore)) = simple {
507 if !self.start_ok(i, start_len) {
508 self.pending.push(c);
509 i += 1;
510 continue;
511 }
512 let content_from = i + start_len;
513 match self.find_end(content_from, end_str, restore) {
514 Some(end) => {
515 let content: String = self.chars[content_from..end].iter().collect();
516 self.emit_inline(kind, &content, restore);
517 i = end + end_str.len();
518 }
519 None => {
520 let start_str: String = self.chars[i..i + start_len].iter().collect();
521 self.emit_problematic(&start_str, construct);
522 i += start_len;
523 }
524 }
525 continue;
526 }
527 if c == '_' && self.chars.get(i + 1) == Some(&'`') && self.start_ok(i, 2) {
528 i = self.inline_internal_target(i);
529 continue;
530 }
531 if c == '`' && self.start_ok(i, 1) {
532 i = self.backtick_construct(i);
533 continue;
534 }
535 if c == '[' && self.start_ok(i, 1) {
536 if let Some(next_i) = self.try_footnote_or_citation(i) {
537 i = next_i;
538 continue;
539 }
540 self.pending.push(c);
541 i += 1;
542 continue;
543 }
544 if c == '|' && self.start_ok(i, 1) {
545 i = self.substitution_ref(i);
546 continue;
547 }
548 if is_word_char(c) && c != '_' {
549 let prev = i.checked_sub(1).map(|p| self.chars[p]);
550 if is_start_prefix_ok(prev) {
551 if let Some(next_i) = self.try_word_reference(i) {
552 i = next_i;
553 continue;
554 }
555 }
556 let mut j = i;
558 while j < n && is_word_char(self.chars[j]) {
559 self.pending.push(self.chars[j]);
560 j += 1;
561 }
562 i = j;
563 continue;
564 }
565 self.pending.push(c);
566 i += 1;
567 }
568 self.flush_text();
569 }
570
571 fn try_word_reference(&mut self, i: usize) -> Option<usize> {
573 let len = match_simplename(&self.chars, i)?;
574 let mut end = i + len;
575 let mut underscores = 0usize;
576 while underscores < 2 && self.chars.get(end) == Some(&'_') {
577 end += 1;
578 underscores += 1;
579 }
580 if underscores == 0 || !is_end_suffix_ok(self.chars.get(end).copied()) {
581 return None;
582 }
583 let name: String = self.chars[i..i + len].iter().collect();
586 if name.contains(NULL) {
587 return None;
588 }
589 self.flush_text();
590 let mut r = Node::elem(kinds::REFERENCE, self.span);
591 if underscores == 2 {
592 r.set("anonymous", AttrValue::Int(1));
593 r.set(
594 "name",
595 AttrValue::Str(crate::doctree::ids::whitespace_normalize_name(&name)),
596 );
597 } else {
598 r.set(
599 "name",
600 AttrValue::Str(crate::doctree::ids::whitespace_normalize_name(&name)),
601 );
602 r.set(
603 "refname",
604 AttrValue::Str(crate::doctree::ids::fully_normalize_name(&name)),
605 );
606 }
607 r.children.push(Node::text_node(name, self.span));
608 self.nodes.push(r);
609 Some(end)
610 }
611
612 fn find_end_with_underscores(
617 &self,
618 from: usize,
619 end_char: char,
620 max_underscores: usize,
621 ) -> Option<(usize, usize)> {
622 let n = self.chars.len();
623 let mut i = from + 1;
624 while i < n {
625 if self.chars[i] == end_char {
626 let prev = self.chars[i - 1];
627 if !crate::utils::py_isspace(prev) && prev != NULL {
628 let mut after = i + 1;
629 let mut u = 0usize;
630 while u < max_underscores && self.chars.get(after) == Some(&'_') {
631 after += 1;
632 u += 1;
633 }
634 if is_end_suffix_ok(self.chars.get(after).copied()) {
635 return Some((i, u));
636 }
637 }
638 }
639 i += 1;
640 }
641 None
642 }
643
644 fn inline_internal_target(&mut self, i: usize) -> usize {
646 let content_from = i + 2;
647 match self.find_end(content_from, &['`'], false) {
648 Some(end) => {
649 let raw: String = self.chars[content_from..end].iter().collect();
650 self.flush_text();
651 let mut t = Node::elem(kinds::TARGET, self.span);
652 t.attrs
653 .names
654 .push(crate::doctree::ids::fully_normalize_name(&unescape(
655 &raw, false,
656 )));
657 let msg = self.registry.set_id_explicit(
658 &mut t,
659 self.lineno,
660 self.source_path,
661 true,
662 None,
663 );
664 t.children
665 .push(Node::text_node(unescape(&raw, false), self.span));
666 self.nodes.push(t);
667 if let Some(m) = msg {
668 self.messages.push(m);
669 }
670 end + 1
671 }
672 None => {
673 self.emit_problematic("_`", "target");
678 i + 2
679 }
680 }
681 }
682
683 fn backtick_construct(&mut self, i: usize) -> usize {
686 let prefix_role = self.role_prefix_before(i);
688 let content_from = i + 1;
689 let (end, underscores) = match self.find_end_with_underscores(content_from, '`', 2) {
690 Some(r) => r,
691 None => {
692 self.emit_problematic("`", "interpreted text or phrase reference");
693 return i + 1;
694 }
695 };
696 let raw: String = self.chars[content_from..end].iter().collect();
697 if underscores > 0 && prefix_role.is_none() {
698 self.phrase_reference(&raw, underscores);
699 return end + 1 + underscores;
700 }
701 let suffix_role = if underscores == 0 {
703 self.role_suffix_after(end)
704 } else {
705 None
706 };
707 match (prefix_role, suffix_role) {
708 (Some((pstart, pname)), Some((send, sname))) => {
709 let _ = (pname, sname);
710 self.trim_pending(i - pstart);
712 let rawsource: String = self.chars[pstart..send].iter().collect();
713 self.role_problematic(
714 &rawsource,
715 None,
716 messages::WARNING,
717 "Multiple roles in interpreted text (both prefix and suffix present; only one allowed).",
718 );
719 send
720 }
721 (Some((pstart, name)), None) => {
722 self.trim_pending(i - pstart);
723 let rawsource: String = self.chars[pstart..end + 1].iter().collect();
724 self.apply_role(&name, &raw, &rawsource);
725 end + 1
726 }
727 (None, Some((send, name))) => {
728 let rawsource: String = self.chars[i..send].iter().collect();
729 self.apply_role(&name, &raw, &rawsource);
730 send
731 }
732 (None, None) => {
733 self.emit_inline(kinds::TITLE_REFERENCE, &raw, false);
734 end + 1
735 }
736 }
737 }
738
739 fn role_prefix_before(&self, i: usize) -> Option<(usize, String)> {
745 if i < 3 || self.chars[i - 1] != ':' {
746 return None;
747 }
748 let mut j = i - 1;
751 while j > 0 {
752 let c = self.chars[j - 1];
753 if is_word_char(c) || matches!(c, '-' | '.' | '+' | ':') {
754 j -= 1;
755 } else {
756 break;
757 }
758 }
759 let mut k = j;
761 while k + 1 < i - 1 {
762 if self.chars[k] == ':' {
763 let name: String = self.chars[k + 1..i - 1].iter().collect();
764 let name_chars: Vec<char> = name.chars().collect();
765 if !name_chars.is_empty()
766 && match_simplename(&name_chars, 0) == Some(name_chars.len())
767 {
768 let prev = k.checked_sub(1).map(|p| self.chars[p]);
769 if is_start_prefix_ok(prev) {
770 return Some((k, name));
771 }
772 }
773 }
774 k += 1;
775 }
776 None
777 }
778
779 fn role_suffix_after(&self, end: usize) -> Option<(usize, String)> {
782 let mut j = end + 1;
783 if self.chars.get(j) != Some(&':') {
784 return None;
785 }
786 j += 1;
787 let len = match_simplename(&self.chars, j)?;
788 j += len;
789 if self.chars.get(j) != Some(&':') {
790 return None;
791 }
792 j += 1;
793 if !is_end_suffix_ok(self.chars.get(j).copied()) {
794 return None;
795 }
796 let name: String = self.chars[end + 2..j - 1].iter().collect();
797 Some((j, name))
798 }
799
800 fn trim_pending(&mut self, n: usize) {
803 for _ in 0..n {
804 self.pending.pop();
805 }
806 }
807
808 fn role_problematic(&mut self, rawsource: &str, info: Option<String>, level: u8, text: &str) {
811 self.flush_text();
812 if let Some(info_text) = info {
813 self.messages.push(messages::system_message(
814 messages::INFO,
815 &info_text,
816 self.lineno,
817 self.source_path,
818 ));
819 }
820 let msg_id = self.registry.allocate_auto_id();
821 let prob_id = self.registry.allocate_auto_id();
822 let mut prob = Node::elem(kinds::PROBLEMATIC, self.span);
823 prob.attrs.ids.push(prob_id.clone());
824 prob.set("refid", AttrValue::Str(msg_id.clone()));
825 prob.children
826 .push(Node::text_node(unescape(rawsource, true), self.span));
827 self.nodes.push(prob);
828 let mut msg = messages::system_message(level, text, self.lineno, self.source_path);
829 msg.attrs.ids.push(msg_id);
830 msg.attrs.backrefs.push(prob_id);
831 self.messages.push(msg);
832 }
833
834 fn emit_sphinx_extlink(&mut self, kind: &str, raw: &str) {
837 let text = unescape(raw, false);
838 let (target, title, explicit) = match (text.rfind('<'), text.ends_with('>')) {
839 (Some(lt), true) => (
840 text[lt + 1..text.len() - 1].trim().to_string(),
841 text[..lt].trim_end().to_string(),
842 true,
843 ),
844 _ => (text.clone(), text.clone(), false),
845 };
846 let (numpart, anchor) = match target.split_once('#') {
847 Some((a, b)) => (a.to_string(), Some(b.to_string())),
848 None => (target.clone(), None),
849 };
850 let (kind_key, index_text, refuri, default_title) = match kind {
851 "pep" => {
852 let Ok(num) = numpart.parse::<u32>() else {
853 self.role_problematic(
854 raw,
855 None,
856 messages::ERROR,
857 &format!("invalid PEP number {target}"),
858 );
859 return;
860 };
861 let mut uri = format!("https://peps.python.org/pep-{num:04}/");
862 if let Some(a) = &anchor {
863 uri.push('#');
864 uri.push_str(a);
865 }
866 (
867 "pep",
868 format!("Python Enhancement Proposals; PEP {target}"),
869 uri,
870 format!("PEP {title}"),
871 )
872 }
873 "rfc" => {
874 let Ok(num) = numpart.parse::<u32>() else {
875 self.role_problematic(
876 raw,
877 None,
878 messages::ERROR,
879 &format!("invalid RFC number {target}"),
880 );
881 return;
882 };
883 let formatted = match &anchor {
884 Some(a) => {
885 let mut f = None;
886 for prefix in ["appendix", "page", "section"] {
887 let cap = {
888 let mut c = prefix.chars();
889 let f0 = c.next().expect("nonempty").to_uppercase();
890 format!("{f0}{}", c.as_str())
891 };
892 if a == prefix {
893 f = Some(format!("RFC {numpart} {cap}"));
894 break;
895 }
896 if let Some(rest) = a.strip_prefix(&format!("{prefix}-")) {
897 f = Some(format!("RFC {numpart} {cap} {rest}"));
898 break;
899 }
900 }
901 f.unwrap_or_else(|| format!("RFC {target}"))
902 }
903 None => format!("RFC {numpart}"),
904 };
905 let mut uri = format!("https://datatracker.ietf.org/doc/html/rfc{num}.html");
906 if let Some(a) = &anchor {
907 uri.push('#');
908 uri.push_str(a);
909 }
910 (
911 "rfc",
912 format!("RFC; {formatted}"),
913 uri,
914 if explicit { title.clone() } else { formatted },
915 )
916 }
917 "cve" => {
918 let mut uri = format!("https://www.cve.org/CVERecord?id=CVE-{numpart}");
919 if let Some(a) = &anchor {
920 uri.push('#');
921 uri.push_str(a);
922 }
923 (
924 "cve",
925 format!("Common Vulnerabilities and Exposures; CVE {target}"),
926 uri,
927 format!("CVE {title}"),
928 )
929 }
930 _ => {
931 let Ok(num) = numpart.parse::<u32>() else {
932 self.role_problematic(
933 raw,
934 None,
935 messages::ERROR,
936 &format!("invalid CWE number {target}"),
937 );
938 return;
939 };
940 let mut uri = format!("https://cwe.mitre.org/data/definitions/{num}.html");
941 if let Some(a) = &anchor {
942 uri.push('#');
943 uri.push_str(a);
944 }
945 (
946 "cwe",
947 format!("Common Weakness Enumeration; CWE {target}"),
948 uri,
949 format!("CWE {title}"),
950 )
951 }
952 };
953 let serial = self.registry.new_index_serialno();
954 let target_id = format!("index-{serial}");
955 self.flush_text();
956 let mut index = Node::elem("index", self.span);
957 index.set(
958 "entries",
959 AttrValue::List(vec![super::block::index_entry_tuple(
960 "single",
961 &index_text,
962 &target_id,
963 "",
964 None,
965 )]),
966 );
967 self.nodes.push(index);
968 let mut tnode = Node::elem(kinds::TARGET, self.span);
969 tnode.attrs.ids.push(target_id.clone());
970 self.registry.note_explicit_id(&target_id);
973 self.nodes.push(tnode);
974 let mut reference = Node::elem(kinds::REFERENCE, self.span);
975 reference.attrs.classes.push(kind_key.to_string());
976 reference.set("internal", AttrValue::Int(0));
977 reference.set("refuri", AttrValue::Str(refuri));
978 let mut strong = Node::elem(kinds::STRONG, self.span);
979 strong.children.push(Node::text_node(
980 if explicit { title } else { default_title },
981 self.span,
982 ));
983 reference.children.push(strong);
984 self.nodes.push(reference);
985 }
986
987 fn emit_sphinx_xref(&mut self, lower: &str, raw: &str) {
993 let (domain, reftype) = match lower.rsplit_once(':') {
994 Some((d, t)) => (d.to_string(), t.to_string()),
995 None => {
996 let d = match lower {
997 "doc" | "ref" | "term" | "option" | "envvar" | "numref" | "keyword"
998 | "token" | "program" | "confval" => "std",
999 "func" | "class" | "meth" | "mod" | "attr" | "data" | "exc" | "obj"
1006 | "const" | "deco" | "type" => "py",
1007 "any" => "",
1012 "eq" => "math",
1018 _ => "std",
1019 };
1020 (d.to_string(), lower.to_string())
1021 }
1022 };
1023 self.emit_xref_node(&domain, &reftype, raw, None);
1024 }
1025
1026 fn emit_external_xref(&mut self, given_name: &str, raw: &str) {
1043 match crate::intersphinx::external_role(given_name) {
1044 crate::intersphinx::ExternalRole::Xref {
1045 inventory,
1046 domain,
1047 role,
1048 } => self.emit_xref_node(&domain, &role, raw, Some(inventory)),
1049 crate::intersphinx::ExternalRole::Failed(diagnostic) => {
1050 self.flush_text();
1051 let mut node = Node::elem("pending_xref", self.span);
1052 node.set("refdoc", AttrValue::Str(self.docname.to_string()));
1053 node.set("intersphinx", AttrValue::Int(1));
1054 node.set("intersphinx_role_error", AttrValue::Str(diagnostic.message));
1055 self.nodes.push(node);
1056 }
1057 }
1058 }
1059
1060 fn update_title_and_target(
1081 target: String,
1082 title: String,
1083 explicit: bool,
1084 add_function_parentheses: bool,
1085 ) -> (String, String) {
1086 let mut title = title;
1087 if !explicit {
1088 if add_function_parentheses {
1089 if !title.ends_with("()") {
1090 title.push_str("()");
1091 }
1092 } else if let Some(stripped) = title.strip_suffix("()") {
1093 title = stripped.to_string();
1094 }
1095 }
1096 let target = match target.strip_suffix("()") {
1097 Some(stripped) => stripped.to_string(),
1098 None => target,
1099 };
1100 (target, title)
1101 }
1102
1103 fn emit_xref_node(
1108 &mut self,
1109 domain: &str,
1110 reftype: &str,
1111 raw: &str,
1112 external: Option<Option<String>>,
1113 ) {
1114 let text = unescape(raw, false);
1115 let (domain, reftype) = (domain.to_string(), reftype.to_string());
1116 let (target, display, explicit) = match (text.rfind('<'), text.ends_with('>')) {
1124 (Some(lt), true) => (
1125 text[lt + 1..text.len() - 1].to_string(),
1126 text[..lt]
1127 .trim_end_matches(crate::utils::py_isspace)
1128 .to_string(),
1129 true,
1130 ),
1131 _ => (text.clone(), text.clone(), false),
1132 };
1133 let py = domain == "py";
1143 let (target, display) = if py && matches!(reftype.as_str(), "func" | "meth") {
1144 Self::update_title_and_target(
1145 target,
1146 display,
1147 explicit,
1148 self.py.add_function_parentheses,
1149 )
1150 } else {
1151 (target, display)
1152 };
1153 let any = domain.is_empty() && reftype == "any";
1177 let (target, display) = match (domain.as_str(), reftype.as_str()) {
1178 ("std", "ref" | "numref") => (collapse_whitespace(&target.to_lowercase()), display),
1179 (d, t) if target_collapses_whitespace(d, t) => (collapse_whitespace(&target), display),
1180 _ => (target, display),
1181 };
1182 let mut refspecific = false;
1186 let (target, display) = if py {
1187 let mut title = display;
1188 let mut target = target;
1189 if !explicit {
1190 title = title.trim_start_matches('.').to_string();
1195 target = target.trim_start_matches('~').to_string();
1196 if title.starts_with('~') {
1201 let rest = title[1..].to_string();
1202 title = match rest.rfind('.') {
1203 Some(dot) => rest[dot + 1..].to_string(),
1204 None => rest,
1205 };
1206 }
1207 }
1208 if target.starts_with('.') {
1213 target = target[1..].to_string();
1214 refspecific = true;
1215 }
1216 if reftype == "deco" {
1220 title = format!("@{title}");
1221 }
1222 (target, title)
1223 } else {
1224 (target, display)
1225 };
1226 let mut node = Node::elem("pending_xref", self.span);
1227 if py {
1228 node.set(
1232 "py:class",
1233 AttrValue::Str(self.ctx.py_class.unwrap_or("True").to_string()),
1234 );
1235 node.set(
1236 "py:module",
1237 AttrValue::Str(self.ctx.py_module.unwrap_or("True").to_string()),
1238 );
1239 }
1240 if any {
1241 if self.ctx.py_class_key {
1251 node.set(
1252 "py:class",
1253 AttrValue::Str(self.ctx.py_class.unwrap_or("True").to_string()),
1254 );
1255 }
1256 if self.ctx.py_classes_key {
1257 node.set("py:classes", AttrValue::Str(String::new()));
1258 }
1259 if self.ctx.py_module_key {
1264 node.set(
1265 "py:module",
1266 AttrValue::Str(self.ctx.py_module.unwrap_or("True").to_string()),
1267 );
1268 }
1269 if self.ctx.py_modules_key {
1270 node.set("py:modules", AttrValue::Str(String::new()));
1271 }
1272 if let Some(program) = self.ctx.program {
1273 node.set("std:program", AttrValue::Str(program.to_string()));
1274 }
1275 }
1276 node.set("refdoc", AttrValue::Str(self.docname.to_string()));
1277 node.set("refdomain", AttrValue::Str(domain.clone()));
1278 if let Some(inventory) = external {
1282 node.set("intersphinx", AttrValue::Int(1));
1283 if let Some(inventory) = inventory {
1284 node.set("inventory", AttrValue::Str(inventory));
1285 }
1286 }
1287 node.set("refexplicit", AttrValue::Int(i64::from(explicit)));
1288 if refspecific {
1289 node.set("refspecific", AttrValue::Int(1));
1293 }
1294 node.set("reftarget", AttrValue::Str(target));
1295 node.set("reftype", AttrValue::Str(reftype.clone()));
1296 let warn_dangling = matches!(
1313 (domain.as_str(), reftype.as_str()),
1314 (
1315 "std",
1316 "ref" | "numref" | "doc" | "term" | "keyword" | "option" | "confval"
1317 ) | ("", "any")
1318 | ("math", "eq")
1319 );
1320 node.set("refwarn", AttrValue::Int(i64::from(warn_dangling)));
1321 if domain == "std" && reftype == "option" {
1325 node.set(
1326 "std:program",
1327 AttrValue::Str(self.ctx.program.unwrap_or("True").to_string()),
1328 );
1329 }
1330 let inline_inner = matches!(
1334 (domain.as_str(), reftype.as_str()),
1335 ("std", "ref" | "term" | "doc")
1336 );
1337 let mut inner = Node::elem(
1338 if inline_inner {
1339 "inline"
1340 } else {
1341 kinds::LITERAL
1342 },
1343 self.span,
1344 );
1345 let registered_domainless = domain.is_empty() || (domain == "math" && reftype == "eq");
1350 inner.attrs.classes = if registered_domainless {
1351 vec!["xref".to_string(), reftype.clone()]
1352 } else {
1353 vec![
1354 "xref".to_string(),
1355 domain.clone(),
1356 format!("{domain}-{reftype}"),
1357 ]
1358 };
1359 inner.children.push(Node::text_node(display, self.span));
1360 node.children.push(inner);
1361 self.flush_text();
1362 if domain == "std" && reftype == "envvar" && !text.starts_with('!') {
1378 let varname = match node.get("reftarget") {
1379 Some(AttrValue::Str(target)) => target.clone(),
1380 _ => String::new(),
1381 };
1382 let target_id = format!("index-{}", self.registry.new_index_serialno());
1383 let mut index = Node::elem("index", self.span);
1384 index.set(
1385 "entries",
1386 AttrValue::List(vec![
1387 super::block::index_entry_tuple("single", &varname, &target_id, "", None),
1388 super::block::index_entry_tuple(
1389 "single",
1390 &format!("environment variable; {varname}"),
1391 &target_id,
1392 "",
1393 None,
1394 ),
1395 ]),
1396 );
1397 self.nodes.push(index);
1398 let mut tnode = Node::elem(kinds::TARGET, self.span);
1399 tnode.attrs.ids.push(target_id.clone());
1400 self.registry.note_explicit_id(&target_id);
1401 self.nodes.push(tnode);
1402 }
1403 self.nodes.push(node);
1404 }
1405
1406 fn apply_role(&mut self, given_name: &str, raw: &str, rawsource: &str) {
1408 let lower = given_name.to_lowercase();
1409 if self.sphinx {
1410 let text = unescape(raw, false);
1413 let (target, display) = match (text.rfind('<'), text.ends_with('>')) {
1414 (Some(lt), true) => (
1415 text[lt + 1..text.len() - 1].trim().to_string(),
1416 Some(text[..lt].trim().to_string()),
1417 ),
1418 _ => (text.clone(), None),
1419 };
1420 let last_segment = lower.rsplit(':').next().unwrap_or(&lower).to_string();
1421 self.roles.push(super::RoleRecord {
1422 source: self.span.source,
1423 name: last_segment,
1424 full_name: given_name.to_string(),
1425 target,
1426 display,
1427 line: self.lineno,
1428 });
1429 }
1430 let canonical = match lower.as_str() {
1432 "abbreviation" | "ab" => "abbreviation",
1433 "acronym" | "ac" => "acronym",
1434 "code" => "code",
1435 "emphasis" => "emphasis",
1436 "literal" => "literal",
1437 "math" => "math",
1438 "pep-reference" | "pep" => "pep-reference",
1439 "rfc-reference" | "rfc" => "rfc-reference",
1440 "strong" => "strong",
1441 "subscript" | "sub" => "subscript",
1442 "superscript" | "sup" => "superscript",
1443 "title-reference" | "title" | "t" => "title-reference",
1444 "raw" => "raw",
1445 "index" | "i" => "index",
1446 "named-reference" => "named-reference",
1447 "anonymous-reference" => "anonymous-reference",
1448 "footnote-reference" => "footnote-reference",
1449 "citation-reference" => "citation-reference",
1450 "substitution-reference" => "substitution-reference",
1451 "target" => "target",
1452 "uri-reference" | "uri" | "url" => "uri-reference",
1453 _ if self.sphinx && crate::intersphinx::is_external_role(given_name) => {
1454 self.emit_external_xref(given_name, raw);
1455 return;
1456 }
1457 _ if self.sphinx && matches!(lower.as_str(), "cve" | "cwe") => {
1458 self.emit_sphinx_extlink(&lower, raw);
1459 return;
1460 }
1461 _ if self.sphinx => {
1462 self.emit_sphinx_xref(&lower, raw);
1467 return;
1468 }
1469 _ => {
1470 let info = format!(
1473 "No role entry for \"{given_name}\" in module \"docutils.parsers.rst.languages.en\".\nTrying \"{given_name}\" as canonical role name."
1474 );
1475 if lower == "restructuredtext-unimplemented-role" {
1476 self.role_problematic(
1477 rawsource,
1478 Some(info),
1479 messages::ERROR,
1480 &format!("Interpreted text role \"{given_name}\" not implemented."),
1481 );
1482 } else {
1483 self.role_problematic(
1484 rawsource,
1485 Some(info),
1486 messages::ERROR,
1487 &format!("Unknown interpreted text role \"{given_name}\"."),
1488 );
1489 }
1490 return;
1491 }
1492 };
1493 match canonical {
1494 "emphasis" => self.emit_inline(kinds::EMPHASIS, raw, false),
1495 "strong" => self.emit_inline(kinds::STRONG, raw, false),
1496 "literal" => self.emit_inline(kinds::LITERAL, raw, false),
1497 "subscript" => self.emit_inline(kinds::SUBSCRIPT, raw, false),
1498 "superscript" => self.emit_inline(kinds::SUPERSCRIPT, raw, false),
1499 "title-reference" => self.emit_inline(kinds::TITLE_REFERENCE, raw, false),
1500 "abbreviation" => self.emit_inline(kinds::ABBREVIATION, raw, false),
1501 "acronym" => self.emit_inline(kinds::ACRONYM, raw, false),
1502 "math" => self.emit_inline(kinds::MATH, raw, true),
1503 "code" => {
1504 self.flush_text();
1505 let mut node = Node::elem(kinds::LITERAL, self.span);
1506 node.attrs.classes.push("code".to_string());
1507 node.children
1508 .push(Node::text_node(unescape(raw, true), self.span));
1509 self.nodes.push(node);
1510 }
1511 "pep-reference" => {
1512 if self.sphinx {
1513 self.emit_sphinx_extlink("pep", raw);
1515 return;
1516 }
1517 let text = unescape(raw, false);
1518 match text.parse::<u32>().ok().filter(|n| *n <= 9999) {
1519 Some(n) => {
1520 self.flush_text();
1521 let mut r = Node::elem(kinds::REFERENCE, self.span);
1522 r.set(
1523 "refuri",
1524 AttrValue::Str(format!("https://peps.python.org/pep-{n:04}")),
1525 );
1526 r.children
1527 .push(Node::text_node(format!("PEP {text}"), self.span));
1528 self.nodes.push(r);
1529 }
1530 None => self.role_problematic(
1531 rawsource,
1532 None,
1533 messages::ERROR,
1534 &format!(
1535 "PEP number must be a number from 0 to 9999; \"{text}\" is invalid."
1536 ),
1537 ),
1538 }
1539 }
1540 "rfc-reference" => {
1541 if self.sphinx {
1542 self.emit_sphinx_extlink("rfc", raw);
1543 return;
1544 }
1545 let text = unescape(raw, false);
1546 let (numpart, fragment) = match text.split_once('#') {
1547 Some((a, b)) => (a.to_string(), Some(b.to_string())),
1548 None => (text.clone(), None),
1549 };
1550 match numpart.parse::<u32>().ok().filter(|n| *n >= 1) {
1551 Some(n) => {
1552 self.flush_text();
1553 let mut r = Node::elem(kinds::REFERENCE, self.span);
1554 let mut uri = format!("https://tools.ietf.org/html/rfc{n}.html");
1555 if let Some(f) = fragment {
1556 uri = format!("{uri}#{f}");
1557 }
1558 r.set("refuri", AttrValue::Str(uri));
1559 r.children
1560 .push(Node::text_node(format!("RFC {n}"), self.span));
1561 self.nodes.push(r);
1562 }
1563 None => self.role_problematic(
1564 rawsource,
1565 None,
1566 messages::ERROR,
1567 &format!(
1568 "RFC number must be a number greater than or equal to 1; \"{numpart}\" is invalid."
1569 ),
1570 ),
1571 }
1572 }
1573 "raw" => self.role_problematic(
1574 rawsource,
1575 None,
1576 messages::ERROR,
1577 "No format (Writer name) is associated with this role: \"raw\".\nThe \"raw\" role cannot be used directly.\nInstead, use the \"role\" directive to create a new role with an associated format.",
1578 ),
1579 other => self.role_problematic(
1580 rawsource,
1581 None,
1582 messages::ERROR,
1583 &format!("Interpreted text role \"{other}\" not implemented."),
1584 ),
1585 }
1586 }
1587
1588 fn phrase_reference(&mut self, raw: &str, underscores: usize) {
1590 self.flush_text();
1591 let ids = |s: &str| crate::doctree::ids::fully_normalize_name(s);
1592 let wsn = |s: &str| crate::doctree::ids::whitespace_normalize_name(s);
1593
1594 let embedded = find_embedded_link(raw);
1597 let mut r = Node::elem(kinds::REFERENCE, self.span);
1598 match embedded {
1599 Some((text_part, link)) => {
1600 let is_alias = link.ends_with('_')
1601 && !link.ends_with("\u{0}_")
1602 && !link
1603 .chars()
1604 .rev()
1605 .nth(1)
1606 .map(|c| c == NULL)
1607 .unwrap_or(false)
1608 && !looks_like_uri(&link);
1609 let display_text;
1610 if is_alias {
1611 let alias = ids(&unescape(&link[..link.len() - 1], false));
1612 display_text = if text_part.is_empty() {
1613 unescape(&link[..link.len() - 1], false)
1614 } else {
1615 unescape(&text_part, false)
1616 };
1617 r.set("name", AttrValue::Str(wsn(&display_text)));
1618 r.set("refname", AttrValue::Str(alias.clone()));
1619 r.children
1620 .push(Node::text_node(display_text.clone(), self.span));
1621 self.nodes.push(r);
1622 if underscores == 1 {
1623 let mut t = Node::elem(kinds::TARGET, self.span);
1624 t.attrs.names.push(ids(&display_text));
1625 let msg =
1626 self.registry
1627 .set_id_implicit(&mut t, self.lineno, self.source_path);
1628 t.set("refname", AttrValue::Str(alias));
1629 self.nodes.push(t);
1630 if let Some(m) = msg {
1631 self.messages.push(m);
1632 }
1633 }
1634 } else {
1635 let mut uri_src = link.clone();
1638 if uri_src.ends_with('_')
1639 && uri_src
1640 .chars()
1641 .rev()
1642 .nth(1)
1643 .map(|c| c == NULL)
1644 .unwrap_or(false)
1645 {
1646 uri_src = format!("{}_", &uri_src[..uri_src.len() - 2]);
1648 }
1649 let uri = clean_uri(&uri_src);
1650 let uri = if looks_like_email(&uri) && !uri.starts_with("mailto:") {
1651 format!("mailto:{uri}")
1652 } else {
1653 uri
1654 };
1655 display_text = if text_part.is_empty() {
1656 uri.clone()
1657 } else {
1658 unescape(&text_part, false)
1659 };
1660 r.set("name", AttrValue::Str(wsn(&display_text)));
1661 r.set("refuri", AttrValue::Str(uri.clone()));
1662 r.children
1663 .push(Node::text_node(display_text.clone(), self.span));
1664 self.nodes.push(r);
1665 if underscores == 1 {
1666 let mut t = Node::elem(kinds::TARGET, self.span);
1667 t.attrs.names.push(ids(&display_text));
1668 let msg =
1669 self.registry
1670 .set_id_implicit(&mut t, self.lineno, self.source_path);
1671 t.set("refuri", AttrValue::Str(uri));
1672 self.nodes.push(t);
1673 if let Some(m) = msg {
1674 self.messages.push(m);
1675 }
1676 }
1677 }
1678 }
1679 None => {
1680 let text = unescape(raw, false);
1681 r.set("name", AttrValue::Str(wsn(&text)));
1682 if underscores == 2 {
1683 r.set("anonymous", AttrValue::Int(1));
1684 } else {
1685 r.set("refname", AttrValue::Str(ids(&text)));
1686 }
1687 r.children.push(Node::text_node(text, self.span));
1688 self.nodes.push(r);
1689 }
1690 }
1691 }
1692
1693 fn try_footnote_or_citation(&mut self, i: usize) -> Option<usize> {
1695 let n = self.chars.len();
1696 let mut j = i + 1;
1697 let label_start = j;
1699 match self.chars.get(j) {
1700 Some(&'#') => {
1701 j += 1;
1702 if let Some(len) = match_simplename(&self.chars, j) {
1703 j += len;
1704 }
1705 }
1706 Some(&'*') => j += 1,
1707 _ => j += match_simplename(&self.chars, j)?,
1708 }
1709 if self.chars.get(j) != Some(&']') || self.chars.get(j + 1) != Some(&'_') {
1710 return None;
1711 }
1712 let after = j + 2;
1713 if !is_end_suffix_ok(self.chars.get(after).copied()) {
1714 return None;
1715 }
1716 let _ = n;
1717 let label: String = self.chars[label_start..j].iter().collect();
1718 self.flush_text();
1719 let is_citation =
1720 !label.starts_with('#') && label != "*" && !label.chars().all(|c| c.is_ascii_digit());
1721 let id = self.registry.allocate_auto_id();
1722 let mut node = if is_citation {
1723 let mut c = Node::elem(kinds::CITATION_REFERENCE, self.span);
1724 c.set(
1725 "refname",
1726 AttrValue::Str(crate::doctree::ids::fully_normalize_name(&label)),
1727 );
1728 c.children.push(Node::text_node(label.clone(), self.span));
1729 c
1730 } else {
1731 let mut f = Node::elem(kinds::FOOTNOTE_REFERENCE, self.span);
1732 if label == "*" {
1733 f.set("auto", AttrValue::Str("*".to_string()));
1734 } else if let Some(rest) = label.strip_prefix('#') {
1735 f.set("auto", AttrValue::Int(1));
1736 if !rest.is_empty() {
1737 f.set(
1738 "refname",
1739 AttrValue::Str(crate::doctree::ids::fully_normalize_name(rest)),
1740 );
1741 }
1742 } else {
1743 f.set(
1744 "refname",
1745 AttrValue::Str(crate::doctree::ids::fully_normalize_name(&label)),
1746 );
1747 f.children.push(Node::text_node(label.clone(), self.span));
1748 }
1749 f
1750 };
1751 node.attrs.ids.push(id);
1752 self.nodes.push(node);
1753 Some(after)
1754 }
1755
1756 fn substitution_ref(&mut self, i: usize) -> usize {
1758 let content_from = i + 1;
1759 let (end, underscores) = match self.find_end_with_underscores(content_from, '|', 2) {
1760 Some(r) => r,
1761 None => {
1762 self.emit_problematic("|", "substitution_reference");
1763 return i + 1;
1764 }
1765 };
1766 let after = end + 1 + underscores;
1767 let raw: String = self.chars[content_from..end].iter().collect();
1768 let text = unescape(&raw, false);
1769 self.flush_text();
1770 let mut subref = Node::elem(kinds::SUBSTITUTION_REFERENCE, self.span);
1771 subref.set(
1772 "refname",
1773 AttrValue::Str(crate::doctree::ids::whitespace_normalize_name(&text)),
1774 );
1775 subref
1776 .children
1777 .push(Node::text_node(text.clone(), self.span));
1778 if underscores == 0 {
1779 self.nodes.push(subref);
1780 } else {
1781 let mut outer = Node::elem(kinds::REFERENCE, self.span);
1782 if underscores == 2 {
1783 outer.set("anonymous", AttrValue::Int(1));
1784 } else {
1785 outer.set(
1786 "refname",
1787 AttrValue::Str(crate::doctree::ids::fully_normalize_name(&text)),
1788 );
1789 }
1790 outer.children.push(subref);
1791 self.nodes.push(outer);
1792 }
1793 after
1794 }
1795}
1796
1797fn clean_uri(link: &str) -> String {
1801 let joined: String = link
1802 .split('\n')
1803 .map(|part| {
1804 part.chars()
1805 .filter(|c| !crate::utils::py_isspace(*c))
1806 .collect::<String>()
1807 })
1808 .collect();
1809 unescape(&joined, false)
1810}
1811
1812fn looks_like_uri(link: &str) -> bool {
1813 let mut chars = link.chars();
1814 match chars.next() {
1815 Some(c) if c.is_ascii_alphabetic() => {}
1816 _ => return looks_like_email(link),
1817 }
1818 let scheme: String = link
1819 .chars()
1820 .take_while(|c| c.is_ascii_alphanumeric() || matches!(c, '.' | '+' | '-'))
1821 .collect();
1822 link[scheme.len()..].starts_with(':')
1823 && punctuation::URI_SCHEMES.contains(&scheme.to_lowercase().as_str())
1824 || looks_like_email(link)
1825}
1826
1827fn looks_like_email(s: &str) -> bool {
1828 let s = s.trim_end_matches('_');
1829 let Some(at) = s.find('@') else { return false };
1830 let (local, domain) = (&s[..at], &s[at + 1..]);
1831 !local.is_empty()
1832 && !domain.is_empty()
1833 && domain.contains('.')
1834 && local.chars().all(|c| is_emailc(c) || c == '.')
1835 && domain.chars().all(|c| is_emailc(c) || c == '.')
1836}
1837
1838fn find_embedded_link(raw: &str) -> Option<(String, String)> {
1842 if !raw.ends_with('>') {
1843 return None;
1844 }
1845 let chars: Vec<char> = raw.chars().collect();
1846 let n = chars.len();
1847 if n >= 2 && (chars[n - 2] == NULL || crate::utils::py_isspace(chars[n - 2])) {
1851 return None;
1852 }
1853 let mut open = None;
1855 for k in (0..n - 1).rev() {
1856 if chars[k] == '<' && (k == 0 || chars[k - 1] != NULL) {
1857 open = Some(k);
1858 break;
1859 }
1860 }
1861 let open = open?;
1862 let mut m = open + 1;
1865 while m < n - 1 {
1866 let c = chars[m];
1867 if c == NULL {
1868 m += 2;
1869 continue;
1870 }
1871 if c == '<' || c == '>' {
1872 return None;
1873 }
1874 m += 1;
1875 }
1876 if open > 0 {
1877 let before = chars[open - 1];
1879 if !(before == ' ' || before == '\n') {
1880 return None;
1881 }
1882 }
1883 if chars
1885 .get(open + 1)
1886 .is_some_and(|c| crate::utils::py_isspace(*c))
1887 {
1888 return None;
1889 }
1890 let link: String = chars[open + 1..n - 1].iter().collect();
1891 if link.is_empty() {
1892 return None;
1893 }
1894 let text: String = chars[..open].iter().collect();
1897 Some((text.trim_end_matches([' ', '\n']).to_string(), link))
1898}
1899
1900fn match_standalone_uri(chars: &[char], at: usize) -> Option<(usize, String, String)> {
1903 if chars[at].is_ascii_alphabetic() {
1905 let mut j = at;
1906 while j < chars.len()
1907 && (chars[j].is_ascii_alphanumeric() || matches!(chars[j], '.' | '+' | '-'))
1908 {
1909 j += 1;
1910 }
1911 if j < chars.len() && chars[j] == ':' {
1912 let scheme: String = chars[at..j].iter().collect();
1913 if punctuation::URI_SCHEMES.contains(&scheme.to_lowercase().as_str()) {
1914 let mut k = j + 1;
1915 let seg_end = |k: usize, stop: &[char]| -> usize {
1916 let mut e = k;
1917 while e < chars.len() && is_uric(chars[e]) && !stop.contains(&chars[e]) {
1918 e += 1;
1919 }
1920 let mut last = e;
1922 while last > k && !is_urilast(chars[last - 1]) {
1923 last -= 1;
1924 }
1925 last
1926 };
1927 let h_end = seg_end(k, &['?', '#']);
1928 if h_end == k {
1929 return None;
1930 }
1931 k = h_end;
1932 if chars.get(k) == Some(&'?') {
1933 let q_end = seg_end(k + 1, &['#']);
1934 if q_end > k + 1 {
1935 k = q_end;
1936 }
1937 }
1938 if chars.get(k) == Some(&'#') {
1939 let f_end = seg_end(k + 1, &[]);
1940 if f_end > k + 1 {
1941 k = f_end;
1942 }
1943 }
1944 if !is_end_suffix_ok(chars.get(k).copied()) && chars.get(k) != Some(&'>') {
1945 return None;
1946 }
1947 let uri: String = chars[at..k].iter().collect();
1948 return Some((k - at, uri.clone(), uri));
1949 }
1950 }
1951 }
1952 {
1955 let mut j = at;
1956 while j < chars.len() && (is_emailc(chars[j]) || chars[j] == '.' || chars[j] == '@') {
1957 j += 1;
1958 }
1959 while j > at && !is_urilast(chars[j - 1]) {
1961 j -= 1;
1962 }
1963 let cand: String = chars[at..j].iter().collect();
1964 if cand.contains('@')
1965 && looks_like_email(&cand)
1966 && (is_end_suffix_ok(chars.get(j).copied()) || chars.get(j) == Some(&'>'))
1967 {
1968 let refuri = if cand.starts_with("mailto:") {
1969 cand.clone()
1970 } else {
1971 format!("mailto:{cand}")
1972 };
1973 return Some((j - at, refuri, cand));
1974 }
1975 }
1976 None
1977}
1978
1979pub fn parse_inline(
1982 text: &str,
1983 span: Span,
1984 lineno: u32,
1985 registry: &mut IdRegistry,
1986 source_path: &str,
1987) -> InlineResult {
1988 let py = crate::py::PySigConfig::default();
1991 parse_inline_ext(
1992 text,
1993 span,
1994 lineno,
1995 registry,
1996 source_path,
1997 false,
1998 "index",
1999 RefContext::default(),
2000 &py,
2001 )
2002}
2003
2004#[allow(clippy::too_many_arguments)]
2005pub fn parse_inline_ext(
2006 text: &str,
2007 span: Span,
2008 lineno: u32,
2009 registry: &mut IdRegistry,
2010 source_path: &str,
2011 sphinx: bool,
2012 docname: &str,
2013 ctx: RefContext<'_>,
2014 py: &crate::py::PySigConfig,
2015) -> InlineResult {
2016 let escaped = escape2null(text);
2017 let mut inliner = Inliner {
2018 chars: escaped.chars().collect(),
2019 span,
2020 lineno,
2021 source_path,
2022 registry,
2023 sphinx,
2024 docname,
2025 ctx,
2026 py,
2027 roles: Vec::new(),
2028 nodes: Vec::new(),
2029 messages: Vec::new(),
2030 pending: String::new(),
2031 failed_end_search: std::collections::HashMap::new(),
2032 };
2033 inliner.run();
2034 InlineResult {
2035 nodes: inliner.nodes,
2036 messages: inliner.messages,
2037 roles: inliner.roles,
2038 }
2039}
2040
2041#[cfg(test)]
2042mod tests {
2043 use super::*;
2044
2045 #[test]
2046 fn escape2null_and_unescape_roundtrip() {
2047 assert_eq!(escape2null("a\\*b"), "a\u{0}*b");
2048 assert_eq!(unescape("a\u{0}*b", false), "a*b");
2049 assert_eq!(unescape("one\u{0} two", false), "onetwo"); assert_eq!(unescape("a\u{0}\\b", true), "a\\\\b"); }
2052
2053 fn pi(text: &str) -> (Vec<Node>, Vec<Node>) {
2054 let mut reg = IdRegistry::new();
2055 let r = parse_inline(text, Span::ZERO, 1, &mut reg, "<snippet>");
2056 (r.nodes, r.messages)
2057 }
2058
2059 #[test]
2060 fn plain_text_stays_single_node() {
2061 let (nodes, msgs) = pi("just text");
2062 assert_eq!(nodes.len(), 1);
2063 assert_eq!(nodes[0].text.as_deref(), Some("just text"));
2064 assert!(msgs.is_empty());
2065 }
2066
2067 #[test]
2068 fn simple_emphasis_strong_literal() {
2069 let (nodes, _) = pi("before *emph* after");
2070 assert_eq!(nodes.len(), 3);
2071 assert_eq!(nodes[0].text.as_deref(), Some("before "));
2072 assert_eq!(nodes[1].kind, kinds::EMPHASIS);
2073 assert_eq!(nodes[1].astext(), "emph");
2074 assert_eq!(nodes[2].text.as_deref(), Some(" after"));
2075
2076 let (nodes, _) = pi("*a* **b** ``c``");
2077 assert_eq!(nodes.len(), 5); assert_eq!(nodes[2].kind, kinds::STRONG);
2079 assert_eq!(nodes[4].kind, kinds::LITERAL);
2080 }
2081
2082 #[test]
2083 fn word_chars_block_recognition() {
2084 let (nodes, msgs) = pi("a*b*c");
2085 assert_eq!(nodes.len(), 1);
2086 assert_eq!(nodes[0].text.as_deref(), Some("a*b*c"));
2087 assert!(msgs.is_empty());
2088 }
2089
2090 #[test]
2091 fn quoted_start_suppresses_silently() {
2092 let (nodes, msgs) = pi("\"*\"");
2093 assert_eq!(nodes.len(), 1);
2094 assert_eq!(nodes[0].text.as_deref(), Some("\"*\""));
2095 assert!(msgs.is_empty());
2096 let (nodes, msgs) = pi("word *");
2097 assert_eq!(nodes[0].text.as_deref(), Some("word *"));
2098 assert!(msgs.is_empty());
2099 }
2100
2101 #[test]
2102 fn unclosed_produces_problematic_and_message() {
2103 let (nodes, msgs) = pi("*oops");
2104 assert_eq!(nodes.len(), 2);
2105 assert_eq!(nodes[0].kind, kinds::PROBLEMATIC);
2106 assert_eq!(nodes[0].astext(), "*");
2107 assert_eq!(nodes[0].attrs.ids, vec!["id2"]);
2108 assert_eq!(nodes[0].get("refid"), Some(&AttrValue::Str("id1".into())));
2109 assert_eq!(nodes[1].text.as_deref(), Some("oops"));
2110 assert_eq!(msgs.len(), 1);
2111 assert_eq!(msgs[0].attrs.ids, vec!["id1"]);
2112 assert_eq!(msgs[0].attrs.backrefs, vec!["id2"]);
2113 assert_eq!(
2114 msgs[0].children[0].astext(),
2115 "Inline emphasis start-string without end-string."
2116 );
2117 }
2118
2119 #[test]
2120 fn no_nesting_and_first_end_wins() {
2121 let (nodes, _) = pi("**a *b* c**");
2122 assert_eq!(nodes[0].kind, kinds::STRONG);
2123 assert_eq!(nodes[0].astext(), "a *b* c");
2124 let (nodes, _) = pi("*word *word*");
2125 assert_eq!(nodes[0].kind, kinds::EMPHASIS);
2126 assert_eq!(nodes[0].astext(), "word *word");
2127 let (nodes, _) = pi("***x***");
2128 assert_eq!(nodes[0].kind, kinds::STRONG);
2129 assert_eq!(nodes[0].astext(), "*x*");
2130 }
2131
2132 #[test]
2133 fn literal_restores_backslashes() {
2134 let (nodes, _) = pi("``a\\*b``");
2135 assert_eq!(nodes[0].kind, kinds::LITERAL);
2136 assert_eq!(nodes[0].astext(), "a\\*b");
2137 }
2138
2139 #[test]
2140 fn escapes_disappear_in_plain_text() {
2141 let (nodes, _) = pi("\\*not markup\\*");
2142 assert_eq!(nodes.len(), 1);
2143 assert_eq!(nodes[0].text.as_deref(), Some("*not markup*"));
2144 }
2145
2146 fn sphinx_nodes(text: &str) -> Vec<Node> {
2148 sphinx_nodes_with(text, &crate::py::PySigConfig::default())
2149 }
2150
2151 fn sphinx_nodes_with(text: &str, py: &crate::py::PySigConfig) -> Vec<Node> {
2153 sphinx_nodes_in(text, RefContext::default(), py)
2154 }
2155
2156 fn sphinx_nodes_in(text: &str, ctx: RefContext<'_>, py: &crate::py::PySigConfig) -> Vec<Node> {
2158 let mut reg = IdRegistry::new();
2159 parse_inline_ext(
2160 text,
2161 Span::ZERO,
2162 1,
2163 &mut reg,
2164 "<snippet>",
2165 true,
2166 "index",
2167 ctx,
2168 py,
2169 )
2170 .nodes
2171 }
2172
2173 fn attr<'a>(node: &'a Node, key: &'static str) -> Option<&'a AttrValue> {
2174 node.get(key)
2175 }
2176
2177 #[test]
2181 fn an_external_role_is_intercepted_before_the_generic_domain_split() {
2182 let nodes = sphinx_nodes(":external:py:func:`os.path.join`");
2183 assert_eq!(nodes.len(), 1);
2184 let xref = &nodes[0];
2185 assert_eq!(xref.kind, "pending_xref");
2186 assert_eq!(attr(xref, "refdomain"), Some(&AttrValue::Str("py".into())));
2187 assert_eq!(attr(xref, "reftype"), Some(&AttrValue::Str("func".into())));
2188 assert_eq!(
2189 attr(xref, "reftarget"),
2190 Some(&AttrValue::Str("os.path.join".into()))
2191 );
2192 assert_eq!(
2193 attr(xref, "intersphinx"),
2194 Some(&AttrValue::Int(1)),
2195 "the node must be stamped so the resolver treats it as external"
2196 );
2197 assert_eq!(
2198 attr(xref, "inventory"),
2199 None,
2200 "`external:` names no inventory"
2201 );
2202 }
2203
2204 #[test]
2205 fn an_external_role_can_name_its_inventory_and_omit_the_domain() {
2206 let nodes = sphinx_nodes(":external+other:ref:`some-label`");
2207 let xref = &nodes[0];
2208 assert_eq!(xref.kind, "pending_xref");
2209 assert_eq!(attr(xref, "refdomain"), Some(&AttrValue::Str("std".into())));
2210 assert_eq!(attr(xref, "reftype"), Some(&AttrValue::Str("ref".into())));
2211 assert_eq!(
2212 attr(xref, "inventory"),
2213 Some(&AttrValue::Str("other".into())),
2214 "the inventory name keeps its case"
2215 );
2216 }
2217
2218 #[test]
2222 fn a_malformed_external_role_carries_its_warning_to_resolution() {
2223 for (text, expected) in [
2224 (
2225 ":external:a:b:c:`x`",
2226 "invalid external cross-reference suffix: 'a:b:c'",
2227 ),
2228 (
2229 ":external:py:function:`x`",
2230 "role for external cross-reference not found in domain 'py': 'function' \
2231 (perhaps you meant one of: 'func', 'obj')",
2232 ),
2233 ] {
2234 let nodes = sphinx_nodes(text);
2235 assert_eq!(nodes.len(), 1, "{text}");
2236 assert_eq!(
2237 attr(&nodes[0], "intersphinx_role_error"),
2238 Some(&AttrValue::Str(expected.to_string())),
2239 "{text}"
2240 );
2241 assert!(
2242 nodes[0].children.is_empty(),
2243 "a failed role contributes no content: {text}"
2244 );
2245 }
2246 }
2247
2248 #[test]
2251 fn a_role_named_like_external_is_still_an_ordinary_xref() {
2252 let nodes = sphinx_nodes(":externally:`x`");
2253 assert_eq!(
2254 attr(&nodes[0], "reftype"),
2255 Some(&AttrValue::Str("externally".into()))
2256 );
2257 assert_eq!(attr(&nodes[0], "intersphinx"), None);
2258 }
2259
2260 fn with_parens(add_function_parentheses: bool) -> crate::py::PySigConfig {
2269 crate::py::PySigConfig {
2270 add_function_parentheses,
2271 ..crate::py::PySigConfig::default()
2272 }
2273 }
2274
2275 fn xref_title_and_target(text: &str, add_function_parentheses: bool) -> (String, String) {
2277 let nodes = sphinx_nodes_with(text, &with_parens(add_function_parentheses));
2278 assert_eq!(nodes.len(), 1, "{text}");
2279 let xref = &nodes[0];
2280 let target = match attr(xref, "reftarget") {
2281 Some(AttrValue::Str(target)) => target.clone(),
2282 other => panic!("reftarget on {text}: {other:?}"),
2283 };
2284 (xref.astext(), target)
2285 }
2286
2287 #[test]
2290 fn an_implicit_fix_parens_title_gains_parens_by_default() {
2291 assert_eq!(
2292 xref_title_and_target(":py:func:`mymod.myfunc`", true),
2293 ("mymod.myfunc()".to_string(), "mymod.myfunc".to_string()),
2294 "P1"
2295 );
2296 assert_eq!(
2297 xref_title_and_target(":py:meth:`Obj.method`", true),
2298 ("Obj.method()".to_string(), "Obj.method".to_string()),
2299 "P6"
2300 );
2301 assert_eq!(
2302 xref_title_and_target(":py:class:`mymod.MyClass`", true),
2303 ("mymod.MyClass".to_string(), "mymod.MyClass".to_string()),
2304 "P7: no fix_parens on the class role"
2305 );
2306 }
2307
2308 #[test]
2311 fn an_already_parenthesized_implicit_title_is_not_doubled() {
2312 assert_eq!(
2313 xref_title_and_target(":py:func:`mymod.myfunc()`", true),
2314 ("mymod.myfunc()".to_string(), "mymod.myfunc".to_string()),
2315 "P3"
2316 );
2317 }
2318
2319 #[test]
2322 fn add_function_parentheses_false_removes_the_parens_the_author_wrote() {
2323 assert_eq!(
2324 xref_title_and_target(":py:func:`mymod.myfunc`", false),
2325 ("mymod.myfunc".to_string(), "mymod.myfunc".to_string()),
2326 "P2"
2327 );
2328 assert_eq!(
2329 xref_title_and_target(":py:func:`mymod.myfunc()`", false),
2330 ("mymod.myfunc".to_string(), "mymod.myfunc".to_string()),
2331 "P4"
2332 );
2333 assert_eq!(
2334 xref_title_and_target(":py:meth:`Obj.method()`", false),
2335 ("Obj.method".to_string(), "Obj.method".to_string()),
2336 "Q7"
2337 );
2338 }
2339
2340 #[test]
2343 fn an_explicit_title_is_untouched_under_both_settings() {
2344 for add_parens in [true, false] {
2345 assert_eq!(
2346 xref_title_and_target(":py:func:`custom title <mymod.myfunc>`", add_parens),
2347 ("custom title".to_string(), "mymod.myfunc".to_string()),
2348 "P5, add_function_parentheses={add_parens}"
2349 );
2350 assert_eq!(
2351 xref_title_and_target(":py:func:`other() <mymod.myfunc>`", add_parens),
2352 ("other()".to_string(), "mymod.myfunc".to_string()),
2353 "Q6: an explicit title keeps parens it wrote itself"
2354 );
2355 }
2356 }
2357
2358 #[test]
2362 fn the_target_always_loses_one_trailing_paren_pair() {
2363 for add_parens in [true, false] {
2364 assert_eq!(
2365 xref_title_and_target(":py:func:`custom title <mymod.myfunc()>`", add_parens).1,
2366 "mymod.myfunc",
2367 "Q4/Q5, add_function_parentheses={add_parens}"
2368 );
2369 }
2370 assert_eq!(
2371 xref_title_and_target(":py:func:`mymod.myfunc()()`", true),
2372 ("mymod.myfunc()()".to_string(), "mymod.myfunc()".to_string()),
2373 "Q10: one pair off the target, and a title already ending in \
2374 `()` gains nothing"
2375 );
2376 assert_eq!(
2377 xref_title_and_target(":py:func:`mymod.myfunc()()`", false),
2378 ("mymod.myfunc()".to_string(), "mymod.myfunc()".to_string()),
2379 "Q11: one pair off each"
2380 );
2381 }
2382
2383 #[test]
2386 fn a_py_role_without_fix_parens_keeps_its_parens() {
2387 for (text, expected) in [
2388 (":py:obj:`mymod.thing()`", "mymod.thing()"),
2389 (":py:data:`mymod.thing()`", "mymod.thing()"),
2390 ] {
2391 assert_eq!(
2392 xref_title_and_target(text, true),
2393 (expected.to_string(), expected.to_string()),
2394 "{text}"
2395 );
2396 }
2397 }
2398
2399 #[test]
2404 fn the_tilde_shortening_sees_the_title_fix_parens_already_produced() {
2405 assert_eq!(
2406 xref_title_and_target(":py:func:`~mymod.myfunc`", true),
2407 ("myfunc()".to_string(), "mymod.myfunc".to_string()),
2408 "Q1"
2409 );
2410 assert_eq!(
2411 xref_title_and_target(":py:func:`~mymod.myfunc()`", true),
2412 ("myfunc()".to_string(), "mymod.myfunc".to_string()),
2413 "Q2"
2414 );
2415 assert_eq!(
2416 xref_title_and_target(":py:func:`~mymod.myfunc()`", false),
2417 ("myfunc".to_string(), "mymod.myfunc".to_string()),
2418 "Q3"
2419 );
2420 }
2421
2422 #[test]
2431 fn a_py_xref_outside_any_scope_stamps_the_none_sentinels() {
2432 let nodes = sphinx_nodes(":py:func:`target`");
2433 let xref = &nodes[0];
2434 assert_eq!(attr(xref, "py:class"), Some(&AttrValue::Str("True".into())));
2435 assert_eq!(
2436 attr(xref, "py:module"),
2437 Some(&AttrValue::Str("True".into()))
2438 );
2439 assert_eq!(attr(xref, "refspecific"), None, "absent, not 0");
2440 }
2441
2442 #[test]
2445 fn a_dot_prefixed_target_becomes_refspecific() {
2446 let nodes = sphinx_nodes(":py:meth:`.Cls.meth`");
2447 let xref = &nodes[0];
2448 assert_eq!(attr(xref, "refspecific"), Some(&AttrValue::Int(1)));
2449 assert_eq!(
2450 attr(xref, "reftarget"),
2451 Some(&AttrValue::Str("Cls.meth".into()))
2452 );
2453 assert_eq!(xref.astext(), "Cls.meth()");
2454
2455 let nodes = sphinx_nodes(":py:meth:`M <.Cls.meth>`");
2459 let xref = &nodes[0];
2460 assert_eq!(attr(xref, "refspecific"), Some(&AttrValue::Int(1)));
2461 assert_eq!(
2462 attr(xref, "reftarget"),
2463 Some(&AttrValue::Str("Cls.meth".into()))
2464 );
2465 assert_eq!(xref.astext(), "M");
2466 }
2467
2468 #[test]
2472 fn title_lstrips_all_dots_while_the_target_loses_one() {
2473 let nodes = sphinx_nodes(":py:func:`..pkg.f`");
2474 let xref = &nodes[0];
2475 assert_eq!(xref.astext(), "pkg.f()");
2476 assert_eq!(
2477 attr(xref, "reftarget"),
2478 Some(&AttrValue::Str(".pkg.f".into()))
2479 );
2480 assert_eq!(attr(xref, "refspecific"), Some(&AttrValue::Int(1)));
2481 }
2482
2483 #[test]
2487 fn the_target_lstrips_every_tilde() {
2488 let nodes = sphinx_nodes(":py:func:`~~pkg.f`");
2489 let xref = &nodes[0];
2490 assert_eq!(xref.astext(), "f()");
2491 assert_eq!(
2492 attr(xref, "reftarget"),
2493 Some(&AttrValue::Str("pkg.f".into()))
2494 );
2495 assert_eq!(attr(xref, "refspecific"), None);
2496 }
2497
2498 #[test]
2502 fn the_deco_role_prefixes_an_at_sign() {
2503 let nodes = sphinx_nodes(":py:deco:`mydeco`");
2504 let xref = &nodes[0];
2505 assert_eq!(xref.astext(), "@mydeco");
2506 assert_eq!(
2507 attr(xref, "reftarget"),
2508 Some(&AttrValue::Str("mydeco".into()))
2509 );
2510 let inner = &xref.children[0];
2511 assert_eq!(inner.kind, kinds::LITERAL);
2512 assert_eq!(
2513 inner.attrs.classes,
2514 vec!["xref".to_string(), "py".to_string(), "py-deco".to_string()]
2515 );
2516
2517 let nodes = sphinx_nodes(":py:deco:`custom <target>`");
2518 let xref = &nodes[0];
2519 assert_eq!(xref.astext(), "@custom");
2520 assert_eq!(
2521 attr(xref, "reftarget"),
2522 Some(&AttrValue::Str("target".into()))
2523 );
2524 }
2525
2526 #[test]
2529 fn a_py_xref_inside_a_scope_stamps_the_ref_context() {
2530 let py = crate::py::PySigConfig::default();
2531 let nodes = sphinx_nodes_in(
2532 ":py:func:`target`",
2533 RefContext {
2534 py_module: Some("mymod"),
2535 py_class: Some("C"),
2536 ..RefContext::default()
2537 },
2538 &py,
2539 );
2540 let xref = &nodes[0];
2541 assert_eq!(attr(xref, "py:class"), Some(&AttrValue::Str("C".into())));
2542 assert_eq!(
2543 attr(xref, "py:module"),
2544 Some(&AttrValue::Str("mymod".into()))
2545 );
2546 }
2547
2548 #[test]
2555 fn an_any_role_emits_a_domainless_pending_xref() {
2556 let nodes = sphinx_nodes(":any:`target`");
2557 let xref = &nodes[0];
2558 assert_eq!(xref.kind, "pending_xref");
2559 assert_eq!(attr(xref, "refdomain"), Some(&AttrValue::Str("".into())));
2560 assert_eq!(attr(xref, "reftype"), Some(&AttrValue::Str("any".into())));
2561 assert_eq!(attr(xref, "refwarn"), Some(&AttrValue::Int(1)));
2562 assert_eq!(
2563 attr(xref, "reftarget"),
2564 Some(&AttrValue::Str("target".into()))
2565 );
2566 for absent in [
2567 "py:class",
2568 "py:classes",
2569 "py:module",
2570 "py:modules",
2571 "std:program",
2572 ] {
2573 assert_eq!(attr(xref, absent), None, "{absent} must not be stamped");
2574 }
2575 let inner = &xref.children[0];
2576 assert_eq!(inner.kind, kinds::LITERAL);
2577 assert_eq!(
2578 inner.attrs.classes,
2579 vec!["xref".to_string(), "any".to_string()]
2580 );
2581 }
2582
2583 #[test]
2587 fn an_any_role_copies_the_ref_context_keys_that_exist() {
2588 let py = crate::py::PySigConfig::default();
2589 let nodes = sphinx_nodes_in(
2590 ":any:`target`",
2591 RefContext {
2592 program: Some("prog"),
2593 py_module: Some("m"),
2594 py_class: None,
2595 py_class_key: true,
2596 py_module_key: true,
2597 py_classes_key: true,
2598 py_modules_key: false,
2599 },
2600 &py,
2601 );
2602 let xref = &nodes[0];
2603 assert_eq!(attr(xref, "py:class"), Some(&AttrValue::Str("True".into())));
2604 assert_eq!(attr(xref, "py:classes"), Some(&AttrValue::Str("".into())));
2605 assert_eq!(attr(xref, "py:module"), Some(&AttrValue::Str("m".into())));
2606 assert_eq!(attr(xref, "py:modules"), None);
2607 assert_eq!(
2608 attr(xref, "std:program"),
2609 Some(&AttrValue::Str("prog".into()))
2610 );
2611 }
2612
2613 #[test]
2622 fn an_any_role_stamps_a_present_but_none_py_module() {
2623 let py = crate::py::PySigConfig::default();
2624 let nodes = sphinx_nodes_in(
2625 ":any:`target`",
2626 RefContext {
2627 program: None,
2628 py_module: None,
2629 py_class: None,
2630 py_class_key: true,
2631 py_module_key: true,
2632 py_classes_key: true,
2633 py_modules_key: true,
2634 },
2635 &py,
2636 );
2637 let xref = &nodes[0];
2638 assert_eq!(
2639 attr(xref, "py:module"),
2640 Some(&AttrValue::Str("True".into()))
2641 );
2642 assert_eq!(attr(xref, "py:modules"), Some(&AttrValue::Str("".into())));
2643 }
2644
2645 #[test]
2647 fn an_any_role_omits_an_absent_py_module() {
2648 let py = crate::py::PySigConfig::default();
2649 let nodes = sphinx_nodes_in(
2650 ":any:`target`",
2651 RefContext {
2652 program: None,
2653 py_module: None,
2654 py_class: None,
2655 py_class_key: false,
2656 py_module_key: false,
2657 py_classes_key: false,
2658 py_modules_key: false,
2659 },
2660 &py,
2661 );
2662 assert_eq!(attr(&nodes[0], "py:module"), None);
2663 }
2664
2665 #[test]
2669 fn an_any_target_collapses_whitespace_and_nothing_else() {
2670 let nodes = sphinx_nodes(":any:`Foo()`");
2671 assert_eq!(
2672 attr(&nodes[0], "reftarget"),
2673 Some(&AttrValue::Str("Foo()".into()))
2674 );
2675 let nodes = sphinx_nodes(":any:`two\nlines`");
2676 assert_eq!(
2677 attr(&nodes[0], "reftarget"),
2678 Some(&AttrValue::Str("two lines".into()))
2679 );
2680 let nodes = sphinx_nodes(":any:`Click <target>`");
2681 let xref = &nodes[0];
2682 assert_eq!(attr(xref, "refexplicit"), Some(&AttrValue::Int(1)));
2683 assert_eq!(
2684 attr(xref, "reftarget"),
2685 Some(&AttrValue::Str("target".into()))
2686 );
2687 assert_eq!(xref.astext(), "Click");
2688 }
2689}