From 7eb1f669b9011cef279468c84eff46e14fc2c2a0 Mon Sep 17 00:00:00 2001 From: shyamd <16827130+shyamd@users.noreply.github.com> Date: Sat, 11 Jan 2025 11:12:28 -0800 Subject: [PATCH 01/58] new config class --- src/mkdocs_bibtex/config.py | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 src/mkdocs_bibtex/config.py diff --git a/src/mkdocs_bibtex/config.py b/src/mkdocs_bibtex/config.py new file mode 100644 index 0000000..caa16fb --- /dev/null +++ b/src/mkdocs_bibtex/config.py @@ -0,0 +1,32 @@ +# 3rd party imports +from mkdocs.config import base, config_options + + +class BibTexConfig(base.Config): + """Configuration of the BibTex pluging for mkdocs. + + Options: + bib_file (string): path or url to a single bibtex file for entries, + url example: https://api.zotero.org/*/items?format=bibtex + bib_dir (string): path to a directory of bibtex files for entries + bib_command (string): command to place a bibliography relevant to just that file + defaults to \bibliography + bib_by_default (bool): automatically appends bib_command to markdown pages + by default, defaults to true + full_bib_command (string): command to place a full bibliography of all references + csl_file (string, optional): path or url to a CSL file, relative to mkdocs.yml. + cite_inline (bool): Whether or not to render inline citations, requires CSL, defaults to false + """ + # Input files + bib_file = config_options.Optional(config_options.Type(str)) + bib_dir = config_options.Optional(config_options.Dir(exists=True)) + csl_file = config_options.Optional(config_options.Type(str)) + + # Commands + bib_command = config_options.Type(str, default="\\bibliography") + full_bib_command = config_options.Type(str, default="\\full_bibliography") + + # Settings + bib_by_default = config_options.Type(bool, default=True) + cite_inline = config_options.Type(bool, default=False) + footnote_format = config_options.Type(str, default="{number}") From ecf4a5e13c93f52904d07c92e7c56a9b4567c8fc Mon Sep 17 00:00:00 2001 From: shyamd <16827130+shyamd@users.noreply.github.com> Date: Sat, 11 Jan 2025 11:46:38 -0800 Subject: [PATCH 02/58] Switch to mkdocs config class for type safety Co-authored-by: Jules Bouton <47297745+JPapir@users.noreply.github.com> --- src/mkdocs_bibtex/plugin.py | 73 +++++++++++++------------------------ test_files/test_features.py | 2 +- 2 files changed, 26 insertions(+), 49 deletions(-) diff --git a/src/mkdocs_bibtex/plugin.py b/src/mkdocs_bibtex/plugin.py index b2bfe90..b7ae07b 100644 --- a/src/mkdocs_bibtex/plugin.py +++ b/src/mkdocs_bibtex/plugin.py @@ -8,6 +8,10 @@ from mkdocs.plugins import BasePlugin from pybtex.database import BibliographyData, parse_file +from mkdocs_bibtex.config import BibTexConfig +from mkdocs.exceptions import ConfigurationError + + from mkdocs_bibtex.utils import ( find_cite_blocks, extract_cite_keys, @@ -20,34 +24,11 @@ ) -class BibTexPlugin(BasePlugin): +class BibTexPlugin(BasePlugin[BibTexConfig]): """ Allows the use of bibtex in markdown content for MKDocs. - - Options: - bib_file (string): path or url to a single bibtex file for entries, - url example: https://api.zotero.org/*/items?format=bibtex - bib_dir (string): path to a directory of bibtex files for entries - bib_command (string): command to place a bibliography relevant to just that file - defaults to \bibliography - bib_by_default (bool): automatically appends bib_command to markdown pages - by default, defaults to true - full_bib_command (string): command to place a full bibliography of all references - csl_file (string, optional): path or url to a CSL file, relative to mkdocs.yml. - cite_inline (bool): Whether or not to render inline citations, requires CSL, defaults to false """ - - config_scheme = [ - ("bib_file", config_options.Type(str, required=False)), - ("bib_dir", config_options.Dir(exists=True, required=False)), - ("bib_command", config_options.Type(str, default="\\bibliography")), - ("bib_by_default", config_options.Type(bool, default=True)), - ("full_bib_command", config_options.Type(str, default="\\full_bibliography")), - ("csl_file", config_options.Type(str, default="")), - ("cite_inline", config_options.Type(bool, default=False)), - ("footnote_format", config_options.Type(str, default="{number}")), - ] - + def __init__(self): self.bib_data = None self.all_references = OrderedDict() @@ -66,17 +47,17 @@ def on_config(self, config): bibfiles = [] # Set bib_file from either url or path - if self.config.get("bib_file", None) is not None: - is_url = validators.url(self.config["bib_file"]) + if self.config.bib_file is not None: + is_url = validators.url(self.config.bib_file) # if bib_file is a valid URL, cache it with tempfile if is_url: - bibfiles.append(tempfile_from_url("bib file", self.config["bib_file"], ".bib")) + bibfiles.append(tempfile_from_url("bib file", self.config.bib_file, ".bib")) else: - bibfiles.append(self.config["bib_file"]) - elif self.config.get("bib_dir", None) is not None: - bibfiles.extend(Path(self.config["bib_dir"]).rglob("*.bib")) + bibfiles.append(self.config.bib_file) + elif self.config.bib_dir is not None: + bibfiles.extend(Path(self.config.bib_dir).rglob("*.bib")) else: # pragma: no cover - raise Exception("Must supply a bibtex file or directory for bibtex files") + raise ConfigurationError("Must supply a bibtex file or directory for bibtex files") # load bibliography data refs = {} @@ -99,21 +80,17 @@ def on_config(self, config): self.bib_data_bibtex = self.bib_data.to_string("bibtex") # Set CSL from either url or path (or empty) - is_url = validators.url(self.config["csl_file"]) - if is_url: - self.csl_file = tempfile_from_url("CSL file", self.config["csl_file"], ".csl") + if self.config.csl_file is not None and validators.url(self.config.csl_file): + self.csl_file = tempfile_from_url("CSL file", self.config.csl_file, ".csl") else: - self.csl_file = self.config.get("csl_file", None) + self.csl_file = self.config.csl_file # Toggle whether or not to render citations inline (Requires CSL) - self.cite_inline = self.config.get("cite_inline", False) - if self.cite_inline and not self.csl_file: # pragma: no cover - raise Exception("Must supply a CSL file in order to use cite_inline") - - if "{number}" not in self.config.get("footnote_format"): - raise Exception("Must include `{number}` placeholder in footnote_format") + if self.config.cite_inline and not self.csl_file: # pragma: no cover + raise ConfigurationError("Must supply a CSL file in order to use cite_inline") - self.footnote_format = self.config.get("footnote_format") + if "{number}" not in self.config.footnote_format: + raise ConfigurationError("Must include `{number}` placeholder in footnote_format") self.last_configured = time.time() return config @@ -142,7 +119,7 @@ def on_page_markdown(self, markdown, page, config, files): # 3. Convert cited keys to citation, # or a footnote reference if inline_cite is false. - if self.cite_inline: + if self.config.cite_inline: markdown = insert_citation_keys( citation_quads, markdown, @@ -153,9 +130,9 @@ def on_page_markdown(self, markdown, page, config, files): markdown = insert_citation_keys(citation_quads, markdown) # 4. Insert in the bibliopgrahy text into the markdown - bib_command = self.config.get("bib_command", "\\bibliography") + bib_command = self.config.bib_command - if self.config.get("bib_by_default"): + if self.config.bib_by_default: markdown += f"\n{bib_command}" bibliography = format_bibliography(citation_quads) @@ -166,7 +143,7 @@ def on_page_markdown(self, markdown, page, config, files): ) # 5. Build the full Bibliography and insert into the text - full_bib_command = self.config.get("full_bib_command", "\\full_bibliography") + full_bib_command = self.config.full_bib_command markdown = re.sub( re.escape(full_bib_command), @@ -186,7 +163,7 @@ def format_footnote_key(self, number): Returns: formatted footnote """ - return self.footnote_format.format(number=number) + return self.config.footnote_format.format(number=number) def format_citations(self, cite_keys): """ diff --git a/test_files/test_features.py b/test_files/test_features.py index a3278a3..2072f8b 100644 --- a/test_files/test_features.py +++ b/test_files/test_features.py @@ -338,7 +338,7 @@ def test_multi_reference(plugin_advanced_pandoc): def test_custom_footnote_formatting(plugin): assert plugin.format_footnote_key(1) == "1" - plugin.footnote_format = "Test Format {number}" + plugin.config.footnote_format = "Test Format {number}" assert plugin.format_footnote_key(1) == "Test Format 1" plugin.csl_file = os.path.join(test_files_dir, "nature.csl") From 3eef662a947b5ae2a63d5942173a260f18a402ba Mon Sep 17 00:00:00 2001 From: shyamd <16827130+shyamd@users.noreply.github.com> Date: Sat, 11 Jan 2025 12:42:37 -0800 Subject: [PATCH 03/58] fix linting issues --- src/mkdocs_bibtex/plugin.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/mkdocs_bibtex/plugin.py b/src/mkdocs_bibtex/plugin.py index b7ae07b..4ad6b60 100644 --- a/src/mkdocs_bibtex/plugin.py +++ b/src/mkdocs_bibtex/plugin.py @@ -4,7 +4,6 @@ from collections import OrderedDict from pathlib import Path -from mkdocs.config import config_options from mkdocs.plugins import BasePlugin from pybtex.database import BibliographyData, parse_file @@ -28,7 +27,7 @@ class BibTexPlugin(BasePlugin[BibTexConfig]): """ Allows the use of bibtex in markdown content for MKDocs. """ - + def __init__(self): self.bib_data = None self.all_references = OrderedDict() From 9c113b2926d0a924239280fb9f0b5ef1ddec1c63 Mon Sep 17 00:00:00 2001 From: shyamd <16827130+shyamd@users.noreply.github.com> Date: Sat, 11 Jan 2025 14:35:43 -0800 Subject: [PATCH 04/58] clean up unused attributes --- src/mkdocs_bibtex/plugin.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/mkdocs_bibtex/plugin.py b/src/mkdocs_bibtex/plugin.py index 4ad6b60..e5e6cda 100644 --- a/src/mkdocs_bibtex/plugin.py +++ b/src/mkdocs_bibtex/plugin.py @@ -31,8 +31,6 @@ class BibTexPlugin(BasePlugin[BibTexConfig]): def __init__(self): self.bib_data = None self.all_references = OrderedDict() - self.unescape_for_arithmatex = False - self.configured = False def on_startup(self, *, command, dirty): """ Having on_startup() tells mkdocs to keep the plugin object upon rebuilds""" From ca6f44911983f1436b35153101b3df2d3b3f5e18 Mon Sep 17 00:00:00 2001 From: shyamd <16827130+shyamd@users.noreply.github.com> Date: Sat, 11 Jan 2025 14:42:26 -0800 Subject: [PATCH 05/58] dont introspect to deteremine if configured --- src/mkdocs_bibtex/plugin.py | 13 +++++++------ test_files/test_features.py | 2 +- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/mkdocs_bibtex/plugin.py b/src/mkdocs_bibtex/plugin.py index e5e6cda..d54bbee 100644 --- a/src/mkdocs_bibtex/plugin.py +++ b/src/mkdocs_bibtex/plugin.py @@ -31,6 +31,7 @@ class BibTexPlugin(BasePlugin[BibTexConfig]): def __init__(self): self.bib_data = None self.all_references = OrderedDict() + self.last_configured = None def on_startup(self, *, command, dirty): """ Having on_startup() tells mkdocs to keep the plugin object upon rebuilds""" @@ -56,6 +57,12 @@ def on_config(self, config): else: # pragma: no cover raise ConfigurationError("Must supply a bibtex file or directory for bibtex files") + # Skip rebuilding bib data if all files are older than the initial config + if self.last_configured is not None: + if all(Path(bibfile).stat().st_mtime < self.last_configured for bibfile in bibfiles): + log.info("BibTexPlugin: No changes in bibfiles.") + return config + # load bibliography data refs = {} log.info(f"Loading data from bib files: {bibfiles}") @@ -64,12 +71,6 @@ def on_config(self, config): bibdata = parse_file(bibfile) refs.update(bibdata.entries) - if hasattr(self,"last_configured"): - # Skip rebuilding bib data if all files are older than the initial config - if all(Path(bibfile).stat().st_mtime < self.last_configured for bibfile in bibfiles): - log.info("BibTexPlugin: No changes in bibfiles.") - return config - # Clear references on reconfig self.all_references = OrderedDict() diff --git a/test_files/test_features.py b/test_files/test_features.py index 2072f8b..1b36de3 100644 --- a/test_files/test_features.py +++ b/test_files/test_features.py @@ -53,7 +53,7 @@ def plugin_advanced_pandoc(plugin): ) plugin.config["cite_inline"] = True - delattr(plugin,"last_configured") + plugin.last_configured = None plugin.on_config(plugin.config) return plugin From 6e3a06e59496fd5c212fea66606782f268fd46ce Mon Sep 17 00:00:00 2001 From: shyamd <16827130+shyamd@users.noreply.github.com> Date: Sat, 11 Jan 2025 17:10:46 -0800 Subject: [PATCH 06/58] add docstring --- src/mkdocs_bibtex/config.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/mkdocs_bibtex/config.py b/src/mkdocs_bibtex/config.py index caa16fb..70c05d5 100644 --- a/src/mkdocs_bibtex/config.py +++ b/src/mkdocs_bibtex/config.py @@ -16,6 +16,7 @@ class BibTexConfig(base.Config): full_bib_command (string): command to place a full bibliography of all references csl_file (string, optional): path or url to a CSL file, relative to mkdocs.yml. cite_inline (bool): Whether or not to render inline citations, requires CSL, defaults to false + footnote_format (string): format for the footnote number, defaults to "{number}" """ # Input files bib_file = config_options.Optional(config_options.Type(str)) From 4939a59e91ce0eadcf5c43cb3cb2a4d9b881bc4d Mon Sep 17 00:00:00 2001 From: shyamd <16827130+shyamd@users.noreply.github.com> Date: Sat, 11 Jan 2025 21:22:02 -0800 Subject: [PATCH 07/58] breakout citation extraction into classes --- src/mkdocs_bibtex/citation.py | 56 ++++++++++++++++ test_files/test_citation.py | 121 ++++++++++++++++++++++++++++++++++ 2 files changed, 177 insertions(+) create mode 100644 src/mkdocs_bibtex/citation.py create mode 100644 test_files/test_citation.py diff --git a/src/mkdocs_bibtex/citation.py b/src/mkdocs_bibtex/citation.py new file mode 100644 index 0000000..851a5cd --- /dev/null +++ b/src/mkdocs_bibtex/citation.py @@ -0,0 +1,56 @@ +from dataclasses import dataclass +from typing import List +import re + + +CITATION_REGEX = re.compile(r"(?:(?P[^@;]*?)\s*)?@(?P[\w-]+)(?:,\s*(?P[^;]+))?") +CITATION_BLOCK_REGEX = re.compile(r"\[(.*?)\]") +EMAIL_REGEX = re.compile(r"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}") + + +@dataclass +class Citation: + """Represents a citation in raw markdown without formatting""" + + prefix: str + suffix: str + key: str + + @classmethod + def from_markdown(cls, markdown: str) -> List["Citation"]: + """Extracts citations from a markdown string""" + citations = [] + + pos_citations = markdown.split(";") + pos_citations = [citation for citation in pos_citations if EMAIL_REGEX.match(citation) is None] + + for citation in pos_citations: + match = CITATION_REGEX.match(citation) + + if match: + result = {group: (match.group(group) or "") for group in ["prefix", "key", "suffix"]} + citations.append(Citation(prefix=result["prefix"], key=result["key"], suffix=result["suffix"])) + return citations + + +@dataclass +class CitationBlock: + citations: List[Citation] + + @classmethod + def from_markdown(cls, markdown: str) -> List["CitationBlock"]: + """Extracts citation blocks from a markdown string""" + """ + Given a markdown string + 1. Find all cite blocks by looking for square brackets + 2. For each cite block, try to extract the citations + - if this errors there are no citations in this block and we move on + - if this succeeds we have a list of citations + """ + citation_blocks = [] + for match in CITATION_BLOCK_REGEX.finditer(markdown): + try: + citation_blocks.append(CitationBlock(citations=Citation.from_markdown(match.group(1)))) + except Exception as e: + print(f"Error extracting citations from block: {e}") + return citation_blocks diff --git a/test_files/test_citation.py b/test_files/test_citation.py new file mode 100644 index 0000000..761e354 --- /dev/null +++ b/test_files/test_citation.py @@ -0,0 +1,121 @@ +""" +This test file tests the citation module and ensures it is compatible with +pybtex basic citations and pandoc citation formattting +""" + +import pytest +from mkdocs_bibtex.citation import Citation, CitationBlock + + +def test_basic_citation(): + """Test basic citation extraction""" + citations = Citation.from_markdown("@test") + assert len(citations) == 1 + assert citations[0].key == "test" + assert citations[0].prefix == "" + assert citations[0].suffix == "" + + +def test_citation_with_prefix(): + """Test citation with prefix""" + citations = Citation.from_markdown("see @test") + assert len(citations) == 1 + assert citations[0].key == "test" + assert citations[0].prefix == "see" + assert citations[0].suffix == "" + + +def test_citation_with_suffix(): + """Test citation with suffix""" + citations = Citation.from_markdown("@test, p. 123") + assert len(citations) == 1 + assert citations[0].key == "test" + assert citations[0].prefix == "" + assert citations[0].suffix == "p. 123" + + +def test_citation_with_prefix_and_suffix(): + """Test citation with both prefix and suffix""" + citations = Citation.from_markdown("see @test, p. 123") + assert len(citations) == 1 + assert citations[0].key == "test" + assert citations[0].prefix == "see" + assert citations[0].suffix == "p. 123" + + +def test_suppressed_author(): + """Test suppressed author citation""" + citations = Citation.from_markdown("-@test") + assert len(citations) == 1 + assert citations[0].key == "test" + assert citations[0].prefix == "-" + assert citations[0].suffix == "" + + +def test_multiple_citations(): + """Test multiple citations separated by semicolon""" + citations = Citation.from_markdown("@test; @test2") + assert len(citations) == 2 + assert citations[0].key == "test" + assert citations[1].key == "test2" + + +def test_complex_multiple_citations(): + """Test multiple citations with prefixes and suffixes""" + citations = Citation.from_markdown("see @test, p. 123; @test2, p. 456") + assert len(citations) == 2 + assert citations[0].key == "test" + assert citations[0].prefix == "see" + assert citations[0].suffix == "p. 123" + assert citations[1].key == "test2" + assert citations[1].prefix == "" + assert citations[1].suffix == "p. 456" + + +def test_citation_block(): + """Test citation block extraction""" + blocks = CitationBlock.from_markdown("[see @test, p. 123]") + assert len(blocks) == 1 + assert len(blocks[0].citations) == 1 + assert blocks[0].citations[0].key == "test" + assert blocks[0].citations[0].prefix == "see" + assert blocks[0].citations[0].suffix == "p. 123" + + +def test_multiple_citation_blocks(): + """Test multiple citation blocks""" + blocks = CitationBlock.from_markdown("[see @test, p. 123] Some text [@test2]") + assert len(blocks) == 2 + assert blocks[0].citations[0].key == "test" + assert blocks[1].citations[0].key == "test2" + + +def test_invalid_citation(): + """Test invalid citation formats""" + citations = Citation.from_markdown("not a citation") + assert len(citations) == 0 + + +def test_email_exclusion(): + """Test that email addresses are not parsed as citations""" + citations = Citation.from_markdown("user@example.com") + assert len(citations) == 0 + + +def test_complex_citation_block(): + """Test complex citation block with multiple citations""" + blocks = CitationBlock.from_markdown("[see @test1, p. 123; @test2, p. 456; -@test3]") + assert len(blocks) == 1 + assert len(blocks[0].citations) == 3 + + assert blocks[0].citations[0].key == "test1" + assert blocks[0].citations[0].prefix == "see" + assert blocks[0].citations[0].suffix == "p. 123" + + assert blocks[0].citations[1].key == "test2" + assert blocks[0].citations[1].prefix == "" + assert blocks[0].citations[1].suffix == "p. 456" + + assert blocks[0].citations[2].key == "test3" + assert blocks[0].citations[2].prefix == " -" + assert blocks[0].citations[2].suffix == "" From 1392239cd7978da54958a031c8f54e5eec11149e Mon Sep 17 00:00:00 2001 From: shyamd <16827130+shyamd@users.noreply.github.com> Date: Mon, 13 Jan 2025 12:02:23 -0800 Subject: [PATCH 08/58] move key to first arg --- src/mkdocs_bibtex/citation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mkdocs_bibtex/citation.py b/src/mkdocs_bibtex/citation.py index 851a5cd..eb84be6 100644 --- a/src/mkdocs_bibtex/citation.py +++ b/src/mkdocs_bibtex/citation.py @@ -12,9 +12,9 @@ class Citation: """Represents a citation in raw markdown without formatting""" + key: str prefix: str suffix: str - key: str @classmethod def from_markdown(cls, markdown: str) -> List["Citation"]: From d03fac5f3968418cc52c901979e4fc17f7a507c7 Mon Sep 17 00:00:00 2001 From: shyamd <16827130+shyamd@users.noreply.github.com> Date: Mon, 13 Jan 2025 12:02:50 -0800 Subject: [PATCH 09/58] abstract out registry concept and implement simple registry --- src/mkdocs_bibtex/registry.py | 70 +++++++++++++++++++++++ test_files/test_simple_registry.py | 91 ++++++++++++++++++++++++++++++ 2 files changed, 161 insertions(+) create mode 100644 src/mkdocs_bibtex/registry.py create mode 100644 test_files/test_simple_registry.py diff --git a/src/mkdocs_bibtex/registry.py b/src/mkdocs_bibtex/registry.py new file mode 100644 index 0000000..7395efc --- /dev/null +++ b/src/mkdocs_bibtex/registry.py @@ -0,0 +1,70 @@ +from abc import ABC, abstractmethod +from mkdocs_bibtex.citation import Citation, CitationBlock +from mkdocs_bibtex.utils import log +from pybtex.database import BibliographyData, parse_file +from pybtex.backends.markdown import Backend as MarkdownBackend +from pybtex.style.formatting.plain import Style as PlainStyle + + +class ReferenceRegistry(ABC): + """ + A registry of references that can be used to format citations + """ + + def __init__(self, bib_files: list[str]): + refs = {} + log.info(f"Loading data from bib files: {bib_files}") + for bibfile in bib_files: + log.debug(f"Parsing bibtex file {bibfile}") + bibdata = parse_file(bibfile) + refs.update(bibdata.entries) + self.bib_data = BibliographyData(entries=refs) + + @abstractmethod + def validate_citation_blocks(self, citation_blocks: list[CitationBlock]) -> None: + """Validates all citation blocks. Throws an error if any citation block is invalid""" + + @abstractmethod + def inline_text(self, citation_block: CitationBlock) -> str: + """Retreives the inline citation text for a citation block""" + + @abstractmethod + def reference_text(self, citation: Citation) -> str: + """Retreives the reference text for a citation""" + + +class SimpleRegistry(ReferenceRegistry): + def __init__(self, bib_files: list[str]): + super().__init__(bib_files) + self.style = PlainStyle() + self.backend = MarkdownBackend() + + def validate_citation_blocks(self, citation_blocks: list[CitationBlock]) -> None: + """Validates all citation blocks. Throws an error if any citation block is invalid""" + for citation_block in citation_blocks: + for citation in citation_block.citations: + if citation.key not in self.bib_data.entries: + pass + # raise ValueError(f"Citation key {citation.key} not found in bibliography") + + for citation_block in citation_blocks: + for citation in citation_block.citations: + if citation.prefix != "" or citation.suffix != "": + pass + # raise ValueError("Simple style does not support any affixes (prefix, suffix, or author suppression)") + + def inline_text(self, citation_block: CitationBlock) -> str: + keys = sorted(set(citation.key for citation in citation_block.citations)) + + return "[" + ",".join(f"^{key}" for key in keys) + "]" + + def reference_text(self, citation: Citation) -> str: + entry = self.bib_data.entries[citation.key] + log.debug(f"Converting bibtex entry {citation.key!r} without pandoc") + formatted_entry = self.style.format_entry("", entry) + entry_text = formatted_entry.text.render(self.backend) + entry_text = entry_text.replace("\n", " ") + # Clean up some common escape sequences + entry_text = entry_text.replace("\\(", "(").replace("\\)", ")").replace("\\.", ".") + log.debug(f"SUCCESS Converting bibtex entry {citation.key!r} without pandoc") + return entry_text diff --git a/test_files/test_simple_registry.py b/test_files/test_simple_registry.py new file mode 100644 index 0000000..a8e5aed --- /dev/null +++ b/test_files/test_simple_registry.py @@ -0,0 +1,91 @@ +import os +import pytest +from mkdocs_bibtex.registry import SimpleRegistry +from mkdocs_bibtex.citation import Citation, CitationBlock + +module_dir = os.path.dirname(os.path.abspath(__file__)) +test_files_dir = os.path.abspath(os.path.join(module_dir, "..", "test_files")) + + +@pytest.fixture +def simple_registry(): + bib_file = os.path.join(test_files_dir, "test.bib") + return SimpleRegistry([bib_file]) + + +def test_simple_registry_initialization(simple_registry): + """Test basic initialization and loading of bib files""" + assert len(simple_registry.bib_data.entries) == 4 + + +def test_validate_citation_blocks_valid(simple_registry): + """Test validation of valid citation blocks""" + # Single citation + citations = [Citation("test", "", "")] + block = CitationBlock(citations) + simple_registry.validate_citation_blocks([block]) + + # Multiple citations + citations = [Citation("test", "", ""), Citation("test2", "", "")] + block = CitationBlock(citations) + simple_registry.validate_citation_blocks([block]) + + +@pytest.mark.xfail(reason="Old logic didn't fail this way but maybe it should") +def test_validate_citation_blocks_invalid_key(simple_registry): + """Test validation fails with invalid citation key""" + citations = [Citation("nonexistent", "", "")] + block = CitationBlock(citations) + with pytest.raises(ValueError, match="Citation key nonexistent not found in bibliography"): + simple_registry.validate_citation_blocks([block]) + + +@pytest.mark.xfail(reason="Old logic didn't fail this way but maybe it should") +def test_validate_citation_blocks_invalid_affixes(simple_registry): + """Test validation fails with affixes (not supported in simple mode)""" + # Test prefix + citations = [Citation("test", "see", "")] + block = CitationBlock(citations) + with pytest.raises(ValueError, match="Simple style does not support any affixes"): + simple_registry.validate_citation_blocks([block]) + + # Test suffix + citations = [Citation("test", "", "p. 123")] + block = CitationBlock(citations) + with pytest.raises(ValueError, match="Simple style does not support any affixes"): + simple_registry.validate_citation_blocks([block]) + + +def test_inline_text(simple_registry): + """Test inline citation text generation""" + # Single citation + citations = [Citation("test", "", "")] + block = CitationBlock(citations) + assert simple_registry.inline_text(block) == "[^test]" + + # Multiple citations + citations = [Citation("test", "", ""), Citation("test2", "", "")] + block = CitationBlock(citations) + assert simple_registry.inline_text(block) == "[^test,^test2]" + + +def test_reference_text(simple_registry): + """Test reference text generation""" + # Test basic citation + citation = Citation("test", "", "") + assert ( + simple_registry.reference_text(citation) + == "First Author and Second Author. Test title. *Testing Journal*, 2019." + ) + + # Test citation with title in braces + citation = Citation("test2", "", "") + assert ( + simple_registry.reference_text(citation) + == "First Author and Second Author. Test Title (TT). *Testing Journal (TJ)*, 2019." + ) + + # Test citation with URL + citation = Citation("test_citavi", "", "") + expected = "First Author and Second Author. Test Title (TT). *Testing Journal (TJ)*, 2019. URL: [\\\\url\\{https://doi.org/10.21577/0103\\-5053.20190253\\}](\\url{https://doi.org/10.21577/0103-5053.20190253})." + assert simple_registry.reference_text(citation) == expected From a9cdbb7d291eea39fc83edc0e1e7e3dd47bfe4b4 Mon Sep 17 00:00:00 2001 From: shyamd <16827130+shyamd@users.noreply.github.com> Date: Mon, 13 Jan 2025 19:33:57 -0800 Subject: [PATCH 10/58] lint --- src/mkdocs_bibtex/registry.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/mkdocs_bibtex/registry.py b/src/mkdocs_bibtex/registry.py index 7395efc..510b131 100644 --- a/src/mkdocs_bibtex/registry.py +++ b/src/mkdocs_bibtex/registry.py @@ -44,14 +44,14 @@ def validate_citation_blocks(self, citation_blocks: list[CitationBlock]) -> None for citation_block in citation_blocks: for citation in citation_block.citations: if citation.key not in self.bib_data.entries: + # TODO: Should this be a warning or fatal error? pass - # raise ValueError(f"Citation key {citation.key} not found in bibliography") for citation_block in citation_blocks: for citation in citation_block.citations: if citation.prefix != "" or citation.suffix != "": + # TODO: Should this be a warning or fatal error? pass - # raise ValueError("Simple style does not support any affixes (prefix, suffix, or author suppression)") def inline_text(self, citation_block: CitationBlock) -> str: keys = sorted(set(citation.key for citation in citation_block.citations)) From 13829d9a8e33b771122b69fe5e77496504b5e7c7 Mon Sep 17 00:00:00 2001 From: shyamd <16827130+shyamd@users.noreply.github.com> Date: Mon, 13 Jan 2025 19:34:05 -0800 Subject: [PATCH 11/58] add integration tests --- test_files/test_integration.py | 129 +++++++++++++++++++++++++++++++++ 1 file changed, 129 insertions(+) create mode 100644 test_files/test_integration.py diff --git a/test_files/test_integration.py b/test_files/test_integration.py new file mode 100644 index 0000000..7f9614d --- /dev/null +++ b/test_files/test_integration.py @@ -0,0 +1,129 @@ +""" +Integration tests for mkdocs-bibtex plugin. These tests verify the complete functionality +of the plugin rather than testing individual components. +""" + +import os +import pytest +import pypandoc +from mkdocs_bibtex.plugin import BibTexPlugin + +module_dir = os.path.dirname(os.path.abspath(__file__)) +test_files_dir = os.path.abspath(os.path.join(module_dir, "..", "test_files")) + + +@pytest.fixture +def plugin(): + """Basic BibTex Plugin without CSL""" + plugin = BibTexPlugin() + plugin.load_config( + options={"bib_file": os.path.join(test_files_dir, "test.bib"), "bib_by_default": False}, + config_file_path=test_files_dir, + ) + plugin.on_config(plugin.config) + + return plugin + + +@pytest.fixture +def pandoc_plugin(plugin): + """BibTex Plugin with Pandoc and CSL support""" + # Skip if Pandoc version is too old + pandoc_version = pypandoc.get_pandoc_version() + if tuple(int(v) for v in pandoc_version.split(".")) <= (2, 11): + pytest.skip(f"Unsupported pandoc version (v{pandoc_version})") + + plugin = BibTexPlugin() + plugin.load_config( + options={ + "bib_file": os.path.join(test_files_dir, "test.bib"), + "csl_file": os.path.join(test_files_dir, "springer-basic-author-date.csl"), + "cite_inline": True, + "bib_by_default": False, + }, + config_file_path=test_files_dir, + ) + plugin.on_config(plugin.config) + # plugin.csl_file = None + + return plugin + + +def test_basic_citation_rendering(plugin): + """Test basic citation functionality without CSL""" + markdown = "Here is a citation [@test] and another one [@test2].\n\n\\bibliography" + result = plugin.on_page_markdown(markdown, None, None, None) + + # Check citation replacements + assert "[^1]" in result + assert "[^2]" in result + + # Check bibliography entries + assert "First Author and Second Author. Test title. *Testing Journal*, 2019." in result + assert "First Author and Second Author. Test Title (TT). *Testing Journal (TJ)*, 2019." in result + + +def test_pandoc_citation_rendering(pandoc_plugin): + """Test citation rendering with Pandoc and CSL""" + markdown = "Here is a citation [@test] and another [@Bivort2016].\n\n\\bibliography" + result = pandoc_plugin.on_page_markdown(markdown, None, None, None) + + # Check inline citations + assert "(Author and Author 2019)" in result + assert "(De Bivort and Van Swinderen 2016)" in result + + # Check bibliography formatting + assert "Author F, Author S (2019)" in result + assert "De Bivort BL, Van Swinderen B (2016)" in result + + +def test_citation_features(pandoc_plugin): + """Test various citation features like prefixes, suffixes, and author suppression""" + markdown = """ + See [-@test] for more. + As shown by [see @test, p. 123]. + Multiple sources [@test; @test2]. + + \\bibliography" + """ + result = pandoc_plugin.on_page_markdown(markdown, None, None, None) + + print(result) + + # Check various citation formats + assert "(2019)" in result # Suppressed author + assert "(see Author and Author 2019, p. 123)" in result # Prefix and suffix + assert "Author and Author 2019; Author and Author 2019" in result # Multiple citations + + +def test_bibliography_controls(plugin): + """Test bibliography inclusion behavior""" + # Test with explicit bibliography command + markdown = "Citation [@test]\n\n\\bibliography" + result = plugin.on_page_markdown(markdown, None, None, None) + assert "[^1]:" in result + + # Test without bibliography command when bib_by_default is False + markdown = "Citation [@test]" + result = plugin.on_page_markdown(markdown, None, None, None) + assert "[^1]:" not in result + + # Test without bibliography command when bib_by_default is True + plugin.config.bib_by_default = True + result = plugin.on_page_markdown(markdown, None, None, None) + assert "[^1]:" in result + + +def test_custom_footnote_format(plugin): + """Test custom footnote formatting""" + plugin.config["footnote_format"] = "ref{number}" + markdown = "Citation [@test]\n\n\\bibliography" + result = plugin.on_page_markdown(markdown, None, None, None) + assert "[^ref1]" in result + + +def test_invalid_citations(plugin): + """Test handling of invalid citations""" + markdown = "Invalid citation [@nonexistent]\n\n\\bibliography" + result = plugin.on_page_markdown(markdown, None, None, None) + assert "[@nonexistent]" in result # Invalid citation should remain unchanged From fdc73fc3611aec24e6d36d765cf2b1f125e23351 Mon Sep 17 00:00:00 2001 From: shyamd <16827130+shyamd@users.noreply.github.com> Date: Mon, 13 Jan 2025 19:39:02 -0800 Subject: [PATCH 12/58] fix assert for multiple authors --- test_files/test_integration.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test_files/test_integration.py b/test_files/test_integration.py index 7f9614d..497f65a 100644 --- a/test_files/test_integration.py +++ b/test_files/test_integration.py @@ -93,7 +93,7 @@ def test_citation_features(pandoc_plugin): # Check various citation formats assert "(2019)" in result # Suppressed author assert "(see Author and Author 2019, p. 123)" in result # Prefix and suffix - assert "Author and Author 2019; Author and Author 2019" in result # Multiple citations + assert "Author and Author 2019a, b" in result # Multiple citations def test_bibliography_controls(plugin): From 566f1f8d29c490ba62f06de8bf61114d79a6a947 Mon Sep 17 00:00:00 2001 From: shyamd <16827130+shyamd@users.noreply.github.com> Date: Mon, 13 Jan 2025 20:13:10 -0800 Subject: [PATCH 13/58] complete test coverage --- test_files/test_integration.py | 21 ++++++++++++++++++--- test_files/test_simple_registry.py | 9 ++++++++- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/test_files/test_integration.py b/test_files/test_integration.py index 497f65a..89c6c31 100644 --- a/test_files/test_integration.py +++ b/test_files/test_integration.py @@ -88,13 +88,19 @@ def test_citation_features(pandoc_plugin): """ result = pandoc_plugin.on_page_markdown(markdown, None, None, None) - print(result) - # Check various citation formats assert "(2019)" in result # Suppressed author assert "(see Author and Author 2019, p. 123)" in result # Prefix and suffix assert "Author and Author 2019a, b" in result # Multiple citations + # Check bibliography formatting + assert "Author F, Author S (2019) Test title. Testing Journal 1" in result + assert "Author F, Author S (2019) Test Title (TT). Testing Journal (TJ) 1" in result + + # Check that the bibliography entries are only shown once + assert result.count("Author F, Author S (2019) Test title. Testing Journal 1") == 1 + assert result.count("Author F, Author S (2019) Test Title (TT). Testing Journal (TJ) 1") == 1 + def test_bibliography_controls(plugin): """Test bibliography inclusion behavior""" @@ -116,11 +122,20 @@ def test_bibliography_controls(plugin): def test_custom_footnote_format(plugin): """Test custom footnote formatting""" - plugin.config["footnote_format"] = "ref{number}" + plugin.config.footnote_format = "ref{number}" markdown = "Citation [@test]\n\n\\bibliography" result = plugin.on_page_markdown(markdown, None, None, None) assert "[^ref1]" in result + # Test that an invalid footnote format raises an exception + bad_plugin = BibTexPlugin() + bad_plugin.load_config( + options={"footnote_format": ""}, + config_file_path=test_files_dir, + ) + with pytest.raises(Exception): + bad_plugin.on_config(bad_plugin.config) + def test_invalid_citations(plugin): """Test handling of invalid citations""" diff --git a/test_files/test_simple_registry.py b/test_files/test_simple_registry.py index a8e5aed..260989c 100644 --- a/test_files/test_simple_registry.py +++ b/test_files/test_simple_registry.py @@ -78,13 +78,20 @@ def test_reference_text(simple_registry): == "First Author and Second Author. Test title. *Testing Journal*, 2019." ) - # Test citation with title in braces + # Test another basic citation citation = Citation("test2", "", "") assert ( simple_registry.reference_text(citation) == "First Author and Second Author. Test Title (TT). *Testing Journal (TJ)*, 2019." ) + # test long citation + citation = Citation("Bivort2016", "", "") + assert ( + simple_registry.reference_text(citation) + == "Benjamin L. De Bivort and Bruno Van Swinderen. Evidence for selective attention in the insect brain. *Current Opinion in Insect Science*, 15:1–7, 2016. [doi:10.1016/j.cois.2016.02.007](https://doi.org/10.1016/j.cois.2016.02.007)." + ) + # Test citation with URL citation = Citation("test_citavi", "", "") expected = "First Author and Second Author. Test Title (TT). *Testing Journal (TJ)*, 2019. URL: [\\\\url\\{https://doi.org/10.21577/0103\\-5053.20190253\\}](\\url{https://doi.org/10.21577/0103-5053.20190253})." From 5187f80c7535c195c681fe8ad657d9f91f10a1cf Mon Sep 17 00:00:00 2001 From: shyamd <16827130+shyamd@users.noreply.github.com> Date: Mon, 13 Jan 2025 20:13:18 -0800 Subject: [PATCH 14/58] remove stale tests --- test_files/test_features.py | 350 ------------------------------------ test_files/test_plugin.py | 60 +------ 2 files changed, 1 insertion(+), 409 deletions(-) delete mode 100644 test_files/test_features.py diff --git a/test_files/test_features.py b/test_files/test_features.py deleted file mode 100644 index 1b36de3..0000000 --- a/test_files/test_features.py +++ /dev/null @@ -1,350 +0,0 @@ -""" -This test file checks to make sure each feature works rather than checking each -function. Each feature should have a single test function that covers all the python -functions it that would need to be tested -""" -import os - -import pytest -import pypandoc - -from mkdocs_bibtex.plugin import BibTexPlugin - -from mkdocs_bibtex.utils import ( - find_cite_blocks, - format_bibliography, - insert_citation_keys, -) - -module_dir = os.path.dirname(os.path.abspath(__file__)) -test_files_dir = os.path.abspath(os.path.join(module_dir, "..", "test_files")) - - -@pytest.fixture -def plugin(): - """ - Basic BibTex Plugin without CSL - """ - plugin = BibTexPlugin() - plugin.load_config( - options={"bib_file": os.path.join(test_files_dir, "test.bib")}, - config_file_path=test_files_dir, - ) - plugin.on_config(plugin.config) - plugin.csl_file = None - return plugin - - - -@pytest.fixture -def plugin_advanced_pandoc(plugin): - """ - Enables advanced features via pandoc - """ - # Only valid for Pandoc > 2.11 - pandoc_version = pypandoc.get_pandoc_version() - pandoc_version_tuple = tuple(int(ver) for ver in pandoc_version.split(".")) - if pandoc_version_tuple <= (2, 11): - pytest.skip(f"Unsupported version of pandoc (v{pandoc_version}) installed.") - - plugin.config["bib_file"] = os.path.join(test_files_dir, "test.bib") - plugin.config["csl_file"] = os.path.join( - test_files_dir, "springer-basic-author-date.csl" - ) - plugin.config["cite_inline"] = True - - plugin.last_configured = None - plugin.on_config(plugin.config) - - return plugin - - -def test_basic_citations(plugin): - """ - Tests super basic citations using the built-in citation style - """ - assert find_cite_blocks("[@test]") == ["[@test]"] - - assert ( - insert_citation_keys( - [ - ( - "[@test]", - "@test", - "1", - "First Author and Second Author", - ) - ], - "[@test]", - ) - == "[^1]" - ) - - ### TODO: test format_bibliography - - assert ( - "[@test]", - "test", - "1", - "First Author and Second Author. Test title. *Testing Journal*, 2019.", - ) == plugin.format_citations(["[@test]"])[0] - - assert ( - "[@test2]", - "test2", - "1", - "First Author and Second Author. Test Title (TT). *Testing Journal (TJ)*, 2019.", - ) == plugin.format_citations(["[@test2]"])[0] - - # test long citation - assert ( - "[@Bivort2016]", - "Bivort2016", - "1", - "Benjamin L. De Bivort and Bruno Van Swinderen. Evidence for selective attention in the insect brain. *Current Opinion in Insect Science*, 15:1–7, 2016. [doi:10.1016/j.cois.2016.02.007](https://doi.org/10.1016/j.cois.2016.02.007).", # noqa: E501 - ) == plugin.format_citations(["[@Bivort2016]"])[0] - - # Test \url embedding - assert ( - "[@test_citavi]", - "test_citavi", - "1", - "First Author and Second Author. Test Title (TT). *Testing Journal (TJ)*, 2019. URL: [\\\\url\\{https://doi.org/10.21577/0103\\-5053.20190253\\}](\\url{https://doi.org/10.21577/0103-5053.20190253}).", # noqa: E501 - ) == plugin.format_citations(["[@test_citavi]"])[0] - - -def test_compound_citations(plugin): - """ - Compound citations are citations that include multiple cite keys - """ - assert find_cite_blocks("[@test; @test2]") == ["[@test; @test2]"] - assert find_cite_blocks("[@test]\n [@test; @test2]") == [ - "[@test]", - "[@test; @test2]", - ] - - assert ( - insert_citation_keys( - [ - ( - "[@test; @test2]", - "@test", - "1", - "First Author and Second Author", - ), - ( - "[@test; @test2]", - "@test2", - "2", - "First Author and Second Author. Test Title (TT). *Testing Journal (TJ)*, 2019", # noqa: E501 - ), - ], - "[@test; @test2]", - ) - == "[^1][^2]" - ) - - quads = [ - ( - "[@test; @test2]", - "@test", - "1", - "First Author and Second Author", - ), - ( - "[@test; @test2]", - "@test2", - "2", - "First Author and Second Author. Test Title (TT). *Testing Journal (TJ)*, 2019", - ), - ] - - bib = format_bibliography(quads) - - assert "[^1]: First Author and Second Author" in bib - assert ( - "[^2]: First Author and Second Author. Test Title (TT). *Testing Journal (TJ)*, 2019" - in bib - ) - - assert [ - ( - "[@test; @test2]", - "test", - "1", - "First Author and Second Author. Test title. *Testing Journal*, 2019.", - ), - ( - "[@test; @test2]", - "test2", - "2", - "First Author and Second Author. Test Title (TT). *Testing Journal (TJ)*, 2019.", - ), - ] == plugin.format_citations(["[@test; @test2]"]) - - -############### -# PANDOC ONLY # -############### - - -def test_basic_pandoc(plugin): - plugin.csl_file = os.path.join(test_files_dir, "nature.csl") - assert ( - "[@test]", - "test", - "1", - "Author, F. & Author, S. Test title. *Testing Journal* **1**, (2019).", - ) == plugin.format_citations(["[@test]"])[0] - - assert ( - "[@Bivort2016]", - "Bivort2016", - "1", - "De Bivort, B. L. & Van Swinderen, B. Evidence for selective attention in the insect brain. *Current Opinion in Insect Science* **15**, 1–7 (2016).", # noqa: E501 - ) == plugin.format_citations(["[@Bivort2016]"])[0] - - # Test a CSL that outputs references in a different style - plugin.csl_file = os.path.join(test_files_dir, "springer-basic-author-date.csl") - assert ( - "[@test]", - "test", - "1", - "Author, F. & Author, S. Test title. *Testing Journal* **1**, (2019).", - ) == plugin.format_citations(["[@test]"])[0] - - assert ( - "[@test_citavi]", - "test_citavi", - "1", - "Author F, Author S (2019) Test Title (TT). Testing Journal (TJ) 1:", - ) == plugin.format_citations(["[@test_citavi]"])[0] - - -def test_inline_ciations(plugin_advanced_pandoc): - plugin = plugin_advanced_pandoc - - # Ensure inline citation works - quads = [("[@test]", None, "1", None)] - test_markdown = "Hello[@test]" - result = "Hello (Author and Author 2019)[^1]" - assert result == insert_citation_keys( - quads, test_markdown, plugin.csl_file, plugin.bib_data.to_string("bibtex") - ) - - -def test_supressed_authors(plugin_advanced_pandoc): - plugin = plugin_advanced_pandoc - - # Ensure suppressed authors works - quads = [("[-@test]", None, "1", None)] - test_markdown = "Suppressed [-@test]" - result = "Suppressed (2019)[^1]" - assert result == insert_citation_keys( - quads, test_markdown, plugin.csl_file, plugin.bib_data.to_string("bibtex") - ) - - -def test_affixes(plugin_advanced_pandoc): - plugin = plugin_advanced_pandoc - # Ensure affixes work - quads = [("[see @test]", None, "1", None)] - test_markdown = "Hello[see @test]" - result = "Hello (see Author and Author 2019)[^1]" - assert result == insert_citation_keys( - quads, test_markdown, plugin.csl_file, plugin.bib_data.to_string("bibtex") - ) - - quads = [("[@test, p. 123]", None, "1", None)] - test_markdown = "[@test, p. 123]" - result = " (Author and Author 2019, p. 123)[^1]" - assert result == insert_citation_keys( - quads, test_markdown, plugin.csl_file, plugin.bib_data.to_string("bibtex") - ) - - # Combined - quads = [("[see @test, p. 123]", None, "1", None)] - test_markdown = "Hello[see @test, p. 123]" - result = "Hello (see Author and Author 2019, p. 123)[^1]" - assert result == insert_citation_keys( - quads, test_markdown, plugin.csl_file, plugin.bib_data.to_string("bibtex") - ) - - # Combined, suppressed author - quads = [("[see -@test, p. 123]", None, "1", None)] - test_markdown = "Suppressed [see -@test, p. 123]" - result = "Suppressed (see 2019, p. 123)[^1]" - assert result == insert_citation_keys( - quads, test_markdown, plugin.csl_file, plugin.bib_data.to_string("bibtex") - ) - - -def test_invalid_blocks(plugin_advanced_pandoc): - pass - - -def test_citavi_format(plugin_advanced_pandoc): - pass - - -def test_duplicate_reference(plugin_advanced_pandoc): - """ - Ensures duplicats references show up appropriately - # TODO: These test cases don't seem right - """ - plugin = plugin_advanced_pandoc - # Ensure multi references work - quads = [ - ("[@test; @Bivort2016]", None, "1", None), - ("[@test; @Bivort2016]", None, "2", None), - ] - test_markdown = "[@test; @Bivort2016]" - # CSL defines the order, this ordering is therefore expected with springer.csl - result = " (De Bivort and Van Swinderen 2016; Author and Author 2019)[^1][^2]" - assert result == insert_citation_keys( - quads, test_markdown, plugin.csl_file, plugin.bib_data.to_string("bibtex") - ) - - quads = [ - ("[@test, p. 12; @Bivort2016, p. 15]", None, "1", None), - ("[@test, p. 12; @Bivort2016, p. 15]", None, "2", None), - ] - test_markdown = "[@test, p. 12; @Bivort2016, p. 15]" - # CSL defines the order, this ordering is therefore expected with springer.csl - result = " (De Bivort and Van Swinderen 2016, p. 15; Author and Author 2019, p. 12)[^1][^2]" - assert result == insert_citation_keys( - quads, test_markdown, plugin.csl_file, plugin.bib_data.to_string("bibtex") - ) - - -def test_multi_reference(plugin_advanced_pandoc): - """ - Ensures multiple inline references show up appropriately - """ - - plugin = plugin_advanced_pandoc - # Ensure multiple inline references works - quads = [ - ("[@test]", None, "1", None), - ("[see @Bivort2016, p. 123]", None, "2", None), - ] - test_markdown = "Hello[@test] World [see @Bivort2016, p. 123]" - result = "Hello (Author and Author 2019)[^1] World (see De Bivort and Van Swinderen 2016, p. 123)[^2]" - assert result == insert_citation_keys( - quads, test_markdown, plugin.csl_file, plugin.bib_data.to_string("bibtex") - ) - - -def test_custom_footnote_formatting(plugin): - - assert plugin.format_footnote_key(1) == "1" - plugin.config.footnote_format = "Test Format {number}" - assert plugin.format_footnote_key(1) == "Test Format 1" - - plugin.csl_file = os.path.join(test_files_dir, "nature.csl") - assert ( - "[@test]", - "test", - "Test Format 1", - "Author, F. & Author, S. Test title. *Testing Journal* **1**, (2019).", - ) == plugin.format_citations(["[@test]"])[0] diff --git a/test_files/test_plugin.py b/test_files/test_plugin.py index 682c1b5..cdbc06a 100644 --- a/test_files/test_plugin.py +++ b/test_files/test_plugin.py @@ -27,9 +27,7 @@ def test_bibtex_loading_bibfile(plugin): def test_bibtex_loading_bib_url(): plugin = BibTexPlugin() plugin.load_config( - options={ - "bib_file": "https://raw.githubusercontent.com/shyamd/mkdocs-bibtex/main/test_files/test.bib" - }, + options={"bib_file": "https://raw.githubusercontent.com/shyamd/mkdocs-bibtex/main/test_files/test.bib"}, config_file_path=test_files_dir, ) @@ -48,61 +46,5 @@ def test_bibtex_loading_bibdir(): assert len(plugin.bib_data.entries) == 2 -def test_on_page_markdown(plugin): - """ - This function just tests to make sure the rendered markdown changees with - options and basic functionality works. It doesn't test "features" - """ - # run test with bib_by_default set to False - plugin.config["bib_by_default"] = False - test_markdown = "This is a citation. [@test]\n\n \\bibliography" - assert ( - "[^1]: First Author and Second Author. Test title. *Testing Journal*, 2019." - in plugin.on_page_markdown(test_markdown, None, None, None) - ) - - # ensure there are two items in bibliography - test_markdown = "This is a citation. [@test2] This is another citation [@test]\n\n \\bibliography" - - assert "[^2]:" in plugin.on_page_markdown(test_markdown, None, None, None) - - # ensure bib_by_default is working - plugin.config["bib_by_default"] = True - test_markdown = "This is a citation. [@test]" - - assert "[^1]:" in plugin.on_page_markdown(test_markdown, None, None, None) - plugin.config["bib_by_default"] = False - - # ensure nonexistant citekeys are removed correctly (not replaced) - test_markdown = "A non-existant citekey. [@i_do_not_exist]" - - assert "[@i_do_not_exist]" in plugin.on_page_markdown( - test_markdown, None, None, None - ) - - # Ensure if an item is referenced multiple times, it only shows up as one reference - test_markdown = "This is a citation. [@test] This is another citation [@test]\n\n \\bibliography" - - assert "[^2]" not in plugin.on_page_markdown(test_markdown, None, None, None) - - # Ensure item only shows up once even if used in multiple places as both a compound and lone cite key - test_markdown = "This is a citation. [@test; @test2] This is another citation [@test]\n\n \\bibliography" - - assert "[^3]" not in plugin.on_page_markdown(test_markdown, None, None, None) - - -def test_footnote_formatting_config(plugin): - """ - This function tests to ensure footnote formatting configuration is working properly - """ - # Test to make sure the config enforces {number} in the format - bad_plugin = BibTexPlugin() - bad_plugin.load_config( - options={"footnote_format": ""}, - config_file_path=test_files_dir, - ) - - with pytest.raises(Exception): - bad_plugin.on_config(bad_plugin.config) From ac1dc2d61e9102e88fff1a0faacefa356c48f1ed Mon Sep 17 00:00:00 2001 From: shyamd <16827130+shyamd@users.noreply.github.com> Date: Tue, 14 Jan 2025 13:10:58 -0800 Subject: [PATCH 15/58] add pandoc registry --- src/mkdocs_bibtex/registry.py | 117 +++++++++++++++++ test_files/test_pandoc_registry.py | 204 +++++++++++++++++++++++++++++ 2 files changed, 321 insertions(+) create mode 100644 test_files/test_pandoc_registry.py diff --git a/src/mkdocs_bibtex/registry.py b/src/mkdocs_bibtex/registry.py index 510b131..a514493 100644 --- a/src/mkdocs_bibtex/registry.py +++ b/src/mkdocs_bibtex/registry.py @@ -4,6 +4,10 @@ from pybtex.database import BibliographyData, parse_file from pybtex.backends.markdown import Backend as MarkdownBackend from pybtex.style.formatting.plain import Style as PlainStyle +import pypandoc +import tempfile +import re +from pathlib import Path class ReferenceRegistry(ABC): @@ -68,3 +72,116 @@ def reference_text(self, citation: Citation) -> str: entry_text = entry_text.replace("\\(", "(").replace("\\)", ")").replace("\\.", ".") log.debug(f"SUCCESS Converting bibtex entry {citation.key!r} without pandoc") return entry_text + + +class PandocRegistry(ReferenceRegistry): + """A registry that uses Pandoc to format citations""" + + def __init__(self, bib_files: list[str], csl_file: str): + super().__init__(bib_files) + self.csl_file = csl_file + + # Get pandoc version for formatting decisions + pandoc_version = tuple(int(ver) for ver in pypandoc.get_pandoc_version().split(".")) + if not pandoc_version >= (2, 11): + raise ValueError("Pandoc version 2.11 or higher is required for this registry") + + # Cache for formatted citations + self._inline_cache = {} + self._reference_cache = {} + + def inline_text(self, citation_block: CitationBlock) -> str: + """Returns cached inline citation text""" + return self._inline_cache.get(str(citation_block), "") + + def reference_text(self, citation: Citation) -> str: + """Returns cached reference text""" + return self._reference_cache.get(citation.key, "") + + def validate_citation_blocks(self, citation_blocks: list[CitationBlock]) -> None: + """Validates citation blocks and pre-formats all citations""" + # First validate all keys exist + for citation_block in citation_blocks: + for citation in citation_block.citations: + if citation.key not in self.bib_data.entries: + raise ValueError(f"Citation key {citation.key} not found in bibliography") + + # Pre-Process with appropriate pandoc version + self._inline_cache, self._reference_cache = _process_with_pandoc( + citation_blocks, self.bib_data_bibtex, self.csl_file + ) + + @property + def bib_data_bibtex(self) -> str: + """Convert bibliography data to BibTeX format""" + return self.bib_data.to_string("bibtex") + + +def _process_with_pandoc(citation_blocks: list[CitationBlock], bib_data: str, csl_file: str) -> tuple[dict, dict]: + """Process citations with pandoc""" + + # Build the document pandoc can process and we can parse to extract inline citations and reference text + full_doc = """ +--- +title: "Test" +link-citations: false +nocite: | + @* +--- +""" + citation_map = {index: block for index, block in enumerate(citation_blocks)} + full_doc += "\n\n".join(f"{index}. {block}" for index, block in citation_map.items()) + full_doc += "# References\n\n" + + with tempfile.TemporaryDirectory() as tmpdir: + bib_path = Path(tmpdir).joinpath("temp.bib") + with open(bib_path, "wt", encoding="utf-8") as bibfile: + bibfile.write(bib_data) + + args = ["--citeproc", "--bibliography", str(bib_path), "--csl", csl_file] + markdown = pypandoc.convert_text(source=full_doc, to="markdown-citations", format="markdown", extra_args=args) + + try: + splits = markdown.split("# References") + inline_citations, references = splits[0], splits[1] + except IndexError: + print(markdown) + raise ValueError("Failed to parse pandoc output") + + # Parse inline citations + inline_citations = inline_citations.strip() + + # Use regex to match numbered entries, handling multi-line citations + citation_pattern = re.compile(r"(\d+)\.\s+(.*?)(?=(?:\n\d+\.|$))", re.DOTALL) + matches = citation_pattern.finditer(inline_citations) + + # Create a dictionary of cleaned citations (removing extra whitespace and newlines) + inline_citations = {int(match.group(1)): " ".join(match.group(2).split()) for match in matches} + + inline_cache = {str(citation_map[index]): citation for index, citation in inline_citations.items()} + + # Parse references + reference_cache = {} + + # Pattern for format with .csl-left-margin and .csl-right-inline + pattern1 = r"::: \{#ref-(?P[^\s]+) .csl-entry\}\n\[.*?\]\{\.csl-left-margin\}\[(?P.*?)\]\{\.csl-right-inline\}" + + # Pattern for simple reference format + pattern2 = r"::: \{#ref-(?P[^\s]+) .csl-entry\}\n(?P.*?)(?=:::|$)" + + # Try first pattern + matches1 = re.finditer(pattern1, references, re.DOTALL) + for match in matches1: + key = match.group("key").strip() + citation = match.group("citation").replace("\n", " ").strip() + reference_cache[key] = citation + + # If no matches found, try second pattern + if not reference_cache: + matches2 = re.finditer(pattern2, references, re.DOTALL) + for match in matches2: + key = match.group("key").strip() + citation = match.group("citation").replace("\n", " ").strip() + reference_cache[key] = citation + + return inline_cache, reference_cache diff --git a/test_files/test_pandoc_registry.py b/test_files/test_pandoc_registry.py new file mode 100644 index 0000000..d0866f1 --- /dev/null +++ b/test_files/test_pandoc_registry.py @@ -0,0 +1,204 @@ +import os +import pytest +import pypandoc +from pathlib import Path +from mkdocs_bibtex.registry import PandocRegistry +from mkdocs_bibtex.citation import Citation, CitationBlock + +module_dir = os.path.dirname(os.path.abspath(__file__)) +test_files_dir = os.path.abspath(os.path.join(module_dir, "..", "test_files")) + +@pytest.fixture +def bib_file(): + return os.path.join(test_files_dir, "test.bib") + +@pytest.fixture +def csl(): + """Provide the Springer CSL file for testing""" + return os.path.join(test_files_dir, "springer-basic-author-date.csl") + +@pytest.fixture +def numeric_csl(): + """Provide the Nature CSL file for testing""" + return os.path.join(test_files_dir, "nature.csl") + +@pytest.fixture +def registry(bib_file, csl): + """Create a registry with Springer style for testing""" + return PandocRegistry([bib_file], csl) + +@pytest.fixture +def numeric_registry(bib_file, nature_csl): + """Create a registry with Nature style for testing""" + return PandocRegistry([bib_file], nature_csl) + +def test_bad_pandoc_registry(bib_file): + """Throw error if no CSL file is provided""" + with pytest.raises(Exception): + PandocRegistry([bib_file]) + +def test_pandoc_registry_initialization(registry, csl): + """Test basic initialization and loading of bib files""" + assert len(registry.bib_data.entries) == 4 + assert registry.csl_file is csl + + +def test_multiple_bib_files(csl): + """Test loading multiple bibliography files""" + bib1 = os.path.join(test_files_dir, "multi_bib", "bib1.bib") + bib2 = os.path.join(test_files_dir, "multi_bib", "multi_bib_child_dir", "bib2.bib") + + registry = PandocRegistry([bib1, bib2], csl) + assert "test1" in registry.bib_data.entries + assert "test2" in registry.bib_data.entries + + # Test citations from both files work + citation1 = Citation("test1", "", "") + citation2 = Citation("test2", "", "") + registry.validate_citation_blocks([CitationBlock([citation1, citation2])]) + text1 = registry.reference_text(citation1) + text2 = registry.reference_text(citation2) + assert "Test title 1" in text1 + assert "Test title 2" in text2 + +def test_validate_citation_blocks_valid(registry): + """Test validation of valid citation blocks""" + # Single citation + citations = [Citation("test", "", "")] + block = CitationBlock(citations) + registry.validate_citation_blocks([block]) + + # Multiple citations + citations = [Citation("test", "", ""), Citation("test2", "", "")] + block = CitationBlock(citations) + registry.validate_citation_blocks([block]) + +def test_validate_citation_blocks_invalid(registry): + """Test validation fails with invalid citation key""" + citations = [Citation("nonexistent", "", "")] + block = CitationBlock(citations) + with pytest.raises(ValueError, match="Citation key nonexistent not found in bibliography"): + registry.validate_citation_blocks([block]) + +def test_inline_text_basic(registry): + """Test basic inline citation formatting with different styles""" + citations = [Citation("test", "", "")] + block = CitationBlock(citations) + registry.validate_citation_blocks([block]) + text = registry.inline_text(block) + assert text # Basic check that we got some text back + assert "Author" in text # Should contain author name + +def test_inline_text_multiple(registry): + """Test inline citation with multiple references""" + citations = [Citation("test", "", ""), Citation("test2", "", "")] + block = CitationBlock(citations) + registry.validate_citation_blocks([block]) + text = registry.inline_text(block) + assert text + assert "Author" in text + +# Use springer style for consistent prefix/suffix tests +def test_inline_text_with_prefix(registry): + """Test inline citation with prefix""" + citations = [Citation("test", "see", "")] + block = CitationBlock(citations) + registry.validate_citation_blocks([block]) + text = registry.inline_text(block) + assert text + assert "see" in text.lower() + +def test_inline_text_with_suffix(registry): + """Test inline citation with suffix""" + citations = [Citation("test", "", "p. 123")] + block = CitationBlock(citations) + registry.validate_citation_blocks([block]) + text = registry.inline_text(block) + assert text + assert "123" in text + +def test_reference_text(registry): + """Test basic reference text formatting""" + citation = Citation("test", "", "") + block = CitationBlock([citation]) + registry.validate_citation_blocks([block]) + text = registry.reference_text(citation) + # Update assertion to match Springer style + assert "Author" in text and "Test title" in text + +def test_pandoc_formatting(registry): + """Test formatting with newer Pandoc versions""" + citation = Citation("test", "", "") + block = CitationBlock([citation]) + registry.validate_citation_blocks([block]) + text = registry.reference_text(citation) + assert text == "Author F, Author S (2019a) Test title. Testing Journal 1:" + + + +def test_multiple_citation_blocks(registry): + """Test multiple citation blocks""" + citations1 = [Citation("test", "", ""), Citation("test2", "", "")] + block1 = CitationBlock(citations1) + + citations2 = [Citation("Bivort2016", "", "")] + block2 = CitationBlock(citations2) + citation_blocks = [block1, block2] + registry.validate_citation_blocks(citation_blocks) + + text = registry.inline_text(block1) + assert text + assert "Author" in text + + # Test individual citations from block1 + text1 = registry.reference_text(citations1[0]) + text2 = registry.reference_text(citations1[1]) + assert text1 + assert text2 + assert "Author" in text1 + assert "Author" in text2 + + text = registry.inline_text(block2) + assert text + assert "Bivort" in text + +def test_unicode_in_citation(registry): + """Test citations containing unicode characters""" + citations = [Citation("testünicode", "", ""), + Citation("test_with_é", "", "")] + block = CitationBlock(citations) + with pytest.raises(ValueError, match="Citation key .* not found in bibliography"): + registry.validate_citation_blocks([block]) + +def test_complex_citation_formatting(registry): + """Test complex citation scenarios""" + citations = [ + Citation("test", "see", "p. 123-125"), + Citation("test2", "compare", "chapter 2"), + Citation("Bivort2016", "also", "figure 3") + ] + block = CitationBlock(citations) + registry.validate_citation_blocks([block]) + text = registry.inline_text(block) + + # Check that prefix, suffix, and multiple citations are formatted correctly + assert "see" in text.lower() + assert "123--125" in text + assert "compare" in text.lower() + assert "chap. 2" in text + assert "also" in text.lower() + assert "fig. 3" in text + +def test_empty_fields(registry): + """Test citations with empty fields in the bib file""" + citations = [Citation("empty_fields", "", "")] + block = CitationBlock(citations) + with pytest.raises(ValueError, match="Citation key .* not found in bibliography"): + registry.validate_citation_blocks([block]) + + +def test_malformed_citation_blocks(registry): + """Test handling of malformed citation blocks""" + # Invalid citation key type + with pytest.raises(ValueError): + registry.validate_citation_blocks([CitationBlock([Citation("123", "", "")])]) From 93b9a48b348a2ed7bffa4eea2a17e5cf695f215a Mon Sep 17 00:00:00 2001 From: shyamd <16827130+shyamd@users.noreply.github.com> Date: Tue, 14 Jan 2025 13:11:09 -0800 Subject: [PATCH 16/58] update citations --- src/mkdocs_bibtex/citation.py | 14 ++++++++++++++ test_files/test_citation.py | 13 +++++++++++++ 2 files changed, 27 insertions(+) diff --git a/src/mkdocs_bibtex/citation.py b/src/mkdocs_bibtex/citation.py index eb84be6..b7c9364 100644 --- a/src/mkdocs_bibtex/citation.py +++ b/src/mkdocs_bibtex/citation.py @@ -16,6 +16,16 @@ class Citation: prefix: str suffix: str + def __str__(self) -> str: + """String representation of the citation""" + parts = [] + if self.prefix: + parts.append(self.prefix) + parts.append(f"@{self.key}") + if self.suffix: + parts.append(self.suffix) + return " ".join(parts) + @classmethod def from_markdown(cls, markdown: str) -> List["Citation"]: """Extracts citations from a markdown string""" @@ -37,6 +47,10 @@ def from_markdown(cls, markdown: str) -> List["Citation"]: class CitationBlock: citations: List[Citation] + def __str__(self) -> str: + """String representation of the citation block""" + return "[" + "; ".join(str(citation) for citation in self.citations) + "]" + @classmethod def from_markdown(cls, markdown: str) -> List["CitationBlock"]: """Extracts citation blocks from a markdown string""" diff --git a/test_files/test_citation.py b/test_files/test_citation.py index 761e354..3f810df 100644 --- a/test_files/test_citation.py +++ b/test_files/test_citation.py @@ -119,3 +119,16 @@ def test_complex_citation_block(): assert blocks[0].citations[2].key == "test3" assert blocks[0].citations[2].prefix == " -" assert blocks[0].citations[2].suffix == "" + + +def test_citation_string(): + """Test citation string""" + citation = Citation("test", "Author", "2020") + assert str(citation) == "Author, 2020" + + block = CitationBlock([citation]) + assert str(block) == "[Author, 2020]" + + citations = [citation, citation] + block = CitationBlock(citations) + assert str(block) == "[Author, 2020; Author, 2020]" From 021507d674db5b1cc6a1b464ca85e863f36ff18a Mon Sep 17 00:00:00 2001 From: shyamd <16827130+shyamd@users.noreply.github.com> Date: Tue, 14 Jan 2025 13:20:28 -0800 Subject: [PATCH 17/58] lint --- src/mkdocs_bibtex/config.py | 1 + src/mkdocs_bibtex/plugin.py | 8 ++------ src/mkdocs_bibtex/utils.py | 34 +++++++++++++++------------------- test_files/test_utils.py | 21 ++++----------------- 4 files changed, 22 insertions(+), 42 deletions(-) diff --git a/src/mkdocs_bibtex/config.py b/src/mkdocs_bibtex/config.py index 70c05d5..990821e 100644 --- a/src/mkdocs_bibtex/config.py +++ b/src/mkdocs_bibtex/config.py @@ -18,6 +18,7 @@ class BibTexConfig(base.Config): cite_inline (bool): Whether or not to render inline citations, requires CSL, defaults to false footnote_format (string): format for the footnote number, defaults to "{number}" """ + # Input files bib_file = config_options.Optional(config_options.Type(str)) bib_dir = config_options.Optional(config_options.Dir(exists=True)) diff --git a/src/mkdocs_bibtex/plugin.py b/src/mkdocs_bibtex/plugin.py index d54bbee..5a403f1 100644 --- a/src/mkdocs_bibtex/plugin.py +++ b/src/mkdocs_bibtex/plugin.py @@ -34,7 +34,7 @@ def __init__(self): self.last_configured = None def on_startup(self, *, command, dirty): - """ Having on_startup() tells mkdocs to keep the plugin object upon rebuilds""" + """Having on_startup() tells mkdocs to keep the plugin object upon rebuilds""" pass def on_config(self, config): @@ -178,11 +178,7 @@ def format_citations(self, cite_keys): # 1. Extract the keys from the keyset entries = OrderedDict() - pairs = [ - [cite_block, key] - for cite_block in cite_keys - for key in extract_cite_keys(cite_block) - ] + pairs = [[cite_block, key] for cite_block in cite_keys for key in extract_cite_keys(cite_block)] keys = list(OrderedDict.fromkeys([k for _, k in pairs]).keys()) numbers = {k: str(n + 1) for n, k in enumerate(keys)} diff --git a/src/mkdocs_bibtex/utils.py b/src/mkdocs_bibtex/utils.py index 54bb261..ccbadc6 100644 --- a/src/mkdocs_bibtex/utils.py +++ b/src/mkdocs_bibtex/utils.py @@ -20,9 +20,10 @@ # Add the warning filter only if the version is lower than 1.2 # Filter doesn't do anything since that version -MKDOCS_LOG_VERSION = '1.2' +MKDOCS_LOG_VERSION = "1.2" if Version(mkdocs.__version__) < Version(MKDOCS_LOG_VERSION): from mkdocs.utils import warning_filter + log.addFilter(warning_filter) @@ -45,9 +46,7 @@ def format_simple(entries): entry_text = formatted_entry.text.render(backend) entry_text = entry_text.replace("\n", " ") # Local reference list for this file - citations[key] = ( - entry_text.replace("\\(", "(").replace("\\)", ")").replace("\\.", ".") - ) + citations[key] = entry_text.replace("\\(", "(").replace("\\)", ")").replace("\\.", ".") log.debug(f"SUCCESS Converting bibtex entry {key!r} without pandoc") return citations @@ -98,9 +97,7 @@ def _convert_pandoc_new(bibtex_string, csl_path): # Remove newlines from any generated span tag (non-capitalized words) markdown = re.compile(r"<\/span>[\r\n]").sub(" ", markdown) - citation_regex = re.compile( - r"(.+?)(?=<\/span>)<\/span>" - ) + citation_regex = re.compile(r"(.+?)(?=<\/span>)<\/span>") try: citation = citation_regex.findall(re.sub(r"(\r|\n)", "", markdown))[1] except IndexError: @@ -124,16 +121,20 @@ def _convert_pandoc_citekey(bibtex_string, csl_path, fullcite): with open(bib_path, "wt", encoding="utf-8") as bibfile: bibfile.write(bibtex_string) - log.debug(f"----Converting pandoc citation key {fullcite!r} with CSL file {csl_path!r} and Bibliography file" - f" '{bib_path!s}'...") + log.debug( + f"----Converting pandoc citation key {fullcite!r} with CSL file {csl_path!r} and Bibliography file" + f" '{bib_path!s}'..." + ) markdown = pypandoc.convert_text( source=fullcite, to="markdown-citations", format="markdown", extra_args=["--citeproc", "--csl", csl_path, "--bibliography", bib_path], ) - log.debug(f"----SUCCESS Converting pandoc citation key {fullcite!r} with CSL file {csl_path!r} and " - f"Bibliography file '{bib_path!s}'") + log.debug( + f"----SUCCESS Converting pandoc citation key {fullcite!r} with CSL file {csl_path!r} and " + f"Bibliography file '{bib_path!s}'" + ) # Return only the citation text (first line(s)) # remove any extra linebreaks to accommodate large author names @@ -250,8 +251,7 @@ def insert_citation_keys(citation_quads, markdown, csl=False, bib=False): pandoc_version_tuple = tuple(int(ver) for ver in pandoc_version.split(".")) if pandoc_version_tuple <= (2, 11): raise RuntimeError( - f"Your version of pandoc (v{pandoc_version}) is " - "incompatible with the cite_inline feature." + f"Your version of pandoc (v{pandoc_version}) is incompatible with the cite_inline feature." ) inline_citation = _convert_pandoc_citekey(bib, csl, full_citation) @@ -294,9 +294,7 @@ def tempfile_from_url(name, url, suffix): try: dl = requests.get(url) if dl.status_code != 200: # pragma: no cover - raise RuntimeError( - f"Couldn't download the url: {url}.\n Status Code: {dl.status_code}" - ) + raise RuntimeError(f"Couldn't download the url: {url}.\n Status Code: {dl.status_code}") file = tempfile.NamedTemporaryFile(mode="wt", encoding="utf-8", suffix=suffix, delete=False) file.write(dl.text) @@ -306,6 +304,4 @@ def tempfile_from_url(name, url, suffix): except requests.exceptions.RequestException: # pragma: no cover pass - raise RuntimeError( - f"Couldn't successfully download the url: {url}" - ) # pragma: no cover + raise RuntimeError(f"Couldn't successfully download the url: {url}") # pragma: no cover diff --git a/test_files/test_utils.py b/test_files/test_utils.py index 618608b..9d5483e 100644 --- a/test_files/test_utils.py +++ b/test_files/test_utils.py @@ -22,7 +22,6 @@ def entries(): def test_find_cite_blocks(): - # Suppressed authors assert find_cite_blocks("[-@test]") == ["[-@test]"] # Affixes @@ -42,14 +41,8 @@ def test_format_simple(entries): assert all(k in citations for k in entries) assert all(entry != citations[k] for k, entry in entries.items()) - assert ( - citations["test"] - == "First Author and Second Author. Test title. *Testing Journal*, 2019." - ) - assert ( - citations["test2"] - == "First Author and Second Author. Test Title (TT). *Testing Journal (TJ)*, 2019." - ) + assert citations["test"] == "First Author and Second Author. Test title. *Testing Journal*, 2019." + assert citations["test2"] == "First Author and Second Author. Test Title (TT). *Testing Journal (TJ)*, 2019." def test_format_pandoc(entries): @@ -58,14 +51,8 @@ def test_format_pandoc(entries): assert all(k in citations for k in entries) assert all(entry != citations[k] for k, entry in entries.items()) - assert ( - citations["test"] - == "Author, F. & Author, S. Test title. *Testing Journal* **1**, (2019)." - ) - assert ( - citations["test2"] - == "Author, F. & Author, S. Test Title (TT). *Testing Journal (TJ)* **1**, (2019)." - ) + assert citations["test"] == "Author, F. & Author, S. Test title. *Testing Journal* **1**, (2019)." + assert citations["test2"] == "Author, F. & Author, S. Test Title (TT). *Testing Journal (TJ)* **1**, (2019)." def test_extract_cite_key(): From a1b3a5f18610c232d529fced5c206935460cd98d Mon Sep 17 00:00:00 2001 From: shyamd <16827130+shyamd@users.noreply.github.com> Date: Tue, 14 Jan 2025 13:24:01 -0800 Subject: [PATCH 18/58] fix build issues --- .github/workflows/testing.yml | 2 +- src/mkdocs_bibtex/registry.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index dcacf57..bbacf25 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -39,7 +39,7 @@ jobs: matrix: os: [ubuntu-latest] python-version: [3.9, '3.10', 3.11, 3.12] - pandoc-version: [2.9.2, 2.14.0.3] + pandoc-version: [2.14.0.3, 3.6.2] runs-on: ${{ matrix.os }} steps: diff --git a/src/mkdocs_bibtex/registry.py b/src/mkdocs_bibtex/registry.py index a514493..ad0d00b 100644 --- a/src/mkdocs_bibtex/registry.py +++ b/src/mkdocs_bibtex/registry.py @@ -164,7 +164,7 @@ def _process_with_pandoc(citation_blocks: list[CitationBlock], bib_data: str, cs reference_cache = {} # Pattern for format with .csl-left-margin and .csl-right-inline - pattern1 = r"::: \{#ref-(?P[^\s]+) .csl-entry\}\n\[.*?\]\{\.csl-left-margin\}\[(?P.*?)\]\{\.csl-right-inline\}" + pattern1 = r"::: \{#ref-(?P[^\s]+) .csl-entry\}\n\[.*?\]\{\.csl-left-margin\}\[(?P.*?)\]\{\.csl-right-inline\}" # noqa: E501 # Pattern for simple reference format pattern2 = r"::: \{#ref-(?P[^\s]+) .csl-entry\}\n(?P.*?)(?=:::|$)" From b8df07f46ad0ba82e8fad7c73f5356b246163e0f Mon Sep 17 00:00:00 2001 From: shyamd <16827130+shyamd@users.noreply.github.com> Date: Tue, 14 Jan 2025 13:27:28 -0800 Subject: [PATCH 19/58] fix ci issues --- test_files/test_citation.py | 6 +++--- test_files/test_pandoc_registry.py | 34 ++++++++++++++++++++++-------- test_files/test_plugin.py | 4 ---- 3 files changed, 28 insertions(+), 16 deletions(-) diff --git a/test_files/test_citation.py b/test_files/test_citation.py index 3f810df..526469c 100644 --- a/test_files/test_citation.py +++ b/test_files/test_citation.py @@ -124,11 +124,11 @@ def test_complex_citation_block(): def test_citation_string(): """Test citation string""" citation = Citation("test", "Author", "2020") - assert str(citation) == "Author, 2020" + assert str(citation) == "Author @test 2020" block = CitationBlock([citation]) - assert str(block) == "[Author, 2020]" + assert str(block) == "[Author @test 2020]" citations = [citation, citation] block = CitationBlock(citations) - assert str(block) == "[Author, 2020; Author, 2020]" + assert str(block) == "[Author @test 2020; Author @test 2020]" diff --git a/test_files/test_pandoc_registry.py b/test_files/test_pandoc_registry.py index d0866f1..cd80870 100644 --- a/test_files/test_pandoc_registry.py +++ b/test_files/test_pandoc_registry.py @@ -8,35 +8,42 @@ module_dir = os.path.dirname(os.path.abspath(__file__)) test_files_dir = os.path.abspath(os.path.join(module_dir, "..", "test_files")) + @pytest.fixture def bib_file(): return os.path.join(test_files_dir, "test.bib") + @pytest.fixture def csl(): """Provide the Springer CSL file for testing""" return os.path.join(test_files_dir, "springer-basic-author-date.csl") + @pytest.fixture def numeric_csl(): """Provide the Nature CSL file for testing""" return os.path.join(test_files_dir, "nature.csl") + @pytest.fixture def registry(bib_file, csl): """Create a registry with Springer style for testing""" return PandocRegistry([bib_file], csl) + @pytest.fixture def numeric_registry(bib_file, nature_csl): """Create a registry with Nature style for testing""" return PandocRegistry([bib_file], nature_csl) + def test_bad_pandoc_registry(bib_file): """Throw error if no CSL file is provided""" with pytest.raises(Exception): PandocRegistry([bib_file]) + def test_pandoc_registry_initialization(registry, csl): """Test basic initialization and loading of bib files""" assert len(registry.bib_data.entries) == 4 @@ -47,11 +54,11 @@ def test_multiple_bib_files(csl): """Test loading multiple bibliography files""" bib1 = os.path.join(test_files_dir, "multi_bib", "bib1.bib") bib2 = os.path.join(test_files_dir, "multi_bib", "multi_bib_child_dir", "bib2.bib") - + registry = PandocRegistry([bib1, bib2], csl) assert "test1" in registry.bib_data.entries assert "test2" in registry.bib_data.entries - + # Test citations from both files work citation1 = Citation("test1", "", "") citation2 = Citation("test2", "", "") @@ -61,6 +68,7 @@ def test_multiple_bib_files(csl): assert "Test title 1" in text1 assert "Test title 2" in text2 + def test_validate_citation_blocks_valid(registry): """Test validation of valid citation blocks""" # Single citation @@ -73,6 +81,7 @@ def test_validate_citation_blocks_valid(registry): block = CitationBlock(citations) registry.validate_citation_blocks([block]) + def test_validate_citation_blocks_invalid(registry): """Test validation fails with invalid citation key""" citations = [Citation("nonexistent", "", "")] @@ -80,6 +89,7 @@ def test_validate_citation_blocks_invalid(registry): with pytest.raises(ValueError, match="Citation key nonexistent not found in bibliography"): registry.validate_citation_blocks([block]) + def test_inline_text_basic(registry): """Test basic inline citation formatting with different styles""" citations = [Citation("test", "", "")] @@ -89,6 +99,7 @@ def test_inline_text_basic(registry): assert text # Basic check that we got some text back assert "Author" in text # Should contain author name + def test_inline_text_multiple(registry): """Test inline citation with multiple references""" citations = [Citation("test", "", ""), Citation("test2", "", "")] @@ -98,6 +109,7 @@ def test_inline_text_multiple(registry): assert text assert "Author" in text + # Use springer style for consistent prefix/suffix tests def test_inline_text_with_prefix(registry): """Test inline citation with prefix""" @@ -108,6 +120,7 @@ def test_inline_text_with_prefix(registry): assert text assert "see" in text.lower() + def test_inline_text_with_suffix(registry): """Test inline citation with suffix""" citations = [Citation("test", "", "p. 123")] @@ -117,6 +130,7 @@ def test_inline_text_with_suffix(registry): assert text assert "123" in text + def test_reference_text(registry): """Test basic reference text formatting""" citation = Citation("test", "", "") @@ -126,6 +140,7 @@ def test_reference_text(registry): # Update assertion to match Springer style assert "Author" in text and "Test title" in text + def test_pandoc_formatting(registry): """Test formatting with newer Pandoc versions""" citation = Citation("test", "", "") @@ -135,7 +150,6 @@ def test_pandoc_formatting(registry): assert text == "Author F, Author S (2019a) Test title. Testing Journal 1:" - def test_multiple_citation_blocks(registry): """Test multiple citation blocks""" citations1 = [Citation("test", "", ""), Citation("test2", "", "")] @@ -145,7 +159,7 @@ def test_multiple_citation_blocks(registry): block2 = CitationBlock(citations2) citation_blocks = [block1, block2] registry.validate_citation_blocks(citation_blocks) - + text = registry.inline_text(block1) assert text assert "Author" in text @@ -162,25 +176,26 @@ def test_multiple_citation_blocks(registry): assert text assert "Bivort" in text + def test_unicode_in_citation(registry): """Test citations containing unicode characters""" - citations = [Citation("testünicode", "", ""), - Citation("test_with_é", "", "")] + citations = [Citation("testünicode", "", ""), Citation("test_with_é", "", "")] block = CitationBlock(citations) with pytest.raises(ValueError, match="Citation key .* not found in bibliography"): registry.validate_citation_blocks([block]) + def test_complex_citation_formatting(registry): """Test complex citation scenarios""" citations = [ Citation("test", "see", "p. 123-125"), Citation("test2", "compare", "chapter 2"), - Citation("Bivort2016", "also", "figure 3") + Citation("Bivort2016", "also", "figure 3"), ] block = CitationBlock(citations) registry.validate_citation_blocks([block]) text = registry.inline_text(block) - + # Check that prefix, suffix, and multiple citations are formatted correctly assert "see" in text.lower() assert "123--125" in text @@ -189,6 +204,7 @@ def test_complex_citation_formatting(registry): assert "also" in text.lower() assert "fig. 3" in text + def test_empty_fields(registry): """Test citations with empty fields in the bib file""" citations = [Citation("empty_fields", "", "")] @@ -198,7 +214,7 @@ def test_empty_fields(registry): def test_malformed_citation_blocks(registry): - """Test handling of malformed citation blocks""" + """Test handling of malformed citation blocks""" # Invalid citation key type with pytest.raises(ValueError): registry.validate_citation_blocks([CitationBlock([Citation("123", "", "")])]) diff --git a/test_files/test_plugin.py b/test_files/test_plugin.py index cdbc06a..f4eec5d 100644 --- a/test_files/test_plugin.py +++ b/test_files/test_plugin.py @@ -44,7 +44,3 @@ def test_bibtex_loading_bibdir(): plugin.on_config(plugin.config) assert len(plugin.bib_data.entries) == 2 - - - - From 93c4f1575606c38ce12a1bf1ef7f69925f05d32b Mon Sep 17 00:00:00 2001 From: shyamd <16827130+shyamd@users.noreply.github.com> Date: Tue, 14 Jan 2025 13:30:48 -0800 Subject: [PATCH 20/58] print because i can't get old versions of pandoc localy --- src/mkdocs_bibtex/registry.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/mkdocs_bibtex/registry.py b/src/mkdocs_bibtex/registry.py index ad0d00b..8dc4886 100644 --- a/src/mkdocs_bibtex/registry.py +++ b/src/mkdocs_bibtex/registry.py @@ -48,8 +48,7 @@ def validate_citation_blocks(self, citation_blocks: list[CitationBlock]) -> None for citation_block in citation_blocks: for citation in citation_block.citations: if citation.key not in self.bib_data.entries: - # TODO: Should this be a warning or fatal error? - pass + log.warning("File '%s' not found. Breaks the build if --strict is passed", my_file_name) for citation_block in citation_blocks: for citation in citation_block.citations: @@ -148,6 +147,7 @@ def _process_with_pandoc(citation_blocks: list[CitationBlock], bib_data: str, cs print(markdown) raise ValueError("Failed to parse pandoc output") + print(markdown) # Parse inline citations inline_citations = inline_citations.strip() From 2bfa8dae002a85e43429782d28d01b5e61bef096 Mon Sep 17 00:00:00 2001 From: shyamd <16827130+shyamd@users.noreply.github.com> Date: Tue, 14 Jan 2025 13:45:23 -0800 Subject: [PATCH 21/58] skip test for old pandoc --- test_files/test_pandoc_registry.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test_files/test_pandoc_registry.py b/test_files/test_pandoc_registry.py index cd80870..2a6fa1d 100644 --- a/test_files/test_pandoc_registry.py +++ b/test_files/test_pandoc_registry.py @@ -185,6 +185,9 @@ def test_unicode_in_citation(registry): registry.validate_citation_blocks([block]) +@pytest.mark.skip_if( + int(pypandoc.get_pandoc_version().split(".")[0]) < 3, reason="Pandoc formatting is different in Pandoc 3.0" +) def test_complex_citation_formatting(registry): """Test complex citation scenarios""" citations = [ From 20982ab754d832b855610d96615009e246c91bf3 Mon Sep 17 00:00:00 2001 From: shyamd <16827130+shyamd@users.noreply.github.com> Date: Tue, 14 Jan 2025 14:10:54 -0800 Subject: [PATCH 22/58] warn for non-existant citaitons --- src/mkdocs_bibtex/registry.py | 9 ++++----- test_files/test_pandoc_registry.py | 29 +++-------------------------- test_files/test_simple_registry.py | 10 +++++----- 3 files changed, 12 insertions(+), 36 deletions(-) diff --git a/src/mkdocs_bibtex/registry.py b/src/mkdocs_bibtex/registry.py index 8dc4886..21aae23 100644 --- a/src/mkdocs_bibtex/registry.py +++ b/src/mkdocs_bibtex/registry.py @@ -48,13 +48,12 @@ def validate_citation_blocks(self, citation_blocks: list[CitationBlock]) -> None for citation_block in citation_blocks: for citation in citation_block.citations: if citation.key not in self.bib_data.entries: - log.warning("File '%s' not found. Breaks the build if --strict is passed", my_file_name) + log.warning(f"Citing unknown reference key {citation.key}") for citation_block in citation_blocks: for citation in citation_block.citations: if citation.prefix != "" or citation.suffix != "": - # TODO: Should this be a warning or fatal error? - pass + log.warning(f"Affixes not supported in simple mode: {citation}") def inline_text(self, citation_block: CitationBlock) -> str: keys = sorted(set(citation.key for citation in citation_block.citations)) @@ -103,7 +102,7 @@ def validate_citation_blocks(self, citation_blocks: list[CitationBlock]) -> None for citation_block in citation_blocks: for citation in citation_block.citations: if citation.key not in self.bib_data.entries: - raise ValueError(f"Citation key {citation.key} not found in bibliography") + log.warning(f"Citing unknown reference key {citation.key}") # Pre-Process with appropriate pandoc version self._inline_cache, self._reference_cache = _process_with_pandoc( @@ -130,7 +129,7 @@ def _process_with_pandoc(citation_blocks: list[CitationBlock], bib_data: str, cs """ citation_map = {index: block for index, block in enumerate(citation_blocks)} full_doc += "\n\n".join(f"{index}. {block}" for index, block in citation_map.items()) - full_doc += "# References\n\n" + full_doc += "\n\n# References\n\n" with tempfile.TemporaryDirectory() as tmpdir: bib_path = Path(tmpdir).joinpath("temp.bib") diff --git a/test_files/test_pandoc_registry.py b/test_files/test_pandoc_registry.py index 2a6fa1d..b11ab84 100644 --- a/test_files/test_pandoc_registry.py +++ b/test_files/test_pandoc_registry.py @@ -1,7 +1,6 @@ import os import pytest import pypandoc -from pathlib import Path from mkdocs_bibtex.registry import PandocRegistry from mkdocs_bibtex.citation import Citation, CitationBlock @@ -82,11 +81,12 @@ def test_validate_citation_blocks_valid(registry): registry.validate_citation_blocks([block]) +@pytest.mark.xfail(reason="For some reason pytest does not catch the warning") def test_validate_citation_blocks_invalid(registry): """Test validation fails with invalid citation key""" citations = [Citation("nonexistent", "", "")] block = CitationBlock(citations) - with pytest.raises(ValueError, match="Citation key nonexistent not found in bibliography"): + with pytest.warns(UserWarning, match="Citing unknown reference key nonexistent"): registry.validate_citation_blocks([block]) @@ -177,15 +177,7 @@ def test_multiple_citation_blocks(registry): assert "Bivort" in text -def test_unicode_in_citation(registry): - """Test citations containing unicode characters""" - citations = [Citation("testünicode", "", ""), Citation("test_with_é", "", "")] - block = CitationBlock(citations) - with pytest.raises(ValueError, match="Citation key .* not found in bibliography"): - registry.validate_citation_blocks([block]) - - -@pytest.mark.skip_if( +@pytest.mark.skipif( int(pypandoc.get_pandoc_version().split(".")[0]) < 3, reason="Pandoc formatting is different in Pandoc 3.0" ) def test_complex_citation_formatting(registry): @@ -206,18 +198,3 @@ def test_complex_citation_formatting(registry): assert "chap. 2" in text assert "also" in text.lower() assert "fig. 3" in text - - -def test_empty_fields(registry): - """Test citations with empty fields in the bib file""" - citations = [Citation("empty_fields", "", "")] - block = CitationBlock(citations) - with pytest.raises(ValueError, match="Citation key .* not found in bibliography"): - registry.validate_citation_blocks([block]) - - -def test_malformed_citation_blocks(registry): - """Test handling of malformed citation blocks""" - # Invalid citation key type - with pytest.raises(ValueError): - registry.validate_citation_blocks([CitationBlock([Citation("123", "", "")])]) diff --git a/test_files/test_simple_registry.py b/test_files/test_simple_registry.py index 260989c..d937c99 100644 --- a/test_files/test_simple_registry.py +++ b/test_files/test_simple_registry.py @@ -31,28 +31,28 @@ def test_validate_citation_blocks_valid(simple_registry): simple_registry.validate_citation_blocks([block]) -@pytest.mark.xfail(reason="Old logic didn't fail this way but maybe it should") +@pytest.mark.xfail(reason="For some reason pytest does not catch the warning") def test_validate_citation_blocks_invalid_key(simple_registry): """Test validation fails with invalid citation key""" citations = [Citation("nonexistent", "", "")] block = CitationBlock(citations) - with pytest.raises(ValueError, match="Citation key nonexistent not found in bibliography"): + with pytest.warns(UserWarning, match="Citing unknown reference key nonexistent"): simple_registry.validate_citation_blocks([block]) -@pytest.mark.xfail(reason="Old logic didn't fail this way but maybe it should") +@pytest.mark.xfail(reason="For some reason pytest does not catch the warning") def test_validate_citation_blocks_invalid_affixes(simple_registry): """Test validation fails with affixes (not supported in simple mode)""" # Test prefix citations = [Citation("test", "see", "")] block = CitationBlock(citations) - with pytest.raises(ValueError, match="Simple style does not support any affixes"): + with pytest.warns(UserWarning, match="Simple style does not support any affixes"): simple_registry.validate_citation_blocks([block]) # Test suffix citations = [Citation("test", "", "p. 123")] block = CitationBlock(citations) - with pytest.raises(ValueError, match="Simple style does not support any affixes"): + with pytest.warns(UserWarning, match="Simple style does not support any affixes"): simple_registry.validate_citation_blocks([block]) From f5bf046be0aed58c08bad91885463cd0eff599a2 Mon Sep 17 00:00:00 2001 From: shyamd <16827130+shyamd@users.noreply.github.com> Date: Tue, 14 Jan 2025 14:12:32 -0800 Subject: [PATCH 23/58] fix mypy --- src/mkdocs_bibtex/registry.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/mkdocs_bibtex/registry.py b/src/mkdocs_bibtex/registry.py index 21aae23..d4e87ae 100644 --- a/src/mkdocs_bibtex/registry.py +++ b/src/mkdocs_bibtex/registry.py @@ -85,8 +85,8 @@ def __init__(self, bib_files: list[str], csl_file: str): raise ValueError("Pandoc version 2.11 or higher is required for this registry") # Cache for formatted citations - self._inline_cache = {} - self._reference_cache = {} + self._inline_cache: dict[str, str] = {} + self._reference_cache: dict[str, str] = {} def inline_text(self, citation_block: CitationBlock) -> str: """Returns cached inline citation text""" From b601af336f996fa79630884285e3e35d551fe6bc Mon Sep 17 00:00:00 2001 From: shyamd <16827130+shyamd@users.noreply.github.com> Date: Tue, 14 Jan 2025 15:31:32 -0800 Subject: [PATCH 24/58] unlink utils test --- test_files/test_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test_files/test_utils.py b/test_files/test_utils.py index 9d5483e..199ec8f 100644 --- a/test_files/test_utils.py +++ b/test_files/test_utils.py @@ -9,7 +9,7 @@ extract_cite_keys, ) -from mkdocs_bibtex.plugin import parse_file +from pybtex.database import parse_file module_dir = os.path.dirname(os.path.abspath(__file__)) test_files_dir = os.path.abspath(os.path.join(module_dir, "..", "test_files")) From 8f9401fe251425381386ecac0ffb5002a84d7529 Mon Sep 17 00:00:00 2001 From: shyamd <16827130+shyamd@users.noreply.github.com> Date: Tue, 14 Jan 2025 15:31:43 -0800 Subject: [PATCH 25/58] update integration tests for non-numeric keys --- test_files/test_integration.py | 45 ++++++++++++++++++---------------- 1 file changed, 24 insertions(+), 21 deletions(-) diff --git a/test_files/test_integration.py b/test_files/test_integration.py index 89c6c31..6a39042 100644 --- a/test_files/test_integration.py +++ b/test_files/test_integration.py @@ -55,8 +55,8 @@ def test_basic_citation_rendering(plugin): result = plugin.on_page_markdown(markdown, None, None, None) # Check citation replacements - assert "[^1]" in result - assert "[^2]" in result + assert "[^test]" in result + assert "[^test2]" in result # Check bibliography entries assert "First Author and Second Author. Test title. *Testing Journal*, 2019." in result @@ -67,39 +67,40 @@ def test_pandoc_citation_rendering(pandoc_plugin): """Test citation rendering with Pandoc and CSL""" markdown = "Here is a citation [@test] and another [@Bivort2016].\n\n\\bibliography" result = pandoc_plugin.on_page_markdown(markdown, None, None, None) - + print(pandoc_plugin.registry._inline_cache) + print(result) # Check inline citations - assert "(Author and Author 2019)" in result + assert "(Author and Author 2019a)" in result assert "(De Bivort and Van Swinderen 2016)" in result # Check bibliography formatting - assert "Author F, Author S (2019)" in result + assert "Author F, Author S (2019a)" in result assert "De Bivort BL, Van Swinderen B (2016)" in result def test_citation_features(pandoc_plugin): """Test various citation features like prefixes, suffixes, and author suppression""" markdown = """ - See [-@test] for more. - As shown by [see @test, p. 123]. - Multiple sources [@test; @test2]. - - \\bibliography" +See [-@test] for more. +As shown by [see @test, p. 123]. +Multiple sources [@test; @test2]. + +\\bibliography """ result = pandoc_plugin.on_page_markdown(markdown, None, None, None) # Check various citation formats - assert "(2019)" in result # Suppressed author - assert "(see Author and Author 2019, p. 123)" in result # Prefix and suffix + assert "(2019" in result # Suppressed author + assert "see Author and Author 2019a, p. 123" in result # Prefix and suffix assert "Author and Author 2019a, b" in result # Multiple citations # Check bibliography formatting - assert "Author F, Author S (2019) Test title. Testing Journal 1" in result - assert "Author F, Author S (2019) Test Title (TT). Testing Journal (TJ) 1" in result + assert "Author F, Author S (2019a) Test title. Testing Journal 1:" in result + assert "Author F, Author S (2019b) Test Title (TT). Testing Journal (TJ) 1:" in result # Check that the bibliography entries are only shown once - assert result.count("Author F, Author S (2019) Test title. Testing Journal 1") == 1 - assert result.count("Author F, Author S (2019) Test Title (TT). Testing Journal (TJ) 1") == 1 + assert result.count("Author F, Author S (2019a) Test title. Testing Journal 1:") == 1 + assert result.count("Author F, Author S (2019b) Test Title (TT). Testing Journal (TJ) 1:") == 1 def test_bibliography_controls(plugin): @@ -107,25 +108,26 @@ def test_bibliography_controls(plugin): # Test with explicit bibliography command markdown = "Citation [@test]\n\n\\bibliography" result = plugin.on_page_markdown(markdown, None, None, None) - assert "[^1]:" in result + assert "[^test]:" in result # Test without bibliography command when bib_by_default is False markdown = "Citation [@test]" result = plugin.on_page_markdown(markdown, None, None, None) - assert "[^1]:" not in result + assert "[^test]:" not in result # Test without bibliography command when bib_by_default is True plugin.config.bib_by_default = True result = plugin.on_page_markdown(markdown, None, None, None) - assert "[^1]:" in result + assert "[^test]:" in result +@pytest.mark.xfail(reason="Need to reimplement footnote formatting") def test_custom_footnote_format(plugin): """Test custom footnote formatting""" plugin.config.footnote_format = "ref{number}" markdown = "Citation [@test]\n\n\\bibliography" result = plugin.on_page_markdown(markdown, None, None, None) - assert "[^ref1]" in result + assert "[^reftest]" in result # Test that an invalid footnote format raises an exception bad_plugin = BibTexPlugin() @@ -141,4 +143,5 @@ def test_invalid_citations(plugin): """Test handling of invalid citations""" markdown = "Invalid citation [@nonexistent]\n\n\\bibliography" result = plugin.on_page_markdown(markdown, None, None, None) - assert "[@nonexistent]" in result # Invalid citation should remain unchanged + # assert "[@nonexistent]" in result # Invalid citation should remain unchanged + assert "[^nonexistent]" not in result From 05e461042a5208faebd91014d1923a58b73f82c3 Mon Sep 17 00:00:00 2001 From: shyamd <16827130+shyamd@users.noreply.github.com> Date: Tue, 14 Jan 2025 15:31:54 -0800 Subject: [PATCH 26/58] update plugin tests for registry --- test_files/test_plugin.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test_files/test_plugin.py b/test_files/test_plugin.py index f4eec5d..c428e0f 100644 --- a/test_files/test_plugin.py +++ b/test_files/test_plugin.py @@ -21,7 +21,7 @@ def plugin(): def test_bibtex_loading_bibfile(plugin): - assert len(plugin.bib_data.entries) == 4 + assert len(plugin.registry.bib_data.entries) == 4 def test_bibtex_loading_bib_url(): @@ -32,7 +32,7 @@ def test_bibtex_loading_bib_url(): ) plugin.on_config(plugin.config) - assert len(plugin.bib_data.entries) == 4 + assert len(plugin.registry.bib_data.entries) == 4 def test_bibtex_loading_bibdir(): @@ -43,4 +43,4 @@ def test_bibtex_loading_bibdir(): ) plugin.on_config(plugin.config) - assert len(plugin.bib_data.entries) == 2 + assert len(plugin.registry.bib_data.entries) == 2 From 9fe9c7b1b334efac91afe4eb4aba831a326e2a56 Mon Sep 17 00:00:00 2001 From: shyamd <16827130+shyamd@users.noreply.github.com> Date: Tue, 14 Jan 2025 15:32:17 -0800 Subject: [PATCH 27/58] fix simple registry --- src/mkdocs_bibtex/registry.py | 9 ++++----- test_files/test_simple_registry.py | 2 +- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/mkdocs_bibtex/registry.py b/src/mkdocs_bibtex/registry.py index d4e87ae..a7737f4 100644 --- a/src/mkdocs_bibtex/registry.py +++ b/src/mkdocs_bibtex/registry.py @@ -56,9 +56,8 @@ def validate_citation_blocks(self, citation_blocks: list[CitationBlock]) -> None log.warning(f"Affixes not supported in simple mode: {citation}") def inline_text(self, citation_block: CitationBlock) -> str: - keys = sorted(set(citation.key for citation in citation_block.citations)) - - return "[" + ",".join(f"^{key}" for key in keys) + "]" + keys = [citation.key for citation in citation_block.citations if citation.key in self.bib_data.entries] + return "".join(f"[^{key}]" for key in keys) def reference_text(self, citation: Citation) -> str: entry = self.bib_data.entries[citation.key] @@ -90,7 +89,8 @@ def __init__(self, bib_files: list[str], csl_file: str): def inline_text(self, citation_block: CitationBlock) -> str: """Returns cached inline citation text""" - return self._inline_cache.get(str(citation_block), "") + keys = [citation.key for citation in citation_block.citations if citation.key in self._reference_cache] + return self._inline_cache.get(str(citation_block), "") + "".join(f"[^{key}]" for key in keys) def reference_text(self, citation: Citation) -> str: """Returns cached reference text""" @@ -146,7 +146,6 @@ def _process_with_pandoc(citation_blocks: list[CitationBlock], bib_data: str, cs print(markdown) raise ValueError("Failed to parse pandoc output") - print(markdown) # Parse inline citations inline_citations = inline_citations.strip() diff --git a/test_files/test_simple_registry.py b/test_files/test_simple_registry.py index d937c99..2ab5dc2 100644 --- a/test_files/test_simple_registry.py +++ b/test_files/test_simple_registry.py @@ -66,7 +66,7 @@ def test_inline_text(simple_registry): # Multiple citations citations = [Citation("test", "", ""), Citation("test2", "", "")] block = CitationBlock(citations) - assert simple_registry.inline_text(block) == "[^test,^test2]" + assert simple_registry.inline_text(block) == "[^test][^test2]" def test_reference_text(simple_registry): From 8a50b10ae6897356477e1cea42ebcd900c937dfa Mon Sep 17 00:00:00 2001 From: shyamd <16827130+shyamd@users.noreply.github.com> Date: Tue, 14 Jan 2025 15:32:44 -0800 Subject: [PATCH 28/58] use registry in plugin --- src/mkdocs_bibtex/plugin.py | 157 +++++++++--------------------------- 1 file changed, 39 insertions(+), 118 deletions(-) diff --git a/src/mkdocs_bibtex/plugin.py b/src/mkdocs_bibtex/plugin.py index 5a403f1..c40a414 100644 --- a/src/mkdocs_bibtex/plugin.py +++ b/src/mkdocs_bibtex/plugin.py @@ -3,21 +3,18 @@ import validators from collections import OrderedDict from pathlib import Path +from collections import OrderedDict from mkdocs.plugins import BasePlugin -from pybtex.database import BibliographyData, parse_file + +from mkdocs_bibtex.citation import CitationBlock, Citation from mkdocs_bibtex.config import BibTexConfig +from mkdocs_bibtex.registry import SimpleRegistry, PandocRegistry from mkdocs.exceptions import ConfigurationError from mkdocs_bibtex.utils import ( - find_cite_blocks, - extract_cite_keys, - format_bibliography, - format_pandoc, - format_simple, - insert_citation_keys, tempfile_from_url, log, ) @@ -32,6 +29,7 @@ def __init__(self): self.bib_data = None self.all_references = OrderedDict() self.last_configured = None + self.registry = None def on_startup(self, *, command, dirty): """Having on_startup() tells mkdocs to keep the plugin object upon rebuilds""" @@ -63,20 +61,9 @@ def on_config(self, config): log.info("BibTexPlugin: No changes in bibfiles.") return config - # load bibliography data - refs = {} - log.info(f"Loading data from bib files: {bibfiles}") - for bibfile in bibfiles: - log.debug(f"Parsing bibtex file {bibfile}") - bibdata = parse_file(bibfile) - refs.update(bibdata.entries) - # Clear references on reconfig self.all_references = OrderedDict() - self.bib_data = BibliographyData(entries=refs) - self.bib_data_bibtex = self.bib_data.to_string("bibtex") - # Set CSL from either url or path (or empty) if self.config.csl_file is not None and validators.url(self.config.csl_file): self.csl_file = tempfile_from_url("CSL file", self.config.csl_file, ".csl") @@ -90,6 +77,11 @@ def on_config(self, config): if "{number}" not in self.config.footnote_format: raise ConfigurationError("Must include `{number}` placeholder in footnote_format") + if self.csl_file: + self.registry = PandocRegistry(bib_files=bibfiles, csl_file=self.csl_file) + else: + self.registry = SimpleRegistry(bib_files=bibfiles) + self.last_configured = time.time() return config @@ -109,23 +101,16 @@ def on_page_markdown(self, markdown, page, config, files): 5. Insert the full bibliograph into the markdown """ - # 1. Grab all the cited keys in the markdown - cite_keys = find_cite_blocks(markdown) - - # 2. Convert all the citations to text references - citation_quads = self.format_citations(cite_keys) - - # 3. Convert cited keys to citation, - # or a footnote reference if inline_cite is false. - if self.config.cite_inline: - markdown = insert_citation_keys( - citation_quads, - markdown, - self.csl_file, - self.bib_data_bibtex, - ) - else: - markdown = insert_citation_keys(citation_quads, markdown) + # 1. Find all cite blocks in the markdown + cite_blocks = CitationBlock.from_markdown(markdown) + + # 2. Validate the cite blocks + self.registry.validate_citation_blocks(cite_blocks) + print(cite_blocks) + # 3. Replace the cite blocks with the inline citations + for block in cite_blocks: + replacement = self.registry.inline_text(block) + markdown = markdown.replace(str(block), replacement) # 4. Insert in the bibliopgrahy text into the markdown bib_command = self.config.bib_command @@ -133,7 +118,18 @@ def on_page_markdown(self, markdown, page, config, files): if self.config.bib_by_default: markdown += f"\n{bib_command}" - bibliography = format_bibliography(citation_quads) + citations = OrderedDict() + for block in cite_blocks: + for citation in block.citations: + citations[citation.key] = citation + + bibliography = [] + for citation in citations.values(): + try: + bibliography.append("[^{}]: {}".format(citation.key, self.registry.reference_text(citation))) + except Exception as e: + log.warning(f"Error formatting citation {citation.key}: {e}") + bibliography = "\n".join(bibliography) markdown = re.sub( re.escape(bib_command), bibliography, @@ -142,88 +138,13 @@ def on_page_markdown(self, markdown, page, config, files): # 5. Build the full Bibliography and insert into the text full_bib_command = self.config.full_bib_command + all_citations = [Citation(key=key) for key in self.registry.bib_data.entries] + full_bibliography = [] + for citation in all_citations: + full_bibliography.append("[^{}]: {}".format(citation.key, self.registry.reference_text(citation))) + full_bibliography = "\n".join(full_bibliography) - markdown = re.sub( - re.escape(full_bib_command), - self.full_bibliography, - markdown, - ) + markdown = markdown.replace(bib_command, bibliography) + markdown = markdown.replace(full_bib_command, full_bibliography) return markdown - - def format_footnote_key(self, number): - """ - Create footnote key based on footnote_format - - Args: - number (int): citation number - - Returns: - formatted footnote - """ - return self.config.footnote_format.format(number=number) - - def format_citations(self, cite_keys): - """ - Formats references into citation quads and adds them to the global registry - - Args: - cite_keys (list): List of full cite_keys that maybe compound keys - - Returns: - citation_quads: quad tuples of the citation inforamtion - """ - - # Deal with arithmatex fix at some point - - # 1. Extract the keys from the keyset - entries = OrderedDict() - pairs = [[cite_block, key] for cite_block in cite_keys for key in extract_cite_keys(cite_block)] - keys = list(OrderedDict.fromkeys([k for _, k in pairs]).keys()) - numbers = {k: str(n + 1) for n, k in enumerate(keys)} - - # Remove non-existant keys from pairs - pairs = [p for p in pairs if p[1] in self.bib_data.entries] - - # 2. Collect any unformatted reference keys - for _, key in pairs: - if key not in self.all_references: - entries[key] = self.bib_data.entries[key] - - # 3. Format entries - log.debug("Formatting all bib entries...") - if self.csl_file: - self.all_references.update(format_pandoc(entries, self.csl_file)) - else: - self.all_references.update(format_simple(entries)) - log.debug("SUCCESS Formatting all bib entries") - - # 4. Construct quads - quads = [ - ( - cite_block, - key, - self.format_footnote_key(numbers[key]), - self.all_references[key], - ) - for cite_block, key in pairs - ] - - # List the quads in order to remove duplicate entries - return list(dict.fromkeys(quads)) - - @property - def full_bibliography(self): - """ - Returns the full bibliography text - """ - - bibliography = [] - for number, (key, citation) in enumerate(self.all_references.items()): - bibliography_text = "[^{}]: {}".format( - number, - citation, - ) - bibliography.append(bibliography_text) - - return "\n".join(bibliography) From 84b12cd12b1544c39ba9d7a43297f8436cece9e7 Mon Sep 17 00:00:00 2001 From: shyamd <16827130+shyamd@users.noreply.github.com> Date: Tue, 14 Jan 2025 15:54:06 -0800 Subject: [PATCH 29/58] fix citation --- src/mkdocs_bibtex/citation.py | 11 ++++++++--- test_files/test_citation.py | 4 ++++ 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src/mkdocs_bibtex/citation.py b/src/mkdocs_bibtex/citation.py index b7c9364..de26016 100644 --- a/src/mkdocs_bibtex/citation.py +++ b/src/mkdocs_bibtex/citation.py @@ -13,8 +13,8 @@ class Citation: """Represents a citation in raw markdown without formatting""" key: str - prefix: str - suffix: str + prefix: str = "" + suffix: str = "" def __str__(self) -> str: """String representation of the citation""" @@ -46,9 +46,12 @@ def from_markdown(cls, markdown: str) -> List["Citation"]: @dataclass class CitationBlock: citations: List[Citation] + raw: str = "" def __str__(self) -> str: """String representation of the citation block""" + if self.raw != "": + return f"[{self.raw}]" return "[" + "; ".join(str(citation) for citation in self.citations) + "]" @classmethod @@ -64,7 +67,9 @@ def from_markdown(cls, markdown: str) -> List["CitationBlock"]: citation_blocks = [] for match in CITATION_BLOCK_REGEX.finditer(markdown): try: - citation_blocks.append(CitationBlock(citations=Citation.from_markdown(match.group(1)))) + citation_blocks.append( + CitationBlock(raw=match.group(1), citations=Citation.from_markdown(match.group(1))) + ) except Exception as e: print(f"Error extracting citations from block: {e}") return citation_blocks diff --git a/test_files/test_citation.py b/test_files/test_citation.py index 526469c..43ed15c 100644 --- a/test_files/test_citation.py +++ b/test_files/test_citation.py @@ -80,6 +80,7 @@ def test_citation_block(): assert blocks[0].citations[0].key == "test" assert blocks[0].citations[0].prefix == "see" assert blocks[0].citations[0].suffix == "p. 123" + assert str(blocks[0]) == "[see @test, p. 123]" def test_multiple_citation_blocks(): @@ -88,6 +89,8 @@ def test_multiple_citation_blocks(): assert len(blocks) == 2 assert blocks[0].citations[0].key == "test" assert blocks[1].citations[0].key == "test2" + assert str(blocks[0]) == "[see @test, p. 123]" + assert str(blocks[1]) == "[@test2]" def test_invalid_citation(): @@ -119,6 +122,7 @@ def test_complex_citation_block(): assert blocks[0].citations[2].key == "test3" assert blocks[0].citations[2].prefix == " -" assert blocks[0].citations[2].suffix == "" + assert str(blocks[0]) == "[see @test1, p. 123; @test2, p. 456; -@test3]" def test_citation_string(): From 9d8d87f0a0128d9d6f86b2ff61657cd2b340da08 Mon Sep 17 00:00:00 2001 From: shyamd <16827130+shyamd@users.noreply.github.com> Date: Tue, 14 Jan 2025 15:56:39 -0800 Subject: [PATCH 30/58] more linting --- src/mkdocs_bibtex/plugin.py | 1 - test_files/test_citation.py | 1 - test_files/test_simple_registry.py | 2 +- 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/mkdocs_bibtex/plugin.py b/src/mkdocs_bibtex/plugin.py index c40a414..0425a53 100644 --- a/src/mkdocs_bibtex/plugin.py +++ b/src/mkdocs_bibtex/plugin.py @@ -3,7 +3,6 @@ import validators from collections import OrderedDict from pathlib import Path -from collections import OrderedDict from mkdocs.plugins import BasePlugin diff --git a/test_files/test_citation.py b/test_files/test_citation.py index 43ed15c..9fc9f03 100644 --- a/test_files/test_citation.py +++ b/test_files/test_citation.py @@ -3,7 +3,6 @@ pybtex basic citations and pandoc citation formattting """ -import pytest from mkdocs_bibtex.citation import Citation, CitationBlock diff --git a/test_files/test_simple_registry.py b/test_files/test_simple_registry.py index 2ab5dc2..1581271 100644 --- a/test_files/test_simple_registry.py +++ b/test_files/test_simple_registry.py @@ -89,7 +89,7 @@ def test_reference_text(simple_registry): citation = Citation("Bivort2016", "", "") assert ( simple_registry.reference_text(citation) - == "Benjamin L. De Bivort and Bruno Van Swinderen. Evidence for selective attention in the insect brain. *Current Opinion in Insect Science*, 15:1–7, 2016. [doi:10.1016/j.cois.2016.02.007](https://doi.org/10.1016/j.cois.2016.02.007)." + == "Benjamin L. De Bivort and Bruno Van Swinderen. Evidence for selective attention in the insect brain. *Current Opinion in Insect Science*, 15:1–7, 2016. [doi:10.1016/j.cois.2016.02.007](https://doi.org/10.1016/j.cois.2016.02.007)." # noqa: E501 ) # Test citation with URL From aefacf1ace72532d347610ed78fd1e8dad39cb49 Mon Sep 17 00:00:00 2001 From: shyamd <16827130+shyamd@users.noreply.github.com> Date: Tue, 14 Jan 2025 16:14:13 -0800 Subject: [PATCH 31/58] remove stale print --- src/mkdocs_bibtex/plugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mkdocs_bibtex/plugin.py b/src/mkdocs_bibtex/plugin.py index 0425a53..9539c57 100644 --- a/src/mkdocs_bibtex/plugin.py +++ b/src/mkdocs_bibtex/plugin.py @@ -105,7 +105,7 @@ def on_page_markdown(self, markdown, page, config, files): # 2. Validate the cite blocks self.registry.validate_citation_blocks(cite_blocks) - print(cite_blocks) + # 3. Replace the cite blocks with the inline citations for block in cite_blocks: replacement = self.registry.inline_text(block) From 9eadc149bb004df308ca6efad0b6019b05f60871 Mon Sep 17 00:00:00 2001 From: shyamd <16827130+shyamd@users.noreply.github.com> Date: Tue, 14 Jan 2025 16:14:25 -0800 Subject: [PATCH 32/58] update min mkdocs version --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index b40f911..989679d 100644 --- a/setup.py +++ b/setup.py @@ -17,7 +17,7 @@ license="BSD-3-Clause-LBNL", python_requires=">=3.6", install_requires=[ - "mkdocs>=1", + "mkdocs>=1.2", "pybtex>=0.22", "pypandoc>=1.5", "requests>=2.8.1", From 2ba49f12581ff61486d22dca11e28cced69fb817 Mon Sep 17 00:00:00 2001 From: shyamd <16827130+shyamd@users.noreply.github.com> Date: Tue, 14 Jan 2025 16:14:36 -0800 Subject: [PATCH 33/58] Add more logging --- src/mkdocs_bibtex/registry.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/mkdocs_bibtex/registry.py b/src/mkdocs_bibtex/registry.py index a7737f4..238da03 100644 --- a/src/mkdocs_bibtex/registry.py +++ b/src/mkdocs_bibtex/registry.py @@ -130,7 +130,8 @@ def _process_with_pandoc(citation_blocks: list[CitationBlock], bib_data: str, cs citation_map = {index: block for index, block in enumerate(citation_blocks)} full_doc += "\n\n".join(f"{index}. {block}" for index, block in citation_map.items()) full_doc += "\n\n# References\n\n" - + log.debug("Converting with pandoc") + log.debug(f"Full doc: {full_doc}") with tempfile.TemporaryDirectory() as tmpdir: bib_path = Path(tmpdir).joinpath("temp.bib") with open(bib_path, "wt", encoding="utf-8") as bibfile: @@ -138,12 +139,13 @@ def _process_with_pandoc(citation_blocks: list[CitationBlock], bib_data: str, cs args = ["--citeproc", "--bibliography", str(bib_path), "--csl", csl_file] markdown = pypandoc.convert_text(source=full_doc, to="markdown-citations", format="markdown", extra_args=args) - + + log.debug(f"Pandoc output: {markdown}") try: splits = markdown.split("# References") inline_citations, references = splits[0], splits[1] except IndexError: - print(markdown) + raise ValueError("Failed to parse pandoc output") # Parse inline citations From de62b6e10886da82c32a408813381fa7ddb080a3 Mon Sep 17 00:00:00 2001 From: shyamd <16827130+shyamd@users.noreply.github.com> Date: Tue, 14 Jan 2025 16:15:38 -0800 Subject: [PATCH 34/58] Remove stale uilts --- src/mkdocs_bibtex/utils.py | 283 ------------------------------------- test_files/test_utils.py | 64 --------- 2 files changed, 347 deletions(-) delete mode 100644 test_files/test_utils.py diff --git a/src/mkdocs_bibtex/utils.py b/src/mkdocs_bibtex/utils.py index ccbadc6..1550c07 100644 --- a/src/mkdocs_bibtex/utils.py +++ b/src/mkdocs_bibtex/utils.py @@ -1,293 +1,10 @@ import logging -import re import requests import tempfile -from collections import OrderedDict -from functools import lru_cache -from itertools import groupby -from pathlib import Path -from packaging.version import Version - -import mkdocs -import pypandoc - -from pybtex.backends.markdown import Backend as MarkdownBackend -from pybtex.database import BibliographyData -from pybtex.style.formatting.plain import Style as PlainStyle # Grab a logger log = logging.getLogger("mkdocs.plugins.mkdocs-bibtex") -# Add the warning filter only if the version is lower than 1.2 -# Filter doesn't do anything since that version -MKDOCS_LOG_VERSION = "1.2" -if Version(mkdocs.__version__) < Version(MKDOCS_LOG_VERSION): - from mkdocs.utils import warning_filter - - log.addFilter(warning_filter) - - -def format_simple(entries): - """ - Format the entries using a simple built in style - - Args: - entries (dict): dictionary of entries - - Returns: - references (dict): dictionary of citation texts - """ - style = PlainStyle() - backend = MarkdownBackend() - citations = OrderedDict() - for key, entry in entries.items(): - log.debug(f"Converting bibtex entry {key!r} without pandoc") - formatted_entry = style.format_entry("", entry) - entry_text = formatted_entry.text.render(backend) - entry_text = entry_text.replace("\n", " ") - # Local reference list for this file - citations[key] = entry_text.replace("\\(", "(").replace("\\)", ")").replace("\\.", ".") - log.debug(f"SUCCESS Converting bibtex entry {key!r} without pandoc") - return citations - - -def format_pandoc(entries, csl_path): - """ - Format the entries using pandoc - - Args: - entries (dict): dictionary of entries - csl_path (str): path to formatting CSL Fle - Returns: - references (dict): dictionary of citation texts - """ - pandoc_version = tuple(int(ver) for ver in pypandoc.get_pandoc_version().split(".")) - citations = OrderedDict() - is_new_pandoc = pandoc_version >= (2, 11) - msg = "pandoc>=2.11" if is_new_pandoc else "pandoc<2.11" - for key, entry in entries.items(): - bibtex_string = BibliographyData(entries={entry.key: entry}).to_string("bibtex") - log.debug(f"--Converting bibtex entry {key!r} with CSL file {csl_path!r} using {msg}") - if is_new_pandoc: - citations[key] = _convert_pandoc_new(bibtex_string, csl_path) - else: - citations[key] = _convert_pandoc_legacy(bibtex_string, csl_path) - log.debug(f"--SUCCESS Converting bibtex entry {key!r} with CSL file {csl_path!r} using {msg}") - - return citations - - -def _convert_pandoc_new(bibtex_string, csl_path): - """ - Converts the PyBtex entry into formatted markdown citation text - using pandoc version 2.11 or newer - """ - markdown = pypandoc.convert_text( - source=bibtex_string, - to="markdown_strict", - format="bibtex", - extra_args=[ - "--citeproc", - "--csl", - csl_path, - ], - ) - - markdown = " ".join(markdown.split("\n")) - # Remove newlines from any generated span tag (non-capitalized words) - markdown = re.compile(r"<\/span>[\r\n]").sub(" ", markdown) - - citation_regex = re.compile(r"(.+?)(?=<\/span>)<\/span>") - try: - citation = citation_regex.findall(re.sub(r"(\r|\n)", "", markdown))[1] - except IndexError: - citation = markdown - return citation.strip() - - -@lru_cache(maxsize=1024) -def _convert_pandoc_citekey(bibtex_string, csl_path, fullcite): - """ - Uses pandoc to convert a markdown citation key reference - to a rendered markdown citation in the given CSL format. - - Limitation (atleast for harvard.csl): multiple citekeys - REQUIRE a '; ' separator to render correctly: - - [see @test; @test2] Works - - [see @test and @test2] Doesn't work - """ - with tempfile.TemporaryDirectory() as tmpdir: - bib_path = Path(tmpdir).joinpath("temp.bib") - with open(bib_path, "wt", encoding="utf-8") as bibfile: - bibfile.write(bibtex_string) - - log.debug( - f"----Converting pandoc citation key {fullcite!r} with CSL file {csl_path!r} and Bibliography file" - f" '{bib_path!s}'..." - ) - markdown = pypandoc.convert_text( - source=fullcite, - to="markdown-citations", - format="markdown", - extra_args=["--citeproc", "--csl", csl_path, "--bibliography", bib_path], - ) - log.debug( - f"----SUCCESS Converting pandoc citation key {fullcite!r} with CSL file {csl_path!r} and " - f"Bibliography file '{bib_path!s}'" - ) - - # Return only the citation text (first line(s)) - # remove any extra linebreaks to accommodate large author names - markdown = re.compile(r"[\r\n]").sub("", markdown) - return markdown.split(":::")[0].strip() - - -def _convert_pandoc_legacy(bibtex_string, csl_path): - """ - Converts the PyBtex entry into formatted markdown citation text - using pandoc version older than 2.11 - """ - with tempfile.TemporaryDirectory() as tmpdir: - bib_path = Path(tmpdir).joinpath("temp.bib") - with open(bib_path, "wt", encoding="utf-8") as bibfile: - bibfile.write(bibtex_string) - citation_text = """ ---- -nocite: '@*' ---- -""" - - markdown = pypandoc.convert_text( - source=citation_text, - to="markdown_strict", - format="md", - extra_args=["--csl", csl_path, "--bibliography", bib_path], - filters=["pandoc-citeproc"], - ) - - citation_regex = re.compile(r"[\d\.\\\s]*(.*)") - citation = citation_regex.findall(markdown.replace("\n", " "))[0] - return citation.strip() - - -def extract_cite_keys(cite_block): - """ - Extract just the keys from a citation block - """ - cite_regex = re.compile(r"@([\w\.:-]*)") - cite_keys = re.findall(cite_regex, cite_block) - - return cite_keys - - -def find_cite_blocks(markdown): - """ - Finds entire cite blocks in the markdown text - - Args: - markdown (str): the markdown text to be extract citation - blocks from - - regex explanation: - - first group (1): everything. (the only thing we need) - - second group (2): (?:(?:\[(-{0,1}[^@]*)) |\[(?=-{0,1}@)) - - third group (3): ((?:-{0,1}@\w*(?:; ){0,1})+) - - fourth group (4): (?:[^\]\n]{0,1} {0,1})([^\]\n]*) - - The first group captures the entire cite block, as is - The second group captures the prefix, which is everything between '[' and ' @| -@' - The third group captures the citekey(s), ';' separated (affixes NOT supported) - The fourth group captures anything after the citekeys, excluding the leading whitespace - (The non-capturing group removes any symbols or whitespaces between the citekey and suffix) - - Matches for [see @author; @doe my suffix here] - [0] entire block: '[see @author; @doe my suffix here]' - [1] prefix: 'see' - [2] citekeys: '@author; @doe' - [3] suffix: 'my suffix here' - - Does NOT match: [mail@example.com] - DOES match [mail @example.com] as [mail][@example][com] - """ - r = r"((?:(?:\[(-{0,1}[^@]*)) |\[(?=-{0,1}@))((?:-{0,1}@\w*(?:; ){0,1})+)(?:[^\]\n]{0,1} {0,1})([^\]\n]*)\])" - cite_regex = re.compile(r) - - citation_blocks = [ - # We only care about the block (group 1) - (matches.group(1)) - for matches in re.finditer(cite_regex, markdown) - ] - - return citation_blocks - - -def insert_citation_keys(citation_quads, markdown, csl=False, bib=False): - """ - Insert citations into the markdown text replacing - the old citation keys - - Args: - citation_quads (tuple): a quad tuple of all citation info - markdown (str): the markdown text to modify - - Returns: - markdown (str): the modified Markdown - """ - - log.debug("Replacing citation keys with the generated ones...") - - # Renumber quads if using numbers for citation links - - grouped_quads = [list(g) for _, g in groupby(citation_quads, key=lambda x: x[0])] - for quad_group in grouped_quads: - full_citation = quad_group[0][0] # the full citation block - replacement_citaton = "".join(["[^{}]".format(quad[2]) for quad in quad_group]) - - # if cite_inline is true, convert full_citation with pandoc and add to replacement_citaton - if csl and bib: - log.debug(f"--Rendering citation inline for {full_citation!r}...") - # Verify that the pandoc installation is newer than 2.11 - pandoc_version = pypandoc.get_pandoc_version() - pandoc_version_tuple = tuple(int(ver) for ver in pandoc_version.split(".")) - if pandoc_version_tuple <= (2, 11): - raise RuntimeError( - f"Your version of pandoc (v{pandoc_version}) is incompatible with the cite_inline feature." - ) - - inline_citation = _convert_pandoc_citekey(bib, csl, full_citation) - replacement_citaton = f" {inline_citation}{replacement_citaton}" - - # Make sure inline citations doesn't get an extra whitespace by - # replacing it with whitespace added first - markdown = markdown.replace(f" {full_citation}", replacement_citaton) - log.debug(f"--SUCCESS Rendering citation inline for {full_citation!r}") - - markdown = markdown.replace(full_citation, replacement_citaton) - - log.debug("SUCCESS Replacing citation keys with the generated ones") - - return markdown - - -def format_bibliography(citation_quads): - """ - Generates a bibliography from the citation quads - - Args: - citation_quads (tuple): a quad tuple of all citation info - - Returns: - markdown (str): the Markdown string for the bibliography - """ - new_bib = {quad[2]: quad[3] for quad in citation_quads} - bibliography = [] - for key, citation in new_bib.items(): - bibliography_text = "[^{}]: {}".format(key, citation) - bibliography.append(bibliography_text) - - return "\n".join(bibliography) - - def tempfile_from_url(name, url, suffix): log.debug(f"Downloading {name} from URL {url} to temporary file...") for i in range(3): diff --git a/test_files/test_utils.py b/test_files/test_utils.py deleted file mode 100644 index 199ec8f..0000000 --- a/test_files/test_utils.py +++ /dev/null @@ -1,64 +0,0 @@ -import os - -import pytest - -from mkdocs_bibtex.utils import ( - find_cite_blocks, - format_simple, - format_pandoc, - extract_cite_keys, -) - -from pybtex.database import parse_file - -module_dir = os.path.dirname(os.path.abspath(__file__)) -test_files_dir = os.path.abspath(os.path.join(module_dir, "..", "test_files")) - - -@pytest.fixture -def entries(): - bibdata = parse_file(os.path.join(test_files_dir, "test.bib")) - return bibdata.entries - - -def test_find_cite_blocks(): - # Suppressed authors - assert find_cite_blocks("[-@test]") == ["[-@test]"] - # Affixes - assert find_cite_blocks("[see @test]") == ["[see @test]"] - assert find_cite_blocks("[@test, p. 15]") == ["[@test, p. 15]"] - assert find_cite_blocks("[see @test, p. 15]") == ["[see @test, p. 15]"] - assert find_cite_blocks("[see -@test, p. 15]") == ["[see -@test, p. 15]"] - # Invalid blocks - assert find_cite_blocks("[ @test]") is not True - # Citavi . format - assert find_cite_blocks("[@Bermudez.2020]") == ["[@Bermudez.2020]"] - - -def test_format_simple(entries): - citations = format_simple(entries) - - assert all(k in citations for k in entries) - assert all(entry != citations[k] for k, entry in entries.items()) - - assert citations["test"] == "First Author and Second Author. Test title. *Testing Journal*, 2019." - assert citations["test2"] == "First Author and Second Author. Test Title (TT). *Testing Journal (TJ)*, 2019." - - -def test_format_pandoc(entries): - citations = format_pandoc(entries, os.path.join(test_files_dir, "nature.csl")) - - assert all(k in citations for k in entries) - assert all(entry != citations[k] for k, entry in entries.items()) - - assert citations["test"] == "Author, F. & Author, S. Test title. *Testing Journal* **1**, (2019)." - assert citations["test2"] == "Author, F. & Author, S. Test Title (TT). *Testing Journal (TJ)* **1**, (2019)." - - -def test_extract_cite_key(): - """ - Test to ensure the extract regex can handle all bibtex keys - TODO: Make this fully compliant with bibtex keys allowed characters - """ - assert extract_cite_keys("[@test]") == ["test"] - assert extract_cite_keys("[@test.3]") == ["test.3"] From d10106d2825241d886148f95b21f4a88965aae36 Mon Sep 17 00:00:00 2001 From: shyamd <16827130+shyamd@users.noreply.github.com> Date: Tue, 14 Jan 2025 16:17:10 -0800 Subject: [PATCH 35/58] lint --- src/mkdocs_bibtex/plugin.py | 2 +- src/mkdocs_bibtex/registry.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/mkdocs_bibtex/plugin.py b/src/mkdocs_bibtex/plugin.py index 9539c57..dc9937c 100644 --- a/src/mkdocs_bibtex/plugin.py +++ b/src/mkdocs_bibtex/plugin.py @@ -105,7 +105,7 @@ def on_page_markdown(self, markdown, page, config, files): # 2. Validate the cite blocks self.registry.validate_citation_blocks(cite_blocks) - + # 3. Replace the cite blocks with the inline citations for block in cite_blocks: replacement = self.registry.inline_text(block) diff --git a/src/mkdocs_bibtex/registry.py b/src/mkdocs_bibtex/registry.py index 238da03..7234a5d 100644 --- a/src/mkdocs_bibtex/registry.py +++ b/src/mkdocs_bibtex/registry.py @@ -139,7 +139,7 @@ def _process_with_pandoc(citation_blocks: list[CitationBlock], bib_data: str, cs args = ["--citeproc", "--bibliography", str(bib_path), "--csl", csl_file] markdown = pypandoc.convert_text(source=full_doc, to="markdown-citations", format="markdown", extra_args=args) - + log.debug(f"Pandoc output: {markdown}") try: splits = markdown.split("# References") From aa559bcd7b28d45e7d2495a05319231c4264ea83 Mon Sep 17 00:00:00 2001 From: shyamd <16827130+shyamd@users.noreply.github.com> Date: Tue, 14 Jan 2025 21:28:14 -0800 Subject: [PATCH 36/58] fix full bibliography --- src/mkdocs_bibtex/plugin.py | 23 ++++++++++------------- test_files/test_integration.py | 12 ++++++++++++ 2 files changed, 22 insertions(+), 13 deletions(-) diff --git a/src/mkdocs_bibtex/plugin.py b/src/mkdocs_bibtex/plugin.py index dc9937c..4879e75 100644 --- a/src/mkdocs_bibtex/plugin.py +++ b/src/mkdocs_bibtex/plugin.py @@ -1,4 +1,3 @@ -import re import time import validators from collections import OrderedDict @@ -129,21 +128,19 @@ def on_page_markdown(self, markdown, page, config, files): except Exception as e: log.warning(f"Error formatting citation {citation.key}: {e}") bibliography = "\n".join(bibliography) - markdown = re.sub( - re.escape(bib_command), - bibliography, - markdown, - ) + markdown = markdown.replace(bib_command, bibliography) + # 5. Build the full Bibliography and insert into the text full_bib_command = self.config.full_bib_command - all_citations = [Citation(key=key) for key in self.registry.bib_data.entries] - full_bibliography = [] - for citation in all_citations: - full_bibliography.append("[^{}]: {}".format(citation.key, self.registry.reference_text(citation))) - full_bibliography = "\n".join(full_bibliography) + if markdown.count(full_bib_command) > 0: + all_citations = [Citation(key=key) for key in self.registry.bib_data.entries] + full_bibliography = [] + for citation in all_citations: + full_bibliography.append("[^{}]: {}".format(citation.key, self.registry.reference_text(citation))) + full_bibliography = "\n".join(full_bibliography) + markdown = markdown.replace(full_bib_command, full_bibliography) + - markdown = markdown.replace(bib_command, bibliography) - markdown = markdown.replace(full_bib_command, full_bibliography) return markdown diff --git a/test_files/test_integration.py b/test_files/test_integration.py index 6a39042..529f5f4 100644 --- a/test_files/test_integration.py +++ b/test_files/test_integration.py @@ -145,3 +145,15 @@ def test_invalid_citations(plugin): result = plugin.on_page_markdown(markdown, None, None, None) # assert "[@nonexistent]" in result # Invalid citation should remain unchanged assert "[^nonexistent]" not in result + + +def test_full_bib_command(plugin): + """Test full bibliography command""" + markdown = "Full bibliography [@test]\n\n\\full_bibliography" + result = plugin.on_page_markdown(markdown, None, None, None) + print(result) + assert "Full bibliography [^test]" in result + assert "[^test]:" in result + assert "[^test2]:" in result + assert "[^Bivort2016]:" in result + assert "[^test_citavi]:" in result From f2c817c520728b30951cb7f5438c2f31fa0a86ac Mon Sep 17 00:00:00 2001 From: shyamd <16827130+shyamd@users.noreply.github.com> Date: Wed, 15 Jan 2025 09:38:48 -0800 Subject: [PATCH 37/58] ensure only one bibliography per page --- src/mkdocs_bibtex/plugin.py | 2 +- test_files/test_integration.py | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/mkdocs_bibtex/plugin.py b/src/mkdocs_bibtex/plugin.py index 4879e75..ccb9bc0 100644 --- a/src/mkdocs_bibtex/plugin.py +++ b/src/mkdocs_bibtex/plugin.py @@ -113,7 +113,7 @@ def on_page_markdown(self, markdown, page, config, files): # 4. Insert in the bibliopgrahy text into the markdown bib_command = self.config.bib_command - if self.config.bib_by_default: + if self.config.bib_by_default and markdown.count(bib_command) == 0: markdown += f"\n{bib_command}" citations = OrderedDict() diff --git a/test_files/test_integration.py b/test_files/test_integration.py index 529f5f4..2206190 100644 --- a/test_files/test_integration.py +++ b/test_files/test_integration.py @@ -157,3 +157,17 @@ def test_full_bib_command(plugin): assert "[^test2]:" in result assert "[^Bivort2016]:" in result assert "[^test_citavi]:" in result + + +def test_bib_by_default(plugin): + """Test bib_by_default behavior""" + markdown = "Citation [@test]" + plugin.config.bib_by_default = False + result = plugin.on_page_markdown(markdown, None, None, None) + assert "[^test]:" not in result + + plugin.config.bib_by_default = True + result = plugin.on_page_markdown(markdown, None, None, None) + assert "[^test]:" in result + + From 9da07a8e63d96a71b90857c53ab9310deb132be2 Mon Sep 17 00:00:00 2001 From: shyamd <16827130+shyamd@users.noreply.github.com> Date: Wed, 15 Jan 2025 15:51:48 -0800 Subject: [PATCH 38/58] fix nocite effects --- src/mkdocs_bibtex/registry.py | 2 -- test_files/test_integration.py | 2 +- test_files/test_pandoc_registry.py | 2 +- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/mkdocs_bibtex/registry.py b/src/mkdocs_bibtex/registry.py index 7234a5d..9d32005 100644 --- a/src/mkdocs_bibtex/registry.py +++ b/src/mkdocs_bibtex/registry.py @@ -123,8 +123,6 @@ def _process_with_pandoc(citation_blocks: list[CitationBlock], bib_data: str, cs --- title: "Test" link-citations: false -nocite: | - @* --- """ citation_map = {index: block for index, block in enumerate(citation_blocks)} diff --git a/test_files/test_integration.py b/test_files/test_integration.py index 2206190..2afdd1b 100644 --- a/test_files/test_integration.py +++ b/test_files/test_integration.py @@ -70,7 +70,7 @@ def test_pandoc_citation_rendering(pandoc_plugin): print(pandoc_plugin.registry._inline_cache) print(result) # Check inline citations - assert "(Author and Author 2019a)" in result + assert "(Author and Author 2019)" in result assert "(De Bivort and Van Swinderen 2016)" in result # Check bibliography formatting diff --git a/test_files/test_pandoc_registry.py b/test_files/test_pandoc_registry.py index b11ab84..5990dce 100644 --- a/test_files/test_pandoc_registry.py +++ b/test_files/test_pandoc_registry.py @@ -147,7 +147,7 @@ def test_pandoc_formatting(registry): block = CitationBlock([citation]) registry.validate_citation_blocks([block]) text = registry.reference_text(citation) - assert text == "Author F, Author S (2019a) Test title. Testing Journal 1:" + assert text == "Author F, Author S (2019) Test title. Testing Journal 1:" def test_multiple_citation_blocks(registry): From 0fba7365d0b75bae71f8c848076722e3e4839b01 Mon Sep 17 00:00:00 2001 From: shyamd <16827130+shyamd@users.noreply.github.com> Date: Wed, 15 Jan 2025 15:52:38 -0800 Subject: [PATCH 39/58] add more logging --- src/mkdocs_bibtex/plugin.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/mkdocs_bibtex/plugin.py b/src/mkdocs_bibtex/plugin.py index ccb9bc0..2e9c4d8 100644 --- a/src/mkdocs_bibtex/plugin.py +++ b/src/mkdocs_bibtex/plugin.py @@ -110,12 +110,13 @@ def on_page_markdown(self, markdown, page, config, files): replacement = self.registry.inline_text(block) markdown = markdown.replace(str(block), replacement) - # 4. Insert in the bibliopgrahy text into the markdown + #4a. Esnure we have a bibliography if desired bib_command = self.config.bib_command if self.config.bib_by_default and markdown.count(bib_command) == 0: markdown += f"\n{bib_command}" + # 4. Insert in the bibliopgrahy text into the markdown citations = OrderedDict() for block in cite_blocks: for citation in block.citations: @@ -134,6 +135,7 @@ def on_page_markdown(self, markdown, page, config, files): # 5. Build the full Bibliography and insert into the text full_bib_command = self.config.full_bib_command if markdown.count(full_bib_command) > 0: + log.info("Building full bibliography") all_citations = [Citation(key=key) for key in self.registry.bib_data.entries] full_bibliography = [] for citation in all_citations: @@ -141,6 +143,6 @@ def on_page_markdown(self, markdown, page, config, files): full_bibliography = "\n".join(full_bibliography) markdown = markdown.replace(full_bib_command, full_bibliography) - + log.debug(f"Markdown: \n{markdown}") return markdown From 3470d41233a02f2b6b303f9cb04b2958fa76b463 Mon Sep 17 00:00:00 2001 From: shyamd <16827130+shyamd@users.noreply.github.com> Date: Wed, 15 Jan 2025 15:58:46 -0800 Subject: [PATCH 40/58] more logging --- src/mkdocs_bibtex/registry.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/mkdocs_bibtex/registry.py b/src/mkdocs_bibtex/registry.py index 9d32005..81d3882 100644 --- a/src/mkdocs_bibtex/registry.py +++ b/src/mkdocs_bibtex/registry.py @@ -182,4 +182,6 @@ def _process_with_pandoc(citation_blocks: list[CitationBlock], bib_data: str, cs citation = match.group("citation").replace("\n", " ").strip() reference_cache[key] = citation + log.debug(f"Inline cache: {inline_cache}") + log.debug(f"Reference cache: {reference_cache}") return inline_cache, reference_cache From 5310e393bc4c1f99b19e13d92c75a6a837b6ab2b Mon Sep 17 00:00:00 2001 From: shyamd <16827130+shyamd@users.noreply.github.com> Date: Wed, 15 Jan 2025 16:00:29 -0800 Subject: [PATCH 41/58] fix full bibliography --- src/mkdocs_bibtex/plugin.py | 2 ++ test_files/test_integration.py | 10 ++++++++++ 2 files changed, 12 insertions(+) diff --git a/src/mkdocs_bibtex/plugin.py b/src/mkdocs_bibtex/plugin.py index 2e9c4d8..b48f854 100644 --- a/src/mkdocs_bibtex/plugin.py +++ b/src/mkdocs_bibtex/plugin.py @@ -137,6 +137,8 @@ def on_page_markdown(self, markdown, page, config, files): if markdown.count(full_bib_command) > 0: log.info("Building full bibliography") all_citations = [Citation(key=key) for key in self.registry.bib_data.entries] + blocks = [CitationBlock(citations=[cite]) for cite in all_citations] + self.registry.validate_citation_blocks(blocks) full_bibliography = [] for citation in all_citations: full_bibliography.append("[^{}]: {}".format(citation.key, self.registry.reference_text(citation))) diff --git a/test_files/test_integration.py b/test_files/test_integration.py index 2afdd1b..bd19416 100644 --- a/test_files/test_integration.py +++ b/test_files/test_integration.py @@ -171,3 +171,13 @@ def test_bib_by_default(plugin): assert "[^test]:" in result +def test_full_bib_command_with_pandoc(pandoc_plugin): + """Test full bibliography command with Pandoc""" + markdown = "Full bibliography\n\n\\full_bibliography" + result = pandoc_plugin.on_page_markdown(markdown, None, None, None) + + assert "[^test]: Author F, Author S (2019a)" in result + assert "[^test2]: Author F, Author S (2019b)" in result + assert "[^Bivort2016]: De Bivort BL, Van Swinderen B (2016)" in result + assert "[^test_citavi]: Author F, Author S (2019c)" in result + From 9c92347b701324bd6b2d1979ba1bb9aa46e52405 Mon Sep 17 00:00:00 2001 From: shyamd <16827130+shyamd@users.noreply.github.com> Date: Wed, 15 Jan 2025 16:01:26 -0800 Subject: [PATCH 42/58] remove stale prints --- test_files/test_integration.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/test_files/test_integration.py b/test_files/test_integration.py index bd19416..cf22930 100644 --- a/test_files/test_integration.py +++ b/test_files/test_integration.py @@ -67,8 +67,7 @@ def test_pandoc_citation_rendering(pandoc_plugin): """Test citation rendering with Pandoc and CSL""" markdown = "Here is a citation [@test] and another [@Bivort2016].\n\n\\bibliography" result = pandoc_plugin.on_page_markdown(markdown, None, None, None) - print(pandoc_plugin.registry._inline_cache) - print(result) + # Check inline citations assert "(Author and Author 2019)" in result assert "(De Bivort and Van Swinderen 2016)" in result @@ -151,7 +150,7 @@ def test_full_bib_command(plugin): """Test full bibliography command""" markdown = "Full bibliography [@test]\n\n\\full_bibliography" result = plugin.on_page_markdown(markdown, None, None, None) - print(result) + assert "Full bibliography [^test]" in result assert "[^test]:" in result assert "[^test2]:" in result From c3601f0a1ea0eb216d70e9ba125f27faebd66862 Mon Sep 17 00:00:00 2001 From: shyamd <16827130+shyamd@users.noreply.github.com> Date: Wed, 15 Jan 2025 16:05:33 -0800 Subject: [PATCH 43/58] move util function into registry --- src/mkdocs_bibtex/registry.py | 128 +++++++++++++++++----------------- 1 file changed, 63 insertions(+), 65 deletions(-) diff --git a/src/mkdocs_bibtex/registry.py b/src/mkdocs_bibtex/registry.py index 81d3882..63102a5 100644 --- a/src/mkdocs_bibtex/registry.py +++ b/src/mkdocs_bibtex/registry.py @@ -105,83 +105,81 @@ def validate_citation_blocks(self, citation_blocks: list[CitationBlock]) -> None log.warning(f"Citing unknown reference key {citation.key}") # Pre-Process with appropriate pandoc version - self._inline_cache, self._reference_cache = _process_with_pandoc( - citation_blocks, self.bib_data_bibtex, self.csl_file - ) + self._inline_cache, self._reference_cache = self._process_with_pandoc(citation_blocks) @property def bib_data_bibtex(self) -> str: """Convert bibliography data to BibTeX format""" return self.bib_data.to_string("bibtex") + def _process_with_pandoc(self, citation_blocks: list[CitationBlock]) -> tuple[dict, dict]: + """Process citations with pandoc""" -def _process_with_pandoc(citation_blocks: list[CitationBlock], bib_data: str, csl_file: str) -> tuple[dict, dict]: - """Process citations with pandoc""" - - # Build the document pandoc can process and we can parse to extract inline citations and reference text - full_doc = """ + # Build the document pandoc can process and we can parse to extract inline citations and reference text + full_doc = """ --- -title: "Test" link-citations: false --- + """ - citation_map = {index: block for index, block in enumerate(citation_blocks)} - full_doc += "\n\n".join(f"{index}. {block}" for index, block in citation_map.items()) - full_doc += "\n\n# References\n\n" - log.debug("Converting with pandoc") - log.debug(f"Full doc: {full_doc}") - with tempfile.TemporaryDirectory() as tmpdir: - bib_path = Path(tmpdir).joinpath("temp.bib") - with open(bib_path, "wt", encoding="utf-8") as bibfile: - bibfile.write(bib_data) - - args = ["--citeproc", "--bibliography", str(bib_path), "--csl", csl_file] - markdown = pypandoc.convert_text(source=full_doc, to="markdown-citations", format="markdown", extra_args=args) - - log.debug(f"Pandoc output: {markdown}") - try: - splits = markdown.split("# References") - inline_citations, references = splits[0], splits[1] - except IndexError: - - raise ValueError("Failed to parse pandoc output") - - # Parse inline citations - inline_citations = inline_citations.strip() - - # Use regex to match numbered entries, handling multi-line citations - citation_pattern = re.compile(r"(\d+)\.\s+(.*?)(?=(?:\n\d+\.|$))", re.DOTALL) - matches = citation_pattern.finditer(inline_citations) - - # Create a dictionary of cleaned citations (removing extra whitespace and newlines) - inline_citations = {int(match.group(1)): " ".join(match.group(2).split()) for match in matches} - - inline_cache = {str(citation_map[index]): citation for index, citation in inline_citations.items()} - - # Parse references - reference_cache = {} - - # Pattern for format with .csl-left-margin and .csl-right-inline - pattern1 = r"::: \{#ref-(?P[^\s]+) .csl-entry\}\n\[.*?\]\{\.csl-left-margin\}\[(?P.*?)\]\{\.csl-right-inline\}" # noqa: E501 - - # Pattern for simple reference format - pattern2 = r"::: \{#ref-(?P[^\s]+) .csl-entry\}\n(?P.*?)(?=:::|$)" - - # Try first pattern - matches1 = re.finditer(pattern1, references, re.DOTALL) - for match in matches1: - key = match.group("key").strip() - citation = match.group("citation").replace("\n", " ").strip() - reference_cache[key] = citation - - # If no matches found, try second pattern - if not reference_cache: - matches2 = re.finditer(pattern2, references, re.DOTALL) - for match in matches2: + citation_map = {index: block for index, block in enumerate(citation_blocks)} + full_doc += "\n\n".join(f"{index}. {block}" for index, block in citation_map.items()) + full_doc += "\n\n# References\n\n" + log.debug("Converting with pandoc") + log.debug(f"Full doc: {full_doc}") + with tempfile.TemporaryDirectory() as tmpdir: + bib_path = Path(tmpdir).joinpath("temp.bib") + with open(bib_path, "wt", encoding="utf-8") as bibfile: + bibfile.write(self.bib_data_bibtex) + + args = ["--citeproc", "--bibliography", str(bib_path), "--csl", self.csl_file] + markdown = pypandoc.convert_text( + source=full_doc, to="markdown-citations", format="markdown", extra_args=args + ) + + log.debug(f"Pandoc output: {markdown}") + try: + splits = markdown.split("# References") + inline_citations, references = splits[0], splits[1] + except IndexError: + raise ValueError("Failed to parse pandoc output") + + # Parse inline citations + inline_citations = inline_citations.strip() + + # Use regex to match numbered entries, handling multi-line citations + citation_pattern = re.compile(r"(\d+)\.\s+(.*?)(?=(?:\n\d+\.|$))", re.DOTALL) + matches = citation_pattern.finditer(inline_citations) + + # Create a dictionary of cleaned citations (removing extra whitespace and newlines) + inline_citations = {int(match.group(1)): " ".join(match.group(2).split()) for match in matches} + + inline_cache = {str(citation_map[index]): citation for index, citation in inline_citations.items()} + + # Parse references + reference_cache = {} + + # Pattern for format with .csl-left-margin and .csl-right-inline + pattern1 = r"::: \{#ref-(?P[^\s]+) .csl-entry\}\n\[.*?\]\{\.csl-left-margin\}\[(?P.*?)\]\{\.csl-right-inline\}" # noqa: E501 + + # Pattern for simple reference format + pattern2 = r"::: \{#ref-(?P[^\s]+) .csl-entry\}\n(?P.*?)(?=:::|$)" + + # Try first pattern + matches1 = re.finditer(pattern1, references, re.DOTALL) + for match in matches1: key = match.group("key").strip() citation = match.group("citation").replace("\n", " ").strip() reference_cache[key] = citation - log.debug(f"Inline cache: {inline_cache}") - log.debug(f"Reference cache: {reference_cache}") - return inline_cache, reference_cache + # If no matches found, try second pattern + if not reference_cache: + matches2 = re.finditer(pattern2, references, re.DOTALL) + for match in matches2: + key = match.group("key").strip() + citation = match.group("citation").replace("\n", " ").strip() + reference_cache[key] = citation + + log.debug(f"Inline cache: {inline_cache}") + log.debug(f"Reference cache: {reference_cache}") + return inline_cache, reference_cache From e88d8edfca493d6eef2d67460df5abd7f926f57a Mon Sep 17 00:00:00 2001 From: shyamd <16827130+shyamd@users.noreply.github.com> Date: Wed, 15 Jan 2025 16:05:44 -0800 Subject: [PATCH 44/58] clean up formatting --- src/mkdocs_bibtex/plugin.py | 3 +-- src/mkdocs_bibtex/utils.py | 1 + test_files/test_integration.py | 3 +-- test_files/test_simple_registry.py | 2 +- 4 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/mkdocs_bibtex/plugin.py b/src/mkdocs_bibtex/plugin.py index b48f854..46f3a36 100644 --- a/src/mkdocs_bibtex/plugin.py +++ b/src/mkdocs_bibtex/plugin.py @@ -110,7 +110,7 @@ def on_page_markdown(self, markdown, page, config, files): replacement = self.registry.inline_text(block) markdown = markdown.replace(str(block), replacement) - #4a. Esnure we have a bibliography if desired + # 4a. Esnure we have a bibliography if desired bib_command = self.config.bib_command if self.config.bib_by_default and markdown.count(bib_command) == 0: @@ -131,7 +131,6 @@ def on_page_markdown(self, markdown, page, config, files): bibliography = "\n".join(bibliography) markdown = markdown.replace(bib_command, bibliography) - # 5. Build the full Bibliography and insert into the text full_bib_command = self.config.full_bib_command if markdown.count(full_bib_command) > 0: diff --git a/src/mkdocs_bibtex/utils.py b/src/mkdocs_bibtex/utils.py index 1550c07..6d52af4 100644 --- a/src/mkdocs_bibtex/utils.py +++ b/src/mkdocs_bibtex/utils.py @@ -5,6 +5,7 @@ # Grab a logger log = logging.getLogger("mkdocs.plugins.mkdocs-bibtex") + def tempfile_from_url(name, url, suffix): log.debug(f"Downloading {name} from URL {url} to temporary file...") for i in range(3): diff --git a/test_files/test_integration.py b/test_files/test_integration.py index cf22930..fe7d27a 100644 --- a/test_files/test_integration.py +++ b/test_files/test_integration.py @@ -67,7 +67,7 @@ def test_pandoc_citation_rendering(pandoc_plugin): """Test citation rendering with Pandoc and CSL""" markdown = "Here is a citation [@test] and another [@Bivort2016].\n\n\\bibliography" result = pandoc_plugin.on_page_markdown(markdown, None, None, None) - + # Check inline citations assert "(Author and Author 2019)" in result assert "(De Bivort and Van Swinderen 2016)" in result @@ -179,4 +179,3 @@ def test_full_bib_command_with_pandoc(pandoc_plugin): assert "[^test2]: Author F, Author S (2019b)" in result assert "[^Bivort2016]: De Bivort BL, Van Swinderen B (2016)" in result assert "[^test_citavi]: Author F, Author S (2019c)" in result - diff --git a/test_files/test_simple_registry.py b/test_files/test_simple_registry.py index 1581271..7c02e08 100644 --- a/test_files/test_simple_registry.py +++ b/test_files/test_simple_registry.py @@ -89,7 +89,7 @@ def test_reference_text(simple_registry): citation = Citation("Bivort2016", "", "") assert ( simple_registry.reference_text(citation) - == "Benjamin L. De Bivort and Bruno Van Swinderen. Evidence for selective attention in the insect brain. *Current Opinion in Insect Science*, 15:1–7, 2016. [doi:10.1016/j.cois.2016.02.007](https://doi.org/10.1016/j.cois.2016.02.007)." # noqa: E501 + == "Benjamin L. De Bivort and Bruno Van Swinderen. Evidence for selective attention in the insect brain. *Current Opinion in Insect Science*, 15:1–7, 2016. [doi:10.1016/j.cois.2016.02.007](https://doi.org/10.1016/j.cois.2016.02.007)." # noqa: E501 ) # Test citation with URL From cd41241e9d1d3da15190b5f07792e20a488861b8 Mon Sep 17 00:00:00 2001 From: shyamd <16827130+shyamd@users.noreply.github.com> Date: Wed, 15 Jan 2025 16:05:55 -0800 Subject: [PATCH 45/58] add example --- .gitignore | 3 + example/docs/full_bib.md | 3 + example/docs/index.md | 24 +++ example/mkdocs.yml | 16 ++ example/nature.csl | 132 ++++++++++++++ example/refs.bib | 41 +++++ example/springer-basic-author-date.csl | 239 +++++++++++++++++++++++++ 7 files changed, 458 insertions(+) create mode 100644 example/docs/full_bib.md create mode 100644 example/docs/index.md create mode 100644 example/mkdocs.yml create mode 100644 example/nature.csl create mode 100644 example/refs.bib create mode 100644 example/springer-basic-author-date.csl diff --git a/.gitignore b/.gitignore index 894a44c..f73d5e3 100644 --- a/.gitignore +++ b/.gitignore @@ -102,3 +102,6 @@ venv.bak/ # mypy .mypy_cache/ + +# example +example/site \ No newline at end of file diff --git a/example/docs/full_bib.md b/example/docs/full_bib.md new file mode 100644 index 0000000..5070fe3 --- /dev/null +++ b/example/docs/full_bib.md @@ -0,0 +1,3 @@ +# This is a full bibliography + +\full_bibliography diff --git a/example/docs/index.md b/example/docs/index.md new file mode 100644 index 0000000..6873766 --- /dev/null +++ b/example/docs/index.md @@ -0,0 +1,24 @@ +# This is an example of how to use the mkdocs-bibtex plugin + +## Citation + +Citation [@test] + +## Non existing citation + +This should fail on --strict mode + +Citation [@nonexistent] + +## Citation with affix + +Citation [@test, see pp. 100] + +## Citation with multiple affixes + +Citation [see @test, pp. 100, 200] + + +## Bibliography + +\bibliography diff --git a/example/mkdocs.yml b/example/mkdocs.yml new file mode 100644 index 0000000..c16fb96 --- /dev/null +++ b/example/mkdocs.yml @@ -0,0 +1,16 @@ +site_name: Example Mkdocs-bibtex + +plugins: + - bibtex: + bib_file: refs.bib + #csl_file: nature.csl + csl_file: springer-basic-author-date.csl + cite_inline: false + +markdown_extensions: + - footnotes + - pymdownx.caret + +nav: + - Index: index.md + - Bibliography: full_bib.md \ No newline at end of file diff --git a/example/nature.csl b/example/nature.csl new file mode 100644 index 0000000..2646cfe --- /dev/null +++ b/example/nature.csl @@ -0,0 +1,132 @@ + + diff --git a/example/refs.bib b/example/refs.bib new file mode 100644 index 0000000..f5d1a96 --- /dev/null +++ b/example/refs.bib @@ -0,0 +1,41 @@ +@article{test, + title={Test Title}, + author={Author, First and Author, Second}, + journal={Testing Journal}, + volume={1}, + year={2019}, + publisher={Test_Publisher} +} + +@article{test2, + title={{Test Title (TT)}}, + author={Author, First and Author, Second}, + journal={Testing Journal (TJ)}, + volume={1}, + year={2019}, + publisher={Test_Publisher (TP)} +} + +@article{Bivort2016, + title = {Evidence for Selective Attention in the Insect Brain}, + author = {De Bivort, Benjamin L. and Van Swinderen, Bruno}, + year = {2016}, + volume = {15}, + pages = {1--7}, + issn = {22145753}, + doi = {10.1016/j.cois.2016.02.007}, + abstract = {The capacity for selective attention appears to be required by any animal responding to an environment containing multiple objects, although this has been difficult to study in smaller animals such as insects. Clear operational characteristics of attention however make study of this crucial brain function accessible to any animal model. Whereas earlier approaches have relied on freely behaving paradigms placed in an ecologically relevant context, recent tethered preparations have focused on brain imaging and electrophysiology in virtual reality environments. Insight into brain activity during attention-like behavior has revealed key elements of attention in the insect brain. Surprisingly, a variety of brain structures appear to be involved, suggesting that even in the smallest brains attention might involve widespread coordination of neural activity.}, + journal = {Current Opinion in Insect Science}, + keywords = {attention,bees,drosophila,insects}, + pmid = {27436727} +} + +@article{test_citavi, + title={{Test Title (TT)}}, + author={Author, First and Author, Second}, + journal={Testing Journal (TJ)}, + volume={1}, + year={2019}, + publisher={Test_Publisher (TP)}, + url = {\url{https://doi.org/10.21577/0103-5053.20190253}} +} diff --git a/example/springer-basic-author-date.csl b/example/springer-basic-author-date.csl new file mode 100644 index 0000000..5edf631 --- /dev/null +++ b/example/springer-basic-author-date.csl @@ -0,0 +1,239 @@ + + From 35c8aa511e41371f65b3af8f0bcaa5e19d2a53e5 Mon Sep 17 00:00:00 2001 From: shyamd <16827130+shyamd@users.noreply.github.com> Date: Wed, 15 Jan 2025 16:11:21 -0800 Subject: [PATCH 46/58] last mistake from nocite --- test_files/test_integration.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test_files/test_integration.py b/test_files/test_integration.py index fe7d27a..aae508c 100644 --- a/test_files/test_integration.py +++ b/test_files/test_integration.py @@ -73,7 +73,7 @@ def test_pandoc_citation_rendering(pandoc_plugin): assert "(De Bivort and Van Swinderen 2016)" in result # Check bibliography formatting - assert "Author F, Author S (2019a)" in result + assert "Author F, Author S (2019)" in result assert "De Bivort BL, Van Swinderen B (2016)" in result From 76b814ef47ee1c0f36dd22b27541d3d693a2c436 Mon Sep 17 00:00:00 2001 From: shyamd <16827130+shyamd@users.noreply.github.com> Date: Wed, 15 Jan 2025 17:16:37 -0800 Subject: [PATCH 47/58] automatically detect inline vs footnote styles --- src/mkdocs_bibtex/config.py | 2 -- src/mkdocs_bibtex/plugin.py | 4 ---- src/mkdocs_bibtex/registry.py | 34 +++++++++++++++++++++++++++++++--- 3 files changed, 31 insertions(+), 9 deletions(-) diff --git a/src/mkdocs_bibtex/config.py b/src/mkdocs_bibtex/config.py index 990821e..8d441fc 100644 --- a/src/mkdocs_bibtex/config.py +++ b/src/mkdocs_bibtex/config.py @@ -15,7 +15,6 @@ class BibTexConfig(base.Config): by default, defaults to true full_bib_command (string): command to place a full bibliography of all references csl_file (string, optional): path or url to a CSL file, relative to mkdocs.yml. - cite_inline (bool): Whether or not to render inline citations, requires CSL, defaults to false footnote_format (string): format for the footnote number, defaults to "{number}" """ @@ -30,5 +29,4 @@ class BibTexConfig(base.Config): # Settings bib_by_default = config_options.Type(bool, default=True) - cite_inline = config_options.Type(bool, default=False) footnote_format = config_options.Type(str, default="{number}") diff --git a/src/mkdocs_bibtex/plugin.py b/src/mkdocs_bibtex/plugin.py index 46f3a36..7a0e12e 100644 --- a/src/mkdocs_bibtex/plugin.py +++ b/src/mkdocs_bibtex/plugin.py @@ -68,10 +68,6 @@ def on_config(self, config): else: self.csl_file = self.config.csl_file - # Toggle whether or not to render citations inline (Requires CSL) - if self.config.cite_inline and not self.csl_file: # pragma: no cover - raise ConfigurationError("Must supply a CSL file in order to use cite_inline") - if "{number}" not in self.config.footnote_format: raise ConfigurationError("Must include `{number}` placeholder in footnote_format") diff --git a/src/mkdocs_bibtex/registry.py b/src/mkdocs_bibtex/registry.py index 63102a5..5f82b19 100644 --- a/src/mkdocs_bibtex/registry.py +++ b/src/mkdocs_bibtex/registry.py @@ -86,11 +86,21 @@ def __init__(self, bib_files: list[str], csl_file: str): # Cache for formatted citations self._inline_cache: dict[str, str] = {} self._reference_cache: dict[str, str] = {} + self._is_inline = self._check_csl_type(self.csl_file) def inline_text(self, citation_block: CitationBlock) -> str: - """Returns cached inline citation text""" - keys = [citation.key for citation in citation_block.citations if citation.key in self._reference_cache] - return self._inline_cache.get(str(citation_block), "") + "".join(f"[^{key}]" for key in keys) + """Get the inline text for a citation block""" + footnotes = " ".join( + f"[^{citation.key}]" for citation in citation_block.citations if citation.key in self._reference_cache + ) + + if self._is_inline: + # For inline styles, return both inline citation and footnote + inline_text = self._inline_cache.get(str(citation_block), str(citation_block)) + return inline_text + footnotes + else: + # For footnote styles, just return footnote links + return footnotes def reference_text(self, citation: Citation) -> str: """Returns cached reference text""" @@ -183,3 +193,21 @@ def _process_with_pandoc(self, citation_blocks: list[CitationBlock]) -> tuple[di log.debug(f"Inline cache: {inline_cache}") log.debug(f"Reference cache: {reference_cache}") return inline_cache, reference_cache + + def _check_csl_type(self, csl_file: str) -> bool: + """Check if CSL file is footnote or inline style""" + if not csl_file: + return False + + try: + with open(csl_file) as f: + csl_content = f.read() + # Check if citation-format is "author-date" + # For "numeric" styles we default to footnotes + if 'citation-format="author-date"' in csl_content: + return True + # Default to footnote style + return False + except Exception as e: + log.warning(f"Error reading CSL file: {e}") + return False From 64f9df3333b3461e5dc41d1bd5c84de295db5c1d Mon Sep 17 00:00:00 2001 From: shyamd <16827130+shyamd@users.noreply.github.com> Date: Wed, 15 Jan 2025 17:24:57 -0800 Subject: [PATCH 48/58] switch example to numeric to demonstrate switching inline citation --- example/mkdocs.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/example/mkdocs.yml b/example/mkdocs.yml index c16fb96..46df12f 100644 --- a/example/mkdocs.yml +++ b/example/mkdocs.yml @@ -3,9 +3,8 @@ site_name: Example Mkdocs-bibtex plugins: - bibtex: bib_file: refs.bib - #csl_file: nature.csl - csl_file: springer-basic-author-date.csl - cite_inline: false + csl_file: nature.csl + #csl_file: springer-basic-author-date.csl markdown_extensions: - footnotes From 3675dda9777575d89e7ee46981eb63c9a3948198 Mon Sep 17 00:00:00 2001 From: shyamd <16827130+shyamd@users.noreply.github.com> Date: Wed, 15 Jan 2025 17:43:28 -0800 Subject: [PATCH 49/58] fix footnote formatting --- src/mkdocs_bibtex/config.py | 2 +- src/mkdocs_bibtex/plugin.py | 22 ++++++++++++++++------ src/mkdocs_bibtex/registry.py | 21 ++++++++++++++------- test_files/test_integration.py | 18 +++++++++++++----- 4 files changed, 44 insertions(+), 19 deletions(-) diff --git a/src/mkdocs_bibtex/config.py b/src/mkdocs_bibtex/config.py index 8d441fc..c491a6e 100644 --- a/src/mkdocs_bibtex/config.py +++ b/src/mkdocs_bibtex/config.py @@ -29,4 +29,4 @@ class BibTexConfig(base.Config): # Settings bib_by_default = config_options.Type(bool, default=True) - footnote_format = config_options.Type(str, default="{number}") + footnote_format = config_options.Type(str, default="{key}") diff --git a/src/mkdocs_bibtex/plugin.py b/src/mkdocs_bibtex/plugin.py index 7a0e12e..09d3898 100644 --- a/src/mkdocs_bibtex/plugin.py +++ b/src/mkdocs_bibtex/plugin.py @@ -68,13 +68,15 @@ def on_config(self, config): else: self.csl_file = self.config.csl_file - if "{number}" not in self.config.footnote_format: - raise ConfigurationError("Must include `{number}` placeholder in footnote_format") + if "{key}" not in self.config.footnote_format: + raise ConfigurationError("Must include `{key}` placeholder in footnote_format") if self.csl_file: - self.registry = PandocRegistry(bib_files=bibfiles, csl_file=self.csl_file) + self.registry = PandocRegistry( + bib_files=bibfiles, csl_file=self.csl_file, footnote_format=self.config.footnote_format + ) else: - self.registry = SimpleRegistry(bib_files=bibfiles) + self.registry = SimpleRegistry(bib_files=bibfiles, footnote_format=self.config.footnote_format) self.last_configured = time.time() return config @@ -121,7 +123,11 @@ def on_page_markdown(self, markdown, page, config, files): bibliography = [] for citation in citations.values(): try: - bibliography.append("[^{}]: {}".format(citation.key, self.registry.reference_text(citation))) + bibliography.append( + "[^{}]: {}".format( + self.registry.footnote_format.format(key=citation.key), self.registry.reference_text(citation) + ) + ) except Exception as e: log.warning(f"Error formatting citation {citation.key}: {e}") bibliography = "\n".join(bibliography) @@ -136,7 +142,11 @@ def on_page_markdown(self, markdown, page, config, files): self.registry.validate_citation_blocks(blocks) full_bibliography = [] for citation in all_citations: - full_bibliography.append("[^{}]: {}".format(citation.key, self.registry.reference_text(citation))) + full_bibliography.append( + "[^{}]: {}".format( + self.registry.footnote_format.format(key=citation.key), self.registry.reference_text(citation) + ) + ) full_bibliography = "\n".join(full_bibliography) markdown = markdown.replace(full_bib_command, full_bibliography) diff --git a/src/mkdocs_bibtex/registry.py b/src/mkdocs_bibtex/registry.py index 5f82b19..915fd74 100644 --- a/src/mkdocs_bibtex/registry.py +++ b/src/mkdocs_bibtex/registry.py @@ -15,7 +15,7 @@ class ReferenceRegistry(ABC): A registry of references that can be used to format citations """ - def __init__(self, bib_files: list[str]): + def __init__(self, bib_files: list[str], footnote_format: str = "{key}"): refs = {} log.info(f"Loading data from bib files: {bib_files}") for bibfile in bib_files: @@ -23,6 +23,7 @@ def __init__(self, bib_files: list[str]): bibdata = parse_file(bibfile) refs.update(bibdata.entries) self.bib_data = BibliographyData(entries=refs) + self.footnote_format = footnote_format @abstractmethod def validate_citation_blocks(self, citation_blocks: list[CitationBlock]) -> None: @@ -38,8 +39,8 @@ def reference_text(self, citation: Citation) -> str: class SimpleRegistry(ReferenceRegistry): - def __init__(self, bib_files: list[str]): - super().__init__(bib_files) + def __init__(self, bib_files: list[str], footnote_format: str = "{key}"): + super().__init__(bib_files, footnote_format) self.style = PlainStyle() self.backend = MarkdownBackend() @@ -56,7 +57,11 @@ def validate_citation_blocks(self, citation_blocks: list[CitationBlock]) -> None log.warning(f"Affixes not supported in simple mode: {citation}") def inline_text(self, citation_block: CitationBlock) -> str: - keys = [citation.key for citation in citation_block.citations if citation.key in self.bib_data.entries] + keys = [ + self.footnote_format.format(key=citation.key) + for citation in citation_block.citations + if citation.key in self.bib_data.entries + ] return "".join(f"[^{key}]" for key in keys) def reference_text(self, citation: Citation) -> str: @@ -74,8 +79,8 @@ def reference_text(self, citation: Citation) -> str: class PandocRegistry(ReferenceRegistry): """A registry that uses Pandoc to format citations""" - def __init__(self, bib_files: list[str], csl_file: str): - super().__init__(bib_files) + def __init__(self, bib_files: list[str], csl_file: str, footnote_format: str = "{key}"): + super().__init__(bib_files, footnote_format) self.csl_file = csl_file # Get pandoc version for formatting decisions @@ -91,7 +96,9 @@ def __init__(self, bib_files: list[str], csl_file: str): def inline_text(self, citation_block: CitationBlock) -> str: """Get the inline text for a citation block""" footnotes = " ".join( - f"[^{citation.key}]" for citation in citation_block.citations if citation.key in self._reference_cache + f"[^{self.footnote_format.format(key=citation.key)}]" + for citation in citation_block.citations + if citation.key in self._reference_cache ) if self._is_inline: diff --git a/test_files/test_integration.py b/test_files/test_integration.py index aae508c..e46cb60 100644 --- a/test_files/test_integration.py +++ b/test_files/test_integration.py @@ -120,14 +120,22 @@ def test_bibliography_controls(plugin): assert "[^test]:" in result -@pytest.mark.xfail(reason="Need to reimplement footnote formatting") -def test_custom_footnote_format(plugin): +def test_custom_footnote_format(): """Test custom footnote formatting""" - plugin.config.footnote_format = "ref{number}" + plugin = BibTexPlugin() + plugin.load_config( + options={ + "bib_file": os.path.join(test_files_dir, "test.bib"), + "bib_by_default": False, + "footnote_format": "ref-{key}", + }, + config_file_path=test_files_dir, + ) + plugin.on_config(plugin.config) + markdown = "Citation [@test]\n\n\\bibliography" result = plugin.on_page_markdown(markdown, None, None, None) - assert "[^reftest]" in result - + assert "[^ref-test]" in result # Test that an invalid footnote format raises an exception bad_plugin = BibTexPlugin() bad_plugin.load_config( From 00bcb1706084a9697bf24c78831d522876d645c0 Mon Sep 17 00:00:00 2001 From: shyamd <16827130+shyamd@users.noreply.github.com> Date: Thu, 16 Jan 2025 08:42:34 -0800 Subject: [PATCH 50/58] clean up deps --- requirements-testing.txt | 1 - requirements.txt | 1 + setup.py | 3 ++- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/requirements-testing.txt b/requirements-testing.txt index f70023d..bc97659 100644 --- a/requirements-testing.txt +++ b/requirements-testing.txt @@ -2,6 +2,5 @@ pytest==8.3.4 pytest-cov==6.0.0 pytest-pretty==1.2.0 mypy==1.14.1 -responses==0.25.6 ruff==0.9.1 types-requests~=2.32.0 \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index a5fcfd3..e7c646f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,3 +3,4 @@ pybtex==0.24.0 pypandoc==1.14 requests==2.32.3 validators==0.34.0 +responses==0.25.6 \ No newline at end of file diff --git a/setup.py b/setup.py index 989679d..78feda8 100644 --- a/setup.py +++ b/setup.py @@ -22,7 +22,8 @@ "pypandoc>=1.5", "requests>=2.8.1", "validators>=0.19.0", - "setuptools>=68.0.0" + "setuptools>=68.0.0", + "responses>=0.25.6" ], tests_require=["pytest"], packages=find_packages("src"), From 66a7fb5d91b1a3f4f30b6c7b9b5ae6325ae52168 Mon Sep 17 00:00:00 2001 From: shyamd <16827130+shyamd@users.noreply.github.com> Date: Thu, 16 Jan 2025 08:42:45 -0800 Subject: [PATCH 51/58] fix test for new registry --- test_files/test_plugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test_files/test_plugin.py b/test_files/test_plugin.py index 807a23f..9cd273e 100644 --- a/test_files/test_plugin.py +++ b/test_files/test_plugin.py @@ -86,7 +86,7 @@ def test_bibtex_loading_zotero(mock_zotero_api: responses.RequestsMock, number_o ) plugin.on_config(plugin.config) - assert len(plugin.bib_data.entries) == number_of_entries + assert len(plugin.registry.bib_data.entries) == number_of_entries def generate_bibtex_entries(n: int) -> list[str]: From c1c27dd2a3da9c7dc011285ab50fd1a21f155be5 Mon Sep 17 00:00:00 2001 From: shyamd <16827130+shyamd@users.noreply.github.com> Date: Thu, 16 Jan 2025 09:49:25 -0800 Subject: [PATCH 52/58] convert integration test into unit test --- test_files/test_plugin.py | 66 ---------------------------------- test_files/test_utils.py | 74 +++++++++++++++++++++++++++++++++++++-- 2 files changed, 71 insertions(+), 69 deletions(-) diff --git a/test_files/test_plugin.py b/test_files/test_plugin.py index 9cd273e..c428e0f 100644 --- a/test_files/test_plugin.py +++ b/test_files/test_plugin.py @@ -1,42 +1,11 @@ -import collections.abc import os -import random -import string import pytest -import responses from mkdocs_bibtex.plugin import BibTexPlugin module_dir = os.path.dirname(os.path.abspath(__file__)) test_files_dir = os.path.abspath(os.path.join(module_dir, "..", "test_files")) -MOCK_ZOTERO_URL = "https://api.zotero.org/groups/FOO/collections/BAR/items?format=bibtex" - - -@pytest.fixture -def mock_zotero_api(request: pytest.FixtureRequest) -> collections.abc.Generator[responses.RequestsMock]: - zotero_api_url = "https://api.zotero.org/groups/FOO/collections/BAR/items?format=bibtex&limit=100" - bibtex_contents = generate_bibtex_entries(request.param) - - limit = 100 - pages = [bibtex_contents[i : i + limit] for i in range(0, len(bibtex_contents), limit)] - - with responses.RequestsMock() as mock_api: - for page_num, page in enumerate(pages): - current_start = "" if page_num == 0 else f"&start={page_num * limit}" - next_start = f"&start={(page_num + 1) * limit}" - mock_api.add( - responses.Response( - method="GET", - url=f"{zotero_api_url}{current_start}", - json="\n".join(page), - headers={} - if page_num == len(pages) - 1 - else {"Link": f"<{zotero_api_url}{next_start}>; rel='next'"}, - ) - ) - - yield mock_api @pytest.fixture @@ -75,38 +44,3 @@ def test_bibtex_loading_bibdir(): plugin.on_config(plugin.config) assert len(plugin.registry.bib_data.entries) == 2 - - -@pytest.mark.parametrize(("mock_zotero_api", "number_of_entries"), ((4, 4), (150, 150)), indirect=["mock_zotero_api"]) -def test_bibtex_loading_zotero(mock_zotero_api: responses.RequestsMock, number_of_entries: int) -> None: - plugin = BibTexPlugin() - plugin.load_config( - options={"bib_file": MOCK_ZOTERO_URL}, - config_file_path=test_files_dir, - ) - - plugin.on_config(plugin.config) - assert len(plugin.registry.bib_data.entries) == number_of_entries - - -def generate_bibtex_entries(n: int) -> list[str]: - """Generates n random bibtex entries.""" - - entries = [] - - for i in range(n): - author_first = "".join(random.choices(string.ascii_letters, k=8)) - author_last = "".join(random.choices(string.ascii_letters, k=8)) - title = "".join(random.choices(string.ascii_letters, k=10)) - journal = "".join(random.choices(string.ascii_uppercase, k=5)) - year = str(random.randint(1950, 2025)) - - entries.append(f""" -@article{{{author_last}_{i}}}, - title = {{{title}}}, - volume = {{1}}, - journal = {{{journal}}}, - author = {{{author_last}, {author_first}}}, - year = {{{year}}}, -""") - return entries diff --git a/test_files/test_utils.py b/test_files/test_utils.py index eba19e3..cc6bd69 100644 --- a/test_files/test_utils.py +++ b/test_files/test_utils.py @@ -1,11 +1,44 @@ import pytest -from mkdocs_bibtex.utils import ( - sanitize_zotero_query, -) +from mkdocs_bibtex.utils import sanitize_zotero_query, tempfile_from_zotero_url +import collections.abc +import os +import random +import string + +import responses +from pybtex.database import parse_file EXAMPLE_ZOTERO_API_ENDPOINT = "https://api.zotero.org/groups/FOO/collections/BAR/items" +MOCK_ZOTERO_URL = "https://api.zotero.org/groups/FOO/collections/BAR/items?format=bibtex" + + +@pytest.fixture +def mock_zotero_api(request: pytest.FixtureRequest) -> collections.abc.Generator[responses.RequestsMock]: + zotero_api_url = "https://api.zotero.org/groups/FOO/collections/BAR/items?format=bibtex&limit=100" + bibtex_contents = generate_bibtex_entries(request.param) + + limit = 100 + pages = [bibtex_contents[i : i + limit] for i in range(0, len(bibtex_contents), limit)] + + with responses.RequestsMock() as mock_api: + for page_num, page in enumerate(pages): + current_start = "" if page_num == 0 else f"&start={page_num * limit}" + next_start = f"&start={(page_num + 1) * limit}" + mock_api.add( + responses.Response( + method="GET", + url=f"{zotero_api_url}{current_start}", + json="\n".join(page), + headers={} + if page_num == len(pages) - 1 + else {"Link": f"<{zotero_api_url}{next_start}>; rel='next'"}, + ) + ) + + yield mock_api + @pytest.mark.parametrize( ("zotero_url", "expected_sanitized_url"), @@ -31,3 +64,38 @@ ) def test_sanitize_zotero_query(zotero_url: str, expected_sanitized_url: str) -> None: assert sanitize_zotero_query(url=zotero_url) == expected_sanitized_url + + +@pytest.mark.parametrize(("mock_zotero_api", "number_of_entries"), ((4, 4), (150, 150)), indirect=["mock_zotero_api"]) +def test_bibtex_loading_zotero(mock_zotero_api: responses.RequestsMock, number_of_entries: int) -> None: + bib_file = tempfile_from_zotero_url("Bib File", MOCK_ZOTERO_URL, ".bib") + + assert os.path.exists(bib_file) + assert os.path.getsize(bib_file) > 0 + + bibdata = parse_file(bib_file) + + assert len(bibdata.entries) == number_of_entries + + +def generate_bibtex_entries(n: int) -> list[str]: + """Generates n random bibtex entries.""" + + entries = [] + + for i in range(n): + author_first = "".join(random.choices(string.ascii_letters, k=8)) + author_last = "".join(random.choices(string.ascii_letters, k=8)) + title = "".join(random.choices(string.ascii_letters, k=10)) + journal = "".join(random.choices(string.ascii_uppercase, k=5)) + year = str(random.randint(1950, 2025)) + + entries.append(f""" +@article{{{author_last}_{i}}}, + title = {{{title}}}, + volume = {{1}}, + journal = {{{journal}}}, + author = {{{author_last}, {author_first}}}, + year = {{{year}}}, +""") + return entries From 8f3aded9cc2bc73d49100f072aa0c982fd6dff0e Mon Sep 17 00:00:00 2001 From: shyamd <16827130+shyamd@users.noreply.github.com> Date: Thu, 16 Jan 2025 09:53:24 -0800 Subject: [PATCH 53/58] update pre-commit --- .pre-commit-config.yaml | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 32a1ac6..8e8e120 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,15 +1,15 @@ repos: - - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.4.0 - hooks: - - id: check-yaml - - id: end-of-file-fixer - - id: trailing-whitespace - - repo: https://github.com/ambv/black - rev: 22.12.0 - hooks: - - id: black - - repo: https://github.com/charliermarsh/ruff-pre-commit - rev: 'v0.0.207' - hooks: - - id: ruff + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.4.0 + hooks: + - id: check-yaml + - id: end-of-file-fixer + - id: trailing-whitespace + - repo: https://github.com/charliermarsh/ruff-pre-commit + rev: 'v0.9.2' + hooks: + # Run the linter. + - id: ruff + args: [ --fix ] + # Run the formatter. + - id: ruff-format From 1cccb4974a0674caaa7d325d7f662b1961b4bf42 Mon Sep 17 00:00:00 2001 From: shyamd <16827130+shyamd@users.noreply.github.com> Date: Thu, 16 Jan 2025 10:04:10 -0800 Subject: [PATCH 54/58] add type hints --- src/mkdocs_bibtex/utils.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/mkdocs_bibtex/utils.py b/src/mkdocs_bibtex/utils.py index 3c4448e..e15b0e2 100644 --- a/src/mkdocs_bibtex/utils.py +++ b/src/mkdocs_bibtex/utils.py @@ -8,7 +8,8 @@ log = logging.getLogger("mkdocs.plugins.mkdocs-bibtex") -def tempfile_from_url(name, url, suffix): +def tempfile_from_url(name: str, url: str, suffix: str) -> str: + """Download bibfile from a URL.""" log.debug(f"Downloading {name} from URL {url} to temporary file...") if urllib.parse.urlparse(url).hostname == "api.zotero.org": return tempfile_from_zotero_url(name, url, suffix) From 5cd18567b539b3e08739834020324b3cce48e5be Mon Sep 17 00:00:00 2001 From: shyamd <16827130+shyamd@users.noreply.github.com> Date: Thu, 16 Jan 2025 10:04:19 -0800 Subject: [PATCH 55/58] switch to pyproject.toml --- pyproject.toml | 38 ++++++++++++++++++++++++++++++++++++++ setup.py | 32 -------------------------------- 2 files changed, 38 insertions(+), 32 deletions(-) delete mode 100644 setup.py diff --git a/pyproject.toml b/pyproject.toml index 05ba743..0ce959e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,39 @@ +[build-system] +requires = ["setuptools>=68.0.0", "setuptools_scm"] +build-backend = "setuptools.build_meta" + +[project] +name = "mkdocs-bibtex" +dynamic = ["version"] +description = "An MkDocs plugin that enables managing citations with BibTex" +readme = "README.md" +requires-python = ">=3.6" +license = {text = "BSD-3-Clause-LBNL"} +keywords = ["mkdocs", "python", "markdown", "bibtex"] +authors = [ + {name = "Shyam Dwaraknath", email = "16827130+shyamd@users.noreply.github.com"}, +] +dependencies = [ + "mkdocs>=1.2", + "pybtex>=0.22", + "pypandoc>=1.5", + "requests>=2.8.1", + "validators>=0.19.0", + "setuptools>=68.0.0", + "responses>=0.25.6", +] + +[project.urls] +Homepage = "https://github.com/shyamd/mkdocs-bibtex/" + +[project.entry-points."mkdocs.plugins"] +bibtex = "mkdocs_bibtex.plugin:BibTexPlugin" + +[tool.setuptools] +package-dir = {"" = "src"} +packages = ["mkdocs_bibtex"] + + [tool.ruff] line-length = 120 exclude = [ @@ -6,6 +42,8 @@ exclude = [ '__init__.py', ] +[tool.setuptools_scm] + [tool.ruff.lint] ignore = [ 'E741', diff --git a/setup.py b/setup.py deleted file mode 100644 index 78feda8..0000000 --- a/setup.py +++ /dev/null @@ -1,32 +0,0 @@ -from setuptools import find_packages, setup - -with open("README.md", "r") as fh: - long_description = fh.read() - -setup( - name="mkdocs-bibtex", - use_scm_version=True, - setup_requires=["setuptools_scm"], - description="An MkDocs plugin that enables managing citations with BibTex", - long_description=long_description, - long_description_content_type="text/markdown", - keywords="mkdocs python markdown bibtex", - url="https://github.com/shyamd/mkdocs-bibtex/", - author="Shyam Dwaraknath", - author_email="shyamd@lbl.gov", - license="BSD-3-Clause-LBNL", - python_requires=">=3.6", - install_requires=[ - "mkdocs>=1.2", - "pybtex>=0.22", - "pypandoc>=1.5", - "requests>=2.8.1", - "validators>=0.19.0", - "setuptools>=68.0.0", - "responses>=0.25.6" - ], - tests_require=["pytest"], - packages=find_packages("src"), - package_dir={"": "src"}, - entry_points={"mkdocs.plugins": ["bibtex = mkdocs_bibtex.plugin:BibTexPlugin"]}, -) From 0f14510eee653a3c6b3a6da75c22fade70e662e9 Mon Sep 17 00:00:00 2001 From: shyamd <16827130+shyamd@users.noreply.github.com> Date: Thu, 16 Jan 2025 10:15:44 -0800 Subject: [PATCH 56/58] fix references for non-existant citations --- src/mkdocs_bibtex/registry.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/mkdocs_bibtex/registry.py b/src/mkdocs_bibtex/registry.py index 915fd74..b7d327d 100644 --- a/src/mkdocs_bibtex/registry.py +++ b/src/mkdocs_bibtex/registry.py @@ -111,7 +111,7 @@ def inline_text(self, citation_block: CitationBlock) -> str: def reference_text(self, citation: Citation) -> str: """Returns cached reference text""" - return self._reference_cache.get(citation.key, "") + return self._reference_cache[citation.key] def validate_citation_blocks(self, citation_blocks: list[CitationBlock]) -> None: """Validates citation blocks and pre-formats all citations""" @@ -142,8 +142,8 @@ def _process_with_pandoc(self, citation_blocks: list[CitationBlock]) -> tuple[di citation_map = {index: block for index, block in enumerate(citation_blocks)} full_doc += "\n\n".join(f"{index}. {block}" for index, block in citation_map.items()) full_doc += "\n\n# References\n\n" - log.debug("Converting with pandoc") - log.debug(f"Full doc: {full_doc}") + log.debug("Converting with pandoc:") + log.debug(full_doc) with tempfile.TemporaryDirectory() as tmpdir: bib_path = Path(tmpdir).joinpath("temp.bib") with open(bib_path, "wt", encoding="utf-8") as bibfile: @@ -154,7 +154,8 @@ def _process_with_pandoc(self, citation_blocks: list[CitationBlock]) -> tuple[di source=full_doc, to="markdown-citations", format="markdown", extra_args=args ) - log.debug(f"Pandoc output: {markdown}") + log.debug(f"Pandoc output:") + log.debug(markdown) try: splits = markdown.split("# References") inline_citations, references = splits[0], splits[1] From 4ebe77fa420ed32c5037099ce1cf728d65764b65 Mon Sep 17 00:00:00 2001 From: shyamd <16827130+shyamd@users.noreply.github.com> Date: Thu, 16 Jan 2025 10:16:02 -0800 Subject: [PATCH 57/58] update README --- README.md | 96 ++++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 71 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index 97b1692..62610f5 100644 --- a/README.md +++ b/README.md @@ -40,44 +40,90 @@ The footnotes extension is how citations are linked for now. - `bib_by_default` - Automatically append the `bib_command` at the end of every markdown document, defaults to `true` - `full_bib_command` - The syntax to render your entire bibliography, defaults to `\full_bibliography` - `csl_file` - The path or url to a bibtex CSL file, specifying your citation format. Defaults to `None`, which renders in a plain format. A registry of citation styles can be found here: https://github.com/citation-style-language/styles -- `cite_inline` - Whether or not to render citations inline, requires `csl_file` to be specified. Defaults to `False`. ## Usage In your markdown files: -1. Add your citations as you would if you used pandoc, IE: `[@first_cite;@second_cite]` +1. Add your citations as you would if you used pandoc, IE: `[@first_cite;@second_cite]`. 2. Add `\bibliography`, or the value of `bib_command`, to the doc you want your references rendered (if `bib_by_default` is set to true this is automatically applied for every page). 3. (Optional) Add `\full_bibliography`, or the value of `full_bib_command`, to where you want the full bibliography rendered. *Note*: This is currently not working properly, since this plugin can't dictate the order in which files are processed. The best way to ensure the file with the full bibliography gets processed last is to use numbers in front of file/folder names to enforce the order of processing, IE: `01_my_first_file.md` -4. (Optional) Configure the `csl_file` option to dictate the citation text formatting. +4. (Optional) Configure the `csl_file` option to dictate the citation text formatting. This plugin automatically detects if the citation is an inline style and inserts that text when appropriate. ## Debugging +You can run mkdocs with the `--strict` flag to fail building on any citations that don't exist in the bibtex file. + You may wish to use the verbose flag in mkdocs (`-v`) to log debug messages. You should see something like this ```bash (...) -DEBUG - Parsing bibtex file 'docs/bib/papers.bib'... -INFO - SUCCESS Parsing bibtex file 'docs/bib/papers.bib' -DEBUG - Downloading CSL file from URL https://raw.githubusercontent.com/citation-style-language/styles/master/apa-6th-edition.csl to temporary file... -INFO - CSL file downladed from URL https://raw.githubusercontent.com/citation-style-language/styles/master/apa-6th-edition.csl to temporary file () +DEBUG - Reading markdown pages. +DEBUG - Reading: index.md +DEBUG - Running `page_markdown` event from plugin 'bibtex' +WARNING - Citing unknown reference key nonexistent +DEBUG - Converting with pandoc: +DEBUG - --- + link-citations: false + --- + + 0. [@test] + + 1. [@nonexistent] + + 2. [@test, see pp. 100] + + 3. [see @test, pp. 100, 200] + + # References + +[WARNING] Citeproc: citation nonexistent not found + +DEBUG - Pandoc output: +DEBUG - 0. ^1^ + + 1. ^**nonexistent?**^ + + 2. ^1,\ see\ pp. 100^ + + 3. ^see\ 1^ + + # References {#references .unnumbered} + + :::: {#refs .references .csl-bib-body entry-spacing="0" line-spacing="2"} + ::: {#ref-test .csl-entry} + [1. ]{.csl-left-margin}[Author, F. & Author, S. Test title. *Testing + Journal* **1**, (2019).]{.csl-right-inline} + ::: + :::: +DEBUG - Inline cache: {'[@test]': '^1^', '[@nonexistent]': '^**nonexistent?**^', '[@test, see pp. 100]': '^1,\\ see\\ pp. 100^', '[see @test, pp. 100, 200]': '^see\\ 1^'} +DEBUG - Reference cache: {'test': 'Author, F. & Author, S. Test title. *Testing Journal* **1**, (2019).'} +WARNING - Error formatting citation nonexistent: 'nonexistent' +DEBUG - Markdown: + # This is an example of how to use the mkdocs-bibtex plugin + + ## Citation + + Citation [^test] + + ## Non existing citation + + This should fail on --strict mode + + Citation + + ## Citation with affix + + Citation [^test] + + ## Citation with multiple affixes + + Citation [^test] + + + ## Bibliography + + [^test]: Author, F. & Author, S. Test title. *Testing Journal* **1**, (2019). +DEBUG - Reading: full_bib.md (...) -DEBUG - Reading: publications.md -DEBUG - Running 2 `page_markdown` events -DEBUG - Formatting all bib entries... -DEBUG - --Converting bibtex entry 'foo2019' with CSL file 'docs/bib/apa_verbose.csl' using pandoc>=2.11 -DEBUG - --SUCCESS Converting bibtex entry 'foo2019' with CSL file 'docs/bib/apa_verbose.csl' using pandoc>=2.11 -DEBUG - --Converting bibtex entry 'bar2024' with CSL file 'docs/bib/apa_verbose.csl' using pandoc>=2.11 -DEBUG - --SUCCESS Converting bibtex entry 'bar2024' with CSL file 'docs/bib/apa_verbose.csl' using pandoc>=2.11 -INFO - SUCCESS Formatting all bib entries -DEBUG - Replacing citation keys with the generated ones... -DEBUG - --Rendering citation inline for '[@foo2019]'... -DEBUG - ----Converting pandoc citation key '[@foo2019]' with CSL file 'docs/bib/apa_verbose.csl' and Bibliography file '(...)/tmpzt7t8p0y/temp.bib'... -DEBUG - ----SUCCESS Converting pandoc citation key '[@foo2019]' with CSL file 'docs/bib/apa_verbose.csl' and Bibliography file '(...)/tmpzt7t8p0y/temp.bib' -DEBUG - --SUCCESS Rendering citation inline for '[@foo2019]' -DEBUG - --Rendering citation inline for '[@bar2024]'... -DEBUG - ----Converting pandoc citation key '[@bar2024]' with CSL file 'docs/bib/apa_verbose.csl' and Bibliography file '(...)/tmpzt7t8p0y/temp.bib'... -DEBUG - ----SUCCESS Converting pandoc citation key '[@bar2024]' with CSL file 'docs/bib/apa_verbose.csl' and Bibliography file '(...)/tmpzt7t8p0y/temp.bib' -DEBUG - --SUCCESS Rendering citation inline for '[@bar2024]' -DEBUG - SUCCESS Replacing citation keys with the generated ones ``` From efcb83aa016a76e5746a75b8db4e1bc4c78a0fa6 Mon Sep 17 00:00:00 2001 From: shyamd <16827130+shyamd@users.noreply.github.com> Date: Thu, 16 Jan 2025 10:19:08 -0800 Subject: [PATCH 58/58] lint --- src/mkdocs_bibtex/registry.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mkdocs_bibtex/registry.py b/src/mkdocs_bibtex/registry.py index b7d327d..5d8ae49 100644 --- a/src/mkdocs_bibtex/registry.py +++ b/src/mkdocs_bibtex/registry.py @@ -154,7 +154,7 @@ def _process_with_pandoc(self, citation_blocks: list[CitationBlock]) -> tuple[di source=full_doc, to="markdown-citations", format="markdown", extra_args=args ) - log.debug(f"Pandoc output:") + log.debug("Pandoc output:") log.debug(markdown) try: splits = markdown.split("# References")