libzettels 0.4.1

A library intended as a backend for applications which implement Niklas Luhmann's system of a 'Zettelkasten'.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
//Copyright (c) 2020-2022 Stefan Thesing
//
//This file is part of libzettels.
//
//libzettels is free software: you can redistribute it and/or modify
//it under the terms of the GNU General Public License as published by
//the Free Software Foundation, either version 3 of the License, or
//(at your option) any later version.
//
//libzettels is distributed in the hope that it will be useful,
//but WITHOUT ANY WARRANTY; without even the implied warranty of
//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//GNU General Public License for more details.
//
//You should have received a copy of the GNU General Public License
//along with Zettels. If not, see http://www.gnu.org/licenses/.

//! Module for building and updating the index.

// --------------------------------------------------------------------------
use gitignore;

// External imports
use std::path::{Path, PathBuf};
use std::fs;
use std::io;

// Internal imports
use backstage::error::Error;
use backstage::index::Index;
use backstage::zettel::Zettel;

// Submodules
mod grep;
mod ripgrep;
mod native;

// I originally used this regex pattern:
//const PATTERN: &str = r"\[.*?\]\(.*?\)";
// I had to amend that to make it less fitting, ironically:
//const PATTERN: &str = r"\[.*?\]\(.*\)";
// That was necessary in order to be able to handle parentheses in links,
// e.g. linking to a file called `f(o)o.md`.
//
// I further amended it so that image links like `![An Image](image.jpg)`
// do no longer match: 
const PATTERN: &str = r"[^!]\[.?*\]\(.*\)|^\[.*?\]\(.*\)";
// These are in fact two patterns separated by an `|` (or).
// 1. The first pattern `[^!]\[.*?\]\(.*?\)` matches markdown links 
//    preceded by any character but `!`.
// 2. The second pattern `^\[.*?\]\(.*?\)` matches markdown links 
//    preceded by nothing.

/// The [`Index`](struct.Index.html) is created not only by reading each
/// zettel's YAML-metadata, but by scanning the document body of each zettel
/// and parsing the markdown for links to other zettel files.
/// The enum `IndexingMethod` defines the different methods that can be used
/// to do this latter task.
/// 
/// Application developers should think about whether and how to offer their
/// users choice in this matter.
/// 1. [`IndexingMethod::Grep`](enum.IndexingMethod.html#variant.Grep)
///    - relies on the UNIX command line tool `grep`.
///    - preinstalled on many platforms
///    - very fast
/// 2. [`IndexingMethod::RipGrep`](enum.IndexingMethod.html#variant.RipGrep)
///    - relies on the external tools `ripgrep`.
///    - probably needs to be installed by the user
///    - even faster than `grep`.
/// 3. [`IndexingMethod::Native`](enum.IndexingMethod.html#variant.Native)
///    - works out of the box without relying on external tools
///    - probably much slower than `grep` or `ripgrep` (at least for a large
///      number of files.)
#[derive(Debug, PartialEq, Serialize, Deserialize)]
pub enum IndexingMethod {
    /// Use the system's variant of [`grep`](https://en.wikipedia.org/wiki/Grep)
    /// to inspect and parse the markdown. If you want to use this, `grep` must
    /// be installed on your system and in your shell's PATH.
    /// In short, grep's output of
    /// ```shell
    /// grep -E -o '[[:space:]]\[.*?\]\(.*?\)|^\[.*?\]\(.*?\)' -r "/home/user/root/of/your/Zettelkasten/"
    /// ```
    /// must look like this:
    /// ```shell
    /// /home/user/root/of/your/Zettelkasten/somezettel.md:[lorem ipsum](otherzettel.md)
    /// /home/user/root/of/your/Zettelkasten/onemorezettel.md:[sit amet](yetanotherzettel.md)
    /// ```
    Grep,
    /// Use [`ripgrep`](https://github.com/BurntSushi/ripgrep) to inspect and 
    /// parse the markdown. If you want to use this `ripgrep` must be installed
    /// on your system and in your shell's PATH. Furthermore, the output of
    /// ```shell
    /// rg --no-heading -N -o '[[:space:]]\[.*?\]\(.*?\)|^\[.*?\]\(.*?\)' "/home/user/root/of/your/Zettelkasten/"
    /// ```
    /// must be formatted like this:
    /// ```shell
    /// /home/user/root/of/your/Zettelkasten/somezettel.md:[lorem ipsum](otherzettel.md)
    /// /home/user/root/of/your/Zettelkasten/onemorezettel.md:[sit amet](yetanotherzettel.md)
    /// ```
    RipGrep,
    /// Use functions native to libzettels to parse the markdown. It works 
    /// without depending on an external tool, but it's in no way optimized for
    /// speed.
    ///
    /// It is intended for users who have neither `grep` nor `ripgrep` 
    /// available, i.e. probably only Windows-users who want to install
    /// neither `ripgrep` nor some flavour of `grep`.
    Native,
}

