1use crate::field::document_format::DocumentFormat;
12use crate::field::observe::{Representation, Selector};
13
14#[derive(Debug, Clone, Copy)]
16pub struct SelectorCapability {
17 pub selector: &'static str,
19 pub representations: &'static [&'static str],
21}
22
23#[derive(Debug, Clone)]
25pub struct Capabilities {
26 pub format: DocumentFormat,
28 pub compiled: bool,
30 pub selectors: Vec<SelectorCapability>,
32 pub profiles: Vec<String>,
34 pub native_selectors: Vec<&'static str>,
36}
37
38impl Capabilities {
39 pub fn to_json(&self) -> String {
41 let selectors = self
42 .selectors
43 .iter()
44 .map(|s| {
45 let reps = s
46 .representations
47 .iter()
48 .map(|r| format!("\"{r}\""))
49 .collect::<Vec<_>>()
50 .join(",");
51 format!(
52 "{{\"selector\":\"{}\",\"representations\":[{reps}]}}",
53 s.selector
54 )
55 })
56 .collect::<Vec<_>>()
57 .join(",");
58 let profiles = self
59 .profiles
60 .iter()
61 .map(|p| format!("\"{}\"", crate::field::provenance::json_escape(p)))
62 .collect::<Vec<_>>()
63 .join(",");
64 let native = self
65 .native_selectors
66 .iter()
67 .map(|s| format!("\"{s}\""))
68 .collect::<Vec<_>>()
69 .join(",");
70 format!(
71 concat!(
72 "{{",
73 "\"root\":\"document\",",
74 "\"format\":\"{}\",",
75 "\"adapter\":\"{}\",",
76 "\"compiled\":{},",
77 "\"selectors\":[{}],",
78 "\"profiles\":[{}],",
79 "\"native_selectors\":[{}]",
80 "}}"
81 ),
82 self.format.name(),
83 self.format.adapter(),
84 self.compiled,
85 selectors,
86 profiles,
87 native
88 )
89 }
90}
91
92const DOCX_NATIVE: &[&str] = &[
93 "docx-story",
94 "docx-paragraph",
95 "docx-table",
96 "docx-cell",
97 "docx-find",
98 "package-part",
99 "relationship",
100 "member",
101];
102const EPUB_NATIVE: &[&str] = &[
103 "epub-package",
104 "epub-manifest-item",
105 "epub-spine-item",
106 "epub-nav",
107 "epub-nav-node",
108 "epub-resource",
109 "epub-block",
110 "epub-cell",
111 "epub-link",
112 "epub-find",
113 "member",
114];
115const ODT_NATIVE: &[&str] = &[
116 "odt-part",
117 "odt-paragraph",
118 "odt-heading",
119 "odt-table",
120 "odt-cell",
121 "odt-list",
122 "odt-find",
123 "package-part",
124 "member",
125];
126const PDF_NATIVE: &[&str] = &[
127 "document",
128 "object",
129 "stream",
130 "revision",
131 "page",
132 "byte-range",
133 "text-match",
134];
135
136const COMMON_METADATA: &[&str] = &["metadata"];
137const COMMON_TEXT: &[&str] = &["text"];
138const COMMON_TEXT_META: &[&str] = &["text", "metadata"];
139const COMMON_RESOURCE: &[&str] = &["metadata"];
140const COMMON_RESOURCE_BYTES: &[&str] = &["metadata", "exact", "decoded"];
141const SEARCH: &[&str] = &["text"];
142
143fn caps(selector: &'static str, representations: &'static [&'static str]) -> SelectorCapability {
144 SelectorCapability {
145 selector,
146 representations,
147 }
148}
149
150pub fn capabilities_for_format(format: DocumentFormat) -> Capabilities {
152 let compiled = format.compiled();
153 if !compiled {
154 return Capabilities {
155 format,
156 compiled,
157 selectors: Vec::new(),
158 profiles: Vec::new(),
159 native_selectors: Vec::new(),
160 };
161 }
162 let (selectors, profiles, native_selectors): (Vec<SelectorCapability>, Vec<String>, Vec<&str>) =
163 match format {
164 DocumentFormat::Pdf => (
165 vec![
166 caps("metadata", COMMON_METADATA),
167 caps("text", COMMON_TEXT),
168 caps("search-match", SEARCH),
169 ],
170 Vec::new(),
171 PDF_NATIVE.to_vec(),
172 ),
173 DocumentFormat::Docx => (
174 vec![
175 caps("metadata", COMMON_METADATA),
176 caps("text", COMMON_TEXT),
177 caps("heading", COMMON_TEXT_META),
178 caps("block", COMMON_TEXT_META),
179 caps("table", COMMON_TEXT_META),
180 caps("cell", COMMON_TEXT_META),
181 caps("resource", COMMON_RESOURCE),
182 caps("link", COMMON_METADATA),
183 caps("search-match", SEARCH),
184 ],
185 docx_profiles(),
186 DOCX_NATIVE.to_vec(),
187 ),
188 DocumentFormat::Epub => (
189 vec![
190 caps("metadata", COMMON_METADATA),
191 caps("text", COMMON_TEXT),
192 caps("heading", COMMON_TEXT_META),
193 caps("block", COMMON_TEXT_META),
194 caps("table", COMMON_TEXT_META),
195 caps("cell", COMMON_TEXT_META),
196 caps("resource", COMMON_RESOURCE_BYTES),
197 caps("link", COMMON_METADATA),
198 caps("search-match", SEARCH),
199 ],
200 epub_profiles(),
201 EPUB_NATIVE.to_vec(),
202 ),
203 DocumentFormat::Odt => (
204 vec![
205 caps("metadata", COMMON_METADATA),
206 caps("text", COMMON_TEXT),
207 caps("heading", COMMON_TEXT_META),
208 caps("block", COMMON_TEXT_META),
209 caps("table", COMMON_TEXT_META),
210 caps("cell", COMMON_TEXT_META),
211 caps("resource", COMMON_RESOURCE),
212 caps("link", COMMON_METADATA),
213 caps("search-match", SEARCH),
214 ],
215 odt_profiles(),
216 ODT_NATIVE.to_vec(),
217 ),
218 DocumentFormat::Opaque => (Vec::new(), Vec::new(), Vec::new()),
219 };
220 Capabilities {
221 format,
222 compiled,
223 selectors,
224 profiles,
225 native_selectors,
226 }
227}
228
229#[cfg(feature = "docx")]
230fn docx_profiles() -> Vec<String> {
231 vec![crate::adapter::docx::DocxExtractProfile::DEFAULT.fingerprint()]
232}
233#[cfg(not(feature = "docx"))]
234fn docx_profiles() -> Vec<String> {
235 Vec::new()
236}
237
238#[cfg(feature = "epub")]
239fn epub_profiles() -> Vec<String> {
240 vec![crate::adapter::epub::EpubExtractProfile::DEFAULT.fingerprint()]
241}
242#[cfg(not(feature = "epub"))]
243fn epub_profiles() -> Vec<String> {
244 Vec::new()
245}
246
247#[cfg(feature = "odt")]
248fn odt_profiles() -> Vec<String> {
249 vec![crate::adapter::odt::OdtExtractProfile::DEFAULT.fingerprint()]
250}
251#[cfg(not(feature = "odt"))]
252fn odt_profiles() -> Vec<String> {
253 Vec::new()
254}
255
256pub fn common_selector_name(selector: &Selector) -> Option<&'static str> {
258 match selector {
259 Selector::Metadata => Some("metadata"),
260 Selector::Text => Some("text"),
261 Selector::Heading(_) => Some("heading"),
262 Selector::Block(_) => Some("block"),
263 Selector::Table(_) => Some("table"),
264 Selector::Cell { .. } => Some("cell"),
265 Selector::Resource(_) => Some("resource"),
266 Selector::Link(_) => Some("link"),
267 Selector::SearchMatch(_) => Some("search-match"),
268 _ => None,
269 }
270}
271
272pub fn common_representations(
275 format: DocumentFormat,
276 selector: &Selector,
277) -> Option<&'static [&'static str]> {
278 if !format.compiled() {
279 return None;
280 }
281 let name = common_selector_name(selector)?;
282 let caps = capabilities_for_format(format);
283 caps.selectors
284 .iter()
285 .find(|c| c.selector == name)
286 .map(|c| c.representations)
287}
288
289pub fn common_supported(
291 format: DocumentFormat,
292 selector: &Selector,
293 representation: Representation,
294) -> bool {
295 common_representations(format, selector)
296 .is_some_and(|reps| reps.contains(&representation.name()))
297}
298
299#[cfg(test)]
300mod tests {
301 use super::*;
302
303 #[test]
304 fn pdf_supports_text_and_search_but_not_tables() {
305 assert!(common_supported(
306 DocumentFormat::Pdf,
307 &Selector::Text,
308 Representation::Text
309 ));
310 assert!(common_supported(
311 DocumentFormat::Pdf,
312 &Selector::SearchMatch("x".into()),
313 Representation::Text
314 ));
315 assert!(!common_supported(
316 DocumentFormat::Pdf,
317 &Selector::Table(0),
318 Representation::Text
319 ));
320 assert!(!common_supported(
321 DocumentFormat::Pdf,
322 &Selector::Text,
323 Representation::Metadata
324 ));
325 }
326
327 #[test]
328 fn opaque_and_unknown_selectors_are_unsupported() {
329 assert!(!common_supported(
330 DocumentFormat::Opaque,
331 &Selector::Text,
332 Representation::Text
333 ));
334 assert!(!common_supported(
335 DocumentFormat::Pdf,
336 &Selector::Document,
337 Representation::Metadata
338 ));
339 }
340}