1use std::collections::{HashMap, HashSet};
10
11use unicode_normalization::UnicodeNormalization;
12
13use super::messages;
14use super::Node;
15
16fn translate_digraph(c: char) -> Option<&'static str> {
18 Some(match c as u32 {
19 223 => "sz", 230 => "ae", 339 => "oe", 568 => "db", 569 => "qp", _ => return None,
25 })
26}
27
28fn translate_single(c: char) -> Option<char> {
30 Some(match c as u32 {
31 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,
65 })
66}
67
68pub fn make_id(s: &str) -> String {
70 let lowered = s.to_lowercase();
72 let mut translated = String::with_capacity(lowered.len());
74 for c in lowered.chars() {
75 if let Some(d) = translate_digraph(c) {
76 translated.push_str(d);
77 } else if let Some(r) = translate_single(c) {
78 translated.push(r);
79 } else {
80 translated.push(c);
81 }
82 }
83 let ascii: String = translated.nfkd().filter(char::is_ascii).collect();
85 let collapsed = ascii.split_whitespace().collect::<Vec<_>>().join(" ");
87 let mut out = String::with_capacity(collapsed.len());
89 let mut in_run = false;
90 for c in collapsed.chars() {
91 if c.is_ascii_lowercase() || c.is_ascii_digit() {
92 out.push(c);
93 in_run = false;
94 } else if !in_run {
95 out.push('-');
96 in_run = true;
97 }
98 }
99 let bytes = out.as_bytes();
101 let mut start = 0;
102 while start < bytes.len() && (bytes[start] == b'-' || bytes[start].is_ascii_digit()) {
103 start += 1;
104 }
105 let mut end = bytes.len();
106 while end > start && bytes[end - 1] == b'-' {
107 end -= 1;
108 }
109 out[start..end].to_string()
110}
111
112pub fn fully_normalize_name(s: &str) -> String {
114 s.to_lowercase()
115 .split_whitespace()
116 .collect::<Vec<_>>()
117 .join(" ")
118}
119
120pub fn whitespace_normalize_name(s: &str) -> String {
122 s.split_whitespace().collect::<Vec<_>>().join(" ")
123}
124
125#[derive(Debug, Clone, PartialEq, Eq)]
130pub struct DupnameFixup {
131 pub name: String,
132 pub node_id: String,
133}
134
135#[derive(Debug, Clone)]
138struct NameEntry {
139 id: Option<String>,
141 explicit: bool,
142 refuri: Option<String>,
143}
144
145#[derive(Debug, Default)]
146pub struct IdRegistry {
147 ids: HashSet<String>,
148 nameids: HashMap<String, NameEntry>,
149 id_counter: HashMap<&'static str, u64>,
151 fixups: Vec<DupnameFixup>,
152}
153
154impl IdRegistry {
155 pub fn new() -> Self {
156 Self::default()
157 }
158
159 fn allocate_id(&mut self, names: &[String]) -> String {
162 for name in names {
163 let base = make_id(name);
164 if !base.is_empty() && !self.ids.contains(&base) {
165 self.ids.insert(base.clone());
166 return base;
167 }
168 }
169 loop {
170 let counter = self.id_counter.entry("id").or_insert(0);
171 *counter += 1;
172 let id = format!("id{counter}");
173 if !self.ids.contains(&id) {
174 self.ids.insert(id.clone());
175 return id;
176 }
177 }
178 }
179
180 fn dupname_new(node: &mut Node, name: &str) {
181 if let Some(pos) = node.attrs.names.iter().position(|n| n == name) {
182 node.attrs.names.remove(pos);
183 node.attrs.dupnames.push(name.to_string());
184 }
185 }
186
187 fn register(
196 &mut self,
197 node: &mut Node,
198 line: u32,
199 source: &str,
200 explicit: bool,
201 backrefs_on_msg: bool,
202 refuri: Option<&str>,
203 ) -> Option<Node> {
204 let id = self.allocate_id(&node.attrs.names);
205 node.attrs.ids.push(id.clone());
206
207 let mut message = None;
208 let names = node.attrs.names.clone();
209 for name in names {
210 let Some(entry) = self.nameids.get(&name).cloned() else {
211 self.nameids.insert(
212 name,
213 NameEntry {
214 id: Some(id.clone()),
215 explicit,
216 refuri: refuri.map(str::to_string),
217 },
218 );
219 continue;
220 };
221 let dup_info = |level: u8, text: String, with_backrefs: bool| {
222 let mut msg = messages::system_message(level, &text, line, source);
223 if with_backrefs {
224 msg.attrs.backrefs.push(id.clone());
225 }
226 msg
227 };
228 match (entry.explicit, explicit) {
229 (true, true) => {
230 if refuri.is_some() && entry.refuri.as_deref() == refuri {
231 Self::dupname_new(node, &name);
233 continue;
234 }
235 if let Some(old_id) = entry.id.clone() {
236 self.fixups.push(DupnameFixup {
237 name: name.clone(),
238 node_id: old_id,
239 });
240 }
241 self.nameids.insert(
242 name.clone(),
243 NameEntry {
244 id: None,
245 explicit: true,
246 refuri: None,
247 },
248 );
249 Self::dupname_new(node, &name);
250 message = Some(dup_info(
251 messages::WARNING,
252 format!("Duplicate explicit target name: \"{name}\"."),
253 backrefs_on_msg,
254 ));
255 }
256 (true, false) => {
257 Self::dupname_new(node, &name);
259 message = Some(dup_info(
260 messages::INFO,
261 format!("Duplicate implicit target name: \"{name}\"."),
262 backrefs_on_msg,
263 ));
264 }
265 (false, true) => {
266 if let Some(old_id) = entry.id.clone() {
268 self.fixups.push(DupnameFixup {
269 name: name.clone(),
270 node_id: old_id,
271 });
272 }
273 self.nameids.insert(
274 name.clone(),
275 NameEntry {
276 id: Some(id.clone()),
277 explicit: true,
278 refuri: refuri.map(str::to_string),
279 },
280 );
281 message = Some(dup_info(
282 messages::INFO,
283 format!("Target name overrides implicit target name \"{name}\"."),
284 false,
285 ));
286 }
287 (false, false) => {
288 if let Some(old_id) = entry.id.clone() {
289 self.fixups.push(DupnameFixup {
290 name: name.clone(),
291 node_id: old_id,
292 });
293 }
294 self.nameids.insert(
295 name.clone(),
296 NameEntry {
297 id: None,
298 explicit: false,
299 refuri: None,
300 },
301 );
302 Self::dupname_new(node, &name);
303 message = Some(dup_info(
304 messages::INFO,
305 format!("Duplicate implicit target name: \"{name}\"."),
306 backrefs_on_msg,
307 ));
308 }
309 }
310 }
311 message
312 }
313
314 pub fn set_id_implicit(&mut self, node: &mut Node, line: u32, source: &str) -> Option<Node> {
319 self.register(node, line, source, false, true, None)
320 }
321
322 pub fn set_id_explicit(
326 &mut self,
327 node: &mut Node,
328 line: u32,
329 source: &str,
330 internal: bool,
331 refuri: Option<&str>,
332 ) -> Option<Node> {
333 self.register(node, line, source, true, internal, refuri)
334 }
335
336 pub fn set_id_anonymous(&mut self, node: &mut Node) {
338 let id = self.allocate_id(&[]);
339 node.attrs.ids.push(id);
340 }
341
342 pub fn take_fixups(&mut self) -> Vec<DupnameFixup> {
343 std::mem::take(&mut self.fixups)
344 }
345}
346
347pub fn apply_dupname_fixups(root: &mut Node, fixups: &[DupnameFixup]) {
350 if fixups.is_empty() {
351 return;
352 }
353 for fixup in fixups {
354 apply_one_fixup(root, fixup);
355 }
356}
357
358fn apply_one_fixup(node: &mut Node, fixup: &DupnameFixup) -> bool {
359 if node.attrs.ids.contains(&fixup.node_id) {
360 if let Some(pos) = node.attrs.names.iter().position(|n| *n == fixup.name) {
361 node.attrs.names.remove(pos);
362 node.attrs.dupnames.push(fixup.name.clone());
363 }
364 return true;
365 }
366 node.children
367 .iter_mut()
368 .any(|child| apply_one_fixup(child, fixup))
369}
370
371#[cfg(test)]
372mod tests {
373 use super::*;
374 use crate::doctree::{kinds, AttrValue, Node, Span};
375
376 #[test]
377 fn make_id_basics() {
378 assert_eq!(make_id("My Section Title!"), "my-section-title");
379 assert_eq!(make_id("Hello World!"), "hello-world");
380 assert_eq!(make_id("1. Intro"), "intro");
381 assert_eq!(make_id("2026 report"), "report");
382 assert_eq!(make_id("Überblick"), "uberblick");
383 assert_eq!(make_id("straße"), "strasze");
384 assert_eq!(make_id("!!!"), "");
385 assert_eq!(make_id("123"), "");
386 assert_eq!(make_id("..."), "");
387 }
388
389 #[test]
390 fn name_normalization() {
391 assert_eq!(
392 fully_normalize_name("My Phrase Target"),
393 "my phrase target"
394 );
395 assert_eq!(fully_normalize_name("Hello World!"), "hello world!");
396 assert_eq!(fully_normalize_name("Überblick"), "überblick");
397 assert_eq!(whitespace_normalize_name("A B"), "A B");
398 }
399
400 #[test]
401 fn registry_assigns_ids_and_handles_implicit_duplicates() {
402 let mut reg = IdRegistry::new();
403 let mut s1 = Node::elem(kinds::SECTION, Span::ZERO);
404 s1.attrs.names.push("duplicate".into());
405 assert!(reg.set_id_implicit(&mut s1, 3, "<snippet>").is_none());
406 assert_eq!(s1.attrs.ids, vec!["duplicate"]);
407
408 let mut s2 = Node::elem(kinds::SECTION, Span::ZERO);
409 s2.attrs.names.push("duplicate".into());
410 let msg = reg
411 .set_id_implicit(&mut s2, 7, "<snippet>")
412 .expect("dup INFO");
413 assert_eq!(s2.attrs.ids, vec!["id1"]);
414 assert!(s2.attrs.names.is_empty());
415 assert_eq!(s2.attrs.dupnames, vec!["duplicate"]);
416 assert_eq!(msg.get("type"), Some(&AttrValue::Str("INFO".into())));
417 assert_eq!(msg.get("line"), Some(&AttrValue::Int(7)));
418 assert_eq!(msg.attrs.backrefs, vec!["id1"]);
419
420 let fixups = reg.take_fixups();
422 assert_eq!(
423 fixups,
424 vec![DupnameFixup {
425 name: "duplicate".into(),
426 node_id: "duplicate".into()
427 }]
428 );
429 let mut root = Node::elem(kinds::DOCUMENT, Span::ZERO);
430 root.children.push(s1);
431 apply_dupname_fixups(&mut root, &fixups);
432 let s1 = &root.children[0];
433 assert!(s1.attrs.names.is_empty());
434 assert_eq!(s1.attrs.dupnames, vec!["duplicate"]);
435 assert_eq!(s1.attrs.ids, vec!["duplicate"]); }
437
438 #[test]
439 fn registry_auto_ids_for_unmakeable_names() {
440 let mut reg = IdRegistry::new();
441 for (i, title) in ["!!!", "123", "..."].iter().enumerate() {
442 let mut s = Node::elem(kinds::SECTION, Span::ZERO);
443 s.attrs.names.push(fully_normalize_name(title));
444 reg.set_id_implicit(&mut s, 1, "<snippet>");
445 assert_eq!(s.attrs.ids, vec![format!("id{}", i + 1)]);
446 assert_eq!(s.attrs.names.len(), 1); }
448 }
449
450 #[test]
451 fn explicit_duplicate_warning_backrefs_only_when_internal() {
452 let mut reg = IdRegistry::new();
453 let mut t1 = Node::elem(kinds::TARGET, Span::ZERO);
454 t1.attrs.names.push("dup".into());
455 assert!(reg
456 .set_id_explicit(&mut t1, 1, "<snippet>", false, Some("https://1/"))
457 .is_none());
458
459 let mut t2 = Node::elem(kinds::TARGET, Span::ZERO);
460 t2.attrs.names.push("dup".into());
461 let msg = reg
462 .set_id_explicit(&mut t2, 3, "<snippet>", false, Some("https://2/"))
463 .expect("dup WARNING");
464 assert_eq!(msg.get("type"), Some(&AttrValue::Str("WARNING".into())));
465 assert!(msg.attrs.backrefs.is_empty()); assert_eq!(t2.attrs.ids, vec!["id1"]);
467 assert_eq!(t2.attrs.dupnames, vec!["dup"]);
468
469 let mut reg = IdRegistry::new();
470 let mut i1 = Node::elem(kinds::TARGET, Span::ZERO);
471 i1.attrs.names.push("t".into());
472 reg.set_id_explicit(&mut i1, 1, "<snippet>", true, None);
473 let mut i2 = Node::elem(kinds::TARGET, Span::ZERO);
474 i2.attrs.names.push("t".into());
475 let msg = reg
476 .set_id_explicit(&mut i2, 5, "<snippet>", true, None)
477 .expect("dup WARNING");
478 assert_eq!(msg.attrs.backrefs, vec!["id1"]); }
480}