Skip to content

page

page #

Page — a website folder: index.html beside every file it shows.

  1. Declare. Page.declare(root, title=, at=) — what a cg.ui.PageBuilder function returns — records a component tree and what the page is the page of; nothing is written yet, and at may still be set.
  2. Build. build() — or the store that seals the value — renders the tree once into the page's folder: index.html at the root, staged files under files/, browser libraries under lib/. A hand-made folder holding an index.html is a page as it stands (Page.load(folder), or the plain folder address every directory scalar answers to).
  3. Read. title is the <title> of index.html, on a built or wrapped page alike.

Page #

Bases: DirScalar

A page: a component tree rendered to a self-contained website folder.

Analysis-only: the result of a page function in a target module, never part of a procedure schema. The value is the folder, so it seals like any other directory; the server hosts it at the URL of the value it is the page of.

title is the page's display name everywhere it is listed — index cards, link cards, the browser chrome — so derive it from the data the page shows rather than repeating the function's name. at is the value the page is the page of; links from the page are relative to its position, and it stays settable until the page is built.

@cgserver.page(Journal, lambda s: s.sessions.for_each(), version=1)
@cg.ui.PageBuilder()
def overview(session: Session) -> None:
    cg.ui.Title(session.captured.date().isoformat())
    cg.ui.Markdown("# Overview")
    cg.ui.ImageViewer(session.photo)
Source code in capturegraph-lib/capturegraph/types/special/page.py
class Page(DirScalar, name="edu.cornell.page"):
    """A page: a component tree rendered to a self-contained website folder.

    Analysis-only: the result of a page function in a target module, never part
    of a procedure schema. The value *is* the folder, so it seals like any other
    directory; the server hosts it at the URL of the value it is the page of.

    ``title`` is the page's display name everywhere it is listed — index cards,
    link cards, the browser chrome — so derive it from the data the page shows
    rather than repeating the function's name. ``at`` is the value the page is
    the page of; links from the page are relative to its position, and it stays
    settable until the page is built.

    ```python
    @cgserver.page(Journal, lambda s: s.sessions.for_each(), version=1)
    @cg.ui.PageBuilder()
    def overview(session: Session) -> None:
        cg.ui.Title(session.captured.date().isoformat())
        cg.ui.Markdown("# Overview")
        cg.ui.ImageViewer(session.photo)
    ```
    """

    __slots__ = ("_at", "_built", "_title", "_tree")
    _tree: Component | None
    _title: str | None
    _at: object | None
    _built: bool

    analysis_only = True

    def __init__(self, *segments: str | os.PathLike[str]) -> None:
        """Take the website folder at ``segments`` as a page as it stands."""
        super().__init__(*segments)
        self._tree, self._title, self._at = None, None, None
        self._built = True

    @classmethod
    def declare(
        cls,
        root: Component,
        *,
        title: str | None = None,
        at: object | None = None,
    ) -> Self:
        """A page to be rendered from ``root`` into a fresh folder in the recipe workspace.

        ``cg.ui.PageBuilder`` calls this with the stack its function built.

        Args:
            root: The component tree to render.
            title: The page's display name.
            at: The value this page is the page of; settable until the page is built.

        Raises:
            ScratchNotConfigured: If no scratch is configured.
        """
        from capturegraph.recipes.values.workspace import allocate, pin, pinned

        allocated = allocate(cls, "")
        page = cls(allocated)
        hold = pinned(allocated)
        if hold is not None:
            pin(page, hold)
        page._tree, page._title, page._at = root, title, at
        page._built = False
        return page

    @property
    def at(self) -> object | None:
        """The value this page is the page of; settable until the page is built."""
        return self._at

    @at.setter
    def at(self, value: object | None) -> None:
        """Set the value this page is the page of.

        Raises:
            ValueError: If the page is already built.
        """
        if self._built:
            raise ValueError("this Page is already built; `at` is fixed once its index.html exists")
        self._at = value

    def build(self) -> Self:
        """Render the tree into this folder, once; a built or wrapped page is returned as is.

        Raises:
            ValueError: If a ``PageLink`` in the tree has nowhere to point.
        """
        if self._tree is None or self._built:
            return self
        from capturegraph.ui.markup import document
        from capturegraph.ui.render import Render

        self.mkdir(parents=True, exist_ok=True)
        body = self._tree.html(Render(self, self._at))
        write_bytes_if_changed(self / INDEX_HTML, document(self._title, body).encode())
        self._built = True
        return self

    @property
    def title(self) -> str | None:
        """The declared title, or for a wrapped folder the ``<title>`` of its ``index.html``."""
        if self._tree is not None:
            return self._title
        return title_of((self / INDEX_HTML).read_text())

    @classmethod
    def load(cls, base: Path) -> Self:
        """The website folder at ``base``.

        Raises:
            FileNotFoundError: If ``base`` is not a directory or holds no ``index.html``.
        """
        page = super().load(base)
        _require_index(page)
        return page

    @classmethod
    def store(cls, base: Path, value: object) -> None:
        """Build ``value`` if it is not yet built, then copy its folder into ``base``.

        Raises:
            FileNotFoundError: If ``value`` wraps a folder without an ``index.html``.
        """
        page = cls.coerce(value).build()
        _require_index(page)
        super().store(base, page)

