1pub mod obfile_in_memory;
4pub mod obfile_on_disk;
5
6use crate::error::Error;
7use serde::de::DeserializeOwned;
8use std::{borrow::Cow, collections::HashMap, path::Path};
9
10pub(crate) type DefaultProperties = HashMap<String, serde_yml::Value>;
11
12pub trait ObFile<T = DefaultProperties>: Sized
36where
37 T: DeserializeOwned + Clone,
38{
39 fn content(&self) -> Result<Cow<'_, str>, Error>;
48
49 fn path(&self) -> Option<Cow<'_, Path>>;
53
54 fn properties(&self) -> Result<Option<Cow<'_, T>>, Error>;
61
62 fn note_name(&self) -> Option<String> {
64 self.path().as_ref().map(|path| {
65 path.file_stem()
66 .expect("Path is not file")
67 .to_string_lossy()
68 .to_string()
69 })
70 }
71
72 fn from_string<P: AsRef<Path>>(raw_text: &str, path: Option<P>) -> Result<Self, Error>;
82
83 fn from_file<P: AsRef<Path>>(path: P) -> Result<Self, Error> {
91 let path_buf = path.as_ref().to_path_buf();
92
93 #[cfg(feature = "logging")]
94 log::trace!("Parse obsidian file from file: {}", path_buf.display());
95
96 let data = std::fs::read(path)?;
97 let text = unsafe { String::from_utf8_unchecked(data) };
98
99 Self::from_string(&text, Some(path_buf))
100 }
101}
102
103pub trait ObFileDefault: ObFile<DefaultProperties> {
108 fn from_string_default<P: AsRef<Path>>(text: &str, path: Option<P>) -> Result<Self, Error>;
114
115 fn from_file_default<P: AsRef<Path>>(path: P) -> Result<Self, Error>;
120}
121
122pub fn parse_links(text: &str) -> impl Iterator<Item = &str> {
139 text.match_indices("[[").filter_map(move |(start_pos, _)| {
140 let end_pos = text[start_pos + 2..].find("]]")?;
141 let inner = &text[start_pos + 2..start_pos + 2 + end_pos];
142
143 let note_name = inner
144 .split('#')
145 .next()?
146 .split('^')
147 .next()?
148 .split('|')
149 .next()?
150 .trim();
151
152 Some(note_name)
153 })
154}
155
156impl<T> ObFileDefault for T
157where
158 T: ObFile<DefaultProperties>,
159{
160 fn from_string_default<P: AsRef<Path>>(text: &str, path: Option<P>) -> Result<Self, Error> {
161 Self::from_string(text, path)
162 }
163
164 fn from_file_default<P: AsRef<Path>>(path: P) -> Result<Self, Error> {
165 Self::from_file(path)
166 }
167}
168
169#[derive(Debug, PartialEq)]
170enum ResultParse<'a> {
171 WithProperties {
172 content: &'a str,
173 properties: &'a str,
174 },
175 WithoutProperties,
176}
177
178fn parse_obfile(raw_text: &str) -> Result<ResultParse<'_>, Error> {
179 let have_start_properties = raw_text
180 .lines()
181 .next()
182 .is_some_and(|line| line.trim_end() == "---");
183
184 if have_start_properties {
185 let closed = raw_text["---".len()..]
186 .find("---")
187 .ok_or(Error::InvalidFormat)?;
188
189 return Ok(ResultParse::WithProperties {
190 content: raw_text[(closed + 2 * "...".len())..].trim(),
191 properties: raw_text["...".len()..(closed + "...".len())].trim(),
192 });
193 }
194
195 Ok(ResultParse::WithoutProperties)
196}
197
198#[cfg(test)]
199mod tests {
200 use super::{ResultParse, parse_obfile};
201 use crate::test_utils::init_test_logger;
202
203 #[test]
204 fn parse_obfile_without_properties() {
205 init_test_logger();
206 let test_data = "test_data";
207 let result = parse_obfile(test_data).unwrap();
208
209 assert_eq!(result, ResultParse::WithoutProperties);
210 }
211
212 #[test]
213 fn parse_obfile_with_properties() {
214 init_test_logger();
215 let test_data = "---\nproperties data\n---\ntest data";
216 let result = parse_obfile(test_data).unwrap();
217
218 assert_eq!(
219 result,
220 ResultParse::WithProperties {
221 content: "test data",
222 properties: "properties data"
223 }
224 );
225 }
226
227 #[test]
228 fn parse_obfile_without_properties_but_with_closed() {
229 init_test_logger();
230 let test_data1 = "test_data---";
231 let test_data2 = "test_data\n---\n";
232
233 let result1 = parse_obfile(test_data1).unwrap();
234 let result2 = parse_obfile(test_data2).unwrap();
235
236 assert_eq!(result1, ResultParse::WithoutProperties);
237 assert_eq!(result2, ResultParse::WithoutProperties);
238 }
239
240 #[test]
241 #[should_panic]
242 fn parse_obfile_with_properties_but_without_closed() {
243 init_test_logger();
244 let test_data = "---\nproperties data\ntest data";
245 let _ = parse_obfile(test_data).unwrap();
246 }
247
248 #[test]
249 fn parse_obfile_with_() {
250 init_test_logger();
251 let test_data = "---properties data";
252
253 let result = parse_obfile(test_data).unwrap();
254 assert_eq!(result, ResultParse::WithoutProperties);
255 }
256
257 #[test]
258 fn parse_obfile_without_properties_but_with_spaces() {
259 init_test_logger();
260 let test_data = " ---\ndata";
261
262 let result = parse_obfile(test_data).unwrap();
263 assert_eq!(result, ResultParse::WithoutProperties);
264 }
265
266 #[test]
267 fn parse_obfile_with_properties_but_check_trim_end() {
268 init_test_logger();
269 let test_data = "---\r\nproperties data\r\n---\r \ntest data";
270 let result = parse_obfile(test_data).unwrap();
271
272 assert_eq!(
273 result,
274 ResultParse::WithProperties {
275 content: "test data",
276 properties: "properties data"
277 }
278 );
279 }
280
281 #[test]
282 fn test_parse_links() {
283 init_test_logger();
284 let test_data =
285 "[[Note]] [[Note|Alias]] [[Note^block]] [[Note#Heading|Alias]] [[Note^block|Alias]]";
286
287 let ds: Vec<_> = super::parse_links(test_data).collect();
288
289 assert!(ds.iter().all(|x| *x == "Note"))
290 }
291}
292
293#[cfg(test)]
294pub(crate) mod impl_tests {
295 use super::*;
296 use crate::test_utils::init_test_logger;
297 use std::io::Write;
298 use tempfile::NamedTempFile;
299
300 pub(crate) static TEST_DATA: &str = "---\n\
301topic: life\n\
302created: 2025-03-16\n\
303---\n\
304Test data\n\
305---\n\
306Two test data";
307
308 pub(crate) fn from_string<T: ObFile>() -> Result<(), Error> {
309 init_test_logger();
310 let file = T::from_string(TEST_DATA, None::<&str>)?;
311 let properties = file.properties().unwrap().unwrap();
312
313 assert_eq!(properties["topic"], "life");
314 assert_eq!(properties["created"], "2025-03-16");
315 assert_eq!(file.content().unwrap(), "Test data\n---\nTwo test data");
316 Ok(())
317 }
318
319 pub(crate) fn from_string_note_name<T: ObFile>() -> Result<(), Error> {
320 init_test_logger();
321 let file1 = T::from_string(TEST_DATA, None::<&str>)?;
322 let file2 = T::from_string(TEST_DATA, Some("Super node.md"))?;
323
324 assert_eq!(file1.note_name(), None);
325 assert_eq!(file2.note_name(), Some("Super node".to_string()));
326 Ok(())
327 }
328
329 pub(crate) fn from_string_without_properties<T: ObFile>() -> Result<(), Error> {
330 init_test_logger();
331 let test_data = "TEST_DATA";
332 let file = T::from_string(test_data, None::<&str>)?;
333
334 assert_eq!(file.properties().unwrap(), None);
335 assert_eq!(file.content().unwrap(), test_data);
336 Ok(())
337 }
338
339 pub(crate) fn from_string_with_invalid_yaml<T: ObFile>() -> Result<(), Error> {
340 init_test_logger();
341 let broken_data = "---\n\
342 asdfv:--fs\n\
343 sfsf\n\
344 ---\n\
345 TestData";
346
347 assert!(matches!(
348 T::from_string(broken_data, None::<&str>),
349 Err(Error::Yaml(_))
350 ));
351 Ok(())
352 }
353
354 pub(crate) fn from_string_invalid_format<T: ObFile>() -> Result<(), Error> {
355 init_test_logger();
356 let broken_data = "---\n";
357
358 assert!(matches!(
359 T::from_string(broken_data, None::<&str>),
360 Err(Error::InvalidFormat)
361 ));
362 Ok(())
363 }
364
365 pub(crate) fn from_string_with_unicode<T: ObFile>() -> Result<(), Error> {
366 init_test_logger();
367 let data = "---\ndata: 💩\n---\nSuper data 💩💩💩";
368 let file = T::from_string(data, None::<&str>)?;
369 let properties = file.properties().unwrap().unwrap();
370
371 assert_eq!(properties["data"], "💩");
372 assert_eq!(file.content().unwrap(), "Super data 💩💩💩");
373 Ok(())
374 }
375
376 pub(crate) fn from_string_space_with_properties<T: ObFile>() -> Result<(), Error> {
377 init_test_logger();
378 let data = " ---\ntest: test-data\n---\n";
379 let file = T::from_string(data, None::<&str>)?;
380 let properties = file.properties().unwrap();
381
382 assert_eq!(file.content().unwrap(), data);
383 assert_eq!(properties, None);
384 Ok(())
385 }
386
387 pub(crate) fn from_file<T: ObFile>() -> Result<(), Error> {
388 init_test_logger();
389 let mut temp_file = NamedTempFile::new().unwrap();
390 temp_file.write_all(b"TEST_DATA").unwrap();
391
392 let file = T::from_file(temp_file.path()).unwrap();
393 assert_eq!(file.content().unwrap(), "TEST_DATA");
394 assert_eq!(file.path().unwrap(), temp_file.path());
395 assert_eq!(file.properties().unwrap(), None);
396 Ok(())
397 }
398
399 pub(crate) fn from_file_note_name<T: ObFile>() -> Result<(), Error> {
400 init_test_logger();
401 let mut temp_file = NamedTempFile::new().unwrap();
402 temp_file.write_all(b"TEST_DATA").unwrap();
403
404 let name_temp_file = temp_file
405 .path()
406 .file_stem()
407 .unwrap()
408 .to_string_lossy()
409 .to_string();
410
411 let file = T::from_file(temp_file.path()).unwrap();
412
413 assert_eq!(file.note_name(), Some(name_temp_file));
414 Ok(())
415 }
416
417 pub(crate) fn from_file_without_properties<T: ObFile>() -> Result<(), Error> {
418 init_test_logger();
419 let test_data = "TEST_DATA";
420 let mut test_file = NamedTempFile::new().unwrap();
421 test_file.write_all(test_data.as_bytes()).unwrap();
422
423 let file = T::from_file(test_file.path())?;
424
425 assert_eq!(file.properties().unwrap(), None);
426 assert_eq!(file.content().unwrap(), test_data);
427 Ok(())
428 }
429
430 pub(crate) fn from_file_with_invalid_yaml<T: ObFile>() -> Result<(), Error> {
431 init_test_logger();
432 let broken_data = "---\n\
433 asdfv:--fs\n\
434 sfsf\n\
435 ---\n\
436 TestData";
437
438 let mut test_file = NamedTempFile::new().unwrap();
439 test_file.write_all(broken_data.as_bytes()).unwrap();
440
441 assert!(matches!(
442 T::from_file(test_file.path()),
443 Err(Error::Yaml(_))
444 ));
445 Ok(())
446 }
447
448 pub(crate) fn from_file_invalid_format<T: ObFile>() -> Result<(), Error> {
449 init_test_logger();
450 let broken_data = "---\n";
451 let mut test_file = NamedTempFile::new().unwrap();
452 test_file.write_all(broken_data.as_bytes()).unwrap();
453
454 assert!(matches!(
455 T::from_file(test_file.path()),
456 Err(Error::InvalidFormat)
457 ));
458 Ok(())
459 }
460
461 pub(crate) fn from_file_with_unicode<T: ObFile>() -> Result<(), Error> {
462 init_test_logger();
463 let data = "---\ndata: 💩\n---\nSuper data 💩💩💩";
464 let mut test_file = NamedTempFile::new().unwrap();
465 test_file.write_all(data.as_bytes()).unwrap();
466
467 let file = T::from_file(test_file.path())?;
468 let properties = file.properties().unwrap().unwrap();
469
470 assert_eq!(properties["data"], "💩");
471 assert_eq!(file.content().unwrap(), "Super data 💩💩💩");
472 Ok(())
473 }
474
475 pub(crate) fn from_file_space_with_properties<T: ObFile>() -> Result<(), Error> {
476 init_test_logger();
477 let data = " ---\ntest: test-data\n---\n";
478 let mut test_file = NamedTempFile::new().unwrap();
479 test_file.write_all(data.as_bytes()).unwrap();
480
481 let file = T::from_string(data, None::<&str>)?;
482
483 assert_eq!(file.content().unwrap(), data);
484 assert_eq!(file.properties().unwrap(), None);
485 Ok(())
486 }
487
488 macro_rules! impl_test_for_obfile {
489 ($name_test:ident, $fn_test:ident, $impl_obfile:path) => {
490 #[test]
491 fn $name_test() {
492 $fn_test::<$impl_obfile>().unwrap();
493 }
494 };
495 }
496
497 pub(crate) use impl_test_for_obfile;
498
499 macro_rules! impl_all_tests_from_string {
500 ($impl_obfile:path) => {
501 #[allow(unused_imports)]
502 use crate::obfile::impl_tests::*;
503
504 impl_test_for_obfile!(impl_from_string, from_string, $impl_obfile);
505
506 impl_test_for_obfile!(
507 impl_from_string_note_name,
508 from_string_note_name,
509 $impl_obfile
510 );
511 impl_test_for_obfile!(
512 impl_from_string_without_properties,
513 from_string_without_properties,
514 $impl_obfile
515 );
516 impl_test_for_obfile!(
517 impl_from_string_with_invalid_yaml,
518 from_string_with_invalid_yaml,
519 $impl_obfile
520 );
521 impl_test_for_obfile!(
522 impl_from_string_invalid_format,
523 from_string_invalid_format,
524 $impl_obfile
525 );
526 impl_test_for_obfile!(
527 impl_from_string_with_unicode,
528 from_string_with_unicode,
529 $impl_obfile
530 );
531 impl_test_for_obfile!(
532 impl_from_string_space_with_properties,
533 from_string_space_with_properties,
534 $impl_obfile
535 );
536 };
537 }
538
539 macro_rules! impl_all_tests_from_file {
540 ($impl_obfile:path) => {
541 #[allow(unused_imports)]
542 use crate::obfile::impl_tests::*;
543
544 impl_test_for_obfile!(impl_from_file, from_file, $impl_obfile);
545 impl_test_for_obfile!(impl_from_file_note_name, from_file_note_name, $impl_obfile);
546
547 impl_test_for_obfile!(
548 impl_from_file_without_properties,
549 from_file_without_properties,
550 $impl_obfile
551 );
552 impl_test_for_obfile!(
553 impl_from_file_with_invalid_yaml,
554 from_file_with_invalid_yaml,
555 $impl_obfile
556 );
557 impl_test_for_obfile!(
558 impl_from_file_invalid_format,
559 from_file_invalid_format,
560 $impl_obfile
561 );
562 impl_test_for_obfile!(
563 impl_from_file_with_unicode,
564 from_file_with_unicode,
565 $impl_obfile
566 );
567 impl_test_for_obfile!(
568 impl_from_file_space_with_properties,
569 from_file_space_with_properties,
570 $impl_obfile
571 );
572 };
573 }
574
575 pub(crate) use impl_all_tests_from_file;
576 pub(crate) use impl_all_tests_from_string;
577}