Move vault_contents out of Context and into Exporter
This reduces the need to pass vault_contents around in various places and restricts Context to dealing with the actual note which is being processed, instead of also carrying program state information. This will help with future feature development as note parsing functions can now access Exporter directly.
This commit is contained in:
parent
e9d5e69e24
commit
207ca1124e
106
src/lib.rs
106
src/lib.rs
@ -84,36 +84,35 @@ pub struct Exporter<'a> {
|
||||
root: PathBuf,
|
||||
destination: PathBuf,
|
||||
frontmatter_strategy: FrontmatterStrategy,
|
||||
vault_contents: Option<Vec<PathBuf>>,
|
||||
walk_options: WalkOptions<'a>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
/// Context holds parser metadata for the file/note currently being parsed.
|
||||
struct Context<'a> {
|
||||
struct Context {
|
||||
file_tree: Vec<PathBuf>,
|
||||
vault_contents: &'a [PathBuf],
|
||||
frontmatter_strategy: FrontmatterStrategy,
|
||||
}
|
||||
|
||||
impl<'a> Context<'a> {
|
||||
impl Context {
|
||||
/// Create a new `Context`
|
||||
fn new(file: PathBuf, vault_contents: &'a [PathBuf]) -> Context<'a> {
|
||||
fn new(file: PathBuf) -> Context {
|
||||
Context {
|
||||
file_tree: vec![file.clone()],
|
||||
vault_contents,
|
||||
frontmatter_strategy: FrontmatterStrategy::Auto,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new `Context` which inherits from a parent Context.
|
||||
fn from_parent(context: &Context<'a>, child: &PathBuf) -> Context<'a> {
|
||||
fn from_parent(context: &Context, child: &PathBuf) -> Context {
|
||||
let mut context = context.clone();
|
||||
context.file_tree.push(child.to_path_buf());
|
||||
context
|
||||
}
|
||||
|
||||
/// Associate a new `FrontmatterStrategy` with this context.
|
||||
fn set_frontmatter_strategy(&mut self, strategy: FrontmatterStrategy) -> &mut Context<'a> {
|
||||
fn set_frontmatter_strategy(&mut self, strategy: FrontmatterStrategy) -> &mut Context {
|
||||
self.frontmatter_strategy = strategy;
|
||||
self
|
||||
}
|
||||
@ -146,6 +145,7 @@ impl<'a> Exporter<'a> {
|
||||
destination,
|
||||
frontmatter_strategy: FrontmatterStrategy::Auto,
|
||||
walk_options: WalkOptions::default(),
|
||||
vault_contents: None,
|
||||
}
|
||||
}
|
||||
|
||||
@ -159,15 +159,18 @@ impl<'a> Exporter<'a> {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn run(self) -> Result<()> {
|
||||
pub fn run(&mut self) -> Result<()> {
|
||||
if !self.root.exists() {
|
||||
return Err(ExportError::PathDoesNotExist { path: self.root });
|
||||
return Err(ExportError::PathDoesNotExist {
|
||||
path: self.root.clone(),
|
||||
});
|
||||
}
|
||||
|
||||
// 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()]);
|
||||
let source_filename = self
|
||||
.root
|
||||
.file_name()
|
||||
@ -188,44 +191,49 @@ impl<'a> Exporter<'a> {
|
||||
self.destination.clone()
|
||||
}
|
||||
};
|
||||
return Ok(self.export_note(&self.root, &destination, &[self.root.clone()])?);
|
||||
return Ok(self.export_note(&self.root, &destination)?);
|
||||
}
|
||||
|
||||
if !self.destination.exists() {
|
||||
return Err(ExportError::PathDoesNotExist {
|
||||
path: self.destination,
|
||||
path: self.destination.clone(),
|
||||
});
|
||||
}
|
||||
|
||||
let vault = vault_contents(self.root.as_path(), self.walk_options.clone())?;
|
||||
vault.clone().into_par_iter().try_for_each(|file| {
|
||||
self.vault_contents = Some(vault_contents(
|
||||
self.root.as_path(),
|
||||
self.walk_options.clone(),
|
||||
)?);
|
||||
self.vault_contents
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.clone()
|
||||
.into_par_iter()
|
||||
.try_for_each(|file| {
|
||||
let relative_path = file
|
||||
.strip_prefix(&self.root.clone())
|
||||
.expect("file should always be nested under root")
|
||||
.to_path_buf();
|
||||
let destination = &self.destination.join(&relative_path);
|
||||
self.export_note(&file, destination, &vault)
|
||||
self.export_note(&file, destination)
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn export_note(&self, src: &Path, dest: &Path, vault_contents: &[PathBuf]) -> Result<()> {
|
||||
fn export_note(&self, src: &Path, dest: &Path) -> Result<()> {
|
||||
match is_markdown_file(src) {
|
||||
true => {
|
||||
parse_and_export_obsidian_note(src, dest, vault_contents, self.frontmatter_strategy)
|
||||
}
|
||||
true => self.parse_and_export_obsidian_note(src, dest, self.frontmatter_strategy),
|
||||
false => copy_file(src, dest),
|
||||
}
|
||||
.context(FileExportError { path: src })
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_and_export_obsidian_note(
|
||||
fn parse_and_export_obsidian_note(
|
||||
&self,
|
||||
src: &Path,
|
||||
dest: &Path,
|
||||
vault_contents: &[PathBuf],
|
||||
frontmatter_strategy: FrontmatterStrategy,
|
||||
) -> Result<()> {
|
||||
) -> Result<()> {
|
||||
let content = fs::read_to_string(&src).context(ReadError { path: src })?;
|
||||
|
||||
let (mut frontmatter, _content) =
|
||||
@ -248,16 +256,16 @@ fn parse_and_export_obsidian_note(
|
||||
.context(WriteError { path: &dest })?;
|
||||
}
|
||||
|
||||
let mut context = Context::new(src.to_path_buf(), vault_contents);
|
||||
let mut context = Context::new(src.to_path_buf());
|
||||
context.set_frontmatter_strategy(frontmatter_strategy);
|
||||
let markdown_tree = parse_obsidian_note(&src, &context)?;
|
||||
let markdown_tree = self.parse_obsidian_note(&src, &context)?;
|
||||
outfile
|
||||
.write_all(render_mdtree_to_mdtext(markdown_tree).as_bytes())
|
||||
.context(WriteError { path: &dest })?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_obsidian_note<'a>(path: &Path, context: &Context) -> Result<MarkdownTree<'a>> {
|
||||
fn parse_obsidian_note<'b>(&self, path: &Path, context: &Context) -> Result<MarkdownTree<'b>> {
|
||||
if context.note_depth() > NOTE_RECURSION_LIMIT {
|
||||
return Err(ExportError::RecursionLimitExceeded {
|
||||
file_tree: context.file_tree(),
|
||||
@ -311,13 +319,14 @@ fn parse_obsidian_note<'a>(path: &Path, context: &Context) -> Result<MarkdownTre
|
||||
if let Event::Text(CowStr::Borrowed(text)) = buffer[2] {
|
||||
match buffer[0] {
|
||||
Event::Text(CowStr::Borrowed("[")) => {
|
||||
let mut link_events = obsidian_note_link_to_markdown(&text, context);
|
||||
let mut link_events =
|
||||
self.obsidian_note_link_to_markdown(&text, context);
|
||||
tree.append(&mut link_events);
|
||||
buffer.clear();
|
||||
continue;
|
||||
}
|
||||
Event::Text(CowStr::Borrowed("![")) => {
|
||||
let mut elements = embed_file(&text, &context)?;
|
||||
let mut elements = self.embed_file(&text, &context)?;
|
||||
tree.append(&mut elements);
|
||||
buffer.clear();
|
||||
continue;
|
||||
@ -337,26 +346,27 @@ fn parse_obsidian_note<'a>(path: &Path, context: &Context) -> Result<MarkdownTre
|
||||
}
|
||||
tree.append(&mut buffer);
|
||||
Ok(tree.into_iter().map(event_to_owned).collect())
|
||||
}
|
||||
}
|
||||
|
||||
// Generate markdown elements for a file that is embedded within another note.
|
||||
//
|
||||
// - If the file being embedded is a note, it's content is included at the point of embed.
|
||||
// - If the file is an image, an image tag is generated.
|
||||
// - For other types of file, a regular link is created instead.
|
||||
fn embed_file<'a, 'b>(note_name: &'a str, context: &'b Context) -> Result<MarkdownTree<'a>> {
|
||||
// Generate markdown elements for a file that is embedded within another note.
|
||||
//
|
||||
// - If the file being embedded is a note, it's content is included at the point of embed.
|
||||
// - If the file is an image, an image tag is generated.
|
||||
// - For other types of file, a regular link is created instead.
|
||||
fn embed_file<'b>(&self, note_name: &'a str, context: &'a Context) -> Result<MarkdownTree<'a>> {
|
||||
// TODO: If a #section is specified, reduce returned MarkdownTree to just
|
||||
// that section.
|
||||
let note_name = note_name.split('#').collect::<Vec<&str>>()[0];
|
||||
|
||||
let tree = match lookup_filename_in_vault(note_name, context.vault_contents) {
|
||||
let tree = match lookup_filename_in_vault(note_name, &self.vault_contents.as_ref().unwrap())
|
||||
{
|
||||
Some(path) => {
|
||||
let context = Context::from_parent(context, path);
|
||||
let no_ext = OsString::new();
|
||||
match path.extension().unwrap_or(&no_ext).to_str() {
|
||||
Some("md") => parse_obsidian_note(&path, &context)?,
|
||||
Some("md") => self.parse_obsidian_note(&path, &context)?,
|
||||
Some("png") | Some("jpg") | Some("jpeg") | Some("gif") | Some("webp") => {
|
||||
make_link_to_file(¬e_name, ¬e_name, &context)
|
||||
self.make_link_to_file(¬e_name, ¬e_name, &context)
|
||||
.into_iter()
|
||||
.map(|event| match event {
|
||||
// make_link_to_file returns a link to a file. With this we turn the link
|
||||
@ -381,7 +391,7 @@ fn embed_file<'a, 'b>(note_name: &'a str, context: &'b Context) -> Result<Markdo
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
_ => make_link_to_file(¬e_name, ¬e_name, &context),
|
||||
_ => self.make_link_to_file(¬e_name, ¬e_name, &context),
|
||||
}
|
||||
}
|
||||
None => {
|
||||
@ -395,9 +405,9 @@ fn embed_file<'a, 'b>(note_name: &'a str, context: &'b Context) -> Result<Markdo
|
||||
}
|
||||
};
|
||||
Ok(tree)
|
||||
}
|
||||
}
|
||||
|
||||
fn obsidian_note_link_to_markdown<'a>(content: &'a str, context: &Context) -> MarkdownTree<'a> {
|
||||
fn obsidian_note_link_to_markdown(&self, content: &'a str, context: &Context) -> MarkdownTree {
|
||||
let captures = OBSIDIAN_NOTE_LINK_RE
|
||||
.captures(&content)
|
||||
.expect("note link regex didn't match - bad input?");
|
||||
@ -406,11 +416,16 @@ fn obsidian_note_link_to_markdown<'a>(content: &'a str, context: &Context) -> Ma
|
||||
.expect("Obsidian links should always reference a file");
|
||||
let label = captures.name("label").unwrap_or(notename);
|
||||
|
||||
make_link_to_file(notename.as_str(), label.as_str(), context)
|
||||
}
|
||||
self.make_link_to_file(notename.as_str(), label.as_str(), context)
|
||||
}
|
||||
|
||||
fn make_link_to_file<'a>(file: &'a str, label: &'a str, context: &Context) -> MarkdownTree<'a> {
|
||||
let target_file = lookup_filename_in_vault(file, context.vault_contents);
|
||||
fn make_link_to_file<'b>(
|
||||
&self,
|
||||
file: &'b str,
|
||||
label: &'b str,
|
||||
context: &Context,
|
||||
) -> MarkdownTree<'b> {
|
||||
let target_file = lookup_filename_in_vault(file, &self.vault_contents.as_ref().unwrap());
|
||||
if target_file.is_none() {
|
||||
// TODO: Extract into configurable function.
|
||||
println!(
|
||||
@ -447,6 +462,7 @@ fn make_link_to_file<'a>(file: &'a str, label: &'a str, context: &Context) -> Ma
|
||||
Event::Text(CowStr::from(label)),
|
||||
Event::End(link.clone()),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
fn lookup_filename_in_vault<'a>(
|
||||
|
Loading…
Reference in New Issue
Block a user