use std::fs;
use std::path::{Path, PathBuf};
use serde::Deserialize;
pub struct M3lFile {
pub path: String,
pub content: String,
}
#[derive(Debug, Deserialize)]
pub struct M3lConfig {
pub name: Option<String>,
pub version: Option<String>,
pub sources: Option<Vec<String>>,
}
fn source_path(path: &Path) -> String {
let text = path.to_string_lossy();
if std::path::MAIN_SEPARATOR == '\\' {
text.replace('\\', "/")
} else {
text.into_owned()
}
}
pub fn read_m3l_files(input_path: &Path) -> Result<Vec<M3lFile>, String> {
if !input_path.exists() {
return Err(format!("Path does not exist: {}", input_path.display()));
}
if input_path.is_file() {
let content = fs::read_to_string(input_path)
.map_err(|e| format!("Failed to read {}: {}", input_path.display(), e))?;
return Ok(vec![M3lFile {
path: source_path(input_path),
content,
}]);
}
if input_path.is_dir() {
let config_path = input_path.join("m3l.config.yaml");
if config_path.exists() {
return read_from_config(&config_path, input_path);
}
return scan_directory(input_path);
}
Err(format!(
"Path is neither a file nor a directory: {}",
input_path.display()
))
}
pub fn read_project_config(dir_path: &Path) -> Option<M3lConfig> {
let config_path = dir_path.join("m3l.config.yaml");
if !config_path.exists() {
return None;
}
let content = fs::read_to_string(&config_path).ok()?;
serde_yaml::from_str(&content).ok()
}
fn scan_directory(dir_path: &Path) -> Result<Vec<M3lFile>, String> {
let patterns = [
dir_path.join("**/*.m3l.md"),
dir_path.join("**/*.m3l"),
dir_path.join("**/*.md"),
];
let mut paths: Vec<PathBuf> = Vec::new();
for pattern in &patterns {
let pattern_str = pattern.to_string_lossy().replace('\\', "/");
let entries =
glob::glob(&pattern_str).map_err(|e| format!("Invalid glob pattern: {}", e))?;
for entry in entries {
match entry {
Ok(path) => {
if !paths.contains(&path) {
paths.push(path);
}
}
Err(e) => {
return Err(format!("Glob error: {}", e));
}
}
}
}
paths.sort();
let mut files = Vec::new();
for path in paths {
let content = fs::read_to_string(&path)
.map_err(|e| format!("Failed to read {}: {}", path.display(), e))?;
files.push(M3lFile {
path: source_path(&path),
content,
});
}
Ok(files)
}
fn read_from_config(config_path: &Path, base_dir: &Path) -> Result<Vec<M3lFile>, String> {
let yaml_content =
fs::read_to_string(config_path).map_err(|e| format!("Failed to read config: {}", e))?;
let config: M3lConfig =
serde_yaml::from_str(&yaml_content).map_err(|e| format!("Invalid YAML config: {}", e))?;
let source_patterns = match config.sources {
Some(ref s) if !s.is_empty() => s.clone(),
_ => return scan_directory(base_dir),
};
let mut files: Vec<M3lFile> = Vec::new();
let mut seen: std::collections::HashSet<PathBuf> = std::collections::HashSet::new();
for pattern in &source_patterns {
let full_pattern = base_dir.join(pattern);
let pattern_str = full_pattern.to_string_lossy().replace('\\', "/");
let entries = glob::glob(&pattern_str)
.map_err(|e| format!("Invalid glob pattern '{}': {}", pattern, e))?;
let mut matched: Vec<PathBuf> = Vec::new();
for entry in entries {
match entry {
Ok(path) => {
if !seen.contains(&path) {
seen.insert(path.clone());
matched.push(path);
}
}
Err(e) => return Err(format!("Glob error: {}", e)),
}
}
matched.sort();
for path in matched {
let content = fs::read_to_string(&path)
.map_err(|e| format!("Failed to read {}: {}", path.display(), e))?;
files.push(M3lFile {
path: source_path(&path),
content,
});
}
}
Ok(files)
}