From 5c8f15bb074db6363bba7d28c18b96ca5c9ccf4d Mon Sep 17 00:00:00 2001 From: Max Heller Date: Mon, 24 Jun 2024 08:52:17 -0400 Subject: [PATCH] fix: resolve links correctly when book contains exactly one chapter (#101) Pandoc's `--file-scope` option [only takes effect if there are at least two files](https://pandoc.org/MANUAL.html#option--file-scope[), so if there is only one file, links will not be adjusted correctly. In this case, add an additional empty dummy file as input to convince Pandoc to perform the link adjustment. --- .github/workflows/ci.yml | 2 +- src/lib.rs | 135 ++++++++++++++++++++++++++++++++++++--- src/pandoc/renderer.rs | 19 ++++++ 3 files changed, 145 insertions(+), 11 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dadad01..ccd5a64 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,7 +17,7 @@ jobs: - name: Install dependencies run: ./scripts/install-ci-deps - name: nextest - run: cargo nextest run + run: cargo nextest run --run-ignored all - name: doctest run: cargo test --doc - name: Archive rendered books diff --git a/src/lib.rs b/src/lib.rs index 2ed3359..efa0231 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -570,7 +570,12 @@ mod tests { │ INFO mdbook::book: Running the pandoc backend │ INFO mdbook_pandoc::pandoc::renderer: Wrote output to book/markdown/book.md ├─ markdown/book.md - │ # Getting Started + │ ::: {#book__markdown__src__getting-startedmd} + │ # Getting Started {#book__markdown__src__getting-startedmd__getting-started} + │ ::: + │ + │ ::: {#book__markdown__dummy} + │ ::: "###); } @@ -590,7 +595,12 @@ mod tests { │ WARN mdbook_pandoc: Unable to resolve one or more relative links within the book, consider setting the `hosted-html` option in `[output.pandoc]` │ INFO mdbook_pandoc::pandoc::renderer: Wrote output to book/markdown/book.md ├─ markdown/book.md + │ ::: {#book__markdown__src__getting-startedmd} │ [broken link](foobarbaz) + │ ::: + │ + │ ::: {#book__markdown__dummy} + │ ::: "###); } @@ -605,7 +615,10 @@ mod tests { │ INFO mdbook::book: Running the pandoc backend │ INFO mdbook_pandoc::pandoc::renderer: Wrote output to book/latex/output.tex ├─ latex/output.tex + │ \phantomsection\label{book__latex__src__chaptermd} │ \st{test1} \st{test2} + │ + │ \phantomsection\label{book__latex__dummy} ├─ latex/src/chapter.md │ ~~test1~~ ~~test2~~ "###); @@ -626,6 +639,7 @@ mod tests { │ INFO mdbook::book: Running the pandoc backend │ INFO mdbook_pandoc::pandoc::renderer: Wrote output to book/latex/output.tex ├─ latex/output.tex + │ \phantomsection\label{book__latex__src__chaptermd} │ \begin{itemize} │ \tightlist │ \item[$\boxtimes$] @@ -633,6 +647,8 @@ mod tests { │ \item[$\square$] │ Incomplete task │ \end{itemize} + │ + │ \phantomsection\label{book__latex__dummy} ├─ latex/src/chapter.md │ * [x] Complete task │ * [ ] Incomplete task @@ -654,9 +670,12 @@ mod tests { │ INFO mdbook::book: Running the pandoc backend │ INFO mdbook_pandoc::pandoc::renderer: Wrote output to book/latex/output.tex ├─ latex/output.tex - │ \chapter{Heading}\label{custom-heading} + │ \phantomsection\label{book__latex__src__chaptermd} + │ \chapter{Heading}\label{book__latex__src__chaptermd__custom-heading} + │ + │ \hyperref[book__latex__src__chaptermd__custom-heading]{heading} │ - │ \hyperref[custom-heading]{heading} + │ \phantomsection\label{book__latex__dummy} ├─ latex/src/chapter.md │ # Heading { #custom-heading } │ @@ -684,8 +703,11 @@ This is an example of a footnote[^note]. │ INFO mdbook::book: Running the pandoc backend │ INFO mdbook_pandoc::pandoc::renderer: Wrote output to book/latex/output.tex ├─ latex/output.tex + │ \phantomsection\label{book__latex__src__chaptermd} │ This is an example of a footnote\footnote{This text is the contents of │ the footnote, which will be rendered towards the bottom.}. + │ + │ \phantomsection\label{book__latex__dummy} ├─ latex/src/chapter.md │ This is an example of a footnote[^note]. │ @@ -713,6 +735,7 @@ This is an example of a footnote[^note]. │ INFO mdbook::book: Running the pandoc backend │ INFO mdbook_pandoc::pandoc::renderer: Wrote output to book/latex/output.tex ├─ latex/output.tex + │ \phantomsection\label{book__latex__src__chaptermd} │ \begin{longtable}[]{@{}ll@{}} │ \toprule\noalign{} │ Header1 & Header2 \\ @@ -722,6 +745,8 @@ This is an example of a footnote[^note]. │ \endlastfoot │ abc & def \\ │ \end{longtable} + │ + │ \phantomsection\label{book__latex__dummy} ├─ latex/src/chapter.md │ |Header1|Header2| │ |-------|-------| @@ -748,6 +773,7 @@ This is an example of a footnote[^note]. │ INFO mdbook::book: Running the pandoc backend │ INFO mdbook_pandoc::pandoc::renderer: Wrote output to book/latex/output.tex ├─ latex/output.tex + │ \phantomsection\label{book__latex__src__chaptermd} │ \begin{longtable}[]{@{} │ >{\raggedright\arraybackslash}p{(\columnwidth - 2\tabcolsep) * \real{0.0986}} │ >{\raggedright\arraybackslash}p{(\columnwidth - 2\tabcolsep) * \real{0.9014}}@{}} @@ -764,6 +790,8 @@ This is an example of a footnote[^note]. │ abc & long long long long long long long long long long long long │ long \\ │ \end{longtable} + │ + │ \phantomsection\label{book__latex__dummy} ├─ latex/src/chapter.md │ │ |Header1|Header2| @@ -882,7 +910,10 @@ This is an example of a footnote[^note]. │ INFO mdbook::book: Running the pandoc backend │ INFO mdbook_pandoc::pandoc::renderer: Wrote output to book/latex/output.tex ├─ latex/output.tex + │ \phantomsection\label{book__latex__src__chaptermd} │ \faicon{print} \faicon{print} \faicon{print} + │ + │ \phantomsection\label{book__latex__dummy} ├─ latex/src/chapter.md │ `\faicon{print}`{=latex} │ `\faicon{print}`{=latex} @@ -901,9 +932,14 @@ This is an example of a footnote[^note]. │ INFO mdbook::book: Running the pandoc backend │ INFO mdbook_pandoc::pandoc::renderer: Wrote output to book/markdown/book.md ├─ markdown/book.md + │ ::: {#book__markdown__src__chaptermd} │ ```{=html} │ │ ``` + │ ::: + │ + │ ::: {#book__markdown__dummy} + │ ::: "###); } @@ -926,9 +962,14 @@ println!("Hello, world!"); │ INFO mdbook::book: Running the pandoc backend │ INFO mdbook_pandoc::pandoc::renderer: Wrote output to book/markdown/book.md ├─ markdown/book.md + │ ::: {#book__markdown__src__chaptermd} │ ``` rust │ println!("Hello, world!"); │ ``` + │ ::: + │ + │ ::: {#book__markdown__dummy} + │ ::: "###); let book = MDBook::init() .config(Config { @@ -944,12 +985,17 @@ println!("Hello, world!"); │ INFO mdbook::book: Running the pandoc backend │ INFO mdbook_pandoc::pandoc::renderer: Wrote output to book/markdown/book.md ├─ markdown/book.md + │ ::: {#book__markdown__src__chaptermd} │ ``` rust │ # fn main() { │ # // another hidden line │ println!("Hello, world!"); │ # } │ ``` + │ ::: + │ + │ ::: {#book__markdown__dummy} + │ ::: "###); } @@ -978,10 +1024,15 @@ python = "~" │ INFO mdbook::book: Running the pandoc backend │ INFO mdbook_pandoc::pandoc::renderer: Wrote output to book/markdown/book.md ├─ markdown/book.md + │ ::: {#book__markdown__src__chaptermd} │ ``` python │ nothidden(): │ nothidden() │ ``` + │ ::: + │ + │ ::: {#book__markdown__dummy} + │ ::: "###); let book = MDBook::init() .config(Config { @@ -997,6 +1048,7 @@ python = "~" │ INFO mdbook::book: Running the pandoc backend │ INFO mdbook_pandoc::pandoc::renderer: Wrote output to book/markdown/book.md ├─ markdown/book.md + │ ::: {#book__markdown__src__chaptermd} │ ``` python │ ~hidden() │ nothidden(): @@ -1004,6 +1056,10 @@ python = "~" │ ~hidden() │ nothidden() │ ``` + │ ::: + │ + │ ::: {#book__markdown__dummy} + │ ::: "###); } @@ -1027,10 +1083,15 @@ nothidden(): │ INFO mdbook::book: Running the pandoc backend │ INFO mdbook_pandoc::pandoc::renderer: Wrote output to book/markdown/book.md ├─ markdown/book.md + │ ::: {#book__markdown__src__chaptermd} │ ``` python │ nothidden(): │ nothidden() │ ``` + │ ::: + │ + │ ::: {#book__markdown__dummy} + │ ::: "###); } @@ -1100,6 +1161,7 @@ fn main() {} │ INFO mdbook::book: Running the pandoc backend │ INFO mdbook_pandoc::pandoc::renderer: Wrote output to book/latex/output.tex ├─ latex/output.tex + │ \phantomsection\label{book__latex__src__chaptermd} │ \begin{Shaded} │ \begin{Highlighting}[] │ \KeywordTok{fn}\NormalTok{ main() }\OperatorTok{\{\}} @@ -1111,6 +1173,8 @@ fn main() {} │ \KeywordTok{fn}\NormalTok{ main() }\OperatorTok{\{\}} │ \end{Highlighting} │ \end{Shaded} + │ + │ \phantomsection\label{book__latex__dummy} ├─ latex/src/chapter.md │ │ ````rust @@ -1142,13 +1206,40 @@ fn main() {} │ INFO mdbook::book: Running the pandoc backend │ INFO mdbook_pandoc::pandoc::renderer: Wrote output to book/latex/output.tex ├─ latex/output.tex - │ \href{book/latex/src/chapter.md}{link} + │ \phantomsection\label{book__latex__src__chaptermd} + │ \hyperref[book__latex__src__chaptermd]{link} + │ + │ \phantomsection\label{book__latex__dummy} ├─ latex/src/chapter.md │ [link](book/latex/src/chapter.md "\"foo\" (bar)") │ "###); } + #[test] + fn single_chapter_with_explicit_self_link() { + let book = MDBook::init() + .config(Config::latex()) + .chapter(Chapter::new( + "Chapter One", + "[link](chapter.md)", + "chapter.md", + )) + .build(); + insta::assert_snapshot!(book, @r###" + ├─ log output + │ INFO mdbook::book: Running the pandoc backend + │ INFO mdbook_pandoc::pandoc::renderer: Wrote output to book/latex/output.tex + ├─ latex/output.tex + │ \phantomsection\label{book__latex__src__chaptermd} + │ \hyperref[book__latex__src__chaptermd]{link} + │ + │ \phantomsection\label{book__latex__dummy} + ├─ latex/src/chapter.md + │ [link](book/latex/src/chapter.md) + "###); + } + #[test] fn preserve_escapes() { let output = MDBook::init() @@ -1160,13 +1251,17 @@ fn main() {} │ INFO mdbook::book: Running the pandoc backend │ INFO mdbook_pandoc::pandoc::renderer: Wrote output to book/markdown/pandoc-ir ├─ markdown/pandoc-ir - │ [ Para - │ [ Str "[Prefix" - │ , Space - │ , Str "@fig:1]" - │ , Space - │ , Str "[-@fig:1]" + │ [ Div + │ ( "book__markdown__src__chaptermd" , [] , [] ) + │ [ Para + │ [ Str "[Prefix" + │ , Space + │ , Str "@fig:1]" + │ , Space + │ , Str "[-@fig:1]" + │ ] │ ] + │ , Div ( "book__markdown__dummy" , [] , [] ) [] │ ] "###); } @@ -1212,6 +1307,17 @@ fn main() {} │ ), │ to: None, │ table_of_contents: true, + @@ -19,8 +19,8 @@ + │ } + │ INFO mdbook_pandoc::pandoc::renderer: Wrote output to book/markdown/book.md + ├─ markdown/book.md + -│ ::: {#book__markdown__src__chaptermd} + -│ # Chapter {#book__markdown__src__chaptermd__chapter} + +│ ::: {#book__markdown__src__chapter.md} + +│ # Chapter + │ ::: + │ + │ ::: {#book__markdown__dummy} "###); } @@ -1388,6 +1494,7 @@ include-in-header = ["file-in-root"] static BOOKS: Lazy = Lazy::new(|| Path::new(env!("CARGO_MANIFEST_DIR")).join("books")); #[test] + #[ignore] fn mdbook_guide() { let logs = MDBook::load(BOOKS.join("mdBook/guide")) .config(Config { @@ -1400,6 +1507,7 @@ include-in-header = ["file-in-root"] } #[test] + #[ignore] fn cargo_book() { let logs = MDBook::options() .max_log_level(tracing::Level::DEBUG) @@ -1414,6 +1522,7 @@ include-in-header = ["file-in-root"] } #[test] + #[ignore] fn rust_book() { let logs = MDBook::load(BOOKS.join("rust-book")) .config(Config { @@ -1426,6 +1535,7 @@ include-in-header = ["file-in-root"] } #[test] + #[ignore] fn nomicon() { let logs = MDBook::load(BOOKS.join("nomicon")) .config(Config { @@ -1438,6 +1548,7 @@ include-in-header = ["file-in-root"] } #[test] + #[ignore] fn rust_by_example() { let logs = MDBook::load(BOOKS.join("rust-by-example")) .config(Config { @@ -1450,6 +1561,7 @@ include-in-header = ["file-in-root"] } #[test] + #[ignore] fn rust_edition_guide() { let logs = MDBook::load(BOOKS.join("rust-edition-guide")) .config(Config { @@ -1462,6 +1574,7 @@ include-in-header = ["file-in-root"] } #[test] + #[ignore] fn rust_embedded() { let logs = MDBook::load(BOOKS.join("rust-embedded")) .config(Config { @@ -1474,6 +1587,7 @@ include-in-header = ["file-in-root"] } #[test] + #[ignore] fn rust_reference() { let logs = MDBook::load(BOOKS.join("rust-reference")) .config(Config { @@ -1486,6 +1600,7 @@ include-in-header = ["file-in-root"] } #[test] + #[ignore] fn rustc_dev_guide() { let logs = MDBook::load(BOOKS.join("rustc-dev-guide")) .config(Config { diff --git a/src/pandoc/renderer.rs b/src/pandoc/renderer.rs index 9819651..a27a272 100644 --- a/src/pandoc/renderer.rs +++ b/src/pandoc/renderer.rs @@ -21,6 +21,7 @@ use crate::{ pub struct Renderer { pandoc: Command, + num_inputs: usize, } pub struct Context<'book> { @@ -45,6 +46,7 @@ impl Renderer { pub(crate) fn new() -> Self { Self { pandoc: Command::new("pandoc"), + num_inputs: 0, } } @@ -60,6 +62,7 @@ impl Renderer { pub fn input(&mut self, input: impl AsRef) -> &mut Self { self.pandoc.arg(input.as_ref()); + self.num_inputs += 1; self } @@ -268,6 +271,22 @@ impl Renderer { }; pandoc.arg("-d").arg(defaults_file.path()); + // --file-scope only works if there are at least two files, so if there is only one file, + // add an additionaly empty file to convince Pandoc to perform its link adjustment pass + let _dummy_tempfile_guard: tempfile::TempPath; + if self.num_inputs == 1 { + let dummy = tempfile::Builder::new() + .prefix("dummy") + .rand_bytes(0) + .tempfile_in(&ctx.destination)?; + let path = dummy + .path() + .normalize() + .context("failed to normalize dummy file path")?; + pandoc.arg(path.as_path().strip_prefix(&ctx.book.root).unwrap()); + _dummy_tempfile_guard = dummy.into_temp_path(); + } + if log::log_enabled!(log::Level::Trace) { log::trace!("Running pandoc with profile: {profile:#?}"); } else {