Skip to content

Commit

Permalink
Implement per-content bibliography (draft)
Browse files Browse the repository at this point in the history
  • Loading branch information
anjos committed Nov 2, 2024
1 parent fb5a825 commit 9e38ab8
Show file tree
Hide file tree
Showing 21 changed files with 587 additions and 123 deletions.
1 change: 1 addition & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ repos:
rev: v1.13.0
hooks:
- id: mypy
exclude: ^tests/.*/pelicanconf\.py$
args:
- --install-types
- --non-interactive
Expand Down
56 changes: 46 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ file](https://docs.pybtex.org/formats.html#bibliography-formats) and populates t
global Jinja2 context used by Pelican with a `publications` entry. The `publications`
entry is a list containing the following fields:

* `label`: The formatted label (depends on the used style, but usually something like
`3`, `Ein05`, or `Einstein, 1905`).
* `key`: The pybtex (BibTeX) database key
* `year`: The year of the entry
* `html`: An HTML-formatted version of the entry
Expand All @@ -53,8 +55,9 @@ If files indicated on that list are present and readable, they will be loaded. E
reported, but ignored during generation. Check Pelican logs for details while building
your site.

Note that relative paths are considered with respect to the location of
`pelicanconf.py`.
Note that relative paths are considered with respect to the location of the setting of
`PATH` in `pelicanconf.py`. If `PATH` itself is relative, it is considered relative to
the location of `pelicanconf.py` itself.

### Extra fields

Expand All @@ -67,22 +70,31 @@ a template override as explained next.
PYBTEX_ADD_ENTRY_FIELDS = ["url", "pdf", "slides", "poster"]
```

### Formatting style

By default, `PYBTEX_FORMAT_STYLE` is set to `plain`. You may further customize this
setting to one of the biobliography formatting styles supported by pybtex (currently
"plain", "alpha", "unsrt", and "unsrtalpha"). You may check the formatting style of
some of these BibTeX styles [on this
reference](https://www.overleaf.com/learn/latex/Bibtex_bibliography_styles).

### Publications page

This plugin provides a [default
`publications.html`](src/pelican/plugins/pybtex/templates/publications.html) template
that will render all publications *correctly loaded* from `PYBTEX_SOURCES`, ordered by
year, in reverse chronological order.
year, in reverse chronological order, and without BibTeX-style labels. Note that, if
there are no valid entries on `PYBTEX_SOURCES`, then a `publications.html` page is not
generated.

You may want to override the default template, or parts of it with your own modifications.

To do so, create your own `publications.html` template, then use
You may also want to override the default template, or parts of it with your own
modifications. To do so, create your own `publications.html` template, then use
`THEME_TEMPLATES_OVERRIDES` and `THEME_STATIC_PATHS` to add search paths for template
resolution. For example, to add a short introductory text, we could override the
`before_content` block on the default template like so:

1. Create a file called `templates/publications.html` on your site sources, with the
following contents:
following (example) contents:

```html
{% extends "!pybtex/publications.html" %}
Expand All @@ -98,7 +110,7 @@ resolution. For example, to add a short introductory text, we could override th
<div id="pybtex">
{% for item in publications %}
<details id="{{ item.key }}">
<summary>{{ item.html }}</summary>
<summary>[{{ item.label }}] {{ item.html }}</summary>
<ul>
{% for k in ("url", "pdf", "slides", "poster") %}
{% if k in item %}<li>{{ k }}: {{ item[k] }}</li>{% endif %}
Expand All @@ -117,8 +129,31 @@ resolution. For example, to add a short introductory text, we could override th
```python
THEME_TEMPLATES_OVERRIDES = ["templates"]
# STATIC_PATHS = ["static"] ## if you also have static files to be copied
# PUBLICATIONS_SAVE_AS = "publications/index.html" ## to change the default output file
# PUBLICATIONS_URL = "publications/" ## to change the default URL for publications
```
### Local bibliography in articles and pages
You may use markers such as `[@bibkey]` or `[@@bibkey]` on your articles and pages in
restructuredtext or markdown formats, to refer to bibliography entries from the
`PYBTEX_SOURCES`. This process is similar to using BibTeX database entries in your
LaTeX sources by using the `\cite{bibkey}` command. In this case, this plugin will
replace these citations with links to a bibliography database *injected* at the end of
the article or post. The global `PYBTEX_FORMAT_STYLE` is respected while formatting
bibliographies.
You may also add further enrich article or page metadata defining a specific
`pybtex_sources`. In such a case, these files will be loaded respecting the same rules
as for `PYBTEX_SOURCES`. Specifically, relative paths are considered w.r.t. the location
of `PATH` in `pelicanconf.py`. Article and page bibliography markers will then be
resolved by first looking at entries in the local `pybtex_sources`, and then on the
global `PYBTEX_SOURCES` entry in `pelicanconf.py`.
Be aware that in case repeated citation keys are found across all bibliography
databases, **the last occurence is used** while resolving local bibliography for
articles an pages.
## Contributing
Contributions are welcome and much appreciated. Every little bit helps. You can
Expand All @@ -133,5 +168,6 @@ with the **Contributing Code** section.
## License
This project was inspired by the [original BibTeX
plugin](https://github.com/vene/pelican-bibtex), developed by Vlad Niculae. This project
and further modifications are licensed under the MIT license.
plugin](https://github.com/vene/pelican-bibtex), developed by Vlad Niculae, and
[pelican-cite plugin](https://github.com/VorpalBlade/pelican-cite), by Arvid Norlander.
This project and further modifications are licensed under the MIT license.
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,7 @@ select = [
"YTT", # https://docs.astral.sh/ruff/rules/#flake8-2020
]
ignore = [
"B905", # https://docs.astral.sh/ruff/rules/zip-without-explicit-strict/
"COM812", # https://docs.astral.sh/ruff/rules/missing-trailing-comma/
"D100", # https://docs.astral.sh/ruff/rules/undocumented-public-module/
"D102", # https://docs.astral.sh/ruff/rules/undocumented-public-method/
Expand Down
22 changes: 18 additions & 4 deletions src/pelican/plugins/pybtex/__init__.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,31 @@
# SPDX-FileCopyrightText: Copyright © 2024 André Anjos <[email protected]>
#
# SPDX-License-Identifier: MIT
"""Manage your academic publications page with Pelican and pybtex (BibTeX)."""

from .injector import PybtexInjector

def _connector(pelican_object):
from .generator import PublicationGenerator
_injector = PybtexInjector()

return PublicationGenerator

def _get_generators(pelican_object):
del pelican_object # shuts-up linter

from .generator import PybtexGenerator

return PybtexGenerator


def register():
"""Register this plugin to pelican."""

import pelican.plugins.signals

pelican.plugins.signals.get_generators.connect(_connector)
from . import signals

# Global bibliography page
pelican.plugins.signals.get_generators.connect(_get_generators)

# Per-content (articles, pages) biobliography injector
signals.pybtex_generator_init.connect(_injector.init)
pelican.plugins.signals.content_object_init.connect(_injector.resolve_bibliography)
130 changes: 47 additions & 83 deletions src/pelican/plugins/pybtex/generator.py
Original file line number Diff line number Diff line change
@@ -1,33 +1,25 @@
# SPDX-FileCopyrightText: Copyright © 2024 André Anjos <[email protected]>
#
# SPDX-License-Identifier: MIT
"""Populate the context with a list of formatted citations.
The citations are loaded from external BibTeX files, at a configurable path. It can
generate a ``Publications'' page for academic websites.
"""
"""Populate generation context with a list of formatted citations."""

import datetime
import locale
import logging
import pathlib
import typing

import jinja2
import pygments
import pygments.formatters
import pygments.lexers

import pelican.generators
import pybtex.backends.html
import pybtex.database
import pybtex.database.input.bibtex
import pybtex.database.output.bibtex
import pybtex.richtext
import pybtex.style.formatting.plain
import pelican.utils

from . import utils

logger = logging.getLogger(__name__)


class PublicationGenerator(pelican.generators.Generator):
class PybtexGenerator(pelican.generators.Generator):
"""Populate context with a list of BibTeX publications.
Parameters
Expand All @@ -54,98 +46,69 @@ def __init__(self, *args, **kwargs):
]
)
)
self.bibdata: list[pybtex.database.BibliographyData] = []

bibtex_parser = pybtex.database.input.bibtex.Parser()
for k in kwargs["settings"].get("PYBTEX_SOURCES", []):
p = pathlib.Path(k)

if not p.is_absolute():
# make it relative to the site path
p = kwargs["path"] / p
# validates pybtex sources
if not isinstance(kwargs["settings"].get("PYBTEX_SOURCES", []), list | tuple):
logger.fatal(
f"Setting `PYBTEX_SOURCES` should be a list or tuple, not "
f"{type(kwargs['settings']['PYBTEX_SOURCES'])}"
)

if not p.exists():
logger.error(f"BibTeX file `{p}` does not exist")
else:
try:
self.bibdata.append(bibtex_parser.parse_file(str(p)))
except pybtex.database.PybtexError as e:
logger.warning(f"`bibtex` plugin failed to parse file `{k}`: {e}")
return
self.bibdata = utils.load(
kwargs["settings"].get("PYBTEX_SOURCES", []), [kwargs["path"]]
)

if not self.bibdata:
logger.info("`bibtex` plugin detected no entries.")
logger.info("`pybtex` (generator) plugin detected no entries.")
else:
sources = len(self.bibdata)
entries = sum([len(k.entries) for k in self.bibdata])
logger.info(
f"`bibtex` plugin detected {entries} entries spread across "
f"`pybtex` plugin detected {entries} entries spread across "
f"{sources} source file(s)."
)

# signals other interested parties on the same configuration
from .signals import pybtex_generator_init

pybtex_generator_init.send(self)

def generate_context(self):
"""Populate context with a list of BibTeX publications.
The generator context is modified to add a ``publications`` entry containing a
list dictionaries, each corresponding to a BibTeX entry (in the declared order),
with the following keys:
with at least the following keys:
* ``key``: The BibTeX database key
* ``year``: The year of the entry
* ``html``: An HTML-formatted version of the entry
* ``bibtex``: A BibTeX-formatted version of the entry
* ``pdf``: set if present on the BibTeX entry verbatim
* ``slides``: set if present on the BibTeX entry verbatim
* ``poster``: set if present on the BibTeX entry verbatim
"""
* ``bibtex``: An HTML-ready (pygments-highlighted) BibTeX-formatted version of
the entry
# format entries
plain_style = pybtex.style.formatting.plain.Style()
html_backend = pybtex.backends.html.Backend()

entries: list[
tuple[str, pybtex.database.Entry, pybtex.style.FormattedEntry]
] = []
for k in self.bibdata:
entries += zip( # noqa: B905
k.entries.keys(),
k.entries.values(),
plain_style.format_bibliography(k),
)

publications = []
for key, entry, text in entries:
# make entry text, and then pass it through pygments for highlighting
bibtex = pybtex.database.BibliographyData(entries={key: entry}).to_string(
"bibtex"
)
bibtex_html = pygments.highlight(
bibtex,
pygments.lexers.BibTeXLexer(),
pygments.formatters.HtmlFormatter(),
)
More keys as defined by ``PYBTEX_ADD_ENTRY_FIELDS`` may also be present in case
they are found in the original database entry. These fields are copied
verbatim to this dictionary.
"""

assert entry.fields is not None

extra_fields = {
k: v
for k, v in entry.fields.items()
if k in self.settings.get("PYBTEX_ADD_ENTRY_FIELDS", [])
}

publications.append(
{
"key": key,
"year": entry.fields.get("year"),
"html": text.text.render(html_backend),
"bibtex": bibtex_html,
}
)
self.context["publications"] = utils.generate_context(
self.bibdata,
self.settings.get("PYBTEX_FORMAT_STYLE", "plain"),
self.settings.get("PYBTEX_ADD_ENTRY_FIELDS", []),
)

publications[-1].update(extra_fields)
# get the right formatting for the date
default_timezone = self.settings.get("TIMEZONE", "UTC")
timezone = getattr(self, "timezone", default_timezone)
date = pelican.utils.set_date_tzinfo(datetime.datetime.now(), timezone)
date_format = self.settings["DEFAULT_DATE_FORMAT"]
if isinstance(date_format, tuple):
locale_string = date_format[0]
locale.setlocale(locale.LC_ALL, locale_string)
date_format = date_format[1]
locale_date = date.strftime(date_format)

self.context["publications"] = publications
self.context["now"] = __import__("datetime").datetime.now()
self.context["locale_date"] = locale_date

def generate_output(self, writer):
"""Generate a publication list on the website.
Expand All @@ -163,6 +126,7 @@ def generate_output(self, writer):

if not self.bibdata:
logger.info(f"Not generating `{template}.html` (no entries)")
return

save_as = self.settings.get(f"{template.upper()}_SAVE_AS", f"{template}.html")
url = self.settings.get(f"{template.upper()}_URL", f"{template}.html")
Expand Down
Loading

0 comments on commit 9e38ab8

Please sign in to comment.