···3737 match self {
3838 Self::Unresolved => Level::WARN,
3939 Self::Debug => Level::TRACE,
4040- Self::Ok => Level::INFO,
4040+ Self::Ok => Level::DEBUG,
4141 }
4242 }
4343-}
44434545-impl fmt::Display for LinkStatus {
4646- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4747- let msg = match self {
4848- Self::Unresolved => "failed to resolve some links",
4949- Self::Ok => "successfully resolved all links",
4444+ fn title(&self) -> impl fmt::Display {
4545+ match self {
4646+ Self::Unresolved => "item could not be resolved",
4747+ Self::Ok => "link resolved",
5048 Self::Debug => "debug info",
5151- };
5252- fmt::Display::fmt(msg, f)
4949+ }
5350 }
5451}
5552···5754 pub fn diagnostic(&self) -> LinkDiagnostic {
5855 let status = match self.state {
5956 LinkState::Unparsed => LinkStatus::Debug,
6060- LinkState::Parsed(_) => LinkStatus::Unresolved,
5757+ LinkState::Pending(_) => LinkStatus::Unresolved,
6158 LinkState::Resolved(_) => LinkStatus::Ok,
6259 };
6360 let label = match &self.state {
6461 LinkState::Unparsed => Some(self.url.as_ref().into()),
6565- LinkState::Parsed(item) => Some(format!("failed to resolve link for {:?}", item.name)),
6262+ LinkState::Pending(item) => Some(format!("could not obtain a link to {:?}", item.name)),
6663 LinkState::Resolved(links) => Some(format!("{}", links.url())),
6764 };
6865 let label = LabeledSpan::new_with_span(label, self.span.clone());
+123-74
crates/mdbook-rustdoc-links/src/main.rs
···11+#![warn(clippy::unwrap_used)]
22+13use std::{collections::HashMap, io::Write};
2435use anyhow::{
···68 Result as Result2,
79};
810use clap::{Parser, Subcommand};
1111+use futures_util::TryFutureExt;
912use mdbook_preprocessor::PreprocessorContext;
1010-use tap::{Pipe, TapFallible};
1111-use tracing::{instrument, level_filters::LevelFilter, warn};
1313+use tap::{Pipe, Tap};
1414+use tracing::{Level, debug, info, info_span, warn};
12151316use mdbookkit::{
1417 book::{BookConfigHelper, BookHelper, book_from_stdin, string_from_stdin},
1518 diagnostics::Issue,
1616- emit_warning,
1919+ emit_debug, emit_error, emit_trace, emit_warning,
2020+ error::{ExitProcess, FutureWithError},
1721 logging::Logging,
1822};
1923···2125 cache::{Cache, FileCache},
2226 client::Client,
2327 env::{Config, Environment, RustAnalyzer},
2424- link::diagnostic::LinkStatus,
2828+ link::{LinkState, diagnostic::LinkStatus},
2529 page::Pages,
2630 resolver::Resolver,
2731};
···3741mod sync;
3842#[cfg(test)]
3943mod tests;
4040-mod url;
41444245#[tokio::main]
4343-async fn main() -> Result2<()> {
4646+async fn main() {
4447 Logging::default().init();
4848+ let _span = info_span!({ env!("CARGO_PKG_NAME") }).entered();
4549 match Program::parse().command {
4650 Some(Command::Supports { .. }) => Ok(()),
4751 Some(Command::Markdown(options)) => markdown(options).await,
···5054 Some(Command::Describe) => describe(),
5155 None => mdbook().await,
5256 }
5757+ .exit(emit_error!())
5358}
54595560#[derive(Parser, Debug, Clone)]
···6671 /// Show which `rust-analyzer` is being used.
6772 RustAnalyzer,
68736969- /// Support command for mdbook.
7474+ /// Support command for mdBook.
7075 ///
7176 /// See <https://rust-lang.github.io/mdBook/for_developers/preprocessors.html#hooking-into-mdbook>
7277 #[clap(hide = true)]
···7782 Describe,
7883}
79848080-#[instrument("mdbook-rustdoc-links")]
8185async fn mdbook() -> Result2<()> {
8282- let (ctx, mut book) = book_from_stdin().context("failed to read from mdbook")?;
8686+ let (ctx, mut book) = book_from_stdin().context("Failed to read from mdBook")?;
83878484- let config = config(&ctx).context("failed to read preprocessor config from book.toml")?;
8888+ let config = config(&ctx).context("Failed to read preprocessor config from book.toml")?;
85898690 let client = Environment::new(config)
8787- .context("failed to initialize `mdbook-rustdoc-link`")?
9191+ .context("Failed to initialize preprocessor")?
9292+ .tap(emit_debug!("{:#?}"))
8893 .pipe(Client::new);
89949090- let cached = FileCache::load(client.env()).await.ok();
9595+ let cached = FileCache::load(client.env())
9696+ .context("Could not load cache")
9797+ .inspect_ok(emit_trace!("cache loaded: {:?}"))
9898+ .inspect_err(emit_debug!())
9999+ .await
100100+ .ok();
9110192102 let mut content = Pages::default();
93103···95105 let stream = client.env().markdown(&ch.content).into_offset_iter();
96106 content
97107 .read(path.clone(), &ch.content, stream)
9898- .with_context(|| path.display().to_string())
9999- .context("failed to parse Markdown source:")?;
108108+ .with_context(|| format!("Failed to parse {}", path.display()))?;
100109 }
101110102111 if let Some(cached) = cached {
112112+ info!("Reusing cached items");
103113 cached.resolve(&mut content).await.ok();
104114 }
105115106106- client
107107- .resolve(&mut content)
108108- .await
109109- .context("failed to resolve some links")?;
110110-111111- let mut result = book
112112- .iter_chapters()
113113- .filter_map(|(path, _)| {
114114- let output = content
115115- .emit(path, &client.env().emit_config())
116116- .tap_err(emit_warning!())
117117- .ok()?;
118118- Some((path.clone(), output.to_string()))
119119- })
120120- .collect::<HashMap<_, _>>();
116116+ client.resolve(&mut content).await?;
121117122118 let env = client.stop().await;
123119124120 let status = content
125121 .reporter()
126126- .names(|path| path.display().to_string())
127127- .level(LevelFilter::WARN)
122122+ .name_display(|path| path.display().to_string())
128123 .build()
129124 .to_stderr()
130125 .to_status();
126126+127127+ link_report(&content);
128128+129129+ match status {
130130+ LinkStatus::Unresolved => {
131131+ if env.config.cache_dir.is_some() {
132132+ warn! { "The `cache-dir` option is enabled, but some items could not \
133133+ be resolved, which will cause rust-analyzer to always run \
134134+ despite the cache." }
135135+ }
136136+ }
137137+ LinkStatus::Ok | LinkStatus::Debug => {
138138+ info!("Finished");
139139+ }
140140+ }
141141+142142+ // bail before emitting changes
143143+ env.config.fail_on_warnings.check(status.level())?;
131144132145 if content.modified() {
133133- FileCache::save(&env, &content).await.ok();
146146+ FileCache::save(&env, &content)
147147+ .context("Failed to save cache")
148148+ .inspect_err(emit_warning!())
149149+ .await
150150+ .ok();
134151 }
135152153153+ let mut result = book
154154+ .iter_chapters()
155155+ .map(|(path, _)| {
156156+ let _span = info_span!("emit", key = ?path).entered();
157157+ debug!("generating output");
158158+ let output = content
159159+ .emit(path, &env.emit_config())
160160+ .context("Error generating output")?;
161161+ Ok((path.clone(), output))
162162+ })
163163+ .collect::<Result2<HashMap<_, _>>>()?;
164164+136165 book.for_each_text_mut(|path, content| {
137166 if let Some(output) = result.remove(path) {
138167 *content = output;
···141170142171 book.to_stdout(&ctx)?;
143172144144- env.config.fail_on_warnings.check(status.level())?;
145145-146146- if env.config.cache_dir.is_some() && status == LinkStatus::Unresolved {
147147- warn!(
148148- "The `cache-dir` option is enabled, but some items could not \
149149- be resolved, which will cause rust-analyzer to always run \
150150- despite the cache."
151151- );
152152- }
153153-154173 Ok(())
155174}
156175157176async fn markdown(config: Config) -> Result2<()> {
158177 let client = Environment::new(config)
159159- .context("failed to initialize")?
178178+ .context("Failed to initialize")?
160179 .pipe(Client::new);
161180162162- let source = string_from_stdin().context("failed to read Markdown source from stdin")?;
181181+ let cached = FileCache::load(client.env())
182182+ .context("Could not load cache")
183183+ .inspect_ok(emit_debug!())
184184+ .inspect_err(emit_debug!())
185185+ .await
186186+ .ok();
163187188188+ let source = string_from_stdin().context("Failed to read Markdown source from stdin")?;
164189 let stream = client.env().markdown(&source).into_offset_iter();
165190166166- let mut content = Pages::one(&source, stream).context("failed to parse Markdown source")?;
191191+ let mut content = Pages::one(&source, stream).context("Failed to parse Markdown source")?;
167192168168- if let Ok(cached) = FileCache::load(client.env()).await {
193193+ if let Some(cached) = cached {
169194 cached.resolve(&mut content).await.ok();
170195 }
171196172172- client
173173- .resolve(&mut content)
174174- .await
175175- .context("failed to resolve some links")?;
197197+ client.resolve(&mut content).await?;
176198177199 let env = client.stop().await;
178200179201 let status = content
180202 .reporter()
181181- .names(|_| "<stdin>".into())
182182- .level(LevelFilter::WARN)
203203+ .name_display(|_| "<stdin>".into())
183204 .build()
184205 .to_stderr()
185206 .to_status();
207207+208208+ link_report(&content);
186209187210 if content.modified() {
188211 FileCache::save(&env, &content).await.ok();
189212 }
190213191191- content
192192- .get(&env.emit_config())
193193- .map(|emit| emit.to_string())
214214+ (content.get(&env.emit_config()).map(|emit| emit.to_string()))
194215 .and_then(|output| Ok(std::io::stdout().write_all(output.as_bytes())?))?;
195216196217 env.config.fail_on_warnings.check(status.level())?;
···198219 Ok(())
199220}
200221201201-fn which() -> Result2<()> {
202202- let env = Environment::new(Default::default())?;
222222+fn link_report<K>(content: &Pages<'_, K>) {
223223+ let mut iter = content.iter();
203224204204- match env.which() {
205205- RustAnalyzer::Custom(cmd) => println!("using a custom command for rust-analyzer: {cmd:?}"),
206206- RustAnalyzer::VsCode(cmd) => println!(
207207- "using rust-analyzer from VS Code extension: {}",
208208- cmd.display()
209209- ),
210210- RustAnalyzer::Path => println!("using rust-analyzer on PATH (run `which rust-analyzer`)"),
211211- }
225225+ let result = iter.deduped(|link| match link.state() {
226226+ LinkState::Pending(..) => Some(None),
227227+ LinkState::Resolved(links) => Some(Some(links.url())),
228228+ LinkState::Unparsed => None,
229229+ });
212230213213- Ok(())
214214-}
231231+ info!("Converted {}", iter.stats().fmt_resolved());
215232216216-#[cfg(feature = "_testing")]
217217-fn describe() -> Result2<()> {
218218- print!("{}", mdbookkit::docs::describe_preprocessor::<Config>()?);
219219- Ok(())
233233+ if tracing::enabled!(target: "link-report", Level::DEBUG) {
234234+ for (item, link) in result
235235+ .into_iter()
236236+ .filter_map(|(k, v)| Some((k, v?)))
237237+ .collect::<Vec<_>>()
238238+ .tap_mut(|items| {
239239+ items.sort_by(|(k1, u1), (k2, u2)| (k1.as_ref(), u1).cmp(&(k2.as_ref(), u2)));
240240+ })
241241+ {
242242+ if let Some(link) = link {
243243+ info!(target: "link-report", "{item} => {link}")
244244+ } else {
245245+ warn!(target: "link-report", "{item} => (unresolved)")
246246+ }
247247+ }
248248+ }
220249}
221250222251fn config(ctx: &PreprocessorContext) -> Result2<Config> {
223223- let mut config = ctx
224224- .config
225225- .preprocessor::<Config>(&[PREPROCESSOR_NAME, "mdbook-rustdoc-link"])?;
252252+ let mut config =
253253+ (ctx.config).preprocessor::<Config>(&[PREPROCESSOR_NAME, "mdbook-rustdoc-link"])?;
226254227255 if let Some(path) = config.manifest_dir {
228256 config.manifest_dir = Some(ctx.root.join(path))
···235263 }
236264237265 Ok(config)
266266+}
267267+268268+fn which() -> Result2<()> {
269269+ let env = Environment::new(Default::default())?;
270270+271271+ match env.which() {
272272+ RustAnalyzer::Custom(cmd) => println!("Using a custom command for rust-analyzer: {cmd:?}"),
273273+ RustAnalyzer::VsCode(cmd) => println!(
274274+ "Using rust-analyzer from VS Code extension: {}",
275275+ cmd.display()
276276+ ),
277277+ RustAnalyzer::Path => println!("Using rust-analyzer on PATH"),
278278+ }
279279+280280+ Ok(())
281281+}
282282+283283+#[cfg(feature = "_testing")]
284284+fn describe() -> Result2<()> {
285285+ print!("{}", mdbookkit::docs::describe_preprocessor::<Config>()?);
286286+ Ok(())
238287}
239288240289static UNIQUE_ID: &str = "__ded48f4d_0c4f_4950_b17d_55fd3b2a0c86__";
+3-3
crates/mdbook-rustdoc-links/src/markdown.rs
···1414/// [`BrokenLinkCallback`] implementation that unconditionally converts all "broken"
1515/// links to links to be further processed.
1616///
1717-/// "Broken" links are links like `[text][link::item]` that don't have associated URLs
1818-/// that are expected for this preprocessor.
1717+/// "Broken" links are links like `[text][link::item]` that don't have associated URLs.
1818+/// Such links are expected for this preprocessor.
1919///
2020/// Links that are "broken" that aren't actually doc links won't show up in the output,
2121/// because the preprocessor ignores links that cannot be parsed and is capable of
···5757 .collect::<Vec<_>>();
58585959 if inner.len() == 1 {
6060- inner.into_iter().next().unwrap()
6060+ inner.into_iter().next().expect("has 1 item")
6161 } else {
6262 inner
6363 .iter()
···22source: crates/mdbook-rustdoc-links/src/tests.rs
33expression: report
44---
55- info: successfully resolved all links
55+ info: link resolved
66 ╭─[docs/src/rustdoc-links/known-issues.md:27:25]
77 │ the derived trait by using the [macro syntax](supported-syntax.md#functions-and-macros),
88 │ for example, by writing [`[serde::Serialize!]`][serde::Serialize] instead of
···122122 .get_opts()
123123 .filter(|opt| !opt.is_hide_set())
124124 .map(|opt| {
125125- let key = opt.get_long().unwrap().to_owned();
125125+ let key = opt
126126+ .get_long()
127127+ .expect("option should have a long name")
128128+ .to_owned();
126129127130 let help = opt.get_help().map(|h| h.to_string()).unwrap_or_default();
128131···136139 let type_id = if cfg!(debug_assertions) {
137140 let ty = format!("{:?}", opt.get_value_parser().type_id())
138141 .replace("alloc::string::", "");
139139- let name = ty.split("::").last().unwrap();
142142+ let name = ty.split("::").last().expect("split() shouldn't be empty");
140143 if matches!(action, ArgAction::Append) {
141144 Some((format!("Vec<{name}>"), format!("Vec<{ty}>")))
142145 } else {
+103-11
crates/mdbookkit/src/error.rs
···11-use anyhow::{Result, anyhow};
11+use std::{fmt::Display, process::exit, sync::LockResult};
22+33+use anyhow::{Context, Error, Result, anyhow};
24use serde::Deserialize;
35use tap::Pipe;
46use tracing::Level;
···2729impl OnWarning {
2830 pub fn check(&self, level: Level) -> Result<()> {
2931 match level {
3030- Level::ERROR => Err(anyhow!("preprocessor has errors")),
3232+ Level::ERROR => Err(anyhow!("Preprocessor has errors")),
3133 Level::WARN => match self {
3232- Self::AlwaysFail => {
3333- anyhow!("treating warnings as errors because the `fail-on-warnings` option is set to \"always\"")
3434- .context("preprocessor has errors")
3535- .pipe(Err)
3636- }
3434+ Self::AlwaysFail => anyhow! {"Treating warnings as errors because the \
3535+ `fail-on-warnings` option is set to \"always\""}
3636+ .pipe(Err),
3737 Self::FailInCi => {
3838 let Some(ci) = is_ci() else {
3939 return Ok(());
4040 };
4141- anyhow!("treating warnings as errors because the `fail-on-warnings` option is set to \"ci\" and CI={ci}")
4242- .context("preprocessor has errors")
4343- .pipe(Err)
4141+ anyhow! {"Treating warnings as errors because CI={ci} and the \
4242+ `fail-on-warnings` option is set to \"ci\""}
4343+ .pipe(Err)
4444 }
4545 },
4646 _ => Ok(()),
···49495050 pub fn adjusted<T, E>(&self, result: Result<Result<T, E>, E>) -> Result<Result<T, E>, E> {
5151 match result {
5252+ Err(error) => Err(error),
5253 Ok(Err(error)) if is_ci().is_some() => Err(error),
5353- result => result,
5454+ Ok(Err(error)) => Ok(Err(error)),
5555+ Ok(Ok(result)) => Ok(Ok(result)),
5656+ }
5757+ }
5858+}
5959+6060+pub trait ExpectFmt {
6161+ fn expect_fmt(self);
6262+}
6363+6464+impl ExpectFmt for std::fmt::Result {
6565+ #[inline(always)]
6666+ fn expect_fmt(self) {
6767+ self.expect("string formatting should not fail")
6868+ }
6969+}
7070+7171+pub trait ExpectLock<T> {
7272+ fn expect_lock(self) -> T;
7373+}
7474+7575+impl<T> ExpectLock<T> for LockResult<T> {
7676+ #[inline(always)]
7777+ fn expect_lock(self) -> T {
7878+ self.expect("lock should not be poisoned")
7979+ }
8080+}
8181+8282+pub trait IntoAnyhow<T> {
8383+ fn anyhow(self) -> Result<T>;
8484+}
8585+8686+impl<T, E: Into<Error>> IntoAnyhow<T> for Result<T, E> {
8787+ #[inline(always)]
8888+ fn anyhow(self) -> Result<T> {
8989+ self.map_err(Into::into)
9090+ }
9191+}
9292+9393+#[allow(async_fn_in_trait)]
9494+pub trait FutureWithError<T> {
9595+ async fn context<C>(self, context: C) -> Result<T>
9696+ where
9797+ C: Display + Send + Sync + 'static;
9898+9999+ async fn with_context<C, G>(self, context: G) -> Result<T>
100100+ where
101101+ C: Display + Send + Sync + 'static,
102102+ G: FnOnce() -> C;
103103+}
104104+105105+impl<F, T, E> FutureWithError<T> for F
106106+where
107107+ F: Future<Output = Result<T, E>>,
108108+ E: Into<Error>,
109109+{
110110+ #[inline(always)]
111111+ async fn context<C>(self, context: C) -> Result<T>
112112+ where
113113+ C: Display + Send + Sync + 'static,
114114+ {
115115+ match self.await {
116116+ Ok(value) => Ok(value),
117117+ Err(error) => Err(error.into()).context(context),
118118+ }
119119+ }
120120+121121+ #[inline(always)]
122122+ async fn with_context<C, G>(self, context: G) -> Result<T>
123123+ where
124124+ C: Display + Send + Sync + 'static,
125125+ G: FnOnce() -> C,
126126+ {
127127+ match self.await {
128128+ Ok(value) => Ok(value),
129129+ Err(error) => Err(error.into()).with_context(context),
130130+ }
131131+ }
132132+}
133133+134134+pub trait ExitProcess {
135135+ fn exit(self, log: impl FnOnce(Error)) -> !;
136136+}
137137+138138+impl ExitProcess for Result<()> {
139139+ fn exit(self, log: impl FnOnce(Error)) -> ! {
140140+ match self {
141141+ Ok(()) => exit(0),
142142+ Err(e) => {
143143+ log(e);
144144+ exit(1)
145145+ }
54146 }
55147 }
56148}
+3
crates/mdbookkit/src/lib.rs
···11+#![warn(clippy::unwrap_used)]
22+13pub mod book;
24pub mod diagnostics;
35#[cfg(feature = "_testing")]
···810pub mod markdown;
911#[cfg(feature = "_testing")]
1012pub mod testing;
1313+pub mod url;
11141215// referenced in docs
1316#[doc(hidden)]
···55use mdbook_markdown::pulldown_cmark::{Event, Options};
66use pulldown_cmark_to_cmark::{Error, cmark};
77use tap::Pipe;
88+use tracing::{debug, trace, trace_span};
99+1010+use crate::error::ExpectFmt;
811912/// _Patch_ a Markdown string, instead of regenerating it entirely, in order to preserve
1013/// as much of the original Markdown source as possible, especially with regard to whitespace.
1114///
1215/// Currently, when using [`pulldown_cmark_to_cmark`] to generate Markdown from a
1313-/// [`pulldown_cmark::Event`][Event] stream, whitespace is NOT preserved. This is problematic
1414-/// for mdBook preprocessors, because preprocessors downstream may need to work on
1616+/// [`pulldown_cmark::Event`][Event] stream, whitespace is not preserved. This is problematic
1717+/// for mdBook preprocessors, because downstream preprocessors may need to work on
1518/// syntax that is whitespace-sensitive. Normalizing all whitespace could cause such
1619/// usage to no longer be recognized.
1720pub struct PatchStream<'a, S> {
1821 source: &'a str,
1922 stream: S,
2020- start: Option<usize>,
2323+ range: Option<Range<usize>>,
2124 patch: Option<String>,
2225}
2326···2932 type Item = Result<Cow<'a, str>, Error>;
30333134 fn next(&mut self) -> Option<Self::Item> {
3232- let start = self.start?;
3535+ let range = self.range.clone()?;
33363437 if let Some(patch) = self.patch.take() {
3838+ trace!("- {range:?} {:?}", &self.source[range.clone()]);
3939+ trace!("+ {range:?} {patch:?}");
3540 return Some(Ok(Cow::Owned(patch)));
3641 }
37423843 let Some((events, span)) = self.stream.next() else {
3939- self.start = None;
4040- return Some(Ok(Cow::Borrowed(&self.source[start..])));
4444+ let range = range.end..;
4545+ trace!(" {range:?}");
4646+ trace!(" EOF");
4747+ self.range = None;
4848+ return Some(Ok(Cow::Borrowed(&self.source[range])));
4149 };
42504343- if start > span.start {
4444- panic!("span {span:?} is backwards from already yielded span ending at {start}")
5151+ if range.start > span.start {
5252+ debug!("span {span:?} is before already yielded span {range:?}");
5353+ return Some(Err(Error::FormatFailed(Default::default())));
4554 }
46554747- let patch = match String::new().pipe(|mut out| cmark(events, &mut out).and(Ok(out))) {
5656+ let patch = match trace_span!("chunk", ?span)
5757+ .in_scope(|| String::new().pipe(|mut out| cmark(events, &mut out).and(Ok(out))))
5858+ {
4859 Err(error) => return Some(Err(error)),
4960 Ok(patch) => patch,
5061 };
51625252- self.start = Some(span.end);
6363+ self.range = Some(span.clone());
5364 self.patch = Some(patch);
54655555- Some(Ok(Cow::Borrowed(&self.source[start..span.start])))
6666+ let range = range.end..span.start;
6767+ trace!(" {range:?}");
6868+ Some(Ok(Cow::Borrowed(&self.source[range])))
5669 }
5770}
58715972impl<'a, S> PatchStream<'a, S> {
6073 /// Create a new patch stream.
6174 ///
6262- /// `stream` should be an [`Iterator`] yielding tuples of (`events`, `range`):
7575+ /// `stream` should be an [`Iterator`] yielding tuples of `(events, range)`:
6376 ///
6477 /// - `events` is an [`Iterator`] yielding [`Event`]s which is the replacement
6578 /// Markdown to be rendered into `source` using [`pulldown_cmark_to_cmark`].
···7790 Self {
7891 source,
7992 stream,
8080- start: Some(0),
9393+ range: Some(0..0),
8194 patch: None,
8295 }
8396 }
···91104 pub fn into_string(self) -> Result<String, Error> {
92105 let mut out = String::new();
93106 for chunk in self {
9494- write!(out, "{}", chunk?).unwrap();
107107+ write!(out, "{}", chunk?).expect_fmt();
95108 }
96109 Ok(out)
97110 }
+34-20
crates/mdbookkit/src/testing.rs
···66use tracing::info;
77use url::Url;
8899+use crate::url::{ExpectUrl, UrlFromPath, UrlToPath};
1010+911#[derive(Debug, PartialEq, Eq, Hash)]
1012pub struct TestDocument {
1113 pub source_path: &'static str,
···2830 pub fn cwd(&self) -> Url {
2931 CARGO_WORKSPACE_DIR
3032 .join(self.source_path)
3131- .unwrap()
3333+ .expect_url()
3234 .join(".")
3333- .unwrap()
3535+ .expect_url()
3436 }
35373638 pub fn url(&self) -> Url {
3737- self.cwd().join(self.target_path).unwrap()
3939+ self.cwd().join(self.target_path).expect_url()
3840 }
39414042 pub fn name(&self) -> String {
4143 let dir = Path::new(self.source_path)
4244 .with_extension("")
4345 .file_name()
4444- .unwrap()
4646+ .expect("source_path should have a file name")
4547 .to_string_lossy()
4648 .into_owned();
47494850 let url = self.url();
4949- let cwd = self.cwd().join(&format!("{dir}/")).unwrap();
5050- let rel = cwd.make_relative(&url).unwrap();
5151+ let cwd = self.cwd().join(&format!("{dir}/")).expect_url();
5252+ let rel = cwd.make_relative(&url).expect("both are file: URLs");
51535254 if rel.starts_with("../") {
5353- let rel = CARGO_WORKSPACE_DIR.make_relative(&url).unwrap();
5555+ let rel = CARGO_WORKSPACE_DIR
5656+ .make_relative(&url)
5757+ .expect("both are file: URLs");
5458 if rel.starts_with("../") {
5555- url.path_segments().unwrap().next_back().unwrap().to_owned()
5959+ url.path_segments()
6060+ .expect("file: URL")
6161+ .next_back()
6262+ .expect("URL path not empty")
6363+ .to_owned()
5664 } else {
5765 rel
5866 }
···89979098 let path = source_path.with_extension("").join("snaps");
9199 let path = CARGO_WORKSPACE_DIR
9292- .to_file_path()
9393- .unwrap()
9494- .join(&*path.to_string_lossy())
9595- .join(name.parent().unwrap());
100100+ .expect_path()
101101+ .join(&*path.to_string_lossy());
102102+ let path = if let Some(parent) = name.parent() {
103103+ path.join(parent)
104104+ } else {
105105+ path
106106+ };
9610797108 let name = name
98109 .file_name()
···133144 cargo_run_bin::metadata::get_binary_packages()?
134145 .into_iter()
135146 .map(cargo_run_bin::binary::install)
136136- .map(|path| Ok(Path::new(&path?).parent().unwrap().to_owned()))
147147+ .map(|path| {
148148+ Ok(Path::new(&path?)
149149+ .parent()
150150+ .expect("install path should not be root")
151151+ .to_owned())
152152+ })
137153 .collect::<Result<Vec<_>>>()?,
138154 );
139155140156 path.push(
141157 CARGO_WORKSPACE_DIR
142158 .join("target")?
143143- .to_file_path()
144144- .unwrap()
159159+ .expect_path()
145160 .join(if cfg!(debug_assertions) {
146161 "debug"
147162 } else {
···163178 .args(["--format-version=1", "--no-deps"])
164179 .current_dir(env!("CARGO_MANIFEST_DIR"))
165180 .output()
166166- .unwrap()
181181+ .expect("cargo metadata must not fail")
167182 .pipe(|output| String::from_utf8(output.stdout))
168168- .unwrap()
183183+ .expect("cargo metadata should output in utf8")
169184 .pipe(|output| serde_json::from_str::<CargoManifest>(&output))
170170- .unwrap()
171171- .pipe(|manifest| Url::from_directory_path(manifest.workspace_root))
172172- .unwrap()
185185+ .expect("cargo metadata format should be correct")
186186+ .pipe(|manifest| manifest.workspace_root.to_directory_url())
173187});
174188175189pub fn not_in_ci<D: std::fmt::Display>(because: D) -> bool {
+54
crates/mdbookkit/src/url.rs
···11+use std::path::{Path, PathBuf};
22+33+use anyhow::{Result, bail};
44+use url::Url;
55+66+pub trait ExpectUrl<T> {
77+ fn expect_url(self) -> T;
88+}
99+1010+impl<T> ExpectUrl<T> for Result<T, url::ParseError> {
1111+ #[inline(always)]
1212+ fn expect_url(self) -> T {
1313+ self.expect("should be a valid URL")
1414+ }
1515+}
1616+1717+pub trait UrlToPath {
1818+ fn to_path(&self) -> Result<PathBuf>;
1919+2020+ fn expect_path(&self) -> PathBuf;
2121+}
2222+2323+impl UrlToPath for Url {
2424+ #[inline(always)]
2525+ fn to_path(&self) -> Result<PathBuf> {
2626+ match self.to_file_path() {
2727+ Ok(path) => Ok(path),
2828+ Err(_) => bail!("{self} does not have a valid file path"),
2929+ }
3030+ }
3131+3232+ #[inline(always)]
3333+ fn expect_path(&self) -> PathBuf {
3434+ self.to_path().expect("URL path should be valid")
3535+ }
3636+}
3737+3838+pub trait UrlFromPath {
3939+ fn to_directory_url(&self) -> Url;
4040+4141+ fn to_file_url(&self) -> Url;
4242+}
4343+4444+impl<P: AsRef<Path> + ?Sized> UrlFromPath for P {
4545+ #[inline(always)]
4646+ fn to_directory_url(&self) -> Url {
4747+ Url::from_directory_path(self).expect("should be a valid absolute path")
4848+ }
4949+5050+ #[inline(always)]
5151+ fn to_file_url(&self) -> Url {
5252+ Url::from_file_path(self).expect("should be a valid absolute path")
5353+ }
5454+}
+1-1
docs/bin/main.rs
···88};
991010fn preprocess() -> Result<()> {
1111- let (ctx, mut book) = book_from_stdin().context("failed to read from mdbook")?;
1111+ let (ctx, mut book) = book_from_stdin().context("Failed to read from mdBook")?;
12121313 book.for_each_text_mut(|_, content| {
1414 let stream = Parser::new(content)
+5-5
utils/mdbook-socials/src/main.rs
···1313use clap::Parser;
1414use glob::glob;
1515use lol_html::{
1616- element, html_content::ContentType, rewrite_str, text, HtmlRewriter, RewriteStrSettings,
1717- Settings,
1616+ HtmlRewriter, RewriteStrSettings, Settings, element, html_content::ContentType, rewrite_str,
1717+ text,
1818};
1919use minijinja::Environment;
2020use serde::Deserialize;
···6161 let image = book_toml_path.join(&image)?;
6262 let image = src_dir
6363 .make_relative(&image)
6464- .context("failed to make relative path to image")?;
6464+ .context("Failed to make relative path to image")?;
6565 book_toml.preprocessor.link_forever.book_url.join(&image)?
6666 };
6767 let metadata = PageMetadata {
···104104105105 let pathname = out_dir
106106 .make_relative(&url)
107107- .context("failed to get page pathname")?
107107+ .context("Failed to get page pathname")?
108108 .replace("index.html", "")
109109 .replace(".html", "")
110110 .pipe(|p| format!("/{p}"));
···209209 <meta name="twitter:card" content="summary_large_image">
210210 <meta name="twitter:title" content="{{ og_title }}">
211211 <meta name="twitter:image" content="{{ og_image }}">
212212- <meta name="twitter:image:alt" content="toolkit for mdbook">
212212+ <meta name="twitter:image:alt" content="toolkit for mdBook">
213213 <meta name="twitter:description" content="{{ og_description }}">
214214 <meta name="theme-color" content="#d2a6ff">
215215"##;