Use nightly rust for formatting to group import statements

This commit is contained in:
Nick Groenen 2024-08-03 13:38:23 +02:00
parent bd1220028b
commit da9238a78c
No known key found for this signature in database
GPG Key ID: 4F0AD019928AE098
13 changed files with 103 additions and 83 deletions

View File

@ -19,7 +19,7 @@ jobs:
strategy:
matrix:
job:
- cargo fmt --all -- --check
- rustup toolchain install nightly --profile minimal --component rustfmt && cargo +nightly fmt --all -- --check
- cargo test --all-targets --all-features
- cargo clippy --all-targets --all-features -- -D warning
fail-fast: false
@ -54,7 +54,7 @@ jobs:
needs: populate-rust-cache
env:
# These hooks are expensive and already run as dedicated jobs above
SKIP: "tests,clippy"
SKIP: "rustfmt,tests,clippy"
steps:
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4

View File

@ -15,7 +15,7 @@ repos:
hooks:
- id: rustfmt
name: Check formatting
entry: cargo fmt --
entry: cargo +nightly fmt --
language: system
files: \.rs$
- id: tests

View File

@ -35,9 +35,11 @@ You can see some examples of this in:
## Conventions
Code is formatted with [rustfmt](https://github.com/rust-lang/rustfmt) using the default options.
In addition, all default [clippy](https://github.com/rust-lang/rust-clippy) checks on the latest stable Rust compiler must also pass.
Both of these are enforced through CI using GitHub actions.
Code is formatted with [rustfmt](https://github.com/rust-lang/rustfmt).
The nightly toolchain is used for this as things like sorting of imports is not yet available on stable yet.
If you don't have the nightly toolchain installed, run: `rustup toolchain install nightly --component rustfmt`
In addition, [clippy](https://github.com/rust-lang/rust-clippy) is configured to be quite pedantic and all of its checks must also pass for CI builds to succeed.
> **💡 Tip: install pre-commit hooks**
>

7
rustfmt.toml Normal file
View File

@ -0,0 +1,7 @@
comment_width = 120
format_code_in_doc_comments = true
group_imports = "StdExternalCrate"
imports_granularity = "Module"
imports_layout = "HorizontalVertical"
use_field_init_shorthand = true
wrap_comments = true

View File

@ -1,6 +1,7 @@
use crate::Frontmatter;
use std::path::{Path, PathBuf};
use crate::Frontmatter;
#[derive(Debug, Clone)]
/// Context holds metadata about a note which is being parsed.
///
@ -36,10 +37,9 @@ pub struct Context {
/// # let mut context = Context::new(PathBuf::from("source"), PathBuf::from("destination"));
/// let key = Value::String("foo".to_string());
///
/// context.frontmatter.insert(
/// key.clone(),
/// Value::String("bar".to_string()),
/// );
/// context
/// .frontmatter
/// .insert(key.clone(), Value::String("bar".to_string()));
/// ```
pub frontmatter: Frontmatter,
}

View File

@ -2,8 +2,8 @@ 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
/// are available with `Frontmatter` as well.
/// This is essentially an alias of [`serde_yaml::Mapping`] so all the methods available on that
/// type are available with `Frontmatter` as well.
///
/// # Examples
///
@ -14,10 +14,7 @@ use serde_yaml::Result;
/// let mut frontmatter = Frontmatter::new();
/// let key = Value::String("foo".to_string());
///
/// frontmatter.insert(
/// key.clone(),
/// Value::String("bar".to_string()),
/// );
/// frontmatter.insert(key.clone(), Value::String("bar".to_string()));
///
/// assert_eq!(
/// frontmatter.get(&key),
@ -67,10 +64,11 @@ pub enum FrontmatterStrategy {
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
use serde_yaml::Value;
use super::*;
#[test]
fn empty_string_should_yield_empty_frontmatter() {
assert_eq!(frontmatter_from_str("").unwrap(), Frontmatter::new());

View File

@ -1,5 +1,4 @@
pub use pulldown_cmark;
pub use serde_yaml;
pub use {pulldown_cmark, serde_yaml};
#[macro_use]
extern crate lazy_static;
@ -10,11 +9,16 @@ pub mod postprocessors;
mod references;
mod walker;
pub use context::Context;
pub use frontmatter::{Frontmatter, FrontmatterStrategy};
pub use walker::{vault_contents, WalkOptions};
use std::ffi::OsString;
use std::fs::{self, File};
use std::io::prelude::*;
use std::io::ErrorKind;
use std::path::{Path, PathBuf};
use std::{fmt, str};
pub use context::Context;
use frontmatter::{frontmatter_from_str, frontmatter_to_str};
pub use frontmatter::{Frontmatter, FrontmatterStrategy};
use pathdiff::diff_paths;
use percent_encoding::{utf8_percent_encode, AsciiSet, CONTROLS};
use pulldown_cmark::{CodeBlockKind, CowStr, Event, HeadingLevel, Options, Parser, Tag};
@ -23,14 +27,8 @@ use rayon::prelude::*;
use references::{ObsidianNoteReference, RefParser, RefParserState, RefType};
use slug::slugify;
use snafu::{ResultExt, Snafu};
use std::ffi::OsString;
use std::fmt;
use std::fs::{self, File};
use std::io::prelude::*;
use std::io::ErrorKind;
use std::path::{Path, PathBuf};
use std::str;
use unicode_normalization::UnicodeNormalization;
pub use walker::{vault_contents, WalkOptions};
/// A series of markdown [Event]s that are generated while traversing an Obsidian markdown note.
pub type MarkdownEvents<'a> = Vec<Event<'a>>;
@ -38,11 +36,12 @@ 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`]
/// just before notes are written out to their final destination.
/// 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`]).
/// 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`]).
@ -64,8 +63,8 @@ pub type MarkdownEvents<'a> = Vec<Event<'a>>;
/// 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
/// whether a note is a root note or an embedded note in this situation.
/// [`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,8 +103,8 @@ 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
/// changing the text when we encounter a [text element][Event::Text].
/// 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
/// definition.
@ -282,8 +281,9 @@ 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.
/// 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 Self {
self.start_at = start_at;
self
@ -305,7 +305,8 @@ 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.
@ -314,7 +315,8 @@ impl<'a> Exporter<'a> {
self
}
/// Append a function to the chain of [postprocessors][Postprocessor] to run on exported Obsidian Markdown notes.
/// 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 Self {
self.postprocessors.push(processor);
self
@ -718,8 +720,7 @@ impl<'a> Exporter<'a> {
///
/// 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>)
/// 3. Unicode normalization rules using normalization form C (<https://www.w3.org/TR/charmod-norm/#unicodeNormalization>)
fn lookup_filename_in_vault<'a>(
filename: &str,
vault_contents: &'a [PathBuf],
@ -897,10 +898,11 @@ fn codeblock_kind_to_owned<'a>(codeblock_kind: CodeBlockKind<'_>) -> CodeBlockKi
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
use rstest::rstest;
use super::*;
lazy_static! {
static ref VAULT: Vec<PathBuf> = vec![
PathBuf::from("NoteA.md"),

View File

@ -1,8 +1,10 @@
use std::env;
use std::path::PathBuf;
use eyre::{eyre, Result};
use gumdrop::Options;
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");

View File

@ -1,9 +1,10 @@
//! A collection of officially maintained [postprocessors][crate::Postprocessor].
use super::{Context, MarkdownEvents, PostprocessorResult};
use pulldown_cmark::Event;
use serde_yaml::Value;
use super::{Context, MarkdownEvents, PostprocessorResult};
/// This postprocessor converts all soft line breaks to hard line breaks. Enabling this mimics
/// Obsidian's _'Strict line breaks'_ setting.
pub fn softbreaks_to_hardbreaks(

View File

@ -1,6 +1,7 @@
use regex::Regex;
use std::fmt;
use regex::Regex;
lazy_static! {
static ref OBSIDIAN_NOTE_LINK_RE: Regex =
Regex::new(r"^(?P<file>[^#|]+)??(#(?P<section>.+?))??(\|(?P<label>.+?))??$").unwrap();

View File

@ -1,9 +1,11 @@
use crate::{ExportError, WalkDirSnafu};
use ignore::{DirEntry, Walk, WalkBuilder};
use snafu::ResultExt;
use std::fmt;
use std::path::{Path, PathBuf};
use ignore::{DirEntry, Walk, WalkBuilder};
use snafu::ResultExt;
use crate::{ExportError, WalkDirSnafu};
type Result<T, E = ExportError> = std::result::Result<T, E>;
type FilterFn = dyn Fn(&DirEntry) -> bool + Send + Sync + 'static;

View File

@ -1,16 +1,16 @@
#![allow(clippy::shadow_unrelated)]
use std::fs::{create_dir, read_to_string, set_permissions, File, Permissions};
use std::io::prelude::*;
#[cfg(not(target_os = "windows"))]
use std::os::unix::fs::PermissionsExt;
use std::path::PathBuf;
use obsidian_export::{ExportError, Exporter, FrontmatterStrategy};
use pretty_assertions::assert_eq;
use std::fs::{create_dir, read_to_string, set_permissions, File, Permissions};
use std::io::prelude::*;
use std::path::PathBuf;
use tempfile::TempDir;
use walkdir::WalkDir;
#[cfg(not(target_os = "windows"))]
use std::os::unix::fs::PermissionsExt;
#[test]
fn test_main_variants_with_default_options() {
let tmp_dir = TempDir::new().expect("failed to make tempdir");
@ -262,9 +262,9 @@ fn test_not_existing_destination_with_source_dir() {
}
#[test]
// This test ensures that when source is a file, but destination points to a regular file
// inside of a non-existent directory, an error is raised instead of that directory path being
// created (like `mkdir -p`)
// This test ensures that when source is a file, but destination points to a
// regular file inside of a non-existent directory, an error is raised instead
// of that directory path being created (like `mkdir -p`)
fn test_not_existing_destination_with_source_file() {
let tmp_dir = TempDir::new().expect("failed to make tempdir");

View File

@ -1,16 +1,18 @@
use std::collections::HashSet;
use std::fs::{read_to_string, remove_file};
use std::path::PathBuf;
use std::sync::Mutex;
use obsidian_export::postprocessors::{filter_by_tags, softbreaks_to_hardbreaks};
use obsidian_export::{Context, Exporter, MarkdownEvents, PostprocessorResult};
use pretty_assertions::assert_eq;
use pulldown_cmark::{CowStr, Event};
use serde_yaml::Value;
use std::collections::HashSet;
use std::fs::{read_to_string, remove_file};
use std::path::PathBuf;
use std::sync::Mutex;
use tempfile::TempDir;
use walkdir::WalkDir;
/// This postprocessor replaces any instance of "foo" with "bar" in the note body.
/// This postprocessor replaces any instance of "foo" with "bar" in the note
/// body.
fn foo_to_bar(_ctx: &mut Context, events: &mut MarkdownEvents<'_>) -> PostprocessorResult {
for event in events.iter_mut() {
if let Event::Text(text) = event {
@ -27,9 +29,9 @@ fn append_frontmatter(ctx: &mut Context, _events: &mut MarkdownEvents<'_>) -> Po
PostprocessorResult::Continue
}
// The purpose of this test to verify the `append_frontmatter` postprocessor is called to extend
// the frontmatter, and the `foo_to_bar` postprocessor is called to replace instances of "foo" with
// "bar" (only in the note body).
// The purpose of this test to verify the `append_frontmatter` postprocessor is
// called to extend the frontmatter, and the `foo_to_bar` postprocessor is
// called to replace instances of "foo" with "bar" (only in the note body).
#[test]
fn test_postprocessors() {
let tmp_dir = TempDir::new().expect("failed to make tempdir");
@ -106,8 +108,8 @@ fn test_postprocessor_change_destination() {
assert!(new_note_path.exists());
}
// Ensure postprocessor type definition has proper lifetimes to allow state (here: `parents`)
// to be passed in. Otherwise, this fails with an error like:
// Ensure postprocessor type definition has proper lifetimes to allow state
// (here: `parents`) to be passed in. Otherwise, this fails with an error like:
// error[E0597]: `parents` does not live long enough
// cast requires that `parents` is borrowed for `'static`
#[test]
@ -139,9 +141,9 @@ fn test_postprocessor_stateful_callback() {
assert!(parents.contains(expected));
}
// The purpose of this test to verify the `append_frontmatter` postprocessor is called to extend
// the frontmatter, and the `foo_to_bar` postprocessor is called to replace instances of "foo" with
// "bar" (only in the note body).
// The purpose of this test to verify the `append_frontmatter` postprocessor is
// called to extend the frontmatter, and the `foo_to_bar` postprocessor is
// called to replace instances of "foo" with "bar" (only in the note body).
#[test]
fn test_embed_postprocessors() {
let tmp_dir = TempDir::new().expect("failed to make tempdir");
@ -162,8 +164,8 @@ fn test_embed_postprocessors() {
assert_eq!(expected, actual);
}
// When StopAndSkipNote is used with an embed_preprocessor, it should skip the embedded note but
// continue with the rest of the note.
// When StopAndSkipNote is used with an embed_preprocessor, it should skip the
// embedded note but continue with the rest of the note.
#[test]
fn test_embed_postprocessors_stop_and_skip() {
let tmp_dir = TempDir::new().expect("failed to make tempdir");
@ -182,9 +184,10 @@ fn test_embed_postprocessors_stop_and_skip() {
assert_eq!(expected, actual);
}
// This test verifies that the context which is passed to an embed postprocessor is actually
// correct. Primarily, this means the frontmatter should reflect that of the note being embedded as
// opposed to the frontmatter of the root note.
// This test verifies that the context which is passed to an embed postprocessor
// is actually correct. Primarily, this means the frontmatter should reflect
// that of the note being embedded as opposed to the frontmatter of the root
// note.
#[test]
#[allow(clippy::manual_assert)]
fn test_embed_postprocessors_context() {
@ -203,9 +206,10 @@ fn test_embed_postprocessors_context() {
.get(&Value::String("is_root_note".into()))
.unwrap();
if is_root_note != &Value::Bool(true) {
// NOTE: Test failure may not give output consistently because the test binary affects
// how output is captured and printed in the thread running this postprocessor. Just
// run the test a couple times until the error shows up.
// NOTE: Test failure may not give output consistently because the test binary
// affects how output is captured and printed in the thread running
// this postprocessor. Just run the test a couple times until the
// error shows up.
panic!(
"postprocessor: expected is_root_note in {} to be true, got false",
&ctx.current_file().display()
@ -219,9 +223,10 @@ fn test_embed_postprocessors_context() {
.get(&Value::String("is_root_note".into()))
.unwrap();
if is_root_note == &Value::Bool(true) {
// NOTE: Test failure may not give output consistently because the test binary affects
// how output is captured and printed in the thread running this postprocessor. Just
// run the test a couple times until the error shows up.
// NOTE: Test failure may not give output consistently because the test binary
// affects how output is captured and printed in the thread running
// this postprocessor. Just run the test a couple times until the
// error shows up.
panic!(
"embed_postprocessor: expected is_root_note in {} to be false, got true",
&ctx.current_file().display()