Make clippy more strict

This commit is contained in:
Nick Groenen 2024-08-02 18:46:17 +02:00
parent b216ac63aa
commit c8cf91c0c8
No known key found for this signature in database
GPG Key ID: 4F0AD019928AE098
9 changed files with 214 additions and 105 deletions

View File

@ -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"

View File

@ -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()
}

View File

@ -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")
)
);
}
}

View File

@ -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)));
}
}

View File

@ -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)),
},

View File

@ -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),

View File

@ -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)
_ => panic!("Reference exists without file or section!"),
},
ToString::to_string,
);
write!(f, "{label}")
}
}

View File

@ -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 })?;

View File

@ -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
});