use std::borrow::Cow;
use cargo_metadata::PackageName;
use miette::Diagnostic;
use pulldown_cmark::{Event, Options};
use snafu::{OptionExt as _, ResultExt as _, Snafu};
use crate::{
cargo,
sync::{
PackageSyncContext,
contents::rustdoc::{document::UrlOptions, intra_link::LinkMappingConfig},
},
};
mod build;
mod code_block;
mod document;
mod escape_markers;
mod heading;
mod intra_link;
#[derive(Debug, Snafu, Diagnostic)]
pub(in crate::sync) enum CreateRustdocError {
#[snafu(display("failed to build rustdoc JSON output for package `{package}`"))]
BuildRustdoc {
package: PackageName,
#[snafu(source)]
#[diagnostic_source]
source: Box<build::BuildRustdocError>,
},
#[snafu(display("package `{package}` does not have a root item"))]
RootNotFound { package: PackageName },
#[snafu(display("package `{package}` does not have crate-level documentation"))]
RootDocNotFound { package: PackageName },
#[snafu(display("failed to determine the Rust toolchain version"))]
DetermineToolchain {
#[snafu(source)]
#[diagnostic_source]
source: cargo::ToolchainError,
},
#[snafu(display("failed to construct markdown from rustdoc output"))]
Render {
#[snafu(source)]
source: pulldown_cmark_to_cmark::Error,
},
}
pub(super) fn create(cx: &PackageSyncContext<'_>) -> Result<String, CreateRustdocError> {
let doc = build::build_rustdoc(cx).with_context(|_source| BuildRustdocSnafu { package: cx })?;
let root = doc
.root_item()
.with_context(|| RootNotFoundSnafu { package: cx })?;
let build_url_options = build_url_options(cx)?;
let mapping_config = build_mapping_config(cx);
let resolver = doc.intra_link_resolver(&build_url_options);
let mapper = mapping_config
.build_mapper(&resolver, root)
.with_context(|| RootDocNotFoundSnafu { package: cx })?;
let events = mapper.build_parser(main_body_opts());
let events = heading::convert(events);
let events = code_block::convert(events);
let events = escape_markers::convert(events);
let output = render(events)?;
Ok(output)
}
fn build_url_options<'a>(
cx: &'a PackageSyncContext<'_>,
) -> Result<UrlOptions<'a>, CreateRustdocError> {
let config = &cx.config;
let local_html_root_url = config.rustdoc.html_root_url.as_deref().map_or_else(
|| format!("https://docs.rs/{}/{}", cx.package.name, cx.package.version).into(),
Cow::Borrowed,
);
let standard_library_url_mode = config.rustdoc.standard_library_url_mode.unwrap_or_default();
let cargo_toolchain =
cargo::toolchain(None, cx.install_toolchain).context(DetermineToolchainSnafu)?;
let rustdoc_toolchain =
cargo::toolchain(cx.config.rustdoc.toolchain.as_deref(), cx.install_toolchain)
.context(DetermineToolchainSnafu)?;
Ok(UrlOptions {
local_html_root_url,
standard_library_url_mode,
cargo_toolchain,
rustdoc_toolchain,
})
}
fn build_mapping_config<'a>(cx: &'a PackageSyncContext<'_>) -> LinkMappingConfig<'a> {
LinkMappingConfig {
mappings: &cx.config.rustdoc.mappings,
}
}
fn main_body_opts() -> Options {
Options::ENABLE_TABLES
| Options::ENABLE_FOOTNOTES
| Options::ENABLE_STRIKETHROUGH
| Options::ENABLE_TASKLISTS
| Options::ENABLE_SMART_PUNCTUATION
}
fn render<'a, I>(events: I) -> Result<String, CreateRustdocError>
where
I: Iterator<Item = Event<'a>>,
{
let mut buf = String::new();
pulldown_cmark_to_cmark::cmark(events, &mut buf).context(RenderSnafu)?;
if !buf.is_empty() && !buf.ends_with('\n') {
buf.push('\n');
}
Ok(buf)
}