use crate::prelude::*;
use serde::{Deserialize, Serialize};
use serde_json::json;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OutlineEntry {
pub title: String,
pub page: Option<String>,
pub level: u8,
pub entry_type: Option<String>,
pub children: Vec<OutlineEntry>,
}
impl OutlineEntry {
pub fn new(title: String, page: Option<String>, level: u8) -> Self {
Self {
title,
page,
level,
entry_type: None,
children: Vec::new(),
}
}
pub fn with_type(mut self, entry_type: String) -> Self {
self.entry_type = Some(entry_type);
self
}
pub fn with_children(mut self, children: Vec<OutlineEntry>) -> Self {
self.children = children;
self
}
pub fn add_child(&mut self, child: OutlineEntry) {
self.children.push(child);
}
pub fn flatten(&self) -> Vec<FlatOutlineEntry> {
let mut result = Vec::new();
self.flatten_recursive(&mut result, Vec::new());
result
}
fn flatten_recursive(&self, result: &mut Vec<FlatOutlineEntry>, mut path: Vec<String>) {
path.push(self.title.clone());
result.push(FlatOutlineEntry {
title: self.title.clone(),
page: self.page.clone(),
level: self.level,
entry_type: self.entry_type.clone(),
path: path.clone(),
});
for child in &self.children {
child.flatten_recursive(result, path.clone());
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FlatOutlineEntry {
pub title: String,
pub page: Option<String>,
pub level: u8,
pub entry_type: Option<String>,
pub path: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ListingOutline {
pub document_title: Option<String>,
pub entries: Vec<OutlineEntry>,
pub confidence: f64,
pub metadata: OutlineMetadata,
}
impl ListingOutline {
pub fn new() -> Self {
Self {
document_title: None,
entries: Vec::new(),
confidence: 0.0,
metadata: OutlineMetadata::default(),
}
}
pub fn flatten(&self) -> Vec<FlatOutlineEntry> {
self.entries
.iter()
.flat_map(|entry| entry.flatten())
.collect()
}
pub fn entries_at_level(&self, level: u8) -> Vec<&OutlineEntry> {
fn collect_at_level<'a>(
entries: &'a [OutlineEntry],
target_level: u8,
result: &mut Vec<&'a OutlineEntry>,
) {
for entry in entries {
if entry.level == target_level {
result.push(entry);
}
collect_at_level(&entry.children, target_level, result);
}
}
let mut result = Vec::new();
collect_at_level(&self.entries, level, &mut result);
result
}
pub fn max_depth(&self) -> u8 {
fn max_depth_recursive(entries: &[OutlineEntry]) -> u8 {
entries
.iter()
.map(|entry| {
let child_depth = if entry.children.is_empty() {
0
} else {
max_depth_recursive(&entry.children)
};
entry.level.max(child_depth)
})
.max()
.unwrap_or(0)
}
max_depth_recursive(&self.entries)
}
}
impl Default for ListingOutline {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct OutlineMetadata {
pub numbering_style: Option<String>,
pub has_leaders: bool,
pub page_style: Option<String>,
pub total_entries: usize,
pub levels: u8,
pub structure_type: Option<String>,
}
pub fn generate_outline_schema() -> serde_json::Value {
json!({
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Table of Contents",
"description": "Hierarchical table of contents structure that can represent various Outline formats",
"type": "object",
"properties": {
"document_title": {
"type": ["string", "null"],
"description": "Title of the document (optional)"
},
"entries": {
"type": "array",
"description": "Main table of contents entries",
"items": {
"$ref": "#/definitions/OutlineEntry"
}
},
"confidence": {
"type": "number",
"minimum": 0.0,
"maximum": 1.0,
"description": "Confidence level of the extraction (0.0 - 1.0)"
},
"metadata": {
"$ref": "#/definitions/OutlineMetadata"
}
},
"required": ["entries", "confidence"],
"definitions": {
"OutlineEntry": {
"type": "object",
"description": "A single table of contents entry with optional hierarchy",
"properties": {
"title": {
"type": "string",
"description": "The heading or title text"
},
"page": {
"type": ["string", "null"],
"description": "Page number or range (e.g., '15', '15-20', 'iv', 'A-1')"
},
"level": {
"type": "integer",
"minimum": 0,
"maximum": 10,
"description": "Hierarchical level (0 = top level, 1 = subsection, etc.)"
},
"entry_type": {
"type": ["string", "null"],
"description": "Optional semantic type (e.g., 'part', 'chapter', 'section', 'appendix')",
"enum": ["part", "chapter", "section", "subsection", "appendix", "index", "bibliography", "preface", "introduction", "conclusion", null]
},
"children": {
"type": "array",
"description": "Child entries for hierarchical structures",
"items": {
"$ref": "#/definitions/OutlineEntry"
}
}
},
"required": ["title", "level"]
},
"OutlineMetadata": {
"type": "object",
"description": "Metadata about the table of contents structure",
"properties": {
"numbering_style": {
"type": ["string", "null"],
"description": "Detected numbering style",
"enum": ["numeric", "roman", "alphabetic", "mixed", null]
},
"has_leaders": {
"type": "boolean",
"description": "Whether the Outline uses dots or other leaders"
},
"page_style": {
"type": ["string", "null"],
"description": "Page numbering style",
"enum": ["arabic", "roman", "alphabetic", "mixed", null]
},
"total_entries": {
"type": "integer",
"minimum": 0,
"description": "Total number of entries"
},
"levels": {
"type": "integer",
"minimum": 1,
"maximum": 10,
"description": "Number of hierarchical levels"
},
"structure_type": {
"type": ["string", "null"],
"description": "Detected overall structure type",
"enum": ["flat", "chapters", "parts_chapters", "sections", "mixed", null]
}
},
"required": ["has_leaders", "total_entries", "levels"]
}
}
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test0024_simple_flat_outline() {
let mut outline = ListingOutline::new();
outline.entries = vec![
OutlineEntry::new("Introduction".to_string(), Some("1".to_string()), 0),
OutlineEntry::new(
"Chapter 1: Getting Started".to_string(),
Some("5".to_string()),
0,
),
OutlineEntry::new(
"Chapter 2: Advanced Topics".to_string(),
Some("15".to_string()),
0,
),
OutlineEntry::new("Conclusion".to_string(), Some("25".to_string()), 0),
];
assert_eq!(outline.max_depth(), 0);
assert_eq!(outline.entries_at_level(0).len(), 4);
assert_eq!(outline.flatten().len(), 4);
}
#[test]
fn test0025_hierarchical_outline() {
let mut outline = ListingOutline::new();
let mut chapter1 =
OutlineEntry::new("Chapter 1: Basics".to_string(), Some("10".to_string()), 0)
.with_type("chapter".to_string());
chapter1.add_child(OutlineEntry::new(
"1.1 Introduction".to_string(),
Some("10".to_string()),
1,
));
chapter1.add_child(OutlineEntry::new(
"1.2 Fundamentals".to_string(),
Some("15".to_string()),
1,
));
let mut chapter2 =
OutlineEntry::new("Chapter 2: Advanced".to_string(), Some("20".to_string()), 0)
.with_type("chapter".to_string());
chapter2.add_child(OutlineEntry::new(
"2.1 Complex Topics".to_string(),
Some("20".to_string()),
1,
));
outline.entries = vec![chapter1, chapter2];
assert_eq!(outline.max_depth(), 1);
assert_eq!(outline.entries_at_level(0).len(), 2);
assert_eq!(outline.entries_at_level(1).len(), 3);
assert_eq!(outline.flatten().len(), 5); }
#[test]
fn test0026_complex_part_based_outline() {
let mut outline = ListingOutline::new();
let mut part1 =
OutlineEntry::new("Part I: Foundations".to_string(), Some("1".to_string()), 0)
.with_type("part".to_string());
let mut chapter1 = OutlineEntry::new(
"Chapter 1: Introduction".to_string(),
Some("3".to_string()),
1,
)
.with_type("chapter".to_string());
chapter1.add_child(OutlineEntry::new(
"1.1 Overview".to_string(),
Some("3".to_string()),
2,
));
chapter1.add_child(OutlineEntry::new(
"1.2 Scope".to_string(),
Some("5".to_string()),
2,
));
let chapter2 = OutlineEntry::new(
"Chapter 2: Background".to_string(),
Some("8".to_string()),
1,
)
.with_type("chapter".to_string());
part1.add_child(chapter1);
part1.add_child(chapter2);
let part2 = OutlineEntry::new(
"Part II: Applications".to_string(),
Some("15".to_string()),
0,
)
.with_type("part".to_string());
outline.entries = vec![part1, part2];
assert_eq!(outline.max_depth(), 2);
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); }
#[test]
fn test0027_flatten_preserves_hierarchy() {
let mut outline = ListingOutline::new();
let mut part = OutlineEntry::new("Part I".to_string(), Some("1".to_string()), 0);
let mut chapter = OutlineEntry::new("Chapter 1".to_string(), Some("3".to_string()), 1);
chapter.add_child(OutlineEntry::new(
"Section 1.1".to_string(),
Some("3".to_string()),
2,
));
part.add_child(chapter);
outline.entries = vec![part];
let flat = outline.flatten();
assert_eq!(flat.len(), 3);
assert_eq!(flat[0].path, vec!["Part I"]);
assert_eq!(flat[1].path, vec!["Part I", "Chapter 1"]);
assert_eq!(flat[2].path, vec!["Part I", "Chapter 1", "Section 1.1"]);
}
#[test]
fn test0028_schema_generation() {
let schema = generate_outline_schema();
assert!(schema.is_object());
assert!(schema["properties"]["entries"].is_object());
assert!(schema["definitions"]["OutlineEntry"].is_object());
assert!(schema["definitions"]["OutlineMetadata"].is_object());
}
}