Skip to main content

specfold_core/import/
sources.rs

1//! Expands the map's patterns into import units: one main file with its chapters and
2//! attachments.
3
4use super::map::{ImportMap, SourceRule};
5use crate::workspace::owners::{PathMatch, matcher};
6use ignore::WalkBuilder;
7use std::collections::BTreeSet;
8use std::path::Path;
9
10#[derive(Debug, Clone)]
11pub struct Unit {
12    pub rule: usize,
13    /// The file or directory that matched.
14    pub source: String,
15    pub main: String,
16    pub chapters: Vec<String>,
17    pub attachments: Vec<String>,
18}
19
20const MAIN_NAMES: &[&str] = &["index.md", "README.md", "readme.md", "00-index.md", "_index.md"];
21
22fn all_files(root: &Path) -> Vec<String> {
23    WalkBuilder::new(root)
24        .hidden(false)
25        .require_git(false)
26        .filter_entry(|e| e.file_name() != ".git")
27        .build()
28        .flatten()
29        .filter(|e| e.file_type().is_some_and(|t| t.is_file()))
30        .filter_map(|e| e.path().strip_prefix(root).ok().map(|p| p.to_string_lossy().replace('\\', "/")))
31        .collect()
32}
33
34fn bundle(files: &[String], dir: &str) -> Option<(String, Vec<String>, Vec<String>)> {
35    let prefix = format!("{dir}/");
36    let inside: Vec<&String> = files.iter().filter(|f| f.starts_with(&prefix)).collect();
37    let main = MAIN_NAMES
38        .iter()
39        .map(|n| format!("{dir}/{n}"))
40        .find(|m| inside.contains(&m))
41        .or_else(|| inside.iter().filter(|f| f.ends_with(".md")).min().map(|s| s.to_string()))?;
42    let chapters = inside.iter().filter(|f| f.ends_with(".md") && **f != &main).map(|s| s.to_string()).collect();
43    let attachments = inside.iter().filter(|f| !f.ends_with(".md")).map(|s| s.to_string()).collect();
44    Some((main, chapters, attachments))
45}
46
47pub fn expand(root: &Path, map: &ImportMap) -> Vec<Unit> {
48    let files = all_files(root);
49    let mut claimed: BTreeSet<String> = BTreeSet::new();
50    let mut out = vec![];
51    for (ri, rule) in map.sources.iter().enumerate() {
52        out.extend(expand_rule(&files, ri, rule, &mut claimed));
53    }
54    out
55}
56
57fn expand_rule(files: &[String], ri: usize, rule: &SourceRule, claimed: &mut BTreeSet<String>) -> Vec<Unit> {
58    let dir_mode = rule.glob.ends_with('/');
59    let Some(m) = matcher(rule.glob.trim_end_matches('/')) else { return vec![] };
60    let exclude: Vec<PathMatch> = rule.exclude.iter().filter_map(|e| matcher(e)).collect();
61    let skip = |p: &str, claimed: &BTreeSet<String>| exclude.iter().any(|e| e.is_match(p)) || claimed.contains(p);
62    let mut out = vec![];
63    if dir_mode {
64        let mut dirs: BTreeSet<String> = BTreeSet::new();
65        for f in files {
66            let mut parts: Vec<&str> = f.split('/').collect();
67            parts.pop();
68            for n in 1..=parts.len() {
69                let d = parts[..n].join("/");
70                if m.is_match(&d) {
71                    dirs.insert(d);
72                }
73            }
74        }
75        for d in dirs {
76            if skip(&d, claimed) {
77                continue;
78            }
79            if let Some((main, chapters, attachments)) = bundle(files, &d) {
80                for f in files.iter().filter(|f| f.starts_with(&format!("{d}/"))) {
81                    claimed.insert(f.clone());
82                }
83                claimed.insert(d.clone());
84                out.push(Unit { rule: ri, source: d, main, chapters, attachments });
85            }
86        }
87        return out;
88    }
89    for f in files {
90        if !m.is_match(f) || skip(f, claimed) {
91            continue;
92        }
93        claimed.insert(f.clone());
94        let (mut chapters, mut attachments) = (vec![], vec![]);
95        if rule.sibling_dir {
96            let dir = f.trim_end_matches(".md");
97            for g in files.iter().filter(|g| g.starts_with(&format!("{dir}/"))) {
98                claimed.insert(g.clone());
99                if g.ends_with(".md") { chapters.push(g.clone()) } else { attachments.push(g.clone()) }
100            }
101        }
102        out.push(Unit { rule: ri, source: f.clone(), main: f.clone(), chapters, attachments });
103    }
104    out
105}