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