sphinx_ultra/env/
dependencies.rs1use std::collections::BTreeSet;
37use std::path::{Path, PathBuf};
38
39use crate::doctree::{kinds, AttrValue, Doctree, Node};
40use crate::utils::relfn2path;
41
42use super::BuildEnvironment;
43
44pub fn process_doc(
55 env: &mut BuildEnvironment,
56 docname: &str,
57 doctree: &Doctree,
58 srcdir: &Path,
59 parse_dependencies: &[String],
60) {
61 let mut paths = BTreeSet::new();
62 collect(&doctree.root, docname, srcdir, &mut paths);
63 for dependency in parse_dependencies {
64 paths.insert(srcdir.join(dependency));
67 }
68 if paths.is_empty() {
69 env.dependencies.remove(docname);
70 } else {
71 env.dependencies.insert(docname.to_string(), paths);
72 }
73}
74
75pub fn note(uri: &str, docname: &str, srcdir: &Path) -> Option<PathBuf> {
82 if uri.starts_with("data:") || uri.contains("://") || uri.contains('*') || uri.is_empty() {
83 return None;
84 }
85 Some(relfn2path(uri, docname, srcdir))
86}
87
88fn collect(node: &Node, docname: &str, srcdir: &Path, out: &mut BTreeSet<PathBuf>) {
91 if node.kind == kinds::IMAGE {
92 if let Some(AttrValue::Str(uri)) = node.get("uri") {
93 if let Some(path) = note(uri, docname, srcdir) {
94 out.insert(path);
95 }
96 }
97 }
98 for child in &node.children {
99 collect(child, docname, srcdir, out);
100 }
101}
102
103#[cfg(test)]
104mod tests {
105 use super::*;
106 use crate::rst;
107
108 fn deps(docname: &str, source: &str) -> Vec<PathBuf> {
109 let doctree = rst::parse_rst(source, &rst::ParseOptions::default());
110 let mut env = BuildEnvironment::default();
111 process_doc(&mut env, docname, &doctree, Path::new("/src"), &[]);
112 env.dependencies
113 .get(docname)
114 .map(|set| set.iter().cloned().collect())
115 .unwrap_or_default()
116 }
117
118 #[test]
121 fn parse_recorded_dependencies_join_the_image_walk() {
122 let doctree = rst::parse_rst(".. image:: pic.png\n", &rst::ParseOptions::default());
123 let mut env = BuildEnvironment::default();
124 process_doc(
125 &mut env,
126 "index",
127 &doctree,
128 Path::new("/src"),
129 &["part.rst".to_string(), "data/snippet.txt".to_string()],
130 );
131 let paths: Vec<PathBuf> = env.dependencies["index"].iter().cloned().collect();
132 assert_eq!(
133 paths,
134 vec![
135 PathBuf::from("/src/data/snippet.txt"),
136 PathBuf::from("/src/part.rst"),
137 PathBuf::from("/src/pic.png"),
138 ]
139 );
140 }
141
142 #[test]
143 fn an_image_uri_resolves_against_the_documents_own_directory() {
144 assert_eq!(
145 deps("chapters/intro", ".. image:: pic.png\n"),
146 vec![PathBuf::from("/src/chapters/pic.png")]
147 );
148 assert_eq!(
149 deps("index", ".. image:: img/pic.png\n"),
150 vec![PathBuf::from("/src/img/pic.png")]
151 );
152 }
153
154 #[test]
155 fn a_rooted_uri_resolves_against_the_source_directory() {
156 assert_eq!(
157 deps("chapters/intro", ".. image:: /img/pic.png\n"),
158 vec![PathBuf::from("/src/img/pic.png")]
159 );
160 }
161
162 #[test]
163 fn dot_segments_are_normalized_away() {
164 assert_eq!(
165 deps("chapters/intro", ".. image:: ../img/./pic.png\n"),
166 vec![PathBuf::from("/src/img/pic.png")]
167 );
168 }
169
170 #[test]
171 fn a_figures_image_is_a_dependency_too() {
172 assert_eq!(
173 deps("index", ".. figure:: pic.png\n\n A caption.\n"),
174 vec![PathBuf::from("/src/pic.png")]
175 );
176 }
177
178 #[test]
179 fn remote_inline_and_candidate_uris_are_not_local_files() {
180 assert!(deps("index", ".. image:: https://example.com/pic.png\n").is_empty());
181 assert!(deps("index", ".. image:: data:image/png;base64,iVBOR\n").is_empty());
182 assert!(
183 deps("index", ".. image:: pic.*\n").is_empty(),
184 "a candidate pattern names no single file; noting it would peg \
185 the document to a re-read on every build"
186 );
187 }
188
189 #[test]
190 fn a_document_without_files_has_no_entry_at_all() {
191 let doctree = rst::parse_rst("Title\n=====\n", &rst::ParseOptions::default());
192 let mut env = BuildEnvironment::default();
193 env.dependencies
194 .insert("index".to_string(), BTreeSet::from([PathBuf::from("/old")]));
195
196 process_doc(&mut env, "index", &doctree, Path::new("/src"), &[]);
197
198 assert!(
199 !env.dependencies.contains_key("index"),
200 "a re-read that finds no dependencies must drop the stale ones"
201 );
202 }
203
204 #[test]
205 fn several_images_all_count_once() {
206 assert_eq!(
207 deps(
208 "index",
209 ".. image:: a.png\n\n.. image:: b.png\n\n.. image:: a.png\n"
210 ),
211 vec![PathBuf::from("/src/a.png"), PathBuf::from("/src/b.png")]
212 );
213 }
214}