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] = &[
358    "doc-fib",
359    "doc-piece",
360    "doc-section",
361    "doc-text",
362    "doc-find",
363];
364
365const XLS_NATIVE: &[&str] = &[
366    "xls-biff",
367    "xls-sheet",
368    "xls-cell",
369    "xls-record",
370    "xls-find",
371];
372
373const PPT_NATIVE: &[&str] = &[
374    "ppt-document",
375    "ppt-slide",
376    "ppt-run",
377    "ppt-record",
378    "ppt-list",
379    "ppt-find",
380];
381
382const MSG_NATIVE: &[&str] = &[
383    "msg-summary",
384    "msg-property",
385    "msg-recipient",
386    "msg-attachment",
387    "msg-find",
388];
389
390const COMMON_METADATA: &[&str] = &["metadata"];
391const COMMON_TEXT: &[&str] = &["text"];
392const COMMON_TEXT_META: &[&str] = &["text", "metadata"];
393const COMMON_RESOURCE: &[&str] = &["metadata"];
394const COMMON_RESOURCE_BYTES: &[&str] = &["metadata", "exact", "decoded"];
395const SEARCH: &[&str] = &["text"];
396
397fn caps(selector: &'static str, representations: &'static [&'static str]) -> SelectorCapability {
398    SelectorCapability {
399        selector,
400        representations,
401    }
402}
403
404/// The capability set for a detected format, filtered by what this build compiles.
405pub fn capabilities_for_format(format: DocumentFormat) -> Capabilities {
406    let compiled = format.compiled();
407    if !compiled {
408        return Capabilities {
409            format,
410            compiled,
411            selectors: Vec::new(),
412            profiles: Vec::new(),
413            native_selectors: Vec::new(),
414        };
415    }
416    let (selectors, profiles, native_selectors): (Vec<SelectorCapability>, Vec<String>, Vec<&str>) =
417        match format {
418            DocumentFormat::Pdf => (
419                vec![
420                    caps("metadata", COMMON_METADATA),
421                    caps("text", COMMON_TEXT),
422                    caps("search-match", SEARCH),
423                ],
424                Vec::new(),
425                PDF_NATIVE.to_vec(),
426            ),
427            DocumentFormat::Docx => (
428                vec![
429                    caps("metadata", COMMON_METADATA),
430                    caps("text", COMMON_TEXT),
431                    caps("heading", COMMON_TEXT_META),
432                    caps("block", COMMON_TEXT_META),
433                    caps("table", COMMON_TEXT_META),
434                    caps("cell", COMMON_TEXT_META),
435                    caps("resource", COMMON_RESOURCE),
436                    caps("link", COMMON_METADATA),
437                    caps("search-match", SEARCH),
438                ],
439                docx_profiles(),
440                DOCX_NATIVE.to_vec(),
441            ),
442            DocumentFormat::Epub => (
443                vec![
444                    caps("metadata", COMMON_METADATA),
445                    caps("text", COMMON_TEXT),
446                    caps("heading", COMMON_TEXT_META),
447                    caps("block", COMMON_TEXT_META),
448                    caps("table", COMMON_TEXT_META),
449                    caps("cell", COMMON_TEXT_META),
450                    caps("resource", COMMON_RESOURCE_BYTES),
451                    caps("link", COMMON_METADATA),
452                    caps("search-match", SEARCH),
453                ],
454                epub_profiles(),
455                EPUB_NATIVE.to_vec(),
456            ),
457            DocumentFormat::Odt => (
458                vec![
459                    caps("metadata", COMMON_METADATA),
460                    caps("text", COMMON_TEXT),
461                    caps("heading", COMMON_TEXT_META),
462                    caps("block", COMMON_TEXT_META),
463                    caps("table", COMMON_TEXT_META),
464                    caps("cell", COMMON_TEXT_META),
465                    caps("resource", COMMON_RESOURCE),
466                    caps("link", COMMON_METADATA),
467                    caps("search-match", SEARCH),
468                ],
469                odt_profiles(),
470                ODT_NATIVE.to_vec(),
471            ),
472            DocumentFormat::Ods => (
473                vec![
474                    caps("metadata", COMMON_METADATA),
475                    caps("text", COMMON_TEXT),
476                    caps("table", COMMON_TEXT_META),
477                    caps("cell", COMMON_TEXT_META),
478                    caps("search-match", SEARCH),
479                ],
480                ods_profiles(),
481                ODS_NATIVE.to_vec(),
482            ),
483            DocumentFormat::Xlsx => (
484                vec![
485                    caps("metadata", COMMON_METADATA),
486                    caps("text", COMMON_TEXT),
487                    caps("table", COMMON_TEXT_META),
488                    caps("cell", COMMON_TEXT_META),
489                    caps("search-match", SEARCH),
490                ],
491                xlsx_profiles(),
492                XLSX_NATIVE.to_vec(),
493            ),
494            DocumentFormat::Pptx => (
495                vec![
496                    caps("metadata", COMMON_METADATA),
497                    caps("text", COMMON_TEXT),
498                    caps("table", COMMON_TEXT_META),
499                    caps("cell", COMMON_TEXT_META),
500                    caps("search-match", SEARCH),
501                ],
502                pptx_profiles(),
503                PPTX_NATIVE.to_vec(),
504            ),
505            DocumentFormat::Odp => (
506                vec![
507                    caps("metadata", COMMON_METADATA),
508                    caps("text", COMMON_TEXT),
509                    caps("table", COMMON_TEXT_META),
510                    caps("cell", COMMON_TEXT_META),
511                    caps("search-match", SEARCH),
512                ],
513                odp_profiles(),
514                ODP_NATIVE.to_vec(),
515            ),
516            DocumentFormat::Json => (
517                vec![
518                    caps("metadata", COMMON_METADATA),
519                    caps("text", COMMON_TEXT),
520                    caps("search-match", SEARCH),
521                ],
522                Vec::new(),
523                JSON_NATIVE.to_vec(),
524            ),
525            DocumentFormat::Json5 => (
526                vec![
527                    caps("metadata", COMMON_METADATA),
528                    caps("text", COMMON_TEXT),
529                    caps("search-match", SEARCH),
530                ],
531                Vec::new(),
532                JSON5_NATIVE.to_vec(),
533            ),
534            DocumentFormat::Yaml => (
535                vec![
536                    caps("metadata", COMMON_METADATA),
537                    caps("text", COMMON_TEXT),
538                    caps("search-match", SEARCH),
539                ],
540                Vec::new(),
541                YAML_NATIVE.to_vec(),
542            ),
543            DocumentFormat::Csv => (
544                vec![
545                    caps("metadata", COMMON_METADATA),
546                    caps("text", COMMON_TEXT),
547                    caps("table", COMMON_TEXT_META),
548                    caps("cell", COMMON_TEXT_META),
549                    caps("search-match", SEARCH),
550                ],
551                Vec::new(),
552                CSV_NATIVE.to_vec(),
553            ),
554            DocumentFormat::Markdown => (
555                vec![
556                    caps("metadata", COMMON_METADATA),
557                    caps("text", COMMON_TEXT),
558                    caps("heading", COMMON_TEXT_META),
559                    caps("block", COMMON_TEXT_META),
560                    caps("search-match", SEARCH),
561                ],
562                Vec::new(),
563                MARKDOWN_NATIVE.to_vec(),
564            ),
565            DocumentFormat::Xml => (
566                vec![
567                    caps("metadata", COMMON_METADATA),
568                    caps("text", COMMON_TEXT),
569                    caps("search-match", SEARCH),
570                ],
571                Vec::new(),
572                XML_NATIVE.to_vec(),
573            ),
574            DocumentFormat::Html => (
575                vec![
576                    caps("metadata", COMMON_METADATA),
577                    caps("text", COMMON_TEXT),
578                    caps("heading", COMMON_TEXT_META),
579                    caps("link", COMMON_METADATA),
580                    caps("search-match", SEARCH),
581                ],
582                Vec::new(),
583                HTML_NATIVE.to_vec(),
584            ),
585            DocumentFormat::Toml => (
586                vec![
587                    caps("metadata", COMMON_METADATA),
588                    caps("text", COMMON_TEXT),
589                    caps("search-match", SEARCH),
590                ],
591                Vec::new(),
592                TOML_NATIVE.to_vec(),
593            ),
594            DocumentFormat::Jsonl => (
595                vec![
596                    caps("metadata", COMMON_METADATA),
597                    caps("text", COMMON_TEXT),
598                    caps("search-match", SEARCH),
599                ],
600                Vec::new(),
601                JSONL_NATIVE.to_vec(),
602            ),
603            DocumentFormat::Eml => (
604                vec![
605                    caps("metadata", COMMON_METADATA),
606                    caps("text", COMMON_TEXT),
607                    caps("resource", COMMON_RESOURCE_BYTES),
608                    caps("search-match", SEARCH),
609                ],
610                Vec::new(),
611                EML_NATIVE.to_vec(),
612            ),
613            DocumentFormat::Parquet => (
614                vec![
615                    caps("metadata", COMMON_METADATA),
616                    caps("text", COMMON_TEXT),
617                    caps("table", COMMON_TEXT_META),
618                    caps("cell", COMMON_TEXT_META),
619                    caps("search-match", SEARCH),
620                ],
621                Vec::new(),
622                PARQUET_NATIVE.to_vec(),
623            ),
624            DocumentFormat::ArrowIpc => (
625                vec![
626                    caps("metadata", COMMON_METADATA),
627                    caps("text", COMMON_TEXT),
628                    caps("table", COMMON_TEXT_META),
629                    caps("cell", COMMON_TEXT_META),
630                    caps("search-match", SEARCH),
631                ],
632                Vec::new(),
633                ARROW_NATIVE.to_vec(),
634            ),
635            DocumentFormat::Cbor => (
636                vec![
637                    caps("metadata", COMMON_METADATA),
638                    caps("text", COMMON_TEXT),
639                    caps("search-match", SEARCH),
640                ],
641                Vec::new(),
642                CBOR_NATIVE.to_vec(),
643            ),
644            DocumentFormat::Msgpack => (
645                vec![
646                    caps("metadata", COMMON_METADATA),
647                    caps("text", COMMON_TEXT),
648                    caps("search-match", SEARCH),
649                ],
650                Vec::new(),
651                MSGPACK_NATIVE.to_vec(),
652            ),
653            DocumentFormat::Config => (
654                vec![
655                    caps("metadata", COMMON_METADATA),
656                    caps("text", COMMON_TEXT),
657                    caps("search-match", SEARCH),
658                ],
659                Vec::new(),
660                CONFIG_NATIVE.to_vec(),
661            ),
662            DocumentFormat::Feed => (
663                vec![
664                    caps("metadata", COMMON_METADATA),
665                    caps("text", COMMON_TEXT),
666                    caps("search-match", SEARCH),
667                ],
668                Vec::new(),
669                FEED_NATIVE.to_vec(),
670            ),
671            DocumentFormat::Geojson => (
672                vec![
673                    caps("metadata", COMMON_METADATA),
674                    caps("text", COMMON_TEXT),
675                    caps("search-match", SEARCH),
676                ],
677                Vec::new(),
678                GEOJSON_NATIVE.to_vec(),
679            ),
680            DocumentFormat::Gis => (
681                vec![
682                    caps("metadata", COMMON_METADATA),
683                    caps("text", COMMON_TEXT),
684                    caps("search-match", SEARCH),
685                ],
686                Vec::new(),
687                GIS_NATIVE.to_vec(),
688            ),
689            DocumentFormat::Notebook => (
690                vec![
691                    caps("metadata", COMMON_METADATA),
692                    caps("text", COMMON_TEXT),
693                    caps("search-match", SEARCH),
694                ],
695                Vec::new(),
696                NOTEBOOK_NATIVE.to_vec(),
697            ),
698            DocumentFormat::FixedWidth => (
699                vec![
700                    caps("metadata", COMMON_METADATA),
701                    caps("text", COMMON_TEXT),
702                    caps("table", COMMON_TEXT_META),
703                    caps("cell", COMMON_TEXT_META),
704                    caps("search-match", SEARCH),
705                ],
706                Vec::new(),
707                FIXEDWIDTH_NATIVE.to_vec(),
708            ),
709            DocumentFormat::Rst => (
710                vec![
711                    caps("metadata", COMMON_METADATA),
712                    caps("text", COMMON_TEXT),
713                    caps("heading", COMMON_TEXT_META),
714                    caps("block", COMMON_TEXT_META),
715                    caps("search-match", SEARCH),
716                ],
717                Vec::new(),
718                RST_NATIVE.to_vec(),
719            ),
720            DocumentFormat::Asciidoc => (
721                vec![
722                    caps("metadata", COMMON_METADATA),
723                    caps("text", COMMON_TEXT),
724                    caps("heading", COMMON_TEXT_META),
725                    caps("block", COMMON_TEXT_META),
726                    caps("search-match", SEARCH),
727                ],
728                Vec::new(),
729                ASCIIDOC_NATIVE.to_vec(),
730            ),
731            DocumentFormat::Mdx => (
732                vec![
733                    caps("metadata", COMMON_METADATA),
734                    caps("text", COMMON_TEXT),
735                    caps("heading", COMMON_TEXT_META),
736                    caps("block", COMMON_TEXT_META),
737                    caps("search-match", SEARCH),
738                ],
739                Vec::new(),
740                MDX_NATIVE.to_vec(),
741            ),
742            DocumentFormat::Mhtml => (
743                vec![
744                    caps("metadata", COMMON_METADATA),
745                    caps("text", COMMON_TEXT),
746                    caps("resource", COMMON_RESOURCE_BYTES),
747                    caps("search-match", SEARCH),
748                ],
749                Vec::new(),
750                MHTML_NATIVE.to_vec(),
751            ),
752            DocumentFormat::Logstream => (
753                vec![
754                    caps("metadata", COMMON_METADATA),
755                    caps("text", COMMON_TEXT),
756                    caps("search-match", SEARCH),
757                ],
758                Vec::new(),
759                LOGSTREAM_NATIVE.to_vec(),
760            ),
761            DocumentFormat::Pkgmeta => (
762                vec![
763                    caps("metadata", COMMON_METADATA),
764                    caps("text", COMMON_TEXT),
765                    caps("search-match", SEARCH),
766                ],
767                Vec::new(),
768                PKGMETA_NATIVE.to_vec(),
769            ),
770            DocumentFormat::Apispec => (
771                vec![
772                    caps("metadata", COMMON_METADATA),
773                    caps("text", COMMON_TEXT),
774                    caps("search-match", SEARCH),
775                ],
776                Vec::new(),
777                APISPEC_NATIVE.to_vec(),
778            ),
779            DocumentFormat::Bib => (
780                vec![
781                    caps("metadata", COMMON_METADATA),
782                    caps("text", COMMON_TEXT),
783                    caps("search-match", SEARCH),
784                ],
785                Vec::new(),
786                BIB_NATIVE.to_vec(),
787            ),
788            DocumentFormat::Rtf => (
789                vec![
790                    caps("metadata", COMMON_METADATA),
791                    caps("text", COMMON_TEXT),
792                    caps("search-match", SEARCH),
793                ],
794                Vec::new(),
795                RTF_NATIVE.to_vec(),
796            ),
797            DocumentFormat::Container => (
798                vec![
799                    caps("metadata", COMMON_METADATA),
800                    caps("text", COMMON_TEXT),
801                    caps("search-match", SEARCH),
802                ],
803                Vec::new(),
804                CONTAINER_NATIVE.to_vec(),
805            ),
806            DocumentFormat::Hdf => (
807                vec![
808                    caps("metadata", COMMON_METADATA),
809                    caps("text", COMMON_TEXT),
810                    caps("search-match", SEARCH),
811                ],
812                Vec::new(),
813                HDF_NATIVE.to_vec(),
814            ),
815            DocumentFormat::Cfbf => (
816                vec![
817                    caps("metadata", COMMON_METADATA),
818                    caps("text", COMMON_TEXT),
819                    caps("search-match", SEARCH),
820                ],
821                Vec::new(),
822                CFBF_NATIVE.to_vec(),
823            ),
824            DocumentFormat::Doc => (
825                vec![
826                    caps("metadata", COMMON_METADATA),
827                    caps("text", COMMON_TEXT),
828                    caps("search-match", SEARCH),
829                ],
830                Vec::new(),
831                DOC_NATIVE.to_vec(),
832            ),
833            DocumentFormat::Xls => (
834                vec![
835                    caps("metadata", COMMON_METADATA),
836                    caps("text", COMMON_TEXT),
837                    caps("search-match", SEARCH),
838                ],
839                Vec::new(),
840                XLS_NATIVE.to_vec(),
841            ),
842            DocumentFormat::Ppt => (
843                vec![
844                    caps("metadata", COMMON_METADATA),
845                    caps("text", COMMON_TEXT),
846                    caps("search-match", SEARCH),
847                ],
848                Vec::new(),
849                PPT_NATIVE.to_vec(),
850            ),
851            DocumentFormat::Msg => (
852                vec![
853                    caps("metadata", COMMON_METADATA),
854                    caps("text", COMMON_TEXT),
855                    caps("search-match", SEARCH),
856                ],
857                Vec::new(),
858                MSG_NATIVE.to_vec(),
859            ),
860            DocumentFormat::Opaque => (Vec::new(), Vec::new(), Vec::new()),
861        };
862    Capabilities {
863        format,
864        compiled,
865        selectors,
866        profiles,
867        native_selectors,
868    }
869}
870
871#[cfg(feature = "docx")]
872fn docx_profiles() -> Vec<String> {
873    vec![crate::adapter::docx::DocxExtractProfile::DEFAULT.fingerprint()]
874}
875#[cfg(not(feature = "docx"))]
876fn docx_profiles() -> Vec<String> {
877    Vec::new()
878}
879
880#[cfg(feature = "epub")]
881fn epub_profiles() -> Vec<String> {
882    vec![crate::adapter::epub::EpubExtractProfile::DEFAULT.fingerprint()]
883}
884#[cfg(not(feature = "epub"))]
885fn epub_profiles() -> Vec<String> {
886    Vec::new()
887}
888
889#[cfg(feature = "odt")]
890fn odt_profiles() -> Vec<String> {
891    vec![crate::adapter::odt::OdtExtractProfile::DEFAULT.fingerprint()]
892}
893#[cfg(not(feature = "odt"))]
894fn odt_profiles() -> Vec<String> {
895    Vec::new()
896}
897
898#[cfg(feature = "ods")]
899fn ods_profiles() -> Vec<String> {
900    vec![crate::adapter::ods::OdsExtractProfile::DEFAULT.fingerprint()]
901}
902#[cfg(not(feature = "ods"))]
903fn ods_profiles() -> Vec<String> {
904    Vec::new()
905}
906
907#[cfg(feature = "xlsx")]
908fn xlsx_profiles() -> Vec<String> {
909    vec![crate::adapter::xlsx::XlsxExtractProfile::DEFAULT.fingerprint()]
910}
911#[cfg(not(feature = "xlsx"))]
912fn xlsx_profiles() -> Vec<String> {
913    Vec::new()
914}
915
916#[cfg(feature = "pptx")]
917fn pptx_profiles() -> Vec<String> {
918    vec![crate::adapter::pptx::PptxExtractProfile::DEFAULT.fingerprint()]
919}
920#[cfg(not(feature = "pptx"))]
921fn pptx_profiles() -> Vec<String> {
922    Vec::new()
923}
924
925#[cfg(feature = "odp")]
926fn odp_profiles() -> Vec<String> {
927    vec![crate::adapter::odp::OdpExtractProfile::DEFAULT.fingerprint()]
928}
929#[cfg(not(feature = "odp"))]
930fn odp_profiles() -> Vec<String> {
931    Vec::new()
932}
933
934/// The canonical name of a common selector, or `None` for a native selector.
935pub fn common_selector_name(selector: &Selector) -> Option<&'static str> {
936    match selector {
937        Selector::Metadata => Some("metadata"),
938        Selector::Text => Some("text"),
939        Selector::Heading(_) => Some("heading"),
940        Selector::Block(_) => Some("block"),
941        Selector::Table(_) => Some("table"),
942        Selector::Cell { .. } => Some("cell"),
943        Selector::Resource(_) => Some("resource"),
944        Selector::Link(_) => Some("link"),
945        Selector::SearchMatch(_) => Some("search-match"),
946        _ => None,
947    }
948}
949
950/// The representations admissible for `selector` under `format`, if the pair is
951/// structurally applicable.
952pub fn common_representations(
953    format: DocumentFormat,
954    selector: &Selector,
955) -> Option<&'static [&'static str]> {
956    if !format.compiled() {
957        return None;
958    }
959    let name = common_selector_name(selector)?;
960    let caps = capabilities_for_format(format);
961    caps.selectors
962        .iter()
963        .find(|c| c.selector == name)
964        .map(|c| c.representations)
965}
966
967/// Whether `format` can serve `selector` with `representation`.
968pub fn common_supported(
969    format: DocumentFormat,
970    selector: &Selector,
971    representation: Representation,
972) -> bool {
973    common_representations(format, selector)
974        .is_some_and(|reps| reps.contains(&representation.name()))
975}
976
977#[cfg(test)]
978mod tests {
979    use super::*;
980
981    #[test]
982    fn pdf_supports_text_and_search_but_not_tables() {
983        assert!(common_supported(
984            DocumentFormat::Pdf,
985            &Selector::Text,
986            Representation::Text
987        ));
988        assert!(common_supported(
989            DocumentFormat::Pdf,
990            &Selector::SearchMatch("x".into()),
991            Representation::Text
992        ));
993        assert!(!common_supported(
994            DocumentFormat::Pdf,
995            &Selector::Table(0),
996            Representation::Text
997        ));
998        assert!(!common_supported(
999            DocumentFormat::Pdf,
1000            &Selector::Text,
1001            Representation::Metadata
1002        ));
1003    }
1004
1005    #[test]
1006    fn opaque_and_unknown_selectors_are_unsupported() {
1007        assert!(!common_supported(
1008            DocumentFormat::Opaque,
1009            &Selector::Text,
1010            Representation::Text
1011        ));
1012        assert!(!common_supported(
1013            DocumentFormat::Pdf,
1014            &Selector::Document,
1015            Representation::Metadata
1016        ));
1017    }
1018}