Make clippy more strict
This commit is contained in:
parent
b216ac63aa
commit
c8cf91c0c8
78
Cargo.toml
78
Cargo.toml
@ -76,3 +76,81 @@ windows-archive = ".zip"
|
||||
pr-run-mode = "plan"
|
||||
# Publish jobs to run in CI
|
||||
publish-jobs = ["./publish-crate"]
|
||||
|
||||
[lints.rust]
|
||||
nonstandard_style = { level = "warn", priority = -1 }
|
||||
rust_2018_idioms = { level = "warn", priority = -1 }
|
||||
rust_2024_compatibility = { level = "warn", priority = -1 }
|
||||
|
||||
noop_method_call = "warn"
|
||||
redundant-lifetimes = "warn"
|
||||
unsafe_op_in_unsafe_fn = "warn"
|
||||
unused_qualifications = "warn"
|
||||
|
||||
[lints.clippy]
|
||||
pedantic = { level = "warn", priority = -1 }
|
||||
nursery = { level = "warn", priority = -1 }
|
||||
|
||||
# Should probably change these back to warn in the future, but it's a
|
||||
# low-priority issue for me at the moment.
|
||||
missing_errors_doc = "allow"
|
||||
missing_panics_doc = "allow"
|
||||
|
||||
# These lints from the pedantic group are actually too pedantic for my taste:
|
||||
match_bool = "allow"
|
||||
similar_names = "allow"
|
||||
string-add = "allow"
|
||||
|
||||
# Enable select lints from the 'restriction' group (which is not meant to be
|
||||
# enabled as a whole)
|
||||
arithmetic_side_effects = "warn"
|
||||
as_conversions = "warn"
|
||||
assertions_on_result_states = "warn"
|
||||
clone_on_ref_ptr = "warn"
|
||||
dbg_macro = "warn"
|
||||
default_numeric_fallback = "warn"
|
||||
else_if_without_else = "warn"
|
||||
empty_enum_variants_with_brackets = "warn"
|
||||
error_impl_error = "warn"
|
||||
exhaustive_enums = "warn"
|
||||
exhaustive_structs = "warn"
|
||||
filetype_is_file = "warn"
|
||||
float_cmp_const = "warn"
|
||||
fn_to_numeric_cast_any = "warn"
|
||||
if_then_some_else_none = "warn"
|
||||
impl_trait_in_params = "warn"
|
||||
indexing_slicing = "warn"
|
||||
infinite_loop = "warn"
|
||||
integer_division = "warn"
|
||||
large_include_file = "warn"
|
||||
lossy_float_literal = "warn"
|
||||
map_err_ignore = "warn"
|
||||
mem_forget = "warn"
|
||||
multiple_inherent_impl = "warn"
|
||||
multiple_unsafe_ops_per_block = "warn"
|
||||
panic_in_result_fn = "warn"
|
||||
rc_buffer = "warn"
|
||||
rc_mutex = "warn"
|
||||
redundant_type_annotations = "warn"
|
||||
same_name_method = "warn"
|
||||
self_named_module_files = "warn"
|
||||
shadow_unrelated = "warn"
|
||||
str_to_string = "warn"
|
||||
string_add = "warn"
|
||||
string_slice = "warn"
|
||||
string_to_string = "warn"
|
||||
suspicious_xor_used_as_pow = "warn"
|
||||
todo = "warn"
|
||||
try_err = "warn"
|
||||
undocumented_unsafe_blocks = "warn"
|
||||
unneeded_field_pattern = "warn"
|
||||
unseparated_literal_suffix = "warn"
|
||||
vec_init_then_push = "warn"
|
||||
#expect_used = "warn"
|
||||
#missing_docs_in_private_items = "warn"
|
||||
#missing_inline_in_public_items = "warn"
|
||||
#pathbuf_init_then_push = "warn" # Rust 1.81.0+
|
||||
#renamed_function_params = "warn" # Rust 1.80.0+
|
||||
#unwrap_in_result = "warn"
|
||||
#unwrap_used = "warn"
|
||||
#wildcard_enum_match_arm = "warn"
|
||||
|
@ -21,7 +21,7 @@ pub struct Context {
|
||||
pub destination: PathBuf,
|
||||
|
||||
/// The [Frontmatter] for this note. Frontmatter may be modified in-place (see
|
||||
/// [serde_yaml::Mapping] for available methods) or replaced entirely.
|
||||
/// [`serde_yaml::Mapping`] for available methods) or replaced entirely.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
@ -46,8 +46,10 @@ pub struct Context {
|
||||
|
||||
impl Context {
|
||||
/// Create a new `Context`
|
||||
pub fn new(src: PathBuf, dest: PathBuf) -> Context {
|
||||
Context {
|
||||
#[inline]
|
||||
#[must_use]
|
||||
pub fn new(src: PathBuf, dest: PathBuf) -> Self {
|
||||
Self {
|
||||
file_tree: vec![src],
|
||||
destination: dest,
|
||||
frontmatter: Frontmatter::new(),
|
||||
@ -55,13 +57,17 @@ impl Context {
|
||||
}
|
||||
|
||||
/// Create a new `Context` which inherits from a parent Context.
|
||||
pub fn from_parent(context: &Context, child: &Path) -> Context {
|
||||
#[inline]
|
||||
#[must_use]
|
||||
pub fn from_parent(context: &Self, child: &Path) -> Self {
|
||||
let mut context = context.clone();
|
||||
context.file_tree.push(child.to_path_buf());
|
||||
context
|
||||
}
|
||||
|
||||
/// Return the path of the file currently being parsed.
|
||||
#[inline]
|
||||
#[must_use]
|
||||
pub fn current_file(&self) -> &PathBuf {
|
||||
self.file_tree
|
||||
.last()
|
||||
@ -72,6 +78,8 @@ impl Context {
|
||||
///
|
||||
/// Typically this will yield the same element as `current_file`, but when a note is embedded
|
||||
/// within another note, this will return the outer-most note.
|
||||
#[inline]
|
||||
#[must_use]
|
||||
pub fn root_file(&self) -> &PathBuf {
|
||||
self.file_tree
|
||||
.first()
|
||||
@ -79,6 +87,8 @@ impl Context {
|
||||
}
|
||||
|
||||
/// Return the note depth (nesting level) for this context.
|
||||
#[inline]
|
||||
#[must_use]
|
||||
pub fn note_depth(&self) -> usize {
|
||||
self.file_tree.len()
|
||||
}
|
||||
@ -87,6 +97,8 @@ impl Context {
|
||||
///
|
||||
/// The first element corresponds to the root file, the final element corresponds to the file
|
||||
/// which is currently being processed (see also `current_file`).
|
||||
#[inline]
|
||||
#[must_use]
|
||||
pub fn file_tree(&self) -> Vec<PathBuf> {
|
||||
self.file_tree.clone()
|
||||
}
|
||||
|
@ -2,7 +2,7 @@ use serde_yaml::Result;
|
||||
|
||||
/// YAML front matter from an Obsidian note.
|
||||
///
|
||||
/// This is essentially an alias of [serde_yaml::Mapping] so all the methods available on that type
|
||||
/// This is essentially an alias of [`serde_yaml::Mapping`] so all the methods available on that type
|
||||
/// are available with `Frontmatter` as well.
|
||||
///
|
||||
/// # Examples
|
||||
@ -26,6 +26,8 @@ use serde_yaml::Result;
|
||||
/// ```
|
||||
pub type Frontmatter = serde_yaml::Mapping;
|
||||
|
||||
// Would be nice to rename this to just from_str, but that would be a breaking change.
|
||||
#[allow(clippy::module_name_repetitions)]
|
||||
pub fn frontmatter_from_str(mut s: &str) -> Result<Frontmatter> {
|
||||
if s.is_empty() {
|
||||
s = "{}";
|
||||
@ -34,9 +36,11 @@ pub fn frontmatter_from_str(mut s: &str) -> Result<Frontmatter> {
|
||||
Ok(frontmatter)
|
||||
}
|
||||
|
||||
pub fn frontmatter_to_str(frontmatter: Frontmatter) -> Result<String> {
|
||||
// Would be nice to rename this to just to_str, but that would be a breaking change.
|
||||
#[allow(clippy::module_name_repetitions)]
|
||||
pub fn frontmatter_to_str(frontmatter: &Frontmatter) -> Result<String> {
|
||||
if frontmatter.is_empty() {
|
||||
return Ok("---\n---\n".to_string());
|
||||
return Ok("---\n---\n".to_owned());
|
||||
}
|
||||
|
||||
let mut buffer = String::new();
|
||||
@ -46,8 +50,11 @@ pub fn frontmatter_to_str(frontmatter: Frontmatter) -> Result<String> {
|
||||
Ok(buffer)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
/// Available strategies for the inclusion of frontmatter in notes.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
// Would be nice to rename this to just Strategy, but that would be a breaking change.
|
||||
#[allow(clippy::module_name_repetitions)]
|
||||
#[non_exhaustive]
|
||||
pub enum FrontmatterStrategy {
|
||||
/// Copy frontmatter when a note has frontmatter defined.
|
||||
Auto,
|
||||
@ -66,16 +73,16 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn empty_string_should_yield_empty_frontmatter() {
|
||||
assert_eq!(frontmatter_from_str("").unwrap(), Frontmatter::new())
|
||||
assert_eq!(frontmatter_from_str("").unwrap(), Frontmatter::new());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_frontmatter_to_str() {
|
||||
let frontmatter = Frontmatter::new();
|
||||
assert_eq!(
|
||||
frontmatter_to_str(frontmatter).unwrap(),
|
||||
frontmatter_to_str(&frontmatter).unwrap(),
|
||||
format!("---\n---\n")
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -86,8 +93,8 @@ mod tests {
|
||||
Value::String("bar".to_string()),
|
||||
);
|
||||
assert_eq!(
|
||||
frontmatter_to_str(frontmatter).unwrap(),
|
||||
frontmatter_to_str(&frontmatter).unwrap(),
|
||||
format!("---\nfoo: bar\n---\n")
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
116
src/lib.rs
116
src/lib.rs
@ -1,5 +1,5 @@
|
||||
pub extern crate pulldown_cmark;
|
||||
pub extern crate serde_yaml;
|
||||
pub use pulldown_cmark;
|
||||
pub use serde_yaml;
|
||||
|
||||
#[macro_use]
|
||||
extern crate lazy_static;
|
||||
@ -20,7 +20,7 @@ use percent_encoding::{utf8_percent_encode, AsciiSet, CONTROLS};
|
||||
use pulldown_cmark::{CodeBlockKind, CowStr, Event, HeadingLevel, Options, Parser, Tag};
|
||||
use pulldown_cmark_to_cmark::cmark_with_options;
|
||||
use rayon::prelude::*;
|
||||
use references::*;
|
||||
use references::{ObsidianNoteReference, RefParser, RefParserState, RefType};
|
||||
use slug::slugify;
|
||||
use snafu::{ResultExt, Snafu};
|
||||
use std::ffi::OsString;
|
||||
@ -38,14 +38,14 @@ pub type MarkdownEvents<'a> = Vec<Event<'a>>;
|
||||
/// A post-processing function that is to be called after an Obsidian note has been fully parsed and
|
||||
/// converted to regular markdown syntax.
|
||||
///
|
||||
/// Postprocessors are called in the order they've been added through [Exporter::add_postprocessor]
|
||||
/// Postprocessors are called in the order they've been added through [`Exporter::add_postprocessor`]
|
||||
/// just before notes are written out to their final destination.
|
||||
/// They may be used to achieve the following:
|
||||
///
|
||||
/// 1. Modify a note's [Context], for example to change the destination filename or update its [Frontmatter] (see [Context::frontmatter]).
|
||||
/// 2. Change a note's contents by altering [MarkdownEvents].
|
||||
/// 3. Prevent later postprocessors from running ([PostprocessorResult::StopHere]) or cause a note
|
||||
/// to be skipped entirely ([PostprocessorResult::StopAndSkipNote]).
|
||||
/// 1. Modify a note's [Context], for example to change the destination filename or update its [Frontmatter] (see [`Context::frontmatter`]).
|
||||
/// 2. Change a note's contents by altering [`MarkdownEvents`].
|
||||
/// 3. Prevent later postprocessors from running ([`PostprocessorResult::StopHere`]) or cause a note
|
||||
/// to be skipped entirely ([`PostprocessorResult::StopAndSkipNote`]).
|
||||
///
|
||||
/// # Postprocessors and embeds
|
||||
///
|
||||
@ -54,17 +54,17 @@ pub type MarkdownEvents<'a> = Vec<Event<'a>>;
|
||||
///
|
||||
/// In some cases it may be desirable to change the contents of these embedded notes *before* they
|
||||
/// are inserted into the final document. This is possible through the use of
|
||||
/// [Exporter::add_embed_postprocessor].
|
||||
/// [`Exporter::add_embed_postprocessor`].
|
||||
/// These "embed postprocessors" run much the same way as regular postprocessors, but they're run on
|
||||
/// the note that is about to be embedded in another note. In addition:
|
||||
///
|
||||
/// - Changes to context carry over to later embed postprocessors, but are then discarded. This
|
||||
/// means that changes to frontmatter do not propagate to the root note for example.
|
||||
/// - [PostprocessorResult::StopAndSkipNote] prevents the embedded note from being included (it's
|
||||
/// - [`PostprocessorResult::StopAndSkipNote`] prevents the embedded note from being included (it's
|
||||
/// replaced with a blank document) but doesn't affect the root note.
|
||||
///
|
||||
/// It's possible to pass the same functions to [Exporter::add_postprocessor] and
|
||||
/// [Exporter::add_embed_postprocessor]. The [Context::note_depth] method may be used to determine
|
||||
/// It's possible to pass the same functions to [`Exporter::add_postprocessor`] and
|
||||
/// [`Exporter::add_embed_postprocessor`]. The [`Context::note_depth`] method may be used to determine
|
||||
/// whether a note is a root note or an embedded note in this situation.
|
||||
///
|
||||
/// # Examples
|
||||
@ -104,7 +104,7 @@ pub type MarkdownEvents<'a> = Vec<Event<'a>>;
|
||||
///
|
||||
/// ## Change note contents
|
||||
///
|
||||
/// In this example a note's markdown content is changed by iterating over the [MarkdownEvents] and
|
||||
/// In this example a note's markdown content is changed by iterating over the [`MarkdownEvents`] and
|
||||
/// changing the text when we encounter a [text element][Event::Text].
|
||||
///
|
||||
/// Instead of using a closure like above, this example shows how to use a separate function
|
||||
@ -132,9 +132,8 @@ pub type MarkdownEvents<'a> = Vec<Event<'a>>;
|
||||
/// exporter.add_postprocessor(&foo_to_bar);
|
||||
/// # exporter.run().unwrap();
|
||||
/// ```
|
||||
|
||||
pub type Postprocessor<'f> =
|
||||
dyn Fn(&mut Context, &mut MarkdownEvents) -> PostprocessorResult + Send + Sync + 'f;
|
||||
dyn Fn(&mut Context, &mut MarkdownEvents<'_>) -> PostprocessorResult + Send + Sync + 'f;
|
||||
type Result<T, E = ExportError> = std::result::Result<T, E>;
|
||||
|
||||
const PERCENTENCODE_CHARS: &AsciiSet = &CONTROLS.add(b' ').add(b'(').add(b')').add(b'%').add(b'?');
|
||||
@ -142,7 +141,7 @@ const NOTE_RECURSION_LIMIT: usize = 10;
|
||||
|
||||
#[non_exhaustive]
|
||||
#[derive(Debug, Snafu)]
|
||||
/// ExportError represents all errors which may be returned when using this crate.
|
||||
/// `ExportError` represents all errors which may be returned when using this crate.
|
||||
pub enum ExportError {
|
||||
#[snafu(display("failed to read from '{}'", path.display()))]
|
||||
/// This occurs when a read IO operation fails.
|
||||
@ -205,8 +204,9 @@ pub enum ExportError {
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
/// Emitted by [Postprocessor]s to signal the next action to take.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[non_exhaustive]
|
||||
pub enum PostprocessorResult {
|
||||
/// Continue with the next post-processor (if any).
|
||||
Continue,
|
||||
@ -236,7 +236,7 @@ pub struct Exporter<'a> {
|
||||
}
|
||||
|
||||
impl<'a> fmt::Debug for Exporter<'a> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("WalkOptions")
|
||||
.field("root", &self.root)
|
||||
.field("destination", &self.destination)
|
||||
@ -265,8 +265,9 @@ impl<'a> fmt::Debug for Exporter<'a> {
|
||||
impl<'a> Exporter<'a> {
|
||||
/// Create a new exporter which reads notes from `root` and exports these to
|
||||
/// `destination`.
|
||||
pub fn new(root: PathBuf, destination: PathBuf) -> Exporter<'a> {
|
||||
Exporter {
|
||||
#[must_use]
|
||||
pub fn new(root: PathBuf, destination: PathBuf) -> Self {
|
||||
Self {
|
||||
start_at: root.clone(),
|
||||
root,
|
||||
destination,
|
||||
@ -283,19 +284,19 @@ impl<'a> Exporter<'a> {
|
||||
///
|
||||
/// 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> {
|
||||
pub fn start_at(&mut self, start_at: PathBuf) -> &mut Self {
|
||||
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> {
|
||||
pub fn walk_options(&mut self, options: WalkOptions<'a>) -> &mut Self {
|
||||
self.walk_options = options;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the [`FrontmatterStrategy`] to be used for this exporter.
|
||||
pub fn frontmatter_strategy(&mut self, strategy: FrontmatterStrategy) -> &mut Exporter<'a> {
|
||||
pub fn frontmatter_strategy(&mut self, strategy: FrontmatterStrategy) -> &mut Self {
|
||||
self.frontmatter_strategy = strategy;
|
||||
self
|
||||
}
|
||||
@ -304,23 +305,23 @@ impl<'a> Exporter<'a> {
|
||||
///
|
||||
/// When `recursive` is true (the default), emdeds are always processed recursively. This may
|
||||
/// lead to infinite recursion when note A embeds B, but B also embeds A.
|
||||
/// (When this happens, [ExportError::RecursionLimitExceeded] will be returned by [Exporter::run]).
|
||||
/// (When this happens, [`ExportError::RecursionLimitExceeded`] will be returned by [`Exporter::run`]).
|
||||
///
|
||||
/// When `recursive` is false, if a note is encountered for a second time while processing the
|
||||
/// original note, instead of embedding it again a link to the note is inserted instead.
|
||||
pub fn process_embeds_recursively(&mut self, recursive: bool) -> &mut Exporter<'a> {
|
||||
pub fn process_embeds_recursively(&mut self, recursive: bool) -> &mut Self {
|
||||
self.process_embeds_recursively = recursive;
|
||||
self
|
||||
}
|
||||
|
||||
/// Append a function to the chain of [postprocessors][Postprocessor] to run on exported Obsidian Markdown notes.
|
||||
pub fn add_postprocessor(&mut self, processor: &'a Postprocessor) -> &mut Exporter<'a> {
|
||||
pub fn add_postprocessor(&mut self, processor: &'a Postprocessor<'_>) -> &mut Self {
|
||||
self.postprocessors.push(processor);
|
||||
self
|
||||
}
|
||||
|
||||
/// Append a function to the chain of [postprocessors][Postprocessor] for embeds.
|
||||
pub fn add_embed_postprocessor(&mut self, processor: &'a Postprocessor) -> &mut Exporter<'a> {
|
||||
pub fn add_embed_postprocessor(&mut self, processor: &'a Postprocessor<'_>) -> &mut Self {
|
||||
self.embed_postprocessors.push(processor);
|
||||
self
|
||||
}
|
||||
@ -408,27 +409,32 @@ impl<'a> Exporter<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
let dest = context.destination;
|
||||
let mut outfile = create_file(&dest)?;
|
||||
let mut outfile = create_file(&context.destination)?;
|
||||
let write_frontmatter = match self.frontmatter_strategy {
|
||||
FrontmatterStrategy::Always => true,
|
||||
FrontmatterStrategy::Never => false,
|
||||
FrontmatterStrategy::Auto => !context.frontmatter.is_empty(),
|
||||
};
|
||||
if write_frontmatter {
|
||||
let mut frontmatter_str = frontmatter_to_str(context.frontmatter)
|
||||
let mut frontmatter_str = frontmatter_to_str(&context.frontmatter)
|
||||
.context(FrontMatterEncodeSnafu { path: src })?;
|
||||
frontmatter_str.push('\n');
|
||||
outfile
|
||||
.write_all(frontmatter_str.as_bytes())
|
||||
.context(WriteSnafu { path: &dest })?;
|
||||
.context(WriteSnafu {
|
||||
path: &context.destination,
|
||||
})?;
|
||||
}
|
||||
outfile
|
||||
.write_all(render_mdevents_to_mdtext(markdown_events).as_bytes())
|
||||
.context(WriteSnafu { path: &dest })?;
|
||||
.write_all(render_mdevents_to_mdtext(&markdown_events).as_bytes())
|
||||
.context(WriteSnafu {
|
||||
path: &context.destination,
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_lines)]
|
||||
#[allow(clippy::panic_in_result_fn)]
|
||||
fn parse_obsidian_note<'b>(
|
||||
&self,
|
||||
path: &Path,
|
||||
@ -441,7 +447,7 @@ impl<'a> Exporter<'a> {
|
||||
}
|
||||
let content = fs::read_to_string(path).context(ReadSnafu { path })?;
|
||||
let (frontmatter, content) =
|
||||
matter::matter(&content).unwrap_or(("".to_string(), content.to_string()));
|
||||
matter::matter(&content).unwrap_or((String::new(), content.clone()));
|
||||
let frontmatter =
|
||||
frontmatter_from_str(&frontmatter).context(FrontMatterDecodeSnafu { path })?;
|
||||
|
||||
@ -616,7 +622,7 @@ impl<'a> Exporter<'a> {
|
||||
}
|
||||
events
|
||||
}
|
||||
Some("png") | Some("jpg") | Some("jpeg") | Some("gif") | Some("webp") | Some("svg") => {
|
||||
Some("png" | "jpg" | "jpeg" | "gif" | "webp" | "svg") => {
|
||||
self.make_link_to_file(note_ref, &child_context)
|
||||
.into_iter()
|
||||
.map(|event| match event {
|
||||
@ -652,10 +658,10 @@ impl<'a> Exporter<'a> {
|
||||
reference: ObsidianNoteReference<'_>,
|
||||
context: &Context,
|
||||
) -> MarkdownEvents<'c> {
|
||||
let target_file = reference
|
||||
.file
|
||||
.map(|file| lookup_filename_in_vault(file, self.vault_contents.as_ref().unwrap()))
|
||||
.unwrap_or_else(|| Some(context.current_file()));
|
||||
let target_file = reference.file.map_or_else(
|
||||
|| Some(context.current_file()),
|
||||
|file| lookup_filename_in_vault(file, self.vault_contents.as_ref().unwrap()),
|
||||
);
|
||||
|
||||
if target_file.is_none() {
|
||||
// TODO: Extract into configurable function.
|
||||
@ -693,7 +699,7 @@ impl<'a> Exporter<'a> {
|
||||
link.push_str(&slugify(section));
|
||||
}
|
||||
|
||||
let link_tag = pulldown_cmark::Tag::Link(
|
||||
let link_tag = Tag::Link(
|
||||
pulldown_cmark::LinkType::Inline,
|
||||
CowStr::from(link),
|
||||
CowStr::from(""),
|
||||
@ -707,13 +713,13 @@ impl<'a> Exporter<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the full path for the given filename when it's contained in vault_contents, taking into
|
||||
/// Get the full path for the given filename when it's contained in `vault_contents`, taking into
|
||||
/// account:
|
||||
///
|
||||
/// 1. Standard Obsidian note references not including a .md extension.
|
||||
/// 2. Case-insensitive matching
|
||||
/// 3. Unicode normalization rules using normalization form C
|
||||
/// (https://www.w3.org/TR/charmod-norm/#unicodeNormalization)
|
||||
/// (<https://www.w3.org/TR/charmod-norm/#unicodeNormalization>)
|
||||
fn lookup_filename_in_vault<'a>(
|
||||
filename: &str,
|
||||
vault_contents: &'a [PathBuf],
|
||||
@ -737,7 +743,7 @@ fn lookup_filename_in_vault<'a>(
|
||||
})
|
||||
}
|
||||
|
||||
fn render_mdevents_to_mdtext(markdown: MarkdownEvents) -> String {
|
||||
fn render_mdevents_to_mdtext(markdown: &MarkdownEvents<'_>) -> String {
|
||||
let mut buffer = String::new();
|
||||
cmark_with_options(
|
||||
markdown.iter(),
|
||||
@ -754,7 +760,7 @@ fn create_file(dest: &Path) -> Result<File> {
|
||||
.or_else(|err| {
|
||||
if err.kind() == ErrorKind::NotFound {
|
||||
let parent = dest.parent().expect("file should have a parent directory");
|
||||
std::fs::create_dir_all(parent)?
|
||||
fs::create_dir_all(parent)?;
|
||||
}
|
||||
File::create(dest)
|
||||
})
|
||||
@ -763,13 +769,13 @@ fn create_file(dest: &Path) -> Result<File> {
|
||||
}
|
||||
|
||||
fn copy_file(src: &Path, dest: &Path) -> Result<()> {
|
||||
std::fs::copy(src, dest)
|
||||
fs::copy(src, dest)
|
||||
.or_else(|err| {
|
||||
if err.kind() == ErrorKind::NotFound {
|
||||
let parent = dest.parent().expect("file should have a parent directory");
|
||||
std::fs::create_dir_all(parent)?
|
||||
fs::create_dir_all(parent)?;
|
||||
}
|
||||
std::fs::copy(src, dest)
|
||||
fs::copy(src, dest)
|
||||
})
|
||||
.context(WriteSnafu { path: dest })?;
|
||||
Ok(())
|
||||
@ -791,7 +797,7 @@ fn reduce_to_section<'a>(events: MarkdownEvents<'a>, section: &str) -> MarkdownE
|
||||
let mut last_level = HeadingLevel::H1;
|
||||
let mut last_tag_was_heading = false;
|
||||
|
||||
for event in events.into_iter() {
|
||||
for event in events {
|
||||
filtered_events.push(event.clone());
|
||||
match event {
|
||||
// FIXME: This should propagate fragment_identifier and classes.
|
||||
@ -831,7 +837,7 @@ fn reduce_to_section<'a>(events: MarkdownEvents<'a>, section: &str) -> MarkdownE
|
||||
filtered_events
|
||||
}
|
||||
|
||||
fn event_to_owned<'a>(event: Event) -> Event<'a> {
|
||||
fn event_to_owned<'a>(event: Event<'_>) -> Event<'a> {
|
||||
match event {
|
||||
Event::Start(tag) => Event::Start(tag_to_owned(tag)),
|
||||
Event::End(tag) => Event::End(tag_to_owned(tag)),
|
||||
@ -848,7 +854,7 @@ fn event_to_owned<'a>(event: Event) -> Event<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
fn tag_to_owned<'a>(tag: Tag) -> Tag<'a> {
|
||||
fn tag_to_owned<'a>(tag: Tag<'_>) -> Tag<'a> {
|
||||
match tag {
|
||||
Tag::Paragraph => Tag::Paragraph,
|
||||
Tag::Heading(level, _fragment_identifier, _classes) => {
|
||||
@ -882,7 +888,7 @@ fn tag_to_owned<'a>(tag: Tag) -> Tag<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
fn codeblock_kind_to_owned<'a>(codeblock_kind: CodeBlockKind) -> CodeBlockKind<'a> {
|
||||
fn codeblock_kind_to_owned<'a>(codeblock_kind: CodeBlockKind<'_>) -> CodeBlockKind<'a> {
|
||||
match codeblock_kind {
|
||||
CodeBlockKind::Indented => CodeBlockKind::Indented,
|
||||
CodeBlockKind::Fenced(cowstr) => CodeBlockKind::Fenced(CowStr::from(cowstr.into_string())),
|
||||
@ -896,7 +902,7 @@ mod tests {
|
||||
use rstest::rstest;
|
||||
|
||||
lazy_static! {
|
||||
static ref VAULT: Vec<std::path::PathBuf> = vec![
|
||||
static ref VAULT: Vec<PathBuf> = vec![
|
||||
PathBuf::from("NoteA.md"),
|
||||
PathBuf::from("Document.pdf"),
|
||||
PathBuf::from("Note.1.md"),
|
||||
@ -956,9 +962,9 @@ mod tests {
|
||||
#[case("Note\u{41}\u{308}", "Note\u{E4}.md")]
|
||||
fn test_lookup_filename_in_vault(#[case] input: &str, #[case] expected: &str) {
|
||||
let result = lookup_filename_in_vault(input, &VAULT);
|
||||
println!("Test input: {:?}", input);
|
||||
println!("Expecting: {:?}", expected);
|
||||
println!("Test input: {input:?}");
|
||||
println!("Expecting: {expected:?}");
|
||||
println!("Got: {:?}", result.unwrap_or(&PathBuf::from("")));
|
||||
assert_eq!(result, Some(&PathBuf::from(expected)))
|
||||
assert_eq!(result, Some(&PathBuf::from(expected)));
|
||||
}
|
||||
}
|
||||
|
12
src/main.rs
12
src/main.rs
@ -1,12 +1,13 @@
|
||||
use eyre::{eyre, Result};
|
||||
use gumdrop::Options;
|
||||
use obsidian_export::{postprocessors::*, ExportError};
|
||||
use obsidian_export::{Exporter, FrontmatterStrategy, WalkOptions};
|
||||
use obsidian_export::postprocessors::{filter_by_tags, softbreaks_to_hardbreaks};
|
||||
use obsidian_export::{ExportError, Exporter, FrontmatterStrategy, WalkOptions};
|
||||
use std::{env, path::PathBuf};
|
||||
|
||||
const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
|
||||
#[derive(Debug, Options)]
|
||||
#[allow(clippy::struct_excessive_bools)]
|
||||
struct Opts {
|
||||
#[options(help = "Display program help")]
|
||||
help: bool,
|
||||
@ -76,7 +77,7 @@ fn main() {
|
||||
// version flag was specified. Without this, "missing required free argument" would get printed
|
||||
// when no other args are specified.
|
||||
if env::args().any(|arg| arg == "-v" || arg == "--version") {
|
||||
println!("obsidian-export {}", VERSION);
|
||||
println!("obsidian-export {VERSION}");
|
||||
std::process::exit(0);
|
||||
}
|
||||
|
||||
@ -107,6 +108,9 @@ fn main() {
|
||||
exporter.start_at(path);
|
||||
}
|
||||
|
||||
#[allow(clippy::pattern_type_mismatch)]
|
||||
#[allow(clippy::ref_patterns)]
|
||||
#[allow(clippy::shadow_unrelated)]
|
||||
if let Err(err) = exporter.run() {
|
||||
match err {
|
||||
ExportError::FileExportError {
|
||||
@ -128,7 +132,7 @@ fn main() {
|
||||
for (idx, path) in file_tree.iter().enumerate() {
|
||||
eprintln!(" {}-> {}", " ".repeat(idx), path.display());
|
||||
}
|
||||
eprintln!("\nHint: Ensure notes are non-recursive, or specify --no-recursive-embeds to break cycles")
|
||||
eprintln!("\nHint: Ensure notes are non-recursive, or specify --no-recursive-embeds to break cycles");
|
||||
}
|
||||
_ => eprintln!("Error: {:?}", eyre!(err)),
|
||||
},
|
||||
|
@ -8,7 +8,7 @@ use serde_yaml::Value;
|
||||
/// Obsidian's _'Strict line breaks'_ setting.
|
||||
pub fn softbreaks_to_hardbreaks(
|
||||
_context: &mut Context,
|
||||
events: &mut MarkdownEvents,
|
||||
events: &mut MarkdownEvents<'_>,
|
||||
) -> PostprocessorResult {
|
||||
for event in events.iter_mut() {
|
||||
if event == &Event::SoftBreak {
|
||||
@ -21,8 +21,8 @@ pub fn softbreaks_to_hardbreaks(
|
||||
pub fn filter_by_tags(
|
||||
skip_tags: Vec<String>,
|
||||
only_tags: Vec<String>,
|
||||
) -> impl Fn(&mut Context, &mut MarkdownEvents) -> PostprocessorResult {
|
||||
move |context: &mut Context, _events: &mut MarkdownEvents| -> PostprocessorResult {
|
||||
) -> impl Fn(&mut Context, &mut MarkdownEvents<'_>) -> PostprocessorResult {
|
||||
move |context: &mut Context, _events: &mut MarkdownEvents<'_>| -> PostprocessorResult {
|
||||
match context.frontmatter.get("tags") {
|
||||
None => filter_by_tags_(&[], &skip_tags, &only_tags),
|
||||
Some(Value::Sequence(tags)) => filter_by_tags_(tags, &skip_tags, &only_tags),
|
||||
|
@ -6,8 +6,8 @@ lazy_static! {
|
||||
Regex::new(r"^(?P<file>[^#|]+)??(#(?P<section>.+?))??(\|(?P<label>.+?))??$").unwrap();
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
/// ObsidianNoteReference represents the structure of a `[[note]]` or `![[embed]]` reference.
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||
/// `ObsidianNoteReference` represents the structure of a `[[note]]` or `![[embed]]` reference.
|
||||
pub struct ObsidianNoteReference<'a> {
|
||||
/// The file (note name or partial path) being referenced.
|
||||
/// This will be None in the case that the reference is to a section within the same document
|
||||
@ -19,7 +19,7 @@ pub struct ObsidianNoteReference<'a> {
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq)]
|
||||
/// RefParserState enumerates all the possible parsing states [RefParser] may enter.
|
||||
/// `RefParserState` enumerates all the possible parsing states [`RefParser`] may enter.
|
||||
pub enum RefParserState {
|
||||
NoState,
|
||||
ExpectSecondOpenBracket,
|
||||
@ -29,13 +29,13 @@ pub enum RefParserState {
|
||||
Resetting,
|
||||
}
|
||||
|
||||
/// RefType indicates whether a note reference is a link (`[[note]]`) or embed (`![[embed]]`).
|
||||
/// `RefType` indicates whether a note reference is a link (`[[note]]`) or embed (`![[embed]]`).
|
||||
pub enum RefType {
|
||||
Link,
|
||||
Embed,
|
||||
}
|
||||
|
||||
/// RefParser holds state which is used to parse Obsidian WikiLinks (`[[note]]`, `![[embed]]`).
|
||||
/// `RefParser` holds state which is used to parse Obsidian `WikiLinks` (`[[note]]`, `![[embed]]`).
|
||||
pub struct RefParser {
|
||||
pub state: RefParserState,
|
||||
pub ref_type: Option<RefType>,
|
||||
@ -49,8 +49,8 @@ pub struct RefParser {
|
||||
}
|
||||
|
||||
impl RefParser {
|
||||
pub fn new() -> RefParser {
|
||||
RefParser {
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
state: RefParserState::NoState,
|
||||
ref_type: None,
|
||||
ref_text: String::new(),
|
||||
@ -69,7 +69,7 @@ impl RefParser {
|
||||
}
|
||||
|
||||
impl<'a> ObsidianNoteReference<'a> {
|
||||
pub fn from_str(text: &str) -> ObsidianNoteReference {
|
||||
pub fn from_str(text: &str) -> ObsidianNoteReference<'_> {
|
||||
let captures = OBSIDIAN_NOTE_LINK_RE
|
||||
.captures(text)
|
||||
.expect("note link regex didn't match - bad input?");
|
||||
@ -85,23 +85,23 @@ impl<'a> ObsidianNoteReference<'a> {
|
||||
}
|
||||
|
||||
pub fn display(&self) -> String {
|
||||
format!("{}", self)
|
||||
format!("{self}")
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> fmt::Display for ObsidianNoteReference<'a> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let label =
|
||||
self.label
|
||||
.map(|v| v.to_string())
|
||||
.unwrap_or_else(|| match (self.file, self.section) {
|
||||
(Some(file), Some(section)) => format!("{} > {}", file, section),
|
||||
(Some(file), None) => file.to_string(),
|
||||
(None, Some(section)) => section.to_string(),
|
||||
let label = self.label.map_or_else(
|
||||
|| match (self.file, self.section) {
|
||||
(Some(file), Some(section)) => format!("{file} > {section}"),
|
||||
(Some(file), None) => file.to_owned(),
|
||||
(None, Some(section)) => section.to_owned(),
|
||||
|
||||
_ => panic!("Reference exists without file or section!"),
|
||||
});
|
||||
write!(f, "{}", label)
|
||||
},
|
||||
ToString::to_string,
|
||||
);
|
||||
write!(f, "{label}")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -7,8 +7,9 @@ use std::path::{Path, PathBuf};
|
||||
type Result<T, E = ExportError> = std::result::Result<T, E>;
|
||||
type FilterFn = dyn Fn(&DirEntry) -> bool + Send + Sync + 'static;
|
||||
|
||||
/// `WalkOptions` specifies how an Obsidian vault directory is scanned for eligible files to export.
|
||||
#[derive(Clone)]
|
||||
/// WalkOptions specifies how an Obsidian vault directory is scanned for eligible files to export.
|
||||
#[allow(clippy::exhaustive_structs)]
|
||||
pub struct WalkOptions<'a> {
|
||||
/// The filename for ignore files, following the
|
||||
/// [gitignore](https://git-scm.com/docs/gitignore) syntax.
|
||||
@ -32,7 +33,7 @@ pub struct WalkOptions<'a> {
|
||||
}
|
||||
|
||||
impl<'a> fmt::Debug for WalkOptions<'a> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let filter_fn_fmt = match self.filter_fn {
|
||||
Some(_) => "<function set>",
|
||||
None => "<not set>",
|
||||
@ -48,7 +49,8 @@ impl<'a> fmt::Debug for WalkOptions<'a> {
|
||||
|
||||
impl<'a> WalkOptions<'a> {
|
||||
/// Create a new set of options using default values.
|
||||
pub fn new() -> WalkOptions<'a> {
|
||||
#[must_use]
|
||||
pub fn new() -> Self {
|
||||
WalkOptions {
|
||||
ignore_filename: ".export-ignore",
|
||||
ignore_hidden: true,
|
||||
@ -83,12 +85,12 @@ impl<'a> Default for WalkOptions<'a> {
|
||||
}
|
||||
|
||||
/// `vault_contents` returns all of the files in an Obsidian vault located at `path` which would be
|
||||
/// exported when using the given [WalkOptions].
|
||||
pub fn vault_contents(path: &Path, opts: WalkOptions) -> Result<Vec<PathBuf>> {
|
||||
/// exported when using the given [`WalkOptions`].
|
||||
pub fn vault_contents(root: &Path, opts: WalkOptions<'_>) -> Result<Vec<PathBuf>> {
|
||||
let mut contents = Vec::new();
|
||||
let walker = opts.build_walker(path);
|
||||
let walker = opts.build_walker(root);
|
||||
for entry in walker {
|
||||
let entry = entry.context(WalkDirSnafu { path })?;
|
||||
let entry = entry.context(WalkDirSnafu { path: root })?;
|
||||
let path = entry.path();
|
||||
let metadata = entry.metadata().context(WalkDirSnafu { path })?;
|
||||
|
||||
|
@ -14,7 +14,7 @@ use walkdir::WalkDir;
|
||||
fn foo_to_bar(_ctx: &mut Context, events: &mut MarkdownEvents) -> PostprocessorResult {
|
||||
for event in events.iter_mut() {
|
||||
if let Event::Text(text) = event {
|
||||
*event = Event::Text(CowStr::from(text.replace("foo", "bar")))
|
||||
*event = Event::Text(CowStr::from(text.replace("foo", "bar")));
|
||||
}
|
||||
}
|
||||
PostprocessorResult::Continue
|
||||
@ -135,7 +135,7 @@ fn test_postprocessor_stateful_callback() {
|
||||
let expected = tmp_dir.path();
|
||||
|
||||
let parents = parents.lock().unwrap();
|
||||
println!("{:?}", parents);
|
||||
println!("{parents:?}");
|
||||
assert_eq!(1, parents.len());
|
||||
assert!(parents.contains(expected));
|
||||
}
|
||||
@ -209,7 +209,7 @@ fn test_embed_postprocessors_context() {
|
||||
panic!(
|
||||
"postprocessor: expected is_root_note in {} to be true, got false",
|
||||
&ctx.current_file().display()
|
||||
)
|
||||
);
|
||||
}
|
||||
PostprocessorResult::Continue
|
||||
});
|
||||
@ -225,7 +225,7 @@ fn test_embed_postprocessors_context() {
|
||||
panic!(
|
||||
"embed_postprocessor: expected is_root_note in {} to be false, got true",
|
||||
&ctx.current_file().display()
|
||||
)
|
||||
);
|
||||
}
|
||||
PostprocessorResult::Continue
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user