Genro Builders for Dummies

## The Problem

You need to generate structured output — HTML pages, Markdown documents, XML files, configuration files, PDF reports. You start with string concatenation. Then templates. Then a DSL. Each project reinvents the same pattern: define a structure, validate it, produce output.

Worse: what if the same data needs to become HTML and PDF and a live widget tree? Now you have three generators that don't share anything.

Genro Builders separates structure from output. You define a grammar (what tags exist, what can contain what), describe your content as a tree, and then render it to any format. One description, many outputs. And it's reactive — change the data, the output updates automatically.

### When does this actually matter?

#### Same data, many formats

A report that needs to be HTML for the web, PDF for printing, and Markdown for documentation — from the same source tree.

#### Validated structure

The grammar catches mistakes at creation time: wrong children, missing required elements, cardinality violations. No broken output.

#### Reactive output

Change a data value after build — the output updates automatically. No manual re-rendering.

#### Reusable components

`@component` defines composite structures that expand lazily. A login_form always generates the right inputs.

#### Domain-specific API

Your code reads like the domain: `menu.first_courses().pasta(name="Lasagne")` — not generic tree manipulation.

#### Manager = document

Use a `BuilderManager`, define `store()` and `main()` — your class is the document. Reusable, testable, composable.

## Your First Builder in 30 Seconds

A contact list page — with a loop, because that's where builders shine over templates:

    from genro_builders.builders import HtmlBuilder

    contacts = [
        ("Giovanni", "giovanni@example.com"),
        ("Maria", "maria@example.com"),
        ("Luca", "luca@example.com"),
    ]

    builder = HtmlBuilder()
    body = builder.source.body()
    body.h1("Contacts")

    table = body.table()
    header = table.tr()
    header.th("Name")
    header.th("Email")

    for name, email in contacts:
        tr = table.tr()
        tr.td(name)
        tr.td(email)

    builder.build()
    print(builder.render())

Output:

    <body>
      <h1>Contacts</h1>
      <table>
        <tr><th>Name</th><th>Email</th></tr>
        <tr><td>Giovanni</td><td>giovanni@example.com</td></tr>
        <tr><td>Maria</td><td>maria@example.com</td></tr>
        <tr><td>Luca</td><td>luca@example.com</td></tr>
      </table>
    </body>

Key insight: this is plain Python — loops, conditionals, variables. No template language, no string concatenation. The builder validates the structure as you go: `th` can only go inside `tr`, `tr` can only go inside `table`. Try putting a `td` directly in `body` and you get an error immediately.

### As a reusable class (via BuilderManager)

    from genro_builders import BuilderManager

    class HtmlManager(BuilderManager):
        def __init__(self):
            self.page = self.set_builder('page', HtmlBuilder)
        def render(self):
            return self.page.render()

    class ContactPage(HtmlManager):
        def __init__(self):
            super().__init__()
            self.setup()
            self.build()

        def store(self, data):
            data['contacts'] = [
                {'name': 'Giovanni', 'email': 'giovanni@example.com'},
                {'name': 'Maria', 'email': 'maria@example.com'},
            ]

        def main(self, source):
            body = source.body()
            body.h1("Contacts")
            self.contact_table(body)

        def contact_table(self, parent):
            table = parent.table()
            header = table.tr()
            header.th("Name")
            header.th("Email")
            for c in self.reactive_store['contacts']:
                tr = table.tr()
                tr.td(c['name'])
                tr.td(c['email'])

    page = ContactPage()
    print(page.render())

Same output — but now the page is a class you can subclass, test, and reuse. The builder is a machine; the manager defines what to build.

## The Three Decorators

A builder's grammar is defined with three decorators. Each decorator marks a method on the builder class:

### @element — a pure tag

Declares that a tag exists and what it can contain. The body must be empty (`...`).

    @element(sub_tags="")           # leaf: no children allowed
    def ingredient(self): ...

    @element(sub_tags="pasta, risotto")  # container: these children only
    def first_courses(self): ...

    @element(sub_tags="*")            # wildcard: any children
    def div(self): ...

### @abstract — inheritance base

