Use nightly rust for formatting to group import statements
This commit is contained in:
parent
bd1220028b
commit
da9238a78c
4
.github/workflows/ci.yml
vendored
4
.github/workflows/ci.yml
vendored
@ -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
|
||||
|
||||
|
@ -15,7 +15,7 @@ repos:
|
||||
hooks:
|
||||
- id: rustfmt
|
||||
name: Check formatting
|
||||
entry: cargo fmt --
|
||||
entry: cargo +nightly fmt --
|
||||
language: system
|
||||
files: \.rs$
|
||||
- id: tests
|
||||
|
@ -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
7
rustfmt.toml
Normal 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
|
@ -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,
|
||||
}
|
||||
|
@ -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());
|
||||
|
54
src/lib.rs
54
src/lib.rs
@ -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"),
|
||||
|
@ -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");
|
||||
|
||||
|
@ -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(
|
||||
|
@ -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();
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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");
|
||||
|
||||
|
@ -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()
|
||||
|
Loading…
Reference in New Issue
Block a user