use super::Note;
use std::collections::{HashMap, HashSet};
const FORBIDDEN: [char; 9] = ['/', '\\', ':', '*', '?', '"', '<', '>', '|'];
pub(super) fn assign_paths(notes: &mut [Note]) {
let preserved = preserved_paths(notes);
let mut taken: HashSet<String> = HashSet::with_capacity(notes.len());
for (at, note) in notes.iter_mut().enumerate() {
let mut path = match preserved.get(&at) {
Some(path) => path.clone(),
None => format!(
"{}/{}.md",
sanitize_segment(¬e.label),
sanitize_segment(provenance_stem(note).unwrap_or_else(|| note.display_name()))
),
};
if taken.contains(&path.to_ascii_lowercase()) {
let stem = path.strip_suffix(".md").unwrap_or(&path).to_string();
path = format!("{stem}-{}.md", sanitize_segment(note.id()));
let mut nth = 2;
while taken.contains(&path.to_ascii_lowercase()) {
path = format!("{stem}-{}-{nth}.md", sanitize_segment(note.id()));
nth += 1;
}
}
taken.insert(path.to_ascii_lowercase());
note.out = path;
}
}
fn preserved_paths(notes: &[Note]) -> HashMap<usize, String> {
let mut out = HashMap::new();
for (at, note) in notes.iter().enumerate() {
let Some(path) = note.file_path.as_deref() else {
continue;
};
if !path.ends_with(".md") {
continue;
}
if top_folder(path) == Some(note.label.as_str()) {
out.insert(at, path.to_string());
}
}
out
}
fn provenance_stem(note: &Note) -> Option<&str> {
let path = note.file_path.as_deref()?;
let file = path.rsplit('/').next().unwrap_or(path);
let stem = file.strip_suffix(".md")?;
(!stem.is_empty()).then_some(stem)
}
fn top_folder(path: &str) -> Option<&str> {
path.split_once('/').map(|(head, _)| head)
}
pub(super) fn sanitize_segment(name: &str) -> String {
let mut out: String = name
.chars()
.map(|c| {
if FORBIDDEN.contains(&c) || c.is_control() {
'-'
} else {
c
}
})
.collect();
while out.ends_with('.') || out.ends_with(' ') {
out.pop();
}
let trimmed = out.trim_start();
if trimmed.len() != out.len() {
out = trimmed.to_string();
}
if out.is_empty() {
out.push_str("untitled");
}
out
}
#[cfg(test)]
#[path = "paths_tests.rs"]
mod paths_tests;