use std::{
fs::{self},
path::{Path, PathBuf},
};
use anyhow::Context;
use scraps_libs::model::{context::Ctx, scrap::Scrap};
use scraps_libs::nfc::nfc;
use crate::error::{ScrapsError, ScrapsResult};
pub(crate) type ScrapWithTimestamp = (Scrap, Option<i64>);
pub(crate) type ScrapsWithReadme = (Vec<ScrapWithTimestamp>, Option<String>);
pub(crate) fn to_scrap_paths(
scraps_dir_path: &Path,
exclude_dirs: &[PathBuf],
) -> ScrapsResult<Vec<PathBuf>> {
let read_dir = fs::read_dir(scraps_dir_path).context(ScrapsError::ReadScraps)?;
let paths = read_dir
.map(|entry_res| {
let entry = entry_res?;
let entry_path = entry.path();
if entry.file_name().to_string_lossy().starts_with('.') {
return Ok(vec![]);
}
match entry.file_type() {
Ok(file_type) if file_type.is_file() => match entry_path.extension() {
Some(ext) if ext == "md" => Ok(vec![entry_path]),
_ => Ok(vec![]),
},
Ok(file_type) if file_type.is_dir() => {
if exclude_dirs.iter().any(|p| p == &entry_path) {
Ok(vec![])
} else {
to_scrap_paths(&entry_path, &[])
}
}
res => res
.map(|_| vec![])
.context(ScrapsError::ReadScrap(entry_path)),
}
})
.collect::<ScrapsResult<Vec<Vec<PathBuf>>>>()?;
Ok(paths.into_iter().flatten().collect::<Vec<PathBuf>>())
}
pub(crate) fn scrap_identity(
scraps_dir_path: &Path,
scrap_file_path: &Path,
) -> ScrapsResult<(String, Option<Ctx>)> {
let file_prefix = scrap_file_path
.file_stem()
.ok_or(ScrapsError::ReadScrap(scrap_file_path.to_path_buf()))
.map(|o| o.to_str())
.and_then(|fp| fp.ok_or(ScrapsError::ReadScrap(scrap_file_path.to_path_buf())))?;
let changed_directory_path = scrap_file_path
.strip_prefix(scraps_dir_path)
.context(ScrapsError::ReadScrap(scrap_file_path.to_path_buf()))?;
let ctx_segments: Vec<String> = changed_directory_path
.parent()
.map(|p| {
p.components()
.map(|c| c.as_os_str().to_string_lossy().to_string())
.filter(|s| !s.is_empty())
.collect()
})
.unwrap_or_default();
let ctx: Option<Ctx> = if ctx_segments.is_empty() {
None
} else {
Some(Ctx::from(ctx_segments.join("/").as_str()))
};
Ok((nfc(file_prefix).into_owned(), ctx))
}
pub(crate) fn relative_path(
scraps_dir_path: &Path,
scrap_file_path: &Path,
) -> ScrapsResult<String> {
let relative = scrap_file_path
.strip_prefix(scraps_dir_path)
.context(ScrapsError::ReadScrap(scrap_file_path.to_path_buf()))?;
Ok(relative
.components()
.map(|c| c.as_os_str().to_string_lossy().to_string())
.collect::<Vec<String>>()
.join("/"))
}
#[cfg(test)]
pub(crate) fn to_scrap_by_path(
scraps_dir_path: &Path,
scrap_file_path: &Path,
) -> ScrapsResult<Scrap> {
let (stem, ctx) = scrap_identity(scraps_dir_path, scrap_file_path)?;
let md_text = fs::read_to_string(scrap_file_path)
.context(ScrapsError::ReadScrap(scrap_file_path.to_path_buf()))?;
Ok(Scrap::new(&stem, &ctx, &md_text))
}
#[cfg(test)]
pub(crate) fn to_all_scraps(
scraps_dir_path: &Path,
exclude_dirs: &[PathBuf],
) -> ScrapsResult<Vec<Scrap>> {
let paths = to_scrap_paths(scraps_dir_path, exclude_dirs)?;
paths
.iter()
.map(|path| to_scrap_by_path(scraps_dir_path, path))
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_fixtures::TempScrapProject;
use std::collections::HashSet;
fn collect_keys(scraps: &[Scrap]) -> HashSet<String> {
scraps.iter().map(|s| s.self_key().to_string()).collect()
}
#[test]
fn skips_static_output_and_dotfiles() {
let project = TempScrapProject::new();
project.add_scrap("intro.md", b"# Intro");
project.add_static_file("style.md", b"# In static");
std::fs::write(project.output_dir.join("page.md"), b"# In output").unwrap();
let dot_dir = project.project_root.join(".cache");
std::fs::create_dir_all(&dot_dir).unwrap();
std::fs::write(dot_dir.join("note.md"), b"# In dotdir").unwrap();
std::fs::write(project.project_root.join(".hidden.md"), b"# Hidden").unwrap();
let exclude = vec![project.static_dir.clone(), project.output_dir.clone()];
let scraps = to_all_scraps(&project.project_root, &exclude).unwrap();
let keys = collect_keys(&scraps);
assert_eq!(keys, HashSet::from(["intro".to_string()]));
}
#[test]
fn derives_the_key_in_nfc_and_keeps_the_path_as_read() {
let root = Path::new("/wiki");
let file = root.join("cafe\u{301}/\u{30ab}\u{3099}\u{30a4}\u{30c8}\u{3099}.md");
let (stem, ctx) = scrap_identity(root, &file).unwrap();
assert_eq!(stem, "\u{30ac}\u{30a4}\u{30c9}");
assert_eq!(ctx, Some(Ctx::from("caf\u{e9}")));
assert_eq!(ctx.unwrap().to_string(), "caf\u{e9}");
assert_eq!(
relative_path(root, &file).unwrap(),
"cafe\u{301}/\u{30ab}\u{3099}\u{30a4}\u{30c8}\u{3099}.md"
);
}
#[test]
fn collects_nested_ctx_under_project_root() {
let project = TempScrapProject::new();
project.add_scrap("root.md", b"# Root");
project.add_scrap_with_context("architecture", "overview.md", b"# Overview");
let exclude = vec![project.static_dir.clone(), project.output_dir.clone()];
let scraps = to_all_scraps(&project.project_root, &exclude).unwrap();
let keys = collect_keys(&scraps);
assert_eq!(
keys,
HashSet::from(["root".to_string(), "architecture/overview".to_string()])
);
}
}