1use anyhow::{anyhow, Result};
8
9pub fn format_iso(ms: i64) -> String {
11 chrono::DateTime::from_timestamp_millis(ms)
12 .unwrap_or_default()
13 .format("%Y-%m-%dT%H:%M:%S%.3fZ")
14 .to_string()
15}
16
17pub fn now_ms() -> i64 {
21 std::time::SystemTime::now()
22 .duration_since(std::time::UNIX_EPOCH)
23 .expect("系统时钟早于 Unix 纪元")
24 .as_millis() as i64
25}
26
27pub fn new_id() -> String {
29 let mut bytes = [0u8; 16];
30 getrandom::getrandom(&mut bytes).expect("getrandom 失败");
31 let mut s = String::with_capacity(32);
32 for b in bytes {
33 s.push_str(&format!("{b:02x}"));
34 }
35 s
36}
37
38pub fn update_note_md(original: &str, new_title: &str, new_body: &str, now: i64) -> Result<String> {
40 let lines: Vec<&str> = original.split('\n').collect();
41
42 let mut sep = None;
44 let mut i = lines.len();
45 while i > 0 {
46 i -= 1;
47 let t = lines[i].trim();
48 if t.is_empty() {
49 sep = Some(i);
50 break;
51 }
52 if !t.contains(':') {
53 break; }
55 }
56 let sep = sep.ok_or_else(|| anyhow!("无法定位元数据块"))?;
57
58 let iso = format_iso(now);
59 let new_meta: Vec<String> = lines[sep + 1..]
60 .iter()
61 .map(|l| {
62 let key = l.trim_start();
63 if key.starts_with("updated_time:") {
64 format!("updated_time: {iso}")
65 } else if key.starts_with("user_updated_time:") {
66 format!("user_updated_time: {iso}")
67 } else {
68 (*l).to_string()
69 }
70 })
71 .collect();
72
73 Ok(format!("{new_title}\n\n{new_body}\n\n{}", new_meta.join("\n")))
74}
75
76pub fn move_note_md(original: &str, new_parent_id: &str, now: i64) -> Result<String> {
79 let lines: Vec<&str> = original.split('\n').collect();
80
81 let mut sep = None;
83 let mut i = lines.len();
84 while i > 0 {
85 i -= 1;
86 let t = lines[i].trim();
87 if t.is_empty() {
88 sep = Some(i);
89 break;
90 }
91 if !t.contains(':') {
92 break;
93 }
94 }
95 let sep = sep.ok_or_else(|| anyhow!("无法定位元数据块"))?;
96
97 let iso = format_iso(now);
98 let new_meta: Vec<String> = lines[sep + 1..]
99 .iter()
100 .map(|l| {
101 let key = l.trim_start();
102 if key.starts_with("parent_id:") {
103 format!("parent_id: {new_parent_id}")
104 } else if key.starts_with("updated_time:") {
105 format!("updated_time: {iso}")
106 } else if key.starts_with("user_updated_time:") {
107 format!("user_updated_time: {iso}")
108 } else {
109 (*l).to_string()
110 }
111 })
112 .collect();
113
114 let head = lines[..sep].join("\n");
116 Ok(format!("{head}\n\n{}", new_meta.join("\n")))
117}
118
119pub fn new_note_md(id: &str, parent_id: &str, title: &str, body: &str, is_todo: bool, now: i64) -> String {
122 let iso = format_iso(now);
123 let props = [
124 format!("id: {id}"),
125 format!("parent_id: {parent_id}"),
126 format!("created_time: {iso}"),
127 format!("updated_time: {iso}"),
128 "is_conflict: 0".to_string(),
129 "latitude: 0.00000000".to_string(),
130 "longitude: 0.00000000".to_string(),
131 "altitude: 0.0000".to_string(),
132 "author: ".to_string(),
133 "source_url: ".to_string(),
134 format!("is_todo: {}", if is_todo { 1 } else { 0 }),
135 "todo_due: 0".to_string(),
136 "todo_completed: 0".to_string(),
137 "source: jasper".to_string(),
138 "source_application: net.cozic.jasper".to_string(),
139 "application_data: ".to_string(),
140 "order: 0".to_string(),
141 format!("user_created_time: {iso}"),
142 format!("user_updated_time: {iso}"),
143 "encryption_cipher_text: ".to_string(),
144 "encryption_applied: 0".to_string(),
145 "markup_language: 1".to_string(),
146 "is_shared: 0".to_string(),
147 "share_id: ".to_string(),
148 "conflict_original_id: ".to_string(),
149 "master_key_id: ".to_string(),
150 "user_data: ".to_string(),
151 "deleted_time: 0".to_string(),
152 "type_: 1".to_string(),
153 ];
154 format!("{title}\n\n{body}\n\n{}", props.join("\n"))
155}
156
157pub fn new_folder_md(id: &str, parent_id: &str, title: &str, now: i64) -> String {
160 let iso = format_iso(now);
161 let props = [
162 format!("id: {id}"),
163 format!("created_time: {iso}"),
164 format!("updated_time: {iso}"),
165 format!("user_created_time: {iso}"),
166 format!("user_updated_time: {iso}"),
167 "encryption_cipher_text: ".to_string(),
168 "encryption_applied: 0".to_string(),
169 format!("parent_id: {parent_id}"),
170 "is_shared: 0".to_string(),
171 "share_id: ".to_string(),
172 "master_key_id: ".to_string(),
173 "icon: ".to_string(),
174 "user_data: ".to_string(),
175 "deleted_time: 0".to_string(),
176 "type_: 2".to_string(),
177 ];
178 format!("{title}\n\n{}", props.join("\n"))
179}
180
181pub fn move_folder_md(original: &str, new_parent_id: &str, now: i64) -> Result<String> {
184 let lines: Vec<&str> = original.split('\n').collect();
185 let mut sep = None;
186 let mut i = lines.len();
187 while i > 0 {
188 i -= 1;
189 let t = lines[i].trim();
190 if t.is_empty() {
191 sep = Some(i);
192 break;
193 }
194 if !t.contains(':') {
195 break;
196 }
197 }
198 let sep = sep.ok_or_else(|| anyhow!("无法定位元数据块"))?;
199 let iso = format_iso(now);
200 let new_meta: Vec<String> = lines[sep + 1..]
201 .iter()
202 .map(|l| {
203 let key = l.trim_start();
204 if key.starts_with("parent_id:") {
205 format!("parent_id: {new_parent_id}")
206 } else if key.starts_with("updated_time:") {
207 format!("updated_time: {iso}")
208 } else if key.starts_with("user_updated_time:") {
209 format!("user_updated_time: {iso}")
210 } else {
211 (*l).to_string()
212 }
213 })
214 .collect();
215 let head = lines[..sep].join("\n");
216 Ok(format!("{head}\n\n{}", new_meta.join("\n")))
217}
218
219pub fn rename_folder_md(original: &str, new_title: &str, now: i64) -> Result<String> {
222 let lines: Vec<&str> = original.split('\n').collect();
223 let mut sep = None;
224 let mut i = lines.len();
225 while i > 0 {
226 i -= 1;
227 let t = lines[i].trim();
228 if t.is_empty() {
229 sep = Some(i);
230 break;
231 }
232 if !t.contains(':') {
233 break;
234 }
235 }
236 let sep = sep.ok_or_else(|| anyhow!("无法定位元数据块"))?;
237 let iso = format_iso(now);
238 let new_meta: Vec<String> = lines[sep + 1..]
239 .iter()
240 .map(|l| {
241 let key = l.trim_start();
242 if key.starts_with("updated_time:") {
243 format!("updated_time: {iso}")
244 } else if key.starts_with("user_updated_time:") {
245 format!("user_updated_time: {iso}")
246 } else {
247 (*l).to_string()
248 }
249 })
250 .collect();
251 Ok(format!("{new_title}\n\n{}", new_meta.join("\n")))
252}
253
254pub fn new_resource_md(
258 id: &str,
259 title: &str,
260 mime: &str,
261 file_extension: &str,
262 size: i64,
263 now: i64,
264) -> String {
265 let iso = format_iso(now);
266 let props = [
267 format!("id: {id}"),
268 format!("mime: {mime}"),
269 "filename: ".to_string(),
270 format!("created_time: {iso}"),
271 format!("updated_time: {iso}"),
272 format!("user_created_time: {iso}"),
273 format!("user_updated_time: {iso}"),
274 format!("file_extension: {file_extension}"),
275 "encryption_cipher_text: ".to_string(),
276 "encryption_applied: 0".to_string(),
277 "encryption_blob_encrypted: 0".to_string(),
278 format!("size: {size}"),
279 "is_shared: 0".to_string(),
280 "share_id: ".to_string(),
281 "master_key_id: ".to_string(),
282 "user_data: ".to_string(),
283 format!("blob_updated_time: {now}"),
284 "ocr_text: ".to_string(),
285 "ocr_details: ".to_string(),
286 "ocr_status: 0".to_string(),
287 "ocr_error: ".to_string(),
288 "ocr_driver_id: 1".to_string(),
289 "type_: 4".to_string(),
290 ];
291 format!("{title}\n\n{}", props.join("\n"))
292}
293
294pub fn update_resource_md(original: &str, new_title: &str, now: i64) -> Result<String> {
297 let lines: Vec<&str> = original.split('\n').collect();
298 let mut sep = None;
299 let mut i = lines.len();
300 while i > 0 {
301 i -= 1;
302 let t = lines[i].trim();
303 if t.is_empty() {
304 sep = Some(i);
305 break;
306 }
307 if !t.contains(':') {
308 break;
309 }
310 }
311 let sep = sep.ok_or_else(|| anyhow!("无法定位元数据块"))?;
312 let iso = format_iso(now);
313 let new_meta: Vec<String> = lines[sep + 1..]
314 .iter()
315 .map(|l| {
316 let key = l.trim_start();
317 if key.starts_with("updated_time:") {
318 format!("updated_time: {iso}")
319 } else if key.starts_with("user_updated_time:") {
320 format!("user_updated_time: {iso}")
321 } else {
322 (*l).to_string()
323 }
324 })
325 .collect();
326 Ok(format!("{new_title}\n\n{}", new_meta.join("\n")))
327}
328
329#[cfg(test)]
330mod tests {
331 use super::*;
332 use crate::parser;
333
334 const SAMPLE: &str = "原标题\n\n原正文第一行\n\n原正文第二行\n\nid: a1b2c3d4e5f60718293a4b5c6d7e8f90\nparent_id: 0f1e2d3c4b5a69788796a5b4c3d2e1f0\ncreated_time: 2024-01-01T00:00:00.000Z\nupdated_time: 2024-01-01T00:00:00.000Z\nsource_url: https://x.com\nmarkup_language: 1\nuser_updated_time: 2024-01-01T00:00:00.000Z\ntype_: 1";
335
336 #[test]
337 fn update_preserves_metadata_changes_body_and_time() {
338 let out = update_note_md(SAMPLE, "新标题", "新正文", 1_700_000_000_000).unwrap();
339 let raw = parser::parse_item(&out).unwrap();
340 let note = parser::to_note(&raw).unwrap();
341 assert_eq!(note.title, "新标题");
342 assert_eq!(note.body, "新正文");
343 assert_eq!(note.parent_id, "0f1e2d3c4b5a69788796a5b4c3d2e1f0");
345 assert_eq!(raw.prop("source_url"), Some("https://x.com"));
346 assert_eq!(raw.prop("id"), Some("a1b2c3d4e5f60718293a4b5c6d7e8f90"));
347 assert_eq!(note.created_time, 1_704_067_200_000); assert_eq!(note.updated_time, 1_700_000_000_000);
350 }
351
352 #[test]
353 fn move_changes_parent_keeps_rest() {
354 let out = move_note_md(SAMPLE, "ffffffffffffffffffffffffffffffff", 1_700_000_000_000).unwrap();
355 let raw = parser::parse_item(&out).unwrap();
356 let note = parser::to_note(&raw).unwrap();
357 assert_eq!(note.parent_id, "ffffffffffffffffffffffffffffffff");
359 assert_eq!(note.title, "原标题");
360 assert_eq!(note.body, "原正文第一行\n\n原正文第二行");
361 assert_eq!(raw.prop("source_url"), Some("https://x.com"));
362 assert_eq!(raw.prop("id"), Some("a1b2c3d4e5f60718293a4b5c6d7e8f90"));
363 assert_eq!(note.created_time, 1_704_067_200_000);
365 assert_eq!(note.updated_time, 1_700_000_000_000);
366 }
367
368 #[test]
369 fn new_note_is_parseable() {
370 let id = "ffffffffffffffffffffffffffffffff";
371 let out = new_note_md(id, "parentparentparentparentparent12", "标题", "正文内容", false, 1_700_000_000_000);
372 let raw = parser::parse_item(&out).unwrap();
373 let note = parser::to_note(&raw).unwrap();
374 assert_eq!(note.id, id);
375 assert_eq!(note.title, "标题");
376 assert_eq!(note.body, "正文内容");
377 assert_eq!(note.parent_id, "parentparentparentparentparent12");
378 assert_eq!(note.markup_language, crate::model::MarkupLanguage::Markdown);
379 assert!(!note.is_todo);
380 }
381
382 #[test]
383 fn new_todo_sets_is_todo() {
384 let out = new_note_md("ffffffffffffffffffffffffffffffff", "parentparentparentparentparent12", "待办", "", true, 1_700_000_000_000);
385 let note = parser::to_note(&parser::parse_item(&out).unwrap()).unwrap();
386 assert!(note.is_todo);
387 assert!(!note.todo_completed);
388 }
389
390 #[test]
391 fn new_folder_is_parseable() {
392 let id = "abcabcabcabcabcabcabcabcabcabc12";
393 let out = new_folder_md(id, "parentparentparentparentparent12", "我的笔记本", 1_700_000_000_000);
394 let raw = parser::parse_item(&out).unwrap();
395 assert_eq!(raw.item_type(), crate::model::ItemType::Folder);
396 let f = parser::to_folder(&raw).unwrap();
397 assert_eq!(f.id, id);
398 assert_eq!(f.title, "我的笔记本");
399 assert_eq!(f.parent_id, "parentparentparentparentparent12");
400 }
401
402 #[test]
403 fn move_folder_changes_parent() {
404 let orig = new_folder_md("abcabcabcabcabcabcabcabcabcabc12", "oldoldoldoldoldoldoldoldoldold12", "本子", 1_700_000_000_000);
405 let out = move_folder_md(&orig, "newnewnewnewnewnewnewnewnewnew12", 1_700_000_999_000).unwrap();
406 let f = parser::to_folder(&parser::parse_item(&out).unwrap()).unwrap();
407 assert_eq!(f.parent_id, "newnewnewnewnewnewnewnewnewnew12");
408 assert_eq!(f.title, "本子");
409 assert_eq!(f.updated_time, 1_700_000_999_000);
410 }
411
412 #[test]
413 fn rename_folder_changes_title_keeps_parent() {
414 let orig = new_folder_md("abcabcabcabcabcabcabcabcabcabc12", "parentparentparentparentparent12", "旧名字", 1_700_000_000_000);
415 let out = rename_folder_md(&orig, "新名字", 1_700_000_999_000).unwrap();
416 let f = parser::to_folder(&parser::parse_item(&out).unwrap()).unwrap();
417 assert_eq!(f.title, "新名字");
418 assert_eq!(f.parent_id, "parentparentparentparentparent12"); assert_eq!(f.updated_time, 1_700_000_999_000); }
421
422 #[test]
423 fn new_resource_is_parseable() {
424 let id = "abcdef0123456789abcdef0123456789";
425 let out = new_resource_md(id, "photo.png", "image/png", "png", 12345, 1_700_000_000_000);
426 let raw = parser::parse_item(&out).unwrap();
427 assert_eq!(raw.item_type(), crate::model::ItemType::Resource);
428 let r = parser::to_resource(&raw).unwrap();
429 assert_eq!(r.id, id);
430 assert_eq!(r.title, "photo.png");
431 assert_eq!(r.mime, "image/png");
432 assert_eq!(r.file_extension, "png");
433 assert_eq!(r.size, 12345);
434 assert!(raw.body.is_none());
436 assert_eq!(raw.prop("ocr_driver_id"), Some("1"));
437 }
438
439 #[test]
440 fn rename_resource_keeps_metadata() {
441 let id = "abcdef0123456789abcdef0123456789";
442 let orig = new_resource_md(id, "old.png", "image/png", "png", 99, 1_700_000_000_000);
443 let out = update_resource_md(&orig, "新名字.png", 1_700_000_999_000).unwrap();
444 let raw = parser::parse_item(&out).unwrap();
445 let r = parser::to_resource(&raw).unwrap();
446 assert_eq!(r.title, "新名字.png");
447 assert_eq!(r.id, id); assert_eq!(r.mime, "image/png");
449 assert_eq!(r.size, 99);
450 assert_eq!(r.updated_time, 1_700_000_999_000); }
452
453 #[test]
454 fn new_id_is_32_hex() {
455 let id = new_id();
456 assert_eq!(id.len(), 32);
457 assert!(id.chars().all(|c| c.is_ascii_hexdigit()));
458 assert_ne!(new_id(), new_id());
459 }
460}