Skip to main content

sphinx_ultra/env/
objects.rs

1//! The domains' `get_objects()` (`domains/__init__.py:285`): every object a
2//! domain exposes to the object inventory (`objects.inv`) and the search
3//! index (`searchindex.js`), as the `(name, dispname, type, docname, anchor,
4//! priority)` rows Sphinx yields — plus the two per-type lookups both
5//! consumers share, the search priority (`ObjType.attrs['searchprio']`) and
6//! the localized type name (`Domain.get_type_name`).
7//!
8//! One producer for both artifacts is the point: the inventory and the
9//! search index can only ever disagree about an object if Sphinx's do.
10
11use crate::env::numbers::clean_astext;
12use crate::env::BuildEnvironment;
13use crate::inventory::InvObject;
14
15/// `ObjType(..., searchprio=...)` for the std domain's types
16/// (`domains/std/__init__.py:725-734`). `ObjType` defaults `searchprio` to
17/// 1 (`domains/__init__.py:53`), so every type the table does not mark
18/// unsearchable — `confval`, `envvar`, `cmdoption`, and whatever
19/// `add_object_type` registers — is 1.
20pub fn std_search_priority(objtype: &str) -> i32 {
21    match objtype {
22        "term" | "token" | "label" | "doc" => -1,
23        _ => 1,
24    }
25}
26
27/// `Domain.get_type_name(otype)` (`domains/__init__.py:318-322`), as
28/// `IndexBuilder.get_objects` calls it: the std domain overrides it to the
29/// bare `ObjType.lname` (`domains/std/__init__.py:1359`); every other domain
30/// prefixes its label, so a Python class is a `Python class`. A type the
31/// domain has no `ObjType` for is named by its type string, which is what
32/// `get_objects` falls back to.
33pub fn object_type_name(domain: &str, objtype: &str) -> String {
34    match domain {
35        "std" => match objtype {
36            "cmdoption" => "program option",
37            "confval" => "configuration value",
38            "envvar" => "environment variable",
39            "term" => "glossary term",
40            "token" => "grammar token",
41            "label" => "reference label",
42            "doc" => "document",
43            other => other,
44        }
45        .to_string(),
46        "py" => {
47            let lname = match objtype {
48                "function" | "data" | "class" | "exception" | "method" | "attribute"
49                | "property" | "module" => objtype,
50                "classmethod" => "class method",
51                "staticmethod" => "static method",
52                "type" => "type alias",
53                _ => return objtype.to_string(),
54            };
55            format!("Python {lname}")
56        }
57        _ => objtype.to_string(),
58    }
59}
60
61/// Both domains this build populates, in `env.domains.sorted()` order, each
62/// sorted the way `sorted(domain.get_objects())` sorts its tuples.
63pub fn domain_objects(env: &BuildEnvironment) -> Vec<(&'static str, Vec<InvObject>)> {
64    vec![
65        ("py", py_domain_objects(env)),
66        ("std", std_domain_objects(env)),
67    ]
68}
69
70/// `StandardDomain.get_objects()` (`domains/std/__init__.py:1283-1308`).
71pub fn std_domain_objects(env: &BuildEnvironment) -> Vec<InvObject> {
72    let mut objects = Vec::new();
73
74    // "handle the special 'doc' reference here"
75    for docname in env.all_docs.keys() {
76        objects.push(InvObject {
77            name: docname.clone(),
78            objtype: "doc".to_string(),
79            priority: -1,
80            docname: docname.clone(),
81            anchor: String::new(),
82            dispname: env
83                .titles
84                .get(docname)
85                .map(clean_astext)
86                .unwrap_or_default(),
87        });
88    }
89
90    for ((program, option), (docname, anchor)) in &env.std.progoptions {
91        let fullname = match program {
92            Some(program) => format!("{program}.{option}"),
93            None => option.clone(),
94        };
95        objects.push(InvObject {
96            name: fullname.clone(),
97            objtype: "cmdoption".to_string(),
98            priority: 1,
99            docname: docname.clone(),
100            anchor: anchor.clone(),
101            dispname: fullname,
102        });
103    }
104
105    for ((objtype, name), (docname, anchor)) in &env.std.objects {
106        objects.push(InvObject {
107            name: name.clone(),
108            objtype: objtype.clone(),
109            priority: std_search_priority(objtype),
110            docname: docname.clone(),
111            anchor: anchor.clone(),
112            dispname: name.clone(),
113        });
114    }
115
116    for (name, (docname, labelid, sectionname)) in &env.std.labels {
117        objects.push(InvObject {
118            name: name.clone(),
119            objtype: "label".to_string(),
120            priority: -1,
121            docname: docname.clone(),
122            anchor: labelid.clone(),
123            dispname: sectionname.clone(),
124        });
125    }
126
127    // "add anonymous-only labels as well"
128    for (name, (docname, labelid)) in &env.std.anonlabels {
129        if !env.std.labels.contains_key(name) {
130            objects.push(InvObject {
131                name: name.clone(),
132                objtype: "label".to_string(),
133                priority: -1,
134                docname: docname.clone(),
135                anchor: labelid.clone(),
136                dispname: name.clone(),
137            });
138        }
139    }
140
141    sort_like_sphinx(&mut objects);
142    objects
143}
144
145/// `PythonDomain.get_objects()` (`domains/python/__init__.py:1056-1065`):
146/// modules come from the module registry at priority 0 and are skipped in
147/// the object registry; aliased objects are not full-text searchable.
148pub fn py_domain_objects(env: &BuildEnvironment) -> Vec<InvObject> {
149    let mut objects = Vec::new();
150    for (name, module) in &env.py.modules {
151        objects.push(InvObject {
152            name: name.clone(),
153            objtype: "module".to_string(),
154            priority: 0,
155            docname: module.docname.clone(),
156            anchor: module.node_id.clone(),
157            dispname: name.clone(),
158        });
159    }
160    for (name, object) in &env.py.objects {
161        if object.objtype == "module" {
162            continue;
163        }
164        objects.push(InvObject {
165            name: name.clone(),
166            objtype: object.objtype.clone(),
167            priority: if object.aliased { -1 } else { 1 },
168            docname: object.docname.clone(),
169            anchor: object.node_id.clone(),
170            dispname: name.clone(),
171        });
172    }
173    sort_like_sphinx(&mut objects);
174    objects
175}
176
177/// `sorted(domain.get_objects())`: tuple order over
178/// `(name, dispname, type, docname, anchor, priority)`.
179fn sort_like_sphinx(objects: &mut [InvObject]) {
180    objects.sort_by(|a, b| {
181        a.name
182            .cmp(&b.name)
183            .then_with(|| a.dispname.cmp(&b.dispname))
184            .then_with(|| a.objtype.cmp(&b.objtype))
185            .then_with(|| a.docname.cmp(&b.docname))
186            .then_with(|| a.anchor.cmp(&b.anchor))
187            .then_with(|| a.priority.cmp(&b.priority))
188    });
189}
190
191#[cfg(test)]
192mod tests {
193    use super::*;
194
195    #[test]
196    fn std_types_the_table_does_not_name_are_searchable() {
197        assert_eq!(std_search_priority("term"), -1);
198        assert_eq!(std_search_priority("doc"), -1);
199        assert_eq!(std_search_priority("confval"), 1);
200        assert_eq!(std_search_priority("envvar"), 1);
201        assert_eq!(std_search_priority("cmdoption"), 1);
202        assert_eq!(std_search_priority("custom-type"), 1);
203    }
204
205    #[test]
206    fn type_names_follow_get_type_name() {
207        assert_eq!(object_type_name("std", "envvar"), "environment variable");
208        assert_eq!(object_type_name("std", "custom"), "custom");
209        assert_eq!(object_type_name("py", "class"), "Python class");
210        assert_eq!(object_type_name("py", "classmethod"), "Python class method");
211        assert_eq!(object_type_name("py", "type"), "Python type alias");
212        assert_eq!(object_type_name("py", "unknown"), "unknown");
213    }
214
215    #[test]
216    fn modules_are_listed_once_from_the_module_registry() {
217        use crate::env::py_domain::{PyModuleEntry, PyObjectEntry};
218
219        let mut env = BuildEnvironment::default();
220        env.py.note_module(
221            "pkg",
222            PyModuleEntry {
223                docname: "api".to_string(),
224                node_id: "module-pkg".to_string(),
225                synopsis: String::new(),
226                platform: String::new(),
227                deprecated: false,
228            },
229        );
230        env.py.note_object(
231            "pkg",
232            PyObjectEntry {
233                docname: "api".to_string(),
234                node_id: "module-pkg".to_string(),
235                objtype: "module".to_string(),
236                aliased: false,
237            },
238        );
239        env.py.note_object(
240            "pkg.run",
241            PyObjectEntry {
242                docname: "api".to_string(),
243                node_id: "pkg.run".to_string(),
244                objtype: "function".to_string(),
245                aliased: false,
246            },
247        );
248        let rows: Vec<(String, String, i32)> = py_domain_objects(&env)
249            .into_iter()
250            .map(|o| (o.name, o.objtype, o.priority))
251            .collect();
252        assert_eq!(
253            rows,
254            vec![
255                ("pkg".to_string(), "module".to_string(), 0),
256                ("pkg.run".to_string(), "function".to_string(), 1),
257            ]
258        );
259    }
260}