asciidoc_parser/document/
catalog.rs1use std::collections::HashMap;
2
3use crate::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
20impl Default for Catalog {
21 fn default() -> Self {
22 Self::new()
23 }
24}
25
26impl Catalog {
27 pub(crate) fn new() -> Self {
28 Self {
29 refs: HashMap::new(),
30 reftext_to_id: HashMap::new(),
31 }
32 }
33
34 pub(crate) fn register_ref(
45 &mut self,
46 id: &str,
47 reftext: Option<&str>,
48 ref_type: RefType,
49 ) -> Result<(), DuplicateIdError> {
50 if self.refs.contains_key(id) {
51 return Err(DuplicateIdError(id.to_string()));
52 }
53
54 let entry = RefEntry {
55 id: id.to_string(),
56 reftext: reftext.map(|s| s.to_owned()),
57 ref_type,
58 };
59
60 self.refs.insert(id.to_string(), entry);
61
62 if let Some(reftext) = reftext {
63 self.reftext_to_id
64 .entry(reftext.to_string())
65 .or_insert_with(|| id.to_string());
66 }
67
68 Ok(())
69 }
70
71 pub(crate) fn generate_and_register_unique_id(
85 &mut self,
86 base_id: &str,
87 reftext: Option<&str>,
88 ref_type: RefType,
89 ) -> String {
90 let unique_id = if !self.contains_id(base_id) {
91 base_id.to_string()
92 } else {
93 let mut counter = 2;
94 loop {
95 let candidate = format!("{}-{}", base_id, counter);
96 if !self.contains_id(&candidate) {
97 break candidate;
98 }
99 counter += 1;
100 }
101 };
102
103 let entry = RefEntry {
105 id: unique_id.clone(),
106 reftext: reftext.map(|s| s.to_owned()),
107 ref_type,
108 };
109
110 self.refs.insert(unique_id.clone(), entry);
111
112 if let Some(reftext) = reftext {
113 self.reftext_to_id
114 .entry(reftext.to_string())
115 .or_insert_with(|| unique_id.clone());
116 }
117
118 unique_id
119 }
120
121 pub fn get_ref(&self, id: &str) -> Option<&RefEntry> {
123 self.refs.get(id)
124 }
125
126 pub fn contains_id(&self, id: &str) -> bool {
128 self.refs.contains_key(id)
129 }
130
131 pub fn resolve_id(&self, reftext: &str) -> Option<String> {
133 self.reftext_to_id.get(reftext).cloned()
134 }
135
136 pub fn len(&self) -> usize {
151 self.refs.len()
152 }
153
154 pub fn is_empty(&self) -> bool {
156 self.refs.is_empty()
157 }
158}
159
160impl std::fmt::Debug for Catalog {
161 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
162 f.debug_struct("Catalog")
163 .field("refs", &DebugHashMapFrom(&self.refs))
164 .field("reftext_to_id", &DebugHashMapFrom(&self.reftext_to_id))
165 .finish()
166 }
167}
168#[derive(Clone, PartialEq, Eq)]
170pub enum RefType {
171 Anchor,
173
174 Section,
176
177 Bibliography,
179}
180
181impl std::fmt::Debug for RefType {
182 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
183 match self {
184 Self::Anchor => f.write_str("RefType::Anchor"),
185 Self::Section => f.write_str("RefType::Section"),
186 Self::Bibliography => f.write_str("RefType::Bibliography"),
187 }
188 }
189}
190
191#[derive(Clone, Debug, Eq, PartialEq)]
193pub struct RefEntry {
194 pub id: String,
196
197 pub reftext: Option<String>,
199
200 pub ref_type: RefType,
202}
203
204#[derive(Clone, Debug, Eq, PartialEq)]
206pub(crate) struct DuplicateIdError(pub(crate) String);
207
208impl std::fmt::Display for DuplicateIdError {
209 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
210 write!(f, "ID '{}' already registered", self.0)
211 }
212}
213
214impl std::error::Error for DuplicateIdError {}
215
216#[cfg(test)]
217mod tests {
218 #![allow(clippy::unwrap_used)]
219
220 use super::*;
221
222 #[test]
223 fn new_catalog_is_empty() {
224 let catalog = Catalog::new();
225 assert!(catalog.is_empty());
226 assert_eq!(catalog.len(), 0);
227 }
228
229 #[test]
230 fn register_ref_success() {
231 let mut catalog = Catalog::new();
232
233 let result = catalog.register_ref("test-id", Some("Test Reference"), RefType::Anchor);
234
235 assert!(result.is_ok());
236 assert_eq!(catalog.len(), 1);
237 assert!(catalog.contains_id("test-id"));
238 }
239
240 #[test]
241 fn register_duplicate_id_fails() {
242 let mut catalog = Catalog::new();
243
244 catalog
246 .register_ref("test-id", Some("First"), RefType::Anchor)
247 .unwrap();
248
249 let result = catalog.register_ref("test-id", Some("Second"), RefType::Section);
251
252 let error = result.unwrap_err();
253 assert_eq!(error.0, "test-id");
254 }
255
256 #[test]
257 fn generate_and_register_unique_id() {
258 let mut catalog = Catalog::new();
259
260 let id1 = catalog.generate_and_register_unique_id(
262 "available",
263 Some("Available Ref"),
264 RefType::Anchor,
265 );
266 assert_eq!(id1, "available");
267 assert!(catalog.contains_id("available"));
268 assert_eq!(
269 catalog.resolve_id("Available Ref"),
270 Some("available".to_string())
271 );
272
273 catalog
275 .register_ref("taken", None, RefType::Anchor)
276 .unwrap();
277 catalog
278 .register_ref("taken-2", None, RefType::Anchor)
279 .unwrap();
280
281 let id2 = catalog.generate_and_register_unique_id("taken", None, RefType::Section);
282 assert_eq!(id2, "taken-3");
283 assert!(catalog.contains_id("taken-3"));
284 }
285
286 #[test]
287 fn get_ref() {
288 let mut catalog = Catalog::new();
289
290 catalog
291 .register_ref("test-id", Some("Test Reference"), RefType::Bibliography)
292 .unwrap();
293
294 let entry = catalog.get_ref("test-id").unwrap();
295 assert_eq!(entry.id, "test-id");
296 assert_eq!(entry.reftext, Some("Test Reference".to_string()));
297 assert_eq!(entry.ref_type, RefType::Bibliography);
298
299 assert!(catalog.get_ref("nonexistent").is_none());
300 }
301
302 #[test]
303 fn resolve_id() {
304 let mut catalog = Catalog::new();
305
306 catalog
307 .register_ref("anchor1", Some("Reference Text"), RefType::Anchor)
308 .unwrap();
309
310 catalog
311 .register_ref("anchor2", Some("Another Reference"), RefType::Section)
312 .unwrap();
313
314 assert_eq!(
315 catalog.resolve_id("Reference Text"),
316 Some("anchor1".to_string())
317 );
318 assert_eq!(
319 catalog.resolve_id("Another Reference"),
320 Some("anchor2".to_string())
321 );
322 assert_eq!(catalog.resolve_id("Nonexistent"), None);
323 }
324
325 #[test]
326 fn resolve_id_first_wins_on_duplicates() {
327 let mut catalog = Catalog::new();
328
329 catalog
331 .register_ref("first", Some("Same Text"), RefType::Anchor)
332 .unwrap();
333
334 catalog
335 .register_ref("second", Some("Same Text"), RefType::Section)
336 .unwrap();
337
338 assert_eq!(catalog.resolve_id("Same Text"), Some("first".to_string()));
339 }
340
341 #[test]
342 fn duplicate_id_error_impl_display() {
343 let did_error = DuplicateIdError("foo".to_string());
344 assert_eq!(did_error.to_string(), "ID 'foo' already registered");
345 }
346
347 #[test]
348 fn ref_type_impl_debug() {
349 assert_eq!(format!("{:#?}", RefType::Anchor), "RefType::Anchor");
350 assert_eq!(format!("{:#?}", RefType::Section), "RefType::Section");
351
352 assert_eq!(
353 format!("{:#?}", RefType::Bibliography),
354 "RefType::Bibliography"
355 );
356 }
357}