/// Creates a new index from scratch. All files are inspected, unless filtered
/// by the specified `ignorefile`. It further takes the path to the root 
/// directory and an indexing method as arguments.
/// **Note:**
/// - If one of the files is non-text (e.g. an image) it is not added
///   to the index. An `info` about this is issued to logging.
/// - If one of the files does not contain YAML-metadata, it is added to the 
///   index with the `title` "untitled" and empty `followups` and `keywords`. 
///   The markdown links (if any) are still parsed and `links` is populated 
///   as usual.
/// # Errors
/// - [`Error::BadLink`](enum.Error.html#variant.BadLink) if one of the files 
///   links to a target that doesn't exist (both via `followups` and via
///   markdown link).
/// - [`Error::IgnoreFile`](enum.Error.html#variant.IgnoreFile) for problems
///   applying an existing ignore file. (A missing ignore file is no problem).
/// - [`Error::Io`](enum.Error.html#variant.Io) wrapping several kinds of 
///   `std::io:Error`, e.g. problems executing grep or ripgrep, problems with 
///   the files etc.
/// - [`Error::NormalizePath`](enum.Error.html#variant.NormalizePath) if one of 
///   the files or a link in `followups` or `links`can not be expressed 
///   relative to the root directory
/// - [`Error::Yaml`](enum.Error.html#variant.Yaml) when deserializing a
///   Zettel from YAML failed for one of the files (but only if it contains
///   any YAML, at all).
pub fn create_index<P: AsRef<Path>>(indexingmethod: &IndexingMethod,
                                    rootdir: P,
                                    ignorefile: P) 
        -> Result<Index, Error> {
    debug!("Creating new index.");
    let rootdir = rootdir.as_ref();
    let ignorefile = ignorefile.as_ref();
    let mut index = Index::empty();
    let mut files = get_list_of_files(rootdir, ignorefile)?;        //error::Error
    trace!("Files: {:#?}", files);
    
    // We prepare a list of successfully added files, in order to 
    // parse their markdown links, later.
    let mut zettels_to_parse = vec![];
    
    debug!("Start with the yaml-metadata of each file.");
    for file in files.drain(..) {
        // add the new entry, skip if non-text file
        // add successfully added files to `zettels_to_parse`.
        // If it wasn't successful, we receive `None` so:
        let added_key = add_textfiles_only(&mut index, rootdir, &file)?;  
                                                                //error::Error
        if let Some(key) = added_key {
            zettels_to_parse.push(key);
        }
    }
    
    // Next: the Markdown
    let _ = parse_and_apply_markdown_links(indexingmethod, 
                                           &mut index,
                                           rootdir, 
                                           zettels_to_parse, 
                                           )?;         //error::Error

    index.update_timestamp();
    Ok(index)
}

