Skip to main content

vole_document/field/
capabilities.rs

1//! Machine-readable capability discovery for the common observation layer
2//! (Phase 12.7, ADR-0031, plan §DEC-5).
3//!
4//! Formats do not share coordinates, so the set of *common* observations a
5//! format can serve is explicit and auditable rather than silently approximated.
6//! [`capabilities_for_format`] returns the supported common selectors and their
7//! admissible representations, the versioned extraction profiles, and the native
8//! selectors; [`common_supported`] is the single predicate the planner and the
9//! evaluator both use, so a capability error is never a surprise.
10
11use crate::field::document_format::DocumentFormat;
12use crate::field::observe::{Representation, Selector};
13
14/// One common selector and the representations it admits for a format.
15#[derive(Debug, Clone, Copy)]
16pub struct SelectorCapability {
17    /// Canonical common selector name.
18    pub selector: &'static str,
19    /// Admissible representation names.
20    pub representations: &'static [&'static str],
21}
22
23/// The capability set for one detected format.
24#[derive(Debug, Clone)]
25pub struct Capabilities {
26    /// The detected format.
27    pub format: DocumentFormat,
28    /// Whether the adapter for this format is compiled into this build.
29    pub compiled: bool,
30    /// Supported common selectors (empty when the adapter is not compiled).
31    pub selectors: Vec<SelectorCapability>,
32    /// Versioned extraction-profile fingerprints (DOCX/EPUB).
33    pub profiles: Vec<String>,
34    /// Native selectors retained as first-class peers of the common vocabulary.
35    pub native_selectors: Vec<&'static str>,
36}
37
38impl Capabilities {
39    /// A deterministic, flat JSON object (machine-readable capability discovery).
40    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    "revisions",
132    "page",
133    "byte-range",
134    "text-match",
135];
136const ODS_NATIVE: &[&str] = &[
137    "ods-sheet",
138    "ods-cell",
139    "ods-find",
140    "ods-styles",
141    "ods-named-expressions",
142    "ods-comments",
143    "package-part",
144    "member",
145];
146const XLSX_NATIVE: &[&str] = &[
147    "xlsx-sheet",
148    "xlsx-cell",
149    "xlsx-find",
150    "xlsx-styles",
151    "xlsx-defined-names",
152    "xlsx-external-rels",
153    "xlsx-comments",
154    "xlsx-hyperlinks",
155    "xlsx-tables",
156    "xlsx-drawing",
157    "package-part",
158    "relationship",
159    "member",
160];
161const PPTX_NATIVE: &[&str] = &[
162    "pptx-slide",
163    "pptx-shape",
164    "pptx-notes",
165    "pptx-layouts",
166    "pptx-masters",
167    "pptx-theme",
168    "pptx-media",
169    "pptx-tables",
170    "pptx-find",
171    "package-part",
172    "relationship",
173    "member",
174];
175const ODP_NATIVE: &[&str] = &[
176    "odp-slide",
177    "odp-shape",
178    "odp-notes",
179    "odp-masters",
180    "odp-media",
181    "odp-tables",
182    "odp-find",
183    "package-part",
184    "member",
185];
186const JSON_NATIVE: &[&str] = &["json-pointer", "json-node", "json-find"];
187const JSON5_NATIVE: &[&str] = &[
188    "json5-pointer",
189    "json5-node",
190    "json5-find",
191    "json5-comments",
192];
193const YAML_NATIVE: &[&str] = &[
194    "yaml-path",
195    "yaml-node",
196    "yaml-documents",
197    "yaml-anchor",
198    "yaml-find",
199];
200const CSV_NATIVE: &[&str] = &["csv-row", "csv-cell", "csv-header", "csv-range", "csv-find"];
201const MARKDOWN_NATIVE: &[&str] = &["md-heading", "md-block", "md-code", "md-link", "md-find"];
202const XML_NATIVE: &[&str] = &[
203    "xml-path",
204    "xml-element",
205    "xml-attr",
206    "xml-namespaces",
207    "xml-find",
208];
209const HTML_NATIVE: &[&str] = &[
210    "html-path",
211    "html-element",
212    "html-attr",
213    "html-scripts",
214    "html-find",
215];
216const TOML_NATIVE: &[&str] = &["toml-path", "toml-table", "toml-find"];
217const JSONL_NATIVE: &[&str] = &["jsonl-line", "jsonl-pointer", "jsonl-find"];
218const EML_NATIVE: &[&str] = &[
219    "eml-header",
220    "eml-part",
221    "eml-attachments",
222    "eml-body",
223    "eml-find",
224];
225const PARQUET_NATIVE: &[&str] = &[
226    "parquet-schema",
227    "parquet-column",
228    "parquet-row-group",
229    "parquet-cell",
230];
231const ARROW_NATIVE: &[&str] = &["arrow-schema", "arrow-column", "arrow-batch", "arrow-cell"];
232const CBOR_NATIVE: &[&str] = &["cbor-pointer", "cbor-node", "cbor-find"];
233const MSGPACK_NATIVE: &[&str] = &["msgpack-pointer", "msgpack-node", "msgpack-find"];
234const CONFIG_NATIVE: &[&str] = &[
235    "config-line",
236    "config-entry",
237    "config-section",
238    "config-find",
239];
240const FEED_NATIVE: &[&str] = &[
241    "feed-channel",
242    "feed-field",
243    "feed-entry",
244    "feed-entry-field",
245    "feed-find",
246];
247const GEOJSON_NATIVE: &[&str] = &[
248    "geojson-type",
249    "geojson-feature",
250    "geojson-geometry",
251    "geojson-coordinates",
252    "geojson-property",
253    "geojson-find",
254];
255const GIS_NATIVE: &[&str] = &[
256    "gis-root",
257    "gis-field",
258    "gis-record",
259    "gis-record-field",
260    "gis-point",
261    "gis-find",
262];
263const NOTEBOOK_NATIVE: &[&str] = &[
264    "notebook-nbformat",
265    "notebook-cell",
266    "notebook-cell-type",
267    "notebook-cell-source",
268    "notebook-cell-output",
269    "notebook-find",
270];
271const FIXEDWIDTH_NATIVE: &[&str] = &[
272    "fixedwidth-row",
273    "fixedwidth-cell",
274    "fixedwidth-header",
275    "fixedwidth-columns",
276    "fixedwidth-range",
277    "fixedwidth-find",
278];
279const RST_NATIVE: &[&str] = &[
280    "rst-heading",
281    "rst-block",
282    "rst-directive",
283    "rst-inline",
284    "rst-find",
285];
286const ASCIIDOC_NATIVE: &[&str] = &[
287    "adoc-heading",
288    "adoc-block",
289    "adoc-attribute",
290    "adoc-inline",
291    "adoc-find",
292];
293const MDX_NATIVE: &[&str] = &[
294    "mdx-heading",
295    "mdx-block",
296    "mdx-esm",
297    "mdx-jsx",
298    "mdx-expression",
299    "mdx-find",
300];
301
302const MHTML_NATIVE: &[&str] = &[
303    "mhtml-root",
304    "mhtml-resource",
305    "mhtml-location",
306    "mhtml-find",
307];
308
309const LOGSTREAM_NATIVE: &[&str] = &["logstream-line", "logstream-field", "logstream-find"];
310
311const PKGMETA_NATIVE: &[&str] = &[
312    "pkgmeta-section",
313    "pkgmeta-entry",
314    "pkgmeta-key",
315    "pkgmeta-find",
316];
317
318const APISPEC_NATIVE: &[&str] = &[
319    "apispec-dialect",
320    "apispec-version",
321    "apispec-object",
322    "apispec-ref",
323    "apispec-find",
324];
325
326const BIB_NATIVE: &[&str] = &[
327    "bib-dialect",
328    "bib-entry",
329    "bib-field",
330    "bib-key",
331    "bib-find",
332];
333
334const RTF_NATIVE: &[&str] = &[
335    "rtf-header",
336    "rtf-group",
337    "rtf-control",
338    "rtf-hex",
339    "rtf-unicode",
340    "rtf-text",
341    "rtf-dest",
342    "rtf-find",
343];
344
345const CONTAINER_NATIVE: &[&str] = &["container-dialect", "container-entry", "container-find"];
346
347const HDF_NATIVE: &[&str] = &[
348    "hdf-dialect",
349    "hdf-object",
350    "hdf-dimension",
351    "hdf-attribute",
352    "hdf-find",
353];
354
355const CFBF_NATIVE: &[&str] = &["cfbf-dialect", "cfbf-entry", "cfbf-stream", "cfbf-find"];
356
357const DOC_NATIVE: &[&str] = &["doc-fib", "doc-piece", "doc-text", "doc-find"];
358
359const XLS_NATIVE: &[&str] = &[
360    "xls-biff",
361    "xls-sheet",
362    "xls-cell",
363    "xls-record",
364    "xls-find",
365];
366
367const PPT_NATIVE: &[&str] = &[
368    "ppt-document",
369    "ppt-slide",
370    "ppt-run",
371    "ppt-record",
372    "ppt-find",
373];
374
375const MSG_NATIVE: &[&str] = &[
376    "msg-summary",
377    "msg-property",
378    "msg-recipient",
379    "msg-attachment",
380    "msg-find",
381];
382
383const COMMON_METADATA: &[&str] = &["metadata"];
384const COMMON_TEXT: &[&str] = &["text"];
385const COMMON_TEXT_META: &[&str] = &["text", "metadata"];
386const COMMON_RESOURCE: &[&str] = &["metadata"];
387const COMMON_RESOURCE_BYTES: &[&str] = &["metadata", "exact", "decoded"];
388const SEARCH: &[&str] = &["text"];
389
390fn caps(selector: &'static str, representations: &'static [&'static str]) -> SelectorCapability {
391    SelectorCapability {
392        selector,
393        representations,
394    }
395}
396
397/// The capability set for a detected format, filtered by what this build compiles.
398pub fn capabilities_for_format(format: DocumentFormat) -> Capabilities {
399    let compiled = format.compiled();
400    if !compiled {
401        return Capabilities {
402            format,
403            compiled,
404            selectors: Vec::new(),
405            profiles: Vec::new(),
406            native_selectors: Vec::new(),
407        };
408    }
409    let (selectors, profiles, native_selectors): (Vec<SelectorCapability>, Vec<String>, Vec<&str>) =
410        match format {
411            DocumentFormat::Pdf => (
412                vec![
413                    caps("metadata", COMMON_METADATA),
414                    caps("text", COMMON_TEXT),
415                    caps("search-match", SEARCH),
416                ],
417                Vec::new(),
418                PDF_NATIVE.to_vec(),
419            ),
420            DocumentFormat::Docx => (
421                vec![
422                    caps("metadata", COMMON_METADATA),
423                    caps("text", COMMON_TEXT),
424                    caps("heading", COMMON_TEXT_META),
425                    caps("block", COMMON_TEXT_META),
426                    caps("table", COMMON_TEXT_META),
427                    caps("cell", COMMON_TEXT_META),
428                    caps("resource", COMMON_RESOURCE),
429                    caps("link", COMMON_METADATA),
430                    caps("search-match", SEARCH),
431                ],
432                docx_profiles(),
433                DOCX_NATIVE.to_vec(),
434            ),
435            DocumentFormat::Epub => (
436                vec![
437                    caps("metadata", COMMON_METADATA),
438                    caps("text", COMMON_TEXT),
439                    caps("heading", COMMON_TEXT_META),
440                    caps("block", COMMON_TEXT_META),
441                    caps("table", COMMON_TEXT_META),
442                    caps("cell", COMMON_TEXT_META),
443                    caps("resource", COMMON_RESOURCE_BYTES),
444                    caps("link", COMMON_METADATA),
445                    caps("search-match", SEARCH),
446                ],
447                epub_profiles(),
448                EPUB_NATIVE.to_vec(),
449            ),
450            DocumentFormat::Odt => (
451                vec![
452                    caps("metadata", COMMON_METADATA),
453                    caps("text", COMMON_TEXT),
454                    caps("heading", COMMON_TEXT_META),
455                    caps("block", COMMON_TEXT_META),
456                    caps("table", COMMON_TEXT_META),
457                    caps("cell", COMMON_TEXT_META),
458                    caps("resource", COMMON_RESOURCE),
459                    caps("link", COMMON_METADATA),
460                    caps("search-match", SEARCH),
461                ],
462                odt_profiles(),
463                ODT_NATIVE.to_vec(),
464            ),
465            DocumentFormat::Ods => (
466                vec![
467                    caps("metadata", COMMON_METADATA),
468                    caps("text", COMMON_TEXT),
469                    caps("table", COMMON_TEXT_META),
470                    caps("cell", COMMON_TEXT_META),
471                    caps("search-match", SEARCH),
472                ],
473                ods_profiles(),
474                ODS_NATIVE.to_vec(),
475            ),
476            DocumentFormat::Xlsx => (
477                vec![
478                    caps("metadata", COMMON_METADATA),
479                    caps("text", COMMON_TEXT),
480                    caps("table", COMMON_TEXT_META),
481                    caps("cell", COMMON_TEXT_META),
482                    caps("search-match", SEARCH),
483                ],
484                xlsx_profiles(),
485                XLSX_NATIVE.to_vec(),
486            ),
487            DocumentFormat::Pptx => (
488                vec![
489                    caps("metadata", COMMON_METADATA),
490                    caps("text", COMMON_TEXT),
491                    caps("table", COMMON_TEXT_META),
492                    caps("cell", COMMON_TEXT_META),
493                    caps("search-match", SEARCH),
494                ],
495                pptx_profiles(),
496                PPTX_NATIVE.to_vec(),
497            ),
498            DocumentFormat::Odp => (
499                vec![
500                    caps("metadata", COMMON_METADATA),
501                    caps("text", COMMON_TEXT),
502                    caps("table", COMMON_TEXT_META),
503                    caps("cell", COMMON_TEXT_META),
504                    caps("search-match", SEARCH),
505                ],
506                odp_profiles(),
507                ODP_NATIVE.to_vec(),
508            ),
509            DocumentFormat::Json => (
510                vec![
511                    caps("metadata", COMMON_METADATA),
512                    caps("text", COMMON_TEXT),
513                    caps("search-match", SEARCH),
514                ],
515                Vec::new(),
516                JSON_NATIVE.to_vec(),
517            ),
518            DocumentFormat::Json5 => (
519                vec![
520                    caps("metadata", COMMON_METADATA),
521                    caps("text", COMMON_TEXT),
522                    caps("search-match", SEARCH),
523                ],
524                Vec::new(),
525                JSON5_NATIVE.to_vec(),
526            ),
527            DocumentFormat::Yaml => (
528                vec![
529                    caps("metadata", COMMON_METADATA),
530                    caps("text", COMMON_TEXT),
531                    caps("search-match", SEARCH),
532                ],
533                Vec::new(),
534                YAML_NATIVE.to_vec(),
535            ),
536            DocumentFormat::Csv => (
537                vec![
538                    caps("metadata", COMMON_METADATA),
539                    caps("text", COMMON_TEXT),
540                    caps("table", COMMON_TEXT_META),
541                    caps("cell", COMMON_TEXT_META),
542                    caps("search-match", SEARCH),
543                ],
544                Vec::new(),
545                CSV_NATIVE.to_vec(),
546            ),
547            DocumentFormat::Markdown => (
548                vec![
549                    caps("metadata", COMMON_METADATA),
550                    caps("text", COMMON_TEXT),
551                    caps("heading", COMMON_TEXT_META),
552                    caps("block", COMMON_TEXT_META),
553                    caps("search-match", SEARCH),
554                ],
555                Vec::new(),
556                MARKDOWN_NATIVE.to_vec(),
557            ),
558            DocumentFormat::Xml => (
559                vec![
560                    caps("metadata", COMMON_METADATA),
561                    caps("text", COMMON_TEXT),
562                    caps("search-match", SEARCH),
563                ],
564                Vec::new(),
565                XML_NATIVE.to_vec(),
566            ),
567            DocumentFormat::Html => (
568                vec![
569                    caps("metadata", COMMON_METADATA),
570                    caps("text", COMMON_TEXT),
571                    caps("heading", COMMON_TEXT_META),
572                    caps("link", COMMON_METADATA),
573                    caps("search-match", SEARCH),
574                ],
575                Vec::new(),
576                HTML_NATIVE.to_vec(),
577            ),
578            DocumentFormat::Toml => (
579                vec![
580                    caps("metadata", COMMON_METADATA),
581                    caps("text", COMMON_TEXT),
582                    caps("search-match", SEARCH),
583                ],
584                Vec::new(),
585                TOML_NATIVE.to_vec(),
586            ),
587            DocumentFormat::Jsonl => (
588                vec![
589                    caps("metadata", COMMON_METADATA),
590                    caps("text", COMMON_TEXT),
591                    caps("search-match", SEARCH),
592                ],
593                Vec::new(),
594                JSONL_NATIVE.to_vec(),
595            ),
596            DocumentFormat::Eml => (
597                vec![
598                    caps("metadata", COMMON_METADATA),
599                    caps("text", COMMON_TEXT),
600                    caps("resource", COMMON_RESOURCE_BYTES),
601                    caps("search-match", SEARCH),
602                ],
603                Vec::new(),
604                EML_NATIVE.to_vec(),
605            ),
606            DocumentFormat::Parquet => (
607                vec![
608                    caps("metadata", COMMON_METADATA),
609                    caps("text", COMMON_TEXT),
610                    caps("table", COMMON_TEXT_META),
611                    caps("cell", COMMON_TEXT_META),
612                    caps("search-match", SEARCH),
613                ],
614                Vec::new(),
615                PARQUET_NATIVE.to_vec(),
616            ),
617            DocumentFormat::ArrowIpc => (
618                vec![
619                    caps("metadata", COMMON_METADATA),
620                    caps("text", COMMON_TEXT),
621                    caps("table", COMMON_TEXT_META),
622                    caps("cell", COMMON_TEXT_META),
623                    caps("search-match", SEARCH),
624                ],
625                Vec::new(),
626                ARROW_NATIVE.to_vec(),
627            ),
628            DocumentFormat::Cbor => (
629                vec![
630                    caps("metadata", COMMON_METADATA),
631                    caps("text", COMMON_TEXT),
632                    caps("search-match", SEARCH),
633                ],
634                Vec::new(),
635                CBOR_NATIVE.to_vec(),
636            ),
637            DocumentFormat::Msgpack => (
638                vec![
639                    caps("metadata", COMMON_METADATA),
640                    caps("text", COMMON_TEXT),
641                    caps("search-match", SEARCH),
642                ],
643                Vec::new(),
644                MSGPACK_NATIVE.to_vec(),
645            ),
646            DocumentFormat::Config => (
647                vec![
648                    caps("metadata", COMMON_METADATA),
649                    caps("text", COMMON_TEXT),
650                    caps("search-match", SEARCH),
651                ],
652                Vec::new(),
653                CONFIG_NATIVE.to_vec(),
654            ),
655            DocumentFormat::Feed => (
656                vec![
657                    caps("metadata", COMMON_METADATA),
658                    caps("text", COMMON_TEXT),
659                    caps("search-match", SEARCH),
660                ],
661                Vec::new(),
662                FEED_NATIVE.to_vec(),
663            ),
664            DocumentFormat::Geojson => (
665                vec![
666                    caps("metadata", COMMON_METADATA),
667                    caps("text", COMMON_TEXT),
668                    caps("search-match", SEARCH),
669                ],
670                Vec::new(),
671                GEOJSON_NATIVE.to_vec(),
672            ),
673            DocumentFormat::Gis => (
674                vec![
675                    caps("metadata", COMMON_METADATA),
676                    caps("text", COMMON_TEXT),
677                    caps("search-match", SEARCH),
678                ],
679                Vec::new(),
680                GIS_NATIVE.to_vec(),
681            ),
682            DocumentFormat::Notebook => (
683                vec![
684                    caps("metadata", COMMON_METADATA),
685                    caps("text", COMMON_TEXT),
686                    caps("search-match", SEARCH),
687                ],
688                Vec::new(),
689                NOTEBOOK_NATIVE.to_vec(),
690            ),
691            DocumentFormat::FixedWidth => (
692                vec![
693                    caps("metadata", COMMON_METADATA),
694                    caps("text", COMMON_TEXT),
695                    caps("table", COMMON_TEXT_META),
696                    caps("cell", COMMON_TEXT_META),
697                    caps("search-match", SEARCH),
698                ],
699                Vec::new(),
700                FIXEDWIDTH_NATIVE.to_vec(),
701            ),
702            DocumentFormat::Rst => (
703                vec![
704                    caps("metadata", COMMON_METADATA),
705                    caps("text", COMMON_TEXT),
706                    caps("heading", COMMON_TEXT_META),
707                    caps("block", COMMON_TEXT_META),
708                    caps("search-match", SEARCH),
709                ],
710                Vec::new(),
711                RST_NATIVE.to_vec(),
712            ),
713            DocumentFormat::Asciidoc => (
714                vec![
715                    caps("metadata", COMMON_METADATA),
716                    caps("text", COMMON_TEXT),
717                    caps("heading", COMMON_TEXT_META),
718                    caps("block", COMMON_TEXT_META),
719                    caps("search-match", SEARCH),
720                ],
721                Vec::new(),
722                ASCIIDOC_NATIVE.to_vec(),
723            ),
724            DocumentFormat::Mdx => (
725                vec![
726                    caps("metadata", COMMON_METADATA),
727                    caps("text", COMMON_TEXT),
728                    caps("heading", COMMON_TEXT_META),
729                    caps("block", COMMON_TEXT_META),
730                    caps("search-match", SEARCH),
731                ],
732                Vec::new(),
733                MDX_NATIVE.to_vec(),
734            ),
735            DocumentFormat::Mhtml => (
736                vec![
737                    caps("metadata", COMMON_METADATA),
738                    caps("text", COMMON_TEXT),
739                    caps("resource", COMMON_RESOURCE_BYTES),
740                    caps("search-match", SEARCH),
741                ],
742                Vec::new(),
743                MHTML_NATIVE.to_vec(),
744            ),
745            DocumentFormat::Logstream => (
746                vec![
747                    caps("metadata", COMMON_METADATA),
748                    caps("text", COMMON_TEXT),
749                    caps("search-match", SEARCH),
750                ],
751                Vec::new(),
752                LOGSTREAM_NATIVE.to_vec(),
753            ),
754            DocumentFormat::Pkgmeta => (
755                vec![
756                    caps("metadata", COMMON_METADATA),
757                    caps("text", COMMON_TEXT),
758                    caps("search-match", SEARCH),
759                ],
760                Vec::new(),
761                PKGMETA_NATIVE.to_vec(),
762            ),
763            DocumentFormat::Apispec => (
764                vec![
765                    caps("metadata", COMMON_METADATA),
766                    caps("text", COMMON_TEXT),
767                    caps("search-match", SEARCH),
768                ],
769                Vec::new(),
770                APISPEC_NATIVE.to_vec(),
771            ),
772            DocumentFormat::Bib => (
773                vec![
774                    caps("metadata", COMMON_METADATA),
775                    caps("text", COMMON_TEXT),
776                    caps("search-match", SEARCH),
777                ],
778                Vec::new(),
779                BIB_NATIVE.to_vec(),
780            ),
781            DocumentFormat::Rtf => (
782                vec![
783                    caps("metadata", COMMON_METADATA),
784                    caps("text", COMMON_TEXT),
785                    caps("search-match", SEARCH),
786                ],
787                Vec::new(),
788                RTF_NATIVE.to_vec(),
789            ),
790            DocumentFormat::Container => (
791                vec![
792                    caps("metadata", COMMON_METADATA),
793                    caps("text", COMMON_TEXT),
794                    caps("search-match", SEARCH),
795                ],
796                Vec::new(),
797                CONTAINER_NATIVE.to_vec(),
798            ),
799            DocumentFormat::Hdf => (
800                vec![
801                    caps("metadata", COMMON_METADATA),
802                    caps("text", COMMON_TEXT),
803                    caps("search-match", SEARCH),
804                ],
805                Vec::new(),
806                HDF_NATIVE.to_vec(),
807            ),
808            DocumentFormat::Cfbf => (
809                vec![
810                    caps("metadata", COMMON_METADATA),
811                    caps("text", COMMON_TEXT),
812                    caps("search-match", SEARCH),
813                ],
814                Vec::new(),
815                CFBF_NATIVE.to_vec(),
816            ),
817            DocumentFormat::Doc => (
818                vec![
819                    caps("metadata", COMMON_METADATA),
820                    caps("text", COMMON_TEXT),
821                    caps("search-match", SEARCH),
822                ],
823                Vec::new(),
824                DOC_NATIVE.to_vec(),
825            ),
826            DocumentFormat::Xls => (
827                vec![
828                    caps("metadata", COMMON_METADATA),
829                    caps("text", COMMON_TEXT),
830                    caps("search-match", SEARCH),
831                ],
832                Vec::new(),
833                XLS_NATIVE.to_vec(),
834            ),
835            DocumentFormat::Ppt => (
836                vec![
837                    caps("metadata", COMMON_METADATA),
838                    caps("text", COMMON_TEXT),
839                    caps("search-match", SEARCH),
840                ],
841                Vec::new(),
842                PPT_NATIVE.to_vec(),
843            ),
844            DocumentFormat::Msg => (
845                vec![
846                    caps("metadata", COMMON_METADATA),
847                    caps("text", COMMON_TEXT),
848                    caps("search-match", SEARCH),
849                ],
850                Vec::new(),
851                MSG_NATIVE.to_vec(),
852            ),
853            DocumentFormat::Opaque => (Vec::new(), Vec::new(), Vec::new()),
854        };
855    Capabilities {
856        format,
857        compiled,
858        selectors,
859        profiles,
860        native_selectors,
861    }
862}
863
864#[cfg(feature = "docx")]
865fn docx_profiles() -> Vec<String> {
866    vec![crate::adapter::docx::DocxExtractProfile::DEFAULT.fingerprint()]
867}
868#[cfg(not(feature = "docx"))]
869fn docx_profiles() -> Vec<String> {
870    Vec::new()
871}
872
873#[cfg(feature = "epub")]
874fn epub_profiles() -> Vec<String> {
875    vec![crate::adapter::epub::EpubExtractProfile::DEFAULT.fingerprint()]
876}
877#[cfg(not(feature = "epub"))]
878fn epub_profiles() -> Vec<String> {
879    Vec::new()
880}
881
882#[cfg(feature = "odt")]
883fn odt_profiles() -> Vec<String> {
884    vec![crate::adapter::odt::OdtExtractProfile::DEFAULT.fingerprint()]
885}
886#[cfg(not(feature = "odt"))]
887fn odt_profiles() -> Vec<String> {
888    Vec::new()
889}
890
891#[cfg(feature = "ods")]
892fn ods_profiles() -> Vec<String> {
893    vec![crate::adapter::ods::OdsExtractProfile::DEFAULT.fingerprint()]
894}
895#[cfg(not(feature = "ods"))]
896fn ods_profiles() -> Vec<String> {
897    Vec::new()
898}
899
900#[cfg(feature = "xlsx")]
901fn xlsx_profiles() -> Vec<String> {
902    vec![crate::adapter::xlsx::XlsxExtractProfile::DEFAULT.fingerprint()]
903}
904#[cfg(not(feature = "xlsx"))]
905fn xlsx_profiles() -> Vec<String> {
906    Vec::new()
907}
908
909#[cfg(feature = "pptx")]
910fn pptx_profiles() -> Vec<String> {
911    vec![crate::adapter::pptx::PptxExtractProfile::DEFAULT.fingerprint()]
912}
913#[cfg(not(feature = "pptx"))]
914fn pptx_profiles() -> Vec<String> {
915    Vec::new()
916}
917
918#[cfg(feature = "odp")]
919fn odp_profiles() -> Vec<String> {
920    vec![crate::adapter::odp::OdpExtractProfile::DEFAULT.fingerprint()]
921}
922#[cfg(not(feature = "odp"))]
923fn odp_profiles() -> Vec<String> {
924    Vec::new()
925}
926
927/// The canonical name of a common selector, or `None` for a native selector.
928pub fn common_selector_name(selector: &Selector) -> Option<&'static str> {
929    match selector {
930        Selector::Metadata => Some("metadata"),
931        Selector::Text => Some("text"),
932        Selector::Heading(_) => Some("heading"),
933        Selector::Block(_) => Some("block"),
934        Selector::Table(_) => Some("table"),
935        Selector::Cell { .. } => Some("cell"),
936        Selector::Resource(_) => Some("resource"),
937        Selector::Link(_) => Some("link"),
938        Selector::SearchMatch(_) => Some("search-match"),
939        _ => None,
940    }
941}
942
943/// The representations admissible for `selector` under `format`, if the pair is
944/// structurally applicable.
945pub fn common_representations(
946    format: DocumentFormat,
947    selector: &Selector,
948) -> Option<&'static [&'static str]> {
949    if !format.compiled() {
950        return None;
951    }
952    let name = common_selector_name(selector)?;
953    let caps = capabilities_for_format(format);
954    caps.selectors
955        .iter()
956        .find(|c| c.selector == name)
957        .map(|c| c.representations)
958}
959
960/// Whether `format` can serve `selector` with `representation`.
961pub fn common_supported(
962    format: DocumentFormat,
963    selector: &Selector,
964    representation: Representation,
965) -> bool {
966    common_representations(format, selector)
967        .is_some_and(|reps| reps.contains(&representation.name()))
968}
969
970#[cfg(test)]
971mod tests {
972    use super::*;
973
974    #[test]
975    fn pdf_supports_text_and_search_but_not_tables() {
976        assert!(common_supported(
977            DocumentFormat::Pdf,
978            &Selector::Text,
979            Representation::Text
980        ));
981        assert!(common_supported(
982            DocumentFormat::Pdf,
983            &Selector::SearchMatch("x".into()),
984            Representation::Text
985        ));
986        assert!(!common_supported(
987            DocumentFormat::Pdf,
988            &Selector::Table(0),
989            Representation::Text
990        ));
991        assert!(!common_supported(
992            DocumentFormat::Pdf,
993            &Selector::Text,
994            Representation::Metadata
995        ));
996    }
997
998    #[test]
999    fn opaque_and_unknown_selectors_are_unsupported() {
1000        assert!(!common_supported(
1001            DocumentFormat::Opaque,
1002            &Selector::Text,
1003            Representation::Text
1004        ));
1005        assert!(!common_supported(
1006            DocumentFormat::Pdf,
1007            &Selector::Document,
1008            Representation::Metadata
1009        ));
1010    }
1011}