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