···11-[](https://tonywu6.github.io/mdbookkit)
11+[](https://tonywu6.github.io/mdbookkit/)
2233# mdbookkit
4455-## [Read the book](https://tonywu6.github.io/mdbookkit)
55+Quality-of-life plugins for your [mdBook] project.
6677[](https://crates.io/crates/mdbookkit)
88[](https://docs.rs/mdbookkit)
99[](/LICENSE-APACHE.md)
1010+1111+## [Read the book](https://tonywu6.github.io/mdbookkit/)
1212+1313+You may be looking for:
1414+1515+[**`mdbook-rustdoc-link`**](https://tonywu6.github.io/mdbookkit/rustdoc-link/)
1616+1717+<!-- prettier-ignore-start -->
1818+1919+- [Install & use](https://tonywu6.github.io/mdbookkit/rustdoc-link/getting-started#install)
2020+ | [Using in CI](https://tonywu6.github.io/mdbookkit/rustdoc-link/continuous-integration)
2121+ | [Caching](https://tonywu6.github.io/mdbookkit/rustdoc-link/caching)
2222+2323+[**`mdbook-link-forever`**](https://tonywu6.github.io/mdbookkit/link-forever/)
2424+2525+- [Install & use](https://tonywu6.github.io/mdbookkit/link-forever/#getting-started)
2626+ | [Using in CI](https://tonywu6.github.io/mdbookkit/link-forever/continuous-integration)
2727+2828+[mdBook]: https://rust-lang.github.io/mdBook/
2929+3030+<!-- prettier-ignore-end -->
+14
crates/mdbookkit/CHANGELOG.md
···11+# CHANGELOG
22+33+The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and
44+this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
55+66+This file is autogenerated using [release-plz](https://release-plz.dev).
77+88+## Unreleased
99+1010+> Unreleased changes appear here, if any.
1111+1212+## [1.0.0](https://tonywu6.github.com/tonywu6/mdbookkit/releases/tag/mdbookkit-v1.0.0) - 2025-04-08
1313+1414+Initial release.
···14141515impl Environment {
1616 pub fn try_from_env(book: &PreprocessorContext) -> Result<Result<Self>> {
1717+ let config = config_from_book::<Config>(&book.config, "link-forever")
1818+ .context("failed to read preprocessor config from book.toml")?;
1919+1720 let repo = match Repository::open_from_env()
1821 .context("preprocessor requires a git repository to work")
1922 .context("failed to find a git repository")
2023 {
2124 Ok(repo) => repo,
2222- Err(err) => return Ok(Err(err)),
2525+ Err(err) => return config.fail_on_warnings.adjusted(Ok(Err(err))),
2326 };
24272528 let vcs_root = repo
···4447 }
4548 });
46494747- let config = config_from_book::<Config>(&book.config, "link-forever")?;
4848-4950 let Some(reference) =
5051 get_head(&repo).context("failed to get a tag or commit id to HEAD")?
5152 else {
5252- return Ok(Err(anyhow!("no commit found in this repo")));
5353+ return config
5454+ .fail_on_warnings
5555+ .adjusted(Ok(Err(anyhow!("no commit found in this repo"))));
5356 };
54575558 let fmt_link: Box<dyn PermalinkFormat> = {
···7174 .context("failed to determine GitHub url to use for permalinks")
7275 .pipe(Err)
7376 .pipe(Ok)
7777+ .pipe(|result| config.fail_on_warnings.adjusted(result))
7478 }
7579 };
7680 let (owner, repo) = remote_as_github(repo.as_ref())
···128132 .and_then(|tag| tag.format(None))
129133 .tap_err(log_debug!())
130134 {
135135+ log::info!("using tag {tag:?}");
131136 Ok(Some(tag))
132137 } else {
133133- Ok(Some(head.id().to_string()))
138138+ let sha = head.id().to_string();
139139+ log::info!("using commit {sha}");
140140+ Ok(Some(sha))
134141 }
135142}
136143
+31-7
crates/mdbookkit/src/bin/link_forever/mod.rs
···112112 self.resolve_file()
113113 } else if let Some(book) = &self.env.config.book_url {
114114 if let Some(page) = book.0.make_relative(&self.file_url) {
115115+ // hard-coded URLs to book pages
115116 self.resolve_page(page);
116117 } else {
117118 self.link.status = LinkStatus::Ignored;
···164165 if link.link.starts_with('/') {
165166 // mdbook doesn't support absolute paths like VS Code does
166167 link.link = page_url.make_relative(&file_url).unwrap().into();
167167- link.status = LinkStatus::Rewritten
168168+ link.status = LinkStatus::Rewritten;
168169 } else {
169170 link.status = LinkStatus::Published;
170171 }
···189190 };
190191191192 if rel.starts_with("../") {
193193+ // `path` could also be a symlink to a file outside source control somehow
194194+ // in which case it would NOT be marked as LinkStatus::External;
192195 link.status = LinkStatus::External;
193196 return;
194197 }
···202205 }
203206 }
204207208208+ /// Check hard-coded URLs to book pages
205209 fn resolve_page(self, page: String) {
206210 let Self {
207211 file_url,
···231235232236 let mut not_found = vec![];
233237234234- for file in [
235235- format!("{path}.md"),
236236- format!("{path}/index.md"),
237237- format!("{path}/README.md"),
238238- ] {
239239- let Ok(file) = self.env.book_src.join(&file).tap_err(log_debug!()) else {
238238+ // one does not simply avoid trailing slash issues...
239239+ // https://github.com/slorber/trailing-slash-guide
240240+ let try_files = if path.is_empty() || path.ends_with('/') {
241241+ &[
242242+ // enforce that index.html pages should consistently
243243+ // be addressed with a trailing slash
244244+ format!("{path}index.md"),
245245+ format!("{path}README.md"),
246246+ ] as &[String]
247247+ } else {
248248+ &[
249249+ format!("{path}.md"),
250250+ // all major hosting providers implicitly redirect
251251+ // /folder to /folder/, so these are okay
252252+ format!("{path}/index.md"),
253253+ format!("{path}/README.md"),
254254+ ]
255255+ };
256256+257257+ for file in try_files {
258258+ let Ok(file) = self.env.book_src.join(file).tap_err(log_debug!()) else {
240259 continue;
241260 };
242261···661680 }
662681}
663682683683+/// Configuration for the preprocessor.
684684+///
685685+/// This is deserialized from book.toml.
686686+///
687687+/// Doc comments for attributes populate the `configuration.md` page in the docs.
664688#[derive(Deserialize, Default)]
665689#[serde(rename_all = "kebab-case", deny_unknown_fields)]
666690#[cfg_attr(feature = "common-cli", derive(clap::Parser))]
···4242};
43434444/// LSP client to talk to rust-analyzer.
4545+///
4646+/// [`Client`] does not implement [`Clone`], because the server instance is lazily spawned
4747+/// and so must be unique for each client. To enable cloning, put this in an [`Arc`].
4548#[derive(Debug)]
4649pub struct Client {
4750 env: Environment,
···5053}
51545255impl Client {
5656+ /// Initialize a new LSP client.
5757+ ///
5858+ /// This does not actually spawn rust-analyzer.
5359 pub fn new(env: Environment) -> Self {
5460 let server = OnceCell::new();
5561 let docs = DocumentLock::default();
···7884 Ok(opened)
7985 }
80868787+ /// Shutdown the server if it was spawned.
8888+ ///
8989+ /// Returns the [`Environment`] struct for further use.
9090+ ///
9191+ /// This moves `self`. To shutdown a [`Client`] that's in an [`Arc`],
9292+ /// use [`client.drop`][Client::drop].
8193 pub async fn stop(self) -> Environment {
8294 if let Some(server) = self.server.into_inner() {
8395 server
···90102 self.env
91103 }
92104105105+ /// See [`client.stop`][Client::stop].
93106 pub async fn drop(self: Arc<Self>) -> Result<Environment> {
94107 let Some(this) = Arc::into_inner(self) else {
95108 bail!("attempted to shutdown a client that is still referenced")
···126139 percent_indexed: Option<u32>,
127140 }
128141142142+ /// Listen for [Work Done Progress][workDoneProgress] events from rust-analyzer
143143+ /// to determine if indexing is done.
144144+ ///
145145+ /// The `cachePriming` events look like this:
146146+ ///
147147+ /// - [`WorkDoneProgress::Begin`]
148148+ /// - [`WorkDoneProgress::Report`] for each crate indexed, with messages like `4/200 (core)`
149149+ /// - ...
150150+ /// - [`WorkDoneProgress::End`]
151151+ ///
152152+ /// (This is the ticker thing that shows up in the VS Code status bar).
153153+ ///
154154+ /// Notably, rust-analyzer seems to do several rounds of indexing, **not all of
155155+ /// which is actually indexing the entire codebase:**
156156+ ///
157157+ /// - First, a `0/1 (<crate name>)` round that only indexes the current crate.
158158+ ///
159159+ /// - RA is **not ready** at this point: if we were to query for external docs
160160+ /// immediately after this [`WorkDoneProgress::End`] event, almost all links
161161+ /// will fail to resolve.
162162+ ///
163163+ /// - Then, a `0/x ...` round that seems to actually indexes everything, including
164164+ /// [`std`] and all the dependencies.
165165+ ///
166166+ /// - RA is likely ready at this point.
167167+ ///
168168+ /// - Then, one or more additional rounds of indexing that finish very quickly.
169169+ ///
170170+ /// To be able to "reliably" determine that RA is ready for querying, the
171171+ /// probing mechanism does essentially the following:
172172+ ///
173173+ /// 1. Ignore the first round of indexing.
174174+ /// 2. Be extra pessimistic and wait for events to quiet down after receiving
175175+ /// the first [`WorkDoneProgress::End`] event; see [`EventSampler`].
176176+ ///
177177+ /// [workDoneProgress]: https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#workDoneProgress
129178 fn probe_progress(state: &mut State, progress: ProgressParams) {
130179 match indexing_progress(&progress) {
131180 Some(WorkDoneProgress::Begin(begin)) => {
+22-16
crates/mdbookkit/src/bin/rustdoc_link/env.rs
···6677use anyhow::{anyhow, bail, Context, Result};
88use cargo_toml::{Manifest, Product};
99-#[cfg(feature = "common-cli")]
1010-use clap::ValueHint;
119use lsp_types::Url;
1210use pulldown_cmark::Options;
1311use serde::{de::DeserializeOwned, Deserialize, Serialize};
···19172018use super::markdown;
21192020+/// Configuration for the preprocessor.
2121+///
2222+/// This is both deserialized from book.toml and parsed from the command line.
2323+///
2424+/// Doc comments for attributes populate the `configuration.md` page in the docs.
2225#[derive(Deserialize, Debug, Default, Clone)]
2326#[cfg_attr(feature = "common-cli", derive(clap::Parser))]
2427#[serde(rename_all = "kebab-case", deny_unknown_fields)]
···3336 #[serde(default)]
3437 #[cfg_attr(
3538 feature = "common-cli",
3636- arg(long, value_name("COMMAND"), value_hint(ValueHint::CommandString))
3939+ arg(
4040+ long,
4141+ value_name("COMMAND"),
4242+ value_hint(clap::ValueHint::CommandString)
4343+ )
3744 )]
3845 pub rust_analyzer: Option<String>,
3946···6370 #[serde(default)]
6471 #[cfg_attr(
6572 feature = "common-cli",
6666- arg(long, value_name("PATH"), value_hint(ValueHint::DirPath))
7373+ arg(long, value_name("PATH"), value_hint(clap::ValueHint::DirPath))
6774 )]
6875 pub manifest_dir: Option<PathBuf>,
6976···7380 #[serde(default)]
7481 #[cfg_attr(
7582 feature = "common-cli",
7676- arg(long, value_name("PATH"), value_hint(ValueHint::DirPath))
8383+ arg(long, value_name("PATH"), value_hint(clap::ValueHint::DirPath))
7784 )]
7885 pub cache_dir: Option<PathBuf>,
7986···193200 .pipe(Url::from_directory_path)
194201 .unwrap();
195202196196- let temp_dir = config
197197- .cache_dir
198198- .clone()
199199- .map(TempDir::Persistent)
200200- .or_else(|| {
201201- tempfile::TempDir::new()
202202- .ok()
203203- .map(Arc::new)
204204- .map(TempDir::Transient)
205205- })
206206- .context("failed to obtain a temporary directory")?;
203203+ let temp_dir = match config.cache_dir.clone() {
204204+ Some(path) => Some(TempDir::Persistent(path)),
205205+ None => tempfile::TempDir::new()
206206+ .ok()
207207+ .map(Arc::new)
208208+ .map(TempDir::Transient),
209209+ }
210210+ .context("failed to obtain a temporary directory")?;
207211208212 Ok(Self {
209213 temp_dir,
···327331 }
328332}
329333334334+/// Look for rust-analyzer binary from the VS Code extension based on locations
335335+/// described in <https://rust-analyzer.github.io/book/vs_code.html>.
330336pub fn find_code_extension() -> Option<PathBuf> {
331337 let home = dirs::home_dir()?;
332338 [
+9-1
crates/mdbookkit/src/bin/rustdoc_link/item.rs
···11+use anyhow::Result;
12use syn::{
23 parenthesized,
34 parse::{End, Parse, ParseStream, Parser},
···67 PathArguments, QSelf, Token, TypePath,
78};
891010+/// Texts that look like Rust items.
911#[derive(Debug)]
1012pub struct Item {
1313+ /// The parsed item name, which may be different from the source text (e.g.
1414+ /// turbofish are expanded.)
1115 pub name: String,
1616+ /// The synthesized, syntactically valid statement that rust-analyzer can parse.
1217 pub stmt: String,
1818+ /// "Points of interest" in [`stmt`][Self::stmt] that can be used to construct
1919+ /// [`TextDocumentPositionParams`][lsp_types::TextDocumentPositionParams].
1320 pub cursor: Cursor,
1421}
15221623impl Item {
1717- pub fn parse(path: &str) -> anyhow::Result<Self> {
2424+ /// Try to parse a link url as a Rust item. See [`ItemName`].
2525+ pub fn parse(path: &str) -> Result<Self> {
1826 let path = match path.split_once('@') {
1927 None => path,
2028 Some((_, path)) => path,
···6262 };
6363 let label = match &self.state {
6464 LinkState::Unparsed => Some(self.url.as_ref().into()),
6565- LinkState::Parsed(item) => Some(format!("failed to resolve links for {:?}", item.name)),
6565+ LinkState::Parsed(item) => Some(format!("failed to resolve link for {:?}", item.name)),
6666 LinkState::Resolved(links) => Some(format!("{}", links.url())),
6767 };
6868 let label = LabeledSpan::new_with_span(label, self.span.clone());
+32-5
crates/mdbookkit/src/bin/rustdoc_link/markdown.rs
···44use crate::markdown::mdbook_markdown;
5566pub fn stream(text: &str, options: Options) -> MarkdownStream<'_> {
77- Parser::new_with_broken_link_callback(text, options, Some(BrokenLinks))
77+ Parser::new_with_broken_link_callback(text, options, Some(ItemLinks))
88}
991010-pub type MarkdownStream<'a> = Parser<'a, BrokenLinks>;
1010+pub type MarkdownStream<'a> = Parser<'a, ItemLinks>;
1111+1212+/// [`BrokenLinkCallback`] implementation that unconditionally converts all "broken"
1313+/// links to links to be further processed.
1414+///
1515+/// "Broken" links are links like `[text][link::item]` that don't have associated URLs,
1616+/// which are actually exactly what [rustdoc_link][super] wants.
1717+///
1818+/// Links that are "broken" that aren't actually doc links won't show up in the output,
1919+/// because the preprocessor ignores links that cannot be parsed and is capable of
2020+/// emitting only changed links, see [`PatchStream`][crate::markdown::PatchStream].
2121+pub struct ItemLinks;
11221212-pub struct BrokenLinks;
2323+impl ItemLinks {
2424+ // Explicitly disable smart punctuation to prevent quotes from being changed
2525+ // or else things like lifetimes may become invalid
2626+ const OPTIONS: pulldown_cmark::Options =
2727+ mdbook_markdown().intersection(Options::ENABLE_SMART_PUNCTUATION.complement());
2828+}
13291414-impl<'input> BrokenLinkCallback<'input> for BrokenLinks {
3030+impl<'input> BrokenLinkCallback<'input> for ItemLinks {
1531 fn handle_broken_link(
1632 &mut self,
1733 link: BrokenLink<'input>,
1834 ) -> Option<(CowStr<'input>, CowStr<'input>)> {
3535+ // try to strip away inline markups in order to support stylized shorthand links
3636+ // for example, this extracts "std" from [`std`], removing the `inline code` markup
1937 let inner = if let CowStr::Borrowed(inner) = link.reference {
2020- let parse = stream(inner, mdbook_markdown());
3838+ // this is currently done by manually parsing the inner text, filtering
3939+ // the event stream, and then re-emitting it as text
4040+ //
4141+ // because of the 'input lifetime, this can only be done on CowStr::Borrowed,
4242+ // otherwise the re-emitted text "may not live long enough."
4343+ //
4444+ // this should be okay in usage, because this is only called by the Parser,
4545+ // which should only provide borrowed text.
4646+4747+ let parse = stream(inner, Self::OPTIONS);
21482249 let inner = parse
2350 .filter_map(|event| match event {
+8
crates/mdbookkit/src/bin/rustdoc_link/sync.rs
···2525 }
2626}
27272828+/// Some kind of [debouncing].
2929+///
3030+/// Listens to events over an [`mpsc::Receiver<Poll<T>>`] and [notifies][Notify]
3131+/// subscribers of [`Poll::Ready`], but only if they are not "immediately"
3232+/// followed by more [`Poll::Pending`], the timing of which is determined by a
3333+/// configured [buffering time][EventSampling::buffer].
3434+///
3535+/// [debouncing]: https://developer.mozilla.org/en-US/docs/Glossary/Debounce
2836#[derive(Debug, Clone)]
2937pub struct EventSampler<T> {
3038 state: Arc<RwLock<State<T>>>,
+4
crates/mdbookkit/src/diagnostics.rs
···2222}
23232424/// Trait for diagnostics classes. This is like a specific error code.
2525+///
2626+/// **For implementors:** The [`Display`] implementation, which is the title of each
2727+/// diagnostic message, should use plurals whenever possible, because error reporters
2828+/// may elect to group together multiple labels of the same [`Issue`]
2529pub trait Issue: Default + Debug + Display + Clone + Send + Sync {
2630 fn level(&self) -> Level;
2731}
+23-4
crates/mdbookkit/src/env.rs
···44use serde::Deserialize;
55use tap::Pipe;
6677+pub fn is_ci() -> Option<String> {
88+ let ci = std::env::var("CI").unwrap_or("".into());
99+ if matches!(ci.as_str(), "" | "0" | "false") {
1010+ None
1111+ } else {
1212+ Some(ci)
1313+ }
1414+}
1515+716/// Flag indicating how the program should proceed when there are warnings.
817///
918/// Used in preprocessor options.
···1322#[derive(Deserialize, Debug, Default, Clone, Copy)]
1423#[serde(rename_all = "lowercase")]
1524pub enum ErrorHandling {
1616- /// Fail if the environment variable `CI` is set to a value other than `0`.
2525+ /// Fail if the environment variable `CI` is set to a value other than `0` or `false`.
1726 /// Environments like GitHub Actions configure this automatically.
1827 #[default]
1928 #[serde(rename = "ci")]
···3544 .pipe(Err)
3645 }
3746 Self::Env => {
3838- let ci = std::env::var("CI").unwrap_or("".into());
3939- if matches!(ci.as_str(), "" | "0" | "false") {
4747+ let Some(ci) = Self::warning_as_error() else {
4048 return Ok(());
4141- }
4949+ };
4250 anyhow!("treating warnings as errors because fail-on-unresolved is \"ci\" and CI={ci}")
4351 .context("preprocessor has errors")
4452 .pipe(Err)
···4654 },
4755 _ => Ok(()),
4856 }
5757+ }
5858+5959+ pub fn adjusted<T, E>(&self, result: Result<Result<T, E>, E>) -> Result<Result<T, E>, E> {
6060+ match result {
6161+ Ok(Err(error)) if Self::warning_as_error().is_some() => Err(error),
6262+ result => result,
6363+ }
6464+ }
6565+6666+ fn warning_as_error() -> Option<String> {
6767+ is_ci()
4968 }
5069}
5170
+3-3
crates/mdbookkit/src/logging/terminal.rs
···17171818/// Either a [`console::Term`] or an [`env_logger::Logger`].
1919///
2020-/// This is automatically detected upon installing as the global logger. The logic is:
2020+/// This is automatically detected upon installation as the global logger. The logic is:
2121///
2222/// - If the `RUST_LOG` env var is set, this will use [`env_logger`].
2323/// - If stderr is not "user-attended", as determined by [`console::user_attended_stderr()`],
···7575 None
7676 } else if !console::user_attended_stderr() {
7777 // RUST_LOG not set but stderr isn't a terminal
7878- // log warnings and above
7979- Some(LevelFilter::Warn)
7878+ // log info and above
7979+ Some(LevelFilter::Info)
8080 } else {
8181 // use spinner instead
8282 Some(LevelFilter::Off)
+15-16
crates/mdbookkit/src/markdown.rs
···66use pulldown_cmark_to_cmark::{cmark, Error};
77use tap::Pipe;
8899-/// _Patch_ a Markdown string, instead of regenerating it entirely.
99+/// _Patch_ a Markdown string, instead of regenerating it entirely, in order to preserve
1010+/// as much of the original Markdown source as possible, especially with regard to whitespace.
1011///
1111-/// Currently, whitespace is NOT preserved when using [`pulldown_cmark_to_cmark`] to
1212-/// generate Markdown from a [`pulldown_cmark::Event`] stream.
1313-///
1414-/// This is problematic for mdBook preprocessors, because preprocessors downstream
1515-/// may need to work on syntax that is whitespace-sensitive. Normalizing all whitespace
1616-/// could cause such usage to no longer be recognized. An example is [`mdbook-alerts`][alerts]
1212+/// Currently, when using [`pulldown_cmark_to_cmark`] to generate Markdown from a
1313+/// [`pulldown_cmark::Event`] stream, whitespace is NOT preserved. This is problematic
1414+/// for mdBook preprocessors, because preprocessors downstream may need to work on
1515+/// syntax that is whitespace-sensitive. Normalizing all whitespace could cause such
1616+/// usage to no longer be recognized. An example is [`mdbook-alerts`][alerts]
1717/// which works on GitHub's ["alerts"][gh-alerts] syntax.
1818///
1919/// [alerts]: https://crates.io/crates/mdbook-alerts
···7474 /// **The yielded ranges must not overlap or decrease**, that is, for `span1` and
7575 /// `span2`, where `span1` is yielded before `span2`, `span1.end <= span2.start`.
7676 ///
7777- /// ## Panics
7777+ /// # Panics
7878 ///
7979 /// Panic if ranges in `stream` are not monotonically increasing.
8080 pub fn new(source: &'a str, stream: S) -> Self {
···102102}
103103104104/// <https://github.com/rust-lang/mdBook/blob/v0.4.47/src/utils/mod.rs#L197-L208>
105105-pub fn mdbook_markdown() -> Options {
106106- let mut opts = Options::empty();
107107- opts.insert(Options::ENABLE_TABLES);
108108- opts.insert(Options::ENABLE_FOOTNOTES);
109109- opts.insert(Options::ENABLE_STRIKETHROUGH);
110110- opts.insert(Options::ENABLE_TASKLISTS);
111111- opts.insert(Options::ENABLE_HEADING_ATTRIBUTES);
112112- opts
105105+pub const fn mdbook_markdown() -> Options {
106106+ Options::empty()
107107+ .union(Options::ENABLE_TABLES)
108108+ .union(Options::ENABLE_FOOTNOTES)
109109+ .union(Options::ENABLE_STRIKETHROUGH)
110110+ .union(Options::ENABLE_TASKLISTS)
111111+ .union(Options::ENABLE_HEADING_ATTRIBUTES)
113112}
114113115114pub type Spanned<T> = (T, Range<usize>);
+36-3
crates/mdbookkit/tests/link_forever.rs
···6666}
67676868#[test]
6969-#[ignore = "should run in CI"]
7069fn test_minimum_env() -> Result<()> {
7170 util::setup_logging();
7271···120119 log::info!("then: book builds with warnings");
121120 Command::new("mdbook")
122121 .arg("build")
122122+ .env("CI", "false")
123123 .env("PATH", &path)
124124 .current_dir(&root)
125125 .assert()
126126 .success()
127127 .stderr(predicate::str::contains("requires a git repository"));
128128129129+ log::info!("when: CI=true");
130130+131131+ log::info!("then: preprocessor fails");
132132+ Command::new("mdbook")
133133+ .arg("build")
134134+ .env("CI", "true")
135135+ .env("PATH", &path)
136136+ .current_dir(&root)
137137+ .assert()
138138+ .failure()
139139+ .stderr(predicate::str::contains("requires a git repository"));
140140+129141 log::info!("when: repo has no commit");
130142 Command::new("git")
131143 .arg("init")
···137149 log::info!("then: book builds with warnings");
138150 Command::new("mdbook")
139151 .arg("build")
152152+ .env("CI", "false")
140153 .env("PATH", &path)
141154 .current_dir(&root)
142155 .assert()
···159172 log::info!("then: book builds with warnings");
160173 Command::new("mdbook")
161174 .arg("build")
175175+ .env("CI", "false")
162176 .env("PATH", &path)
163177 .current_dir(&root)
164178 .assert()
···178192 .assert()
179193 .success();
180194181181- log::info!("then: book builds without warnings");
195195+ log::info!("then: book builds");
196196+ Command::new("mdbook")
197197+ .arg("build")
198198+ .env("PATH", &path)
199199+ .current_dir(&root)
200200+ .assert()
201201+ .success()
202202+ .stderr(predicate::str::contains("[WARN]").not())
203203+ .stderr(predicate::str::contains("using commit"));
204204+205205+ log::info!("when: HEAD is tagged");
206206+ Command::new("git")
207207+ .args(["tag", "v0.1.0", "HEAD"])
208208+ .env("PATH", &path)
209209+ .current_dir(&root)
210210+ .assert()
211211+ .success();
212212+213213+ log::info!("then: items are linked using tag instead of commit SHA");
182214 Command::new("mdbook")
183215 .arg("build")
184216 .env("PATH", &path)
185217 .current_dir(&root)
186218 .assert()
187219 .success()
188188- .stderr(predicate::str::contains("[WARN]").not());
220220+ .stderr(predicate::str::contains("[WARN]").not())
221221+ .stderr(predicate::str::contains("using tag \"v0.1.0\""));
189222190223 Ok(())
191224}
+4-3
crates/mdbookkit/tests/rustdoc_link.rs
···7979 test_document!("../../../docs/src/rustdoc-link/supported-syntax.md"),
8080 test_document!("../../../docs/src/rustdoc-link/known-issues.md"),
8181 test_document!("../../../docs/src/rustdoc-link/getting-started.md"),
8282- test_document!("../../../docs/src/rustdoc-link.md"),
8282+ test_document!("../../../docs/src/rustdoc-link/index.md"),
8383 test_document!("tests/ra-known-quirks.md"),
8484 ];
8585···120120}
121121122122#[test]
123123-#[ignore = "should run in CI"]
123123+#[ignore = "should run in a dedicated environment"]
124124fn test_minimum_env() -> Result<()> {
125125 util::setup_logging();
126126···237237 .stderr(
238238 predicate::str::contains("failed to spawn rust-analyzer")
239239 // https://github.com/rust-lang/rustup/issues/3846
240240- // rustup shims rust-analyzer even when it's not installed
240240+ // rustup shims rust-analyzer when it's not installed
241241 .or(predicate::str::contains("Unknown binary 'rust-analyzer")),
242242+ // ^ doesn't have a closing `'` because on windows it says 'rust-analyzer.exe'
242243 );
243244244245 log::info!("when: code extension is installed");
···62626363
64646565-To read more about this project, feel free to return to
6666-[Overview](../rustdoc-link.md#overview).
6565+To read more about this project, feel free to return to [Overview](index.md#overview).
67666867> [!IMPORTANT]
6968>
···89889089<!-- prettier-ignore-start -->
91909292-[preprocessor]: https://rust-lang.github.io/mdBook/format/configuration/preprocessors.html
9393-[rust-analyzer]: https://rust-analyzer.github.io/
9494-[ra-install]: https://rust-analyzer.github.io/book/rust_analyzer_binary.html
9191+[gh-releases]: https://github.com/tonywu6/mdbookkit/releases
9592[open-docs]: https://rust-analyzer.github.io/book/features.html#open-docs
9393+[preprocessor]: https://rust-lang.github.io/mdBook/format/configuration/preprocessors.html
9694[ra-extension]: https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer
9797-[gh-releases]: https://github.com/tonywu6/mdbookkit/releases
9595+[ra-install]: https://rust-analyzer.github.io/book/rust_analyzer_binary.html
9696+[rust-analyzer]: https://rust-analyzer.github.io/
98979998<!-- prettier-ignore-end -->
···1111- [Incorrect links](#incorrect-links)
1212 - [Macros](#macros)
1313 - [Trait items](#trait-items)
1414+ - [Private items](#private-items)
1415- [Unresolved items](#unresolved-items)
1516 - [Associated items on primitive types](#associated-items-on-primitive-types)
1617- [Sites other than docs.rs](#sites-other-than-docsrs)
···31323233## Incorrect links
33343434-In limited circumstances, rust-analyzer generates links that are incorrect or
3535-inaccessible. Unfortunately, for such items, you will still have to link by hand.
3535+In limited circumstances, the preprocessor generates links that are incorrect or
3636+inaccessible.
36373738> [!NOTE]
3839>
···7879https://doc.rust-lang.org/stable/core/net/enum.IpAddr.html#method.from<strong>-1</strong>
7980</a>
80818282+### Private items
8383+8484+rustdoc has a [`private_intra_doc_links`][private_intra_doc_links] lint that warns you
8585+when your public documentation tries to link to private items.
8686+8787+The preprocessor does not yet warn you about links to private items: rust-analyzer will
8888+generate links for items regardless of their crate-level visibility.
8989+8190## Unresolved items
82918392### Associated items on primitive types
···123132124133<!-- prettier-ignore-start -->
125134135135+[IpV6Addr]: https://doc.rust-lang.org/stable/core/net/enum.IpAddr.html#method.from-1
126136[macro_export]: https://doc.rust-lang.org/stable/reference/macros-by-example.html#path-based-scope
127137[panic]: https://doc.rust-lang.org/stable/std/macro.panic.html
138138+[private_intra_doc_links]: https://doc.rust-lang.org/rustdoc/lints.html#private_intra_doc_links
128139[serde_json::json]: https://docs.rs/serde_json/1.0.140/serde_json/macro.json.html
140140+[sourcemap]: https://developer.mozilla.org/en-US/docs/Glossary/Source_map
129141[tokio::main]: https://docs.rs/tokio-macros/2.5.0/tokio_macros/attr.main.html
130130-[IpV6Addr]: https://doc.rust-lang.org/stable/core/net/enum.IpAddr.html#method.from-1
131131-[sourcemap]: https://developer.mozilla.org/en-US/docs/Glossary/Source_map
132142133143<!-- prettier-ignore-end -->
···33expression: report
44---
55 info: successfully resolved all links
66- ╭─[known-issues:44:3]
77- 43 │
88- 44 │ - [~~`panic!`~~], and many other `std` macros
66+ ╭─[known-issues:45:3]
77+ 44 │
88+ 45 │ - [~~`panic!`~~], and many other `std` macros
99 · ───────┬──────
1010 · ╰── https://doc.rust-lang.org/stable/std/macros/macro.panic.html
1111- 45 │ - The correct link is
1212- 46 │ [https://doc.rust-lang.org/stable/std~~/macros~~/macro.panic.html][panic]
1313- 47 │ - [~~`serde_json::json!`~~]
1111+ 46 │ - The correct link is
1212+ 47 │ [https://doc.rust-lang.org/stable/std~~/macros~~/macro.panic.html][panic]
1313+ 48 │ - [~~`serde_json::json!`~~]
1414 · ────────────┬────────────
1515 · ╰── https://docs.rs/serde_json/1.0.140/serde_json/macros/macro.json.html
1616- 48 │ - The correct link is
1717- 49 │ [https://docs.rs/serde_json/1.0.140/serde_json~~/macros~~/macro.json.html][serde_json::json]
1818- 50 │
1919- 51 │ Attribute macros generate links that use `macro.<macro_name>.html`, but rustdoc actually
2020- 52 │ generates `attr.<macro_name>.html`. For example:
2121- 53 │
2222- 54 │ - [~~`tokio::main!`~~]
1616+ 49 │ - The correct link is
1717+ 50 │ [https://docs.rs/serde_json/1.0.140/serde_json~~/macros~~/macro.json.html][serde_json::json]
1818+ 51 │
1919+ 52 │ Attribute macros generate links that use `macro.<macro_name>.html`, but rustdoc actually
2020+ 53 │ generates `attr.<macro_name>.html`. For example:
2121+ 54 │
2222+ 55 │ - [~~`tokio::main!`~~]
2323 · ──────────┬─────────
2424 · ╰── https://docs.rs/tokio-macros/2.5.0/tokio_macros/macro.main.html
2525- 55 │ - The correct link is
2626- 56 │ [https://docs.rs/tokio-macros/2.5.0/tokio_macros/~~macro~~attr.main.html][tokio::main]
2727- 57 │
2828- 58 │ ### Trait items
2929- 59 │
3030- 60 │ Rust allows methods to have the same name if they are from different traits, and types
3131- 61 │ can implement the same trait multiple times if the trait is generic. All such methods
3232- 62 │ will appear on the same page for the type.
3333- 63 │
3434- 64 │ rustdoc will number the generated URL fragments so that they remain unique within the
3535- 65 │ HTML document. rust-analyzer does not yet have the ability to do so.
3636- 66 │
3737- 67 │ For example, these are the same links:
3838- 68 │
3939- 69 │ - [`<std::net::IpAddr as From<std::net::Ipv4Addr>>::from`]
2525+ 56 │ - The correct link is
2626+ 57 │ [https://docs.rs/tokio-macros/2.5.0/tokio_macros/~~macro~~attr.main.html][tokio::main]
2727+ 58 │
2828+ 59 │ ### Trait items
2929+ 60 │
3030+ 61 │ Rust allows methods to have the same name if they are from different traits, and types
3131+ 62 │ can implement the same trait multiple times if the trait is generic. All such methods
3232+ 63 │ will appear on the same page for the type.
3333+ 64 │
3434+ 65 │ rustdoc will number the generated URL fragments so that they remain unique within the
3535+ 66 │ HTML document. rust-analyzer does not yet have the ability to do so.
3636+ 67 │
3737+ 68 │ For example, these are the same links:
3838+ 69 │
3939+ 70 │ - [`<std::net::IpAddr as From<std::net::Ipv4Addr>>::from`]
4040 · ────────────────────────────┬───────────────────────────
4141 · ╰── https://doc.rust-lang.org/stable/core/net/ip_addr/enum.IpAddr.html#method.from
4242- 70 │ - [`<std::net::IpAddr as From<std::net::Ipv6Addr>>::from`]
4242+ 71 │ - [`<std::net::IpAddr as From<std::net::Ipv6Addr>>::from`]
4343 · ────────────────────────────┬───────────────────────────
4444 · ╰── https://doc.rust-lang.org/stable/core/net/ip_addr/enum.IpAddr.html#method.from
4545- 71 │
4545+ 72 │
4646 ╰────
···39394040<figure>
41414242-
4242+
43434444</figure>
45454646## Overview
47474848-To get started, simply follow the [quickstart guide](rustdoc-link/getting-started.md)!
4848+To get started, simply follow the [quickstart guide](getting-started.md)!
49495050If you would like to read more about this crate:
51515252For **writing documentation** —
53535454- To learn more about how it is resolving items into links, including
5555- [feature-gated items](rustdoc-link/name-resolution.md#feature-gated-items), see
5656- [Name resolution](rustdoc-link/name-resolution.md).
5555+ [feature-gated items](name-resolution.md#feature-gated-items), see
5656+ [Name resolution](name-resolution.md).
5757- To know how to link to other types of items like
5858- [functions, macros](rustdoc-link/supported-syntax.md#functions-and-macros), and
5959- [implementors](rustdoc-link/supported-syntax.md#implementors-and-fully-qualified-syntax),
6060- see [Supported syntax](rustdoc-link/supported-syntax.md).
5858+ [functions, macros](supported-syntax.md#functions-and-macros), and
5959+ [implementors](supported-syntax.md#implementors-and-fully-qualified-syntax), see
6060+ [Supported syntax](supported-syntax.md).
61616262For **adapting this crate to your project** —
63636464- If you use [Cargo workspaces][workspaces], see specific instructions in
6565- [Workspace layout](rustdoc-link/workspace-layout.md).
6565+ [Workspace layout](workspace-layout.md).
6666- If you are working on a large project, and processing is taking a long time, see the
6767- discussion in [Caching](rustdoc-link/caching.md).
6767+ discussion in [Caching](caching.md).
68686969For **additional usage information** —
70707171- You can use this as a standalone command line tool: see
7272- [Standalone usage](rustdoc-link/standalone-usage.md).
7373-- For all available options and how to set them, see
7474- [Configuration](rustdoc-link/configuration.md).
7575-- Finally, review [Known issues](rustdoc-link/known-issues.md) and limitations.
7272+ [Standalone usage](standalone-usage.md).
7373+- For tips on using this in CI, see [Continuous integration](continuous-integration.md).
7474+- For all available options and how to set them, see [Configuration](configuration.md).
7575+- Finally, review [Known issues](known-issues.md) and limitations.
76767777Happy linking!
7878
···165165>
166166> [`std::vec`](https://doc.rust-lang.org/stable/alloc/vec/index.html "std::vec"), [`mod@std::vec`](https://doc.rust-lang.org/stable/alloc/vec/index.html "mod@std::vec"), and [`macro@std::vec`](https://doc.rust-lang.org/stable/alloc/vec/index.html "macro@std::vec") all link to the `vec` _module_.
167167168168-Currently, duplicate names in Rust are allowed only if they correspond to items in
169169-different [namespaces], for example, between macros and modules, and between struct
170170-fields and methods — this is mostly covered by the function and macro syntax, described
171171-[above](#functions-and-macros).
168168+This is largely okay because currently, duplicate names in Rust are allowed only if they
169169+correspond to items in different [namespaces], for example, between macros and modules,
170170+and between struct fields and methods — this is mostly covered by the function and macro
171171+syntax, described [above](#functions-and-macros).
172172173173If you encounter items that must be disambiguated using rustdoc's disambiguator syntax,
174174other than [the "special types" listed below](#special-types), please [file an
···166166 · │ ╰── https://doc.rust-lang.org/stable/alloc/vec/index.html
167167 · ╰── https://doc.rust-lang.org/stable/alloc/vec/index.html
168168 163 │
169169- 164 │ Currently, duplicate names in Rust are allowed only if they correspond to items in
170170- 165 │ different [namespaces], for example, between macros and modules, and between struct
171171- 166 │ fields and methods — this is mostly covered by the function and macro syntax, described
172172- 167 │ [above](#functions-and-macros).
169169+ 164 │ This is largely okay because currently, duplicate names in Rust are allowed only if they
170170+ 165 │ correspond to items in different [namespaces], for example, between macros and modules,
171171+ 166 │ and between struct fields and methods — this is mostly covered by the function and macro
172172+ 167 │ syntax, described [above](#functions-and-macros).
173173 168 │
174174 169 │ If you encounter items that must be disambiguated using rustdoc's disambiguator syntax,
175175 170 │ other than [the "special types" listed below](#special-types), please [file an
···11+# INTERNAL: Repo README
22+33+> [!NOTE]
44+>
55+> This page is included only for link validation during build.
66+77+{{#include ../../../README.md}}
+1-1
docs/src/lib.rs
···77use mdbookkit::bin::rustdoc_link::Resolver;
8899mod env {
1010- pub use mdbookkit::bin::rustdoc_link::env::Config;
1010+ pub use mdbookkit::{bin::rustdoc_link::env::Config, env::is_ci};
1111}
···66more hard-coded GitHub URLs.
7788```md
99-Here's a link to the crate's [Cargo manifest](../../crates/mdbookkit/Cargo.toml).
99+Here's a link to the [Cargo workspace manifest](../../../Cargo.toml).
1010```
11111212<figure class="fig-text">
13131414-Here's a link to the crate's [Cargo manifest](../../crates/mdbookkit/Cargo.toml).
1414+Here's a link to the [Cargo workspace manifest](../../../Cargo.toml).
15151616</figure>
17171818- Versions are determined at build time. Supports both tags and commit hashes.
1919- Because paths are readily accessible at build time, it also
2020- [validates](link-forever/features.md#link-validation) them for you.
2020+ [validates](features.md#link-validation) them for you.
21212222## Getting started
2323···44443. Link to files using paths, like this:
45454646 ```md
4747- See [`book.toml`](../book.toml#L44-L48) for an example config.
4747+ See [`book.toml`](../../book.toml#L44-L48) for an example config.
4848 ```
49495050 <figure class="fig-text">
51515252- See [`book.toml`](../book.toml#L44-L48) for an example config.
5252+ See [`book.toml`](../../book.toml#L44-L48) for an example config.
53535454 </figure>
5555
+85
docs/src/link-forever/continuous-integration.md
···11+# Continuous integration
22+33+This page gives information and tips for using `mdbook-link-forever` in a continuous
44+integration (CI) environment.
55+66+The preprocessor behaves differently in terms of logging, error handling, etc., when it
77+detects it is running in CI.
88+99+<details class="toc" open>
1010+ <summary>Sections</summary>
1111+1212+- [Detecting CI](#detecting-ci)
1313+- [Linking to Git tags](#linking-to-git-tags)
1414+- [Logging](#logging)
1515+- [Error handling](#error-handling)
1616+1717+</details>
1818+1919+## Detecting CI
2020+2121+To determine whether it is running in CI, the preprocessor honors the `CI` environment
2222+variable. Specifically:
2323+2424+- If `CI` is set to `"true"`, then it is considered in CI[^ci-true];
2525+- Otherwise, it is considered not in CI.
2626+2727+Most major CI/CD services, such as [GitHub Actions][github-actions-ci] and [GitLab
2828+CI/CD][gitlab-ci], automatically configure this variable for you.
2929+3030+## Linking to Git tags
3131+3232+The preprocessor supports both tags and commit SHAs when generating permalinks. The use
3333+of tags is contingent on HEAD being tagged in local Git at build time. You should ensure
3434+that tags are present when building in CI, or the preprocessor will fallback to using
3535+the full SHA (it would still be a permalink, just that it will be more verbose).
3636+3737+For example, in [GitHub Actions][actions/checkout], you can use:
3838+3939+```yaml
4040+steps:
4141+ - uses: actions/checkout@v4
4242+ with:
4343+ fetch-tags: true
4444+ fetch-depth: 0 # https://github.com/actions/checkout/issues/1471#issuecomment-1771231294
4545+```
4646+4747+## Logging
4848+4949+By default, the preprocessor shows a progress spinner when it is running.
5050+5151+When running in CI, progress is instead printed as logs (using [log] and
5252+[env_logger])[^stderr].
5353+5454+You can control logging levels using the [`RUST_LOG`] environment variable.
5555+5656+## Error handling
5757+5858+By default, when the preprocessor encounters any non-fatal issues, such as when a link
5959+fails to resolve, it prints them as warnings but continues to run. This is so that your
6060+book continues to build via `mdbook serve` while you make edits.
6161+6262+When running in CI, all such warnings are promoted to errors. The preprocessor will exit
6363+with a non-zero status code when there are warnings, which will fail your build. This
6464+prevents outdated or incorrect links from being accidentally deployed.
6565+6666+You can explicitly control this behavior using the
6767+[`fail-on-warnings`](configuration.md#fail-on-warnings) option.
6868+6969+[^ci-true]:
7070+ Specifically, when `CI` is anything other than `""`, `"0"`, or `"false"`. The logic
7171+ is encapsulated in the [`is_ci`][crate::env::is_ci] function.
7272+7373+[^stderr]:
7474+ Specifically, when stderr is redirected to something that isn't a terminal, such as
7575+ a file.
7676+7777+<!-- prettier-ignore-start -->
7878+7979+[`RUST_LOG`]: https://docs.rs/env_logger/latest/env_logger/#enabling-logging
8080+[actions/checkout]: https://github.com/actions/checkout
8181+[github-actions-ci]: https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/store-information-in-variables#default-environment-variables
8282+[gitlab-ci]: https://docs.gitlab.com/ci/variables/predefined_variables/
8383+[rustup-ra]: https://rust-analyzer.github.io/book/rust_analyzer_binary.html#rustup
8484+8585+<!-- prettier-ignore-end -->
+9-7
docs/src/link-forever/features.md
···33333434> ```md
3535> This book uses [esbuild] to
3636-> [preprocess its style sheet](../../app/build/build.ts#L7-L16).
3636+> [preprocess its style sheet](../../app/build/build.ts#L13-L24).
3737> ```
3838>
3939> This book uses [esbuild] to
4040-> [preprocess its style sheet](../../app/build/build.ts#L7-L16).
4040+> [preprocess its style sheet](../../app/build/build.ts#L13-L24).
41414242By default, links to files under your book's `src/` directory are not converted, since
4343mdBook already [copies them to build output][mdbook-src-build], but this is configurable
···4949places, in order:
505051511. The [`output.html.git-repository-url`] option in your `book.toml`
5252-2. The URL of a Git remote named `origin`
5252+2. The URL of a Git remote named `origin`[^1]
53535454> [!TIP]
5555>
···87878888<!-- prettier-ignore-start -->
89899090-[vscode-path-completions]: https://code.visualstudio.com/docs/languages/markdown#_path-completions
9090+[`mdbook-linkcheck`]: https://github.com/Michael-F-Bryan/mdbook-linkcheck
9191+[`output.html.git-repository-url`]: https://rust-lang.github.io/mdBook/format/configuration/renderers.html#html-renderer-options
9292+[esbuild]: https://esbuild.github.io
9193[github-relative-links]: https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax#relative-links
9294[link-validation]: https://code.visualstudio.com/docs/languages/markdown#_link-validation
9395[mdbook-src-build]: https://rust-lang.github.io/mdBook/guide/creating.html#source-files
9494-[`output.html.git-repository-url`]: https://rust-lang.github.io/mdBook/format/configuration/renderers.html#html-renderer-options
9595-[esbuild]: https://esbuild.github.io
9696-[`mdbook-linkcheck`]: https://github.com/Michael-F-Bryan/mdbook-linkcheck
9696+[vscode-path-completions]: https://code.visualstudio.com/docs/languages/markdown#_path-completions
97979898<!-- prettier-ignore-end -->
9999+100100+[^1]: The remote must be exactly named `origin`. No other name is recognized.
+4-4
docs/src/link-forever/working-with-include.md
···1616This page describes some workarounds for linking in included files.
17171818- [Using absolute paths](#using-absolute-paths)
1919-- [Using URLs to link to book pages](#using-urls-to-link-to-book-pages)
1919+- [Extra feature: Using URLs to link to book pages](#extra-feature-using-urls-to-link-to-book-pages)
20202121## Using absolute paths
2222···3636> This is also the behavior both in [VS Code][vscode-path-completions] and on
3737> [GitHub][github-relative-links].
38383939-## Using URLs to link to book pages
3939+## Extra feature: Using URLs to link to book pages
40404141You may be in a situation where you have to use full URLs to link to your book rather
4242than relying on paths.
···82828383<!-- prettier-ignore-start -->
84848585+[cargo-readme]: https://doc.rust-lang.org/cargo/reference/manifest.html#the-readme-field
8686+[github-relative-links]: https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax#relative-links
8587[mdbook-include]: https://rust-lang.github.io/mdBook/format/mdbook.html#including-files
8688[vscode-path-completions]: https://code.visualstudio.com/docs/languages/markdown#_path-completions
8787-[github-relative-links]: https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax#relative-links
8888-[cargo-readme]: https://doc.rust-lang.org/cargo/reference/manifest.html#the-readme-field
89899090<!-- prettier-ignore-end -->
···35353636<figure>
37373838-
3838+
39394040</figure>
41414242## Overview
43434444-To get started, simply follow the [quickstart guide](rustdoc-link/getting-started.md)!
4444+To get started, simply follow the [quickstart guide](getting-started.md)!
45454646If you would like to read more about this crate:
47474848For **writing documentation** —
49495050- To learn more about how it is resolving items into links, including
5151- [feature-gated items](rustdoc-link/name-resolution.md#feature-gated-items), see
5252- [Name resolution](rustdoc-link/name-resolution.md).
5151+ [feature-gated items](name-resolution.md#feature-gated-items), see
5252+ [Name resolution](name-resolution.md).
5353- To know how to link to other types of items like
5454- [functions, macros](rustdoc-link/supported-syntax.md#functions-and-macros), and
5555- [implementors](rustdoc-link/supported-syntax.md#implementors-and-fully-qualified-syntax),
5656- see [Supported syntax](rustdoc-link/supported-syntax.md).
5454+ [functions, macros](supported-syntax.md#functions-and-macros), and
5555+ [implementors](supported-syntax.md#implementors-and-fully-qualified-syntax), see
5656+ [Supported syntax](supported-syntax.md).
57575858For **adapting this crate to your project** —
59596060- If you use [Cargo workspaces][workspaces], see specific instructions in
6161- [Workspace layout](rustdoc-link/workspace-layout.md).
6161+ [Workspace layout](workspace-layout.md).
6262- If you are working on a large project, and processing is taking a long time, see the
6363- discussion in [Caching](rustdoc-link/caching.md).
6363+ discussion in [Caching](caching.md).
64646565For **additional usage information** —
66666767- You can use this as a standalone command line tool: see
6868- [Standalone usage](rustdoc-link/standalone-usage.md).
6969-- For all available options and how to set them, see
7070- [Configuration](rustdoc-link/configuration.md).
7171-- Finally, review [Known issues](rustdoc-link/known-issues.md) and limitations.
6868+ [Standalone usage](standalone-usage.md).
6969+- For tips on using this in CI, see [Continuous integration](continuous-integration.md).
7070+- For all available options and how to set them, see [Configuration](configuration.md).
7171+- Finally, review [Known issues](known-issues.md) and limitations.
72727373Happy linking!
7474
+4-4
docs/src/rustdoc-link/caching.md
···11# Caching
2233By default, `mdbook-rustdoc-link` spawns a fresh `rust-analyzer` process every time it
44-is run. `rust-analyzer` then reindexes your entire project before resolving links.
44+is run. rust-analyzer then reindexes your entire project before resolving links.
5566This significantly impacts the responsiveness of `mdbook serve` — it is as if for every
77live reload, you had to reopen your editor, and it gets even worse the more dependencies
···8585## Help wanted 🙌
86868787The cache feature, as it currently stands, is a workaround at best. If you have insights
8888-on how performance could be further improved, please [open an issue!][gh-issues].
8888+on how performance could be further improved, please [open an issue!][gh-issues]
89899090### Cache priming and progress tracking
9191···9797cache priming, before actually sending out external docs requests. This requires parsing
9898non-structured log messages that rust-analyzer sends out and some debouncing/throttling
9999logic, which is not ideal, see
100100-[client.rs](/crates/mdbookkit/src/bin/rustdoc_link/client.rs#L129-L132).
100100+[client.rs](/crates/mdbookkit/src/bin/rustdoc_link/client.rs#L142).
101101102102Not waiting for indexing to finish and sending out requests too early causes
103103rust-analyzer to respond with empty results.
···151151152152<!-- prettier-ignore-start -->
153153154154+[`ra-multiplex`]: https://github.com/pr2502/ra-multiplex
154155[gh-issues]: https://github.com/tonywu6/mdbookkit/issues
155156[lsp-work-done-progress]: https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#workDoneProgress
156157[ra-architecture]: https://rust-analyzer.github.io/book/contributing/architecture.html#:~:text=The%20analyzer%20keeps%20all%20this%20input%20data%20in%20memory%20and%20never%20does%20any%20IO.
157158[ra-cache-priming]: https://rust-analyzer.github.io/book/configuration.html?highlight=cache%20priming#configuration
158159[ra-persistent-cache]: https://github.com/rust-lang/rust-analyzer/issues/4712
159159-[`ra-multiplex`]: https://github.com/pr2502/ra-multiplex
160160[salsa]: https://rust-analyzer.github.io/thisweek/2025/03/17/changelog-277.html
161161[specify-exclude-patterns]: https://rust-lang.github.io/mdBook/cli/serve.html#specify-exclude-patterns
162162
+90
docs/src/rustdoc-link/continuous-integration.md
···11+# Continuous integration
22+33+This page gives information and tips for using `mdbook-rustdoc-link` in a continuous
44+integration (CI) environment.
55+66+The preprocessor behaves differently in terms of logging, error handling, etc., when it
77+detects it is running in CI.
88+99+<details class="toc" open>
1010+ <summary>Sections</summary>
1111+1212+- [Detecting CI](#detecting-ci)
1313+- [Installing rust-analyzer](#installing-rust-analyzer)
1414+- [Logging](#logging)
1515+- [Error handling](#error-handling)
1616+1717+</details>
1818+1919+## Detecting CI
2020+2121+To determine whether it is running in CI, the preprocessor honors the `CI` environment
2222+variable. Specifically:
2323+2424+- If `CI` is set to `"true"`, then it is considered in CI[^ci-true];
2525+- Otherwise, it is considered not in CI.
2626+2727+Most major CI/CD services, such as [GitHub Actions][github-actions-ci] and [GitLab
2828+CI/CD][gitlab-ci], automatically configure this variable for you.
2929+3030+## Installing rust-analyzer
3131+3232+rust-analyzer must be on `PATH` when running in CI[^ra-on-path].
3333+3434+One way is to install it via [rustup][rustup-ra]. For example, in [GitHub
3535+Actions][dtolnay/rust-toolchain], you can use:
3636+3737+```yaml
3838+steps:
3939+ - uses: dtolnay/rust-toolchain@stable
4040+ with:
4141+ components: rust-analyzer
4242+```
4343+4444+> [!NOTE]
4545+>
4646+> Be aware that rust-analyzer from rustup follows Rust's release schedule, which means
4747+> it may lag behind the version bundled with the VS Code extension.
4848+4949+## Logging
5050+5151+By default, the preprocessor shows a progress spinner when it is running.
5252+5353+When running in CI, progress is instead printed as logs (using [log] and
5454+[env_logger])[^stderr].
5555+5656+You can control logging levels using the [`RUST_LOG`] environment variable.
5757+5858+## Error handling
5959+6060+By default, when the preprocessor encounters any non-fatal issues, such as when a link
6161+fails to resolve, it prints them as warnings but continues to run. This is so that your
6262+book continues to build via `mdbook serve` while you make edits.
6363+6464+When running in CI, all such warnings are promoted to errors. The preprocessor will exit
6565+with a non-zero status code when there are warnings, which will fail your build. This
6666+prevents outdated or incorrect links from being accidentally deployed.
6767+6868+You can explicitly control this behavior using the
6969+[`fail-on-warnings`](configuration.md#fail-on-warnings) option.
7070+7171+[^ra-on-path]:
7272+ Unless you use the [`rust-analyzer`](configuration.md#rust-analyzer) option.
7373+7474+[^ci-true]:
7575+ Specifically, when `CI` is anything other than `""`, `"0"`, or `"false"`. The logic
7676+ is encapsulated in the [`is_ci`][crate::env::is_ci] function.
7777+7878+[^stderr]:
7979+ Specifically, when stderr is redirected to something that isn't a terminal, such as
8080+ a file.
8181+8282+<!-- prettier-ignore-start -->
8383+8484+[`RUST_LOG`]: https://docs.rs/env_logger/latest/env_logger/#enabling-logging
8585+[dtolnay/rust-toolchain]: https://github.com/dtolnay/rust-toolchain
8686+[github-actions-ci]: https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/store-information-in-variables#default-environment-variables
8787+[gitlab-ci]: https://docs.gitlab.com/ci/variables/predefined_variables/
8888+[rustup-ra]: https://rust-analyzer.github.io/book/rust_analyzer_binary.html#rustup
8989+9090+<!-- prettier-ignore-end -->
+5-6
docs/src/rustdoc-link/getting-started.md
···58585959
60606161-To read more about this project, feel free to return to
6262-[Overview](../rustdoc-link.md#overview).
6161+To read more about this project, feel free to return to [Overview](index.md#overview).
63626463> [!IMPORTANT]
6564>
···85848685<!-- prettier-ignore-start -->
87868888-[preprocessor]: https://rust-lang.github.io/mdBook/format/configuration/preprocessors.html
8989-[rust-analyzer]: https://rust-analyzer.github.io/
9090-[ra-install]: https://rust-analyzer.github.io/book/rust_analyzer_binary.html
8787+[gh-releases]: https://github.com/tonywu6/mdbookkit/releases
9188[open-docs]: https://rust-analyzer.github.io/book/features.html#open-docs
8989+[preprocessor]: https://rust-lang.github.io/mdBook/format/configuration/preprocessors.html
9290[ra-extension]: https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer
9393-[gh-releases]: https://github.com/tonywu6/mdbookkit/releases
9191+[ra-install]: https://rust-analyzer.github.io/book/rust_analyzer_binary.html
9292+[rust-analyzer]: https://rust-analyzer.github.io/
94939594<!-- prettier-ignore-end -->
+14-4
docs/src/rustdoc-link/known-issues.md
···77- [Incorrect links](#incorrect-links)
88 - [Macros](#macros)
99 - [Trait items](#trait-items)
1010+ - [Private items](#private-items)
1011- [Unresolved items](#unresolved-items)
1112 - [Associated items on primitive types](#associated-items-on-primitive-types)
1213- [Sites other than docs.rs](#sites-other-than-docsrs)
···27282829## Incorrect links
29303030-In limited circumstances, rust-analyzer generates links that are incorrect or
3131-inaccessible. Unfortunately, for such items, you will still have to link by hand.
3131+In limited circumstances, the preprocessor generates links that are incorrect or
3232+inaccessible.
32333334> [!NOTE]
3435>
···7475https://doc.rust-lang.org/stable/core/net/enum.IpAddr.html#method.from<strong>-1</strong>
7576</a>
76777878+### Private items
7979+8080+rustdoc has a [`private_intra_doc_links`][private_intra_doc_links] lint that warns you
8181+when your public documentation tries to link to private items.
8282+8383+The preprocessor does not yet warn you about links to private items: rust-analyzer will
8484+generate links for items regardless of their crate-level visibility.
8585+7786## Unresolved items
78877988### Associated items on primitive types
···119128120129<!-- prettier-ignore-start -->
121130131131+[IpV6Addr]: https://doc.rust-lang.org/stable/core/net/enum.IpAddr.html#method.from-1
122132[macro_export]: https://doc.rust-lang.org/stable/reference/macros-by-example.html#path-based-scope
123133[panic]: https://doc.rust-lang.org/stable/std/macro.panic.html
134134+[private_intra_doc_links]: https://doc.rust-lang.org/rustdoc/lints.html#private_intra_doc_links
124135[serde_json::json]: https://docs.rs/serde_json/1.0.140/serde_json/macro.json.html
136136+[sourcemap]: https://developer.mozilla.org/en-US/docs/Glossary/Source_map
125137[tokio::main]: https://docs.rs/tokio-macros/2.5.0/tokio_macros/attr.main.html
126126-[IpV6Addr]: https://doc.rust-lang.org/stable/core/net/enum.IpAddr.html#method.from-1
127127-[sourcemap]: https://developer.mozilla.org/en-US/docs/Glossary/Source_map
128138129139<!-- prettier-ignore-end -->
+3-3
docs/src/rustdoc-link/name-resolution.md
···4949> [`FromIterator`] is in the prelude starting from Rust 2021.
50505151Though technically not required — to make items from your crate more distinguishable
5252-from others in the Markdown source code, you can write `crate::*`:
5252+from others in your Markdown source, you can write `crate::*`:
53535454> ```md
5555> [Configurations](configuration.md) for the preprocessor is defined in the
···184184185185<!-- prettier-ignore-start -->
186186187187-[rustdoc-scoping]: https://doc.rust-lang.org/rustdoc/write-documentation/linking-to-items-by-name.html#valid-links
188187[didOpen]: https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_didOpen
189189-[why-lsp]: https://matklad.github.io/2022/04/25/why-lsp.html#Alternative-Theory:~:text=a%20language%20server%20must%20analyze%20any%20invalid%20program%20as%20best%20as%20it%20can.%20Working%20with%20incomplete%20and%20invalid%20programs%20is%20the%20first%20complication%20of%20a%20language%20server%20in%20comparison%20to%20a%20compiler.
190188[externalDocs]: https://rust-analyzer.github.io/book/contributing/lsp-extensions.html#open-external-documentation
189189+[rustdoc-scoping]: https://doc.rust-lang.org/rustdoc/write-documentation/linking-to-items-by-name.html#valid-links
190190+[why-lsp]: https://matklad.github.io/2022/04/25/why-lsp.html#Alternative-Theory:~:text=a%20language%20server%20must%20analyze%20any%20invalid%20program%20as%20best%20as%20it%20can.%20Working%20with%20incomplete%20and%20invalid%20programs%20is%20the%20first%20complication%20of%20a%20language%20server%20in%20comparison%20to%20a%20compiler.
191191192192<!-- prettier-ignore-end -->
+4-4
docs/src/rustdoc-link/supported-syntax.md
···161161>
162162> [`std::vec`], [`mod@std::vec`], and [`macro@std::vec`] all link to the `vec` _module_.
163163164164-Currently, duplicate names in Rust are allowed only if they correspond to items in
165165-different [namespaces], for example, between macros and modules, and between struct
166166-fields and methods — this is mostly covered by the function and macro syntax, described
167167-[above](#functions-and-macros).
164164+This is largely okay because currently, duplicate names in Rust are allowed only if they
165165+correspond to items in different [namespaces], for example, between macros and modules,
166166+and between struct fields and methods — this is mostly covered by the function and macro
167167+syntax, described [above](#functions-and-macros).
168168169169If you encounter items that must be disambiguated using rustdoc's disambiguator syntax,
170170other than [the "special types" listed below](#special-types), please [file an
+2-2
docs/src/rustdoc-link/workspace-layout.md
···1616Error: Cargo.toml does not have any lib or bin target
1717```
18181919-This means it found your workspace `Cargo.toml` instead of a crate manifest. To use the
1919+This means it found your workspace `Cargo.toml` instead of a member crate's. To use the
2020preprocessor in this case, some extra setup is needed.
21212222<details class="toc" open>
···90909191A possible workaround would be to turn your book folder into a private crate that
9292depends on the crates you would like to document. Then you can link to them as if they
9393-are third-party crates.
9393+were third-party crates.
94949595```
9696my-workspace/
+14
release-plz.toml
···3344[[package]]
55name = "mdbookkit"
66+77+[changelog]
88+header = """# CHANGELOG
99+1010+The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and
1111+this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
1212+1313+This file is autogenerated using [release-plz](https://release-plz.dev).
1414+1515+## Unreleased
1616+1717+> Unreleased changes appear here, if any.
1818+1919+"""