1use chrono::Utc;
2use kimun_core::NoteVault;
3use kimun_core::nfs::NoteEntryData;
4use kimun_core::nfs::VaultPath;
5use kimun_core::note::{NoteContentData, NoteHeading, NoteMetadata, PropertyEntry};
6use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8
9#[derive(Debug, Serialize, Deserialize, Clone)]
10pub struct JsonHeader {
11 pub level: u32,
12 pub text: String,
13}
14
15impl From<NoteHeading> for JsonHeader {
16 fn from(heading: NoteHeading) -> Self {
17 Self {
18 level: heading.level.into(),
19 text: heading.text,
20 }
21 }
22}
23
24#[derive(Debug, Serialize, Deserialize)]
25pub struct JsonOutputMetadata {
26 pub workspace: String,
27 pub workspace_path: String,
28 pub total_results: usize,
29 pub query: Option<String>,
30 pub is_listing: bool,
31 pub generated_at: String,
32}
33
34#[derive(Debug, Default, Clone, PartialEq)]
38pub struct JsonProperties(pub Vec<PropertyEntry>);
39
40impl Serialize for JsonProperties {
41 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
42 use serde::ser::SerializeMap;
43 let mut map = serializer.serialize_map(Some(self.0.len()))?;
44 for (key, value) in &self.0 {
45 map.serialize_entry(key, value)?;
46 }
47 map.end()
48 }
49}
50
51#[derive(Debug, Serialize, Deserialize)]
53pub struct JsonNoteMetadata {
54 pub tags: Vec<String>,
55 pub links: Vec<String>,
56 pub headers: Vec<JsonHeader>,
57 #[serde(skip_deserializing, default)]
59 pub properties: JsonProperties,
60}
61
62impl JsonNoteMetadata {
63 pub fn from_content(content: &str) -> Self {
66 let meta = NoteMetadata::of(content);
67 Self {
68 tags: meta.tags,
69 links: meta.links,
70 headers: meta.headings.into_iter().map(JsonHeader::from).collect(),
71 properties: JsonProperties(meta.properties),
72 }
73 }
74}
75
76#[derive(Debug, Serialize, Deserialize)]
77pub struct JsonNoteEntry {
78 pub path: String,
79 pub title: String,
80 pub content: String,
81 pub size: u64,
82 pub modified: u64,
83 pub created: u64,
84 pub hash: String,
85 #[serde(skip_serializing_if = "Option::is_none")]
86 pub journal_date: Option<String>,
87 pub metadata: JsonNoteMetadata,
88 #[serde(skip_serializing_if = "Option::is_none")]
89 pub backlinks: Option<Vec<String>>,
90}
91
92#[derive(Debug, Serialize, Deserialize)]
93pub struct JsonOutput {
94 pub metadata: JsonOutputMetadata,
95 pub notes: Vec<JsonNoteEntry>,
96}
97
98pub fn format_notes_with_content_as_json(
100 vault: &NoteVault,
101 entries: &[(NoteEntryData, NoteContentData)],
102 content_map: &[(VaultPath, String)],
103 workspace_name: &str,
104 workspace_path: &str,
105 query: Option<&str>,
106 is_listing: bool,
107) -> Result<String, Box<dyn std::error::Error>> {
108 let output_metadata = JsonOutputMetadata {
109 workspace: workspace_name.to_string(),
110 workspace_path: workspace_path.to_string(),
111 total_results: entries.len(),
112 query: query.map(|q| q.to_string()),
113 is_listing,
114 generated_at: Utc::now().to_rfc3339(),
115 };
116
117 let content_lookup: HashMap<String, &str> = content_map
118 .iter()
119 .map(|(p, c)| (p.to_string(), c.as_str()))
120 .collect();
121
122 let notes = entries
123 .iter()
124 .map(|(entry_data, content_data)| {
125 let path_str = entry_data.path.to_string();
126 let path_with_ext = entry_data.path.to_string_with_ext();
127
128 let content: &str = content_lookup.get(&path_str).copied().unwrap_or("");
129
130 let journal_date = vault
132 .journal_date(&entry_data.path)
133 .map(|d| d.format("%Y-%m-%d").to_string());
134
135 let created = entry_data.modified_secs;
137
138 JsonNoteEntry {
139 path: path_with_ext,
140 title: content_data.title.clone(),
141 content: content.to_owned(),
142 size: entry_data.size,
143 modified: entry_data.modified_secs,
144 created,
145 hash: format!("{:x}", content_data.hash),
146 journal_date,
147 metadata: JsonNoteMetadata::from_content(content),
148 backlinks: None,
149 }
150 })
151 .collect();
152
153 let output = JsonOutput {
154 metadata: output_metadata,
155 notes,
156 };
157 Ok(serde_json::to_string(&output)?)
158}
159
160pub async fn format_notes_as_json(
162 vault: &NoteVault,
163 entries: &[(NoteEntryData, NoteContentData)],
164 workspace_name: &str,
165 query: Option<&str>,
166 is_listing: bool,
167) -> Result<String, Box<dyn std::error::Error>> {
168 let workspace_path = vault.workspace_path().to_string();
169
170 const JSON_FETCH_CONCURRENCY: usize = 32;
173 use futures::stream::StreamExt;
174 let content_results: Vec<_> = futures::stream::iter(entries.iter().map(|(entry_data, _)| {
175 let path = entry_data.path.clone();
176 async move {
177 match vault.get_note_text(&path).await {
178 Ok(content) => Some((path, content)),
179 Err(_) => None,
180 }
181 }
182 }))
183 .buffered(JSON_FETCH_CONCURRENCY)
184 .collect()
185 .await;
186 let content_map: Vec<_> = content_results.into_iter().flatten().collect();
187
188 format_notes_with_content_as_json(
189 vault,
190 entries,
191 &content_map,
192 workspace_name,
193 &workspace_path,
194 query,
195 is_listing,
196 )
197}