Ir para o conteúdo

tempestweb.html

O renderizador de HTML estático — um renderizador-folha que produz markup a partir da mesma árvore de widgets, sem browser. É o que sustenta o SSR e a geração de páginas no build.

Guia com exemplos: SSR estático.

tempestweb.html

tempestweb.html — static server-side HTML renderer (a leaf renderer).

Turns a typed :class:~tempest_core.widgets.base.Widget tree into a static HTML string, reusing :func:tempest_core.build. It is a sibling of the DOM-JS client (client/dom.js): the same declarative tree renders to interactive DOM in the browser and to plain HTML on the server ("one tree, N renderers").

  • :func:render_to_html — a widget tree to an HTML fragment.
  • :func:render_document — a widget tree to a full <!doctype html> page.
  • :func:style_to_css — a Style dump to a CSS declaration body (a Python port of client/style.js).
  • :func:theme_css — an app's :class:~tempest_core.Theme as the --tw-* custom properties the base stylesheet reads (light plus dark).
  • :func:escape_text / :func:escape_attr — the HTML-escaping choke points.

See docs/ssr.md for the tutorial.

ROLE_BY_VARIABLE module-attribute

ROLE_BY_VARIABLE: dict[str, str] = {'--tw-primary': 'primary', '--tw-on-primary': 'on_primary', '--tw-primary-container': 'primary_container', '--tw-on-primary-container': 'on_primary_container', '--tw-secondary-container': 'secondary_container', '--tw-on-secondary-container': 'on_secondary_container', '--tw-surface': 'surface', '--tw-on-surface': 'on_surface', '--tw-on-surface-variant': 'on_surface_variant', '--tw-outline': 'outline', '--tw-error': 'error', '--tw-success': 'success', '--tw-warning': 'warning', '--tw-info': 'info', '--tw-neutral': 'on_surface_variant'}

Which Material 3 role each variable the base sheet reads comes from.

--tw-neutral has no role of its own: it tints the "nothing is claimed here" state of an indicator, which is the same job on_surface_variant does for text.

style_to_css

style_to_css(style: dict[str, Any] | None, widget_type: str | None = None) -> str

Translate a Style dump into a CSS string (declarations joined by "; ").

A faithful port of styleToCss in client/style.js. Field order follows the source model (flex, box model, paint, typography, dimensions, transition) so the emitted declarations are stable and identical to the client's. None/absent fields are skipped entirely — they mean "unset" and let the browser default apply.

A Row/Column (and their lazy variants) becomes a flex container by type even with no explicit direction in the style, so gap/justify/ align are never silently inert.

Parameters:

Name Type Description Default
style dict[str, Any] | None

A Style dump (model_dump(mode="json")), or None.

required
widget_type str | None

The widget type ("Row"/"Column"/...), so flex containers default to the right flex-direction even when the style does not set one. Optional.

None

Returns:

Type Description
str

A "; "-joined CSS declaration body ("" when empty/null).

