use crate::error::ApiError; use crate::state::AppState; use axum::extract::{Path, Query, State}; use axum::routing::get; use axum::{Json, Router}; use pulldown_cmark::{html, Parser}; use serde::Deserialize; use serde_json::{json, Value}; use std::sync::Arc; use vault_core::entity::VaultEntity; use vault_core::filesystem; use vault_core::types::KnowledgeNote; pub fn routes() -> Router> { Router::new() .route("/knowledge", get(list_knowledge)) .route("/knowledge/{*path}", get(get_knowledge)) } #[derive(Deserialize, Default)] struct SearchQuery { #[serde(default)] q: Option, #[serde(default)] tag: Option, } async fn list_knowledge( State(state): State>, Query(query): Query, ) -> Result, ApiError> { let dir = state.vault_root.join("knowledge"); let files = filesystem::list_md_files_recursive(&dir).map_err(ApiError::Vault)?; let mut notes = Vec::new(); for file in files { // Try parsing with frontmatter let content = std::fs::read_to_string(&file) .map_err(|e| ApiError::Vault(vault_core::VaultError::io(e, &file)))?; let (title, tags) = if let Ok(entity) = VaultEntity::::from_content(file.clone(), &content) { ( entity.frontmatter.title.unwrap_or_else(|| { file.file_stem() .and_then(|s| s.to_str()) .unwrap_or("untitled") .to_string() }), entity.frontmatter.tags, ) } else { ( file.file_stem() .and_then(|s| s.to_str()) .unwrap_or("untitled") .to_string(), vec![], ) }; // Apply filters if let Some(ref q) = query.q { let q_lower = q.to_lowercase(); if !title.to_lowercase().contains(&q_lower) && !content.to_lowercase().contains(&q_lower) { continue; } } if let Some(ref tag) = query.tag { if !tags.iter().any(|t| t == tag) { continue; } } let relative = file.strip_prefix(&state.vault_root).unwrap_or(&file); notes.push(json!({ "path": relative, "title": title, "tags": tags, })); } Ok(Json(json!(notes))) } async fn get_knowledge( State(state): State>, Path(path): Path, ) -> Result, ApiError> { let file_path = state.vault_root.join("knowledge").join(&path); if !file_path.exists() { return Err(ApiError::NotFound(format!("Knowledge note '{}' not found", path))); } let content = std::fs::read_to_string(&file_path) .map_err(|e| ApiError::Vault(vault_core::VaultError::io(e, &file_path)))?; let (frontmatter, body) = if let Ok(entity) = VaultEntity::::from_content(file_path.clone(), &content) { ( json!({ "title": entity.frontmatter.title, "tags": entity.frontmatter.tags, "source": entity.frontmatter.source, "created": entity.frontmatter.created, "related": entity.frontmatter.related, }), entity.body, ) } else { (json!({}), content.clone()) }; // Render markdown to HTML let parser = Parser::new(&body); let mut html_output = String::new(); html::push_html(&mut html_output, parser); Ok(Json(json!({ "path": path, "frontmatter": frontmatter, "body": body, "html": html_output, }))) }