diff --git a/src/docc/plugins/html/static/docc.css b/src/docc/plugins/html/static/docc.css
index cc003f4..1ee3028 100644
--- a/src/docc/plugins/html/static/docc.css
+++ b/src/docc/plugins/html/static/docc.css
@@ -316,3 +316,21 @@ details[open] > summary::after {
.header-anchor:focus-within .link-logo {
visibility: visible;
}
+
+.footnote-ref {
+ text-decoration: none;
+}
+
+.footnotes {
+ margin-top: 2rem;
+ font-size: 0.9em;
+}
+
+.footnotes hr {
+ margin-bottom: 1rem;
+}
+
+.footnote-backref {
+ text-decoration: none;
+ margin-left: 0.25rem;
+}
\ No newline at end of file
diff --git a/src/docc/plugins/mistletoe.py b/src/docc/plugins/mistletoe.py
index 1edd661..3538d80 100644
--- a/src/docc/plugins/mistletoe.py
+++ b/src/docc/plugins/mistletoe.py
@@ -17,6 +17,9 @@
Markdown support for docc.
"""
+from lib2to3.fixes.fix_input import context
+from mistletoe import block_token as blocks
+from mistletoe import span_token as spans
from typing import (
Callable,
Final,
@@ -41,6 +44,7 @@
from docc.plugins import html, python, references, search
from docc.settings import PluginSettings
from docc.transform import Transform
+from dataclasses import dataclass, field
class MarkdownNode(Node, search.Searchable):
@@ -91,6 +95,53 @@ def search_children(self) -> bool:
otherwise.
"""
return False
+@dataclass
+class FootnoteRegistry:
+ """
+ Tracks footnotes during document rendering.
+ """
+ definitions: dict[str, MarkdownNode] = field(default_factory=dict)
+ references: dict[str, int] = field(default_factory=dict)
+ citation_counts: dict[str, int] = field(default_factory=dict)
+ next_number: int = 1
+
+ def register_reference(self, label: str) -> tuple[int, int]:
+ """Register a footnote reference and return (number, citation_index)."""
+ if label not in self.references:
+ self.references[label] = self.next_number
+ self.next_number += 1
+ self.citation_counts[label] = 0
+
+ self.citation_counts[label] += 1
+ return self.references[label], self.citation_counts[label]
+
+ def register_definition(self, label: str, node: MarkdownNode) -> None:
+ """Register a footnote definition."""
+ self.definitions[label] = node
+
+ def has_definitions(self) -> bool:
+ """Check if any footnote definitions exist."""
+ return bool(self.definitions)
+
+ def get_ordered_definitions(self) -> list[tuple[str, int, MarkdownNode]]:
+ """Get definitions ordered by number."""
+ result = []
+ for label, node in self.definitions.items():
+ number = self.references.get(label)
+ if number is not None:
+ result.append((label, number, node))
+ result.sort(key=lambda x: x[1])
+ return result
+
+
+def _get_footnote_registry(context: Context) -> FootnoteRegistry:
+ """Get or create the footnote registry for the current context."""
+ registry_key = "__footnote_registry__"
+
+ if not hasattr(context, registry_key):
+ setattr(context, registry_key, FootnoteRegistry())
+
+ return getattr(context, registry_key)
class DocstringTransform(Transform):
@@ -509,20 +560,28 @@ def _render_html_block(
parent.append(child)
return None
+# Reset footnote registry for new document
+registry_key = "__footnote_registry__"
+if hasattr(context, registry_key):
+ delattr(context, registry_key)
def _render_document(
context: Context,
parent: Union[html.HTMLRoot, html.HTMLTag],
node: MarkdownNode,
) -> html.RenderResult:
- # TODO: footnotes?
+ """Render a document, including footnotes at the end."""
+ # Reset footnote registry for new document
+ registry_key = "__footnote_registry__"
+ if hasattr(context, registry_key):
+ delattr(context, registry_key)
+
token = node.token
assert isinstance(token, blocks.Document)
tag = html.HTMLTag("div", {"class": "markdown"})
parent.append(tag)
return tag
-
_RENDER_FUNC: TypeAlias = Callable[
[Context, Union[html.HTMLRoot, html.HTMLTag], MarkdownNode],
html.RenderResult,
@@ -554,6 +613,8 @@ def _render_document(
"Document": _render_document,
"HTMLBlock": _render_html_block,
"HTMLSpan": _render_html_span,
+ "FootnoteRef": _render_footnote_ref, # pyright: ignore[reportUndefinedVariable]
+ "FootnoteEntry": _render_footnote_entry, # pyright: ignore[reportUndefinedVariable]
}
@@ -562,14 +623,17 @@ def render_html(
parent: object,
node: object,
) -> html.RenderResult:
- """
- Render a markdown node as HTML.
- """
+ """Render a markdown node as HTML."""
assert isinstance(context, Context)
assert isinstance(parent, (html.HTMLRoot, html.HTMLTag))
assert isinstance(node, MarkdownNode)
- return _RENDERERS[node.token.__class__.__name__](context, parent, node)
+ result = _RENDERERS[node.token.__class__.__name__](context, parent, node)
+ if isinstance(node.token, blocks.Document) and isinstance(result, html.HTMLTag):
+
+ setattr(result, '__needs_footnotes__', (context, result))
+
+ return result
class _SearchVisitor(Visitor):