1use std::collections::HashMap;
2use chrono::Utc;
3use kimun_core::nfs::NoteEntryData;
4use kimun_core::note::NoteContentData;
5use kimun_core::nfs::VaultPath;
6use kimun_core::NoteVault;
7use serde::{Deserialize, Serialize};
8use crate::cli::metadata_extractor::{extract_tags, extract_links, extract_headers};
9
10#[derive(Debug, Serialize, Deserialize, Clone)]
11pub struct JsonHeader {
12 pub level: u32,
13 pub text: String,
14}
15
16#[derive(Debug, Serialize, Deserialize)]
17pub struct JsonOutputMetadata {
18 pub workspace: String,
19 pub workspace_path: String,
20 pub total_results: usize,
21 pub query: Option<String>,
22 pub is_listing: bool,
23 pub generated_at: String,
24}
25
26#[derive(Debug, Serialize, Deserialize)]
28pub struct JsonNoteMetadata {
29 pub tags: Vec<String>,
30 pub links: Vec<String>,
31 pub headers: Vec<JsonHeader>,
32}
33
34#[derive(Debug, Serialize, Deserialize)]
35pub struct JsonNoteEntry {
36 pub path: String,
37 pub title: String,
38 pub content: String,
39 pub size: u64,
40 pub modified: u64,
41 pub created: u64,
42 pub hash: String,
43 #[serde(skip_serializing_if = "Option::is_none")]
44 pub journal_date: Option<String>,
45 pub metadata: JsonNoteMetadata,
46 #[serde(skip_serializing_if = "Option::is_none")]
47 pub backlinks: Option<Vec<String>>,
48}
49
50#[derive(Debug, Serialize, Deserialize)]
51pub struct JsonOutput {
52 pub metadata: JsonOutputMetadata,
53 pub notes: Vec<JsonNoteEntry>,
54}
55
56pub fn ensure_md_extension(path: &str) -> String {
58 if path.ends_with(".md") {
59 path.to_owned()
60 } else {
61 format!("{}.md", path)
62 }
63}
64
65pub fn format_notes_with_content_as_json(
67 vault: &NoteVault,
68 entries: &[(NoteEntryData, NoteContentData)],
69 content_map: &[(VaultPath, String)],
70 workspace_name: &str,
71 workspace_path: &str,
72 query: Option<&str>,
73 is_listing: bool,
74) -> Result<String, Box<dyn std::error::Error>> {
75 let output_metadata = JsonOutputMetadata {
76 workspace: workspace_name.to_string(),
77 workspace_path: workspace_path.to_string(),
78 total_results: entries.len(),
79 query: query.map(|q| q.to_string()),
80 is_listing,
81 generated_at: Utc::now().to_rfc3339(),
82 };
83
84 let content_lookup: HashMap<String, &str> = content_map
85 .iter()
86 .map(|(p, c)| (p.to_string(), c.as_str()))
87 .collect();
88
89 let notes = entries
90 .iter()
91 .map(|(entry_data, content_data)| {
92 let path_str = entry_data.path.to_string();
93 let path_with_ext = entry_data.path.to_string_with_ext();
94
95 let content: &str = content_lookup.get(&path_str).copied().unwrap_or("");
96
97 let tags = extract_tags(content);
98 let links = extract_links(content);
99 let headers = extract_headers(content);
100
101 let journal_date = vault
103 .journal_date(&entry_data.path)
104 .map(|d| d.format("%Y-%m-%d").to_string());
105
106 let created = entry_data.modified_secs;
108
109 JsonNoteEntry {
110 path: path_with_ext,
111 title: content_data.title.clone(),
112 content: content.to_owned(),
113 size: entry_data.size,
114 modified: entry_data.modified_secs,
115 created,
116 hash: format!("{:x}", content_data.hash),
117 journal_date,
118 metadata: JsonNoteMetadata {
119 tags,
120 links,
121 headers,
122 },
123 backlinks: None,
124 }
125 })
126 .collect();
127
128 let output = JsonOutput { metadata: output_metadata, notes };
129 Ok(serde_json::to_string(&output)?)
130}
131
132pub async fn format_notes_as_json(
134 vault: &NoteVault,
135 entries: &[(NoteEntryData, NoteContentData)],
136 workspace_name: &str,
137 query: Option<&str>,
138 is_listing: bool,
139) -> Result<String, Box<dyn std::error::Error>> {
140 let workspace_path = vault.workspace_path.to_string_lossy().to_string();
141
142 let content_futures: Vec<_> = entries
144 .iter()
145 .map(|(entry_data, _)| async {
146 let path = entry_data.path.clone();
147 match vault.get_note_text(&path).await {
148 Ok(content) => Some((path, content)),
149 Err(_) => None,
150 }
151 })
152 .collect();
153
154 let content_results = futures::future::join_all(content_futures).await;
155 let content_map: Vec<_> = content_results.into_iter().flatten().collect();
156
157 format_notes_with_content_as_json(
158 vault,
159 entries,
160 &content_map,
161 workspace_name,
162 &workspace_path,
163 query,
164 is_listing,
165 )
166}