1use crate::{ImageData, ListItem, PdfBlockType, SourceLocation};
4
5#[derive(Clone, Debug, PartialEq)]
11#[non_exhaustive]
12pub enum PdfBlock {
13 Heading {
15 level: u8,
17 text: String,
19 source: SourceLocation,
21 },
22 Paragraph {
24 text: String,
26 source: SourceLocation,
28 },
29 List {
31 ordered: bool,
33 items: Vec<ListItem>,
35 source: SourceLocation,
37 },
38 Table {
40 headers: Vec<String>,
42 rows: Vec<Vec<String>>,
44 source: SourceLocation,
46 },
47 Image {
49 data: ImageData,
51 source: SourceLocation,
53 },
54 Code {
56 language: Option<String>,
58 text: String,
60 source: SourceLocation,
62 },
63 Formula {
65 latex: String,
67 source: SourceLocation,
69 },
70 PageBreak {
72 source: SourceLocation,
74 },
75 Footnote {
77 reference_id: String,
79 text: String,
81 source: SourceLocation,
83 },
84 TableCell {
86 row_span: u32,
88 col_span: u32,
90 text: String,
92 source: SourceLocation,
94 },
95 BlockQuote {
97 text: String,
99 source: SourceLocation,
101 },
102 HorizontalRule {
104 source: SourceLocation,
106 },
107 Link {
109 url: String,
111 text: String,
113 source: SourceLocation,
115 },
116 Unknown {
118 raw: String,
120 source: SourceLocation,
122 },
123}
124
125impl PdfBlock {
126 #[must_use]
132 pub fn heading(level: u8, text: impl Into<String>, source: SourceLocation) -> Self {
133 Self::Heading {
134 level,
135 text: text.into(),
136 source,
137 }
138 }
139
140 #[must_use]
142 pub fn paragraph(text: impl Into<String>, source: SourceLocation) -> Self {
143 Self::Paragraph {
144 text: text.into(),
145 source,
146 }
147 }
148
149 #[must_use]
151 pub fn list(ordered: bool, items: Vec<ListItem>, source: SourceLocation) -> Self {
152 Self::List {
153 ordered,
154 items,
155 source,
156 }
157 }
158
159 #[must_use]
161 pub fn table(headers: Vec<String>, rows: Vec<Vec<String>>, source: SourceLocation) -> Self {
162 Self::Table {
163 headers,
164 rows,
165 source,
166 }
167 }
168
169 #[must_use]
171 pub fn image(data: ImageData, source: SourceLocation) -> Self {
172 Self::Image { data, source }
173 }
174
175 #[must_use]
177 pub fn code(text: impl Into<String>, source: SourceLocation) -> Self {
178 Self::Code {
179 language: None,
180 text: text.into(),
181 source,
182 }
183 }
184
185 #[must_use]
187 pub fn code_with_language(
188 language: impl Into<String>,
189 text: impl Into<String>,
190 source: SourceLocation,
191 ) -> Self {
192 Self::Code {
193 language: Some(language.into()),
194 text: text.into(),
195 source,
196 }
197 }
198
199 #[must_use]
201 pub fn formula(latex: impl Into<String>, source: SourceLocation) -> Self {
202 Self::Formula {
203 latex: latex.into(),
204 source,
205 }
206 }
207
208 #[must_use]
210 pub const fn page_break(source: SourceLocation) -> Self {
211 Self::PageBreak { source }
212 }
213
214 #[must_use]
216 pub fn footnote(
217 ref_id: impl Into<String>,
218 text: impl Into<String>,
219 source: SourceLocation,
220 ) -> Self {
221 Self::Footnote {
222 reference_id: ref_id.into(),
223 text: text.into(),
224 source,
225 }
226 }
227
228 #[must_use]
230 pub fn table_cell(
231 row_span: u32,
232 col_span: u32,
233 text: impl Into<String>,
234 source: SourceLocation,
235 ) -> Self {
236 Self::TableCell {
237 row_span,
238 col_span,
239 text: text.into(),
240 source,
241 }
242 }
243
244 #[must_use]
246 pub fn block_quote(text: impl Into<String>, source: SourceLocation) -> Self {
247 Self::BlockQuote {
248 text: text.into(),
249 source,
250 }
251 }
252
253 #[must_use]
255 pub const fn horizontal_rule(source: SourceLocation) -> Self {
256 Self::HorizontalRule { source }
257 }
258
259 #[must_use]
261 pub fn link(url: impl Into<String>, text: impl Into<String>, source: SourceLocation) -> Self {
262 Self::Link {
263 url: url.into(),
264 text: text.into(),
265 source,
266 }
267 }
268
269 #[must_use]
271 pub fn unknown(raw: impl Into<String>, source: SourceLocation) -> Self {
272 Self::Unknown {
273 raw: raw.into(),
274 source,
275 }
276 }
277
278 #[must_use]
284 pub const fn source(&self) -> &SourceLocation {
285 match self {
286 Self::Heading { source, .. }
287 | Self::Paragraph { source, .. }
288 | Self::List { source, .. }
289 | Self::Table { source, .. }
290 | Self::Image { source, .. }
291 | Self::Code { source, .. }
292 | Self::Formula { source, .. }
293 | Self::PageBreak { source }
294 | Self::Footnote { source, .. }
295 | Self::TableCell { source, .. }
296 | Self::BlockQuote { source, .. }
297 | Self::HorizontalRule { source }
298 | Self::Link { source, .. }
299 | Self::Unknown { source, .. } => source,
300 }
301 }
302
303 #[must_use]
316 pub const fn block_type(&self) -> PdfBlockType {
317 match self {
318 Self::Heading { .. } => PdfBlockType::Heading,
319 Self::Paragraph { .. } => PdfBlockType::Paragraph,
320 Self::List { .. } => PdfBlockType::List,
321 Self::Table { .. } => PdfBlockType::Table,
322 Self::Image { .. } => PdfBlockType::Image,
323 Self::Code { .. } => PdfBlockType::Code,
324 Self::Formula { .. } => PdfBlockType::Formula,
325 Self::PageBreak { .. } => PdfBlockType::PageBreak,
326 Self::Footnote { .. } => PdfBlockType::Footnote,
327 Self::TableCell { .. } => PdfBlockType::TableCell,
328 Self::BlockQuote { .. } => PdfBlockType::BlockQuote,
329 Self::HorizontalRule { .. } => PdfBlockType::HorizontalRule,
330 Self::Link { .. } => PdfBlockType::Link,
331 Self::Unknown { .. } => PdfBlockType::Unknown,
332 }
333 }
334}
335
336#[cfg(test)]
337mod tests {
338 use super::*;
339 use crate::PageIndex;
340
341 fn loc() -> SourceLocation {
342 SourceLocation::new(PageIndex::new(0), 1.0)
343 }
344
345 #[test]
346 fn heading_construction() {
347 let b = PdfBlock::heading(2, "Section", loc());
348 assert_eq!(b.block_type(), PdfBlockType::Heading);
349 assert_eq!(b.source().page_index().value(), 0);
350 }
351
352 #[test]
353 fn paragraph_construction() {
354 let b = PdfBlock::paragraph("Hello", loc());
355 assert_eq!(b.block_type(), PdfBlockType::Paragraph);
356 }
357
358 #[test]
359 fn list_construction() {
360 let items = vec![ListItem::new("A"), ListItem::new("B")];
361 let b = PdfBlock::list(false, items, loc());
362 assert_eq!(b.block_type(), PdfBlockType::List);
363 }
364
365 #[test]
366 fn table_construction() {
367 let b = PdfBlock::table(vec!["H".into()], vec![vec!["C".into()]], loc());
368 assert_eq!(b.block_type(), PdfBlockType::Table);
369 }
370
371 #[test]
372 fn image_construction() {
373 let data = ImageData::new(crate::ImageFormat::Png);
374 let b = PdfBlock::image(data, loc());
375 assert_eq!(b.block_type(), PdfBlockType::Image);
376 }
377
378 #[test]
379 fn code_construction() {
380 let b = PdfBlock::code("fn main() {}", loc());
381 assert_eq!(b.block_type(), PdfBlockType::Code);
382 }
383
384 #[test]
385 fn code_with_language_construction() {
386 let b = PdfBlock::code_with_language("rust", "fn main() {}", loc());
387 assert_eq!(b.block_type(), PdfBlockType::Code);
388 }
389
390 #[test]
391 fn formula_construction() {
392 let b = PdfBlock::formula("E = mc^2", loc());
393 assert_eq!(b.block_type(), PdfBlockType::Formula);
394 }
395
396 #[test]
397 fn page_break_construction() {
398 let b = PdfBlock::page_break(loc());
399 assert_eq!(b.block_type(), PdfBlockType::PageBreak);
400 }
401
402 #[test]
403 fn footnote_construction() {
404 let b = PdfBlock::footnote("1", "See page 5", loc());
405 assert_eq!(b.block_type(), PdfBlockType::Footnote);
406 }
407
408 #[test]
409 fn table_cell_construction() {
410 let b = PdfBlock::table_cell(2, 1, "Merged", loc());
411 assert_eq!(b.block_type(), PdfBlockType::TableCell);
412 }
413
414 #[test]
415 fn block_quote_construction() {
416 let b = PdfBlock::block_quote("A wise saying", loc());
417 assert_eq!(b.block_type(), PdfBlockType::BlockQuote);
418 }
419
420 #[test]
421 fn horizontal_rule_construction() {
422 let b = PdfBlock::horizontal_rule(loc());
423 assert_eq!(b.block_type(), PdfBlockType::HorizontalRule);
424 }
425
426 #[test]
427 fn link_construction() {
428 let b = PdfBlock::link("https://example.com", "Example", loc());
429 assert_eq!(b.block_type(), PdfBlockType::Link);
430 }
431
432 #[test]
433 fn unknown_construction() {
434 let b = PdfBlock::unknown("???binary???", loc());
435 assert_eq!(b.block_type(), PdfBlockType::Unknown);
436 }
437
438 #[test]
439 fn source_returns_correct_location() {
440 let loc2 = SourceLocation::new(PageIndex::new(3), 0.85);
441 let b = PdfBlock::paragraph("test", loc2);
442 assert_eq!(b.source().page_index().value(), 3);
443 assert!((b.source().confidence() - 0.85).abs() < f32::EPSILON);
444 }
445
446 #[test]
447 fn block_type_covers_all_variants() {
448 let loc = loc();
449 let variants = [
450 PdfBlock::heading(1, "h", loc),
451 PdfBlock::paragraph("p", loc),
452 PdfBlock::list(false, vec![], loc),
453 PdfBlock::table(vec![], vec![], loc),
454 PdfBlock::image(ImageData::new(crate::ImageFormat::Png), loc),
455 PdfBlock::code("c", loc),
456 PdfBlock::formula("f", loc),
457 PdfBlock::page_break(loc),
458 PdfBlock::footnote("1", "t", loc),
459 PdfBlock::table_cell(1, 1, "t", loc),
460 PdfBlock::block_quote("q", loc),
461 PdfBlock::horizontal_rule(loc),
462 PdfBlock::link("u", "t", loc),
463 PdfBlock::unknown("r", loc),
464 ];
465 let types: Vec<_> = variants.iter().map(PdfBlock::block_type).collect();
466 assert_eq!(types.len(), 14);
467 for i in 0..types.len() {
469 for j in (i + 1)..types.len() {
470 assert_ne!(types[i], types[j], "duplicate at {i} vs {j}");
471 }
472 }
473 }
474
475 #[test]
476 fn clone_and_eq() {
477 let a = PdfBlock::paragraph("hello", loc());
478 let b = a.clone();
479 assert_eq!(a, b);
480 }
481}