/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #![cfg_attr(feature = "fatal-warnings", deny(warnings))] // Note: If you change this remember to update `README.md`. To do so run `cargo run`. //! Cargo command to create your README from your crate’s documentation. //! //! # Installation //! //! You can install cargo rdme with cargo by running `cargo install cargo-rdme`. //! //! # Usage //! //! Cargo rdme will insert your crate’s documentation in your README file. To control where the //! documentation will be inserted you need to insert a marker: ``. For example, //! you can start your README with some glorious badges and follow up with the rustdoc //! documentation: //! //! ```markdown //! [![Build Status](https://example.org/badge.svg)](https://example.org/link-to-ci) //! //! //! ``` //! //! After running `cargo rdme` you will find your README to be something like: //! //! ```markdown //! [![Build Status](https://example.org/badge.svg)](https://example.org/link-to-ci) //! //! //! //! //! //! //! ``` //! //! Whenever change your crate’s documentation you just need to run `cargo rdme` to update your //! README file. //! //! # Automatic transformations //! //! The documentation of your crate doesn’t always map directly to a good README. For example, //! rust code blocks can have hidden lines. Those should not be shown in the README file. //! //! This section covers the transformation cargo rdme automatically apply to generate a better //! README. //! //! ## Rust code block //! //! Rust code block are transformed in two ways by cargo rdme: //! //! 1. Rust code blocks with lines starting with `#` will be omitted, just like in `rustdoc`. //! 2. Rust code blocks get annotated with the `rust` markdown tag so it gets proper syntax //! highlighting. We also remove tags that only concern `rustdoc` such as `should_panic`. //! //! In the table below you can see an example of these modification. The code block now is //! tagged with `rust` and hidden lines were removed: //! //! //! //! //! //! //! //! //! //! //! //! //! //!
Crate’s rustdoc
README.md
//! //! ```rust //! //! To check if a number is prime do: //! //! //! //! ``` //! //! # fn main() { //! //! for i in 2.. { //! //! if is_prime(i) { //! //! println!("{i}"); //! //! } //! //! } //! //! # } //! //! ``` //! ``` //! //! //! //! ````markdown //! To check if a number is prime do: //! //! ```rust //! for i in 2.. { //! if is_prime(i) { //! println!("{i}"); //! } //! } //! ``` //! ```` //! //!
//! //! ## Intralinks //! //! Rust documentation can contain [links to items defined in the crate](https://doc.rust-lang.org/stable/rustdoc/linking-to-items-by-name.html). //! This links would not make sense in your README file, so cargo rdme automatically generate //! links to [docs.rs](https://docs.rs) for these intralinks. //! //! Currently we only support links of the form `[⋯](crate::⋯)`, so be sure to use that format. //! Links to the standard library are also supported, and they must be of the form //! `[⋯](::::⋯)`, where `` is a crate that is part of the standard library, such as //! `std`, `core`, or `alloc`. Reference-style links are also supported. //! //! Take a look at the example below: //! //! //! //! //! //! //! //! //! //! //! //! //! //!
Crate’s rustdoc
README.md
//! //! ```rust //! //! To check if a number is prime use //! //! [`is_prime`](crate::is_prime). //! ``` //! //! //! //! ```markdown //! To check if a number is prime use //! [`is_prime`](https://docs.rs/prime/latest/prime/fn.is_prime.html). //! ``` //! //!
//! //! Note that there is some limitations in intralink support. This is a complex feature: cargo rdme //! needs to do some work to be able to create the link to docs.rs. This is because the link //! includes the kind of item the intralink points to, in the case of `is_prime` we need to discover //! that is a function to generate a link that ends in `fn.is_prime.html`. Therefore, intralink //! support should be considered "best effort" (for instance, don’t expect items generated by macros //! to be resolved). If cargo rdme is unable to generate the link it will still generate the README //! file, but a warning will be emitted. //! //! ## Heading levels //! //! The heading levels in the crate’s documentation will, by default, be nested under the level //! of the section of the README where it is inserted into. This behavior can be changed with //! the `--heading-base-level` command line flag, or in the configuration file (see example //! below). //! //! # Configuration file //! //! If the default behavior of `cargo rdme` is not appropriate for your project you can crate a //! configuration file `.cargo-rdme.toml` in the root of your project. This is how that //! configuration file can look like: //! //! ```toml //! # Override the README file path. When this is not set cargo rdme will use the file path defined //! # in the project’s `Cargo.toml`. //! readme-path = "MY-README.md" //! //! # What line terminator to use when generating the README file. This can be "lf" or "crlf". //! line-terminator = "lf" //! //! # If you are using a workspace to hold multiple projects, use this to select the project from //! # which to extract the documentation from. It can be useful to also set `readme-path` to create //! # the README file in the root of the project. //! workspace-project = "subproject" //! //! # Defines the base heading level to use when inserting the crate’s documentation in the //! # README. If this is not set the crate’s documentation will be inserted with its sections //! # belonging to the README section where the insertion happens. //! heading-base-level = 0 //! //! # The default entrypoint will be `src/lib.rs`. You can change that in the `entrypoint` table. //! [entrypoint] //! # The entrypoint type can be "lib" or "bin". //! type = "bin" //! # When you set type to "bin" the entrypoint default to `src/main.rs`. If you have binary targets //! # specified in your cargo manifest you can select them by name with `bin-name`. //! bin-name = "my-bin-name" //! //! [intralinks] //! # Defines the base url to use in intralinks urls. The default value is `https://docs.rs`. //! docs-rs-base-url = "https://mydocs.rs" //! # Defines the version to use in intralinks urls. The default value is `latest`. //! docs-rs-version = "1.0.0" //! # If this is set the intralinks will be stripping in the README file. //! strip-links = false //! ``` //! //! These setting can be overridden with command line flags. Run `cargo rdme --help` for more //! information. //! //! # Integration with CI //! //! To verify that your README is up to date with your crate’s documentation you can run //! `cargo rdme --check`. The exit code will be `0` if the README is up to date, `3` if it’s //! not, or `4` if there were warnings. //! //! If you use GitHub Actions you can add this step to verify if the README is up to date: //! //! ```yaml //! - name: Check if the README is up to date. //! run: | //! cargo install cargo-rdme //! cargo rdme --check //! ``` use crate::options::{EntrypointOpt, LineTerminatorOpt}; use cargo_rdme::transform::IntralinkError; use cargo_rdme::{ extract_doc_from_source_file, infer_line_terminator, inject_doc_in_readme, LineTerminator, Project, }; use cargo_rdme::{Doc, ProjectError, Readme}; use std::cell::Cell; use std::path::{Path, PathBuf}; use thiserror::Error; #[macro_use] mod console; mod options; enum ExitCode { Ok = 0, Error = 1, /// Exit code we don't update the README because we would overwrite uncommitted changes. ReadmeNotUpdatedUncommittedChanges = 2, /// Exit code when we run in "check mode" and the README is not up to date. CheckMismatch = 3, /// Exit code when we run in "check mode" and there were warnings. CheckHasWarnings = 4, } impl From for ExitCode { fn from(value: RunError) -> ExitCode { match value { RunError::ProjectError(_) | RunError::ExtractDocError(_) | RunError::ReadmeError(_) | RunError::NoEntrySourceFile | RunError::NoReadmeFile | RunError::NoRustdoc | RunError::InjectDocError(_) | RunError::TransformIntraLinkError(_) | RunError::IOError(_) => ExitCode::Error, RunError::ReadmeNotUpdatedUncommittedChanges => { ExitCode::ReadmeNotUpdatedUncommittedChanges } RunError::CheckReadmeMismatch => ExitCode::CheckMismatch, RunError::CheckHasWarnings => ExitCode::CheckHasWarnings, } } } #[derive(Error, Debug)] enum RunError { #[error("failed to get project info: {0}")] ProjectError(ProjectError), #[error("failed to extract rust doc: {0}")] ExtractDocError(cargo_rdme::ExtractDocError), #[error("failed to process README: {0}")] ReadmeError(cargo_rdme::ReadmeError), #[error("failed get crate's entry source file")] NoEntrySourceFile, #[error("crate's README file not found")] NoReadmeFile, #[error("crate-level rustdoc not found")] NoRustdoc, #[error("failed to inject the documentation in the README: {0}")] InjectDocError(cargo_rdme::InjectDocError), #[error("IO error: {0}")] IOError(std::io::Error), #[error( "not updating README: it has uncommitted changes (use `--force` to bypass this check)" )] ReadmeNotUpdatedUncommittedChanges, #[error("failed to transform intralinks: {0}")] TransformIntraLinkError(IntralinkError), #[error("README is not up to date")] CheckReadmeMismatch, #[error("README is up to date, but warnings were emitted")] CheckHasWarnings, } impl From for RunError { fn from(e: ProjectError) -> RunError { RunError::ProjectError(e) } } impl From for RunError { fn from(e: cargo_rdme::ExtractDocError) -> RunError { RunError::ExtractDocError(e) } } impl From for RunError { fn from(e: cargo_rdme::ReadmeError) -> RunError { RunError::ReadmeError(e) } } impl From for RunError { fn from(e: cargo_rdme::InjectDocError) -> RunError { RunError::InjectDocError(e) } } impl From for RunError { fn from(e: std::io::Error) -> RunError { RunError::IOError(e) } } impl From for RunError { fn from(e: IntralinkError) -> RunError { RunError::TransformIntraLinkError(e) } } impl From for RunError { fn from(_: std::convert::Infallible) -> RunError { unreachable!() } } /// Check if the README is up to date. /// /// This will check if the README has the given line terminator as well. fn is_readme_up_to_date( readme_path: impl AsRef, new_readme: &Readme, line_terminator: LineTerminator, ) -> Result { let current_readme_raw: String = std::fs::read_to_string(readme_path)?; let new_readme_raw: Vec = { let mut bytes: Vec = Vec::with_capacity(32 * 1024); new_readme.write(&mut bytes, line_terminator)?; bytes }; Ok(current_readme_raw.as_bytes() == new_readme_raw.as_slice()) } fn entrypoint<'a>(project: &'a Project, entrypoint_opt: &EntrypointOpt) -> Option<&'a Path> { match entrypoint_opt { EntrypointOpt::Auto => { project.get_lib_entryfile_path().or_else(|| project.get_bin_default_entryfile_path()) } EntrypointOpt::Lib => project.get_lib_entryfile_path(), EntrypointOpt::BinDefault => project.get_bin_default_entryfile_path(), EntrypointOpt::BinName(name) => project.get_bin_entryfile_path(name), } } fn line_terminator( line_terminator_opt: LineTerminatorOpt, readme_path: impl AsRef, ) -> std::io::Result { match line_terminator_opt { LineTerminatorOpt::Auto => infer_line_terminator(readme_path), LineTerminatorOpt::Lf => Ok(LineTerminator::Lf), LineTerminatorOpt::CrLf => Ok(LineTerminator::CrLf), } } struct Warnings { had_warnings: bool, } fn transform_doc( doc: &Doc, project: &Project, entrypoint: impl AsRef, options: &options::Options, ) -> Result<(Doc, Warnings), RunError> { use cargo_rdme::transform::{ DocTransform, DocTransformIntralinks, DocTransformRustMarkdownTag, DocTransformRustRemoveComments, }; let transform = DocTransformRustRemoveComments::new(); // TODO Use `into_ok()` once it is stable (https://github.com/rust-lang/rust/issues/61695). let doc = transform.transform(doc)?; let transform = DocTransformRustMarkdownTag::new(); // TODO Use `into_ok()` once it is stable (https://github.com/rust-lang/rust/issues/61695). let doc = transform.transform(&doc)?; let had_warnings = Cell::new(false); let transform = DocTransformIntralinks::new( project.get_package_name(), entrypoint, |msg| { print_warning!("{}", msg); had_warnings.set(true); }, options.intralinks.clone(), ); Ok((transform.transform(&doc)?, Warnings { had_warnings: had_warnings.into_inner() })) } /// Check if the `path` has local changes that were not yet commited. /// /// This returns `None` if we were not able to determine that. fn git_is_current(path: impl AsRef) -> Option { use git2::{Repository, Status}; let repository = Repository::discover(path.as_ref().parent()?).ok()?; let repository_path = repository.path().parent()?; let path_repository_base = path.as_ref().strip_prefix(repository_path).ok()?; let status = repository.status_file(path_repository_base).ok()?; Some(status == Status::CURRENT) } fn update_readme( new_readme: &Readme, readme_path: impl AsRef, line_terminator: LineTerminator, ignore_uncommitted_changes: bool, ) -> Result<(), RunError> { match ignore_uncommitted_changes || git_is_current(&readme_path).unwrap_or(true) { true => Ok(new_readme.write_to_file(&readme_path, line_terminator)?), false => Err(RunError::ReadmeNotUpdatedUncommittedChanges), } } fn run(options: options::Options) -> Result<(), RunError> { let project: Project = match options.workspace_project { None => Project::from_current_dir()?, Some(ref project) => Project::from_current_dir_workspace_project(project)?, }; let entryfile: &Path = entrypoint(&project, &options.entrypoint).ok_or(RunError::NoEntrySourceFile)?; let doc: Doc = match extract_doc_from_source_file(entryfile)? { None => return Err(RunError::NoRustdoc), Some(doc) => doc, }; let (doc, warnings) = transform_doc(&doc, &project, entryfile, &options)?; let readme_path: PathBuf = match options.readme_path { None => project.get_readme_path().ok_or(RunError::NoReadmeFile)?, Some(path) => { if !path.is_file() { return Err(RunError::NoReadmeFile); } path } }; let original_readme: Readme = Readme::from_file(&readme_path)?; let new_readme = inject_doc_in_readme(&original_readme, &doc, options.heading_base_level)?; if !new_readme.had_marker { let msg = indoc::formatdoc! { " No marker found in the README file ({readme_filepath}). cargo-rdme expects a marker in the README where the crate’s documentation will be inserted. This is the marker you should add to your README: {marker}", readme_filepath = readme_path.display(), marker = cargo_rdme::MARKER_RDME, }; print_info!("{}", msg); } let line_terminator = line_terminator(options.line_terminator, &readme_path)?; match options.check { false => update_readme(&new_readme.readme, readme_path, line_terminator, options.force), true => { if !is_readme_up_to_date(&readme_path, &new_readme.readme, line_terminator)? { return Err(RunError::CheckReadmeMismatch); } if warnings.had_warnings && !options.no_fail_on_warnings { return Err(RunError::CheckHasWarnings); } Ok(()) } } } fn main() { let cmd_options = options::cmd_options(); let exit_code: ExitCode = match std::env::current_dir() { Ok(current_dir) => match options::config_file_options(current_dir) { Ok(config_file_options) => { let options = options::merge_options(cmd_options, config_file_options); match run(options) { Ok(()) => ExitCode::Ok, Err(e) => { print_error!("{}", &e); e.into() } } } Err(e) => { print_error!("unable to read config file: {}", e); ExitCode::Error } }, Err(e) => { print_error!("unable to get current directory: {}", e); ExitCode::Error } }; std::process::exit(exit_code as i32); }