@@ -5,9 +5,11 @@ use mdbook::BookItem;
5
5
use regex:: { Captures , Regex } ;
6
6
use semver:: { Version , VersionReq } ;
7
7
use std:: collections:: BTreeMap ;
8
- use std:: io;
8
+ use std:: fmt:: Write as _;
9
+ use std:: fs;
10
+ use std:: io:: { self , Write as _} ;
9
11
use std:: path:: PathBuf ;
10
- use std:: process;
12
+ use std:: process:: { self , Command } ;
11
13
12
14
fn main ( ) {
13
15
let mut args = std:: env:: args ( ) . skip ( 1 ) ;
@@ -57,17 +59,38 @@ struct Spec {
57
59
deny_warnings : bool ,
58
60
rule_re : Regex ,
59
61
admonition_re : Regex ,
62
+ std_link_re : Regex ,
63
+ std_link_extract_re : Regex ,
60
64
}
61
65
62
66
impl Spec {
63
67
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_!:<>{}()\[\]]+ )?" ;
64
72
Spec {
65
73
deny_warnings : std:: env:: var ( "SPEC_DENY_WARNINGS" ) . as_deref ( ) == Ok ( "1" ) ,
66
74
rule_re : Regex :: new ( r"(?m)^r\[([^]]+)]$" ) . unwrap ( ) ,
67
75
admonition_re : Regex :: new (
68
76
r"(?m)^ *> \[!(?<admon>[^]]+)\]\n(?<blockquote>(?: *> .*\n)+)" ,
69
77
)
70
78
. 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 ( ) ,
71
94
}
72
95
}
73
96
@@ -152,6 +175,122 @@ impl Spec {
152
175
} )
153
176
. to_string ( )
154
177
}
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
+ }
155
294
}
156
295
157
296
impl Preprocessor for Spec {
@@ -170,6 +309,7 @@ impl Preprocessor for Spec {
170
309
}
171
310
ch. content = self . rule_definitions ( & ch, & mut found_rules) ;
172
311
ch. content = self . admonitions ( & ch) ;
312
+ ch. content = self . std_links ( & ch) ;
173
313
}
174
314
for section in & mut book. sections {
175
315
let BookItem :: Chapter ( ch) = section else {
0 commit comments