mdsf 0.12.1

Format, and lint, markdown code snippets using your favorite tools
Documentation
use mdsf::{
    caching::hash_config,
    cli::{FormatCommandArguments, read_stdin},
    config::MdsfConfig,
    error::MdsfError,
    execution::setup_snippet,
    format_file, handle_file,
    terminal::print_config_schema_version_mismatch,
};
use threadpool::ThreadPool;

const MDSF_IGNORE_FILE_NAME: &str = ".mdsfignore";

#[inline]
fn determine_threads_to_use(argument: Option<usize>) -> usize {
    if let Some(thread_arg) = argument
        && thread_arg > 0
    {
        return thread_arg.to_owned();
    }

    if let Ok(available_threads) = std::thread::available_parallelism().map(core::num::NonZero::get)
        && available_threads > 0
    {
        return available_threads;
    }

    1
}

#[inline]
pub fn run(args: FormatCommandArguments, dry_run: bool) -> Result<(), MdsfError> {
    let mut conf = args
        .config
        .map_or_else(MdsfConfig::auto_load, MdsfConfig::load)?;

    if let Some((version, false)) = conf.parse_schema_version()
        && !(version == "development" || version == "stable")
    {
        print_config_schema_version_mismatch(version);
    }

    conf.setup_language_aliases()?;

    let config_hash = if args.cache {
        Some(hash_config(&conf))
    } else {
        None
    };

    let changed_file_count = std::sync::Arc::new(core::sync::atomic::AtomicU32::new(0));

    let on_missing_tool_binary = args
        .on_missing_tool_binary
        .unwrap_or_else(|| conf.on_missing_tool_binary.unwrap_or_default());

    let on_missing_language_definition = args
        .on_missing_language_definition
        .unwrap_or_else(|| conf.on_missing_language_definition.unwrap_or_default());

    if args.stdin {
        let stdin_input = read_stdin().map_err(MdsfError::ReadStdin)?;

        let f = setup_snippet(&stdin_input, ".md")?;

        let (was_formatted, output) = format_file(
            &conf,
            f.path(),
            &stdin_input,
            args.timeout.unwrap_or_default(),
            args.debug,
            on_missing_tool_binary,
            on_missing_language_definition,
        );

        if was_formatted {
            changed_file_count.fetch_add(1, core::sync::atomic::Ordering::SeqCst);
        }

        if !dry_run {
            println!("{output}");
        }
    } else if let Some(first_path) = args.input.first() {
        if conf.files.extensions.is_empty() {
            return Err(MdsfError::EmptyFilesExtensions);
        }

        let mut walk_builder = ignore::WalkBuilder::new(first_path);

        walk_builder
            .standard_filters(true)
            .parents(true)
            .hidden(true)
            .add_custom_ignore_filename(MDSF_IGNORE_FILE_NAME);

        args.input.iter().skip(1).for_each(|p| {
            walk_builder.add(p);
        });

        let thread_count = determine_threads_to_use(args.threads).max(1);

        let pool = ThreadPool::new(thread_count);

        let shared_config = std::sync::Arc::new(conf);

        for entry in walk_builder.build().flatten() {
            let file_path = entry.path().to_path_buf();

            if file_path.extension().is_some_and(|ext_os| {
                ext_os
                    .to_str()
                    .is_some_and(|ext_str| shared_config.files.is_enabled_file_extension(ext_str))
            }) {
                let config_ref = shared_config.clone();

                let changed_file_count_ref = changed_file_count.clone();

                let config_hash_clone = config_hash.clone();

                pool.execute(move || {
                    let was_formatted = handle_file(
                        &config_ref,
                        &file_path,
                        dry_run,
                        config_hash_clone,
                        args.timeout.unwrap_or_default(),
                        args.debug,
                        on_missing_tool_binary,
                        on_missing_language_definition,
                    );

                    if was_formatted {
                        changed_file_count_ref.fetch_add(1, core::sync::atomic::Ordering::SeqCst);
                    }
                });
            }
        }

        pool.join();
    } else {
        return Err(MdsfError::MissingInput);
    }

    let total_changed_files = changed_file_count.load(core::sync::atomic::Ordering::SeqCst);

    if dry_run && total_changed_files > 0 {
        Err(MdsfError::CheckModeChanges(total_changed_files))
    } else {
        Ok(())
    }
}