/// Updates an existing index. Only files modified after the index' timestamp
/// are inspected. It further takes the paths to the root directory, to an 
/// ignorefile and an indexing method as arguments.
/// **Note:**
/// - If one of the files is non-text (e.g. an image) it is not added
///  to the index. An `info` about this is issued to logging.
/// - If one of the files does not contain YAML-metadata, it is added to the 
///   index with the `title` "untitled" and empty `followups` and `keywords`. 
///   The markdown links (if any) are still parsed and `links` is populated 
///   as usual.
/// # Errors
/// - [`Error::BadLink`](enum.Error.html#variant.BadLink) if one of the files 
///   links to a target that doesn't exist (both via `followups` and via
///   markdown link).
/// - [`Error::IgnoreFile`](enum.Error.html#variant.IgnoreFile) for problems
///   applying an existing ignore file. (A missing ignore file is no problem).
/// - [`Error::Io`](enum.Error.html#variant.Io) wrapping several kinds of 
///   `std::io:Error`, e.g. problems executing grep or ripgrep, problems with 
///   the files etc.
/// - [`Error::NormalizePath`](enum.Error.html#variant.NormalizePath) if one of 
///   the files or a link in `followups` or `links`can not be expressed 
///   relative to the root directory
/// - [`Error::Yaml`](enum.Error.html#variant.Yaml) when deserializing a
///   Zettel from YAML failed for one of the files (but only if it contains
///   any YAML, at all).
pub fn update_index<P: AsRef<Path>>(index: &mut Index,
                                    indexingmethod: &IndexingMethod,
                                    rootdir: P, 
                                    ignorefile: P) -> Result<(), Error> {
    debug!("Updating index.");
    let rootdir = rootdir.as_ref();
    let ignorefile = ignorefile.as_ref();
    let (mut zettels_to_update, mut zettels_to_remove) 
                = get_changed_zettels(index, rootdir, ignorefile)?; 
                                                                 //error::Error
    trace!("Update: {:?}", zettels_to_update);
    trace!("Remove: {:?}", zettels_to_remove);
    
    debug!("Removing files that no longer exist.");
    for k in zettels_to_remove.drain(..) {
        index.files.remove(&k);
    }
    
    // We prepare a list of successfully added files, in order to 
    // parse their markdown links, later.
    let mut zettels_to_parse = vec![];
    
    debug!("Start with the yaml-metadata of each updated file.");
    // Start with the yaml-metadata of each file
    for k in zettels_to_update.drain(..) { 
        // remove entry if already present
        if index.files.contains_key(&k) {
            trace!("Removing old entry for {:?}.", &k);
            index.files.remove(&k);
        }
        // add the updated or new entry to the index, skip if non-text file
        // add successfully added files to `zettels_to_parse`.
        // If it wasn't successful, we receive `None` so:
        let added_key = add_textfiles_only(index, rootdir, &k)?;
        if let Some(key) = added_key {
            zettels_to_parse.push(key);
        }
    }
    
    // Next: the Markdown
    let _ = parse_and_apply_markdown_links(indexingmethod, 
                                           index,
                                           rootdir, 
                                           zettels_to_parse, 
                                           )?;              //error::Error
    
    index.update_timestamp();
    Ok(())
}

