| Context | Need to generate Python from .hyper. Options: return string, return list, or yield. |
| Decision | Components are generators that yield chunks. @html decorator handles str() conversion. |
| Trade-off | Generator overhead, but enables streaming responses without API changes. |
| Context | Without decorator, str(Template()) returns "<generator object>". |
| Decision | @html on all components. Built-in to .hyper (no import needed). In compiled output: from hyper import html. |
| Trade-off | Decorator overhead, but uniform API (iterable, str()-able). |
Usage:
str(Button(text="Click")) # full render
for chunk in Button(text="Click"): # streaming
response.write(chunk)| Context | Props could be positional or keyword. Positional is error-prone for many props. |
| Decision | All params keyword-only via *, prefix. *args rejected at parse time. |
| Trade-off | Verbose call sites, but matches JSX model and allows defaults before non-defaults. |
| Context | Expressions need HTML escaping. Can't escape at compile time (values unknown). |
| Decision | Emit ‹ESCAPE:{expr}› markers, process via replace_markers() at runtime. |
| Trade-off | Runtime overhead, but single f-string per block and unified marker system. |
| Context | Need consistent function naming that works for imports and avoids linter issues. |
| Decision | Single-component files: generate def render() with alias from filename (Button = render). Multi-component files: named functions directly (def Button, def ButtonGroup). |
| Trade-off | Two patterns, but clean imports and linter-compliant. |
| Context | Need to distinguish single-component files from multi-component modules. |
| Decision | PascalCase file (Button.hyper) = single component, promoted to package level. lowercase file (buttons.hyper) = multi-component module, import from submodule. |
| Trade-off | Filename matters, but clear intent and Pythonic import patterns. |
Import patterns:
from components import Button, Card # single-component files
from components.buttons import IconButton # multi-component module
from components.forms import Input, Select # multi-component module| Context | Need syntax to use components in templates that's distinct from HTML elements. |
| Decision | <{Component} prop={value} /> for components. Braces indicate "this is Python". |
| Trade-off | Slightly more verbose than JSX, but clear distinction between HTML and components. |
Compiles to:
yield from Component(prop=value)| Context | Components need to accept child content, both default and named slots. |
| Decision | {...} = default slot, {...name} = named slot (in definition). <{...name}>content</{...name}> for filling named slots. |
| Trade-off | New syntax to learn, but consistent with {} = Python convention. |
Definition:
# Card.hyper
<div class="card">
<header>{...header}</header>
<main>{...}</main>
</div>
Usage:
<{Card}>
<{...header}>
<h1>Title</h1>
</{...header}>
<p>Default slot content</p>
</{Card}>
Slot parameters: Default slot: _content. Named slots: _header, _footer, etc.
| Context | Need to distinguish between components (yield HTML) and helper functions (return values). |
| Decision | @html decorator marks functions that produce HTML. Plain def = helper function. |
| Trade-off | Explicit decorator, but clear intent and no casing rules. |
Component (yields):
@html
def Badge(text: str):
<span class="badge">{text}</span>
end
<{Badge} text="New" />
Helper (returns):
def format_price(cents: int) -> str:
return f"${cents / 100:.2f}"
<span>{format_price(item.price)}</span>
Enforcement:
@htmlfunctions → use with<{...}>syntax- Plain
deffunctions → use with{...}syntax <{lowercase}>→ Compiler error: "Component names must be PascalCase"
| Context | Need clear boundary between setup (params, imports, helpers) and template body. |
| Decision | --- required when there's any setup code above. Separates "code" from "output". |
| Trade-off | Extra line, but explicit and readable. |
| Context | Mixed HTML/Python needs clear block boundaries. Indentation alone is ambiguous. |
| Decision | end required for control flow blocks (if, for, while, match) inside HTML context. Function definitions follow Python rules (end by dedent). |
| Trade-off | Hybrid approach - functions are Pythonic, control flow in HTML is explicit. |
Function definitions (no end, use dedent):
@html
def Badge(text: str):
<span>{text}</span>
@html
def List(items: list):
<ul>
for item in items:
<li>{item}</li>
end
</ul>
Control flow needs end:
if show:
<div>Visible</div>
end
for item in items:
<li>{item}</li>
end
Pure Python helper (standard Python):
def format_price(cents: int) -> str:
return f"${cents / 100:.2f}"
| Context | How does indentation interact with end keywords? |
| Decision | Function definitions use indentation (Python rules). Control flow in HTML uses end (indentation optional within). Element tags are self-contained. |
| Trade-off | Hybrid approach - familiar for functions, flexible for HTML content. |
Functions use indentation:
@html
def Badge(text: str):
<span>{text}</span>
# dedent ends the function
Control flow in HTML - indentation optional:
# Valid (though discouraged):
if items:
<ul>
for item in items:
<li>{item}</li>
end
</ul>
end
Element tags are self-contained - no control flow inside <...>:
<div class={...} /> # ✓ attributes in one unit
| Context | Need to conditionally include/omit attributes based on runtime values. |
| Decision | Ternary without else: class={"active" if condition}. If falsy, attribute is omitted entirely. |
| Trade-off | Hyper-specific extension (Python requires else), but cleaner syntax. |
Examples:
<div class={"active" if is_active}> # omitted if falsy
<div class={"active" if is_active else ""}> # empty string if falsy
<div data-id={user_id if user_id}> # omitted if None/falsy
Compiles to:
# Attribute conditionally included
**({'class': 'active'} if is_active else {})| Context | Invalid HTML causes browser quirks and a11y issues. |
| Decision | Parser validates: void elements, nesting rules, duplicate attributes. Errors include help text. |
| Trade-off | More complex parser, but catches errors early with clear messages. |
| Context | Generated Python could collapse content to single lines or preserve structure. |
| Decision | Preserve newlines/indentation from source. Use triple quotes. Combine adjacent HTML into single yields until Python/slot breaks it. |
| Trade-off | Larger output files, but readable/debuggable code and accurate source maps. |
| Context | Attributes like class="card {theme}" mix static and dynamic content. |
| Decision | {expr} inside quoted attributes = template expression, gets ESCAPE markers. {{ = literal brace. |
| Trade-off | Implicit f-string behavior, but intuitive for interpolation. |
Example:
<div class="card {theme}" data-id="{id}">
Compiles to:
yield f"""<div class="card {escape(theme)}" data-id="{escape(id)}">"""| Context | The compiler already parses .hyper → AST in Rust. Adding a second codegen backend to emit .rs instead of .py would give Hyper a server-side Rust target. |
| Decision | Rust variant uses Rust types and braces (not indentation/end). Props implicitly public, string conversions implicit (.into()), variable refs implicit ({name} → &self.name). Components compile to struct + impl Display. Slots use buffer strategy initially (render children to String). IDE support via ghost macro wrapping (hyper_component! { ... } in memory) with span mapping back to source. |
| Trade-off | Buffer strategy allocates intermediate strings (not zero-cost), but trivial to implement and still far faster than Python. Generic strategy (Layout<T: Display>) deferred. |
See: docs/design/rust-backend.md
| Context | Some .hyper files are pure component libraries with no main template. |
| Decision | No --- body = pure module. Use @html to define components explicitly. |
| Trade-off | Explicit decorators, but clear what's exported. |
Example:
# buttons.hyper
@html
def Button(text: str):
<button>{text}</button>
end
@html
def IconButton(icon: str, text: str):
<button><i class={icon} />{text}</button>
end
| Context | Markers (‹ESCAPE:{expr}›) round-tripped Python values through string serialization. safe() was broken (f-string stringified the Safe object before escape_html saw it). ast.literal_eval failed for non-literal reprs. |
| Decision | Replace all markers with direct function calls in f-strings. {escape(expr)} for content, {render_class(expr)} for class, {render_attr("name", expr)} for booleans, etc. |
| Trade-off | Generated output diverges slightly more from source HTML structure, but IDE injection mapping still works (prefix/suffix mechanism unchanged). Eliminates replace_markers(), regex patterns, and ast.literal_eval. |