Skip to content

Commit

Permalink
Support pre and post text in citation targets. (#316)
Browse files Browse the repository at this point in the history
  • Loading branch information
mcmtroffaes committed Aug 23, 2023
1 parent bff8ca9 commit 514231c
Show file tree
Hide file tree
Showing 24 changed files with 555 additions and 29 deletions.
13 changes: 13 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,19 @@
2.6.0 (in development)
----------------------

* Pre- and post-text in citations are now supported for the
author_year, label, and super referencing styles. The syntax is
``:cite:p:`{pre-text}key{post-text}``` (requested by RobertoBagnara,
see issue #288 and pull request #316).
Refer to the documentation for more details.

* New alternative style citations are now supported for the
author_year, label, and super parenthetical referencing styles,
which are identical to parenthetical citations but without the brackets.
The syntax is
``:cite:alp:`key``` (requested by davidorme, see pull request #316).
Refer to the documentation for more details.

* Exclude docutils 0.18 and 0.19 to fix generation of a spurious div tag in the
html builder (see issues #330, #329, #323, #322, #309).

Expand Down
41 changes: 41 additions & 0 deletions doc/usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,19 @@ Roles and Directives
The ones starting with ``c`` will capitalize the first letter.
The ones ending with ``s`` will give the full author list.

.. rst:role:: cite:alp
.. rst:role:: cite:alps
.. versionadded:: 2.6.0

These are identical to :rst:role:`cite:p` and :rst:role:`cite:ps`
but suppress brackets.
This is useful for instance when needing to add formatted pre-text or post-text.

.. seealso::

:ref:`section-pre-post-text`

.. rst:role:: cite
This is an alias for the :rst:role:`cite:p` role, and will create a
Expand Down Expand Up @@ -382,6 +395,34 @@ Markdown syntax. For example:
Advanced Features
-----------------

.. _section-pre-post-text:

Adding pre-text and post-text to citations
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

.. versionadded:: 2.6.0

You can add unformatted pre-text and post-text to any citation reference using the
following syntax:

.. code-block:: rest
The axioms were introduced by :cite:t:`{see}1977:nelson`.
The axioms were introduced by :cite:t:`1977:nelson{p. 1166}`.
The axioms were introduced by :cite:t:`{see}1977:nelson{p. 1166}`.
Pre- and post-text is not supported for footnote citations.

For formatted pre- and post-text in parenthetical citations,
you can use the :rst:role:`cite:alp` and :rst:role:`cite:alps` roles.
These roles suppress the brackets, leaving it to you to add them in the right
format and place:

.. code-block:: rest
The three new axioms [the *IST axioms*, :cite:alp:`1977:nelson`] are discussed next.
Splitting Bibliographies Per Bib File
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Expand Down
37 changes: 37 additions & 0 deletions src/sphinxcontrib/bibtex/citation_target.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
"""Citation keys."""

import re

from typing import NamedTuple, Iterable


class CitationTarget(NamedTuple):
"""Citation key, pre-text, and post-text."""

key: str
pre: str
post: str


_re_citation_target = re.compile(
r"\s*([{](?P<pre>[^{}]+)[}])?"
r"\s*(?P<key>[^{}\s,]+)"
r"\s*([{](?P<post>[^{}]+)[}])?\s*"
)


def parse_citation_targets(targets: str, pos=0) -> Iterable[CitationTarget]:
"""Parse citation target string into a list of citation keys."""
match = _re_citation_target.match(targets, pos=pos)
if match is None:
raise ValueError(f"malformed citation target: {targets}")
yield CitationTarget(
key=match.group("key") or "",
pre=match.group("pre") or "",
post=match.group("post") or "",
)
end = match.end()
if end < len(targets):
if targets[end] != ",":
raise ValueError(f"malformed citation target: {targets}")
yield from parse_citation_targets(targets, end + 1)
12 changes: 8 additions & 4 deletions src/sphinxcontrib/bibtex/domain.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
from sphinx.errors import ExtensionError
from sphinx.locale import _

from .citation_target import parse_citation_targets, CitationTarget
from .roles import CiteRole
from .bibfile import normpath_filename, process_bibdata, BibData
from .style.referencing import BaseReferenceStyle, format_references
Expand Down Expand Up @@ -395,7 +396,8 @@ def resolve_xref(
contnode: docutils.nodes.Element,
) -> docutils.nodes.Element:
"""Replace node by list of citation references (one for each key)."""
keys = [key.strip() for key in target.split(",")]
targets = parse_citation_targets(target)
keys: Dict[str, CitationTarget] = {target2.key: target2 for target2 in targets}
citations: Dict[str, Citation] = {
cit.key: cit
for cit in self.citations
Expand Down Expand Up @@ -427,6 +429,8 @@ def resolve_xref(
if citation.tooltip_entry
else None
),
pre_text=keys[citation.key].pre,
post_text=keys[citation.key].post,
),
)
for citation in citations.values()
Expand Down Expand Up @@ -470,8 +474,8 @@ def get_all_cited_keys(self, docnames):
for citation_ref in sorted(
self.citation_refs, key=lambda c: docnames.index(c.docname)
):
for key in citation_ref.keys:
yield key
for target in citation_ref.targets:
yield target.key

def get_entries(self, bibfiles: List[str]) -> Iterable["Entry"]:
"""Return all bibliography entries from the bib files, unsorted (i.e.
Expand All @@ -493,7 +497,7 @@ def get_filtered_entries(
cited_docnames = {
citation_ref.docname
for citation_ref in self.citation_refs
if key in citation_ref.keys
if key in {target.key for target in citation_ref.targets}
}
visitor = _FilterVisitor(
entry=entry,
Expand Down
5 changes: 3 additions & 2 deletions src/sphinxcontrib/bibtex/roles.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"""

import docutils.nodes
from .citation_target import CitationTarget, parse_citation_targets

from typing import TYPE_CHECKING, cast, NamedTuple, List
from pybtex.plugin import find_plugin
Expand All @@ -24,7 +25,7 @@ class CitationRef(NamedTuple):
citation_ref_id: str #: Unique id of this citation reference.
docname: str #: Document name.
line: int #: Line number.
keys: List[str] #: Citation keys (including key prefix).
targets: List[CitationTarget] #: Citation targets (key, pre, post).


class CiteRole(XRefRole):
Expand All @@ -49,7 +50,7 @@ def result_nodes(self, document, env, node, is_ref):
citation_ref_id=node["ids"][0],
docname=env.docname,
line=document.line,
keys=[key.strip() for key in self.target.split(",")],
targets=list(parse_citation_targets(self.target)),
)
)
return [node], []
10 changes: 10 additions & 0 deletions src/sphinxcontrib/bibtex/style/referencing/author_year.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,18 +53,28 @@ class AuthorYearReferenceStyle(GroupReferenceStyle):
#: Separator between text and reference for textual citations.
text_reference_sep: Union["BaseText", str] = " "

#: Separator between pre-text and citation.
pre_text_sep: Union["BaseText", str] = " "

#: Separator between citation and post-text.
post_text_sep: Union["BaseText", str] = ", "

def __post_init__(self):
self.styles.extend(
[
BasicAuthorYearParentheticalReferenceStyle(
bracket=self.bracket_parenthetical,
person=self.person,
author_year_sep=self.author_year_sep,
pre_text_sep=self.pre_text_sep,
post_text_sep=self.post_text_sep,
),
BasicAuthorYearTextualReferenceStyle(
bracket=self.bracket_textual,
person=self.person,
text_reference_sep=self.text_reference_sep,
pre_text_sep=self.pre_text_sep,
post_text_sep=self.post_text_sep,
),
ExtraAuthorReferenceStyle(
bracket=self.bracket_author, person=self.person
Expand Down
52 changes: 44 additions & 8 deletions src/sphinxcontrib/bibtex/style/referencing/basic_author_year.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
from dataclasses import dataclass, field
from typing import TYPE_CHECKING, List, Iterable, Union
from sphinxcontrib.bibtex.style.template import reference, join, year
from sphinxcontrib.bibtex.style.template import (
reference,
join,
year,
pre_text,
post_text,
join2,
)
from . import BaseReferenceStyle, BracketStyle, PersonStyle

if TYPE_CHECKING:
Expand All @@ -21,17 +28,32 @@ class BasicAuthorYearParentheticalReferenceStyle(BaseReferenceStyle):
#: Separator between author and year.
author_year_sep: Union["BaseText", str] = ", "

#: Separator between pre-text and citation.
pre_text_sep: Union["BaseText", str] = " "

#: Separator between citation and post-text.
post_text_sep: Union["BaseText", str] = ", "

def role_names(self) -> Iterable[str]:
return [f"p{full_author}" for full_author in ["", "s"]]
return [
f"{alt}p{full_author}" for alt in ["", "al"] for full_author in ["", "s"]
]

def outer(self, role_name: str, children: List["BaseText"]) -> "Node":
return self.bracket.outer(children, brackets=True, capfirst=False)
return self.bracket.outer(
children, brackets="al" not in role_name, capfirst=False
)

def inner(self, role_name: str) -> "Node":
return reference[
join(sep=self.author_year_sep)[
self.person.author_or_editor_or_title(full="s" in role_name), year
]
return join2(sep1=self.pre_text_sep, sep2=self.post_text_sep)[
pre_text,
reference[
join(sep=self.author_year_sep)[
self.person.author_or_editor_or_title(full="s" in role_name),
year,
]
],
post_text,
]


Expand All @@ -48,6 +70,12 @@ class BasicAuthorYearTextualReferenceStyle(BaseReferenceStyle):
#: Separator between text and reference.
text_reference_sep: Union["BaseText", str] = " "

#: Separator between pre-text and citation.
pre_text_sep: Union["BaseText", str] = " "

#: Separator between citation and post-text.
post_text_sep: Union["BaseText", str] = ", "

def role_names(self) -> Iterable[str]:
return [
f"{capfirst}t{full_author}"
Expand All @@ -61,5 +89,13 @@ def outer(self, role_name: str, children: List["BaseText"]) -> "Node":
def inner(self, role_name: str) -> "Node":
return join(sep=self.text_reference_sep)[
self.person.author_or_editor_or_title(full="s" in role_name),
join[self.bracket.left, reference[year], self.bracket.right],
join[
self.bracket.left,
join2(sep1=self.pre_text_sep, sep2=self.post_text_sep)[
pre_text,
reference[year],
post_text,
],
self.bracket.right,
],
]
45 changes: 40 additions & 5 deletions src/sphinxcontrib/bibtex/style/referencing/basic_label.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
from dataclasses import dataclass, field

from typing import TYPE_CHECKING, List, Iterable, Union
from sphinxcontrib.bibtex.style.template import reference, entry_label, join
from sphinxcontrib.bibtex.style.template import (
reference,
entry_label,
join,
pre_text,
post_text,
join2,
)
from . import BracketStyle, PersonStyle, BaseReferenceStyle

if TYPE_CHECKING:
Expand All @@ -18,14 +25,28 @@ class BasicLabelParentheticalReferenceStyle(BaseReferenceStyle):
#: Bracket style.
bracket: BracketStyle = field(default_factory=BracketStyle)

#: Separator between pre-text and citation.
pre_text_sep: Union["BaseText", str] = " "

#: Separator between citation and post-text.
post_text_sep: Union["BaseText", str] = ", "

def role_names(self) -> Iterable[str]:
return [f"p{full_author}" for full_author in ["", "s"]]
return [
f"{alt}p{full_author}" for alt in ["", "al"] for full_author in ["", "s"]
]

def outer(self, role_name: str, children: List["BaseText"]) -> "Node":
return self.bracket.outer(children, brackets=True, capfirst=False)
return self.bracket.outer(
children, brackets="al" not in role_name, capfirst=False
)

def inner(self, role_name: str) -> "Node":
return reference[entry_label]
return join2(sep1=self.pre_text_sep, sep2=self.post_text_sep)[
pre_text,
reference[entry_label],
post_text,
]


@dataclass
Expand All @@ -43,6 +64,12 @@ class BasicLabelTextualReferenceStyle(BaseReferenceStyle):
#: Separator between text and reference.
text_reference_sep: Union["BaseText", str] = " "

#: Separator between pre-text and citation.
pre_text_sep: Union["BaseText", str] = " "

#: Separator between citation and post-text.
post_text_sep: Union["BaseText", str] = ", "

def role_names(self) -> Iterable[str]:
return [
f"{capfirst}t{full_author}"
Expand All @@ -56,5 +83,13 @@ def outer(self, role_name: str, children: List["BaseText"]) -> "Node":
def inner(self, role_name: str) -> "Node":
return join(sep=self.text_reference_sep)[
self.person.author_or_editor_or_title(full="s" in role_name),
join[self.bracket.left, reference[entry_label], self.bracket.right],
join[
self.bracket.left,
join2(sep1=self.pre_text_sep, sep2=self.post_text_sep)[
pre_text,
reference[entry_label],
post_text,
],
self.bracket.right,
],
]

0 comments on commit 514231c

Please sign in to comment.