/// Calls Zettel::from_file and adds the result to the index, if deserializing
/// went well.
/// Furthermore, this function handles one kind of Error emitted by 
/// `Zettel::from_file`: an `Error::Io`](enum.Error.html#variant.Io) wrapping 
/// a `std::io::Error` of the `InvalidData` kind, if the file is a non-text
/// file (e.g. an image). All other errors received from `Zettel::from_file`
/// are propagated.
/// 
/// Thus, it returns 
/// - an Error if something else went wrong (see below)
/// - `Ok(None)` if nothing else went wrong, but the file was non-text and
///   has not been added to the index.
/// - `Ok(Some(file))` if everything went successful
/// As of now, both Ok-values are simply disregarded by indexing::create_index
/// and indexing::update_index. These functions simply skip to the next file.
/// # Errors
/// - [`Error::BadLink`](enum.Error.html#variant.BadLink) if an entry in
///   `followups` links to a file that doesn't exist 
///   (wrapping an `std::io::Error` of the `NotFound` kind).
/// - [`Error::IgnoreFile`](enum.Error.html#variant.IgnoreFile) for problems
///   applying an existing ignore file. (A missing ignore file is no problem).
/// - [`Error::Io`](enum.Error.html#variant.Io) wrapping several kinds of 
///   `std::io:Error`.
/// - [`Error::NormalizePath`](enum.Error.html#variant.NormalizePath) if 
///   `zettelfile` or a link in `followups``can not be expressed 
///   relative to the root directory.
/// - [`Error::Yaml`](enum.Error.html#variant.Yaml) when deserializing the
///   Zettel from YAML failed.
// --------------------------------------------------------------------------
// Memo: Zettel::from_file propagates the following error, which is handled, 
// here:
// - [`Error::Io`](enum.Error.html#variant.Io) wrapping a `std::io::Error`. 
//   of the `InvalidData` kind for non-text files. See above.
fn add_textfiles_only<P: AsRef<Path>>(index: &mut Index, 
                                      rootdir: P,
                                      zettelfile: P) -> Result<Option<PathBuf>, Error> {
    let zettelfile = zettelfile.as_ref();
    let rootdir = rootdir.as_ref();
    trace!("Adding new or updated entry for {:?}.", &zettelfile);
    // We handle one kind of error here:
    // io errors of the InvalidData kind. Zettel::from_file emits those for 
    // non-text files. For those, we don't create a zettel and skip to the
    // next file.
    // All other errors are propagated.
    let z = Zettel::from_file(rootdir.join(&zettelfile), rootdir.to_path_buf());
    let k = zettelfile; // from now on, the file path serves as a key
    match z {
        Ok(zettel) => { // Everything went well, add it to the index
            trace!("Key:  {:?}", k);
            trace!("{:?}", zettel);
                
            index.add_zettel(&k, zettel);
            //return Ok and Some with the key to the successfully added zettel
            Ok(Some(k.to_path_buf()))
        },
        Err(e) => match e { // Handle non-text files, propagate the rest.
            Error::Io(io_e) => match io_e.kind() {
                io::ErrorKind::InvalidData => {
                    info!("File {:?} seems to be non-text. Skipping.", k);
                    // return OK with None
                    Ok(None)
                },
                // Other io errors are propagated
                _ => return Err(Error::from(io_e)),         //error::Error
            },
            _ => return Err(e),                             //error::Error
        },
    }
}

/// Calls the specified indexingmethod on the list of files, and writes
/// the returned links to the index.
/// # Errors
/// - [`Error::BadLink`](enum.Error.html#variant.BadLink) if one of the files 
///   links to a target that doesn't exist.
/// - [`Error::Io`](enum.Error.html#variant.Io) wrapping several kinds of 
///   `std::io:Error`, e.g. problems executing grep or ripgrep, problems with 
///   the files etc.
/// - [`Error::NormalizePath`](enum.Error.html#variant.NormalizePath) if the 
///   path of one of the files contained in output can not be expressed 
///   relative to the root directory.
fn parse_and_apply_markdown_links<P: AsRef<Path>>(
                                            indexingmethod: &IndexingMethod,
                                            index: &mut Index,
                                            rootdir: P,
                                            files: Vec<PathBuf>,
                                            ) -> Result<(), Error> {
    let rootdir = rootdir.as_ref();
    debug!("Begin to parse markdown links.");

    let links_tuples = match indexingmethod {
        IndexingMethod::Grep => grep::parse_files(&rootdir.to_path_buf(),
                                                 PATTERN, 
                                                 files),
        IndexingMethod::RipGrep => ripgrep::parse_files(&rootdir.to_path_buf(),
                                                        PATTERN, 
                                                        files),
        IndexingMethod::Native => native::parse_files(&rootdir.to_path_buf(),
                                                      PATTERN, 
                                                      files),
    }?;//error::Error
    debug!("Got {} links tuples.", links_tuples.len());
    
    for (k, link) in links_tuples {
        trace!("Working on link from {:?} to {:?}", k, link);
        let z = index.get_mut_zettel(k);
        match z {
            Some(z) => z.add_link(link),
            None    => return Err(
                            Error::from(io::Error::new(io::ErrorKind::Other, 
                            "A zettel should be there, but it isn't."))
                            ),
        }
    }
    // If we're still her, no error has occured.
    Ok(())
}

