@@ -5,9 +5,11 @@ use mdbook::BookItem;
55use regex:: { Captures , Regex } ;
66use semver:: { Version , VersionReq } ;
77use std:: collections:: BTreeMap ;
8- use std:: io;
8+ use std:: fmt:: Write as _;
9+ use std:: fs;
10+ use std:: io:: { self , Write as _} ;
911use std:: path:: PathBuf ;
10- use std:: process;
12+ use std:: process:: { self , Command } ;
1113
1214fn main ( ) {
1315 let mut args = std:: env:: args ( ) . skip ( 1 ) ;
@@ -57,17 +59,38 @@ struct Spec {
5759 deny_warnings : bool ,
5860 rule_re : Regex ,
5961 admonition_re : Regex ,
62+ std_link_re : Regex ,
63+ std_link_extract_re : Regex ,
6064}
6165
6266impl Spec {
6367 pub fn new ( ) -> Spec {
68+ // This is roughly a rustdoc intra-doc link definition.
69+ let std_link = r"(?: [a-z]+@ )?
70+ (?: std|core|alloc|proc_macro|test )
71+ (?: ::[A-Za-z_!:<>{}()\[\]]+ )?" ;
6472 Spec {
6573 deny_warnings : std:: env:: var ( "SPEC_DENY_WARNINGS" ) . as_deref ( ) == Ok ( "1" ) ,
6674 rule_re : Regex :: new ( r"(?m)^r\[([^]]+)]$" ) . unwrap ( ) ,
6775 admonition_re : Regex :: new (
6876 r"(?m)^ *> \[!(?<admon>[^]]+)\]\n(?<blockquote>(?: *> .*\n)+)" ,
6977 )
7078 . unwrap ( ) ,
79+ std_link_re : Regex :: new ( & format ! (
80+ r"(?x)
81+ (?:
82+ ( \[`[^`]+`\] ) \( ({std_link}) \)
83+ )
84+ | (?:
85+ ( \[`{std_link}`\] )
86+ )
87+ "
88+ ) )
89+ . unwrap ( ) ,
90+ std_link_extract_re : Regex :: new (
91+ r#"<li><a [^>]*href="(https://doc.rust-lang.org/[^"]+)""# ,
92+ )
93+ . unwrap ( ) ,
7194 }
7295 }
7396
@@ -152,6 +175,122 @@ impl Spec {
152175 } )
153176 . to_string ( )
154177 }
178+
179+ /// Converts links to the standard library to the online documentation in
180+ /// a fashion similar to rustdoc intra-doc links.
181+ fn std_links ( & self , chapter : & Chapter ) -> String {
182+ // This is very hacky, but should work well enough.
183+ //
184+ // Collect all standard library links.
185+ //
186+ // links are tuples of ("[`std::foo`]", None) for links without dest,
187+ // or ("[`foo`]", "std::foo") with a dest.
188+ let mut links: Vec < _ > = self
189+ . std_link_re
190+ . captures_iter ( & chapter. content )
191+ . map ( |cap| {
192+ if let Some ( no_dest) = cap. get ( 3 ) {
193+ ( no_dest. as_str ( ) , None )
194+ } else {
195+ (
196+ cap. get ( 1 ) . unwrap ( ) . as_str ( ) ,
197+ Some ( cap. get ( 2 ) . unwrap ( ) . as_str ( ) ) ,
198+ )
199+ }
200+ } )
201+ . collect ( ) ;
202+ if links. is_empty ( ) {
203+ return chapter. content . clone ( ) ;
204+ }
205+ links. sort ( ) ;
206+ links. dedup ( ) ;
207+
208+ // Write a Rust source file to use with rustdoc to generate intra-doc links.
209+ let tmp = tempfile:: TempDir :: with_prefix ( "mdbook-spec-" ) . unwrap ( ) ;
210+ let src_path = tmp. path ( ) . join ( "a.rs" ) ;
211+ // Allow redundant since there could some in-scope things that are
212+ // technically not necessary, but we don't care about (like
213+ // [`Option`](std::option::Option)).
214+ let mut src = format ! (
215+ "#![deny(rustdoc::broken_intra_doc_links)]\n \
216+ #![allow(rustdoc::redundant_explicit_links)]\n "
217+ ) ;
218+ for ( link, dest) in & links {
219+ write ! ( src, "//! - {link}" ) . unwrap ( ) ;
220+ if let Some ( dest) = dest {
221+ write ! ( src, "({})" , dest) . unwrap ( ) ;
222+ }
223+ src. push ( '\n' ) ;
224+ }
225+ writeln ! (
226+ src,
227+ "extern crate alloc;\n \
228+ extern crate proc_macro;\n \
229+ extern crate test;\n "
230+ )
231+ . unwrap ( ) ;
232+ fs:: write ( & src_path, & src) . unwrap ( ) ;
233+ let output = Command :: new ( "rustdoc" )
234+ . arg ( "--edition=2021" )
235+ . arg ( & src_path)
236+ . current_dir ( tmp. path ( ) )
237+ . output ( )
238+ . expect ( "rustdoc installed" ) ;
239+ if !output. status . success ( ) {
240+ eprintln ! (
241+ "error: failed to extract std links ({:?}) in chapter {} ({:?})\n " ,
242+ output. status,
243+ chapter. name,
244+ chapter. source_path. as_ref( ) . unwrap( )
245+ ) ;
246+ io:: stderr ( ) . write_all ( & output. stderr ) . unwrap ( ) ;
247+ process:: exit ( 1 ) ;
248+ }
249+
250+ // Extract the links from the generated html.
251+ let generated =
252+ fs:: read_to_string ( tmp. path ( ) . join ( "doc/a/index.html" ) ) . expect ( "index.html generated" ) ;
253+ let urls: Vec < _ > = self
254+ . std_link_extract_re
255+ . captures_iter ( & generated)
256+ . map ( |cap| cap. get ( 1 ) . unwrap ( ) . as_str ( ) )
257+ . collect ( ) ;
258+ if urls. len ( ) != links. len ( ) {
259+ eprintln ! (
260+ "error: expected rustdoc to generate {} links, but found {} in chapter {} ({:?})" ,
261+ links. len( ) ,
262+ urls. len( ) ,
263+ chapter. name,
264+ chapter. source_path. as_ref( ) . unwrap( )
265+ ) ;
266+ process:: exit ( 1 ) ;
267+ }
268+
269+ // Replace any disambiguated links with just the disambiguation.
270+ let mut output = self
271+ . std_link_re
272+ . replace_all ( & chapter. content , |caps : & Captures | {
273+ if let Some ( dest) = caps. get ( 2 ) {
274+ // Replace destination parenthesis with a link definition (square brackets).
275+ format ! ( "{}[{}]" , & caps[ 1 ] , dest. as_str( ) )
276+ } else {
277+ caps[ 0 ] . to_string ( )
278+ }
279+ } )
280+ . to_string ( ) ;
281+
282+ // Append the link definitions to the bottom of the chapter.
283+ write ! ( output, "\n " ) . unwrap ( ) ;
284+ for ( ( link, dest) , url) in links. iter ( ) . zip ( urls) {
285+ if let Some ( dest) = dest {
286+ write ! ( output, "[{dest}]: {url}\n " ) . unwrap ( ) ;
287+ } else {
288+ write ! ( output, "{link}: {url}\n " ) . unwrap ( ) ;
289+ }
290+ }
291+
292+ output
293+ }
155294}
156295
157296impl Preprocessor for Spec {
@@ -170,6 +309,7 @@ impl Preprocessor for Spec {
170309 }
171310 ch. content = self . rule_definitions ( & ch, & mut found_rules) ;
172311 ch. content = self . admonitions ( & ch) ;
312+ ch. content = self . std_links ( & ch) ;
173313 }
174314 for section in & mut book. sections {
175315 let BookItem :: Chapter ( ch) = section else {
0 commit comments