# Bengal Templates Guide

This directory contains Kida templates for the Bengal default theme. This guide covers critical patterns for working with Bengal's templating system.

## Key Concepts

Bengal uses Kida with **StrictUndefined** mode in development (`bengal serve`). This means accessing undefined variables or attributes raises errors rather than silently returning empty strings. This catches bugs early but requires careful template design.

---

## The Macro/Include Separation Pattern

### The Problem

When you use `{% from 'file.html' import macro_name %}`, Kida executes **all top-level code** in that file, not just the macro definition. In StrictUndefined mode, this causes errors if the body code references variables that don't exist in the importing context.

```jinja
{# BAD: cards.html - mixes macro with body code #}
{% def element_card(child) %}
  <div>{{ child.name }}</div>
{% end %}

{# This body code runs during import! #}
{% if children %}  {# ERROR: 'children' is undefined #}
  {% for child in children %}
    {{ element_card(child) }}
  {% endfor %}
{% endif %}
```

When another template does `{% from 'cards.html' import element_card %}`, the `{% if children %}` line executes and fails because `children` doesn't exist in that context.

### The Solution: Separate Macros from Body Code

Split templates that need both macros AND body code into two files:

```
partials/
├── _macros/
│   └── element-card.html    # Pure function file (safe to import)
└── cards.html               # Include-only file (uses the macro)
```

**Pure function file** (`_macros/element-card.html`):
```jinja
{# Only function definitions - no body code #}
{% def element_card(child) %}
  <div>{{ child.name }}</div>
{% end %}
```

**Include-only file** (`cards.html`):
```jinja
{% from 'partials/_macros/element-card.html' import element_card %}

{# Body code is safe here - this file is included, not imported #}
{% if children %}
  {% for child in children %}
    {{ element_card(child) }}
  {% endfor %}
{% endif %}
```

### When to Use Each Pattern

| Pattern | When to Use | How to Reference |
|---------|-------------|------------------|
| **Pure function file** | Macro needs to be imported by multiple templates | `{% from '...' import macro %}` |
| **Include-only file** | Template renders content directly | `{% include '...' %}` |
| **Self-contained template** | Macro is only used within same file | Define macro inline |

---

## Safe Attribute Access with `getattr()`

### The Problem

DocElement objects from autodoc may have optional attributes. In StrictUndefined mode, accessing a missing attribute fails:

```jinja
{# BAD: Fails if element.children doesn't exist #}
{% for child in element.children %}
```

The common workaround `element.children or []` also fails because it still accesses the attribute first.

### The Solution: Use `getattr()`

Bengal exposes Python's `getattr()` as a template global:

```jinja
{# GOOD: Safe access with default value #}
{% for child in getattr(element, 'children', []) %}

{# Also works for nested access #}
{% set params = getattr(element.metadata, 'parameters', []) %}
```

### When to Use `getattr()`

- Accessing attributes on DocElement objects (`element`, `child`, `method`, etc.)
- Any attribute that might not exist on all element types
- When iterating over optional collections

### When NOT Needed

- Accessing template variables you control (e.g., `{% set foo = 'bar' %}`)
- Using the `default` filter on simple values: `{{ value | default('fallback') }}`
- Accessing dictionary keys with `.get()`: `{{ dict.get('key', 'default') }}`

---

## Template Organization

```
templates/
├── base.html                    # Base layout (extends nothing)
├── README.md                    # This file
│
├── partials/                    # Shared components
│   ├── components/              # Modular component macros
│   │   ├── article.html         # Article card component
│   │   ├── helpers.html         # Utility functions
│   │   ├── tags.html            # Tag list and tag cloud
│   │   ├── tiles.html           # Content tiles component
│   │   └── widgets.html         # Discovery widgets
│   ├── _macros/                 # Pure function files (safe to import)
│   ├── navigation-components.html  # Pure functions
│   └── page-hero-*.html            # Include-only (have body code)
│
├── autodoc/                     # Autodoc-specific templates
│   └── partials/
│       ├── _macros/             # Pure function files
│       │   └── element-card.html
│       ├── header.html          # Include-only partials
│       ├── signature.html
│       └── cards.html           # Uses macro from _macros/
│
├── autodoc/python/               # Python API docs
│   ├── module.html
│   └── partials/
│       ├── _macros/
│       │   └── render-method.html
│       └── method-item.html
│
├── autodoc/cli/               # CLI docs
│   ├── command.html
│   └── command-group.html
│
└── autodoc/openapi/               # OpenAPI docs
    ├── endpoint.html
    ├── overview.html
    ├── schema.html
    └── section-index.html
```

