kopitiam_document/document.rs
1use crate::{Block, Citation};
2
3#[derive(Debug, Clone, Default, PartialEq)]
4pub struct Metadata {
5 pub source_pages: usize,
6}
7
8#[derive(Debug, Clone, Default, PartialEq)]
9pub struct Document {
10 pub title: Option<String>,
11 pub metadata: Metadata,
12 pub blocks: Vec<Block>,
13
14 /// The 1-based page each block **starts** on, parallel to [`Self::blocks`]
15 /// (`block_pages[i]` is the page of `blocks[i]`).
16 ///
17 /// # Why this exists
18 ///
19 /// Reconstruction always *knew* the page — it builds one block list per
20 /// page and then flattens them — and until now it simply threw that away.
21 /// That made the Document Engine unusable for every consumer that has to
22 /// **cite** what it extracted: a citation without a page is not one a
23 /// reader can follow, and "provenance" that cannot be checked is a promise
24 /// rather than a fact.
25 ///
26 /// This was found the hard way. The `kopitiam-legal` engine needed page
27 /// provenance for statutes, could not get it, and worked around the gap by
28 /// calling `reconstruct()` **one page at a time** so the page was known by
29 /// construction — which silently gave up the cross-page paragraph merging
30 /// (`merge_page_breaks`) that legal text needs constantly, since statutes
31 /// split provisions across pages all the time. It paid a real cost to
32 /// recover a number this crate already had.
33 ///
34 /// Every provenance-carrying consumer needs it — legal, insurance, health,
35 /// finance, literature all cite by page — so it belongs here rather than
36 /// being rediscovered and worked around five more times.
37 ///
38 /// # A block that spans a page break
39 ///
40 /// Records the page it **started** on. A paragraph merged across a page
41 /// break by [`crate::reconstruct`] therefore reports the earlier page,
42 /// which is the right answer for a citation: it is where a reader should
43 /// begin looking.
44 pub block_pages: Vec<usize>,
45
46 /// Citations detected in paragraph text, for provenance reporting. See
47 /// [`Citation`] -- these are pointers into already-rendered text, not
48 /// separate content.
49 pub citations: Vec<Citation>,
50}
51
52impl Document {
53 /// The 1-based page that `blocks[index]` starts on.
54 ///
55 /// Returns `None` for an out-of-range index, and — deliberately — also for
56 /// a `Document` constructed without page information (e.g. via
57 /// `Default`), rather than guessing page 1. A wrong page in a citation is
58 /// worse than an absent one: it sends a reader to the wrong place and looks
59 /// authoritative doing it.
60 pub fn page_of(&self, index: usize) -> Option<usize> {
61 self.block_pages.get(index).copied()
62 }
63
64 /// Iterates blocks paired with the page each starts on.
65 ///
66 /// Yields `None` for the page when a `Document` carries no page
67 /// information, so a consumer that requires provenance can detect that
68 /// rather than silently attribute everything to page 1.
69 pub fn blocks_with_pages(&self) -> impl Iterator<Item = (&Block, Option<usize>)> {
70 self.blocks.iter().enumerate().map(|(i, block)| (block, self.page_of(i)))
71 }
72}