at property writable #

The value this page is the page of; settable until the page is built.

title property #

The declared title, or for a wrapped folder the <title> of its index.html.

__init__(*segments) #

Take the website folder at segments as a page as it stands.

Source code in capturegraph-lib/capturegraph/types/special/page.py
def __init__(self, *segments: str | os.PathLike[str]) -> None:
    """Take the website folder at ``segments`` as a page as it stands."""
    super().__init__(*segments)
    self._tree, self._title, self._at = None, None, None
    self._built = True

build() #

Render the tree into this folder, once; a built or wrapped page is returned as is.

Raises:

Type Description
ValueError

If a PageLink in the tree has nowhere to point.

Source code in capturegraph-lib/capturegraph/types/special/page.py
def build(self) -> Self:
    """Render the tree into this folder, once; a built or wrapped page is returned as is.

    Raises:
        ValueError: If a ``PageLink`` in the tree has nowhere to point.
    """
    if self._tree is None or self._built:
        return self
    from capturegraph.ui.markup import document
    from capturegraph.ui.render import Render

    self.mkdir(parents=True, exist_ok=True)
    body = self._tree.html(Render(self, self._at))
    write_bytes_if_changed(self / INDEX_HTML, document(self._title, body).encode())
    self._built = True
    return self

declare(root, *, title=None, at=None) classmethod #

A page to be rendered from root into a fresh folder in the recipe workspace.

cg.ui.PageBuilder calls this with the stack its function built.

Parameters:

Name Type Description Default
root Component

The component tree to render.

required
title str | None

The page's display name.

None
at object | None

The value this page is the page of; settable until the page is built.

None

Raises:

Type Description
ScratchNotConfigured

If no scratch is configured.

Source code in capturegraph-lib/capturegraph/types/special/page.py
@classmethod
def declare(
    cls,
    root: Component,
    *,
    title: str | None = None,
    at: object | None = None,
) -> Self:
    """A page to be rendered from ``root`` into a fresh folder in the recipe workspace.

    ``cg.ui.PageBuilder`` calls this with the stack its function built.

    Args:
        root: The component tree to render.
        title: The page's display name.
        at: The value this page is the page of; settable until the page is built.

    Raises:
        ScratchNotConfigured: If no scratch is configured.
    """
    from capturegraph.recipes.values.workspace import allocate, pin, pinned

    allocated = allocate(cls, "")
    page = cls(allocated)
    hold = pinned(allocated)
    if hold is not None:
        pin(page, hold)
    page._tree, page._title, page._at = root, title, at
    page._built = False
    return page

load(base) classmethod #

The website folder at base.

Raises:

Type Description
FileNotFoundError

If base is not a directory or holds no index.html.

Source code in capturegraph-lib/capturegraph/types/special/page.py
@classmethod
def load(cls, base: Path) -> Self:
    """The website folder at ``base``.

    Raises:
        FileNotFoundError: If ``base`` is not a directory or holds no ``index.html``.
    """
    page = super().load(base)
    _require_index(page)
    return page

store(base, value) classmethod #

Build value if it is not yet built, then copy its folder into base.

Raises:

Type Description
FileNotFoundError

If value wraps a folder without an index.html.

Source code in capturegraph-lib/capturegraph/types/special/page.py
@classmethod
def store(cls, base: Path, value: object) -> None:
    """Build ``value`` if it is not yet built, then copy its folder into ``base``.

    Raises:
        FileNotFoundError: If ``value`` wraps a folder without an ``index.html``.
    """
    page = cls.coerce(value).build()
    _require_index(page)
    super().store(base, page)

title_of(html) #

The text of the first <title> in html, or None when it has none.

assert title_of("<html><head><title>Plant Journal</title></head></html>") == "Plant Journal"
Source code in capturegraph-lib/capturegraph/types/special/page.py
def title_of(html: str) -> str | None:
    """The text of the first ``<title>`` in ``html``, or ``None`` when it has none.

    ```python
    assert title_of("<html><head><title>Plant Journal</title></head></html>") == "Plant Journal"
    ```
    """
    parser = _TitleParser()
    parser.feed(html)
    parser.close()
    return parser.title.strip() if parser.title is not None else None