Source code in tempestweb/html/css.py
def style_to_css(style: dict[str, Any] | None, widget_type: str | None = None) -> str:
    """Translate a Style dump into a CSS string (declarations joined by ``"; "``).

    A faithful port of ``styleToCss`` in ``client/style.js``. Field order follows
    the source model (flex, box model, paint, typography, dimensions, transition)
    so the emitted declarations are stable and identical to the client's.
    ``None``/absent fields are skipped entirely — they mean "unset" and let the
    browser default apply.

    A ``Row``/``Column`` (and their lazy variants) becomes a flex container by
    type even with no explicit ``direction`` in the style, so ``gap``/``justify``/
    ``align`` are never silently inert.

    Args:
        style: A Style dump (``model_dump(mode="json")``), or ``None``.
        widget_type: The widget type (``"Row"``/``"Column"``/...), so flex
            containers default to the right ``flex-direction`` even when the style
            does not set one. Optional.

    Returns:
        A ``"; "``-joined CSS declaration body (``""`` when empty/null).
    """
    direction = (style.get("direction") if style else None) or (
        _FLEX_DIRECTION_BY_TYPE.get(widget_type) if widget_type else None
    )
    if style is None and direction is None:
        return ""

    rules: list[str] = []

    # Flexbox layout. Row/Column are flex containers by type; an explicit
    # ``direction`` in the style overrides the type's natural axis.
    if direction is not None:
        rules.append("display: flex")
        rules.append(f"flex-direction: {direction}")
    if style is None:
        return "; ".join(rules)
    if style.get("justify") is not None:
        rules.append(
            f"justify-content: {_FLEX_EDGE.get(style['justify'], style['justify'])}"
        )
    if style.get("align") is not None:
        rules.append(f"align-items: {_FLEX_EDGE.get(style['align'], style['align'])}")
    if style.get("align_self") is not None:
        rules.append(
            f"align-self: {_FLEX_EDGE.get(style['align_self'], style['align_self'])}"
        )
    if style.get("grow") is not None:
        rules.append(f"flex-grow: {_num(style['grow'])}")
    if style.get("gap") is not None:
        rules.append(f"gap: {_num(style['gap'])}px")
    if style.get("flex_wrap") is not None:
        rules.append(f"flex-wrap: {style['flex_wrap']}")

    # Box model.
    if style.get("padding") is not None:
        rules.append(f"padding: {_edge_to_css(style['padding'])}")
    if style.get("margin") is not None:
        rules.append(f"margin: {_edge_to_css(style['margin'])}")
    if style.get("border") is not None:
        rules.extend(_border_rules(style["border"]))
    if style.get("radius") is not None:
        rules.append(f"border-radius: {_radius_value(style['radius'])}")

    # Paint.
    if style.get("background") is not None:
        rules.append(f"background: {_background_to_css(style['background'])}")
    if style.get("color") is not None:
        rules.append(f"color: {_color_to_rgba(style['color'])}")
    if style.get("opacity") is not None:
        rules.append(f"opacity: {_num(style['opacity'])}")
    if style.get("shadow") is not None:
        rules.append(f"box-shadow: {_shadow_to_css(style['shadow'])}")

    # Typography.
    if style.get("font_family") is not None:
        rules.append(f"font-family: {style['font_family']}")
    if style.get("font_size") is not None:
        rules.append(f"font-size: {_num(style['font_size'])}px")
    if style.get("font_weight") is not None:
        rules.append(f"font-weight: {_num(style['font_weight'])}")
    if style.get("font_style") is not None:
        rules.append(
            f"font-style: {_FONT_STYLE.get(style['font_style'], style['font_style'])}"
        )
    if style.get("text_align") is not None:
        rules.append(f"text-align: {style['text_align']}")
    if style.get("text_decoration") is not None:
        decoration = _TEXT_DECORATION.get(
            style["text_decoration"], style["text_decoration"]
        )
        rules.append(f"text-decoration: {decoration}")
    if style.get("letter_spacing") is not None:
        rules.append(f"letter-spacing: {_num(style['letter_spacing'])}px")
    if style.get("line_height") is not None:
        rules.append(f"line-height: {_num(style['line_height'])}")

    # Dimensions.
    if style.get("width") is not None:
        rules.append(f"width: {_num(style['width'])}px")
    if style.get("height") is not None:
        rules.append(f"height: {_num(style['height'])}px")
    if style.get("min_width") is not None:
        rules.append(f"min-width: {_num(style['min_width'])}px")
    if style.get("max_width") is not None:
        rules.append(f"max-width: {_num(style['max_width'])}px")
    if style.get("min_height") is not None:
        rules.append(f"min-height: {_num(style['min_height'])}px")
    if style.get("max_height") is not None:
        rules.append(f"max-height: {_num(style['max_height'])}px")
    if style.get("aspect_ratio") is not None:
        rules.append(f"aspect-ratio: {_num(style['aspect_ratio'])}")

    # Implicit animation: tween changed visual properties on the next rebuild.
    if style.get("transition") is not None:
        rules.append(f"transition: {_transition_to_css(style['transition'])}")

    if widget_type in _NATIVE_CONTROL_STYLE_TYPES:
        rules = _adapt_native_control_rules(rules)
    return "; ".join(rules)

escape_attr

escape_attr(value: object) -> str

Escape a value for use as a double-quoted HTML attribute value.

Escapes &, <, > and both quote characters, so the result can be safely placed inside attr="..." without breaking out of the attribute. None becomes the empty string.

Parameters:

Name Type Description Default
value object