Cannot be used directly. Defines `sub_tags` that concrete elements inherit.

    @abstract(sub_tags="ingredient, note")
    def base_dish(self): ...

    @element(inherits_from="@base_dish")   # inherits sub_tags
    def pasta(self): ...

### @component — reusable structure

Has a body with logic. The body is NOT executed at call time — only during `build()`, lazily.

    @component(sub_tags="")
    def soffritto(self, comp, **kw):
        comp.ingredient("onion")
        comp.ingredient("carrot")
        comp.ingredient("celery")

Lazy expansion: when you call `source.soffritto()`, only a placeholder node is created. At `build()` time, the body runs and the three ingredients appear. The source stays a clean "recipe" until materialization.

### sub_tags cardinality

Syntax | Meaning
--- | ---
`""` | Leaf — no children allowed
`"*"` | Any children
`"a, b"` | Only a and b (any count)
`"a[1]"` | Exactly 1 of a
`"a[:2]"` | 0 to 2 of a
`"a[1:3]"` | 1 to 3 of a

### Return value: leaf vs container

    # h1 is leaf (sub_tags="") → returns PARENT → p is sibling
    source.h1("Title").p("Subtitle")

    # div is container (sub_tags="*") → returns SELF → p is inside
    div = source.div(id="main")
    div.p("Inside the div")

## The Manager Pattern — store() / main()

A builder is a machine — it knows how to build, not what. To define the content declaratively, use a `BuilderManager`. The three-level hierarchy: manager mixin → specialized manager → your app.

    from genro_builders.builders import MarkdownBuilder
    from genro_builders import BuilderManager

    class MdManager(BuilderManager):
        def __init__(self):
            self.doc = self.set_builder('doc', MarkdownBuilder)
        def render(self):
            return self.doc.render()

    class MonthlyReport(MdManager):
        def __init__(self):
            super().__init__()
            self.setup()       # store → main
            self.build()       # source → built

        def store(self, data):
            # Populate data. Called by setup().
            data['title'] = 'January 2026 Report'
            data['author'] = 'Giovanni'

        def main(self, source):
            # Entry point: build the tree. Called by setup() after store().
            source.h1(value='^title')
            source.p(value='^author')
            self.sections(source)

        def sections(self, source):
            # Helper method — organize as you like
            source.h2('Sales')
            source.p('Sales increased by 15%.')

    report = MonthlyReport()
    print(report.render())

The manager IS the document. `store()` and `main()` live on the manager, not on the builder. The builder is a machine that materializes and renders. `setup()` calls store → main. `build()` materializes. `render()` produces output.

### Both are optional

What you define | What happens
--- | ---
`main()` only | Source populated from main, data populated manually
`store()` only | Data populated, source populated manually from outside
Both | `setup()` calls `store()` → `main()`
Neither | Populate manually: `app.page.source.h1("...")`

### node_id: find nodes directly

    builder = HtmlBuilder()
    builder.source.div(node_id='header').h1('Title')
    builder.source.div(node_id='content').p('Body')
    builder.build()

    header = builder.node_by_id('header')   # O(1) dict lookup

Duplicate `node_id` raises `ValueError`.

## ^Pointers: Data Binding

A `^pointer` connects a node in the tree to a value in the data store. At `build()` time, pointers are resolved. After `subscribe()`, changing the data triggers automatic re-rendering.

    builder.data['user'] = 'Giovanni'
    builder.source.h1(value='^user')
    builder.build()
    builder.subscribe()    # activate reactivity
    # h1 shows "Giovanni"

    builder.data['user'] = 'Marco'
    # h1 automatically updates to "Marco"

### Three path forms

Syntax | Type | Resolves to
--- | --- | ---
`^user.name` | Absolute | `data['user.name']`
`^.name` | Relative | From this node's `datapath` ancestor chain
`^#header.title` | Symbolic | Find `node_id='header'`, use its datapath

### Relative paths with datapath

    outer = source.div(datapath='customer')
    outer.p(value='^.name')      # resolves to ^customer.name
    outer.p(value='^.email')     # resolves to ^customer.email