### Naming Conventions

- `_macros/` directories contain pure function files (the underscore signals "internal")
- Files in `_macros/` should only define macros, never render content
- Files outside `_macros/` can be either include-only or pure functions

---

## Common Patterns

### Pattern 1: Iterating Over Optional Children

```jinja
{# Safe iteration over children #}
{% set element_children = getattr(element, 'children', []) %}
{% set methods = element_children | selectattr('element_type', 'eq', 'method') | list %}

{% for method in methods %}
  {{ render_method(method) }}
{% endfor %}
```

### Pattern 2: Conditional Sections Based on Children

```jinja
{% set options = getattr(element, 'children', []) | selectattr('element_type', 'eq', 'option') | list %}

{% if options %}
<section class="autodoc-section">
  <h2>Options</h2>
  {# ... render options ... #}
</section>
{% endif %}
```

### Pattern 3: Importing Macros for Use in Another Template

```jinja
{# Import from _macros/ directory - safe, no body code will execute #}
{% from 'autodoc/partials/_macros/element-card.html' import element_card %}

{# Now use the macro #}
{% for child in children %}
  {{ element_card(child) }}
{% endfor %}
```

### Pattern 4: Include-Only Template with Context

```jinja
{# In the parent template #}
{% set element = some_element %}
{% include 'autodoc/partials/header.html' %}

{# header.html expects 'element' to be defined in context #}
```

---

## Debugging Template Errors

### Malformed Frontmatter

Frontmatter collections (`tags`, `params.author_links`, `params.quick_links`) can be malformed: string instead of list, empty items, `null`, or link objects missing `href`/`url`. Iterating without guards causes character-by-character output (for strings) or missing-attribute errors.

**Solution:** Use defense-in-depth patterns (see `SAFE_PATTERNS.md` → "Iterating Over Frontmatter Collections"):

- `tags ?? []` and `{% if tag %}` inside the loop
- `params?.quick_links ?? []` with `{% if link and (link?.href or link?.url) %}` guard
- Reference: autodoc template hardening (same pattern)

---

### Error: `'X' is undefined`

1. **Check if you're importing a file with body code**
   - Look for `{% from 'file.html' import ... %}`
   - Check if `file.html` has code outside macros
   - Solution: Move macro to `_macros/` subdirectory

2. **Check if you're accessing an optional attribute**
   - Use `getattr(obj, 'attr', default)` instead of `obj.attr`

3. **Check if variable exists in context**
   - For include-only files, ensure parent sets needed variables
   - Use `{% if var is defined %}` guards if variable is truly optional

### Error: `'dict object' has no attribute 'X'`

- You're using dot notation on a dict that needs bracket notation
- Use `dict.get('key', default)` or `dict['key']`

### Error: `'DocElement object' has no attribute 'get'`

- You're using dict methods on a DocElement object
- Use direct attribute access: `element.name` not `element.get('name')`

---

## Testing Templates

Run `bengal serve` (not `bengal build`) to test templates with StrictUndefined mode enabled. This catches errors that would silently pass in production builds.

```bash
cd site
bengal s
```

Watch for warnings like:
```
● autodoc_template_render_failed  template=... error='X' is undefined
```

---

## Quick Reference

| Task | Pattern |
|------|---------|
| Import a macro | `{% from 'path/_macros/macro.html' import macro_name %}` |
| Include a partial | `{% include 'path/partial.html' %}` |
| Safe attribute access | `getattr(element, 'children', [])` |
| Filter with default | `element.description \| default('No description')` |
| Check if defined | `{% if var is defined and var %}` |
| Dict key with default | `dict.get('key', 'default')` |

---

## Further Reading

- [Kida Documentation](/docs/theming/templating/kida/) - Bengal's template engine
- Bengal's `bengal/rendering/template_engine/environment.py` - Template environment setup
- Bengal's `bengal/autodoc/orchestration/template_env.py` - Autodoc-specific environment