/// Returns a list of files in the specified directory and its sub directories,
/// relative to the specified directory.
/// # Errors
/// - [`Error::Io`](enum.Error.html#variant.Io) wrapping several kinds of 
///   `std::io:Error`.
// Note normalize_path emits the following error, too, which is impossible,
// in this case.
// - [`Error::NormalizePath`](enum.Error.html#variant.NormalizePath) if the 
//   path of one of the files to can not be expressed relative to the  
//   directory. 
fn get_list_of_files_no_ignore<P: AsRef<Path>>(dir: P) -> Result<Vec<PathBuf>, Error> {
    let dir = dir.as_ref();
    let mut files = vec![]; // prepare return value
    for entry in fs::read_dir(dir)? { //iterate over the directory  //std::io::Error
        // shadow entry to unwrap it and propagate any errors
        let entry = entry?;                                         //std::io::Error
        let path = entry.path();
        if path.is_dir() { // do it recursively, if path is a directory
            let subdir_files = get_list_of_files_no_ignore(&path)?;
                                                                 //error::Error
            let norm_path = normalize_path(dir.to_path_buf(), path)?;
                                                                 //error::Error
            for file in subdir_files {
                files.push(norm_path.join(file));
            }
        } else {
            let path = normalize_path(dir.to_path_buf(), path)?;                //error::Error
            files.push(path);
        }
    }
    Ok(files)
}

/// Returns a list of files in the specified directory and its sub directories,
/// relative to the specified directory.
/// # Errors
/// - [`Error::IgnoreFile`](enum.Error.html#variant.IgnoreFile) for problems
///   applying an existing ignore file. (A missing ignore file is no problem).
/// - [`Error::Io`](enum.Error.html#variant.Io) wrapping several kinds of 
///   `std::io:Error`.
// Note normalize_path emits the following error, too, which is impossible,
// in this case.
// - [`Error::NormalizePath`](enum.Error.html#variant.NormalizePath) if the 
//   path of one of the files to can not be expressed relative to the  
//   directory. 
fn get_list_of_files<P: AsRef<Path>>(dir: P, ignorefile: P) 
                                        -> Result<Vec<PathBuf>, Error> {
    let dir = dir.as_ref();
    let ignorefile = ignorefile.as_ref();
    let gitignore_path = dir.join(ignorefile);
    // If there is no ignore file present, return all the files.
    if !gitignore_path.exists() {
        info!("The ignore file {:?} doesn't exist. Proceeding without it.", 
              &gitignore_path);
        return get_list_of_files_no_ignore(&dir.to_path_buf());  //error::Error 
    }
    
    // Otherwise, let the gitignore crate do the work.
    let gitignore_file = gitignore::File::new(&gitignore_path)?; // gitignore::Error
    let mut entries = gitignore_file.included_files()?;          // gitignore::Error
    let mut files = vec![]; // prepare return value
    
    // Normalize the entries
    for entry in entries.drain(..) {
        if entry.is_file() {
            let entry = normalize_path(&dir.to_path_buf(), &entry)?;
                                                                // error::Error
            files.push(entry);
        }
    }
    Ok(files)
}

