Source code for nbprint.config.core.page

from typing import TYPE_CHECKING, Tuple

from nbformat import NotebookNode
from pydantic import Field, field_validator

from nbprint.config.base import BaseModel, Role, _append_or_extend
from nbprint.config.common import PageOrientation, PageSize, Style
from nbprint.config.exceptions import NBPrintNullCellError

from .content import Section

PAGE_REGION_ATTRS = (
    "top",
    "top_left",
    "top_left_corner",
    "top_right",
    "top_right_corner",
    "bottom",
    "bottom_left",
    "bottom_left_corner",
    "bottom_right",
    "bottom_right_corner",
    "left",
    "left_top",
    "left_bottom",
    "right",
    "right_top",
    "right_bottom",
)

if TYPE_CHECKING:
    from .config import Configuration

__all__ = (
    "Page",
    "PageGlobal",
    "PageNumber",
    "PageRegion",
    "PageRegionContent",
)


class PageRegionContent(BaseModel):
    content: str | None = ""

    # common
    tags: list[str] = Field(default_factory=list)
    role: Role = Role.PAGE
    ignore: bool = True

    def __str__(self) -> str:
        if self.content:
            return f"content: {self.content};"
        return ""

    @field_validator("tags", mode="after")
    @classmethod
    def _ensure_tags(cls, v: list[str]) -> list[str]:
        if "nbprint:page" not in v:
            v.append("nbprint:page")
        return v


class PageNumber(PageRegionContent):
    content: str | None = "counter(page)"


class PageRegion(BaseModel):
    _region: str = ""
    content: PageRegionContent | None = Field(default_factory=PageNumber)
    style: Style | None = None

    css: str = ""

    # common
    tags: list[str] = Field(default_factory=list)
    role: Role = Role.PAGE
    ignore: bool = True

    @field_validator("tags", mode="after")
    @classmethod
    def _ensure_tags(cls, v: list[str]) -> list[str]:
        if "nbprint:page" not in v:
            v.append("nbprint:page")
        return v

    def generate(self, metadata: dict, config: "Configuration", parent: "BaseModel", attr: str, **_) -> NotebookNode:
        cell = super().generate(metadata=metadata, config=config, parent=parent, attr=attr)
        cell.metadata.tags.append(f"nbprint:page:{attr}")
        return cell


