1use crate::prelude::*;
2use serde::{Deserialize, Serialize};
3use serde_json::json;
4
5#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct OutlineEntry {
14 pub title: String,
16
17 pub page: Option<String>,
20
21 pub level: u8,
24
25 pub entry_type: Option<String>,
28
29 pub children: Vec<OutlineEntry>,
32}
33
34impl OutlineEntry {
35 pub fn new(title: String, page: Option<String>, level: u8) -> Self {
36 Self {
37 title,
38 page,
39 level,
40 entry_type: None,
41 children: Vec::new(),
42 }
43 }
44
45 pub fn with_type(mut self, entry_type: String) -> Self {
46 self.entry_type = Some(entry_type);
47 self
48 }
49
50 pub fn with_children(mut self, children: Vec<OutlineEntry>) -> Self {
51 self.children = children;
52 self
53 }
54
55 pub fn add_child(&mut self, child: OutlineEntry) {
57 self.children.push(child);
58 }
59
60 pub fn flatten(&self) -> Vec<FlatOutlineEntry> {
62 let mut result = Vec::new();
63 self.flatten_recursive(&mut result, Vec::new());
64 result
65 }
66
67 fn flatten_recursive(&self, result: &mut Vec<FlatOutlineEntry>, mut path: Vec<String>) {
68 path.push(self.title.clone());
69
70 result.push(FlatOutlineEntry {
71 title: self.title.clone(),
72 page: self.page.clone(),
73 level: self.level,
74 entry_type: self.entry_type.clone(),
75 path: path.clone(),
76 });
77
78 for child in &self.children {
79 child.flatten_recursive(result, path.clone());
80 }
81 }
82}
83
84#[derive(Debug, Clone, Serialize, Deserialize)]
86pub struct FlatOutlineEntry {
87 pub title: String,
88 pub page: Option<String>,
89 pub level: u8,
90 pub entry_type: Option<String>,
91 pub path: Vec<String>,
93}
94
95#[derive(Debug, Clone, Serialize, Deserialize)]
97pub struct ListingOutline {
98 pub document_title: Option<String>,
100
101 pub entries: Vec<OutlineEntry>,
103
104 pub confidence: f64,
106
107 pub metadata: OutlineMetadata,
109}
110
111impl ListingOutline {
112 pub fn new() -> Self {
113 Self {
114 document_title: None,
115 entries: Vec::new(),
116 confidence: 0.0,
117 metadata: OutlineMetadata::default(),
118 }
119 }
120
121 pub fn flatten(&self) -> Vec<FlatOutlineEntry> {
123 self.entries
124 .iter()
125 .flat_map(|entry| entry.flatten())
126 .collect()
127 }
128
129 pub fn entries_at_level(&self, level: u8) -> Vec<&OutlineEntry> {
131 fn collect_at_level<'a>(
132 entries: &'a [OutlineEntry],
133 target_level: u8,
134 result: &mut Vec<&'a OutlineEntry>,
135 ) {
136 for entry in entries {
137 if entry.level == target_level {
138 result.push(entry);
139 }
140 collect_at_level(&entry.children, target_level, result);
141 }
142 }
143
144 let mut result = Vec::new();
145 collect_at_level(&self.entries, level, &mut result);
146 result
147 }
148
149 pub fn max_depth(&self) -> u8 {
151 fn max_depth_recursive(entries: &[OutlineEntry]) -> u8 {
152 entries
153 .iter()
154 .map(|entry| {
155 let child_depth = if entry.children.is_empty() {
156 0
157 } else {
158 max_depth_recursive(&entry.children)
159 };
160 entry.level.max(child_depth)
161 })
162 .max()
163 .unwrap_or(0)
164 }
165
166 max_depth_recursive(&self.entries)
167 }
168}
169
170impl Default for ListingOutline {
171 fn default() -> Self {
172 Self::new()
173 }
174}
175
176#[derive(Debug, Clone, Serialize, Deserialize, Default)]
178pub struct OutlineMetadata {
179 pub numbering_style: Option<String>,
181
182 pub has_leaders: bool,
184
185 pub page_style: Option<String>,
187
188 pub total_entries: usize,
190
191 pub levels: u8,
193
194 pub structure_type: Option<String>,
196}
197
198pub fn generate_outline_schema() -> serde_json::Value {
200 json!({
201 "$schema": "http://json-schema.org/draft-07/schema#",
202 "title": "Table of Contents",
203 "description": "Hierarchical table of contents structure that can represent various Outline formats",
204 "type": "object",
205 "properties": {
206 "document_title": {
207 "type": ["string", "null"],
208 "description": "Title of the document (optional)"
209 },
210 "entries": {
211 "type": "array",
212 "description": "Main table of contents entries",
213 "items": {
214 "$ref": "#/definitions/OutlineEntry"
215 }
216 },
217 "confidence": {
218 "type": "number",
219 "minimum": 0.0,
220 "maximum": 1.0,
221 "description": "Confidence level of the extraction (0.0 - 1.0)"
222 },
223 "metadata": {
224 "$ref": "#/definitions/OutlineMetadata"
225 }
226 },
227 "required": ["entries", "confidence"],
228 "definitions": {
229 "OutlineEntry": {
230 "type": "object",
231 "description": "A single table of contents entry with optional hierarchy",
232 "properties": {
233 "title": {
234 "type": "string",
235 "description": "The heading or title text"
236 },
237 "page": {
238 "type": ["string", "null"],
239 "description": "Page number or range (e.g., '15', '15-20', 'iv', 'A-1')"
240 },
241 "level": {
242 "type": "integer",
243 "minimum": 0,
244 "maximum": 10,
245 "description": "Hierarchical level (0 = top level, 1 = subsection, etc.)"
246 },
247 "entry_type": {
248 "type": ["string", "null"],
249 "description": "Optional semantic type (e.g., 'part', 'chapter', 'section', 'appendix')",
250 "enum": ["part", "chapter", "section", "subsection", "appendix", "index", "bibliography", "preface", "introduction", "conclusion", null]
251 },
252 "children": {
253 "type": "array",
254 "description": "Child entries for hierarchical structures",
255 "items": {
256 "$ref": "#/definitions/OutlineEntry"
257 }
258 }
259 },
260 "required": ["title", "level"]
261 },
262 "OutlineMetadata": {
263 "type": "object",
264 "description": "Metadata about the table of contents structure",
265 "properties": {
266 "numbering_style": {
267 "type": ["string", "null"],
268 "description": "Detected numbering style",
269 "enum": ["numeric", "roman", "alphabetic", "mixed", null]
270 },
271 "has_leaders": {
272 "type": "boolean",
273 "description": "Whether the Outline uses dots or other leaders"
274 },
275 "page_style": {
276 "type": ["string", "null"],
277 "description": "Page numbering style",
278 "enum": ["arabic", "roman", "alphabetic", "mixed", null]
279 },
280 "total_entries": {
281 "type": "integer",
282 "minimum": 0,
283 "description": "Total number of entries"
284 },
285 "levels": {
286 "type": "integer",
287 "minimum": 1,
288 "maximum": 10,
289 "description": "Number of hierarchical levels"
290 },
291 "structure_type": {
292 "type": ["string", "null"],
293 "description": "Detected overall structure type",
294 "enum": ["flat", "chapters", "parts_chapters", "sections", "mixed", null]
295 }
296 },
297 "required": ["has_leaders", "total_entries", "levels"]
298 }
299 }
300 })
301}
302
303#[cfg(test)]
304mod tests {
305 use super::*;
306
307 #[test]
309 fn test0024_simple_flat_outline() {
310 let mut outline = ListingOutline::new();
311 outline.entries = vec![
312 OutlineEntry::new("Introduction".to_string(), Some("1".to_string()), 0),
313 OutlineEntry::new(
314 "Chapter 1: Getting Started".to_string(),
315 Some("5".to_string()),
316 0,
317 ),
318 OutlineEntry::new(
319 "Chapter 2: Advanced Topics".to_string(),
320 Some("15".to_string()),
321 0,
322 ),
323 OutlineEntry::new("Conclusion".to_string(), Some("25".to_string()), 0),
324 ];
325
326 assert_eq!(outline.max_depth(), 0);
327 assert_eq!(outline.entries_at_level(0).len(), 4);
328 assert_eq!(outline.flatten().len(), 4);
329 }
330
331 #[test]
333 fn test0025_hierarchical_outline() {
334 let mut outline = ListingOutline::new();
335
336 let mut chapter1 =
337 OutlineEntry::new("Chapter 1: Basics".to_string(), Some("10".to_string()), 0)
338 .with_type("chapter".to_string());
339 chapter1.add_child(OutlineEntry::new(
340 "1.1 Introduction".to_string(),
341 Some("10".to_string()),
342 1,
343 ));
344 chapter1.add_child(OutlineEntry::new(
345 "1.2 Fundamentals".to_string(),
346 Some("15".to_string()),
347 1,
348 ));
349
350 let mut chapter2 =
351 OutlineEntry::new("Chapter 2: Advanced".to_string(), Some("20".to_string()), 0)
352 .with_type("chapter".to_string());
353 chapter2.add_child(OutlineEntry::new(
354 "2.1 Complex Topics".to_string(),
355 Some("20".to_string()),
356 1,
357 ));
358
359 outline.entries = vec![chapter1, chapter2];
360
361 assert_eq!(outline.max_depth(), 1);
362 assert_eq!(outline.entries_at_level(0).len(), 2);
363 assert_eq!(outline.entries_at_level(1).len(), 3);
364 assert_eq!(outline.flatten().len(), 5); }
366
367 #[test]
369 fn test0026_complex_part_based_outline() {
370 let mut outline = ListingOutline::new();
371
372 let mut part1 =
374 OutlineEntry::new("Part I: Foundations".to_string(), Some("1".to_string()), 0)
375 .with_type("part".to_string());
376
377 let mut chapter1 = OutlineEntry::new(
378 "Chapter 1: Introduction".to_string(),
379 Some("3".to_string()),
380 1,
381 )
382 .with_type("chapter".to_string());
383 chapter1.add_child(OutlineEntry::new(
384 "1.1 Overview".to_string(),
385 Some("3".to_string()),
386 2,
387 ));
388 chapter1.add_child(OutlineEntry::new(
389 "1.2 Scope".to_string(),
390 Some("5".to_string()),
391 2,
392 ));
393
394 let chapter2 = OutlineEntry::new(
395 "Chapter 2: Background".to_string(),
396 Some("8".to_string()),
397 1,
398 )
399 .with_type("chapter".to_string());
400
401 part1.add_child(chapter1);
402 part1.add_child(chapter2);
403
404 let part2 = OutlineEntry::new(
406 "Part II: Applications".to_string(),
407 Some("15".to_string()),
408 0,
409 )
410 .with_type("part".to_string());
411
412 outline.entries = vec![part1, part2];
413
414 assert_eq!(outline.max_depth(), 2);
415 assert_eq!(outline.entries_at_level(0).len(), 2); assert_eq!(outline.entries_at_level(1).len(), 2); assert_eq!(outline.entries_at_level(2).len(), 2); assert_eq!(outline.flatten().len(), 6); }
420
421 #[test]
423 fn test0027_flatten_preserves_hierarchy() {
424 let mut outline = ListingOutline::new();
425
426 let mut part = OutlineEntry::new("Part I".to_string(), Some("1".to_string()), 0);
427 let mut chapter = OutlineEntry::new("Chapter 1".to_string(), Some("3".to_string()), 1);
428 chapter.add_child(OutlineEntry::new(
429 "Section 1.1".to_string(),
430 Some("3".to_string()),
431 2,
432 ));
433 part.add_child(chapter);
434 outline.entries = vec![part];
435
436 let flat = outline.flatten();
437 assert_eq!(flat.len(), 3);
438
439 assert_eq!(flat[0].path, vec!["Part I"]);
441 assert_eq!(flat[1].path, vec!["Part I", "Chapter 1"]);
442 assert_eq!(flat[2].path, vec!["Part I", "Chapter 1", "Section 1.1"]);
443 }
444
445 #[test]
447 fn test0028_schema_generation() {
448 let schema = generate_outline_schema();
449 assert!(schema.is_object());
450 assert!(schema["properties"]["entries"].is_object());
451 assert!(schema["definitions"]["OutlineEntry"].is_object());
452 assert!(schema["definitions"]["OutlineMetadata"].is_object());
453 }
454}