Skip to content

Commit 6b784be

Browse files
committed
Stabilize ties in preproc order through name sort
1 parent 9c34e60 commit 6b784be

File tree

2 files changed

+35
-20
lines changed

2 files changed

+35
-20
lines changed

guide/src/format/configuration/preprocessors.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,8 @@ command = "python random.py"
5959

6060
### Require A Certain Order
6161

62-
The order in which preprocessors are run is not guaranteed, but you can request some to run before or after others.
63-
For example, suppose you want your `linenos` preprocessor to process lines that may have been `{{#include}}`d; then you want it to run after the built-in `links` preprocessor, which you can require using the `before` and/or `after` fields.
62+
The order in which preprocessors are run can be controlled with the `before` and `after` fields.
63+
For example, suppose you want your `linenos` preprocessor to process lines that may have been `{{#include}}`d; then you want it to run after the built-in `links` preprocessor, which you can require using either the `before` or `after` field:
6464

6565
```toml
6666
[preprocessor.linenos]
@@ -74,7 +74,7 @@ or
7474
before = [ "linenos" ]
7575
```
7676

77-
It would be possible, though redundant, to specify both of the above in the same config file.
77+
It would also be possible, though redundant, to specify both of the above in the same config file.
7878

79-
`mdbook` will detect any infinite loops and error out.
80-
Note that order of preprocessors besides what is specified using `before` and `after` is not guaranteed, and may change within the same run of `mdbook`.
79+
Preprocessors having the same priority specified through `before` and `after` are sorted by name.
80+
Any infinite loops will be detected and produce an error.

src/book/mod.rs

Lines changed: 30 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -435,21 +435,36 @@ fn determine_preprocessors(config: &Config) -> Result<Vec<Box<dyn Preprocessor>>
435435

436436
// Now that all links have been established, queue preprocessors in a suitable order
437437
let mut preprocessors = Vec::with_capacity(preprocessor_names.len());
438-
while let Some(name) = preprocessor_names.pop() {
439-
let preprocessor: Box<dyn Preprocessor> = match name.as_str() {
440-
"links" => Box::new(LinkPreprocessor::new()),
441-
"index" => Box::new(IndexPreprocessor::new()),
442-
_ => {
443-
// The only way to request a custom preprocessor is through the `preprocessor`
444-
// table, so it must exist, be a table, and contain the key.
445-
let table = &config.get("preprocessor").unwrap().as_table().unwrap()[&name];
446-
let command = get_custom_preprocessor_cmd(&name, table);
447-
Box::new(CmdPreprocessor::new(name, command))
448-
}
449-
};
450-
preprocessors.push(preprocessor);
438+
// `pop_all()` returns an empty vector when no more items are not being depended upon
439+
for mut names in std::iter::repeat_with(|| preprocessor_names.pop_all())
440+
.take_while(|names| !names.is_empty())
441+
{
442+
// The `topological_sort` crate does not guarantee a stable order for ties, even across
443+
// runs of the same program. Thus, we break ties manually by sorting.
444+
// Careful: `str`'s default sorting, which we are implicitly invoking here, uses code point
445+
// values ([1]), which may not be an alphabetical sort.
446+
// As mentioned in [1], doing so depends on locale, which is not desirable for deciding
447+
// preprocessor execution order.
448+
// [1]: https://doc.rust-lang.org/stable/std/cmp/trait.Ord.html#impl-Ord-14
449+
names.sort();
450+
for name in names {
451+
let preprocessor: Box<dyn Preprocessor> = match name.as_str() {
452+
"links" => Box::new(LinkPreprocessor::new()),
453+
"index" => Box::new(IndexPreprocessor::new()),
454+
_ => {
455+
// The only way to request a custom preprocessor is through the `preprocessor`
456+
// table, so it must exist, be a table, and contain the key.
457+
let table = &config.get("preprocessor").unwrap().as_table().unwrap()[&name];
458+
let command = get_custom_preprocessor_cmd(&name, table);
459+
Box::new(CmdPreprocessor::new(name, command))
460+
}
461+
};
462+
preprocessors.push(preprocessor);
463+
}
451464
}
452465

466+
// "If `pop_all` returns an empty vector and `len` is not 0, there are cyclic dependencies."
467+
// Normally, `len() == 0` is equivalent to `is_empty()`, so we'll use that.
453468
if preprocessor_names.is_empty() {
454469
Ok(preprocessors)
455470
} else {
@@ -562,8 +577,8 @@ mod tests {
562577

563578
assert!(got.is_ok());
564579
assert_eq!(got.as_ref().unwrap().len(), 2);
565-
assert_eq!(got.as_ref().unwrap()[0].name(), "links");
566-
assert_eq!(got.as_ref().unwrap()[1].name(), "index");
580+
assert_eq!(got.as_ref().unwrap()[0].name(), "index");
581+
assert_eq!(got.as_ref().unwrap()[1].name(), "links");
567582
}
568583

569584
#[test]

0 commit comments

Comments
 (0)