Rendering Planners ================== The :class:`~pyplanner.Planner` class ties a Jinja2/HTML template together with a :class:`~pyplanner.Calendar` and template parameters, then renders the result to HTML or PDF. Creating a Planner ------------------ .. code-block:: python from pyplanner import Calendar, Planner cal = Calendar() planner = Planner("planners/demo/demo.html", calendar=cal) The template path must point to a Jinja2/HTML file. The Jinja2 environment is configured with: - ``%%`` as the line statement prefix - ``##`` as the line comment prefix - ``trim_blocks`` and ``lstrip_blocks`` enabled - Autoescaping for HTML The template directory (parent of the template file) is used as the Jinja2 file-system loader root. Template parameters ------------------- Templates may accept parameters defined in a ``params.xml`` file next to the template. :class:`~pyplanner.Params` loads the schema and produces a :class:`~types.SimpleNamespace` tree: .. code-block:: python from pyplanner import Params params = Params.load_xml("planners/demo/params.xml") ns = params.apply() print(ns.year) # 2026 print(ns.month) # 1 print(ns.day_off_color) # #C00000 Override individual values with ``-D``-style strings: .. code-block:: python ns = params.apply(["year=2027", "day_off_color=#0000FF"]) print(ns.year) # 2027 Nested parameters use dot notation: .. code-block:: python # Given a params.xml with #FFF ns = params.apply(["colors.primary=#000"]) print(ns.colors.primary) # #000 The ``params.xml`` format ^^^^^^^^^^^^^^^^^^^^^^^^^ The XML schema uses ```` as the root element. Leaf parameters have a ``type`` attribute (``str``, ``int``, ``float`` or ``bool``) and their text content is the default value. Namespace elements group related parameters under a nested namespace: .. code-block:: xml 2026 #4A90D9 yes #4A90D9 #FDD Rules: - Element names must be valid Python identifiers (use underscores, not hyphens). - Omitting ``type`` defaults to ``str``. - Boolean values accept ``true``/``false``, ``yes``/``no``, ``y``/``n``, ``on``/``off``, ``1``/``0`` (case-insensitive). - Use ```` for values containing XML special characters (e.g. inline SVG). Programmatic schema ^^^^^^^^^^^^^^^^^^^ You can also build a :class:`~pyplanner.Params` from a dict without an XML file: .. code-block:: python from pyplanner import Params params = Params({ "year": {"type": "int", "default": 2026}, "accent": {"type": "str", "default": "#4A90D9"}, }) ns = params.apply(["year=2027"]) Rendering to HTML ----------------- :meth:`Planner.html() ` renders the template and returns an HTML string: .. code-block:: python html = planner.html(base="planners/demo") with open("demo.html", "w", encoding="utf-8") as f: f.write(html) The ``base`` parameter controls how asset paths (images, CSS, fonts) are resolved. Templates reference assets as ``{{ base }}/assets/...``. When ``base`` is a relative path, assets resolve relative to wherever the output HTML is saved. When ``base`` is ``None``, it defaults to the template directory's ``file://`` URI. ``base`` is a render-time parameter rather than a constructor argument because the correct value depends on *where the output is written*, not on the template location. When generating HTML to a file, ``base`` should be a relative path from the output to the template directory. When generating a PDF, it should be a ``file://`` URI. The ``--watch`` mode needs yet another value (relative to the livereload server root). Making it a render-time parameter lets the same ``Planner`` instance produce output for different contexts without reconstruction. Rendering to PDF ---------------- :meth:`Planner.pdf() ` launches a headless Chromium browser via Playwright, loads the rendered HTML and prints it to PDF: .. code-block:: python pdf_bytes = planner.pdf() with open("demo.pdf", "wb") as f: f.write(pdf_bytes) The PDF renderer: - Routes ``file://`` requests to the local file system so images and fonts load correctly. - Waits for web fonts to finish loading before printing. - Respects CSS ``@page`` rules for page size and margins. - Extracts ``.page`` element IDs and adds year/month bookmarks to the PDF outline automatically. Page size and margins are controlled entirely by CSS ``@page`` rules in the template's stylesheet, not by API parameters. An earlier version accepted ``margin_top``, ``margin_bottom``, etc. as parameters to ``pdf()``. This created two competing sources of truth: the CSS and the Python API. Removing the API parameters makes CSS the single source for page geometry, which is what designers expect. Playwright's ``prefer_css_page_size=True`` flag ensures the browser respects the template's ``@page`` declarations. Template context variables -------------------------- Every template receives four variables: ``base`` Base URL string for resolving asset paths. ``calendar`` The :class:`~pyplanner.Calendar` instance. Call ``calendar.year(n)`` to build a year. Access ``calendar.weekdays`` for the ordered weekday list. ``lang`` Language code string (e.g. ``"en"``). ``params`` The :class:`~types.SimpleNamespace` tree from ``params.xml``. Live preview ------------ For interactive design work, the :func:`~pyplanner.watch` function starts a livereload server that rebuilds the HTML on every file change: .. code-block:: python from pyplanner import Planner, watch planner = Planner("planners/demo/demo.html") watch(planner, "demo.html") This is equivalent to running ``pyplanner planners/demo --watch`` on the command line. .. warning:: The ``livereload`` package is imported lazily inside ``watch()`` rather than at module level. Importing it triggers ``logging.basicConfig()`` which alters the global logging state of the host application. By deferring the import to call time, the side effect is confined to the moment the user explicitly opts into live preview, leaving the logging setup untouched for all other code paths. If you use pyplanner as a library, be aware that calling :func:`~pyplanner.watch` will trigger that side effect and may affect your log handlers. API reference ------------- .. autoclass:: pyplanner.Planner :members: html, pdf .. autoclass:: pyplanner.Params :members: load_xml, apply .. autofunction:: pyplanner.params.parse_bool