@@ -20,6 +20,7 @@ use std::process::Command;
20
20
use std:: string:: ToString ;
21
21
use tempfile:: Builder as TempFileBuilder ;
22
22
use toml:: Value ;
23
+ use topological_sort:: TopologicalSort ;
23
24
24
25
use crate :: errors:: * ;
25
26
use crate :: preprocess:: {
@@ -372,12 +373,7 @@ fn determine_renderers(config: &Config) -> Vec<Box<dyn Renderer>> {
372
373
renderers
373
374
}
374
375
375
- fn default_preprocessors ( ) -> Vec < Box < dyn Preprocessor > > {
376
- vec ! [
377
- Box :: new( LinkPreprocessor :: new( ) ) ,
378
- Box :: new( IndexPreprocessor :: new( ) ) ,
379
- ]
380
- }
376
+ const DEFAULT_PREPROCESSORS : & [ & ' static str ] = & [ "links" , "index" ] ;
381
377
382
378
fn is_default_preprocessor ( pre : & dyn Preprocessor ) -> bool {
383
379
let name = pre. name ( ) ;
@@ -386,36 +382,127 @@ fn is_default_preprocessor(pre: &dyn Preprocessor) -> bool {
386
382
387
383
/// Look at the `MDBook` and try to figure out what preprocessors to run.
388
384
fn determine_preprocessors ( config : & Config ) -> Result < Vec < Box < dyn Preprocessor > > > {
389
- let mut preprocessors = Vec :: new ( ) ;
385
+ // Collect the names of all preprocessors intended to be run, and the order
386
+ // in which they should be run.
387
+ let mut preprocessor_names = TopologicalSort :: < String > :: new ( ) ;
390
388
391
389
if config. build . use_default_preprocessors {
392
- preprocessors. extend ( default_preprocessors ( ) ) ;
390
+ for name in DEFAULT_PREPROCESSORS {
391
+ preprocessor_names. insert ( name. to_string ( ) ) ;
392
+ }
393
393
}
394
394
395
395
if let Some ( preprocessor_table) = config. get ( "preprocessor" ) . and_then ( Value :: as_table) {
396
- for key in preprocessor_table. keys ( ) {
397
- match key. as_ref ( ) {
398
- "links" => preprocessors. push ( Box :: new ( LinkPreprocessor :: new ( ) ) ) ,
399
- "index" => preprocessors. push ( Box :: new ( IndexPreprocessor :: new ( ) ) ) ,
400
- name => preprocessors. push ( interpret_custom_preprocessor (
401
- name,
402
- & preprocessor_table[ name] ,
403
- ) ) ,
396
+ for ( name, table) in preprocessor_table. iter ( ) {
397
+ preprocessor_names. insert ( name. to_string ( ) ) ;
398
+
399
+ let exists = |name| {
400
+ ( config. build . use_default_preprocessors && DEFAULT_PREPROCESSORS . contains ( & name) )
401
+ || preprocessor_table. contains_key ( name)
402
+ } ;
403
+
404
+ if let Some ( before) = table. get ( "before" ) {
405
+ let before = before. as_array ( ) . ok_or_else ( || {
406
+ Error :: msg ( format ! (
407
+ "Expected preprocessor.{}.before to be an array" ,
408
+ name
409
+ ) )
410
+ } ) ?;
411
+ for after in before {
412
+ let after = after. as_str ( ) . ok_or_else ( || {
413
+ Error :: msg ( format ! (
414
+ "Expected preprocessor.{}.before to contain strings" ,
415
+ name
416
+ ) )
417
+ } ) ?;
418
+
419
+ if !exists ( after) {
420
+ // Only warn so that preprocessors can be toggled on and off (e.g. for
421
+ // troubleshooting) without having to worry about order too much.
422
+ warn ! (
423
+ "preprocessor.{}.after contains \" {}\" , which was not found" ,
424
+ name, after
425
+ ) ;
426
+ } else {
427
+ preprocessor_names. add_dependency ( name, after) ;
428
+ }
429
+ }
430
+ }
431
+
432
+ if let Some ( after) = table. get ( "after" ) {
433
+ let after = after. as_array ( ) . ok_or_else ( || {
434
+ Error :: msg ( format ! (
435
+ "Expected preprocessor.{}.after to be an array" ,
436
+ name
437
+ ) )
438
+ } ) ?;
439
+ for before in after {
440
+ let before = before. as_str ( ) . ok_or_else ( || {
441
+ Error :: msg ( format ! (
442
+ "Expected preprocessor.{}.after to contain strings" ,
443
+ name
444
+ ) )
445
+ } ) ?;
446
+
447
+ if !exists ( before) {
448
+ // See equivalent warning above for rationale
449
+ warn ! (
450
+ "preprocessor.{}.before contains \" {}\" , which was not found" ,
451
+ name, before
452
+ ) ;
453
+ } else {
454
+ preprocessor_names. add_dependency ( before, name) ;
455
+ }
456
+ }
404
457
}
405
458
}
406
459
}
407
460
408
- Ok ( preprocessors)
461
+ // Now that all links have been established, queue preprocessors in a suitable order
462
+ let mut preprocessors = Vec :: with_capacity ( preprocessor_names. len ( ) ) ;
463
+ // `pop_all()` returns an empty vector when no more items are not being depended upon
464
+ for mut names in std:: iter:: repeat_with ( || preprocessor_names. pop_all ( ) )
465
+ . take_while ( |names| !names. is_empty ( ) )
466
+ {
467
+ // The `topological_sort` crate does not guarantee a stable order for ties, even across
468
+ // runs of the same program. Thus, we break ties manually by sorting.
469
+ // Careful: `str`'s default sorting, which we are implicitly invoking here, uses code point
470
+ // values ([1]), which may not be an alphabetical sort.
471
+ // As mentioned in [1], doing so depends on locale, which is not desirable for deciding
472
+ // preprocessor execution order.
473
+ // [1]: https://doc.rust-lang.org/stable/std/cmp/trait.Ord.html#impl-Ord-14
474
+ names. sort ( ) ;
475
+ for name in names {
476
+ let preprocessor: Box < dyn Preprocessor > = match name. as_str ( ) {
477
+ "links" => Box :: new ( LinkPreprocessor :: new ( ) ) ,
478
+ "index" => Box :: new ( IndexPreprocessor :: new ( ) ) ,
479
+ _ => {
480
+ // The only way to request a custom preprocessor is through the `preprocessor`
481
+ // table, so it must exist, be a table, and contain the key.
482
+ let table = & config. get ( "preprocessor" ) . unwrap ( ) . as_table ( ) . unwrap ( ) [ & name] ;
483
+ let command = get_custom_preprocessor_cmd ( & name, table) ;
484
+ Box :: new ( CmdPreprocessor :: new ( name, command) )
485
+ }
486
+ } ;
487
+ preprocessors. push ( preprocessor) ;
488
+ }
489
+ }
490
+
491
+ // "If `pop_all` returns an empty vector and `len` is not 0, there are cyclic dependencies."
492
+ // Normally, `len() == 0` is equivalent to `is_empty()`, so we'll use that.
493
+ if preprocessor_names. is_empty ( ) {
494
+ Ok ( preprocessors)
495
+ } else {
496
+ Err ( Error :: msg ( "Cyclic dependency detected in preprocessors" ) )
497
+ }
409
498
}
410
499
411
- fn interpret_custom_preprocessor ( key : & str , table : & Value ) -> Box < CmdPreprocessor > {
412
- let command = table
500
+ fn get_custom_preprocessor_cmd ( key : & str , table : & Value ) -> String {
501
+ table
413
502
. get ( "command" )
414
503
. and_then ( Value :: as_str)
415
504
. map ( ToString :: to_string)
416
- . unwrap_or_else ( || format ! ( "mdbook-{}" , key) ) ;
417
-
418
- Box :: new ( CmdPreprocessor :: new ( key. to_string ( ) , command) )
505
+ . unwrap_or_else ( || format ! ( "mdbook-{}" , key) )
419
506
}
420
507
421
508
fn interpret_custom_renderer ( key : & str , table : & Value ) -> Box < CmdRenderer > {
@@ -515,8 +602,8 @@ mod tests {
515
602
516
603
assert ! ( got. is_ok( ) ) ;
517
604
assert_eq ! ( got. as_ref( ) . unwrap( ) . len( ) , 2 ) ;
518
- assert_eq ! ( got. as_ref( ) . unwrap( ) [ 0 ] . name( ) , "links " ) ;
519
- assert_eq ! ( got. as_ref( ) . unwrap( ) [ 1 ] . name( ) , "index " ) ;
605
+ assert_eq ! ( got. as_ref( ) . unwrap( ) [ 0 ] . name( ) , "index " ) ;
606
+ assert_eq ! ( got. as_ref( ) . unwrap( ) [ 1 ] . name( ) , "links " ) ;
520
607
}
521
608
522
609
#[ test]
@@ -563,9 +650,123 @@ mod tests {
563
650
564
651
// make sure the `preprocessor.random` table exists
565
652
let random = cfg. get_preprocessor ( "random" ) . unwrap ( ) ;
566
- let random = interpret_custom_preprocessor ( "random" , & Value :: Table ( random. clone ( ) ) ) ;
653
+ let random = get_custom_preprocessor_cmd ( "random" , & Value :: Table ( random. clone ( ) ) ) ;
654
+
655
+ assert_eq ! ( random, "python random.py" ) ;
656
+ }
657
+
658
+ #[ test]
659
+ fn preprocessor_before_must_be_array ( ) {
660
+ let cfg_str = r#"
661
+ [preprocessor.random]
662
+ before = 0
663
+ "# ;
567
664
568
- assert_eq ! ( random. cmd( ) , "python random.py" ) ;
665
+ let cfg = Config :: from_str ( cfg_str) . unwrap ( ) ;
666
+
667
+ assert ! ( determine_preprocessors( & cfg) . is_err( ) ) ;
668
+ }
669
+
670
+ #[ test]
671
+ fn preprocessor_after_must_be_array ( ) {
672
+ let cfg_str = r#"
673
+ [preprocessor.random]
674
+ after = 0
675
+ "# ;
676
+
677
+ let cfg = Config :: from_str ( cfg_str) . unwrap ( ) ;
678
+
679
+ assert ! ( determine_preprocessors( & cfg) . is_err( ) ) ;
680
+ }
681
+
682
+ #[ test]
683
+ fn preprocessor_order_is_honored ( ) {
684
+ let cfg_str = r#"
685
+ [preprocessor.random]
686
+ before = [ "last" ]
687
+ after = [ "index" ]
688
+
689
+ [preprocessor.last]
690
+ after = [ "links", "index" ]
691
+ "# ;
692
+
693
+ let cfg = Config :: from_str ( cfg_str) . unwrap ( ) ;
694
+
695
+ let preprocessors = determine_preprocessors ( & cfg) . unwrap ( ) ;
696
+ let index = |name| {
697
+ preprocessors
698
+ . iter ( )
699
+ . enumerate ( )
700
+ . find ( |( _, preprocessor) | preprocessor. name ( ) == name)
701
+ . unwrap ( )
702
+ . 0
703
+ } ;
704
+ let assert_before = |before, after| {
705
+ if index ( before) >= index ( after) {
706
+ eprintln ! ( "Preprocessor order:" ) ;
707
+ for preprocessor in & preprocessors {
708
+ eprintln ! ( " {}" , preprocessor. name( ) ) ;
709
+ }
710
+ panic ! ( "{} should come before {}" , before, after) ;
711
+ }
712
+ } ;
713
+
714
+ assert_before ( "index" , "random" ) ;
715
+ assert_before ( "index" , "last" ) ;
716
+ assert_before ( "random" , "last" ) ;
717
+ assert_before ( "links" , "last" ) ;
718
+ }
719
+
720
+ #[ test]
721
+ fn cyclic_dependencies_are_detected ( ) {
722
+ let cfg_str = r#"
723
+ [preprocessor.links]
724
+ before = [ "index" ]
725
+
726
+ [preprocessor.index]
727
+ before = [ "links" ]
728
+ "# ;
729
+
730
+ let cfg = Config :: from_str ( cfg_str) . unwrap ( ) ;
731
+
732
+ assert ! ( determine_preprocessors( & cfg) . is_err( ) ) ;
733
+ }
734
+
735
+ #[ test]
736
+ fn dependencies_dont_register_undefined_preprocessors ( ) {
737
+ let cfg_str = r#"
738
+ [preprocessor.links]
739
+ before = [ "random" ]
740
+ "# ;
741
+
742
+ let cfg = Config :: from_str ( cfg_str) . unwrap ( ) ;
743
+
744
+ let preprocessors = determine_preprocessors ( & cfg) . unwrap ( ) ;
745
+
746
+ assert ! ( preprocessors
747
+ . iter( )
748
+ . find( |preprocessor| preprocessor. name( ) == "random" )
749
+ . is_none( ) ) ;
750
+ }
751
+
752
+ #[ test]
753
+ fn dependencies_dont_register_builtin_preprocessors_if_disabled ( ) {
754
+ let cfg_str = r#"
755
+ [preprocessor.random]
756
+ before = [ "links" ]
757
+
758
+ [build]
759
+ use-default-preprocessors = false
760
+ "# ;
761
+
762
+ let cfg = Config :: from_str ( cfg_str) . unwrap ( ) ;
763
+
764
+ let preprocessors = determine_preprocessors ( & cfg) . unwrap ( ) ;
765
+
766
+ assert ! ( preprocessors
767
+ . iter( )
768
+ . find( |preprocessor| preprocessor. name( ) == "links" )
769
+ . is_none( ) ) ;
569
770
}
570
771
571
772
#[ test]
0 commit comments