From 7117d92eed6f1e08cd8598a4c22c1a713804288e Mon Sep 17 00:00:00 2001 From: Dennis den Ouden-van der Horst Date: Mon, 17 Nov 2025 16:48:07 +0100 Subject: [PATCH 1/2] Switch to sphinx-apa-references extension for APA style Removed local APA style extensions and related static files, updated _config.yml to use the sphinx_apa_references extension, and revised documentation in apa.md to reflect installation and usage of the new package. Added sphinx-apa-references to requirements.txt and references.bib now includes a multi-author example for APA citation demonstration. --- book/_config.yml | 4 +- book/_ext/apastyle.py | 17 - book/_ext/bracket_citation_style.py | 27 - book/_ext/pybtexapastyle/LICENSE | 21 - .../pybtexapastyle/formatting/__init__.py | 0 book/_ext/pybtexapastyle/formatting/apa.py | 486 ------------------ .../pybtex/style/formatting/__init__.py | 69 --- .../pybtex/style/sorting/__init__.py | 12 - .../pybtex/style/sorting/author_year_title.py | 39 -- book/_ext/pybtexapastyle/labels/__init__.py | 0 book/_ext/pybtexapastyle/labels/apa.py | 67 --- book/_ext/pybtexapastyle/names/__init__.py | 0 book/_ext/pybtexapastyle/names/firstlast.py | 39 -- book/_ext/pybtexapastyle/setup.py | 27 - book/_static/apastyle.css | 3 - book/features/apa.md | 105 ++-- book/features/apa_config.zip | Bin 12280 -> 0 bytes book/references.bib | 11 +- requirements.txt | 4 +- 19 files changed, 54 insertions(+), 877 deletions(-) delete mode 100644 book/_ext/apastyle.py delete mode 100644 book/_ext/bracket_citation_style.py delete mode 100644 book/_ext/pybtexapastyle/LICENSE delete mode 100644 book/_ext/pybtexapastyle/formatting/__init__.py delete mode 100644 book/_ext/pybtexapastyle/formatting/apa.py delete mode 100644 book/_ext/pybtexapastyle/formatting/pybtex/style/formatting/__init__.py delete mode 100644 book/_ext/pybtexapastyle/formatting/pybtex/style/sorting/__init__.py delete mode 100644 book/_ext/pybtexapastyle/formatting/pybtex/style/sorting/author_year_title.py delete mode 100644 book/_ext/pybtexapastyle/labels/__init__.py delete mode 100644 book/_ext/pybtexapastyle/labels/apa.py delete mode 100644 book/_ext/pybtexapastyle/names/__init__.py delete mode 100644 book/_ext/pybtexapastyle/names/firstlast.py delete mode 100644 book/_ext/pybtexapastyle/setup.py delete mode 100644 book/_static/apastyle.css delete mode 100644 book/features/apa_config.zip diff --git a/book/_config.yml b/book/_config.yml index a81c825..809a809 100644 --- a/book/_config.yml +++ b/book/_config.yml @@ -16,6 +16,7 @@ sphinx: - sphinx.ext.imgconverter - teachbooks_favourites - teachbooks_sphinx_grasple + - sphinx_apa_references config: html_js_files: - https://cdnjs.cloudflare.com/ajax/libs/require.js/2.3.4/require.min.js @@ -45,9 +46,6 @@ sphinx: macros: "newcolor": ['\definecolor{newcolor}{RGB}{111, 29, 119}{\color{newcolor}#1}',1] named_colors_custom_colors: {'onlylight':[165,21,160],'lightanddark':[45,180,117,204,158,110],'hyphen-color':[45,180,117,165,21,160],'attributiongrey':[150,150,150]} - local_extensions: - apastyle: _ext/ - bracket_citation_style: _ext/ teachbooks: attribution_location: margin diff --git a/book/_ext/apastyle.py b/book/_ext/apastyle.py deleted file mode 100644 index 16828fe..0000000 --- a/book/_ext/apastyle.py +++ /dev/null @@ -1,17 +0,0 @@ - -# from pybtex.style.formatting.unsrt import Style -from pybtexapastyle.formatting.apa import APAStyle -from pybtexapastyle.labels.apa import LabelStyle as APALabelStyle -from pybtex.plugin import register_plugin -# from pybtex.style.template import names, sentence - -class MyAPALabelStyle(APALabelStyle): - def format_label(self, entry): - return APALabelStyle.format_label(self, entry) - -class MyAPAStyle(APAStyle): - default_label_style = 'myapa' - -def setup(app): - register_plugin('pybtex.style.labels', 'myapa', MyAPALabelStyle) - register_plugin('pybtex.style.formatting', 'myapastyle', MyAPAStyle) \ No newline at end of file diff --git a/book/_ext/bracket_citation_style.py b/book/_ext/bracket_citation_style.py deleted file mode 100644 index cab10f3..0000000 --- a/book/_ext/bracket_citation_style.py +++ /dev/null @@ -1,27 +0,0 @@ -from dataclasses import dataclass, field -import sphinxcontrib.bibtex.plugin - -from sphinxcontrib.bibtex.style.referencing import BracketStyle -from sphinxcontrib.bibtex.style.referencing.author_year import AuthorYearReferenceStyle - - -def bracket_style() -> BracketStyle: - return BracketStyle( - left='(', - right=')', - ) - - -@dataclass -class MyReferenceStyle(AuthorYearReferenceStyle): - bracket_parenthetical: BracketStyle = field(default_factory=bracket_style) - bracket_textual: BracketStyle = field(default_factory=bracket_style) - bracket_author: BracketStyle = field(default_factory=bracket_style) - bracket_label: BracketStyle = field(default_factory=bracket_style) - bracket_year: BracketStyle = field(default_factory=bracket_style) - - -def setup(app): - sphinxcontrib.bibtex.plugin.register_plugin( - 'sphinxcontrib.bibtex.style.referencing', - 'author_year_round', MyReferenceStyle) \ No newline at end of file diff --git a/book/_ext/pybtexapastyle/LICENSE b/book/_ext/pybtexapastyle/LICENSE deleted file mode 100644 index 579d367..0000000 --- a/book/_ext/pybtexapastyle/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2018 Naeka - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. \ No newline at end of file diff --git a/book/_ext/pybtexapastyle/formatting/__init__.py b/book/_ext/pybtexapastyle/formatting/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/book/_ext/pybtexapastyle/formatting/apa.py b/book/_ext/pybtexapastyle/formatting/apa.py deleted file mode 100644 index dfa5a58..0000000 --- a/book/_ext/pybtexapastyle/formatting/apa.py +++ /dev/null @@ -1,486 +0,0 @@ -# -*- coding:Utf-8 -*- -from __future__ import unicode_literals - -import re -import six - -from pybtex.plugin import find_plugin -from pybtex.style.formatting import BaseStyle, toplevel -from pybtex.style.template import ( - field, first_of, href, join, optional, optional_field, sentence, tag, - together, words, node, FieldIsMissing -) - -from pybtex.richtext import Text, Symbol - - -firstlast = find_plugin('pybtex.style.names', 'lastfirst')() - -if six.PY2: - def format_pages(text): - dash_re = re.compile(r'-+') - pages = Text(Symbol('ndash')).join( - re.split(dash_re, six.text_type(text))) - if re.search('[-‒–—―]', six.text_type(text)): - return Text("pp.", Symbol('nbsp'), pages) - return Text("p.", Symbol('nbsp'), pages) -else: - def format_pages(text): - dash_re = re.compile(r'-+') - pages = Text(Symbol('ndash')).join(text.split(dash_re)) - if re.search('[-‒–—―]', str(text)): - return Text("pp.", Symbol('nbsp'), pages) - return Text("p.", Symbol('nbsp'), pages) - - -pages = field('pages', apply_func=format_pages) -date = words[field('year'), optional[", ", field('month')]] - - -@node -def apa_names(children, context, role, **kwargs): - """ - Returns formatted names as an APA compliant reference list citation. - """ - assert not children - - try: - persons = context['entry'].persons[role] - except KeyError: - raise FieldIsMissing(role, context['entry']) - - style = context['style'] - - if len(persons) > 7: - persons = persons[:6] + persons[-1:] - formatted_names = [style.format_name( - person, style.abbreviate_names) for person in persons] - return join(sep=', ', last_sep=', … ')[ - formatted_names].format_data(context) - else: - formatted_names = [style.format_name( - person, style.abbreviate_names) for person in persons] - return join(sep=', ', sep2=', & ', last_sep=', & ')[ - formatted_names].format_data(context) - - -@node -def editor_names(children, context, with_suffix=True, **kwargs): - """ - Returns formatted editor names for inbook. - """ - assert not children - - try: - editors = context['entry'].persons['editor'] - except KeyError: - raise FieldIsMissing('editor', context['entry']) - - formatted_names = [ - firstlast.format(editor, True) for editor in editors] - - if with_suffix: - return words[ - join(sep=', ', sep2=', & ', last_sep=', & ')[formatted_names], - "(Eds.)" if len(editors) > 1 else "(Ed.)" - ].format_data(context) - - return join(sep=', ', sep2=', & ', last_sep=', & ')[ - formatted_names - ].format_data(context) - - -class APAStyle(BaseStyle): - name = 'apa' - default_name_style = 'lastfirst' - default_sorting_style = 'author_year_title' - default_label_style = 'apa' - - def __init__(self, *args, **kwargs): - super(APAStyle, self).__init__(*args, **kwargs) - self.abbreviate_names = True - - def format_names(self, role, as_sentence=True): - formatted_names = apa_names(role) - if as_sentence: - return sentence(capfirst=False)[formatted_names] - else: - return formatted_names - - def format_author_or_editor_and_date(self, e): - if 'author' in e.persons and 'editor' in e.persons: - return sentence(sep=' ')[ - self.format_names('author'), join["(", date, ")."], - self.format_editor(e, as_sentence=False)] - elif 'author' in e.persons: - return sentence(sep=' ')[ - self.format_names('author'), join["(", date, ")"]] - else: - return sentence(sep=' ')[ - self.format_editor(e, as_sentence=False), - join["(", date, ")"]] - - def format_editor(self, e, as_sentence=True): - editors = self.format_names('editor', as_sentence=False) - if 'editor' not in e.persons: - # when parsing the template, a FieldIsMissing exception - # will be thrown anyway; no need to do anything now, - # just return the template that will throw the exception - return editors - if len(e.persons['editor']) > 1: - word = '(Eds.)' - else: - word = '(Ed.)' - result = join(sep=' ')[editors, word] - if as_sentence: - return sentence[result] - else: - return result - - def format_volume(self, e, for_article=False): - prefix = "Vol." - if for_article: - return join[ - tag('em')[field('volume')], - optional['(', field('number'), ')'], - ] - else: - return optional[together[prefix, field('volume')]] - - def format_title(self, e, which_field, as_sentence=True): - formatted_title = field( - which_field, apply_func=lambda text: text.capitalize() - ) - if as_sentence: - return sentence[formatted_title] - else: - return formatted_title - - def format_btitle(self, e, which_field, as_sentence=True): - formatted_title = tag('em')[field(which_field)] - if as_sentence: - return sentence[formatted_title] - else: - return formatted_title - - def format_web_refs(self, e): - # Based on urlbst output.web.refs - return sentence(add_period=False)[ - optional[self.format_url(e)], - optional[self.format_eprint(e)], - optional[self.format_pubmed(e)], - optional[self.format_doi(e)], - ] - - def format_url(self, e): - # Based on urlbst format.url - return words[ - 'URL:', - href[ - field('url', raw=True), - field('url', raw=True) - ] - ] - - def format_pubmed(self, e): - # Based on urlbst format.pubmed - return href[ - join[ - 'https://www.ncbi.nlm.nih.gov/pubmed/', - field('pubmed', raw=True) - ], - join[ - 'PMID:', - field('pubmed', raw=True) - ] - ] - - def format_doi(self, e): - # Based on urlbst format.doi - return href[ - join[ - 'https://doi.org/', - field('doi', raw=True) - ], - join[ - 'doi:', - field('doi', raw=True) - ] - ] - - def format_eprint(self, e): - # Based on urlbst format.eprint - return href[ - join[ - 'https://arxiv.org/abs/', - field('eprint', raw=True) - ], - join[ - 'arXiv:', - field('eprint', raw=True) - ] - ] - - def get_article_template(self, e): - # Required fields: author, title, journal, year - # Optional fields: volume, number, pages, month, note, key - volume_and_pages = first_of[ - # Volume and pages, with optional issue number - optional[ - join[ - self.format_volume(e, for_article=True), - optional[', ', field('pages')] - ], - ], - # Pages only - pages, - ] - template = toplevel[ - self.format_names('author'), - sentence[ - join["(", date, ")"] - ], - self.format_title(e, 'title'), - sentence[ - tag('em')[field('journal')], - optional[volume_and_pages], - ], - sentence[optional_field('note')], - self.format_web_refs(e), - ] - return template - - def get_book_template(self, e): - # Required fields: author/editor, title, publisher, year - # Optional fields: volume, series, address, edition, month, note, key, - # isbn - return toplevel[ - self.format_author_or_editor_and_date(e), - sentence(sep=' ')[ - self.format_btitle(e, 'title'), - optional[ - sentence[ - optional[field('edition'), ' ed.'], - self.format_volume(e), - ] - ] - ], - sentence(sep=': ')[ - optional_field('address'), - field('publisher'), - ], - sentence[optional_field('note')], - ] - - def get_booklet_template(self, e): - # Required fields: title - # Optional fields: author, howpublished, address, month, year, note, - # key - return toplevel[ - sentence(sep=' ')[ - first_of[ - optional[self.format_names( - 'author', as_sentence=False)], - "None to claim their bones" - ], - join["(", first_of[optional[date], "n.d."], ")"] - ], - self.format_title(e, 'title'), - sentence[optional_field('address')], - sentence[optional_field('note')], - ] - - def get_inbook_template(self, e): - # Required fields: author/editor, title, chapter/pages, publisher, year - # Optional fields: volume, series, address, edition, month, note, key - return toplevel[ - sentence(sep=' ')[ - self.format_names('author'), - join["(", date, ")"] - ], - self.format_title(e, 'title'), - sentence(sep=' ')[ - optional[ - "In ", - editor_names(), - "," - ], - self.format_btitle(e, 'booktitle', as_sentence=False), - optional[ - join[ - "(", - sentence(add_period=False)[ - optional[field('edition'), ' ed.'], - self.format_volume(e), - pages, - ], - ")" - ] - ] - ], - sentence(sep=': ')[ - optional_field('address'), - field('publisher'), - ], - sentence[optional_field('note')], - ] - - def get_incollection_template(self, e): - # Required fields: author, title, booktitle, year - # Optional fields: editor, pages, organization, publisher, address, - # month, note, key - return toplevel[ - self.format_author_or_editor_and_date(e), - self.format_title(e, 'title'), - sentence(sep=' ')[ - self.format_btitle(e, 'booktitle', as_sentence=False), - optional["(", pages, ")"] - ], - sentence(sep=': ')[ - optional_field('address'), - optional_field('publisher'), - ], - sentence[optional_field('note')], - ] - - def get_inproceedings_template(self, e): - # Required fields: author, title, booktitle, year - # Optional fields: editor, pages, organization, publisher, address, - # month, note, key - return toplevel[ - self.format_author_or_editor_and_date(e), - self.format_title(e, 'title'), - sentence(sep=' ')[ - self.format_btitle(e, 'booktitle', as_sentence=False), - optional["(", pages, ")"] - ], - sentence(sep=': ')[ - optional_field('address'), - optional_field('publisher'), - ], - sentence[optional_field('note')], - self.format_web_refs(e) - ] - - def get_manual_template(self, e): - # Required fields: title - # Optional fields: author, organization, address, edition, month, year, - # note, key - return toplevel[ - sentence(sep=' ')[ - first_of[ - optional[self.format_names('author', as_sentence=False)], - optional_field('organization'), - "None to claim their bones" - ], - join["(", first_of[optional[date], "n.d."], ")"] - ], - self.format_btitle(e, 'title'), - sentence[optional_field('address')], - sentence[optional_field('note')], - self.format_web_refs(e) - ] - - def get_mastersthesis_template(self, e): - # Required fields: author, title, school, year - # Optional fields: address, month, note, key - return toplevel[ - sentence(sep=' ')[ - self.format_names('author'), - join["(", date, ")"] - ], - sentence(sep=' ')[ - self.format_btitle(e, 'title', as_sentence=False), - "(Master's thesis)" - ], - sentence[ - field('school'), - optional_field('address'), - ], - sentence[optional_field('note')], - ] - - def get_misc_template(self, e): - # Required fields: aucun - # Optional fields: author, title, howpublished, month, year, note, key, - # type - template = toplevel[ - sentence(sep=' ')[ - first_of[ - optional[self.format_names('author', as_sentence=False)], - optional_field('publisher'), - "None to claim their bones" - ], - join["(", first_of[optional[date], "n.d."], ")"] - ], - optional[self.format_title(e, 'title')], - sentence[optional_field('note')], - self.format_web_refs(e) - ] - return template - - def get_phdthesis_template(self, e): - # Required fields: author, title, school, year - # Optional fields: address, month, note, key - return toplevel[ - sentence(sep=' ')[ - self.format_names('author'), - join["(", date, ")"] - ], - sentence(sep=' ')[ - self.format_btitle(e, 'title', as_sentence=False), - "(Doctoral dissertation)" - ], - sentence[ - field('school'), - optional_field('address'), - ], - sentence[optional_field('note')], - ] - - def get_proceedings_template(self, e): - # Required fields: title, year - # Optional fields: editor, publisher, organization, address, month, - # note, key - return toplevel[ - sentence(sep=' ')[ - first_of[ - optional[self.format_editor(e, as_sentence=False)], - optional_field('organization'), - "None to claim their bones" - ], - join["(", first_of[optional[date], "n.d."], ")"] - ], - self.format_btitle(e, 'title'), - sentence(sep=': ')[ - optional_field('address'), - optional_field('publisher'), - ], - sentence[optional_field('note')], - ] - - def get_techreport_template(self, e): - # Required fields: author, title, institution, year - # Optional fields: type, number, address, month, note, key - return toplevel[ - sentence(sep=' ')[ - self.format_names('author', as_sentence=False), - join["(", date, ")"] - ], - self.format_btitle(e, 'title'), - sentence[field('institution')], - sentence[optional_field('note')], - ] - - def get_unpublished_template(self, e): - # Required fields: author, title, note - # Optional fields: month, year, key - template = toplevel[ - sentence(sep=' ')[ - self.format_names('author', as_sentence=False), - join["(", first_of[optional[date], "n.d."], ")"] - ], - self.format_btitle(e, 'title'), - sentence[field('note')], - ] - return template diff --git a/book/_ext/pybtexapastyle/formatting/pybtex/style/formatting/__init__.py b/book/_ext/pybtexapastyle/formatting/pybtex/style/formatting/__init__.py deleted file mode 100644 index 5ac42b8..0000000 --- a/book/_ext/pybtexapastyle/formatting/pybtex/style/formatting/__init__.py +++ /dev/null @@ -1,69 +0,0 @@ -from __future__ import unicode_literals - -from pybtex.style import FormattedEntry, FormattedBibliography -from pybtex.style.template import node, join -from pybtex.richtext import Symbol -from pybtex.plugin import Plugin, find_plugin - - -@node -def toplevel(children, data): - return join(sep=Symbol('newblock')) [children].format_data(data) - - -class BaseStyle(Plugin): - """ - The base class for pythonic formatting styles. - """ - - default_name_style = None - default_label_style = None - default_sorting_style = None - - def __init__(self, label_style=None, name_style=None, sorting_style=None, abbreviate_names=False, min_crossrefs=2, **kwargs): - self.name_style = find_plugin('pybtex.style.names', name_style or self.default_name_style)() - self.label_style = find_plugin('pybtex.style.labels', label_style or self.default_label_style)() - self.sorting_style = find_plugin('pybtex.style.sorting', sorting_style or self.default_sorting_style)() - self.format_name = self.name_style.format - self.format_labels = self.label_style.format_labels - self.sort = self.sorting_style.sort - self.abbreviate_names = abbreviate_names - self.min_crossrefs = min_crossrefs - - def format_entries(self, entries, bib_data=None): - sorted_entries = self.sort(entries) - labels = self.format_labels(sorted_entries) - for label, entry in zip(labels, sorted_entries): - yield self.format_entry(label, entry, bib_data=bib_data) - - def format_entry(self, label, entry, bib_data=None): - context = { - 'entry': entry, - 'style': self, - 'bib_data': bib_data, - } - try: - get_template = getattr(self, 'get_{}_template'.format(entry.type)) - except AttributeError: - format_method = getattr(self, "format_" + entry.type) - text = format_method(context) - else: - text = get_template(entry).format_data(context) - return FormattedEntry(entry.key, text, label) - - def format_bibliography(self, bib_data, citations=None): - """ - Format bibliography entries with the given keys and return a - ``FormattedBibliography`` object. - - :param bib_data: A :py:class:`pybtex.database.BibliographyData` object. - :param citations: A list of citation keys. - """ - - if citations is None: - citations = list(bib_data.entries.keys()) - citations = bib_data.add_extra_citations(citations, self.min_crossrefs) - entries = [bib_data.entries[key] for key in citations] - formatted_entries = self.format_entries(entries) - formatted_bibliography = FormattedBibliography(formatted_entries, style=self, preamble=bib_data.preamble) - return formatted_bibliography diff --git a/book/_ext/pybtexapastyle/formatting/pybtex/style/sorting/__init__.py b/book/_ext/pybtexapastyle/formatting/pybtex/style/sorting/__init__.py deleted file mode 100644 index 3430087..0000000 --- a/book/_ext/pybtexapastyle/formatting/pybtex/style/sorting/__init__.py +++ /dev/null @@ -1,12 +0,0 @@ -from __future__ import unicode_literals - - -from pybtex.plugin import Plugin - - -class BaseSortingStyle(Plugin): - def sorting_key(self, entry): - raise NotImplementedError - - def sort(self, entries): - return sorted(entries, key=self.sorting_key) \ No newline at end of file diff --git a/book/_ext/pybtexapastyle/formatting/pybtex/style/sorting/author_year_title.py b/book/_ext/pybtexapastyle/formatting/pybtex/style/sorting/author_year_title.py deleted file mode 100644 index c315c0c..0000000 --- a/book/_ext/pybtexapastyle/formatting/pybtex/style/sorting/author_year_title.py +++ /dev/null @@ -1,39 +0,0 @@ -from __future__ import unicode_literals - -from pybtex.style.sorting import BaseSortingStyle - -class SortingStyle(BaseSortingStyle): - - def sorting_key(self, entry): - if entry.type in ('book', 'inbook'): - author_key = self.author_editor_key(entry) - elif 'author' in entry.persons: - author_key = self.persons_key(entry.persons['author']) - else: - author_key = '' - return (author_key, entry.fields.get('year', ''), entry.fields.get('title', '')) - - def persons_key(self, persons): - return ' '.join(self.person_key(person) for person in persons) - - def person_key(self, person): - if person.prelast_names[0] != '{' and person.last_names[0] != '{' and person.first_names[0] != '{': - return ' '.join(( - ' '.join(person.prelast_names + person.last_names), - ' '.join(person.first_names + person.middle_names), - ' '.join(person.lineage_names), - )).lower() - else: - return ' '.join(( - ' '.join(person.first_names + person.middle_names), - ' '.join(person.prelast_names + person.last_names), - ' '.join(person.lineage_names), - )).lower() - - def author_editor_key(self, entry): - if entry.persons.get('author'): - return self.persons_key(entry.persons['author']) - elif entry.persons.get('editor'): - return self.persons_key(entry.persons['editor']) - else: - return '' diff --git a/book/_ext/pybtexapastyle/labels/__init__.py b/book/_ext/pybtexapastyle/labels/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/book/_ext/pybtexapastyle/labels/apa.py b/book/_ext/pybtexapastyle/labels/apa.py deleted file mode 100644 index 3632dc6..0000000 --- a/book/_ext/pybtexapastyle/labels/apa.py +++ /dev/null @@ -1,67 +0,0 @@ -# -*- coding:Utf-8 -*- -from __future__ import unicode_literals - -from collections import Counter -import re -import unicodedata - -from pybtex.style.labels import BaseLabelStyle - - -_nonalnum_pattern = re.compile('[^A-Za-z0-9 \-]+', re.UNICODE) - - -def _strip_accents(s): - return "".join( - (c for c in unicodedata.normalize('NFD', s) - if not unicodedata.combining(c))) - - -def _strip_nonalnum(parts): - """Strip all non-alphanumerical characters from a list of strings. - - >>> print(_strip_nonalnum([u"ÅA. B. Testing 12+}[.@~_", u" 3%"])) - AABTesting123 - """ - s = "".join(parts) - return _nonalnum_pattern.sub("", _strip_accents(s)) - - -class LabelStyle(BaseLabelStyle): - def format_labels(self, sorted_entries): - labels = [self.format_label(entry) for entry in sorted_entries] - count = Counter(labels) - counted = Counter() - for label in labels: - if count[label] == 1: - yield label - else: - yield label # + chr(ord('a') + counted[label]) - # counted.update([label]) - - def format_label(self, entry): - label = "Anonymous" - if 'author' in entry.persons: - label = self.format_author_or_editor_names(entry.persons['author']) - elif 'editor' in entry.persons: - label = self.format_author_or_editor_names(entry.persons['editor']) - elif 'organization' in entry.fields: - label = entry.fields['organization'] - if label.startswith("The "): - label = label[4:] - return '' - # if 'year' in entry.fields: - # return "{}, {}".format(label, entry.fields['year']) - # else: - # return "{}, n.d.".format(label) - - def format_author_or_editor_names(self, persons): - if len(persons)==1: - return _strip_nonalnum(persons[0].last_names) - elif len(persons)==2: - return "{} & {}".format( - _strip_nonalnum(persons[0].last_names), - _strip_nonalnum(persons[1].last_names)) - else: - return "{} et al.".format( - _strip_nonalnum(persons[0].last_names)) diff --git a/book/_ext/pybtexapastyle/names/__init__.py b/book/_ext/pybtexapastyle/names/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/book/_ext/pybtexapastyle/names/firstlast.py b/book/_ext/pybtexapastyle/names/firstlast.py deleted file mode 100644 index 8f23263..0000000 --- a/book/_ext/pybtexapastyle/names/firstlast.py +++ /dev/null @@ -1,39 +0,0 @@ -# -*- coding:Utf-8 -*- -from __future__ import unicode_literals - -from pybtex.style.names import BaseNameStyle, name_part -from pybtex.style.template import join - - -class NameStyle(BaseNameStyle): - - def format(self, person, abbr=False): - r""" - Format names similarly to {vv~}{ll}{, jj}{, f.} in BibTeX. - - >>> from pybtex.database import Person - >>> name = Person(string=r"Charles Louis Xavier Joseph de la Vall{\'e}e Poussin") - >>> firstlast = NameStyle().format - - >>> print(firstlast(name).format().render_as('latex')) - Charles Louis Xavier~Joseph de~la Vall{é}e~Poussin - >>> print(firstlast(name).format().render_as('html')) - Charles Louis Xavier Joseph de la Vallée Poussin - - >>> print(firstlast(name, abbr=True).format().render_as('latex')) - C.~L. X.~J. de~la Vall{é}e~Poussin - >>> print(firstlast(name, abbr=True).format().render_as('html')) - C. L. X. J. de la Vallée Poussin - - >>> name = Person(first='First', last='Last', middle='Middle') - >>> print(firstlast(name).format().render_as('latex')) - First~Middle Last - >>> print(firstlast(name, abbr=True).format().render_as('latex')) - F.~M. Last - - """ - return join[ - name_part(abbr=abbr)[person.rich_first_names + person.rich_middle_names], - name_part(before=' ')[person.rich_prelast_names], - name_part(before=' ')[person.rich_last_names], - ] diff --git a/book/_ext/pybtexapastyle/setup.py b/book/_ext/pybtexapastyle/setup.py deleted file mode 100644 index a62d122..0000000 --- a/book/_ext/pybtexapastyle/setup.py +++ /dev/null @@ -1,27 +0,0 @@ -import setuptools - - -setuptools.setup( - name='pybtex-apa-style', - version='1.3', - author='Naeka', - author_email='contact@naeka.fr', - description='Pybtex APA-like style', - url='https://github.com/naeka/pybtex-apa-style', - py_modules=['formatting.apa', 'labels.apa', 'names.firstlast'], - entry_points={ - 'pybtex.style.formatting': 'apa = formatting.apa:APAStyle', - 'pybtex.style.labels': 'apa = labels.apa:LabelStyle', - 'pybtex.style.names': 'firstlast = names.firstlast:NameStyle', - }, - classifiers=( - 'Operating System :: OS Independent', - 'Programming Language :: Python', - 'Programming Language :: Python :: 2.7', - 'Programming Language :: Python :: 3', - 'License :: OSI Approved :: MIT License', - 'Operating System :: OS Independent', - 'Topic :: Text Processing :: Markup', - 'Topic :: Utilities', - ), -) diff --git a/book/_static/apastyle.css b/book/_static/apastyle.css deleted file mode 100644 index 01d759c..0000000 --- a/book/_static/apastyle.css +++ /dev/null @@ -1,3 +0,0 @@ -span.label:not(:has(:not(.fn-bracket))) { - display: none; -} diff --git a/book/features/apa.md b/book/features/apa.md index 897e160..b30d141 100644 --- a/book/features/apa.md +++ b/book/features/apa.md @@ -4,25 +4,13 @@ This page is useful for user type 4 and 5. ``` -{bdg-primary}`(To be converted into) Sphinx Extension` -{bdg-link-secondary}`Supported by TeachBooks package <./teachbooks_intro.html>` - +{bdg-primary}`Sphinx Extension` ```` (apa)= # APA References -```{note} -This feature is stable but because it relies on a local extension that is part of your book files and _not_ managed as part of a computing environment (e.g., `pip` or `conda`), unknown issues may arise. We would like to convert this feature into an independent Sphinx extension and/or Pybtext plugin. Visit the README in the [project page on GitHub](https://github.com/orgs/TeachBooks/projects/17?pane=info) to learn more. - -The instructions provided here will work under conventional usage with TeachBooks and Jupyter Book v1 (e.g., `jupyter book build book`). - -A temporary fix has also been implemented in the TeachBooks package for use with the Deploy Book Workflow (release mode) (see [release notes from v0.1.0](https://github.com/TeachBooks/TeachBooks/releases/tag/v0.1.0)). - -_Update, October 21, 2025:_ a new fix was necessitated due to dependency issues with docutils making it no longer necessary to pin this package. See [PR 207 of the Manual for a description](https://github.com/TeachBooks/manual/pull/207). -``` - ## Introduction Do you include references in your book, but you're tired of the default options available in a TeachBook / Jupyter Book v1? For example, the square-bracket style of citation and reference, where the first three letters of the name year are combined in a way that seems designed to minimize transparency? @@ -30,77 +18,64 @@ Do you include references in your book, but you're tired of the default options There is a solution! This extension allows you to have APA formatting in your book. ## Installation -To use APA-references, a set of Python files need to be manually loaded into your book. -**Step 1: files to root directory of your book** +To use APA-references, install the [Sphinx-APA-References extension](https://github.com/TeachBooks/Sphinx-APA-References) by following these two steps: -Download this zip-file [`apa_config.zip`](apa_config.zip), which contains the necessary files, located in _two_ subdirectories: `_ext` and `_static`. Unzip and place the contents in the root of your book directory so that it looks like the schematic here: +**Step 1: Install the Package** +Install the module `sphinx-apa-references` package using `pip`: ``` -book -├── _ext/ -│ ├── pybtexapastyle/ -│ ├── apastyle.py -│ └── bracket_citation_style.py -├── _static/ -| ├── ... -│ └── apastyle.css -├── _config.yml -├── _toc.yml -├── requirements.txt -├── +pip install sphinx-apa-references +``` + +**Step 2: Add to `requirements.txt`** + +Make sure that the package is included in your project's `requirements.txt` to track the dependency: +``` +sphinx-apa-references ``` -Note that you may or may not already have a `_static` subdirectory, to which the file `apastyle.css` should be added. - -**Step 2: Enable in `_config.yml`** - -In your `_config.yml` file, add a `bibtex_default_style`, `bybtex_reference_style` and the local extensions `apastyle` and `bracket_citation_style.py`. This enables the (local) extension. +**Step 3: Enable in `_config.yml` +In your `_config.yml` file, add the extension to the list of Sphinx extra extensions (**important**: underscore, not dash this time): ``` -sphinx: - config: - ... - bibtex_default_style: myapastyle - bibtex_reference_style: author_year_round - local_extensions: - apastyle: _ext/ - bracket_citation_style: _ext/ - ... +sphinx: + extra_extensions: + . + . + . + - sphinx_apa_references + . + . + . ``` ## Usage -All references are now made in APA-style. See for example this reference: {cite:t}`jason_moore` which shows up on the [references page](../references.md) too. The form of the citation looks like this: +All references are now made in APA-style. Here are five examples how to create a citation in APA style: -``` -{cite:t}`jason_moore` -``` - -Here are three examples for making citations: - -| Syntax | Result | -| :---: | :---: | -|`{cite:t}` | {cite:t}`jason_moore` | -|`{cite:p}`| {cite:p}`jason_moore` | -|`{cite}`| {cite}`jason_moore` | +| Syntax | Result with 1 author | Result with many authors | Explanation | +| :---: | :---: | :---: | :---: | +|`{cite:t}` | {cite:t}`jason_moore` | {cite:t}`dummyreference` | Textual citation | +|`{cite:p}`| {cite:p}`jason_moore` | {cite:p}`dummyreference` | Parenthetical citation | +|`{cite}`| {cite}`jason_moore` | {cite}`dummyreference` | Same as `{cite:p}` | +|`{cite:ts}` | {cite:ts}`jason_moore` | {cite:ts}`dummyreference` | Same as `{cite:t}`, but without abbreviation of authors | +|`{cite:ps}`| {cite:ps}`jason_moore` | {cite:ps}`dummyreference` |Same as `{cite:p}`, but without abbreviation of authors | For more options on the in-line citation style, see https://jupyterbook.org/v1/content/citations.html#change-the-in-line-citation-style. -### Known Issues +The references section at the end of your book will also be formatted in APA style: -Two issues are known: -- During the build, warning will be raised with `... WARNING: duplicate label for keys ...`. In most cases, these warnings can be ignored. As noted above, the book will not build references properly -- In case you have both an `.ipynb` and `.md` version of a file, the `.md` version will always be used whenever this extension is used. This removes the possibility to show code outputs. +::::{admonition} Example of APA References +:class: example + +```{iframe} ../references.html +:class: no-blend +``` +:::: ## Implementation The extension is based on [`pybtex`](https://pybtex.org/), which is a BibTeX-compatible bibliography processor in Python that is extendible with plugins. -Although some customization is possible with the standard Jupyter Book v1 features, [as described here](https://jupyterbook.org/v1/content/citations.html#change-the-in-line-citation-style), this extension implements the complete APA style, as well as enforcing round brackets (like this). - -The need to include a CSS file is an easy solution to the issue where empty brackets `[]` are left on the references page (to try it, simply delete `apastyle.css`). - -## Contribute - -This tool needs to be developed further to make it into a proper sphinx-extension (and/or an independent `pybtext` plugin). The process is described in the README of this [project page on GitHub](https://github.com/orgs/TeachBooks/projects/17?pane=info). If you've ideas or questions, please reach out to us at info@teachbooks.io! +Although some customization is possible with the standard Jupyter Book v1 features, [as described here](https://jupyterbook.org/v1/content/citations.html#change-the-in-line-citation-style), this extension implements a complete APA style, as well as enforcing round brackets (like this). diff --git a/book/features/apa_config.zip b/book/features/apa_config.zip deleted file mode 100644 index eedf090178e4b353ce0bf42a59af00c3908b8515..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12280 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 Date: Mon, 17 Nov 2025 17:08:31 +0100 Subject: [PATCH 2/2] Remove custom bibtex styles and add APA warning info Removed custom bibtex style settings from _config.yml. Added documentation in apa.md about handling duplicate label warnings in APA references, including advice on adding letters to years in .bib files. --- book/_config.yml | 2 -- book/features/apa.md | 31 +++++++++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/book/_config.yml b/book/_config.yml index 809a809..769fcce 100644 --- a/book/_config.yml +++ b/book/_config.yml @@ -39,8 +39,6 @@ sphinx: html_show_copyright: false html_last_updated_fmt: '%B %-d, %Y' html_static_path: ["images"] - bibtex_default_style: myapastyle - bibtex_reference_style: author_year_round mathjax3_config: tex: macros: diff --git a/book/features/apa.md b/book/features/apa.md index b30d141..6d2c670 100644 --- a/book/features/apa.md +++ b/book/features/apa.md @@ -74,6 +74,37 @@ The references section at the end of your book will also be formatted in APA sty ``` :::: +## Duplicate label warning + +You might encounter a warning during the build process that has this structure: + +``` +:: WARNING: duplicate label ", " for keys "" and "" +``` + +This is might have different cause, but often it is because two different references have the same author(s) and the same year. A common solution is to add letters to the year in the `.bib` file, like this: + +```bibtex +@misc{launch_buttons, + author = {{TeachBooks}}, + title = {Custom launch buttons}, + url = {https://github.com/TeachBooks/Sphinx-launch-buttons}, + year = {2024a}, + note = {Retrieved December, 2024. BSD-3-Clause.}, +} + +@misc{nested, + author = {{TeachBooks}}, + title = {Git workflow: Share content between book}, + url = {https://github.com/TeachBooks/Nested-Books}, + year = {2024b}, + note = {Retrieved December, 2024. CC BY 4.0.}, +} + +``` + +This does negate the automatic ordering of references by the order `authors > year > title`, so our advice is to only use this solution when the bibliography contains duplicate author-year combinations ánd the bibliography is final. That way the you can use the sorted order with warnings to determine the correct letters to add to the years. + ## Implementation The extension is based on [`pybtex`](https://pybtex.org/), which is a BibTeX-compatible bibliography processor in Python that is extendible with plugins.