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 len(&self) -> usize {
161 self.refs.len()
162 }
163
164 pub fn is_empty(&self) -> bool {
166 self.refs.is_empty()
167 }
168
169 pub fn footnotes(&self) -> &[Footnote] {
171 &self.footnotes
172 }
173
174 pub(crate) fn register_footnote(&mut self, footnote: Footnote) {
176 self.footnotes.push(footnote);
177 }
178
179 pub(crate) fn footnote_with_id(&self, id: &str) -> Option<&Footnote> {
181 self.footnotes.iter().find(|f| f.id.as_deref() == Some(id))
182 }
183
184 pub(crate) fn take_footnotes(&mut self) -> Vec<Footnote> {
189 std::mem::take(&mut self.footnotes)
190 }
191
192 pub(crate) fn restore_footnotes(&mut self, footnotes: Vec<Footnote>) {
195 self.footnotes = footnotes;
196 }
197}
198
199#[derive(Clone, Eq, PartialEq)]
207pub struct Footnote {
208 pub index: String,
214
215 pub id: Option<String>,
219
220 pub text: String,
225
226 pub(crate) deferred: Option<Box<FootnoteDeferred>>,
230}
231
232impl Footnote {
233 pub(crate) fn resolve_references(
239 &mut self,
240 resolver: &dyn crate::parser::ReferenceResolver,
241 renderer: &dyn crate::parser::InlineSubstitutionRenderer,
242 warnings: &mut Vec<crate::parser::ReferenceWarning>,
243 ) {
244 if let Some(deferred) = self.deferred.as_mut() {
245 deferred.resolve(resolver, warnings);
246 self.text = deferred.render(renderer);
247 }
248 }
249}
250
251impl std::fmt::Debug for Footnote {
252 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
253 let mut s = f.debug_struct("Footnote");
257 s.field("index", &self.index);
258 s.field("id", &self.id);
259 s.field("text", &self.text);
260
261 if let Some(deferred) = self.deferred.as_ref() {
262 s.field("deferred", deferred);
263 }
264
265 s.finish()
266 }
267}
268
269impl std::fmt::Debug for Catalog {
270 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
271 f.debug_struct("Catalog")
272 .field("refs", &DebugHashMapFrom(&self.refs))
273 .field("reftext_to_id", &DebugHashMapFrom(&self.reftext_to_id))
274 .field("footnotes", &self.footnotes)
275 .finish()
276 }
277}
278#[derive(Clone, PartialEq, Eq)]
280pub enum RefType {
281 Anchor,
283
284 Section,
286
287 Bibliography,
289}
290
291impl std::fmt::Debug for RefType {
292 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
293 match self {
294 Self::Anchor => f.write_str("RefType::Anchor"),
295 Self::Section => f.write_str("RefType::Section"),
296 Self::Bibliography => f.write_str("RefType::Bibliography"),
297 }
298 }
299}
300
301#[derive(Clone, Debug, Eq, PartialEq)]
303pub struct RefEntry {
304 pub id: String,
306
307 pub reftext: Option<String>,
309
310 pub ref_type: RefType,
312}
313
314#[derive(Clone, Debug, Eq, PartialEq)]
316pub(crate) struct DuplicateIdError(pub(crate) String);
317
318impl std::fmt::Display for DuplicateIdError {
319 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
320 write!(f, "ID '{}' already registered", self.0)
321 }
322}
323
324impl std::error::Error for DuplicateIdError {}
325
326#[cfg(test)]
327mod tests {
328 #![allow(clippy::unwrap_used)]
329
330 use super::*;
331
332 #[test]
333 fn new_catalog_is_empty() {
334 let catalog = Catalog::new();
335 assert!(catalog.is_empty());
336 assert_eq!(catalog.len(), 0);
337 }
338
339 #[test]
340 fn register_ref_success() {
341 let mut catalog = Catalog::new();
342
343 let result = catalog.register_ref("test-id", Some("Test Reference"), RefType::Anchor);
344
345 assert!(result.is_ok());
346 assert_eq!(catalog.len(), 1);
347 assert!(catalog.contains_id("test-id"));
348 }
349
350 #[test]
351 fn register_duplicate_id_fails() {
352 let mut catalog = Catalog::new();
353
354 catalog
356 .register_ref("test-id", Some("First"), RefType::Anchor)
357 .unwrap();
358
359 let result = catalog.register_ref("test-id", Some("Second"), RefType::Section);
361
362 let error = result.unwrap_err();
363 assert_eq!(error.0, "test-id");
364 }
365
366 #[test]
367 fn generate_and_register_unique_id() {
368 let mut catalog = Catalog::new();
369
370 let id1 = catalog.generate_and_register_unique_id(
372 "available",
373 Some("Available Ref"),
374 RefType::Anchor,
375 );
376 assert_eq!(id1, "available");
377 assert!(catalog.contains_id("available"));
378 assert_eq!(
379 catalog.resolve_id("Available Ref"),
380 Some("available".to_string())
381 );
382
383 catalog
385 .register_ref("taken", None, RefType::Anchor)
386 .unwrap();
387 catalog
388 .register_ref("taken-2", None, RefType::Anchor)
389 .unwrap();
390
391 let id2 = catalog.generate_and_register_unique_id("taken", None, RefType::Section);
392 assert_eq!(id2, "taken-3");
393 assert!(catalog.contains_id("taken-3"));
394 }
395
396 #[test]
397 fn get_ref() {
398 let mut catalog = Catalog::new();
399
400 catalog
401 .register_ref("test-id", Some("Test Reference"), RefType::Bibliography)
402 .unwrap();
403
404 let entry = catalog.get_ref("test-id").unwrap();
405 assert_eq!(entry.id, "test-id");
406 assert_eq!(entry.reftext, Some("Test Reference".to_string()));
407 assert_eq!(entry.ref_type, RefType::Bibliography);
408
409 assert!(catalog.get_ref("nonexistent").is_none());
410 }
411
412 #[test]
413 fn resolve_id() {
414 let mut catalog = Catalog::new();
415
416 catalog
417 .register_ref("anchor1", Some("Reference Text"), RefType::Anchor)
418 .unwrap();
419
420 catalog
421 .register_ref("anchor2", Some("Another Reference"), RefType::Section)
422 .unwrap();
423
424 assert_eq!(
425 catalog.resolve_id("Reference Text"),
426 Some("anchor1".to_string())
427 );
428 assert_eq!(
429 catalog.resolve_id("Another Reference"),
430 Some("anchor2".to_string())
431 );
432 assert_eq!(catalog.resolve_id("Nonexistent"), None);
433 }
434
435 #[test]
436 fn resolve_id_first_wins_on_duplicates() {
437 let mut catalog = Catalog::new();
438
439 catalog
441 .register_ref("first", Some("Same Text"), RefType::Anchor)
442 .unwrap();
443
444 catalog
445 .register_ref("second", Some("Same Text"), RefType::Section)
446 .unwrap();
447
448 assert_eq!(catalog.resolve_id("Same Text"), Some("first".to_string()));
449 }
450
451 #[test]
452 fn duplicate_id_error_impl_display() {
453 let did_error = DuplicateIdError("foo".to_string());
454 assert_eq!(did_error.to_string(), "ID 'foo' already registered");
455 }
456
457 #[test]
458 fn ref_type_impl_debug() {
459 assert_eq!(format!("{:#?}", RefType::Anchor), "RefType::Anchor");
460 assert_eq!(format!("{:#?}", RefType::Section), "RefType::Section");
461
462 assert_eq!(
463 format!("{:#?}", RefType::Bibliography),
464 "RefType::Bibliography"
465 );
466 }
467}