Skip to main content

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}