Any value to render as an attribute value; coerced with :func:str.

required

Returns:

Type Description
str

The escaped attribute value, or "" when value is None.

Source code in tempestweb/html/escape.py
def escape_attr(value: object) -> str:
    """Escape a value for use as a double-quoted HTML **attribute value**.

    Escapes ``&``, ``<``, ``>`` and both quote characters, so the result can be
    safely placed inside ``attr="..."`` without breaking out of the attribute.
    ``None`` becomes the empty string.

    Args:
        value: Any value to render as an attribute value; coerced with
            :func:`str`.

    Returns:
        The escaped attribute value, or ``""`` when ``value`` is ``None``.
    """
    if value is None:
        return ""
    return html.escape(str(value), quote=True)

escape_text

escape_text(value: object) -> str

Escape a value for use as HTML text content.

Escapes &, < and > (but not quotes — they are safe in text nodes). None becomes the empty string.

Parameters:

Name Type Description Default
value object

Any value to render as text; coerced with :func:str.

required

Returns:

Type Description
str

The escaped text, or "" when value is None.

Source code in tempestweb/html/escape.py
def escape_text(value: object) -> str:
    """Escape a value for use as HTML **text content**.

    Escapes ``&``, ``<`` and ``>`` (but not quotes — they are safe in text
    nodes). ``None`` becomes the empty string.

    Args:
        value: Any value to render as text; coerced with :func:`str`.

    Returns:
        The escaped text, or ``""`` when ``value`` is ``None``.
    """
    if value is None:
        return ""
    return html.escape(str(value), quote=False)

render_document

render_document(widget: Widget, *, title: str, lang: str = 'pt-BR', head: str = '', htmx: bool = False, css_reset: bool = True) -> str

Render a widget tree to a complete, self-contained HTML document.

Wraps :func:render_to_html in a <!doctype html> shell with a charset meta, an escaped <title>, an optional CSS reset, any extra head markup, and — when htmx is set — the htmx runtime script tag.

htmx delivery

With htmx=True the document currently links htmx from a public CDN (unpkg.com). A later cycle's SDK will serve htmx locally; the URL is kept parameter-driven (a plain string in the output) so that change is a one-line swap and never a hard dependency here.

Parameters:

Name Type Description Default
widget Widget

The typed widget tree to render as the document body.

required
title str

The page title (escaped into <title>).

required
lang str

The document language for <html lang="...">. Defaults to "pt-BR".

'pt-BR'
head str

Extra raw markup to inject into <head> verbatim (the caller owns its safety). Defaults to "".

''
htmx bool

When True, inject the htmx runtime <script> tag. Defaults to False.

False
css_reset bool

When True, inject a minimal CSS reset. Defaults to True.

True

Returns:

Type Description
str

A complete HTML document string.

Raises:

Type Description
ValueError

If any widget carries an attrs key that is not a valid HTML attribute name.

Source code in tempestweb/html/renderer.py
def render_document(
    widget: Widget,
    *,
    title: str,
    lang: str = "pt-BR",
    head: str = "",
    htmx: bool = False,
    css_reset: bool = True,
) -> str:
    """Render a widget tree to a complete, self-contained HTML document.

    Wraps :func:`render_to_html` in a ``<!doctype html>`` shell with a charset
    meta, an escaped ``<title>``, an optional CSS reset, any extra ``head``
    markup, and — when ``htmx`` is set — the htmx runtime script tag.

    !!! info "htmx delivery"
        With ``htmx=True`` the document currently links htmx from a public CDN
        (``unpkg.com``). A later cycle's SDK will serve htmx locally; the URL is
        kept parameter-driven (a plain string in the output) so that change is a
        one-line swap and never a hard dependency here.

    Args:
        widget: The typed widget tree to render as the document body.
        title: The page title (escaped into ``<title>``).
        lang: The document language for ``<html lang="...">``. Defaults to
            ``"pt-BR"``.
        head: Extra raw markup to inject into ``<head>`` verbatim (the caller owns
            its safety). Defaults to ``""``.
        htmx: When ``True``, inject the htmx runtime ``<script>`` tag. Defaults to
            ``False``.
        css_reset: When ``True``, inject a minimal CSS reset. Defaults to
            ``True``.

    Returns:
        A complete HTML document string.

    Raises:
        ValueError: If any widget carries an ``attrs`` key that is not a valid
            HTML attribute name.
    """
    body = render_to_html(widget)
    reset = f"<style>{_CSS_RESET}</style>" if css_reset else ""
    script = _HTMX_SCRIPT if htmx else ""
    return (
        "<!doctype html>"
        f'<html lang="{escape_attr(lang)}">'
        "<head>"
        '<meta charset="utf-8">'
        '<meta name="viewport" content="width=device-width, initial-scale=1">'
        f"<title>{escape_text(title)}</title>"
        f"{reset}{head}{script}"
        "</head>"
        f"<body>{body}</body>"
        "</html>"
    )

