asciidoc_parser/document/
catalog.rs1use std::collections::HashMap;
2
3use crate::{content::FootnoteDeferred, internal::debug::DebugHashMapFrom};
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 };
69
70 self.refs.insert(id.to_string(), entry);
71
72 if let Some(reftext) = reftext {
73 self.reftext_to_id
74 .entry(reftext.to_string())
75 .or_insert_with(|| id.to_string());
76 }
77
78 Ok(())
79 }
80
81 pub(crate) fn generate_and_register_unique_id(
95 &mut self,
96 base_id: &str,
97 reftext: Option<&str>,
98 ref_type: RefType,
99 ) -> String {
100 let unique_id = if !self.contains_id(base_id) {
101 base_id.to_string()
102 } else {
103 let mut counter = 2;
104 loop {
105 let candidate = format!("{}-{}", base_id, counter);
106 if !self.contains_id(&candidate) {
107 break candidate;
108 }
109 counter += 1;
110 }
111 };
112
113 let entry = RefEntry {
115 id: unique_id.clone(),
116 reftext: reftext.map(|s| s.to_owned()),
117 ref_type,
118 };
119
120 self.refs.insert(unique_id.clone(), entry);
121
122 if let Some(reftext) = reftext {
123 self.reftext_to_id
124 .entry(reftext.to_string())
125 .or_insert_with(|| unique_id.clone());
126 }
127
128 unique_id
129 }
130
131 pub fn get_ref(&self, id: &str) -> Option<&RefEntry> {
133 self.refs.get(id)
134 }
135
136 pub fn contains_id(&self, id: &str) -> bool {
138 self.refs.contains_key(id)
139 }
140
141 pub fn resolve_id(&self, reftext: &str) -> Option<String> {
143 self.reftext_to_id.get(reftext).cloned()
144 }
145
146 pub fn ids(&self) -> impl Iterator<Item = &str> {
153 self.refs.keys().map(String::as_str)
154 }
155
156 pub fn entries(&self) -> impl Iterator<Item = (&str, &RefEntry)> {
162 self.refs.iter().map(|(id, entry)| (id.as_str(), entry))
163 }
164
165 pub fn len(&self) -> usize {
167 self.refs.len()
168 }
169
170 pub fn is_empty(&self) -> bool {
172 self.refs.is_empty()
173 }
174
175 pub fn footnotes(&self) -> &[Footnote] {
177 &self.footnotes
178 }
179
180 pub(crate) fn register_footnote(&mut self, footnote: Footnote) {
182 self.footnotes.push(footnote);
183 }
184
185 pub(crate) fn footnote_with_id(&self, id: &str) -> Option<&Footnote> {
187 self.footnotes.iter().find(|f| f.id.as_deref() == Some(id))
188 }
189
190 pub(crate) fn take_footnotes(&mut self) -> Vec<Footnote> {
195 std::mem::take(&mut self.footnotes)
196 }
197
198 pub(crate) fn restore_footnotes(&mut self, footnotes: Vec<Footnote>) {
201 self.footnotes = footnotes;
202 }
203}
204
205#[derive(Clone, Eq, PartialEq)]
213pub struct Footnote {
214 pub index: String,
220
221 pub id: Option<String>,
225
226 pub text: String,
231
232 pub(crate) deferred: Option<Box<FootnoteDeferred>>,
236}
237
238impl Footnote {
239 pub(crate) fn resolve_references(
245 &mut self,
246 resolver: &dyn crate::parser::ReferenceResolver,
247 renderer: &dyn crate::parser::InlineSubstitutionRenderer,
248 warnings: &mut Vec<crate::parser::ReferenceWarning>,
249 ) {
250 if let Some(deferred) = self.deferred.as_mut() {
251 deferred.resolve(resolver, warnings);
252 self.text = deferred.render(renderer);
253 }
254 }
255}
256
257impl std::fmt::Debug for Footnote {
258 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
259 let mut s = f.debug_struct("Footnote");
263 s.field("index", &self.index);
264 s.field("id", &self.id);
265 s.field("text", &self.text);
266
267 if let Some(deferred) = self.deferred.as_ref() {
268 s.field("deferred", deferred);
269 }
270
271 s.finish()
272 }
273}
274
275impl std::fmt::Debug for Catalog {
276 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
277 f.debug_struct("Catalog")
278 .field("refs", &DebugHashMapFrom(&self.refs))
279 .field("reftext_to_id", &DebugHashMapFrom(&self.reftext_to_id))
280 .field("footnotes", &self.footnotes)
281 .finish()
282 }
283}
284#[derive(Clone, PartialEq, Eq)]
286pub enum RefType {
287 Anchor,
289
290 Section,
292
293 Bibliography,
295}
296
297impl std::fmt::Debug for RefType {
298 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
299 match self {
300 Self::Anchor => f.write_str("RefType::Anchor"),
301 Self::Section => f.write_str("RefType::Section"),
302 Self::Bibliography => f.write_str("RefType::Bibliography"),
303 }
304 }
305}
306
307#[derive(Clone, Debug, Eq, PartialEq)]
309pub struct RefEntry {
310 pub id: String,
312
313 pub reftext: Option<String>,
315
316 pub ref_type: RefType,
318}
319
320#[derive(Clone, Debug, Eq, PartialEq)]
322pub(crate) struct DuplicateIdError(pub(crate) String);
323
324impl std::fmt::Display for DuplicateIdError {
325 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
326 write!(f, "ID '{}' already registered", self.0)
327 }
328}
329
330impl std::error::Error for DuplicateIdError {}
331
332#[cfg(test)]
333mod tests {
334 #![allow(clippy::unwrap_used)]
335
336 use super::*;
337
338 #[test]
339 fn new_catalog_is_empty() {
340 let catalog = Catalog::new();
341 assert!(catalog.is_empty());
342 assert_eq!(catalog.len(), 0);
343 }
344
345 #[test]
346 fn register_ref_success() {
347 let mut catalog = Catalog::new();
348
349 let result = catalog.register_ref("test-id", Some("Test Reference"), RefType::Anchor);
350
351 assert!(result.is_ok());
352 assert_eq!(catalog.len(), 1);
353 assert!(catalog.contains_id("test-id"));
354 }
355
356 #[test]
357 fn register_duplicate_id_fails() {
358 let mut catalog = Catalog::new();
359
360 catalog
362 .register_ref("test-id", Some("First"), RefType::Anchor)
363 .unwrap();
364
365 let result = catalog.register_ref("test-id", Some("Second"), RefType::Section);
367
368 let error = result.unwrap_err();
369 assert_eq!(error.0, "test-id");
370 }
371
372 #[test]
373 fn generate_and_register_unique_id() {
374 let mut catalog = Catalog::new();
375
376 let id1 = catalog.generate_and_register_unique_id(
378 "available",
379 Some("Available Ref"),
380 RefType::Anchor,
381 );
382 assert_eq!(id1, "available");
383 assert!(catalog.contains_id("available"));
384 assert_eq!(
385 catalog.resolve_id("Available Ref"),
386 Some("available".to_string())
387 );
388
389 catalog
391 .register_ref("taken", None, RefType::Anchor)
392 .unwrap();
393 catalog
394 .register_ref("taken-2", None, RefType::Anchor)
395 .unwrap();
396
397 let id2 = catalog.generate_and_register_unique_id("taken", None, RefType::Section);
398 assert_eq!(id2, "taken-3");
399 assert!(catalog.contains_id("taken-3"));
400 }
401
402 #[test]
403 fn get_ref() {
404 let mut catalog = Catalog::new();
405
406 catalog
407 .register_ref("test-id", Some("Test Reference"), RefType::Bibliography)
408 .unwrap();
409
410 let entry = catalog.get_ref("test-id").unwrap();
411 assert_eq!(entry.id, "test-id");
412 assert_eq!(entry.reftext, Some("Test Reference".to_string()));
413 assert_eq!(entry.ref_type, RefType::Bibliography);
414
415 assert!(catalog.get_ref("nonexistent").is_none());
416 }
417
418 #[test]
419 fn enumerate_ids_and_entries() {
420 let mut catalog = Catalog::new();
421
422 catalog
423 .register_ref("intro", Some("Introduction"), RefType::Section)
424 .unwrap();
425 catalog
426 .register_ref("fig-1", None, RefType::Anchor)
427 .unwrap();
428
429 let mut ids: Vec<&str> = catalog.ids().collect();
431 ids.sort_unstable();
432 assert_eq!(ids, vec!["fig-1", "intro"]);
433
434 let entries: Vec<(&str, &RefEntry)> = catalog.entries().collect();
436 assert_eq!(entries.len(), 2);
437
438 let (fig_id, fig_entry) = entries.iter().find(|(id, _)| *id == "fig-1").unwrap();
439 assert_eq!(*fig_id, "fig-1");
440 assert_eq!(fig_entry.id, "fig-1");
441 assert_eq!(fig_entry.reftext, None);
442 assert_eq!(fig_entry.ref_type, RefType::Anchor);
443
444 let (_, intro_entry) = entries.iter().find(|(id, _)| *id == "intro").unwrap();
445 assert_eq!(intro_entry.reftext, Some("Introduction".to_string()));
446 assert_eq!(intro_entry.ref_type, RefType::Section);
447 }
448
449 #[test]
450 fn resolve_id() {
451 let mut catalog = Catalog::new();
452
453 catalog
454 .register_ref("anchor1", Some("Reference Text"), RefType::Anchor)
455 .unwrap();
456
457 catalog
458 .register_ref("anchor2", Some("Another Reference"), RefType::Section)
459 .unwrap();
460
461 assert_eq!(
462 catalog.resolve_id("Reference Text"),
463 Some("anchor1".to_string())
464 );
465 assert_eq!(
466 catalog.resolve_id("Another Reference"),
467 Some("anchor2".to_string())
468 );
469 assert_eq!(catalog.resolve_id("Nonexistent"), None);
470 }
471
472 #[test]
473 fn resolve_id_first_wins_on_duplicates() {
474 let mut catalog = Catalog::new();
475
476 catalog
478 .register_ref("first", Some("Same Text"), RefType::Anchor)
479 .unwrap();
480
481 catalog
482 .register_ref("second", Some("Same Text"), RefType::Section)
483 .unwrap();
484
485 assert_eq!(catalog.resolve_id("Same Text"), Some("first".to_string()));
486 }
487
488 #[test]
489 fn duplicate_id_error_impl_display() {
490 let did_error = DuplicateIdError("foo".to_string());
491 assert_eq!(did_error.to_string(), "ID 'foo' already registered");
492 }
493
494 #[test]
495 fn ref_type_impl_debug() {
496 assert_eq!(format!("{:#?}", RefType::Anchor), "RefType::Anchor");
497 assert_eq!(format!("{:#?}", RefType::Section), "RefType::Section");
498
499 assert_eq!(
500 format!("{:#?}", RefType::Bibliography),
501 "RefType::Bibliography"
502 );
503 }
504}