New: add start_at option to export a partial vault

This introduces a new `--start-at` CLI argument and corresponding
`start_at()` method on the Exporter type that allows exporting of only a
given subdirectory within a vault.

See the updated README file for more details on when and how this may be
used.
This commit is contained in:
Nick Groenen 2021-08-27 15:38:02 +02:00
parent c64d75967e
commit 634b0d70ac
No known key found for this signature in database
GPG Key ID: 4F0AD019928AE098
11 changed files with 220 additions and 60 deletions

107
README.md
View File

@ -67,6 +67,8 @@ Running `obsidian-export --version` should print a version number rather than gi
>
> For example `~/Downloads/obsidian-export --version` on Mac/Linux or `~\Downloads\obsidian-export --version` on Windows (PowerShell).
## Exporting notes
In it's most basic form, `obsidian-export` takes just two mandatory arguments, a source and a destination:
````sh
@ -89,6 +91,31 @@ obsidian-export my-obsidian-vault/some-note.md /tmp/export/
obsidian-export my-obsidian-vault/some-note.md /tmp/exported-note.md
````
Note that in this mode, obsidian-export sees `some-note.md` as being the only file that exists in your vault so references to other notes won't be resolved.
This is by design.
If you'd like to export a single note while resolving links or embeds to other areas in your vault then you should instead specify the root of your vault as the source, passing the file you'd like to export with `--start-at`, as described in the next section.
### Exporting a partial vault
Using the `--start-at` argument, you can export just a subset of your vault.
Given the following vault structure:
````
my-obsidian-vault
├── Notes/
├── Books/
└── People/
````
This will export only the notes in the `Books` directory to `exported-notes`:
````sh
obsidian-export my-obsidian-vault --start-at my-obsidian-vault/Books exported-notes
````
In this mode, all notes under the source (the first argument) are considered part of the vault so any references to these files will remain intact, even if they're not part of the exported notes.
## Character encodings
At present, UTF-8 character encoding is assumed for all note text as well as filenames.
@ -230,7 +257,7 @@ Unless you explicitly state otherwise, any contribution intentionally submitted
### New
* Postprocessing support. \[Nick Groenen]
* Postprocessing support. \[Nick Groenen\]
Add support for postprocessing of Markdown prior to writing converted
notes to disk.
@ -254,7 +281,7 @@ Unless you explicitly state otherwise, any contribution intentionally submitted
### Fixes
* Also percent-encode `?` in filenames. \[Nick Groenen]
* Also percent-encode `?` in filenames. \[Nick Groenen\]
A recent Obsidian update expanded the list of allowed characters in
filenames, which now includes `?` as well. This needs to be
@ -262,20 +289,20 @@ Unless you explicitly state otherwise, any contribution intentionally submitted
### Other
* Bump pretty_assertions from 0.6.1 to 0.7.1. \[dependabot\[bot]]
* Bump pretty_assertions from 0.6.1 to 0.7.1. \[dependabot\[bot\]\]
Bumps [pretty_assertions](https://github.com/colin-kiegel/rust-pretty-assertions) from 0.6.1 to 0.7.1.
* [Release notes](https://github.com/colin-kiegel/rust-pretty-assertions/releases)
* [Changelog](https://github.com/colin-kiegel/rust-pretty-assertions/blob/main/CHANGELOG.md)
* [Commits](https://github.com/colin-kiegel/rust-pretty-assertions/compare/v0.6.1...v0.7.1)
* Bump walkdir from 2.3.1 to 2.3.2. \[dependabot\[bot]]
* Bump walkdir from 2.3.1 to 2.3.2. \[dependabot\[bot\]\]
Bumps [walkdir](https://github.com/BurntSushi/walkdir) from 2.3.1 to 2.3.2.
* [Release notes](https://github.com/BurntSushi/walkdir/releases)
* [Commits](https://github.com/BurntSushi/walkdir/compare/2.3.1...2.3.2)
* Bump regex from 1.4.3 to 1.4.5. \[dependabot\[bot]]
* Bump regex from 1.4.3 to 1.4.5. \[dependabot\[bot\]\]
Bumps [regex](https://github.com/rust-lang/regex) from 1.4.3 to 1.4.5.
@ -287,11 +314,11 @@ Unless you explicitly state otherwise, any contribution intentionally submitted
### New
* Add `--version` flag. \[Nick Groenen]
* Add `--version` flag. \[Nick Groenen\]
### Changes
* Don't Box FilterFn in WalkOptions. \[Nick Groenen]
* Don't Box FilterFn in WalkOptions. \[Nick Groenen\]
Previously, `filter_fn` on the `WalkOptions` struct looked like:
@ -313,7 +340,7 @@ Unless you explicitly state otherwise, any contribution intentionally submitted
### Fixes
* Recognize notes beginning with underscores. \[Nick Groenen]
* Recognize notes beginning with underscores. \[Nick Groenen\]
Notes with an underscore would fail to be recognized within Obsidian
`[[_WikiLinks]]` due to the assumption that the underlying Markdown
@ -324,46 +351,46 @@ Unless you explicitly state otherwise, any contribution intentionally submitted
machine which correctly recognizes this corner-case (and likely some
others).
* Support self-references. \[Joshua Coles]
* Support self-references. \[Joshua Coles\]
This ensures links to headings within the same note (`[[#Heading]]`)
resolve correctly.
### Other
* Avoid redundant "Release" in GitHub release titles. \[Nick Groenen]
* Avoid redundant "Release" in GitHub release titles. \[Nick Groenen\]
* Add failing testcase for files with underscores. \[Nick Groenen]
* Add failing testcase for files with underscores. \[Nick Groenen\]
* Add unit tests for display of ObsidianNoteReference. \[Nick Groenen]
* Add unit tests for display of ObsidianNoteReference. \[Nick Groenen\]
* Add some unit tests for ObsidianNoteReference::from_str. \[Nick Groenen]
* Add some unit tests for ObsidianNoteReference::from_str. \[Nick Groenen\]
* Also run tests on pull requests. \[Nick Groenen]
* Also run tests on pull requests. \[Nick Groenen\]
* Apply clippy suggestions following rust 1.50.0. \[Nick Groenen]
* Apply clippy suggestions following rust 1.50.0. \[Nick Groenen\]
* Fix infinite recursion bug with references to current file. \[Joshua Coles]
* Fix infinite recursion bug with references to current file. \[Joshua Coles\]
* Add tests for self-references. \[Joshua Coles]
* Add tests for self-references. \[Joshua Coles\]
Note as there is no support for block references at the moment, the generated link goes nowhere, however it is to a reasonable ID
* Bump tempfile from 3.1.0 to 3.2.0. \[dependabot\[bot]]
* Bump tempfile from 3.1.0 to 3.2.0. \[dependabot\[bot\]\]
Bumps [tempfile](https://github.com/Stebalien/tempfile) from 3.1.0 to 3.2.0.
* [Release notes](https://github.com/Stebalien/tempfile/releases)
* [Changelog](https://github.com/Stebalien/tempfile/blob/master/NEWS)
* [Commits](https://github.com/Stebalien/tempfile/commits)
* Bump eyre from 0.6.3 to 0.6.5. \[dependabot\[bot]]
* Bump eyre from 0.6.3 to 0.6.5. \[dependabot\[bot\]\]
Bumps [eyre](https://github.com/yaahc/eyre) from 0.6.3 to 0.6.5.
* [Release notes](https://github.com/yaahc/eyre/releases)
* [Changelog](https://github.com/yaahc/eyre/blob/v0.6.5/CHANGELOG.md)
* [Commits](https://github.com/yaahc/eyre/compare/v0.6.3...v0.6.5)
* Bump regex from 1.4.2 to 1.4.3. \[dependabot\[bot]]
* Bump regex from 1.4.2 to 1.4.3. \[dependabot\[bot\]\]
Bumps [regex](https://github.com/rust-lang/regex) from 1.4.2 to 1.4.3.
@ -375,7 +402,7 @@ Unless you explicitly state otherwise, any contribution intentionally submitted
### Fixes
* Find uppercased notes when referenced with lowercase. \[Nick Groenen]
* Find uppercased notes when referenced with lowercase. \[Nick Groenen\]
This commit fixes a bug where, if a note contained uppercase characters
(for example `Note.md`) but was referred to using lowercase
@ -385,7 +412,7 @@ Unless you explicitly state otherwise, any contribution intentionally submitted
### New
* Add --no-recursive-embeds to break infinite recursion cycles. \[Nick Groenen]
* Add --no-recursive-embeds to break infinite recursion cycles. \[Nick Groenen\]
It's possible to end up with "recursive embeds" when two notes embed
each other. This happens for example when a `Note A.md` contains
@ -400,14 +427,14 @@ Unless you explicitly state otherwise, any contribution intentionally submitted
See also: https://github.com/zoni/obsidian-export/issues/1
* Make walk options configurable on CLI. \[Nick Groenen]
* Make walk options configurable on CLI. \[Nick Groenen\]
By default hidden files, patterns listed in `.export-ignore` as well as
any files ignored by git are excluded from exports. This behavior has
been made configurable on the CLI using the new flags `--hidden`,
`--ignore-file` and `--no-git`.
* Support links referencing headings. \[Nick Groenen]
* Support links referencing headings. \[Nick Groenen\]
Previously, links referencing a heading (`[[note#heading]]`) would just
link to the file name without including an anchor in the link target.
@ -427,7 +454,7 @@ Unless you explicitly state otherwise, any contribution intentionally submitted
end with a smiley. The slug library, and thus obsidian-export, will
avoid such dangling dashes).
* Support embeds referencing headings. \[Nick Groenen]
* Support embeds referencing headings. \[Nick Groenen\]
Previously, partial embeds (`![[note#heading]]`) would always include
the entire file into the source note. Now, such embeds will only include
@ -437,20 +464,20 @@ Unless you explicitly state otherwise, any contribution intentionally submitted
### Changes
* Print warnings to stderr rather than stdout. \[Nick Groenen]
* Print warnings to stderr rather than stdout. \[Nick Groenen\]
Warning messages emitted when encountering broken links/references will
now be printed to stderr as opposed to stdout.
### Other
* Include filter_fn field in WalkOptions debug display. \[Nick Groenen]
* Include filter_fn field in WalkOptions debug display. \[Nick Groenen\]
## v0.4.0 (2020-12-23)
### Fixes
* Correct relative links within embedded notes. \[Nick Groenen]
* Correct relative links within embedded notes. \[Nick Groenen\]
Links within an embedded note would point to other local resources
relative to the filesystem location of the note being embedded.
@ -463,13 +490,13 @@ Unless you explicitly state otherwise, any contribution intentionally submitted
### Other
* Add brief library documentation to all public types and functions. \[Nick Groenen]
* Add brief library documentation to all public types and functions. \[Nick Groenen\]
## v0.3.0 (2020-12-21)
### New
* Report file tree when RecursionLimitExceeded is hit. \[Nick Groenen]
* Report file tree when RecursionLimitExceeded is hit. \[Nick Groenen\]
This refactors the Context to maintain a list of all the files which
have been processed so far in a chain of embeds. This information is
@ -478,37 +505,37 @@ Unless you explicitly state otherwise, any contribution intentionally submitted
### Changes
* Add extra whitespace around multi-line warnings. \[Nick Groenen]
* Add extra whitespace around multi-line warnings. \[Nick Groenen\]
This makes errors a bit easier to distinguish after a number of warnings
has been printed.
### Other
* Setup gitchangelog. \[Nick Groenen]
* Setup gitchangelog. \[Nick Groenen\]
This adds a changelog (CHANGES.md) which is automatically generated with
[gitchangelog](https://github.com/vaab/gitchangelog).
## v0.2.0 (2020-12-13)
* Allow custom filter function to be passed with WalkOptions. \[Nick Groenen]
* Allow custom filter function to be passed with WalkOptions. \[Nick Groenen\]
* Re-export vault_contents and WalkOptions as pub from crate root. \[Nick Groenen]
* Re-export vault_contents and WalkOptions as pub from crate root. \[Nick Groenen\]
* Run mdbook hook against README.md too. \[Nick Groenen]
* Run mdbook hook against README.md too. \[Nick Groenen\]
* Update installation instructions. \[Nick Groenen]
* Update installation instructions. \[Nick Groenen\]
Installation no longer requires a git repository URL now that a crate is
published.
* Add MdBook generation script and precommit hook. \[Nick Groenen]
* Add MdBook generation script and precommit hook. \[Nick Groenen\]
* Add more reliable non-ASCII tetscase. \[Nick Groenen]
* Add more reliable non-ASCII tetscase. \[Nick Groenen\]
* Create FUNDING.yml. \[Nick Groenen]
* Create FUNDING.yml. \[Nick Groenen\]
## v0.1.0 (2020-11-28)
* Public release. \[Nick Groenen]
* Public release. \[Nick Groenen\]

View File

@ -10,6 +10,8 @@ Running `obsidian-export --version` should print a version number rather than gi
>
> For example `~/Downloads/obsidian-export --version` on Mac/Linux or `~\Downloads\obsidian-export --version` on Windows (PowerShell).
## Exporting notes
In it's most basic form, `obsidian-export` takes just two mandatory arguments, a source and a destination:
```sh
@ -31,6 +33,31 @@ obsidian-export my-obsidian-vault/some-note.md /tmp/export/
obsidian-export my-obsidian-vault/some-note.md /tmp/exported-note.md
```
Note that in this mode, obsidian-export sees `some-note.md` as being the only file that exists in your vault so references to other notes won't be resolved.
This is by design.
If you'd like to export a single note while resolving links or embeds to other areas in your vault then you should instead specify the root of your vault as the source, passing the file you'd like to export with `--start-at`, as described in the next section.
### Exporting a partial vault
Using the `--start-at` argument, you can export just a subset of your vault.
Given the following vault structure:
```
my-obsidian-vault
├── Notes/
├── Books/
└── People/
```
This will export only the notes in the `Books` directory to `exported-notes`:
```sh
obsidian-export my-obsidian-vault --start-at my-obsidian-vault/Books exported-notes
```
In this mode, all notes under the source (the first argument) are considered part of the vault so any references to these files will remain intact, even if they're not part of the exported notes.
## Character encodings
At present, UTF-8 character encoding is assumed for all note text as well as filenames.

View File

@ -211,6 +211,7 @@ pub enum PostprocessorResult {
pub struct Exporter<'a> {
root: PathBuf,
destination: PathBuf,
start_at: PathBuf,
frontmatter_strategy: FrontmatterStrategy,
vault_contents: Option<Vec<PathBuf>>,
walk_options: WalkOptions<'a>,
@ -239,11 +240,12 @@ impl<'a> fmt::Debug for Exporter<'a> {
}
impl<'a> Exporter<'a> {
/// Create a new exporter which reads notes from `source` and exports these to
/// Create a new exporter which reads notes from `root` and exports these to
/// `destination`.
pub fn new(source: PathBuf, destination: PathBuf) -> Exporter<'a> {
pub fn new(root: PathBuf, destination: PathBuf) -> Exporter<'a> {
Exporter {
root: source,
start_at: root.clone(),
root,
destination,
frontmatter_strategy: FrontmatterStrategy::Auto,
walk_options: WalkOptions::default(),
@ -253,6 +255,15 @@ impl<'a> Exporter<'a> {
}
}
/// Set a custom starting point for the export.
///
/// Normally all notes under `root` (except for notes excluded by ignore rules) will be exported.
/// When `start_at` is set, only notes under this path will be exported to the target destination.
pub fn start_at(&mut self, start_at: PathBuf) -> &mut Exporter<'a> {
self.start_at = start_at;
self
}
/// Set the [`WalkOptions`] to be used for this exporter.
pub fn walk_options(&mut self, options: WalkOptions<'a>) -> &mut Exporter<'a> {
self.walk_options = options;
@ -292,13 +303,17 @@ impl<'a> Exporter<'a> {
});
}
// When a single file is specified, we can short-circuit contruction of walk and associated
// directory traversal. This also allows us to accept destination as either a file or a
// directory name.
if self.root.is_file() {
self.vault_contents = Some(vec![self.root.clone()]);
self.vault_contents = Some(vault_contents(
self.root.as_path(),
self.walk_options.clone(),
)?);
// When a single file is specified, just need to export that specific file instead of
// iterating over all discovered files. This also allows us to accept destination as either
// a file or a directory name.
if self.root.is_file() || self.start_at.is_file() {
let source_filename = self
.root
.start_at
.file_name()
.expect("File without a filename? How is that possible?")
.to_string_lossy();
@ -317,7 +332,7 @@ impl<'a> Exporter<'a> {
self.destination.clone()
}
};
return self.export_note(&self.root, &destination);
return self.export_note(&self.start_at, &destination);
}
if !self.destination.exists() {
@ -325,19 +340,15 @@ impl<'a> Exporter<'a> {
path: self.destination.clone(),
});
}
self.vault_contents = Some(vault_contents(
self.root.as_path(),
self.walk_options.clone(),
)?);
self.vault_contents
.as_ref()
.unwrap()
.clone()
.into_par_iter()
.filter(|file| file.starts_with(&self.start_at))
.try_for_each(|file| {
let relative_path = file
.strip_prefix(&self.root.clone())
.strip_prefix(&self.start_at.clone())
.expect("file should always be nested under root")
.to_path_buf();
let destination = &self.destination.join(&relative_path);

View File

@ -13,12 +13,15 @@ struct Opts {
#[options(help = "Display version information")]
version: bool,
#[options(help = "Source file containing reference", free, required)]
#[options(help = "Read notes from this source", free, required)]
source: Option<PathBuf>,
#[options(help = "Destination file being linked to", free, required)]
#[options(help = "Write notes to this destination", free, required)]
destination: Option<PathBuf>,
#[options(no_short, help = "Only export notes under this sub-path")]
start_at: Option<PathBuf>,
#[options(
help = "Frontmatter strategy (one of: always, never, auto)",
no_short,
@ -64,7 +67,7 @@ fn main() {
}
let args = Opts::parse_args_default_or_exit();
let source = args.source.unwrap();
let root = args.source.unwrap();
let destination = args.destination.unwrap();
let walk_options = WalkOptions {
@ -74,11 +77,15 @@ fn main() {
..Default::default()
};
let mut exporter = Exporter::new(source, destination);
let mut exporter = Exporter::new(root, destination);
exporter.frontmatter_strategy(args.frontmatter_strategy);
exporter.process_embeds_recursively(!args.no_recursive_embeds);
exporter.walk_options(walk_options);
if let Some(path) = args.start_at {
exporter.start_at(path);
}
if let Err(err) = exporter.run() {
match err {
ExportError::FileExportError {

View File

@ -161,6 +161,79 @@ fn test_single_file_to_file() {
);
}
#[test]
fn test_start_at_subdir() {
let tmp_dir = TempDir::new().expect("failed to make tempdir");
let mut exporter = Exporter::new(
PathBuf::from("tests/testdata/input/start-at/"),
tmp_dir.path().to_path_buf(),
);
exporter.start_at(PathBuf::from("tests/testdata/input/start-at/subdir"));
exporter.run().unwrap();
let expected = if cfg!(windows) {
read_to_string("tests/testdata/expected/start-at/subdir/Note B.md")
.unwrap()
.replace("/", "\\")
} else {
read_to_string("tests/testdata/expected/start-at/subdir/Note B.md").unwrap()
};
assert_eq!(
expected,
read_to_string(tmp_dir.path().clone().join(PathBuf::from("Note B.md"))).unwrap(),
);
}
#[test]
fn test_start_at_file_within_subdir_destination_is_dir() {
let tmp_dir = TempDir::new().expect("failed to make tempdir");
let mut exporter = Exporter::new(
PathBuf::from("tests/testdata/input/start-at/"),
tmp_dir.path().to_path_buf(),
);
exporter.start_at(PathBuf::from(
"tests/testdata/input/start-at/subdir/Note B.md",
));
exporter.run().unwrap();
let expected = if cfg!(windows) {
read_to_string("tests/testdata/expected/start-at/single-file/Note B.md")
.unwrap()
.replace("/", "\\")
} else {
read_to_string("tests/testdata/expected/start-at/single-file/Note B.md").unwrap()
};
assert_eq!(
expected,
read_to_string(tmp_dir.path().clone().join(PathBuf::from("Note B.md"))).unwrap(),
);
}
#[test]
fn test_start_at_file_within_subdir_destination_is_file() {
let tmp_dir = TempDir::new().expect("failed to make tempdir");
let dest = tmp_dir.path().clone().join(PathBuf::from("note.md"));
let mut exporter = Exporter::new(
PathBuf::from("tests/testdata/input/start-at/"),
dest.clone(),
);
exporter.start_at(PathBuf::from(
"tests/testdata/input/start-at/subdir/Note B.md",
));
exporter.run().unwrap();
let expected = if cfg!(windows) {
read_to_string("tests/testdata/expected/start-at/single-file/Note B.md")
.unwrap()
.replace("/", "\\")
} else {
read_to_string("tests/testdata/expected/start-at/single-file/Note B.md").unwrap()
};
assert_eq!(expected, read_to_string(dest).unwrap(),);
}
#[test]
fn test_not_existing_source() {
let tmp_dir = TempDir::new().expect("failed to make tempdir");

View File

@ -0,0 +1,4 @@
This is note B. It links to:
* [Note A](../Note%20A.md)
* [Note C](Note%20C.md)

View File

@ -0,0 +1,4 @@
This is note B. It links to:
* [Note A](../Note%20A.md)
* [Note C](Note%20C.md)

View File

@ -0,0 +1 @@
This is note C.

View File

@ -0,0 +1 @@
This is note A.

View File

@ -0,0 +1,4 @@
This is note B. It links to:
- [[Note A]]
- [[Note C]]

View File

@ -0,0 +1 @@
This is note C.