-
Notifications
You must be signed in to change notification settings - Fork 21
Allow multiple references to the same footnote #40
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1 @@ | ||
<a href="#endnote-{{ index }}" id="endnote-source-{{ index }}"><sup>{{ index }}</sup></a> | ||
<a href="#endnote-{{ index }}" id="endnote-source-{{ index }}-{{ reference_index }}"><sup>{{ index }}</sup></a> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,12 +1,13 @@ | ||
import re | ||
|
||
from django.conf import settings | ||
from django.core.exceptions import ValidationError | ||
from django.template.loader import get_template | ||
from django.utils.safestring import mark_safe | ||
from wagtail.blocks import RichTextBlock | ||
from wagtail.models import Page | ||
|
||
from wagtail_footnotes.models import Footnote | ||
|
||
|
||
FIND_FOOTNOTE_TAG = re.compile(r'<footnote id="(.*?)">.*?</footnote>') | ||
|
||
|
@@ -15,69 +16,93 @@ class RichTextBlockWithFootnotes(RichTextBlock): | |
""" | ||
Rich Text block that renders footnotes in the format | ||
'<footnote id="long-id">short-id</footnote>' as anchor elements. It also | ||
adds the Footnote object to the 'page' object for later use. It uses | ||
adds the Footnote object(s) to the 'page' object for later use. It uses | ||
'page' because variables added to 'context' do not persist into the | ||
final template context. | ||
""" | ||
|
||
all_footnotes: dict[str, Footnote] | ||
|
||
def __init__(self, **kwargs): | ||
super().__init__(**kwargs) | ||
if not self.features: | ||
self.features = [] | ||
if "footnotes" not in self.features: | ||
self.features.append("footnotes") | ||
|
||
def render_footnote_tag(self, index): | ||
def render_footnote_tag(self, index: int, reference_index: int): | ||
template_name = getattr( | ||
settings, | ||
"WAGTAIL_FOOTNOTES_REFERENCE_TEMPLATE", | ||
"wagtail_footnotes/includes/footnote_reference.html", | ||
) | ||
template = get_template(template_name) | ||
return template.render({"index": index}) | ||
return template.render({"index": index, "reference_index": reference_index}) | ||
|
||
def replace_footnote_tags(self, value, html, context=None): | ||
if context is None: | ||
new_context = self.get_context(value) | ||
else: | ||
new_context = self.get_context(value, parent_context=dict(context)) | ||
|
||
if not isinstance(new_context.get("page"), Page): | ||
page = new_context.get("page") | ||
if page is None or not isinstance(page, Page): | ||
return html | ||
|
||
page = new_context["page"] | ||
if not hasattr(page, "footnotes_list"): | ||
page.footnotes_list = [] | ||
self.footnotes = { | ||
# Map Footnote UUIDs to Footnote instances to simplify lookups once a reference has been found in the text. | ||
# NOTE: Footnotes may exist in the database for a given page but this does not necessarily mean that the | ||
# footnote was referenced in the text. | ||
self.all_footnotes = { | ||
str(footnote.uuid): footnote for footnote in page.footnotes.all() | ||
} | ||
|
||
# Patch the page to track the footnotes that are actually referenced in the text, so that they can be rendered | ||
# in footnotes.html | ||
if not hasattr(page, "footnotes_list"): | ||
page.footnotes_list = [] | ||
|
||
def replace_tag(match): | ||
footnote_uuid = match.group(1) | ||
try: | ||
index = self.process_footnote(match.group(1), page) | ||
except (KeyError, ValidationError): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't see anything that would raise a |
||
footnote = self.attach_footnote_to_page(footnote_uuid, page) | ||
except KeyError: | ||
return "" | ||
else: | ||
return self.render_footnote_tag(index) | ||
# Add 1 to the footnote index as footnotes are rendered in footnotes.html using `{{ forloop.counter }}` | ||
# which is 1-based. | ||
footnote_index = page.footnotes_list.index(footnote) + 1 | ||
reference_index = footnote.references[-1] | ||
# Supplying both indexes allows for unique id values to be generated in the HTML. E.g., the first | ||
# reference to the first footnote will have `id="footnote-source-1-1"`, and the second reference to the | ||
# first footnote will have `id="footnote-source-1-2"`, etc. | ||
return self.render_footnote_tag(footnote_index, reference_index) | ||
|
||
# note: we return safe html | ||
return mark_safe(FIND_FOOTNOTE_TAG.sub(replace_tag, html)) # noqa: S308 | ||
|
||
def render(self, value, context=None): | ||
if not self.get_template(value=value, context=context): | ||
return self.render_basic(value, context=context) | ||
|
||
html = super().render(value, context=context) | ||
return self.replace_footnote_tags(value, html, context=context) | ||
|
||
def render_basic(self, value, context=None): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I removed |
||
html = super().render_basic(value, context) | ||
def attach_footnote_to_page(self, footnote_uuid: str, page: Page) -> Footnote: | ||
"""Finds the Footnote object matching `footnote_uuid`, then modifies it to track how many times it has been | ||
referenced, and attaches it to the `page` so the footnote can be rendered in the page template. | ||
""" | ||
# Fetch the unmodified Footnote | ||
footnote = self.all_footnotes[footnote_uuid] | ||
|
||
return self.replace_footnote_tags(value, html, context=context) | ||
|
||
def process_footnote(self, footnote_id, page): | ||
footnote = self.footnotes[footnote_id] | ||
# If this is the first time the Footnote has been referenced, modify it to track references before appending it | ||
# to the page | ||
if footnote not in page.footnotes_list: | ||
footnote.references = [1] | ||
page.footnotes_list.append(footnote) | ||
# Add 1 to the index as footnotes are indexed starting at 1 not 0. | ||
return page.footnotes_list.index(footnote) + 1 | ||
else: | ||
# If this Footnote has been processed by a previous reference, fetch the modified Footnote from the page and | ||
# update its reference tracking | ||
footnote_index = page.footnotes_list.index(footnote) | ||
footnote = page.footnotes_list[footnote_index] | ||
# Update the references e.g., [1, 2] | ||
footnote.references.append(footnote.references[-1] + 1) | ||
# Update the page with the updated footnote | ||
page.footnotes_list[footnote_index] = footnote | ||
return footnote |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1 @@ | ||
<a href="#footnote-{{ index }}" id="footnote-source-{{ index }}"><sup>[{{ index }}]</sup></a> | ||
<a href="#footnote-{{ index }}" id="footnote-source-{{ index }}-{{ reference_index }}"><sup>[{{ index }}]</sup></a> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The
[{uuid[:6]}]
bit isn't strictly required sinceFIND_FOOTNOTE_TAG
doesn't care what's between<footnote></footnote>
but I opted to make it an accurate representation of what is actually stored in the database.