### Symbolic paths with #node_id

    source.div(node_id='addr', datapath='customer.address')

    # Anywhere else in the tree:
    source.span(value='^#addr.street')  # resolves to ^customer.address.street

abs_datapath() resolves any form to absolute: `node.abs_datapath('.name')` returns the full path. Works with absolute, relative, and symbolic paths.

### Attribute pointers

    # Read attribute 'color' from data node 'theme.button'
    source.div(bg='^theme.button?color')

## Renderers: Serialized Output

A renderer transforms the built Bag into a string (HTML, Markdown, XML, etc.). It's "dead" output — no live objects, no reactivity in the result itself.

### Two styles

    from genro_builders.renderer import BagRendererBase, renderer

    class MyRenderer(BagRendererBase):

        # Declarative: empty body + template
        @renderer(template="# {node_value}")
        def h1(self): ...

        # Logic: method receives (self, node, ctx)
        @renderer()
        def table(self, node, ctx):
            lines = []
            for row in node.value:
                cells = [str(c.static_value) for c in row.value]
                lines.append("| " + " | ".join(cells) + " |")
            return "\n".join(lines)

### Template variables

The context (`ctx`) passed to handlers and templates contains:

Variable | Content
--- | ---
`{node_value}` | The node's value as string
`{node_label}` | The node's label
`{children}` | Rendered children joined together
`{any_attr}` | Any node attribute (e.g. `{href}`, `{lang}`)

### Register on builder

    class MyBuilder(BagBuilderBase):
        _renderers = {'markdown': MyRenderer}

## Compilers: Live Objects

A compiler produces live objects — Textual widgets, openpyxl workbooks, GUI elements. Same concept as renderer, different output type.

    from genro_builders.compiler import BagCompilerBase, compiler

    class TextualCompiler(BagCompilerBase):

        # Declarative: module + class to instantiate
        @compiler(module="textual.widgets", cls="Button")
        def button(self): ...

        # Logic: build the object manually
        @compiler()
        def datatable(self, node, ctx):
            return build_datatable(node)

        def compile(self, built_bag, target=None):  # must override
            return list(self._walk_compile(built_bag))

### Register on builder

    class MyBuilder(BagBuilderBase):
        _renderers  = {'html': HtmlRenderer}
        _compilers  = {'textual': TextualCompiler}

    # Use:
    builder.build()                       # materialize source → built
    html = builder.render()               # string output
    html = builder.render(output='page.html')  # with destination
    widgets = builder.compile(name='textual')  # live objects
    widgets = builder.compile(name='textual', target=container)  # with target

render() = strings. compile() = objects. A builder can have both. Multiple renderers and compilers are supported — select by name.

## BuilderManager: Multiple Builders, Shared Data

When you need the same data to produce different outputs (HTML + PDF, or page + sidebar), a `BuilderManager` coordinates multiple builders with a shared data store.

    from genro_builders import BuilderManager

    class MyReport(BuilderManager):
        def __init__(self):
            self.html = self.set_builder('html', HtmlBuilder)
            self.pdf  = self.set_builder('pdf', PdfBuilder)
            self.setup()   # store → main_html → main_pdf
            self.build()   # materialize all builders

        def store(self, data):
            # Shared data — all builders see this
            data['title'] = 'Q1 Report'
            data['author'] = 'Giovanni'

        def main_html(self, source):
            source.h1(value='^title')
            source.p(value='^author')

        def main_pdf(self, source):
            source.title(value='^title')
            source.subtitle(value='^author')

    report = MyReport()

### Data namespaces

Pointer | Reads from
--- | ---
`^title` | Shared store root
`^.subtitle` | Builder's private namespace (`builders.html.subtitle`)

## Putting It All Together

