Skip to content

Design discussion: formal Renderer interface for alternative render targets #39

Description

@LoboGuardian

Opening this as a design conversation rather than a change request — I'd like to hear how you see the renderer evolving before proposing any code.

Context

Today HTMLRenderer is the only renderer, and the "renderer contract" exists implicitly in its implementation: it walks the Component tree, pulls js_functions / state_initializers from the render context and appends them to head, escapes text nodes, treats RAW_TEXT_TAGS as verbatim, and emits {{...}} placeholders for JSCode children. None of that is declared anywhere as guaranteed behavior — it can only be learned by reading elements/renderer/html.py.

Two observations from working with the code recently:

  • fix(server): propagate render context to sync handlers in executor #38 showed how easy it is to break implicit behavior invisibly: the threading.localContextVar change silently disconnected sync handlers from the render context, and nothing defined (or tested) what was supposed to hold.
  • The Component tree is a clean, target-agnostic UI AST, and py2js is reusable on its own. That combination makes HTeaLeaf renderable to targets other than an HTML string — but today an external renderer has to depend on undeclared internals (get_render_ctx(), ctx.js_functions, ctx.state_initializers).

What I'd like to discuss

  1. Would you accept extracting a minimal formal interface — e.g. an abstract Renderer base (or Protocol) with render(cmpt: Component) -> str — that HTMLRenderer implements, with the render-context interaction (what a renderer may read, and when injection happens) documented as part of the contract?
  2. Related surface question: which of the currently un-exported names do you consider public? (elements.py's __all__ omits tl_if and the table elements, and elements/__init__.py doesn't re-export them, so consumers must import from htealeaf.elements.elements.)
  3. Is Server.__process_response__ the intended seam for choosing a renderer, or would you prefer renderer selection to live elsewhere (per-app config, per-route, a hook)?

Motivation (full disclosure)

I'm exploring an external, independent package that consumes HTeaLeaf to emit Astro components (server-first HTML + islands from @js/LocalState maps naturally onto Astro's islands model). It would live outside this repo as a normal dependency, not a fork — but it only works cleanly if the renderer boundary is something HTeaLeaf actually promises. Rather than copying internals into that package, I'd prefer to send small PRs here (like #38) so the public API gets formalized where it benefits every consumer.

No urgency — happy to draft the base-class PR if you like the direction, or drop it if you'd rather keep the renderer private for now.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions