1pub mod obfile_in_memory;
4pub mod obfile_on_disk;
5
6use crate::error::Error;
7use serde::{Serialize, de::DeserializeOwned};
8use std::{borrow::Cow, collections::HashMap, fs::OpenOptions, io::Write, path::Path};
9
10pub(crate) type DefaultProperties = HashMap<String, serde_yml::Value>;
11
12pub trait ObFile<T = DefaultProperties>: Sized
39where
40 T: DeserializeOwned + Clone,
41{
42 fn content(&self) -> Result<Cow<'_, str>, Error>;
51
52 fn path(&self) -> Option<Cow<'_, Path>>;
56
57 fn properties(&self) -> Result<Option<Cow<'_, T>>, Error>;
64
65 fn note_name(&self) -> Option<String> {
67 self.path().as_ref().map(|path| {
68 path.file_stem()
69 .expect("Path is not file")
70 .to_string_lossy()
71 .to_string()
72 })
73 }
74
75 fn from_string<P: AsRef<Path>>(raw_text: &str, path: Option<P>) -> Result<Self, Error>;
85
86 fn from_file<P: AsRef<Path>>(path: P) -> Result<Self, Error> {
94 let path_buf = path.as_ref().to_path_buf();
95
96 #[cfg(feature = "logging")]
97 log::trace!("Parse obsidian file from file: {}", path_buf.display());
98
99 let data = std::fs::read(path)?;
100
101 let text = unsafe { String::from_utf8_unchecked(data) };
103
104 Self::from_string(&text, Some(path_buf))
105 }
106}
107
108pub trait ObFileDefault: ObFile<DefaultProperties> {
113 fn from_string_default<P: AsRef<Path>>(text: &str, path: Option<P>) -> Result<Self, Error>;
119
120 fn from_file_default<P: AsRef<Path>>(path: P) -> Result<Self, Error>;
125}
126
127pub trait ObFileFlush<T = DefaultProperties>: ObFile<T>
132where
133 T: DeserializeOwned + Serialize + Clone,
134{
135 fn flush_content(&self, open_option: &OpenOptions) -> Result<(), Error> {
142 if let Some(path) = self.path() {
143 let text = std::fs::read_to_string(&path)?;
144 let parsed = parse_obfile(&text)?;
145
146 let mut file = open_option.open(path)?;
147
148 match parsed {
149 ResultParse::WithProperties {
150 content: _,
151 properties,
152 } => file.write_all(
153 format!("---\n{}\n---\n{}", properties, self.content()?).as_bytes(),
154 )?,
155 ResultParse::WithoutProperties => file.write_all(self.content()?.as_bytes())?,
156 }
157 }
158
159 Ok(())
160 }
161
162 fn flush_properties(&self, open_option: &OpenOptions) -> Result<(), Error> {
168 if let Some(path) = self.path() {
169 let text = std::fs::read_to_string(&path)?;
170 let parsed = parse_obfile(&text)?;
171
172 let mut file = open_option.open(path)?;
173
174 match parsed {
175 ResultParse::WithProperties {
176 content,
177 properties: _,
178 } => match self.properties()? {
179 Some(properties) => file.write_all(
180 format!(
181 "---\n{}\n---\n{}",
182 serde_yml::to_string(&properties)?,
183 content
184 )
185 .as_bytes(),
186 )?,
187 None => file.write_all(self.content()?.as_bytes())?,
188 },
189 ResultParse::WithoutProperties => file.write_all(self.content()?.as_bytes())?,
190 }
191 }
192
193 Ok(())
194 }
195
196 fn flush(&self, open_option: &OpenOptions) -> Result<(), Error> {
202 if let Some(path) = self.path() {
203 let mut file = open_option.open(path)?;
204
205 match self.properties()? {
206 Some(properties) => file.write_all(
207 format!(
208 "---\n{}\n---\n{}",
209 serde_yml::to_string(&properties)?,
210 self.content()?
211 )
212 .as_bytes(),
213 )?,
214 None => file.write_all(self.content()?.as_bytes())?,
215 }
216 }
217
218 Ok(())
219 }
220}
221
222pub fn parse_links(text: &str) -> impl Iterator<Item = &str> {
239 text.match_indices("[[").filter_map(move |(start_pos, _)| {
240 let end_pos = text[start_pos + 2..].find("]]")?;
241 let inner = &text[start_pos + 2..start_pos + 2 + end_pos];
242
243 let note_name = inner
244 .split('#')
245 .next()?
246 .split('^')
247 .next()?
248 .split('|')
249 .next()?
250 .trim();
251
252 Some(note_name)
253 })
254}
255
256impl<T> ObFileDefault for T
257where
258 T: ObFile<DefaultProperties>,
259{
260 fn from_string_default<P: AsRef<Path>>(text: &str, path: Option<P>) -> Result<Self, Error> {
261 Self::from_string(text, path)
262 }
263
264 fn from_file_default<P: AsRef<Path>>(path: P) -> Result<Self, Error> {
265 Self::from_file(path)
266 }
267}
268
269#[derive(Debug, PartialEq)]
270enum ResultParse<'a> {
271 WithProperties {
272 content: &'a str,
273 properties: &'a str,
274 },
275 WithoutProperties,
276}
277
278fn parse_obfile(raw_text: &str) -> Result<ResultParse<'_>, Error> {
279 let have_start_properties = raw_text
280 .lines()
281 .next()
282 .is_some_and(|line| line.trim_end() == "---");
283
284 if have_start_properties {
285 let closed = raw_text["---".len()..]
286 .find("---")
287 .ok_or(Error::InvalidFormat)?;
288
289 return Ok(ResultParse::WithProperties {
290 content: raw_text[(closed + 2 * "...".len())..].trim(),
291 properties: raw_text["...".len()..(closed + "...".len())].trim(),
292 });
293 }
294
295 Ok(ResultParse::WithoutProperties)
296}
297
298#[cfg(test)]
299mod tests {
300 use super::{ResultParse, parse_obfile};
301 use crate::test_utils::init_test_logger;
302
303 #[test]
304 fn parse_obfile_without_properties() {
305 init_test_logger();
306 let test_data = "test_data";
307 let result = parse_obfile(test_data).unwrap();
308
309 assert_eq!(result, ResultParse::WithoutProperties);
310 }
311
312 #[test]
313 fn parse_obfile_with_properties() {
314 init_test_logger();
315 let test_data = "---\nproperties data\n---\ntest data";
316 let result = parse_obfile(test_data).unwrap();
317
318 assert_eq!(
319 result,
320 ResultParse::WithProperties {
321 content: "test data",
322 properties: "properties data"
323 }
324 );
325 }
326
327 #[test]
328 fn parse_obfile_without_properties_but_with_closed() {
329 init_test_logger();
330 let test_data1 = "test_data---";
331 let test_data2 = "test_data\n---\n";
332
333 let result1 = parse_obfile(test_data1).unwrap();
334 let result2 = parse_obfile(test_data2).unwrap();
335
336 assert_eq!(result1, ResultParse::WithoutProperties);
337 assert_eq!(result2, ResultParse::WithoutProperties);
338 }
339
340 #[test]
341 #[should_panic]
342 fn parse_obfile_with_properties_but_without_closed() {
343 init_test_logger();
344 let test_data = "---\nproperties data\ntest data";
345 let _ = parse_obfile(test_data).unwrap();
346 }
347
348 #[test]
349 fn parse_obfile_with_() {
350 init_test_logger();
351 let test_data = "---properties data";
352
353 let result = parse_obfile(test_data).unwrap();
354 assert_eq!(result, ResultParse::WithoutProperties);
355 }
356
357 #[test]
358 fn parse_obfile_without_properties_but_with_spaces() {
359 init_test_logger();
360 let test_data = " ---\ndata";
361
362 let result = parse_obfile(test_data).unwrap();
363 assert_eq!(result, ResultParse::WithoutProperties);
364 }
365
366 #[test]
367 fn parse_obfile_with_properties_but_check_trim_end() {
368 init_test_logger();
369 let test_data = "---\r\nproperties data\r\n---\r \ntest data";
370 let result = parse_obfile(test_data).unwrap();
371
372 assert_eq!(
373 result,
374 ResultParse::WithProperties {
375 content: "test data",
376 properties: "properties data"
377 }
378 );
379 }
380
381 #[test]
382 fn test_parse_links() {
383 init_test_logger();
384 let test_data =
385 "[[Note]] [[Note|Alias]] [[Note^block]] [[Note#Heading|Alias]] [[Note^block|Alias]]";
386
387 let ds: Vec<_> = super::parse_links(test_data).collect();
388
389 assert!(ds.iter().all(|x| *x == "Note"))
390 }
391}
392
393#[cfg(test)]
394pub(crate) mod impl_tests {
395 use super::*;
396 use crate::test_utils::init_test_logger;
397 use std::io::Write;
398 use tempfile::NamedTempFile;
399
400 pub(crate) static TEST_DATA: &str = "---\n\
401topic: life\n\
402created: 2025-03-16\n\
403---\n\
404Test data\n\
405---\n\
406Two test data";
407
408 pub(crate) fn from_string<T: ObFile>() -> Result<(), Error> {
409 init_test_logger();
410 let file = T::from_string(TEST_DATA, None::<&str>)?;
411 let properties = file.properties().unwrap().unwrap();
412
413 assert_eq!(properties["topic"], "life");
414 assert_eq!(properties["created"], "2025-03-16");
415 assert_eq!(file.content().unwrap(), "Test data\n---\nTwo test data");
416 Ok(())
417 }
418
419 pub(crate) fn from_string_note_name<T: ObFile>() -> Result<(), Error> {
420 init_test_logger();
421 let file1 = T::from_string(TEST_DATA, None::<&str>)?;
422 let file2 = T::from_string(TEST_DATA, Some("Super node.md"))?;
423
424 assert_eq!(file1.note_name(), None);
425 assert_eq!(file2.note_name(), Some("Super node".to_string()));
426 Ok(())
427 }
428
429 pub(crate) fn from_string_without_properties<T: ObFile>() -> Result<(), Error> {
430 init_test_logger();
431 let test_data = "TEST_DATA";
432 let file = T::from_string(test_data, None::<&str>)?;
433
434 assert_eq!(file.properties().unwrap(), None);
435 assert_eq!(file.content().unwrap(), test_data);
436 Ok(())
437 }
438
439 pub(crate) fn from_string_with_invalid_yaml<T: ObFile>() -> Result<(), Error> {
440 init_test_logger();
441 let broken_data = "---\n\
442 asdfv:--fs\n\
443 sfsf\n\
444 ---\n\
445 TestData";
446
447 assert!(matches!(
448 T::from_string(broken_data, None::<&str>),
449 Err(Error::Yaml(_))
450 ));
451 Ok(())
452 }
453
454 pub(crate) fn from_string_invalid_format<T: ObFile>() -> Result<(), Error> {
455 init_test_logger();
456 let broken_data = "---\n";
457
458 assert!(matches!(
459 T::from_string(broken_data, None::<&str>),
460 Err(Error::InvalidFormat)
461 ));
462 Ok(())
463 }
464
465 pub(crate) fn from_string_with_unicode<T: ObFile>() -> Result<(), Error> {
466 init_test_logger();
467 let data = "---\ndata: 💩\n---\nSuper data 💩💩💩";
468 let file = T::from_string(data, None::<&str>)?;
469 let properties = file.properties().unwrap().unwrap();
470
471 assert_eq!(properties["data"], "💩");
472 assert_eq!(file.content().unwrap(), "Super data 💩💩💩");
473 Ok(())
474 }
475
476 pub(crate) fn from_string_space_with_properties<T: ObFile>() -> Result<(), Error> {
477 init_test_logger();
478 let data = " ---\ntest: test-data\n---\n";
479 let file = T::from_string(data, None::<&str>)?;
480 let properties = file.properties().unwrap();
481
482 assert_eq!(file.content().unwrap(), data);
483 assert_eq!(properties, None);
484 Ok(())
485 }
486
487 pub(crate) fn from_file<T: ObFile>() -> Result<(), Error> {
488 init_test_logger();
489 let mut temp_file = NamedTempFile::new().unwrap();
490 temp_file.write_all(b"TEST_DATA").unwrap();
491
492 let file = T::from_file(temp_file.path()).unwrap();
493 assert_eq!(file.content().unwrap(), "TEST_DATA");
494 assert_eq!(file.path().unwrap(), temp_file.path());
495 assert_eq!(file.properties().unwrap(), None);
496 Ok(())
497 }
498
499 pub(crate) fn from_file_note_name<T: ObFile>() -> Result<(), Error> {
500 init_test_logger();
501 let mut temp_file = NamedTempFile::new().unwrap();
502 temp_file.write_all(b"TEST_DATA").unwrap();
503
504 let name_temp_file = temp_file
505 .path()
506 .file_stem()
507 .unwrap()
508 .to_string_lossy()
509 .to_string();
510
511 let file = T::from_file(temp_file.path()).unwrap();
512
513 assert_eq!(file.note_name(), Some(name_temp_file));
514 Ok(())
515 }
516
517 pub(crate) fn from_file_without_properties<T: ObFile>() -> Result<(), Error> {
518 init_test_logger();
519 let test_data = "TEST_DATA";
520 let mut test_file = NamedTempFile::new().unwrap();
521 test_file.write_all(test_data.as_bytes()).unwrap();
522
523 let file = T::from_file(test_file.path())?;
524
525 assert_eq!(file.properties().unwrap(), None);
526 assert_eq!(file.content().unwrap(), test_data);
527 Ok(())
528 }
529
530 pub(crate) fn from_file_with_invalid_yaml<T: ObFile>() -> Result<(), Error> {
531 init_test_logger();
532 let broken_data = "---\n\
533 asdfv:--fs\n\
534 sfsf\n\
535 ---\n\
536 TestData";
537
538 let mut test_file = NamedTempFile::new().unwrap();
539 test_file.write_all(broken_data.as_bytes()).unwrap();
540
541 assert!(matches!(
542 T::from_file(test_file.path()),
543 Err(Error::Yaml(_))
544 ));
545 Ok(())
546 }
547
548 pub(crate) fn from_file_invalid_format<T: ObFile>() -> Result<(), Error> {
549 init_test_logger();
550 let broken_data = "---\n";
551 let mut test_file = NamedTempFile::new().unwrap();
552 test_file.write_all(broken_data.as_bytes()).unwrap();
553
554 assert!(matches!(
555 T::from_file(test_file.path()),
556 Err(Error::InvalidFormat)
557 ));
558 Ok(())
559 }
560
561 pub(crate) fn from_file_with_unicode<T: ObFile>() -> Result<(), Error> {
562 init_test_logger();
563 let data = "---\ndata: 💩\n---\nSuper data 💩💩💩";
564 let mut test_file = NamedTempFile::new().unwrap();
565 test_file.write_all(data.as_bytes()).unwrap();
566
567 let file = T::from_file(test_file.path())?;
568 let properties = file.properties().unwrap().unwrap();
569
570 assert_eq!(properties["data"], "💩");
571 assert_eq!(file.content().unwrap(), "Super data 💩💩💩");
572 Ok(())
573 }
574
575 pub(crate) fn from_file_space_with_properties<T: ObFile>() -> Result<(), Error> {
576 init_test_logger();
577 let data = " ---\ntest: test-data\n---\n";
578 let mut test_file = NamedTempFile::new().unwrap();
579 test_file.write_all(data.as_bytes()).unwrap();
580
581 let file = T::from_string(data, None::<&str>)?;
582
583 assert_eq!(file.content().unwrap(), data);
584 assert_eq!(file.properties().unwrap(), None);
585 Ok(())
586 }
587
588 pub(crate) fn flush_properties<T: ObFileFlush>() -> Result<(), Error> {
589 let mut test_file = NamedTempFile::new().unwrap();
590 test_file.write_all(TEST_DATA.as_bytes()).unwrap();
591
592 let file = T::from_file_default(test_file.path())?;
593 let open_options = OpenOptions::new().write(true).create(false).clone();
594 file.flush_properties(&open_options)?;
595 drop(file);
596
597 let file = T::from_file_default(test_file.path())?;
598 let properties = file.properties()?.unwrap();
599 assert_eq!(properties["topic"], "life");
600 assert_eq!(properties["created"], "2025-03-16");
601 assert_eq!(file.content().unwrap(), "Test data\n---\nTwo test data");
602
603 Ok(())
604 }
605
606 pub(crate) fn flush_content<T: ObFileFlush>() -> Result<(), Error> {
607 let mut test_file = NamedTempFile::new().unwrap();
608 test_file.write_all(TEST_DATA.as_bytes()).unwrap();
609
610 let file = T::from_file_default(test_file.path())?;
611 let open_options = OpenOptions::new().write(true).create(false).clone();
612 file.flush_content(&open_options)?;
613 drop(file);
614
615 let file = T::from_file_default(test_file.path())?;
616 let properties = file.properties()?.unwrap();
617 assert_eq!(properties["topic"], "life");
618 assert_eq!(properties["created"], "2025-03-16");
619 assert_eq!(file.content().unwrap(), "Test data\n---\nTwo test data");
620
621 Ok(())
622 }
623
624 pub(crate) fn flush<T: ObFileFlush>() -> Result<(), Error> {
625 let mut test_file = NamedTempFile::new().unwrap();
626 test_file.write_all(TEST_DATA.as_bytes()).unwrap();
627
628 let file = T::from_file_default(test_file.path())?;
629 let open_options = OpenOptions::new().write(true).create(false).clone();
630 file.flush(&open_options)?;
631 drop(file);
632
633 let file = T::from_file_default(test_file.path())?;
634 let properties = file.properties()?.unwrap();
635 assert_eq!(properties["topic"], "life");
636 assert_eq!(properties["created"], "2025-03-16");
637 assert_eq!(file.content().unwrap(), "Test data\n---\nTwo test data");
638
639 Ok(())
640 }
641
642 macro_rules! impl_test_for_obfile {
643 ($name_test:ident, $fn_test:ident, $impl_obfile:path) => {
644 #[test]
645 fn $name_test() {
646 $fn_test::<$impl_obfile>().unwrap();
647 }
648 };
649 }
650
651 pub(crate) use impl_test_for_obfile;
652
653 macro_rules! impl_all_tests_from_string {
654 ($impl_obfile:path) => {
655 #[allow(unused_imports)]
656 use $crate::obfile::impl_tests::*;
657
658 impl_test_for_obfile!(impl_from_string, from_string, $impl_obfile);
659
660 impl_test_for_obfile!(
661 impl_from_string_note_name,
662 from_string_note_name,
663 $impl_obfile
664 );
665 impl_test_for_obfile!(
666 impl_from_string_without_properties,
667 from_string_without_properties,
668 $impl_obfile
669 );
670 impl_test_for_obfile!(
671 impl_from_string_with_invalid_yaml,
672 from_string_with_invalid_yaml,
673 $impl_obfile
674 );
675 impl_test_for_obfile!(
676 impl_from_string_invalid_format,
677 from_string_invalid_format,
678 $impl_obfile
679 );
680 impl_test_for_obfile!(
681 impl_from_string_with_unicode,
682 from_string_with_unicode,
683 $impl_obfile
684 );
685 impl_test_for_obfile!(
686 impl_from_string_space_with_properties,
687 from_string_space_with_properties,
688 $impl_obfile
689 );
690 };
691 }
692
693 macro_rules! impl_all_tests_from_file {
694 ($impl_obfile:path) => {
695 #[allow(unused_imports)]
696 use $crate::obfile::impl_tests::*;
697
698 impl_test_for_obfile!(impl_from_file, from_file, $impl_obfile);
699 impl_test_for_obfile!(impl_from_file_note_name, from_file_note_name, $impl_obfile);
700
701 impl_test_for_obfile!(
702 impl_from_file_without_properties,
703 from_file_without_properties,
704 $impl_obfile
705 );
706 impl_test_for_obfile!(
707 impl_from_file_with_invalid_yaml,
708 from_file_with_invalid_yaml,
709 $impl_obfile
710 );
711 impl_test_for_obfile!(
712 impl_from_file_invalid_format,
713 from_file_invalid_format,
714 $impl_obfile
715 );
716 impl_test_for_obfile!(
717 impl_from_file_with_unicode,
718 from_file_with_unicode,
719 $impl_obfile
720 );
721 impl_test_for_obfile!(
722 impl_from_file_space_with_properties,
723 from_file_space_with_properties,
724 $impl_obfile
725 );
726 };
727 }
728
729 macro_rules! impl_all_tests_flush {
730 ($impl_obfile:path) => {
731 #[allow(unused_imports)]
732 use $crate::obfile::impl_tests::*;
733
734 impl_test_for_obfile!(impl_flush, flush, $impl_obfile);
735 impl_test_for_obfile!(impl_flush_content, flush_content, $impl_obfile);
736 impl_test_for_obfile!(impl_flush_properties, flush_properties, $impl_obfile);
737 };
738 }
739
740 pub(crate) use impl_all_tests_flush;
741 pub(crate) use impl_all_tests_from_file;
742 pub(crate) use impl_all_tests_from_string;
743}