A complete custom builder: grammar + renderer + subclass usage.

    from genro_builders import BagBuilderBase
    from genro_builders.builders import element, component
    from genro_builders.renderer import BagRendererBase, renderer

    # ── 1. Grammar ──

    class InvoiceBuilder(BagBuilderBase):

        @element(sub_tags="line_item, note")
        def invoice(self): ...

        @element(sub_tags="")
        def line_item(self, description: str, qty: int = 1,
                      price: float = 0): ...

        @element(sub_tags="")
        def note(self): ...

        @component(sub_tags="")
        def standard_footer(self, comp, **kw):
            comp.note("Payment within 30 days")
            comp.note("VAT included")

    # ── 2. Renderer ──

    class InvoiceRenderer(BagRendererBase):

        @renderer(template="=== INVOICE ===\n{children}")
        def invoice(self): ...

        @renderer()
        def line_item(self, node, ctx):
            return f"  {ctx['description']} x{ctx['qty']} @ EUR {ctx['price']}"

        @renderer(template="  NOTE: {node_value}")
        def note(self): ...

    InvoiceBuilder._renderers = {'text': InvoiceRenderer}

    # ── 3. Manager + App ──

    class InvoiceManager(BuilderManager):
        def __init__(self):
            self.inv = self.set_builder('inv', InvoiceBuilder)
        def render(self):
            return self.inv.render()

    class MyInvoice(InvoiceManager):
        def __init__(self):
            super().__init__()
            self.setup()
            self.build()

        def store(self, data):
            data['client'] = 'Acme Corp'

        def main(self, source):
            inv = source.invoice()
            inv.line_item(description="Widget A", qty=10, price=5.99)
            inv.line_item(description="Widget B", qty=3, price=12.50)
            inv.standard_footer()

    invoice = MyInvoice()
    print(invoice.render())

## Cheat Sheet

### I want to…

I want to… | How
--- | ---
Create a builder | `class MyBuilder(BagBuilderBase): ...`
Define a tag | `@element(sub_tags="child1, child2")`
Define a leaf tag | `@element(sub_tags="")`
Define a wildcard container | `@element(sub_tags="*")`
Inherit sub_tags from abstract | `@element(inherits_from="@base")`
Create a reusable component | `@component(sub_tags="")` with body
Limit where a tag can go | `@element(parent_tags="ul,ol")`
Limit children count | `sub_tags="item[1:3]"` (1 to 3)
Map multiple tags to one handler | `@element(tags="wine, beer, juice")`
Bind a node to data | `source.h1(value='^title')`
Use relative data path | `source.div(datapath='user').p(value='^.name')`
Use symbolic data path | `source.span(value='^#header.title')`
Identify a node for lookup | `source.div(node_id='header')`
Find a node by id | `builder.node_by_id('header')`
Resolve any path to absolute | `node.abs_datapath('.name')`
Define a template renderer | `@renderer(template="# {node_value}")`
Define a logic renderer | `@renderer()` + method body
Define a compiler handler | `@compiler(module="x", cls="Y")`
Register renderer on builder | `_renderers = {'name': MyRenderer}`
Register compiler on builder | `_compilers = {'name': MyCompiler}`
Render to a file/stream | `builder.render(output='page.html')`
Compile with a target container | `builder.compile(target=parent_widget)`
Coordinate multiple builders | Subclass `BuilderManager` + `set_builder()`
Set shared data (manager) | Override `store(data)` on manager
Build each builder's source | Override `main_html(source)`, `main_pdf(source)` on manager
Validate structure | `builder._check(bag)`
Validate attribute types | Type hints + `Annotated[str, Regex(...)]`
Rebuild with new content | `builder.rebuild(main=my_func)`

### Pipeline summary

Step | Where | What happens
--- | --- | ---
`setup()` | Manager | Calls store(data) → main(source) to populate
`build()` | Manager/Builder | Expand @components, resolve ^pointers (materialize)
`subscribe()` | Manager/Builder | Activate reactive bindings (optional)
`render()` | Builder | Produce string output via BagRendererBase
`compile()` | Builder | Produce live objects via BagCompilerBase

### Built-in builders

Builder | Import | Output
--- | --- | ---
HtmlBuilder | `from genro_builders.builders import HtmlBuilder` | HTML5
MarkdownBuilder | `from genro_builders.builders import MarkdownBuilder` | Markdown
XsdBuilder | `from genro_builders.builders.xsd import XsdBuilder` | XML from XSD