toolkit for mdBook [mirror of my GitHub repo] docs.tonywu.dev/mdbookkit/
permalinks rust-analyzer mdbook
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

docs: fix preprocessors

Tony Wu d137d4f1 3068b18f

+266 -142
+1
Cargo.lock
··· 1573 1573 "serde", 1574 1574 "serde_json", 1575 1575 "shlex", 1576 + "tap", 1576 1577 "tokio", 1577 1578 "tracing", 1578 1579 "tracing-subscriber",
+4 -3
TODO.md
··· 17 17 - [x] rustdoc-links 18 18 - [x] mdbookkit 19 19 20 - - [ ] preprocess 21 - - [ ] ra-version 22 - - [ ] describe 20 + - [x] preprocess 21 + - [x] ra-version 22 + - [x] describe 23 23 24 24 - [ ] postprocess 25 25 - [ ] anchors 26 26 - [ ] external links 27 27 - [ ] socials 28 28 29 + - [ ] upgrade deps 29 30 - [ ] msrv 30 31 31 32 - [ ] changelog
+13 -13
crates/mdbook-permalinks/src/main.rs
··· 43 43 let Program { command } = clap::Parser::parse(); 44 44 match command { 45 45 None => mdbook().exit(emit_error!()), 46 - Some(Command::Supports { .. }) => Ok(()), 46 + Some(Command::Supports { .. }) => {} 47 47 #[cfg(feature = "_testing")] 48 48 Some(Command::Describe) => { 49 - print!("{}", mdbookkit::docs::describe_preprocessor::<Config>()?); 50 - Ok(()) 49 + let desc = mdbookkit::docs::Reflect::default() 50 + .map_type::<UrlPrefix>("String") 51 + .describe::<Config>()?; 52 + print!("{desc}") 51 53 } 52 54 } 55 + Ok(()) 53 56 } 54 57 55 58 fn mdbook() -> Result<()> { ··· 532 535 /// Should be a string that contains the following placeholders that will be 533 536 /// filled in at build time: 534 537 /// 538 + /// - `{tree}` — will be `tree` if the generated URL is for a clickable link, or 539 + /// `raw` if the URL is for a Markdown image 535 540 /// - `{ref}` — the Git reference (tag or commit ID) resolved at build time 536 541 /// - `{path}` — path to the linked file relative to repo root, without a leading `/` 537 542 /// 538 543 /// For example, the following configures generated links to use GitLab's format: 539 544 /// 540 545 /// ```toml 541 - /// repo-url-template = "https://gitlab.haskell.org/ghc/ghc/-/tree/{ref}/{path}" 546 + /// repo-url-template = "https://gitlab.haskell.org/ghc/ghc/-/{tree}/{ref}/{path}" 542 547 /// ``` 543 548 /// 544 549 /// Note that information such as repo owner or name will not be filled in. If URLs to ··· 549 554 550 555 /// Specify the canonical URL at which you deploy your book. 551 556 /// 552 - /// Should be a qualified URL. For example: 557 + /// For example: 553 558 /// 554 559 /// ```toml 555 560 /// book-url = "https://me.github.io/my-awesome-crate/" 556 561 /// ``` 557 562 /// 558 563 /// Enables validation of hard-coded links to book pages. The preprocessor will 559 - /// warn you about links that are no longer valid (file not found) at build time. 560 - /// 561 - /// This is mainly used with mdBook's `{{#include}}` feature, where sometimes you 562 - /// have to specify full URLs because path-based links are not supported. 564 + /// warn you about links that are no longer valid at build time. 563 565 #[serde(default)] 564 566 #[arg(long, value_name("URL"), verbatim_doc_comment)] 565 567 book_url: Option<UrlPrefix>, 566 568 567 569 /// Convert some paths to permalinks even if they are under `src/`. 568 570 /// 569 - /// By default, links to files in your book's `src/` directory will not be transformed, 570 - /// since they are already copied to build output as static files. If you want such files 571 + /// By default, links to files in your book's `src/` directory will not be converted, 572 + /// since they are already copied to the output directory. If you want such files 571 573 /// to always be rendered as permalinks, specify their file extensions here. 572 574 /// 573 575 /// For example, to use permalinks for Rust source files even if they are in the book's ··· 586 588 always_link: Vec<String>, 587 589 588 590 /// Exit with a non-zero status code when there are warnings. 589 - /// 590 - /// Warnings are always printed to the console regardless of this option. 591 591 #[serde(default)] 592 592 #[arg(long, value_enum, value_name("MODE"), default_value_t = Default::default())] 593 593 fail_on_warnings: OnWarning,
+2 -3
crates/mdbook-rustdoc-links/src/env.rs
··· 70 70 71 71 /// Directory in which to persist build cache. 72 72 /// 73 - /// Setting this will enable caching. Will skip rust-analyzer if cache hits. 73 + /// Setting this will enable caching. The preprocessor will skip running 74 + /// rust-analyzer if cache hits. 74 75 #[serde(default)] 75 76 #[arg(long, value_name("PATH"), value_hint(clap::ValueHint::DirPath))] 76 77 pub cache_dir: Option<PathBuf>, 77 78 78 79 /// Exit with a non-zero status code when some links fail to resolve. 79 - /// 80 - /// Warnings are always printed to the console regardless of this option. 81 80 #[serde(default)] 82 81 #[arg(long, value_enum, value_name("MODE"), default_value_t = Default::default())] 83 82 pub fail_on_warnings: OnWarning,
+2 -1
crates/mdbook-rustdoc-links/src/main.rs
··· 280 280 281 281 #[cfg(feature = "_testing")] 282 282 fn describe() -> Result2<()> { 283 - print!("{}", mdbookkit::docs::describe_preprocessor::<Config>()?); 283 + let desc = mdbookkit::docs::Reflect::default().describe::<Config>()?; 284 + print!("{desc}"); 284 285 Ok(()) 285 286 } 286 287
+105 -74
crates/mdbookkit/src/docs.rs
··· 1 + use std::any::TypeId; 2 + 1 3 use anyhow::Result; 2 - use clap::{ArgAction, CommandFactory}; 4 + use clap::{ArgAction, CommandFactory, builder::ValueParser}; 3 5 use minijinja::render; 4 6 use serde::Serialize; 5 7 use tap::Tap; 6 8 7 - pub fn describe_preprocessor<C: CommandFactory>() -> Result<String> { 8 - let template = r#" 9 + // TODO: define options using facet 10 + 11 + #[derive(Debug, Default)] 12 + pub struct Reflect { 13 + type_map: Vec<(TypeId, &'static str)>, 14 + } 15 + 16 + impl Reflect { 17 + pub fn map_type<T: 'static>(&mut self, repl: &'static str) -> &mut Self { 18 + self.type_map.push((TypeId::of::<T>(), repl)); 19 + self 20 + } 21 + 22 + fn get_type(&self, opt: &ValueParser) -> String { 23 + let source = opt.type_id(); 24 + self.type_map 25 + .iter() 26 + .find_map(|(id, mapped)| { 27 + if source == *id { 28 + Some((*mapped).into()) 29 + } else { 30 + None 31 + } 32 + }) 33 + .unwrap_or_else(|| format!("{source:?}")) 34 + } 35 + 36 + pub fn describe<C: CommandFactory>(&self) -> Result<String> { 37 + let template = r#" 9 38 <div class="table-wrapper"> 10 39 <table> 11 40 ··· 118 147 {% endfor -%} 119 148 "#; 120 149 121 - let options = C::command() 122 - .get_opts() 123 - .filter(|opt| !opt.is_hide_set()) 124 - .map(|opt| { 125 - let key = opt 126 - .get_long() 127 - .expect("option should have a long name") 128 - .to_owned(); 150 + let options = C::command() 151 + .get_opts() 152 + .filter(|opt| !opt.is_hide_set()) 153 + .map(|opt| { 154 + let key = opt 155 + .get_long() 156 + .expect("option should have a long name") 157 + .to_owned(); 129 158 130 - let help = opt.get_help().map(|h| h.to_string()).unwrap_or_default(); 159 + let help = opt.get_help().map(|h| h.to_string()).unwrap_or_default(); 131 160 132 - let description = opt 133 - .get_long_help() 134 - .map(|h| h.to_string()) 135 - .unwrap_or(help.clone()); 161 + let description = opt 162 + .get_long_help() 163 + .map(|h| h.to_string()) 164 + .unwrap_or(help.clone()); 136 165 137 - let action = opt.get_action(); 166 + let action = opt.get_action(); 138 167 139 - let type_id = if cfg!(debug_assertions) { 140 - let ty = format!("{:?}", opt.get_value_parser().type_id()) 141 - .replace("alloc::string::", ""); 142 - let name = ty.split("::").last().expect("split() shouldn't be empty"); 143 - if matches!(action, ArgAction::Append) { 144 - Some((format!("Vec<{name}>"), format!("Vec<{ty}>"))) 168 + let type_id = if cfg!(debug_assertions) { 169 + let ty = self 170 + .get_type(opt.get_value_parser()) 171 + .replace("alloc::string::", ""); 172 + let name = ty.split("::").last().expect("split() shouldn't be empty"); 173 + if matches!(action, ArgAction::Append) { 174 + Some((format!("Vec<{name}>"), format!("Vec<{ty}>"))) 175 + } else { 176 + Some((name.to_owned(), ty)) 177 + } 145 178 } else { 146 - Some((name.to_owned(), ty)) 147 - } 148 - } else { 149 - None 150 - }; 179 + None 180 + }; 151 181 152 - let default = if let Some(d) = opt.get_default_values().iter().next() { 153 - Some(format!("{:?}", d.to_string_lossy().into_owned())) 154 - } else if matches!(action, ArgAction::SetTrue) { 155 - Some("false".into()) 156 - } else if matches!(action, ArgAction::SetFalse) { 157 - Some("true".into()) 158 - } else if matches!(action, ArgAction::Append) { 159 - Some("[]".into()) 160 - } else if !opt.is_required_set() { 161 - Some("None".into()) 162 - } else { 163 - None 164 - }; 182 + let default = if let Some(d) = opt.get_default_values().iter().next() { 183 + Some(format!("{:?}", d.to_string_lossy().into_owned())) 184 + } else if matches!(action, ArgAction::SetTrue) { 185 + Some("false".into()) 186 + } else if matches!(action, ArgAction::SetFalse) { 187 + Some("true".into()) 188 + } else if matches!(action, ArgAction::Append) { 189 + Some("[]".into()) 190 + } else if !opt.is_required_set() { 191 + Some("None".into()) 192 + } else { 193 + None 194 + }; 165 195 166 - let choices = opt 167 - .get_possible_values() 168 - .iter() 169 - .filter_map(|v| { 170 - if v.is_hide_set() { 171 - None 172 - } else { 173 - let help = v.get_help()?; 174 - Some((format!("{:?}", v.get_name()), help.to_string())) 175 - } 176 - }) 177 - .collect(); 196 + let choices = opt 197 + .get_possible_values() 198 + .iter() 199 + .filter_map(|v| { 200 + if v.is_hide_set() { 201 + None 202 + } else { 203 + let help = v.get_help()?; 204 + Some((format!("{:?}", v.get_name()), help.to_string())) 205 + } 206 + }) 207 + .collect(); 178 208 179 - #[derive(Serialize)] 180 - struct OptionItem { 181 - key: String, 182 - help: String, 183 - description: String, 184 - type_id: Option<(String, String)>, 185 - default: Option<String>, 186 - choices: Vec<(String, String)>, 187 - } 209 + #[derive(Serialize)] 210 + struct OptionItem { 211 + key: String, 212 + help: String, 213 + description: String, 214 + type_id: Option<(String, String)>, 215 + default: Option<String>, 216 + choices: Vec<(String, String)>, 217 + } 188 218 189 - OptionItem { 190 - key, 191 - help, 192 - description, 193 - type_id, 194 - default, 195 - choices, 196 - } 197 - }) 198 - .collect::<Vec<_>>() 199 - .tap_mut(|opts| opts.sort_by(|a, b| a.key.cmp(&b.key))); 219 + OptionItem { 220 + key, 221 + help, 222 + description, 223 + type_id, 224 + default, 225 + choices, 226 + } 227 + }) 228 + .collect::<Vec<_>>() 229 + .tap_mut(|opts| opts.sort_by(|a, b| a.key.cmp(&b.key))); 200 230 201 - Ok(render!(template, options)) 231 + Ok(render!(template, options)) 232 + } 202 233 }
+5 -5
crates/mdbookkit/src/error.rs
··· 176 176 } 177 177 } 178 178 179 - pub trait ExitProcess { 180 - fn exit(self, log: impl FnOnce(Error)) -> !; 179 + pub trait ExitProcess<T> { 180 + fn exit(self, log: impl FnOnce(Error)) -> T; 181 181 } 182 182 183 - impl ExitProcess for Result<()> { 184 - fn exit(self, log: impl FnOnce(Error)) -> ! { 183 + impl<T> ExitProcess<T> for Result<T> { 184 + fn exit(self, log: impl FnOnce(Error)) -> T { 185 185 match self { 186 - Ok(()) => exit(0), 186 + Ok(v) => v, 187 187 Err(e) => { 188 188 log(e); 189 189 exit(1)
+1
docs/Cargo.toml
··· 18 18 serde = { workspace = true } 19 19 serde_json = { workspace = true } 20 20 shlex = { workspace = true } 21 + tap = { workspace = true } 21 22 tokio = { workspace = true } 22 23 tracing = { workspace = true } 23 24 tracing-subscriber = { workspace = true, features = ["env-filter"] }
+123 -26
docs/bin/main.rs
··· 1 + use std::{ops::Range, process::Stdio, sync::LazyLock}; 2 + 1 3 use anyhow::{Context, Result}; 2 4 3 5 use mdbook_markdown::pulldown_cmark::{CodeBlockKind, CowStr, Event, Parser, Tag, TagEnd}; 6 + use tap::Pipe; 7 + use tracing::info_span; 8 + 4 9 use mdbookkit::{ 5 10 book::{BookHelper, book_from_stdin}, 11 + emit_error, 12 + error::ExitProcess, 6 13 logging::Logging, 7 14 markdown::PatchStream, 8 15 }; ··· 10 17 fn preprocess() -> Result<()> { 11 18 let (ctx, mut book) = book_from_stdin().context("Failed to read from mdBook")?; 12 19 20 + #[derive(Default)] 21 + struct State { 22 + mermaid: Option<Range<usize>>, 23 + ra_version: Option<Range<usize>>, 24 + describe: Option<Range<usize>>, 25 + } 26 + 27 + enum Replace<'a> { 28 + Mermaid { 29 + text: CowStr<'a>, 30 + span: Range<usize>, 31 + }, 32 + RustAnalyzerVersion { 33 + span: Range<usize>, 34 + }, 35 + Describe { 36 + package: &'static str, 37 + span: Range<usize>, 38 + }, 39 + } 40 + 13 41 book.for_each_text_mut(|_, content| { 14 42 let stream = Parser::new(content) 15 43 .into_offset_iter() 16 - .scan(None, |state, (event, span)| match event { 17 - Event::Start(Tag::CodeBlock(CodeBlockKind::Fenced(tag))) if &*tag == "mermaid" => { 18 - *state = Some(span); 19 - Some(None) 44 + .scan(State::default(), |state, (event, span)| { 45 + match event { 46 + Event::Start(Tag::CodeBlock(CodeBlockKind::Fenced(tag))) 47 + if &*tag == "mermaid" => 48 + { 49 + state.mermaid = Some(span); 50 + } 51 + Event::Text(text) => { 52 + if let Some(ref span) = state.mermaid { 53 + let span = span.clone(); 54 + return Some(Some(Replace::Mermaid { text, span })); 55 + } 56 + } 57 + Event::End(TagEnd::CodeBlock) => { 58 + state.mermaid = None; 59 + } 60 + Event::InlineHtml(tag) => match &*tag { 61 + "<ra-version>" => state.ra_version = Some(span), 62 + "</ra-version>" => { 63 + if let Some(start) = state.ra_version.take() { 64 + let span = start.start..span.end; 65 + return Some(Some(Replace::RustAnalyzerVersion { span })); 66 + } 67 + } 68 + "<rustdoc-links-options>" => state.describe = Some(span), 69 + "</rustdoc-links-options>" => { 70 + if let Some(start) = state.describe.take() { 71 + let span = start.start..span.end; 72 + let package = "mdbook-rustdoc-links"; 73 + return Some(Some(Replace::Describe { package, span })); 74 + } 75 + } 76 + "<permalinks-options>" => state.describe = Some(span), 77 + "</permalinks-options>" => { 78 + if let Some(start) = state.describe.take() { 79 + let span = start.start..span.end; 80 + let package = "mdbook-permalinks"; 81 + return Some(Some(Replace::Describe { package, span })); 82 + } 83 + } 84 + _ => {} 85 + }, 86 + _ => {} 87 + } 88 + Some(None) 89 + }) 90 + .flat_map(|chunk| match chunk? { 91 + Replace::Mermaid { text, span } => { 92 + let repl = vec![ 93 + Event::Start(Tag::HtmlBlock), 94 + Event::Html(CowStr::Borrowed("<pre class=\"mermaid\">")), 95 + Event::Html(text), 96 + Event::Html(CowStr::Borrowed("</pre>")), 97 + Event::End(TagEnd::HtmlBlock), 98 + ] 99 + .into_iter(); 100 + Some((repl, span)) 20 101 } 21 - Event::Text(text) => { 22 - if let Some(span) = state { 23 - Some(Some((text, span.clone()))) 24 - } else { 25 - Some(None) 26 - } 102 + Replace::RustAnalyzerVersion { span } => { 103 + static RA_VERSION: LazyLock<String> = LazyLock::new(|| { 104 + std::process::Command::new(env!("CARGO")) 105 + .args(["run", "--package", "util-rust-analyzer", "--", "version"]) 106 + .stdout(Stdio::piped()) 107 + .output() 108 + .context("failed to run util-rust-analyzer") 109 + .exit(emit_error!()) 110 + .stdout 111 + .pipe(String::from_utf8) 112 + .context("failed to parse version") 113 + .exit(emit_error!()) 114 + }); 115 + let repl = vec![Event::Code(RA_VERSION.clone().into())].into_iter(); 116 + Some((repl, span)) 27 117 } 28 - Event::End(TagEnd::CodeBlock) => { 29 - *state = None; 30 - Some(None) 118 + Replace::Describe { package, span } => { 119 + let described = std::process::Command::new(env!("CARGO")) 120 + .args([ 121 + "run", 122 + "--package", 123 + package, 124 + "--features", 125 + "_testing", 126 + "--", 127 + "describe", 128 + ]) 129 + .stdout(Stdio::piped()) 130 + .stderr(Stdio::inherit()) 131 + .output() 132 + .with_context(|| format!("failed to describe {package}")) 133 + .exit(emit_error!()) 134 + .stdout 135 + .pipe(String::from_utf8) 136 + .context("failed to parse version") 137 + .exit(emit_error!()); 138 + let repl = vec![Event::Html(described.into())].into_iter(); 139 + Some((repl, span)) 31 140 } 32 - _ => Some(None), 33 - }) 34 - .flat_map(|chunk| { 35 - let (text, span) = chunk?; 36 - let repl = vec![ 37 - Event::Start(Tag::HtmlBlock), 38 - Event::Html(CowStr::Borrowed("<pre class=\"mermaid\">")), 39 - Event::Html(text), 40 - Event::Html(CowStr::Borrowed("</pre>")), 41 - Event::End(TagEnd::HtmlBlock), 42 - ] 43 - .into_iter(); 44 - Some((repl, span)) 45 141 }) 46 142 .collect::<Vec<_>>(); 47 143 ··· 57 153 58 154 fn main() -> Result<()> { 59 155 Logging::default().init(); 156 + let _span = info_span!({ env!("CARGO_PKG_NAME") }).entered(); 60 157 let Program { command } = clap::Parser::parse(); 61 158 match command { 62 159 Command::Preprocess { command: None } => preprocess(),
+1 -16
docs/book.toml
··· 22 22 [output.html.playground] 23 23 runnable = false 24 24 25 - # [preprocessor.rustdoc-link-options] 26 - # before = ["rustdoc-link"] 27 - # command = "cargo run --package util-clap-reflect -- --reflect rustdoc-link-options" 28 - 29 - # [preprocessor.link-forever-options] 30 - # before = ["rustdoc-link"] 31 - # command = "cargo run --package util-clap-reflect -- --reflect link-forever-options" 32 - 33 25 [preprocessor.rustdoc-links] 34 26 after = ["links"] 35 27 cache-dir = "build" ··· 43 35 book-url = "https://tonywu6.github.io/mdbookkit/" 44 36 command = "cargo run --package mdbook-permalinks" 45 37 46 - # [preprocessor.rust-analyzer-version] 47 - # after = ["rustdoc-link"] 48 - # command = "cargo run --package util-rust-analyzer --all-features -- version" 49 - 50 - # [preprocessor.alerts] 51 - # after = ["rustdoc-link"] 52 - # command = "cargo bin mdbook-alerts" 53 - 54 38 [preprocessor.app] 55 39 command = "deno run --allow-all app/build/preprocessor.ts" 56 40 57 41 [preprocessor.preprocess] 42 + after = ["links"] 58 43 command = "cargo run -- preprocess" 59 44 60 45 # [_metadata.socials."/"]
+2
docs/src/permalinks/configuration.md
··· 9 9 [preprocessor.permalinks] 10 10 always-link = [".rs"] 11 11 ``` 12 + 13 + <permalinks-options>(auto-generated)</permalinks-options>
+2
docs/src/rustdoc-links/configuration.md
··· 16 16 ```bash 17 17 mdbook-rustdoc-links markdown --rust-analyzer "..." 18 18 ``` 19 + 20 + <rustdoc-links-options>(auto-generated)</rustdoc-links-options>
-1
docs/src/snippets/logging.md
··· 1 - <!-- TODO: -->
+5
utils/rust-analyzer/src/main.rs
··· 137 137 #[arg(trailing_var_arg = true, allow_hyphen_values = true, hide = true)] 138 138 args: Vec<String>, 139 139 }, 140 + Version, 140 141 } 141 142 142 143 #[derive(clap::Subcommand, Debug)] ··· 164 165 match program.command { 165 166 Command::Download => download.download(), 166 167 Command::Analyzer { args } => analyzer(&download, args), 168 + Command::Version => { 169 + print!("{}", download.release); 170 + Ok(()) 171 + } 167 172 } 168 173 } 169 174