use std::collections::BTreeSet;
use std::path::{Path, PathBuf};
use crate::doctree::{kinds, AttrValue, Doctree, Node};
use crate::utils::relfn2path;
use super::BuildEnvironment;
pub fn process_doc(
env: &mut BuildEnvironment,
docname: &str,
doctree: &Doctree,
srcdir: &Path,
parse_dependencies: &[String],
) {
let mut paths = BTreeSet::new();
collect(&doctree.root, docname, srcdir, &mut paths);
for dependency in parse_dependencies {
paths.insert(srcdir.join(dependency));
}
if paths.is_empty() {
env.dependencies.remove(docname);
} else {
env.dependencies.insert(docname.to_string(), paths);
}
}
pub fn note(uri: &str, docname: &str, srcdir: &Path) -> Option<PathBuf> {
if uri.starts_with("data:") || uri.contains("://") || uri.contains('*') || uri.is_empty() {
return None;
}
Some(relfn2path(uri, docname, srcdir))
}
fn collect(node: &Node, docname: &str, srcdir: &Path, out: &mut BTreeSet<PathBuf>) {
if node.kind == kinds::IMAGE {
if let Some(AttrValue::Str(uri)) = node.get("uri") {
if let Some(path) = note(uri, docname, srcdir) {
out.insert(path);
}
}
}
for child in &node.children {
collect(child, docname, srcdir, out);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::rst;
fn deps(docname: &str, source: &str) -> Vec<PathBuf> {
let doctree = rst::parse_rst(source, &rst::ParseOptions::default());
let mut env = BuildEnvironment::default();
process_doc(&mut env, docname, &doctree, Path::new("/src"), &[]);
env.dependencies
.get(docname)
.map(|set| set.iter().cloned().collect())
.unwrap_or_default()
}
#[test]
fn parse_recorded_dependencies_join_the_image_walk() {
let doctree = rst::parse_rst(".. image:: pic.png\n", &rst::ParseOptions::default());
let mut env = BuildEnvironment::default();
process_doc(
&mut env,
"index",
&doctree,
Path::new("/src"),
&["part.rst".to_string(), "data/snippet.txt".to_string()],
);
let paths: Vec<PathBuf> = env.dependencies["index"].iter().cloned().collect();
assert_eq!(
paths,
vec![
PathBuf::from("/src/data/snippet.txt"),
PathBuf::from("/src/part.rst"),
PathBuf::from("/src/pic.png"),
]
);
}
#[test]
fn an_image_uri_resolves_against_the_documents_own_directory() {
assert_eq!(
deps("chapters/intro", ".. image:: pic.png\n"),
vec![PathBuf::from("/src/chapters/pic.png")]
);
assert_eq!(
deps("index", ".. image:: img/pic.png\n"),
vec![PathBuf::from("/src/img/pic.png")]
);
}
#[test]
fn a_rooted_uri_resolves_against_the_source_directory() {
assert_eq!(
deps("chapters/intro", ".. image:: /img/pic.png\n"),
vec![PathBuf::from("/src/img/pic.png")]
);
}
#[test]
fn dot_segments_are_normalized_away() {
assert_eq!(
deps("chapters/intro", ".. image:: ../img/./pic.png\n"),
vec![PathBuf::from("/src/img/pic.png")]
);
}
#[test]
fn a_figures_image_is_a_dependency_too() {
assert_eq!(
deps("index", ".. figure:: pic.png\n\n A caption.\n"),
vec![PathBuf::from("/src/pic.png")]
);
}
#[test]
fn remote_inline_and_candidate_uris_are_not_local_files() {
assert!(deps("index", ".. image:: https://example.com/pic.png\n").is_empty());
assert!(deps("index", ".. image:: data:image/png;base64,iVBOR\n").is_empty());
assert!(
deps("index", ".. image:: pic.*\n").is_empty(),
"a candidate pattern names no single file; noting it would peg \
the document to a re-read on every build"
);
}
#[test]
fn a_document_without_files_has_no_entry_at_all() {
let doctree = rst::parse_rst("Title\n=====\n", &rst::ParseOptions::default());
let mut env = BuildEnvironment::default();
env.dependencies
.insert("index".to_string(), BTreeSet::from([PathBuf::from("/old")]));
process_doc(&mut env, "index", &doctree, Path::new("/src"), &[]);
assert!(
!env.dependencies.contains_key("index"),
"a re-read that finds no dependencies must drop the stale ones"
);
}
#[test]
fn several_images_all_count_once() {
assert_eq!(
deps(
"index",
".. image:: a.png\n\n.. image:: b.png\n\n.. image:: a.png\n"
),
vec![PathBuf::from("/src/a.png"), PathBuf::from("/src/b.png")]
);
}
}