/// Compares the list of files present in the index to the files actually 
/// existing in the root directory. The result is returned as a tuple of
/// vectors.
/// The first vector (`zettels_to_update`)contains the paths to files that 
/// still exist, but have changed since the the index was last updated. 
/// The second vector (`zettels_to_remove`) contains the paths to files that 
/// are still referenced in the index, but no longer exist.
/// # Errors
/// - [`Error::IgnoreFile`](enum.Error.html#variant.IgnoreFile) for problems
///   applying an existing ignore file. (A missing ignore file is no problem).
/// - [`Error::Io`](enum.Error.html#variant.Io) wrapping several kinds of 
///   `std::io:Error`.
// Note: normalize_path (via get_list_of_file) emits the following error, too, 
// which is impossible, in this case:
// - [`Error::NormalizePath`](enum.Error.html#variant.NormalizePath) if the 
//   path of one of the files to can not be expressed relative to the  
//   directory. 
fn get_changed_zettels<P: AsRef<Path>>(index: &mut Index, rootdir: P, ignorefile: P) 
        -> Result<(Vec<PathBuf>, Vec<PathBuf>), Error> {
    let rootdir = rootdir.as_ref();
    let ignorefile = ignorefile.as_ref();
    let mut zettels_to_update = vec![];
    let mut zettels_to_remove = vec![];
    
    // get a list of all files in rootdir (recursively)
    let mut files = get_list_of_files(&rootdir, &ignorefile)?; //error::Error
    
    // Some might be new, some might have been deleted, others might have been
    // modified. 
    // Handle the new ones, first. The rest is moved to `rest`.
    let mut rest = vec![];
    for file in files.drain(..) {
        if !&index.files.contains_key(&file) {
            // The index doesn't know this file, so we might treat it as 
            // updated.
            zettels_to_update.push(file);
        } else {
            // The index knows this. So it belongs to the rest
            rest.push(file);
        }
    }
    
    // continue with the rest.
    let files = rest;
    
    // iterate over the keys of the index
    for (k, _) in &index.files {
        if !files.contains(&k) { // the file at k doesn't exist, anymore
            zettels_to_remove.push(k.clone()); // mark it for removal from index
        } else {
            let modified = fs::metadata(&rootdir.join(&k))?.modified()?; 
                                                              // std::io::Error
            if modified > index.timestamp {
                zettels_to_update.push(k.clone()); // mark it for update
            }
        }
    }
    Ok((zettels_to_update, zettels_to_remove))
}

/// ## Extended API
/// Takes paths `zettelfile`, `link` and `rootdir`.
/// - `link` is interpreted as relative to `zettelfile`.
/// - `zettelfile` is interpreted as relative to `rootdir`.
/// Returns `link` relative to `rootdir`.
///
/// The function is aware of symlinks and can cope with mixed up symlinked and 
/// real paths.
/// //# Example
/// //```
/// //# use std::path::Path;
/// //# use libzettels::normalize_link;
/// //let rootdir = Path::new("examples/Zettelkasten");
/// //let zettelfile = Path::new("subdir/file4.md");
/// //let link = Path::new("../file1.md");
/// //let normalized_link = normalize_link(rootdir, zettelfile, link);
/// //assert!(normalized_link.is_ok());
/// //let normalized_link = normalized_link.unwrap();
/// //assert_eq!(normalized_link, Path::new("file1.md"));
/// //```
/// 
/// # Errors
/// - [`Error::BadLink`](enum.Error.html#variant.BadLink) if `link` doesn't 
///   exist (wrapping an `std::io::Error` of the `NotFound` kind).
/// - [`Error::Io`](enum.Error.html#variant.Io) wrapping various kinds of 
///   `std::io::Error`.
/// - [`Error::NormalizePath`](enum.Error.html#variant.NormalizePath) wrapping 
///   a `std::path::StripPrefixError` if `link` can not be expresses relative 
///   to root directory.
pub fn normalize_link<P: AsRef<Path>>(rootdir: P, zettelfile: P, link: P) 
                                -> Result<PathBuf, Error> {
    let rootdir = rootdir.as_ref();
    let zettelfile = zettelfile.as_ref();
    let link = link.as_ref();
    trace!("Normalizing link {:?} from {:?}.", link, zettelfile);
    trace!("Rootdir: {:?}", rootdir);
    //trace!("File: {:?}", zettelfile);
    //trace!("link: {:?}", link);
    
    // We need to get the directory-part of the path `zettelfile`. To get it, we 
    // call `zettelfile.parent()`.
    // That can be None which we should handle as an error, since it means that
    // `zettelfile` can not be in rootdir or one of its subdirectories.
    let file_dir = match zettelfile.parent() { // could be done with ok_or()?
        None => return Err(
                       Error::Io(
                       io::Error::new(
                        io::ErrorKind::InvalidInput, 
                        format!("{:#?} is not a valid path to a Zettel.", zettelfile)
                        ))),
        Some(parent) => parent, 
    };
    trace!("File dir: {:?}", file_dir);
    // Join the link to file_dir
    let link  = file_dir.join(link);
    // And join it to the rootdir.
    let link  = rootdir.join(link);
    trace!("Joined link: {:?}", link);
    // Make it absolute, resolving symlinks, emit a BadLink, if it doesn't exist.
    let canon_link        = link.canonicalize();
    let link = match canon_link {
        Ok(canon_link) => {
            trace!("Absolute link: {:?}", canon_link);
            Ok(canon_link)
        }, 
        Err(e) => {
            // canonicalize() only emits io::Error, so we match for ErrorKind
            match e.kind() {  
                io::ErrorKind::NotFound => {
                    trace!("{:?} doesn't exist. Emmiting Error::BadLink.", link);
                    Err(Error::BadLink(zettelfile.clone().to_path_buf(), link, e))
                }, 
                _ => Err(Error::Io(e)), // propagate all other kinds
            }
        }
    }?;
    // Make the rootdir absoulte, too (also resolving symlinks)
    let rootdir     = rootdir.canonicalize()?;
    trace!("Absolute rootdir: {:?}", rootdir);
    // strip the absolute path to rootdir from link, resulting in a relative
    // link
    let rellink     = link.strip_prefix(rootdir)?; // std::path::StripPrefixError
    trace!("Normalized link: {:?}", rellink);
    Ok(rellink.to_path_buf())
}

