"""Reportlab document template + page chrome helpers.
Generic reportlab plumbing extracted from ``cli/audit.py``: a
``BaseDocTemplate`` subclass that handles bookmarks + TOC entries +
``Page X of N`` page-count threading via a multiBuild-aware closure,
plus level-tagged ``Paragraph`` helpers and a themed footer factory.
Deliberately stays domain-agnostic: no audit-specific story builders
or content live here. ``cli/audit.py`` (and any future PDF-generating
artifact) imports these primitives and assembles its own story.
"""
from __future__ import annotations
from datetime import datetime
from typing import Any, Callable
from recon_gen.common.provenance import (
ProvenanceFingerprint,
short_fingerprint_placeholder,
)
[docs]
def bookmarked_h1(text: str, styles: Any) -> Any:
"""Heading1 paragraph tagged for PDF outline + TOC at level 0.
``styles`` is a reportlab ``StyleSheet1``; returns a reportlab
``Paragraph``. Both annotated ``Any`` to avoid forcing the reportlab
import at module load — the PDF extra is optional.
Used by every per-section heading the reader should be able to
jump to from the bookmark sidebar or the TOC page.
"""
from reportlab.platypus import Paragraph
p = Paragraph(text, styles["Heading1"])
p._bookmark_level = 0 # pyright: ignore[reportAttributeAccessIssue]: reportlab Paragraph monkey-patch for bookmark generation
return p
[docs]
def bookmarked_h3(text: str, styles: Any) -> Any:
"""Heading3 paragraph tagged for PDF outline + TOC at level 1.
``styles``/return both ``Any`` for the same reason as ``bookmarked_h1``.
"""
from reportlab.platypus import Paragraph
p = Paragraph(text, styles["Heading3"])
p._bookmark_level = 1 # pyright: ignore[reportAttributeAccessIssue]: reportlab Paragraph monkey-patch for bookmark generation
return p
[docs]
class BookmarkedDocTemplate:
"""``BaseDocTemplate`` proxy with bookmark + TOC + page-count support.
Defined as a thin proxy via ``__new__``-style indirection so
reportlab is only imported when the caller actually needs it (so
the parent CLI loads cleanly without the PDF extra installed).
Builds the PDF outline (left-sidebar nav) + feeds the
``TableOfContents`` flowable's notification stream from any
flowable tagged with a ``_bookmark_level`` attribute (use
``bookmarked_h1`` / ``bookmarked_h3``). Also records the final
page count after each ``multiBuild`` pass into the
caller-provided ``total_pages_holder`` so the footer drawer can
render "Page X of Y" without resorting to a NumberedCanvas
(which breaks bookmark→page refs).
"""
def __new__(
cls,
*args: Any,
total_pages_holder: list[int] | None = None,
**kwargs: Any,
) -> Any:
# Returns a reportlab ``BaseDocTemplate`` subclass instance;
# typed ``Any`` to avoid forcing the runtime reportlab import.
from reportlab.platypus import BaseDocTemplate
class _Inner(BaseDocTemplate):
def afterFlowable(self, flowable: Any) -> None:
# reportlab Flowable callback override; ``flowable``
# is the page flowable just emitted (Paragraph / etc.).
level = getattr(flowable, "_bookmark_level", None)
if level is None:
return
text: str = flowable.getPlainText() # pyright: ignore[reportUnknownMemberType]: reportlab Flowable.getPlainText() returns str
key = f"qsg-bm-{id(flowable)}"
self.canv.bookmarkPage(key) # pyright: ignore[reportUnknownMemberType]: reportlab Canvas, unstubbed
self.canv.addOutlineEntry(text, key, level=level) # pyright: ignore[reportUnknownMemberType]: reportlab Canvas, unstubbed
self.notify("TOCEntry", (level, text, self.page, key))
def _allSatisfied(self) -> Any:
# multiBuild calls this after each pass to decide
# whether to run another. We piggyback to publish the
# just-stabilized page count into the holder so the
# footer drawer's "Page X of Y" picks it up on the
# next pass.
if total_pages_holder is not None:
total_pages_holder[0] = self.page
return super()._allSatisfied() # pyright: ignore[reportAttributeAccessIssue, reportUnknownMemberType, reportUnknownVariableType]: reportlab BaseDocTemplate hook, unstubbed
return _Inner(*args, **kwargs)