1use std::{collections::HashSet, path::PathBuf};
2
3use crate::block::{Block, BlockType, DocumentId, PropertyValue};
4
5#[derive(Debug, Clone, Default, PartialEq)]
10pub struct ZoneMaps {
11 pub max_heading_depth: u8,
13 pub heading_slugs: HashSet<String>,
16 pub heading_contents: HashSet<String>,
18 pub code_languages: HashSet<String>,
20 pub frontmatter_keys: HashSet<String>,
22 pub title: Option<String>,
24 pub tags: Vec<String>,
26}
27
28#[derive(Debug, Clone, PartialEq)]
30pub struct Document {
31 pub id: DocumentId,
32 pub path: Option<PathBuf>,
34 pub blocks: Vec<Block>,
36 pub block_count: u32,
38 pub zone_maps: ZoneMaps,
40 pub(crate) first_block_page: u32,
42 pub(crate) index_start_page: u32,
44}
45
46impl Document {
47 pub fn new(id: DocumentId, path: Option<PathBuf>, blocks: Vec<Block>) -> Self {
48 let zone_maps = ZoneMaps::build(&blocks);
49 Self::from_parts(id, path, blocks, zone_maps)
50 }
51
52 pub fn from_parts(
53 id: DocumentId,
54 path: Option<PathBuf>,
55 blocks: Vec<Block>,
56 zone_maps: ZoneMaps,
57 ) -> Self {
58 let block_count = blocks.len() as u32;
59 Self {
60 id,
61 path,
62 blocks,
63 block_count,
64 zone_maps,
65 first_block_page: 0,
66 index_start_page: 0,
67 }
68 }
69
70 pub fn from_catalog(
72 id: DocumentId,
73 path: Option<PathBuf>,
74 block_count: u32,
75 zone_maps: ZoneMaps,
76 ) -> Self {
77 Self {
78 id,
79 path,
80 blocks: Vec::new(),
81 block_count,
82 zone_maps,
83 first_block_page: 0,
84 index_start_page: 0,
85 }
86 }
87
88 pub(crate) fn from_catalog_lazy(
90 id: DocumentId,
91 path: Option<PathBuf>,
92 block_count: u32,
93 zone_maps: ZoneMaps,
94 first_block_page: u32,
95 index_start_page: u32,
96 ) -> Self {
97 Self {
98 id,
99 path,
100 blocks: Vec::new(),
101 block_count,
102 zone_maps,
103 first_block_page,
104 index_start_page,
105 }
106 }
107
108 pub fn next_sibling<'a>(&'a self, block: &Block) -> Option<&'a Block> {
113 let target_pre = block.post + 1;
114 self.blocks.iter().find(|b| b.pre == target_pre)
115 }
116
117 pub fn first_child<'a>(&'a self, block: &Block) -> Option<&'a Block> {
122 let target_pre = block.pre + 1;
123 self.blocks.iter().find(|b| b.pre == target_pre)
124 }
125
126 pub fn descendants_of<'a>(&'a self, ancestor: &Block) -> impl Iterator<Item = &'a Block> {
129 let (anc_pre, anc_post) = (ancestor.pre, ancestor.post);
130 self.blocks
131 .iter()
132 .filter(move |b| b.is_under_interval(anc_pre, anc_post))
133 }
134}
135
136impl ZoneMaps {
137 pub fn build(blocks: &[Block]) -> Self {
138 let mut maps = ZoneMaps::default();
139
140 for block in blocks {
141 match &block.block_type {
142 BlockType::Heading => {
143 if let Some(d) = block.heading_depth() {
144 maps.max_heading_depth = maps.max_heading_depth.max(d);
145 }
146 maps.heading_contents.insert(block.content.clone());
147 if let Some(PropertyValue::String(slug)) = block.properties.get("slug") {
148 maps.heading_slugs.insert(slug.clone());
149 }
150 if block.heading_depth() == Some(1) && maps.title.is_none() {
152 maps.title = Some(block.content.clone());
153 }
154 }
155 BlockType::Code => {
156 if let Some(lang) = block.code_lang() {
157 maps.code_languages.insert(lang.to_string());
158 }
159 }
160 BlockType::Yaml | BlockType::Toml => {
161 for (k, v) in block.properties.iter() {
162 maps.frontmatter_keys.insert(k.clone());
163 if k == "title"
164 && let PropertyValue::String(s) = v
165 {
166 maps.title = Some(s.clone());
167 }
168 if k == "tags"
169 && let PropertyValue::Array(arr) = v
170 {
171 maps.tags = arr
172 .iter()
173 .filter_map(|pv| pv.as_str().map(|s| s.to_string()))
174 .collect();
175 }
176 }
177 }
178 _ => {}
179 }
180 }
181
182 maps
183 }
184}