/// ## Extended API
/// Takes the path to a `zettelfile` and returns a PathBuf containing 
/// the path to that file relative to `rootdir`.
///
/// The function is aware of symlinks and can cope with mixed up symlinked and 
/// real paths.
///
/// # Errors
/// - [`Error::Io`](enum.Error.html#variant.Io) wrapping several kinds of 
///   `std::io:Error`.
/// - [`Error::NormalizePath`](enum.Error.html#variant.NormalizePath) if the 
///   path to can not be expressed relative to the root directory. 
pub fn normalize_path<P: AsRef<Path>>(rootdir: P, zettelfile: P) 
                                -> Result<PathBuf, Error> {
    let zettelfile = zettelfile.as_ref().canonicalize()?;                 // std::io::Error
    let rootdir = rootdir.as_ref().canonicalize()?;           // std::io::Error
    let relfile = zettelfile.strip_prefix(rootdir)?;   // std::path::StripPrefixError
    Ok(relfile.to_path_buf())
}

// --------------------------------------------------------------------------
// Tests
// --------------------------------------------------------------------------
// Since all these functions require existing directories and files in order
// to test anything but error handling, the unit tests were moved to two
// submodules of tests: `valid` and `invalid`, testing the various functions
// of this module with valid and invalid data, respectively.
// --------------------------------------------------------------------------

#[cfg(test)]
mod tests {
    use super::PATTERN;
    use regex::Regex;
    
    mod valid;
    mod invalid;
    
    
    #[test]
    fn test_regex() {
        // Does our pattern work as intended?    
        let r = Regex::new(PATTERN)
            .expect("Failed to build regex.");

        let text = "Duis [ornare](enim) magna";
        assert!(r.is_match(text));
        
        let text = "Duis [ornare](enim) [magna](foo)";
        assert!(r.is_match(text));
        
        let text = "[ornare](enim) magna";
        assert!(r.is_match(text));
        
        let text = "Integer consectetur neque velit, at.";
        assert!(!r.is_match(text));
        
        let text = "Integer [consectetur (neque)][velit], at.";
        assert!(!r.is_match(text));
        
        let text = "[consectetur (neque)][velit], at.";
        assert!(!r.is_match(text));
        
        let text = "Maecenas [rutrum][pretium] velit vitae.";
        assert!(!r.is_match(text));
        
        let text = "[rutrum][pretium] velit vitae.";
        assert!(!r.is_match(text));
    }
}