1use std::collections::{HashMap, HashSet};
10
11use unicode_normalization::UnicodeNormalization;
12
13use super::messages;
14use super::Node;
15use crate::utils::py_isspace;
16
17fn translate_digraph(c: char) -> Option<&'static str> {
19 Some(match c as u32 {
20 223 => "sz", 230 => "ae", 339 => "oe", 568 => "db", 569 => "qp", _ => return None,
26 })
27}
28
29fn translate_single(c: char) -> Option<char> {
31 Some(match c as u32 {
32 248 => 'o', 273 => 'd', 295 => 'h', 305 => 'i', 322 => 'l', 359 => 't', 384 => 'b', 387 => 'b', 392 => 'c', 396 => 'd', 402 => 'f', 409 => 'k', 410 => 'l', 414 => 'n', 421 => 'p', 427 => 't', 429 => 't', 436 => 'y', 438 => 'z', 485 => 'g', 549 => 'z', 564 => 'l', 565 => 'n', 566 => 't', 567 => 'j', 572 => 'c', 575 => 's', 576 => 'z', 583 => 'e', 585 => 'j', 587 => 'q', 589 => 'r', 591 => 'y', _ => return None,
66 })
67}
68
69pub fn make_id(s: &str) -> String {
71 let lowered = s.to_lowercase();
73 let mut translated = String::with_capacity(lowered.len());
75 for c in lowered.chars() {
76 if let Some(d) = translate_digraph(c) {
77 translated.push_str(d);
78 } else if let Some(r) = translate_single(c) {
79 translated.push(r);
80 } else {
81 translated.push(c);
82 }
83 }
84 let ascii: String = translated.nfkd().filter(char::is_ascii).collect();
86 let collapsed = py_split_join(&ascii);
89 let mut out = String::with_capacity(collapsed.len());
91 let mut in_run = false;
92 for c in collapsed.chars() {
93 if c.is_ascii_lowercase() || c.is_ascii_digit() {
94 out.push(c);
95 in_run = false;
96 } else if !in_run {
97 out.push('-');
98 in_run = true;
99 }
100 }
101 let bytes = out.as_bytes();
103 let mut start = 0;
104 while start < bytes.len() && (bytes[start] == b'-' || bytes[start].is_ascii_digit()) {
105 start += 1;
106 }
107 let mut end = bytes.len();
108 while end > start && bytes[end - 1] == b'-' {
109 end -= 1;
110 }
111 out[start..end].to_string()
112}
113
114pub fn sphinx_make_id(s: &str) -> String {
123 let mut translated = String::with_capacity(s.len());
125 for c in s.chars() {
126 if let Some(d) = translate_digraph(c) {
127 translated.push_str(d);
128 } else if let Some(r) = translate_single(c) {
129 translated.push(r);
130 } else {
131 translated.push(c);
132 }
133 }
134 let ascii: String = translated.nfkd().filter(char::is_ascii).collect();
136 let collapsed = py_split_join(&ascii);
138 let mut out = String::with_capacity(collapsed.len());
140 let mut in_run = false;
141 for c in collapsed.chars() {
142 if c.is_ascii_alphanumeric() || c == '.' || c == '_' {
143 out.push(c);
144 in_run = false;
145 } else if !in_run {
146 out.push('-');
147 in_run = true;
148 }
149 }
150 let bytes = out.as_bytes();
152 let mut start = 0;
153 while start < bytes.len() && matches!(bytes[start], b'-' | b'.' | b'_' | b'0'..=b'9') {
154 start += 1;
155 }
156 let mut end = bytes.len();
157 while end > start && bytes[end - 1] == b'-' {
158 end -= 1;
159 }
160 out[start..end].to_string()
161}
162
163fn py_split_join(s: &str) -> String {
170 s.split(py_isspace)
171 .filter(|w| !w.is_empty())
172 .collect::<Vec<_>>()
173 .join(" ")
174}
175
176pub fn fully_normalize_name(s: &str) -> String {
178 py_split_join(&s.to_lowercase())
179}
180
181pub fn whitespace_normalize_name(s: &str) -> String {
183 py_split_join(s)
184}
185
186#[derive(Debug, Clone, PartialEq, Eq)]
191pub struct DupnameFixup {
192 pub name: String,
193 pub node_id: String,
194}
195
196#[derive(Debug, Clone)]
199struct NameEntry {
200 id: Option<String>,
202 explicit: bool,
203 refuri: Option<String>,
204}
205
206#[derive(Debug, Default)]
207pub struct IdRegistry {
208 ids: HashSet<String>,
209 nameids: HashMap<String, NameEntry>,
210 id_counter: HashMap<&'static str, u64>,
212 fixups: Vec<DupnameFixup>,
213 index_serial: u32,
216 serialnos: HashMap<String, u32>,
223}
224
225impl IdRegistry {
226 pub fn new() -> Self {
227 Self::default()
228 }
229
230 pub fn new_index_serialno(&mut self) -> u32 {
233 let n = self.index_serial;
234 self.index_serial += 1;
235 n
236 }
237
238 pub fn sphinx_make_id(&mut self, prefix: &str, term: &str) -> String {
256 let mut node_id = if term.is_empty() {
257 None
258 } else if prefix.is_empty() {
259 let candidate = sphinx_make_id(term);
260 (!candidate.is_empty()).then_some(candidate)
262 } else {
263 let candidate = sphinx_make_id(&format!("{prefix}-{term}"));
264 (candidate != prefix).then_some(candidate)
266 };
267 loop {
268 match &node_id {
269 Some(id) if !self.ids.contains(id) => return id.clone(),
270 _ => {}
271 }
272 let counter = self.serialnos.entry(prefix.to_string()).or_insert(0);
273 let serial = *counter;
274 *counter += 1;
275 node_id = Some(if prefix.is_empty() {
276 format!("id{serial}")
277 } else {
278 format!("{prefix}-{serial}")
279 });
280 }
281 }
282
283 pub fn note_explicit_id(&mut self, id: &str) {
288 self.ids.insert(id.to_string());
289 }
290
291 fn allocate_id(&mut self, names: &[String]) -> String {
294 for name in names {
295 let base = make_id(name);
296 if !base.is_empty() && !self.ids.contains(&base) {
297 self.ids.insert(base.clone());
298 return base;
299 }
300 }
301 loop {
302 let counter = self.id_counter.entry("id").or_insert(0);
303 *counter += 1;
304 let id = format!("id{counter}");
305 if !self.ids.contains(&id) {
306 self.ids.insert(id.clone());
307 return id;
308 }
309 }
310 }
311
312 fn dupname_new(node: &mut Node, name: &str) {
313 if let Some(pos) = node.attrs.names.iter().position(|n| n == name) {
314 node.attrs.names.remove(pos);
315 node.attrs.dupnames.push(name.to_string());
316 }
317 }
318
319 fn register(
328 &mut self,
329 node: &mut Node,
330 line: u32,
331 source: &str,
332 explicit: bool,
333 backrefs_on_msg: bool,
334 refuri: Option<&str>,
335 ) -> Option<Node> {
336 let id = self.allocate_id(&node.attrs.names);
337 node.attrs.ids.push(id.clone());
338
339 let mut message = None;
340 let names = node.attrs.names.clone();
341 for name in names {
342 let Some(entry) = self.nameids.get(&name).cloned() else {
343 self.nameids.insert(
344 name,
345 NameEntry {
346 id: Some(id.clone()),
347 explicit,
348 refuri: refuri.map(str::to_string),
349 },
350 );
351 continue;
352 };
353 let dup_info = |level: u8, text: String, with_backrefs: bool| {
354 let mut msg = messages::system_message(level, &text, line, source);
355 if with_backrefs {
356 msg.attrs.backrefs.push(id.clone());
357 }
358 msg
359 };
360 match (entry.explicit, explicit) {
361 (true, true) => {
362 if refuri.is_some() && entry.refuri.as_deref() == refuri {
363 Self::dupname_new(node, &name);
365 continue;
366 }
367 if let Some(old_id) = entry.id.clone() {
368 self.fixups.push(DupnameFixup {
369 name: name.clone(),
370 node_id: old_id,
371 });
372 }
373 self.nameids.insert(
374 name.clone(),
375 NameEntry {
376 id: None,
377 explicit: true,
378 refuri: None,
379 },
380 );
381 Self::dupname_new(node, &name);
382 message = Some(dup_info(
383 messages::WARNING,
384 format!("Duplicate explicit target name: \"{name}\"."),
385 backrefs_on_msg,
386 ));
387 }
388 (true, false) => {
389 Self::dupname_new(node, &name);
391 message = Some(dup_info(
392 messages::INFO,
393 format!("Duplicate implicit target name: \"{name}\"."),
394 backrefs_on_msg,
395 ));
396 }
397 (false, true) => {
398 if let Some(old_id) = entry.id.clone() {
400 self.fixups.push(DupnameFixup {
401 name: name.clone(),
402 node_id: old_id,
403 });
404 }
405 self.nameids.insert(
406 name.clone(),
407 NameEntry {
408 id: Some(id.clone()),
409 explicit: true,
410 refuri: refuri.map(str::to_string),
411 },
412 );
413 message = Some(dup_info(
414 messages::INFO,
415 format!("Target name overrides implicit target name \"{name}\"."),
416 false,
417 ));
418 }
419 (false, false) => {
420 if let Some(old_id) = entry.id.clone() {
421 self.fixups.push(DupnameFixup {
422 name: name.clone(),
423 node_id: old_id,
424 });
425 }
426 self.nameids.insert(
427 name.clone(),
428 NameEntry {
429 id: None,
430 explicit: false,
431 refuri: None,
432 },
433 );
434 Self::dupname_new(node, &name);
435 message = Some(dup_info(
436 messages::INFO,
437 format!("Duplicate implicit target name: \"{name}\"."),
438 backrefs_on_msg,
439 ));
440 }
441 }
442 }
443 message
444 }
445
446 pub fn set_id_implicit(&mut self, node: &mut Node, line: u32, source: &str) -> Option<Node> {
451 self.register(node, line, source, false, true, None)
452 }
453
454 pub fn set_id_explicit(
458 &mut self,
459 node: &mut Node,
460 line: u32,
461 source: &str,
462 internal: bool,
463 refuri: Option<&str>,
464 ) -> Option<Node> {
465 self.register(node, line, source, true, internal, refuri)
466 }
467
468 pub fn set_id_anonymous(&mut self, node: &mut Node) {
470 let id = self.allocate_id(&[]);
471 node.attrs.ids.push(id);
472 }
473
474 pub fn allocate_auto_id(&mut self) -> String {
477 self.allocate_id(&[])
478 }
479
480 pub fn take_fixups(&mut self) -> Vec<DupnameFixup> {
481 std::mem::take(&mut self.fixups)
482 }
483
484 pub fn nameids_snapshot(&self) -> Vec<(String, Option<String>, bool)> {
489 self.nameids
490 .iter()
491 .map(|(name, entry)| (name.clone(), entry.id.clone(), entry.explicit))
492 .collect()
493 }
494
495 pub fn index_serial(&self) -> u32 {
498 self.index_serial
499 }
500}
501
502pub fn apply_dupname_fixups(root: &mut Node, fixups: &[DupnameFixup]) {
505 if fixups.is_empty() {
506 return;
507 }
508 for fixup in fixups {
509 apply_one_fixup(root, fixup);
510 }
511}
512
513fn apply_one_fixup(node: &mut Node, fixup: &DupnameFixup) -> bool {
514 if node.attrs.ids.contains(&fixup.node_id) {
515 if let Some(pos) = node.attrs.names.iter().position(|n| *n == fixup.name) {
516 node.attrs.names.remove(pos);
517 node.attrs.dupnames.push(fixup.name.clone());
518 }
519 return true;
520 }
521 node.children
522 .iter_mut()
523 .any(|child| apply_one_fixup(child, fixup))
524}
525
526#[cfg(test)]
527mod tests {
528 use super::*;
529 use crate::doctree::{kinds, AttrValue, Node, Span};
530
531 #[test]
538 fn name_normalizers_split_on_python_whitespace() {
539 assert_eq!(fully_normalize_name("a\x1fb"), "a b");
540 assert_eq!(fully_normalize_name("Sec\x1fC"), "sec c");
541 assert_eq!(fully_normalize_name("\x1f a \x1c\x1d\x1e b \x1f"), "a b");
542 assert_eq!(whitespace_normalize_name("A\x1fB"), "A B");
543 assert_eq!(whitespace_normalize_name("\x1fA B\x1f"), "A B");
544 assert_eq!(make_id("a\x1fb"), "a-b");
545 assert_eq!(make_id("\x1fa\x1f"), "a");
546 assert_eq!(sphinx_make_id("envvar-FOO\x1fBAR"), "envvar-FOO-BAR");
547 assert_eq!(sphinx_make_id("\x1fA.b\x1f"), "A.b");
548 }
549
550 #[test]
551 fn make_id_basics() {
552 assert_eq!(make_id("My Section Title!"), "my-section-title");
553 assert_eq!(make_id("Hello World!"), "hello-world");
554 assert_eq!(make_id("1. Intro"), "intro");
555 assert_eq!(make_id("2026 report"), "report");
556 assert_eq!(make_id("Überblick"), "uberblick");
557 assert_eq!(make_id("straße"), "strasze");
558 assert_eq!(make_id("!!!"), "");
559 assert_eq!(make_id("123"), "");
560 assert_eq!(make_id("..."), "");
561 }
562
563 #[test]
567 fn sphinx_make_id_keeps_case_dots_and_underscores() {
568 assert_eq!(sphinx_make_id("envvar-HOME_A"), "envvar-HOME_A");
569 assert_eq!(sphinx_make_id("confval-my_setting"), "confval-my_setting");
570 assert_eq!(sphinx_make_id("a.b.C"), "a.b.C");
571 assert_eq!(
573 sphinx_make_id("cmdoption-myprog---verbose"),
574 "cmdoption-myprog-verbose"
575 );
576 assert_eq!(
577 sphinx_make_id("term-source directory"),
578 "term-source-directory"
579 );
580 assert_eq!(sphinx_make_id("._-1abc--"), "abc");
582 assert_eq!(sphinx_make_id("Überblick"), "Uberblick");
583 assert_eq!(sphinx_make_id("!!!"), "");
584 }
585
586 #[test]
587 fn sphinx_registry_make_id_falls_back_to_a_per_prefix_serial() {
588 let mut reg = IdRegistry::new();
589 assert_eq!(reg.sphinx_make_id("envvar", "HOME"), "envvar-HOME");
590 assert_eq!(reg.sphinx_make_id("envvar", "HOME"), "envvar-HOME");
592 reg.note_explicit_id("envvar-HOME");
593 assert_eq!(reg.sphinx_make_id("envvar", "HOME"), "envvar-0");
594 reg.note_explicit_id("envvar-0");
595 assert_eq!(reg.sphinx_make_id("envvar", "HOME"), "envvar-1");
596 assert_eq!(reg.sphinx_make_id("cmdoption", "!!!"), "cmdoption-0");
600 }
601
602 #[test]
608 fn sphinx_registry_make_id_empty_prefix_keeps_fullname_and_serials_as_id_n() {
609 let mut reg = IdRegistry::new();
610 assert_eq!(reg.sphinx_make_id("", "mymod.C.meth"), "mymod.C.meth");
611 reg.note_explicit_id("mymod.C.meth");
612 assert_eq!(reg.sphinx_make_id("", "mymod.C.meth"), "id0");
613 reg.note_explicit_id("id0");
614 assert_eq!(reg.sphinx_make_id("", "mymod.C.meth"), "id1");
615 reg.note_explicit_id("id1");
618 assert_eq!(reg.sphinx_make_id("", "!!!"), "id2");
619 reg.note_explicit_id("id2");
621 assert_eq!(reg.sphinx_make_id("", ""), "id3");
622 }
623
624 #[test]
625 fn name_normalization() {
626 assert_eq!(
627 fully_normalize_name("My Phrase Target"),
628 "my phrase target"
629 );
630 assert_eq!(fully_normalize_name("Hello World!"), "hello world!");
631 assert_eq!(fully_normalize_name("Überblick"), "überblick");
632 assert_eq!(whitespace_normalize_name("A B"), "A B");
633 }
634
635 #[test]
636 fn registry_assigns_ids_and_handles_implicit_duplicates() {
637 let mut reg = IdRegistry::new();
638 let mut s1 = Node::elem(kinds::SECTION, Span::ZERO);
639 s1.attrs.names.push("duplicate".into());
640 assert!(reg.set_id_implicit(&mut s1, 3, "<snippet>").is_none());
641 assert_eq!(s1.attrs.ids, vec!["duplicate"]);
642
643 let mut s2 = Node::elem(kinds::SECTION, Span::ZERO);
644 s2.attrs.names.push("duplicate".into());
645 let msg = reg
646 .set_id_implicit(&mut s2, 7, "<snippet>")
647 .expect("dup INFO");
648 assert_eq!(s2.attrs.ids, vec!["id1"]);
649 assert!(s2.attrs.names.is_empty());
650 assert_eq!(s2.attrs.dupnames, vec!["duplicate"]);
651 assert_eq!(msg.get("type"), Some(&AttrValue::Str("INFO".into())));
652 assert_eq!(msg.get("line"), Some(&AttrValue::Int(7)));
653 assert_eq!(msg.attrs.backrefs, vec!["id1"]);
654
655 let fixups = reg.take_fixups();
657 assert_eq!(
658 fixups,
659 vec![DupnameFixup {
660 name: "duplicate".into(),
661 node_id: "duplicate".into()
662 }]
663 );
664 let mut root = Node::elem(kinds::DOCUMENT, Span::ZERO);
665 root.children.push(s1);
666 apply_dupname_fixups(&mut root, &fixups);
667 let s1 = &root.children[0];
668 assert!(s1.attrs.names.is_empty());
669 assert_eq!(s1.attrs.dupnames, vec!["duplicate"]);
670 assert_eq!(s1.attrs.ids, vec!["duplicate"]); }
672
673 #[test]
674 fn registry_auto_ids_for_unmakeable_names() {
675 let mut reg = IdRegistry::new();
676 for (i, title) in ["!!!", "123", "..."].iter().enumerate() {
677 let mut s = Node::elem(kinds::SECTION, Span::ZERO);
678 s.attrs.names.push(fully_normalize_name(title));
679 reg.set_id_implicit(&mut s, 1, "<snippet>");
680 assert_eq!(s.attrs.ids, vec![format!("id{}", i + 1)]);
681 assert_eq!(s.attrs.names.len(), 1); }
683 }
684
685 #[test]
686 fn explicit_duplicate_warning_backrefs_only_when_internal() {
687 let mut reg = IdRegistry::new();
688 let mut t1 = Node::elem(kinds::TARGET, Span::ZERO);
689 t1.attrs.names.push("dup".into());
690 assert!(reg
691 .set_id_explicit(&mut t1, 1, "<snippet>", false, Some("https://1/"))
692 .is_none());
693
694 let mut t2 = Node::elem(kinds::TARGET, Span::ZERO);
695 t2.attrs.names.push("dup".into());
696 let msg = reg
697 .set_id_explicit(&mut t2, 3, "<snippet>", false, Some("https://2/"))
698 .expect("dup WARNING");
699 assert_eq!(msg.get("type"), Some(&AttrValue::Str("WARNING".into())));
700 assert!(msg.attrs.backrefs.is_empty()); assert_eq!(t2.attrs.ids, vec!["id1"]);
702 assert_eq!(t2.attrs.dupnames, vec!["dup"]);
703
704 let mut reg = IdRegistry::new();
705 let mut i1 = Node::elem(kinds::TARGET, Span::ZERO);
706 i1.attrs.names.push("t".into());
707 reg.set_id_explicit(&mut i1, 1, "<snippet>", true, None);
708 let mut i2 = Node::elem(kinds::TARGET, Span::ZERO);
709 i2.attrs.names.push("t".into());
710 let msg = reg
711 .set_id_explicit(&mut i2, 5, "<snippet>", true, None)
712 .expect("dup WARNING");
713 assert_eq!(msg.attrs.backrefs, vec!["id1"]); }
715}