Skip to main content

fop_layout/layout/
page_number_resolver.rs

1//! Page number resolution for fo:page-number-citation
2//!
3//! This module implements a two-pass layout system:
4//! Pass 1: Layout everything, track element IDs → page mappings
5//! Pass 2: Resolve citations and update area content with actual page numbers
6
7use crate::area::AreaId;
8use std::collections::HashMap;
9
10/// Tracks page numbers for elements with IDs and resolves page-number-citations
11pub struct PageNumberResolver {
12    /// Maps element ID to page number
13    id_to_page: HashMap<String, usize>,
14
15    /// Maps element ID to the page sequence format string when registered
16    id_to_format: HashMap<String, String>,
17
18    /// Maps element ID to area ID (for tracking during layout)
19    id_to_area: HashMap<String, AreaId>,
20
21    /// List of citations that need to be resolved (area_id, ref_id)
22    citations: Vec<(AreaId, String)>,
23
24    /// Current page number during layout
25    current_page: usize,
26
27    /// Current page number format for fo:page-sequence format attribute
28    current_format: String,
29
30    /// Current grouping separator (e.g. ',' for "1,000")
31    current_grouping_separator: Option<char>,
32
33    /// Current grouping size (e.g. 3 for "1,000")
34    current_grouping_size: Option<usize>,
35}
36
37impl PageNumberResolver {
38    /// Create a new page number resolver
39    pub fn new() -> Self {
40        Self {
41            id_to_page: HashMap::new(),
42            id_to_format: HashMap::new(),
43            id_to_area: HashMap::new(),
44            citations: Vec::new(),
45            current_page: 1,
46            current_format: "1".to_string(),
47            current_grouping_separator: None,
48            current_grouping_size: None,
49        }
50    }
51
52    /// Set the current page number
53    pub fn set_current_page(&mut self, page: usize) {
54        self.current_page = page;
55    }
56
57    /// Get the current page number
58    pub fn current_page(&self) -> usize {
59        self.current_page
60    }
61
62    /// Set the current page number format
63    pub fn set_current_format(&mut self, format: String) {
64        self.current_format = format;
65    }
66
67    /// Get the current page number format
68    pub fn current_format(&self) -> &str {
69        &self.current_format
70    }
71
72    /// Register an element with an ID on the current page
73    pub fn register_element(&mut self, id: String, area_id: AreaId) {
74        self.id_to_page.insert(id.clone(), self.current_page);
75        self.id_to_format
76            .insert(id.clone(), self.current_format.clone());
77        self.id_to_area.insert(id, area_id);
78    }
79
80    /// Get the page number format that was active when the element was registered
81    pub fn get_format_for_id(&self, id: &str) -> Option<&str> {
82        self.id_to_format.get(id).map(|s| s.as_str())
83    }
84
85    /// Set the current grouping separator
86    pub fn set_current_grouping_separator(&mut self, sep: Option<char>) {
87        self.current_grouping_separator = sep;
88    }
89
90    /// Get the current grouping separator
91    pub fn current_grouping_separator(&self) -> Option<char> {
92        self.current_grouping_separator
93    }
94
95    /// Set the current grouping size
96    pub fn set_current_grouping_size(&mut self, size: Option<usize>) {
97        self.current_grouping_size = size;
98    }
99
100    /// Get the current grouping size
101    pub fn current_grouping_size(&self) -> Option<usize> {
102        self.current_grouping_size
103    }
104
105    /// Register a page-number-citation that needs to be resolved
106    pub fn register_citation(&mut self, area_id: AreaId, ref_id: String) {
107        self.citations.push((area_id, ref_id));
108    }
109
110    /// Get the page number for a referenced element
111    pub fn get_page_number(&self, ref_id: &str) -> Option<usize> {
112        self.id_to_page.get(ref_id).copied()
113    }
114
115    /// Get all citations that need to be resolved
116    pub fn get_citations(&self) -> &[(AreaId, String)] {
117        &self.citations
118    }
119
120    /// Check if all citations can be resolved
121    pub fn can_resolve_all(&self) -> bool {
122        self.citations
123            .iter()
124            .all(|(_, ref_id)| self.id_to_page.contains_key(ref_id))
125    }
126
127    /// Get a list of unresolved citations
128    pub fn unresolved_citations(&self) -> Vec<String> {
129        self.citations
130            .iter()
131            .filter_map(|(_, ref_id)| {
132                if !self.id_to_page.contains_key(ref_id) {
133                    Some(ref_id.clone())
134                } else {
135                    None
136                }
137            })
138            .collect()
139    }
140
141    /// Clear all registered data (for a new layout pass)
142    pub fn clear(&mut self) {
143        self.id_to_page.clear();
144        self.id_to_format.clear();
145        self.id_to_area.clear();
146        self.citations.clear();
147        self.current_page = 1;
148        self.current_format = "1".to_string();
149        self.current_grouping_separator = None;
150        self.current_grouping_size = None;
151    }
152}
153
154impl Default for PageNumberResolver {
155    fn default() -> Self {
156        Self::new()
157    }
158}
159
160#[cfg(test)]
161mod tests {
162    use super::*;
163
164    #[test]
165    fn test_resolver_creation() {
166        let resolver = PageNumberResolver::new();
167        assert_eq!(resolver.current_page(), 1);
168        assert!(resolver.can_resolve_all());
169    }
170
171    #[test]
172    fn test_register_element() {
173        let mut resolver = PageNumberResolver::new();
174        resolver.set_current_page(5);
175
176        let area_id = AreaId::from_index(10);
177        resolver.register_element("chapter1".to_string(), area_id);
178
179        assert_eq!(resolver.get_page_number("chapter1"), Some(5));
180    }
181
182    #[test]
183    fn test_register_citation() {
184        let mut resolver = PageNumberResolver::new();
185        let area_id = AreaId::from_index(20);
186
187        resolver.register_citation(area_id, "chapter1".to_string());
188
189        assert_eq!(resolver.get_citations().len(), 1);
190        assert_eq!(
191            resolver.get_citations()[0],
192            (area_id, "chapter1".to_string())
193        );
194    }
195
196    #[test]
197    fn test_can_resolve_all() {
198        let mut resolver = PageNumberResolver::new();
199        let area_id = AreaId::from_index(10);
200
201        // Register a citation
202        resolver.register_citation(area_id, "chapter1".to_string());
203        assert!(!resolver.can_resolve_all());
204
205        // Register the referenced element
206        resolver.register_element("chapter1".to_string(), area_id);
207        assert!(resolver.can_resolve_all());
208    }
209
210    #[test]
211    fn test_unresolved_citations() {
212        let mut resolver = PageNumberResolver::new();
213        let area_id = AreaId::from_index(10);
214
215        resolver.register_citation(area_id, "chapter1".to_string());
216        resolver.register_citation(area_id, "chapter2".to_string());
217
218        // Only register chapter1
219        resolver.register_element("chapter1".to_string(), area_id);
220
221        let unresolved = resolver.unresolved_citations();
222        assert_eq!(unresolved.len(), 1);
223        assert_eq!(unresolved[0], "chapter2");
224    }
225
226    #[test]
227    fn test_clear() {
228        let mut resolver = PageNumberResolver::new();
229        let area_id = AreaId::from_index(10);
230
231        resolver.register_element("chapter1".to_string(), area_id);
232        resolver.register_citation(area_id, "chapter1".to_string());
233        resolver.set_current_page(5);
234
235        resolver.clear();
236
237        assert_eq!(resolver.current_page(), 1);
238        assert_eq!(resolver.get_page_number("chapter1"), None);
239        assert_eq!(resolver.get_citations().len(), 0);
240    }
241}
242
243#[cfg(test)]
244mod extended_tests {
245    use super::*;
246
247    #[test]
248    fn test_resolver_default_same_as_new() {
249        let r1 = PageNumberResolver::new();
250        let r2 = PageNumberResolver::default();
251        assert_eq!(r1.current_page(), r2.current_page());
252        assert_eq!(r1.current_format(), r2.current_format());
253    }
254
255    #[test]
256    fn test_set_current_page() {
257        let mut resolver = PageNumberResolver::new();
258        resolver.set_current_page(10);
259        assert_eq!(resolver.current_page(), 10);
260    }
261
262    #[test]
263    fn test_register_element_tracks_format() {
264        let mut resolver = PageNumberResolver::new();
265        resolver.set_current_page(3);
266        resolver.set_current_format("i".to_string());
267
268        let area_id = AreaId::from_index(5);
269        resolver.register_element("section-2".to_string(), area_id);
270
271        // The format for that id should be "i"
272        assert_eq!(resolver.get_format_for_id("section-2"), Some("i"));
273    }
274
275    #[test]
276    fn test_get_format_for_unregistered_id_returns_none() {
277        let resolver = PageNumberResolver::new();
278        assert_eq!(resolver.get_format_for_id("nonexistent"), None);
279    }
280
281    #[test]
282    fn test_register_element_multiple_pages() {
283        let mut resolver = PageNumberResolver::new();
284
285        let area1 = AreaId::from_index(1);
286        let area2 = AreaId::from_index(2);
287
288        resolver.set_current_page(1);
289        resolver.register_element("sec-1".to_string(), area1);
290
291        resolver.set_current_page(5);
292        resolver.register_element("sec-5".to_string(), area2);
293
294        assert_eq!(resolver.get_page_number("sec-1"), Some(1));
295        assert_eq!(resolver.get_page_number("sec-5"), Some(5));
296    }
297
298    #[test]
299    fn test_get_page_number_for_unregistered_returns_none() {
300        let resolver = PageNumberResolver::new();
301        assert_eq!(resolver.get_page_number("missing"), None);
302    }
303
304    #[test]
305    fn test_can_resolve_all_empty_citations() {
306        let resolver = PageNumberResolver::new();
307        // No citations => can resolve all
308        assert!(resolver.can_resolve_all());
309    }
310
311    #[test]
312    fn test_can_resolve_all_with_unresolved() {
313        let mut resolver = PageNumberResolver::new();
314        let area_id = AreaId::from_index(1);
315        resolver.register_citation(area_id, "unknown-id".to_string());
316        assert!(!resolver.can_resolve_all());
317    }
318
319    #[test]
320    fn test_unresolved_citations_empty_when_all_resolved() {
321        let mut resolver = PageNumberResolver::new();
322        let area_id = AreaId::from_index(1);
323        resolver.register_element("para-1".to_string(), area_id);
324        resolver.register_citation(area_id, "para-1".to_string());
325
326        let unresolved = resolver.unresolved_citations();
327        assert!(unresolved.is_empty());
328    }
329
330    #[test]
331    fn test_clear_resets_format_to_default() {
332        let mut resolver = PageNumberResolver::new();
333        resolver.set_current_format("I".to_string());
334        resolver.clear();
335        assert_eq!(resolver.current_format(), "1");
336    }
337
338    #[test]
339    fn test_clear_resets_grouping() {
340        let mut resolver = PageNumberResolver::new();
341        resolver.set_current_grouping_separator(Some(','));
342        resolver.set_current_grouping_size(Some(3));
343        resolver.clear();
344        assert_eq!(resolver.current_grouping_separator(), None);
345        assert_eq!(resolver.current_grouping_size(), None);
346    }
347
348    #[test]
349    fn test_grouping_separator_get_set() {
350        let mut resolver = PageNumberResolver::new();
351        assert_eq!(resolver.current_grouping_separator(), None);
352        resolver.set_current_grouping_separator(Some('.'));
353        assert_eq!(resolver.current_grouping_separator(), Some('.'));
354    }
355
356    #[test]
357    fn test_grouping_size_get_set() {
358        let mut resolver = PageNumberResolver::new();
359        assert_eq!(resolver.current_grouping_size(), None);
360        resolver.set_current_grouping_size(Some(3));
361        assert_eq!(resolver.current_grouping_size(), Some(3));
362    }
363
364    #[test]
365    fn test_multiple_citations_some_resolved_some_not() {
366        let mut resolver = PageNumberResolver::new();
367        let area_id = AreaId::from_index(10);
368
369        resolver.register_element("known".to_string(), area_id);
370        resolver.register_citation(area_id, "known".to_string());
371        resolver.register_citation(area_id, "unknown-a".to_string());
372        resolver.register_citation(area_id, "unknown-b".to_string());
373
374        assert!(!resolver.can_resolve_all());
375
376        let unresolved = resolver.unresolved_citations();
377        assert_eq!(unresolved.len(), 2);
378        assert!(unresolved.contains(&"unknown-a".to_string()));
379        assert!(unresolved.contains(&"unknown-b".to_string()));
380    }
381
382    #[test]
383    fn test_register_citation_count() {
384        let mut resolver = PageNumberResolver::new();
385        let area_id = AreaId::from_index(1);
386
387        resolver.register_citation(area_id, "ref-1".to_string());
388        resolver.register_citation(area_id, "ref-2".to_string());
389        resolver.register_citation(area_id, "ref-3".to_string());
390
391        assert_eq!(resolver.get_citations().len(), 3);
392    }
393
394    #[test]
395    fn test_clear_removes_all_citations() {
396        let mut resolver = PageNumberResolver::new();
397        let area_id = AreaId::from_index(1);
398        resolver.register_citation(area_id, "ref-1".to_string());
399        resolver.register_citation(area_id, "ref-2".to_string());
400
401        resolver.clear();
402        assert_eq!(resolver.get_citations().len(), 0);
403        assert!(resolver.can_resolve_all());
404    }
405}