@@ -111,6 +111,14 @@ impl HtmlHandlebars {
111
111
. insert ( "section" . to_owned ( ) , json ! ( section. to_string( ) ) ) ;
112
112
}
113
113
114
+ let redirects = collect_redirects_for_path ( & filepath, & ctx. html_config . redirect ) ?;
115
+ if !redirects. is_empty ( ) {
116
+ ctx. data . insert (
117
+ "fragment_map" . to_owned ( ) ,
118
+ json ! ( serde_json:: to_string( & redirects) ?) ,
119
+ ) ;
120
+ }
121
+
114
122
// Render the handlebars template with the data
115
123
debug ! ( "Render template" ) ;
116
124
let rendered = ctx. handlebars . render ( "index" , & ctx. data ) ?;
@@ -266,15 +274,27 @@ impl HtmlHandlebars {
266
274
}
267
275
268
276
log:: debug!( "Emitting redirects" ) ;
277
+ let redirects = combine_fragment_redirects ( redirects) ;
269
278
270
- for ( original, new) in redirects {
271
- log:: debug!( "Redirecting \" {}\" → \" {}\" " , original, new) ;
279
+ for ( original, ( dest, fragment_map) ) in redirects {
272
280
// Note: all paths are relative to the build directory, so the
273
281
// leading slash in an absolute path means nothing (and would mess
274
282
// up `root.join(original)`).
275
283
let original = original. trim_start_matches ( '/' ) ;
276
284
let filename = root. join ( original) ;
277
- self . emit_redirect ( handlebars, & filename, new) ?;
285
+ if filename. exists ( ) {
286
+ // This redirect is handled by the in-page fragment mapper.
287
+ continue ;
288
+ }
289
+ if dest. is_empty ( ) {
290
+ bail ! (
291
+ "redirect entry for `{original}` only has source paths with `#` fragments\n \
292
+ There must be an entry without the `#` fragment to determine the default \
293
+ destination."
294
+ ) ;
295
+ }
296
+ log:: debug!( "Redirecting \" {}\" → \" {}\" " , original, dest) ;
297
+ self . emit_redirect ( handlebars, & filename, & dest, & fragment_map) ?;
278
298
}
279
299
280
300
Ok ( ( ) )
@@ -285,23 +305,17 @@ impl HtmlHandlebars {
285
305
handlebars : & Handlebars < ' _ > ,
286
306
original : & Path ,
287
307
destination : & str ,
308
+ fragment_map : & BTreeMap < String , String > ,
288
309
) -> Result < ( ) > {
289
- if original. exists ( ) {
290
- // sanity check to avoid accidentally overwriting a real file.
291
- let msg = format ! (
292
- "Not redirecting \" {}\" to \" {}\" because it already exists. Are you sure it needs to be redirected?" ,
293
- original. display( ) ,
294
- destination,
295
- ) ;
296
- return Err ( Error :: msg ( msg) ) ;
297
- }
298
-
299
310
if let Some ( parent) = original. parent ( ) {
300
311
std:: fs:: create_dir_all ( parent)
301
312
. with_context ( || format ! ( "Unable to ensure \" {}\" exists" , parent. display( ) ) ) ?;
302
313
}
303
314
315
+ let js_map = serde_json:: to_string ( fragment_map) ?;
316
+
304
317
let ctx = json ! ( {
318
+ "fragment_map" : js_map,
305
319
"url" : destination,
306
320
} ) ;
307
321
let f = File :: create ( original) ?;
@@ -934,6 +948,62 @@ struct RenderItemContext<'a> {
934
948
chapter_titles : & ' a HashMap < PathBuf , String > ,
935
949
}
936
950
951
+ /// Redirect mapping.
952
+ ///
953
+ /// The key is the source path (like `foo/bar.html`). The value is a tuple
954
+ /// `(destination_path, fragment_map)`. The `destination_path` is the page to
955
+ /// redirect to. `fragment_map` is the map of fragments that override the
956
+ /// destination. For example, a fragment `#foo` could redirect to any other
957
+ /// page or site.
958
+ type CombinedRedirects = BTreeMap < String , ( String , BTreeMap < String , String > ) > ;
959
+ fn combine_fragment_redirects ( redirects : & HashMap < String , String > ) -> CombinedRedirects {
960
+ let mut combined: CombinedRedirects = BTreeMap :: new ( ) ;
961
+ // This needs to extract the fragments to generate the fragment map.
962
+ for ( original, new) in redirects {
963
+ if let Some ( ( source_path, source_fragment) ) = original. rsplit_once ( '#' ) {
964
+ let e = combined. entry ( source_path. to_string ( ) ) . or_default ( ) ;
965
+ if let Some ( old) = e. 1 . insert ( format ! ( "#{source_fragment}" ) , new. clone ( ) ) {
966
+ log:: error!(
967
+ "internal error: found duplicate fragment redirect \
968
+ {old} for {source_path}#{source_fragment}"
969
+ ) ;
970
+ }
971
+ } else {
972
+ let e = combined. entry ( original. to_string ( ) ) . or_default ( ) ;
973
+ e. 0 = new. clone ( ) ;
974
+ }
975
+ }
976
+ combined
977
+ }
978
+
979
+ /// Collects fragment redirects for an existing page.
980
+ ///
981
+ /// The returned map has keys like `#foo` and the value is the new destination
982
+ /// path or URL.
983
+ fn collect_redirects_for_path (
984
+ path : & Path ,
985
+ redirects : & HashMap < String , String > ,
986
+ ) -> Result < BTreeMap < String , String > > {
987
+ let path = format ! ( "/{}" , path. display( ) . to_string( ) . replace( '\\' , "/" ) ) ;
988
+ if redirects. contains_key ( & path) {
989
+ bail ! (
990
+ "redirect found for existing chapter at `{path}`\n \
991
+ Either delete the redirect or remove the chapter."
992
+ ) ;
993
+ }
994
+
995
+ let key_prefix = format ! ( "{path}#" ) ;
996
+ let map = redirects
997
+ . iter ( )
998
+ . filter_map ( |( source, dest) | {
999
+ source
1000
+ . strip_prefix ( & key_prefix)
1001
+ . map ( |fragment| ( format ! ( "#{fragment}" ) , dest. to_string ( ) ) )
1002
+ } )
1003
+ . collect ( ) ;
1004
+ Ok ( map)
1005
+ }
1006
+
937
1007
#[ cfg( test) ]
938
1008
mod tests {
939
1009
use crate :: config:: TextDirection ;
0 commit comments