/
e4779
/
okf-viewer
Обзор
Документация
Войти
/
e4779
/
okf-viewer
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
main
src/bundle.rs
117 строк
4 KB
Ерм Ник
feat: initial release — okf-viewer v0.1.0
05 июл 2026, 18:03
05 июл 2026, 18:03
664d11a
Код
Авторство
О чём код?
//! Walk an OKF bundle directory and collect concepts. use std::path::{Path, PathBuf}; use anyhow::Context; use walkdir::WalkDir; use crate::frontmatter; use crate::links; /// Filenames that are NOT concepts even if they have frontmatter. const RESERVED: &[&str] = &["index.md", "log.md"]; /// A parsed concept. #[derive(Debug, Clone)] pub struct Concept { pub id: String, #[allow(dead_code)] pub path: PathBuf, pub ctype: String, pub title: String, pub description: String, pub tags: Vec<String>, pub body: String, pub outbound: Vec<String>, } /// Walk `bundle_path` and parse every concept file. pub fn load(bundle_path: &Path) -> anyhow::Result<Vec<Concept>> { let bundle_path = bundle_path.canonicalize().context("bundle path not found")?; if !bundle_path.is_dir() { anyhow::bail!("not a directory: {}", bundle_path.display()); } let mut out = Vec::new(); for entry in WalkDir::new(&bundle_path) .into_iter() .filter_map(|e| e.ok()) { if !entry.file_type().is_file() { continue; } let path = entry.path(); let rel = match path.strip_prefix(&bundle_path) { Ok(r) => r, Err(_) => continue, }; let name = match rel.file_name().and_then(|n| n.to_str()) { Some(n) => n, None => continue, }; if !name.ends_with(".md") { continue; } if RESERVED.contains(&name) { continue; } let src = match std::fs::read_to_string(path) { Ok(s) => s, Err(_) => continue, }; let Some((fm_text, body)) = frontmatter::split_frontmatter(&src) else { continue; }; let fm = frontmatter::parse(fm_text); let ctype = fm.get_str("type").unwrap_or("").trim().to_string(); if ctype.is_empty() { continue; } let title = fm.get_str("title").map(|s| s.to_string()).unwrap_or_default(); let description = fm .get_str("description") .map(|s| s.to_string()) .unwrap_or_default(); let tags = fm.get_list("tags"); let id = rel .to_string_lossy() .trim_end_matches(".md") .to_string(); let outbound = links::extract(body) .into_iter() .map(|l| l.target) .collect::<Vec<_>>(); out.push(Concept { id, path: path.to_path_buf(), ctype, title, description, tags, body: body.to_string(), outbound, }); } Ok(out) } /// Resolve a bundle-absolute target like `/foo.md` to a concept id `foo`. /// Walks up: if the bundle has `bundles/common/foo.md`, then `/foo.md` /// resolves to the concept with id `foo` (top-level under bundle root). /// We also try matching by suffix against known ids. pub fn resolve_target<'a>(target: &str, ids: &'a [String]) -> Option<&'a str> { debug_assert!(target.starts_with('/') && target.ends_with(".md")); let bare = &target[1..target.len() - 3]; // strip leading `/` and trailing `.md` // Direct match: id equals `bare`. for id in ids { if id == bare { return Some(id.as_str()); } } // Suffix match: id ends with `/bare` (same filename, different dir). for id in ids { if id.ends_with(bare) && id.as_bytes().get(id.len() - bare.len() - 1) == Some(&b'/') { return Some(id.as_str()); } } None }