asciidoc_parser/document/
catalog.rs1use std::collections::HashMap;
2
3use crate::{content::FootnoteDeferred, internal::debug::DebugHashMapFrom, parser::XrefSignifier};
4
5#[derive(Clone, Eq, PartialEq)]
12pub struct Catalog {
13 pub(crate) refs: HashMap<String, RefEntry>,
15
16 pub(crate) reftext_to_id: HashMap<String, String>,
18
19 pub(crate) footnotes: Vec<Footnote>,
27}
28
29impl Default for Catalog {
30 fn default() -> Self {
31 Self::new()
32 }
33}
34
35impl Catalog {
36 pub(crate) fn new() -> Self {
37 Self {
38 refs: HashMap::new(),
39 reftext_to_id: HashMap::new(),
40 footnotes: Vec::new(),
41 }
42 }
43
44 pub(crate) fn register_ref(
55 &mut self,
56 id: &str,
57 reftext: Option<&str>,
58 ref_type: RefType,
59 ) -> Result<(), DuplicateIdError> {
60 if self.refs.contains_key(id) {
61 return Err(DuplicateIdError(id.to_string()));
62 }
63
64 let entry = RefEntry {
65 id: id.to_string(),
66 reftext: reftext.map(|s| s.to_owned()),
67 ref_type,
68 signifier: None,
69 };
70
71 self.refs.insert(id.to_string(), entry);
72
73 if let Some(reftext) = reftext {
74 self.reftext_to_id
75 .entry(reftext.to_string())
76 .or_insert_with(|| id.to_string());
77 }
78
79 Ok(())
80 }
81
82 pub(crate) fn generate_and_register_unique_id(
96 &mut self,
97 base_id: &str,
98 reftext: Option<&str>,
99 ref_type: RefType,
100 ) -> String {
101 let unique_id = if !self.contains_id(base_id) {
102 base_id.to_string()
103 } else {
104 let mut counter = 2;
105 loop {
106 let candidate = format!("{}-{}", base_id, counter);
107 if !self.contains_id(&candidate) {
108 break candidate;
109 }
110 counter += 1;
111 }
112 };
113
114 let entry = RefEntry {
116 id: unique_id.clone(),
117 reftext: reftext.map(|s| s.to_owned()),
118 ref_type,
119 signifier: None,
120 };
121
122 self.refs.insert(unique_id.clone(), entry);
123
124 if let Some(reftext) = reftext {
125 self.reftext_to_id
126 .entry(reftext.to_string())
127 .or_insert_with(|| unique_id.clone());
128 }
129
130 unique_id
131 }
132
133 pub fn get_ref(&self, id: &str) -> Option<&RefEntry> {
135 self.refs.get(id)
136 }
137
138 pub fn contains_id(&self, id: &str) -> bool {
140 self.refs.contains_key(id)
141 }
142
143 pub fn resolve_id(&self, reftext: &str) -> Option<String> {
145 self.reftext_to_id.get(reftext).cloned()
146 }
147
148 pub(crate) fn set_signifier(&mut self, id: &str, signifier: XrefSignifier) {
153 if let Some(entry) = self.refs.get_mut(id) {
154 entry.signifier = Some(signifier);
155 }
156 }
157
158 pub fn ids(&self) -> impl Iterator<Item = &str> {
165 self.refs.keys().map(String::as_str)
166 }
167
168 pub fn entries(&self) -> impl Iterator<Item = (&str, &RefEntry)> {
174 self.refs.iter().map(|(id, entry)| (id.as_str(), entry))
175 }
176
177 pub fn len(&self) -> usize {
179 self.refs.len()
180 }
181
182 pub fn is_empty(&self) -> bool {
184 self.refs.is_empty()
185 }
186
187 pub fn footnotes(&self) -> &[Footnote] {
189 &self.footnotes
190 }
191
192 pub(crate) fn register_footnote(&mut self, footnote: Footnote) {
194 self.footnotes.push(footnote);
195 }
196
197 pub(crate) fn footnote_with_id(&self, id: &str) -> Option<&Footnote> {
199 self.footnotes.iter().find(|f| f.id.as_deref() == Some(id))
200 }
201
202 pub(crate) fn take_footnotes(&mut self) -> Vec<Footnote> {
207 std::mem::take(&mut self.footnotes)
208 }
209
210 pub(crate) fn restore_footnotes(&mut self, footnotes: Vec<Footnote>) {
213 self.footnotes = footnotes;
214 }
215}
216
217#[derive(Clone, Eq, PartialEq)]
225pub struct Footnote {
226 pub index: String,
232
233 pub id: Option<String>,
237
238 pub text: String,
243
244 pub(crate) deferred: Option<Box<FootnoteDeferred>>,
248}
249
250impl Footnote {
251 pub(crate) fn resolve_references(
257 &mut self,
258 resolver: &dyn crate::parser::ReferenceResolver,
259 renderer: &dyn crate::parser::InlineSubstitutionRenderer,
260 warnings: &mut Vec<crate::parser::ReferenceWarning>,
261 ) {
262 if let Some(deferred) = self.deferred.as_mut() {
263 deferred.resolve(resolver, warnings);
264 self.text = deferred.render(renderer);
265 }
266 }
267}
268
269impl std::fmt::Debug for Footnote {
270 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
271 let mut s = f.debug_struct("Footnote");
275 s.field("index", &self.index);
276 s.field("id", &self.id);
277 s.field("text", &self.text);
278
279 if let Some(deferred) = self.deferred.as_ref() {
280 s.field("deferred", deferred);
281 }
282
283 s.finish()
284 }
285}
286
287impl std::fmt::Debug for Catalog {
288 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
289 f.debug_struct("Catalog")
290 .field("refs", &DebugHashMapFrom(&self.refs))
291 .field("reftext_to_id", &DebugHashMapFrom(&self.reftext_to_id))
292 .field("footnotes", &self.footnotes)
293 .finish()
294 }
295}
296#[derive(Clone, PartialEq, Eq)]
298pub enum RefType {
299 Anchor,
301
302 Section,
304
305 Bibliography,
307}
308
309impl std::fmt::Debug for RefType {
310 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
311 match self {
312 Self::Anchor => f.write_str("RefType::Anchor"),
313 Self::Section => f.write_str("RefType::Section"),
314 Self::Bibliography => f.write_str("RefType::Bibliography"),
315 }
316 }
317}
318
319#[derive(Clone, Debug, Eq, PartialEq)]
321pub struct RefEntry {
322 pub id: String,
324
325 pub reftext: Option<String>,
327
328 pub ref_type: RefType,
330
331 pub signifier: Option<XrefSignifier>,
338}
339
340#[derive(Clone, Debug, Eq, PartialEq)]
342pub(crate) struct DuplicateIdError(pub(crate) String);
343
344impl std::fmt::Display for DuplicateIdError {
345 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
346 write!(f, "ID '{}' already registered", self.0)
347 }
348}
349
350impl std::error::Error for DuplicateIdError {}
351
352#[cfg(test)]
353mod tests {
354 #![allow(clippy::unwrap_used)]
355
356 use super::*;
357
358 #[test]
359 fn new_catalog_is_empty() {
360 let catalog = Catalog::new();
361 assert!(catalog.is_empty());
362 assert_eq!(catalog.len(), 0);
363 }
364
365 #[test]
366 fn register_ref_success() {
367 let mut catalog = Catalog::new();
368
369 let result = catalog.register_ref("test-id", Some("Test Reference"), RefType::Anchor);
370
371 assert!(result.is_ok());
372 assert_eq!(catalog.len(), 1);
373 assert!(catalog.contains_id("test-id"));
374 }
375
376 #[test]
377 fn register_duplicate_id_fails() {
378 let mut catalog = Catalog::new();
379
380 catalog
382 .register_ref("test-id", Some("First"), RefType::Anchor)
383 .unwrap();
384
385 let result = catalog.register_ref("test-id", Some("Second"), RefType::Section);
387
388 let error = result.unwrap_err();
389 assert_eq!(error.0, "test-id");
390 }
391
392 #[test]
393 fn generate_and_register_unique_id() {
394 let mut catalog = Catalog::new();
395
396 let id1 = catalog.generate_and_register_unique_id(
398 "available",
399 Some("Available Ref"),
400 RefType::Anchor,
401 );
402 assert_eq!(id1, "available");
403 assert!(catalog.contains_id("available"));
404 assert_eq!(
405 catalog.resolve_id("Available Ref"),
406 Some("available".to_string())
407 );
408
409 catalog
411 .register_ref("taken", None, RefType::Anchor)
412 .unwrap();
413 catalog
414 .register_ref("taken-2", None, RefType::Anchor)
415 .unwrap();
416
417 let id2 = catalog.generate_and_register_unique_id("taken", None, RefType::Section);
418 assert_eq!(id2, "taken-3");
419 assert!(catalog.contains_id("taken-3"));
420 }
421
422 #[test]
423 fn get_ref() {
424 let mut catalog = Catalog::new();
425
426 catalog
427 .register_ref("test-id", Some("Test Reference"), RefType::Bibliography)
428 .unwrap();
429
430 let entry = catalog.get_ref("test-id").unwrap();
431 assert_eq!(entry.id, "test-id");
432 assert_eq!(entry.reftext, Some("Test Reference".to_string()));
433 assert_eq!(entry.ref_type, RefType::Bibliography);
434
435 assert!(catalog.get_ref("nonexistent").is_none());
436 }
437
438 #[test]
439 fn enumerate_ids_and_entries() {
440 let mut catalog = Catalog::new();
441
442 catalog
443 .register_ref("intro", Some("Introduction"), RefType::Section)
444 .unwrap();
445 catalog
446 .register_ref("fig-1", None, RefType::Anchor)
447 .unwrap();
448
449 let mut ids: Vec<&str> = catalog.ids().collect();
451 ids.sort_unstable();
452 assert_eq!(ids, vec!["fig-1", "intro"]);
453
454 let entries: Vec<(&str, &RefEntry)> = catalog.entries().collect();
456 assert_eq!(entries.len(), 2);
457
458 let (fig_id, fig_entry) = entries.iter().find(|(id, _)| *id == "fig-1").unwrap();
459 assert_eq!(*fig_id, "fig-1");
460 assert_eq!(fig_entry.id, "fig-1");
461 assert_eq!(fig_entry.reftext, None);
462 assert_eq!(fig_entry.ref_type, RefType::Anchor);
463
464 let (_, intro_entry) = entries.iter().find(|(id, _)| *id == "intro").unwrap();
465 assert_eq!(intro_entry.reftext, Some("Introduction".to_string()));
466 assert_eq!(intro_entry.ref_type, RefType::Section);
467 }
468
469 #[test]
470 fn resolve_id() {
471 let mut catalog = Catalog::new();
472
473 catalog
474 .register_ref("anchor1", Some("Reference Text"), RefType::Anchor)
475 .unwrap();
476
477 catalog
478 .register_ref("anchor2", Some("Another Reference"), RefType::Section)
479 .unwrap();
480
481 assert_eq!(
482 catalog.resolve_id("Reference Text"),
483 Some("anchor1".to_string())
484 );
485 assert_eq!(
486 catalog.resolve_id("Another Reference"),
487 Some("anchor2".to_string())
488 );
489 assert_eq!(catalog.resolve_id("Nonexistent"), None);
490 }
491
492 #[test]
493 fn resolve_id_first_wins_on_duplicates() {
494 let mut catalog = Catalog::new();
495
496 catalog
498 .register_ref("first", Some("Same Text"), RefType::Anchor)
499 .unwrap();
500
501 catalog
502 .register_ref("second", Some("Same Text"), RefType::Section)
503 .unwrap();
504
505 assert_eq!(catalog.resolve_id("Same Text"), Some("first".to_string()));
506 }
507
508 #[test]
509 fn duplicate_id_error_impl_display() {
510 let did_error = DuplicateIdError("foo".to_string());
511 assert_eq!(did_error.to_string(), "ID 'foo' already registered");
512 }
513
514 #[test]
515 fn ref_type_impl_debug() {
516 assert_eq!(format!("{:#?}", RefType::Anchor), "RefType::Anchor");
517 assert_eq!(format!("{:#?}", RefType::Section), "RefType::Section");
518
519 assert_eq!(
520 format!("{:#?}", RefType::Bibliography),
521 "RefType::Bibliography"
522 );
523 }
524}