mdbook-bibtex 0.1.0

Adds support for bibliographies to mdbook. Backed by hayagriva, supporting all CSL citation styles.
Documentation
use std::fs;

use tempfile::NamedTempFile;

use super::*;

const BIB_ENTRY: &str = r##"
@online{ctan,
  title        = {CTAN},
  date         = 2006,
  url          = {http://www.ctan.org},
  subtitle     = {The {Comprehensive TeX Archive Network}},
  urldate      = {2006-10-01},
  label        = {CTAN},
  langid       = {english},
  langidopts   = {variant=american},
  annotation   = {This is an \texttt{online} entry. The \textsc{url}, which is
                  given in the \texttt{url} field, is transformed into a
                  clickable link if \texttt{hyperref} support has been
                  enabled. Note the format of the \texttt{urldate} field
                  (\texttt{yyyy-mm-dd}) in the database file. Also note the
                  \texttt{label} field which may be used as a fallback by
                  citation styles which need an \texttt{author} and\slash or a
                  \texttt{year}},
}
@online{ctan2,
  title        = {CTAN},
  date         = 2006,
  url          = {http://www.ctan.org},
  subtitle     = {The {Comprehensive TeX Archive Network}},
  urldate      = {2006-10-01},
  label        = {CTAN},
  langid       = {english},
  langidopts   = {variant=american},
  annotation   = {This is an \texttt{online} entry. The \textsc{url}, which is
                  given in the \texttt{url} field, is transformed into a
                  clickable link if \texttt{hyperref} support has been
                  enabled. Note the format of the \texttt{urldate} field
                  (\texttt{yyyy-mm-dd}) in the database file. Also note the
                  \texttt{label} field which may be used as a fallback by
                  citation styles which need an \texttt{author} and\slash or a
                  \texttt{year}},
}
"##;

const JSON_TEMPLATE: &str = r##"[
    {
        "root": "/path/to/book",
        "config": {
            "book": {
                "authors": ["AUTHOR"],
                "language": "en",
                "src": "src",
                "title": "TITLE"
            },
            "preprocessor": {
                "bibtex": {
                    CONFIG_PLACEHOLDER
                }
            }
        },
        "renderer": "html",
        "mdbook_version": "0.5.2"
    },
    {
        "items": [
            {
                "Chapter": {
                    "name": "Chapter 1",
                    "content": "CONTENT_PLACEHOLDER",
                    "number": [1],
                    "sub_items": [],
                    "path": "chapter_1.md",
                    "source_path": "chapter_1.md",
                    "parent_names": []
                }
            }
        ]
    }
]"##;

fn get_bibtex() -> anyhow::Result<NamedTempFile> {
    let tmp = tempfile::Builder::new().suffix(".bib").tempfile()?;
    fs::write(&tmp, BIB_ENTRY)?;
    Ok(tmp)
}

#[test]
fn test_no_bib() -> anyhow::Result<()> {
    let input = JSON_TEMPLATE
        .replace(
            "CONFIG_PLACEHOLDER",
            r##"
            "style": "ieee",
            "errors_on_missing_keys": true,
            "biblio_location": "global",
            "display_all": true,
            "citation_preview": false
        "##,
        )
        .replace("CONTENT_PLACEHOLDER", "# Chapter 1\\n");
    let input_json = input.as_bytes();

    let (ctx, book) = mdbook_preprocessor::parse_input(input_json)?;
    let _ = BibtexPreprocessor.run(&ctx, book)?;
    Ok(())
}

#[test]
fn test_conf() -> anyhow::Result<()> {
    let bib_file = get_bibtex()?;
    let input = JSON_TEMPLATE
        .replace(
            "CONFIG_PLACEHOLDER",
            &format!(
                r##"
            "bibliography": "{}",
            "style": "ieee",
            "errors_on_missing_keys": true,
            "biblio_location": "global",
            "display_all": true,
            "citation_preview": false
            "##,
                bib_file.path().display()
            ),
        )
        .replace("CONTENT_PLACEHOLDER", "# Chapter 1\\n@@ctan");
    let input_json = input.as_bytes();

    let (ctx, book) = mdbook_preprocessor::parse_input(input_json).unwrap();
    let book = BibtexPreprocessor.run(&ctx, book)?;

    assert!(
        book.chapters()
            .find(|c| c.name == "Chapter 1")
            .unwrap()
            .content
            .contains("[1]")
    );

    assert_eq!(2, book.chapters().count());

    assert!(
        book.chapters()
            .find(|c| c.name == "Bibliography")
            .unwrap()
            .content
            .contains("[2]")
    );

    assert!(
        !book
            .chapters()
            .find(|c| c.name == "Chapter 1")
            .unwrap()
            .content
            .contains(">[1]</span>")
    );

    Ok(())
}

#[test]
fn test_style_conf() -> anyhow::Result<()> {
    let bib_file = get_bibtex()?;
    let input = JSON_TEMPLATE
        .replace(
            "CONFIG_PLACEHOLDER",
            &format!(
                r##"
            "bibliography": "{}",
            "style": "apa",
            "errors_on_missing_keys": true,
            "biblio_location": "global",
            "display_all": true,
            "citation_preview": false
            "##,
                bib_file.path().display()
            ),
        )
        .replace("CONTENT_PLACEHOLDER", "# Chapter 1\\n@@ctan");
    let input_json = input.as_bytes();

    let (ctx, book) = mdbook_preprocessor::parse_input(input_json).unwrap();
    let book = BibtexPreprocessor.run(&ctx, book)?;

    assert!(
        book.chapters()
            .find(|c| c.name == "Chapter 1")
            .unwrap()
            .content
            .contains("(<span style=\"font-style: italic;\">CTAN</span>, 2006a)")
    );

    Ok(())
}

#[test]
fn test_warn_on_missing_keys() -> anyhow::Result<()> {
    let input = JSON_TEMPLATE
        .replace(
            "CONFIG_PLACEHOLDER",
            r##"
                "style": "ieee",
                "errors_on_missing_keys": false,
                "biblio_location": "global",
                "display_all": true,
                "citation_preview": false
            "##,
        )
        .replace("CONTENT_PLACEHOLDER", "# Chapter 1\\n@@ctan");
    let input_json = input.as_bytes();

    let (ctx, book) = mdbook_preprocessor::parse_input(input_json).unwrap();
    let book = BibtexPreprocessor.run(&ctx, book)?;

    assert!(
        book.chapters()
            .find(|c| c.name == "Chapter 1")
            .unwrap()
            .content
            .contains("@@ctan")
    );

    Ok(())
}

#[test]
fn test_chapter_bib() -> anyhow::Result<()> {
    let bib_file = get_bibtex()?;
    let input = JSON_TEMPLATE
        .replace(
            "CONFIG_PLACEHOLDER",
            &format!(
                r##"
                    "bibliography": "{}",
                    "style": "ieee",
                    "errors_on_missing_keys": true,
                    "biblio_location": "chapter",
                    "display_all": false,
                    "citation_preview": false
                "##,
                bib_file.path().display()
            ),
        )
        .replace("CONTENT_PLACEHOLDER", "# Chapter 1\\n@@ctan");
    let input_json = input.as_bytes();

    let (ctx, book) = mdbook_preprocessor::parse_input(input_json).unwrap();
    let book = BibtexPreprocessor.run(&ctx, book)?;

    assert!(
        book.chapters()
            .find(|c| c.name == "Chapter 1")
            .unwrap()
            .content
            .contains("[1]")
    );

    assert!(
        book.chapters()
            .find(|c| c.name == "Chapter 1")
            .unwrap()
            .content
            .contains("## Bibliography")
    );

    assert_eq!(1, book.chapters().count());

    Ok(())
}

#[test]
fn test_footnote_bib() -> anyhow::Result<()> {
    let bib_file = get_bibtex()?;
    let input = JSON_TEMPLATE
        .replace(
            "CONFIG_PLACEHOLDER",
            &format!(
                r##"
                    "bibliography": "{}",
                    "style": "ieee",
                    "errors_on_missing_keys": true,
                    "biblio_location": "footnotes",
                    "display_all": false,
                    "citation_preview": false
                "##,
                bib_file.path().display()
            ),
        )
        .replace("CONTENT_PLACEHOLDER", "# Chapter 1\\n@@ctan");
    let input_json = input.as_bytes();

    let (ctx, book) = mdbook_preprocessor::parse_input(input_json).unwrap();
    let book = BibtexPreprocessor.run(&ctx, book)?;

    assert!(
        book.chapters()
            .find(|c| c.name == "Chapter 1")
            .unwrap()
            .content
            .contains("[^ctan]")
    );
    assert!(
        book.chapters()
            .find(|c| c.name == "Chapter 1")
            .unwrap()
            .content
            .contains("[^ctan]:")
    );

    assert_eq!(1, book.chapters().count());

    Ok(())
}

#[test]
fn test_display_all_false() -> anyhow::Result<()> {
    let bib_file = get_bibtex()?;
    let input = JSON_TEMPLATE
        .replace(
            "CONFIG_PLACEHOLDER",
            &format!(
                r##"
            "bibliography": "{}",
            "style": "ieee",
            "errors_on_missing_keys": true,
            "biblio_location": "global",
            "display_all": false,
            "citation_preview": false
            "##,
                bib_file.path().display()
            ),
        )
        .replace("CONTENT_PLACEHOLDER", "# Chapter 1\\n@@ctan");
    let input_json = input.as_bytes();

    let (ctx, book) = mdbook_preprocessor::parse_input(input_json).unwrap();
    let book = BibtexPreprocessor.run(&ctx, book)?;

    assert!(
        !book
            .chapters()
            .find(|c| c.name == "Bibliography")
            .unwrap()
            .content
            .contains("[2]")
    );

    Ok(())
}

#[test]
fn test_preview_citations_on() -> anyhow::Result<()> {
    let bib_file = get_bibtex()?;
    let input = JSON_TEMPLATE
        .replace(
            "CONFIG_PLACEHOLDER",
            &format!(
                r##"
            "bibliography": "{}",
            "style": "ieee",
            "errors_on_missing_keys": true,
            "biblio_location": "global",
            "display_all": true,
            "citation_preview": true
            "##,
                bib_file.path().display()
            ),
        )
        .replace("CONTENT_PLACEHOLDER", "# Chapter 1\\n@@ctan");
    let input_json = input.as_bytes();

    let (ctx, book) = mdbook_preprocessor::parse_input(input_json).unwrap();
    let book = BibtexPreprocessor.run(&ctx, book)?;

    assert!(
        book.chapters()
            .find(|c| c.name == "Chapter 1")
            .unwrap()
            .content
            .contains(">[1]</span>")
    );

    Ok(())
}