[docs] class Page(BaseModel): top: PageRegion | None = None top_left: PageRegion | None = None top_left_corner: PageRegion | None = None top_right: PageRegion | None = None top_right_corner: PageRegion | None = None bottom: PageRegion | None = None bottom_left: PageRegion | None = None bottom_left_corner: PageRegion | None = None bottom_right: PageRegion | None = None bottom_right_corner: PageRegion | None = None left: PageRegion | None = None left_top: PageRegion | None = None left_bottom: PageRegion | None = None right: PageRegion | None = None right_top: PageRegion | None = None right_bottom: PageRegion | None = None counter_reset: bool = False counter_style: str | None = None size: PageSize | Tuple[float, float] | None = Field(default=PageSize.letter) orientation: PageOrientation | None = Field(default=PageOrientation.portrait) css: str = ""
[docs] @classmethod def convert_region_from_obj(cls, v, region) -> PageRegion: ret = BaseModel._to_type(v, PageRegion) ret._region = region base_css_scope = "@page {{ @{region} {{ {content} }} }}".format(region=region, content=str(ret.content or "")) ret.css += base_css_scope return ret
[docs] @field_validator("top", mode="before") @classmethod def convert_top_from_obj(cls, v) -> PageRegion: return Page.convert_region_from_obj(v, "top-center")
[docs] @field_validator("top_left", mode="before") @classmethod def convert_top_left_from_obj(cls, v) -> PageRegion: return Page.convert_region_from_obj(v, "top-left")
[docs] @field_validator("top_left_corner", mode="before") @classmethod def convert_top_left_corner_from_obj(cls, v) -> PageRegion: return Page.convert_region_from_obj(v, "top-left-corner")
[docs] @field_validator("top_right", mode="before") @classmethod def convert_top_right_from_obj(cls, v) -> PageRegion: return Page.convert_region_from_obj(v, "top-right")
[docs] @field_validator("top_right_corner", mode="before") @classmethod def convert_top_right_corner_from_obj(cls, v) -> PageRegion: return Page.convert_region_from_obj(v, "top-right-corner")
[docs] @field_validator("bottom", mode="before") @classmethod def convert_bottom_from_obj(cls, v) -> PageRegion: return Page.convert_region_from_obj(v, "bottom-center")
[docs] @field_validator("bottom_left", mode="before") @classmethod def convert_bottom_left_from_obj(cls, v) -> PageRegion: return Page.convert_region_from_obj(v, "bottom-left")
[docs] @field_validator("bottom_left_corner", mode="before") @classmethod def convert_bottom_left_corner_from_obj(cls, v) -> PageRegion: return Page.convert_region_from_obj(v, "bottom-left-corner")
[docs] @field_validator("bottom_right", mode="before") @classmethod def convert_bottom_right_from_obj(cls, v) -> PageRegion: return Page.convert_region_from_obj(v, "bottom-right")
[docs] @field_validator("bottom_right_corner", mode="before") @classmethod def convert_bottom_right_corner_from_obj(cls, v) -> PageRegion: return Page.convert_region_from_obj(v, "bottom-right-corner")
[docs] @field_validator("left", mode="before") @classmethod def convert_left_from_obj(cls, v) -> PageRegion: return Page.convert_region_from_obj(v, "left-center")
[docs] @field_validator("left_top", mode="before") @classmethod def convert_left_top_from_obj(cls, v) -> PageRegion: return Page.convert_region_from_obj(v, "left-top")
[docs] @field_validator("left_bottom", mode="before") @classmethod def convert_left_bottom_from_obj(cls, v) -> PageRegion: return Page.convert_region_from_obj(v, "left-bottom")
[docs] @field_validator("right", mode="before") @classmethod def convert_right_from_obj(cls, v) -> PageRegion: return Page.convert_region_from_obj(v, "right-center")
[docs] @field_validator("right_top", mode="before") @classmethod def convert_right_top_from_obj(cls, v) -> PageRegion: return Page.convert_region_from_obj(v, "right-top")
[docs] @field_validator("right_bottom", mode="before") @classmethod def convert_right_bottom_from_obj(cls, v) -> PageRegion: return Page.convert_region_from_obj(v, "right-bottom")
[docs] def generate(self, metadata: dict, config: "Configuration", parent: "BaseModel", attr: str = "page", **_) -> NotebookNode | list[NotebookNode] | None: cells = [] # parent and config should be equal for the global page layout assert parent == config main_cell = super()._base_generate(metadata=metadata, config=config, parent=config, attr=attr or "page") main_cell.metadata.tags.append("nbprint:page") main_cell.metadata.nbprint.role = "page" main_cell.metadata.nbprint.ignore = True cells.append(main_cell) for set_attr in PAGE_REGION_ATTRS: if getattr(self, set_attr) is not None: # pass in `self` as `parent here` _append_or_extend(cells, getattr(self, set_attr).generate(metadata=metadata, config=config, parent=self, attr=set_attr)) for cell in cells: if cell is None: raise NBPrintNullCellError return cells
class PageGlobal(Page): pages: dict[Section, "Page"] | None = Field(default_factory=dict) css: str = "" def render(self, **_) -> None: if "@page { size:" not in self.css: if isinstance(self.size, tuple): self.css += f"\n@page {{ size: {self.size[0]}in {self.size[1]}in; }}" else: self.css += f"\n@page {{ size: {self.size.value} {self.orientation.value}; }}" # Generate per-section named page CSS for section_name, section_page in (self.pages or {}).items(): marker = f"@page {section_name}" if marker in self.css: continue inner_rules = [] for region_attr in PAGE_REGION_ATTRS: region = getattr(section_page, region_attr, None) if region is not None and region.content: inner_rules.append(f"@{region._region} {{ {region.content} }}") if section_page.counter_reset: inner_rules.append("counter-reset: page 1;") if section_page.counter_style: inner_rules.append(f"@bottom-center {{ content: counter(page, {section_page.counter_style}); }}") if section_page.css: inner_rules.append(section_page.css) if inner_rules: self.css += f"\n@page {section_name} {{ {' '.join(inner_rules)} }}" self.css += f'\n[data-nbprint-section="{section_name}"] {{ page: {section_name}; }}' @field_validator("pages", mode="before") @classmethod def convert_pages_from_obj(cls, v) -> "Page": if v is None: return [] if isinstance(v, dict): for k, element in v.items(): if isinstance(element, str): v[k] = Page(type_=element) elif isinstance(element, dict): v[k] = BaseModel._to_type(element) elif element is None: v[k] = Page() return v