render_to_html

render_to_html(widget: Widget) -> str

Render a widget tree to a static HTML fragment string.

Builds the widget with :func:tempest_core.build and walks the resulting IR into HTML. The output is a fragment (no <html>/<body> wrapper); use :func:render_document for a full page.

Parameters:

Name Type Description Default
widget Widget

The typed widget tree to render.

required

Returns:

Type Description
str

The static HTML fragment.

Raises:

Type Description
ValueError

If any widget carries an attrs key that is not a valid HTML attribute name.

Source code in tempestweb/html/renderer.py
def render_to_html(widget: Widget) -> str:
    """Render a widget tree to a static HTML fragment string.

    Builds the widget with :func:`tempest_core.build` and walks the resulting IR
    into HTML. The output is a fragment (no ``<html>``/``<body>`` wrapper); use
    :func:`render_document` for a full page.

    Args:
        widget: The typed widget tree to render.

    Returns:
        The static HTML fragment.

    Raises:
        ValueError: If any widget carries an ``attrs`` key that is not a valid
            HTML attribute name.
    """
    return _node_to_html(build(widget))

theme_css

theme_css(theme: Theme) -> str

Render a theme as the CSS custom properties the client reads.

Parameters:

Name Type Description Default
theme Theme

The app's theme, usually from :meth:~tempest_core.Theme.from_seed.

required

Returns:

Name Type Description
str str

A CSS block for the document head — :root declarations, plus a

str

:root[data-tw-theme="dark"] block when the theme can go dark. Never a

str

prefers-color-scheme query: see the module docstring for why the OS

str

alone must not flip the page.

Example

```python from tempest_core import Theme, ThemeMode from tempest_core import Color from tempestweb.html import theme_css

def head() -> str: """Build the head markup that rebrands every widget.

Returns:

Name Type Description
str str

A style element carrying the app's palette.

    """
    theme = Theme.from_seed(Color(r=39, g=58, b=79), mode=ThemeMode.SYSTEM)
    return f"<style>{theme_css(theme)}</style>"
```
Source code in tempestweb/html/theme.py
def theme_css(theme: Theme) -> str:
    """Render a theme as the CSS custom properties the client reads.

    Args:
        theme (Theme): The app's theme, usually from
            :meth:`~tempest_core.Theme.from_seed`.

    Returns:
        str: A CSS block for the document head — ``:root`` declarations, plus a
        ``:root[data-tw-theme="dark"]`` block when the theme can go dark. Never a
        ``prefers-color-scheme`` query: see the module docstring for why the OS
        alone must not flip the page.

    Example:
        ```python
        from tempest_core import Theme, ThemeMode
        from tempest_core import Color
        from tempestweb.html import theme_css


        def head() -> str:
            \"\"\"Build the head markup that rebrands every widget.

    Returns:
                str: A style element carrying the app's palette.
            \"\"\"
            theme = Theme.from_seed(Color(r=39, g=58, b=79), mode=ThemeMode.SYSTEM)
            return f"<style>{theme_css(theme)}</style>"
        ```
    """
    schemes = theme.tokens.schemes
    dark_selector = f':root[{THEME_MODE_ATTR}="dark"]'
    dark = _declarations(schemes.dark, "  ")
    if theme.mode is ThemeMode.DARK:
        return f":root, {dark_selector} {{\n{dark}\n}}"
    light = f":root {{\n{_declarations(schemes.light, '  ')}\n}}"
    if theme.mode is ThemeMode.LIGHT:
        return light
    return f"{light}\n{dark_selector} {{\n{dark}\n}}"