# Template System Guide

This guide explains how pyDocExtractor uses Jinja2 templates to control Markdown output formatting.

## Table of Contents

1. [Overview](#overview)
2. [How Templates Work](#how-templates-work)
3. [Built-in Templates](#built-in-templates)
4. [Template Context Variables](#template-context-variables)
5. [Creating Custom Templates](#creating-custom-templates)
6. [Template Filters](#template-filters)
7. [Advanced Template Techniques](#advanced-template-techniques)
8. [Using Templates in Code](#using-templates-in-code)
9. [Template Best Practices](#template-best-practices)

## Overview

pyDocExtractor uses **Jinja2** template engine to convert normalized document blocks into formatted Markdown output. Templates control:

- **Structure**: How blocks are organized and sequenced
- **Formatting**: Markdown syntax applied to content
- **Metadata**: What information appears in frontmatter
- **Presentation**: Tables, lists, headings, and other elements

```
┌─────────────────┐
│  Raw Document   │
│  (PDF/DOCX/etc) │
└────────┬────────┘
         │
         │ [Extractor]
         ↓
┌─────────────────┐
│ Normalized Doc  │
│   (Blocks)      │
└────────┬────────┘
         │
         │ [Template Engine]
         ↓
┌─────────────────┐
│ Markdown Output │
│   (Formatted)   │
└─────────────────┘
```

## How Templates Work

### 1. Template Selection

Templates are selected in three ways:

**A. Explicit Selection (Code)**
```python
result = service.convert_to_markdown(doc, template_name="simple")
```

**B. Explicit Selection (CLI)**
```bash
pydocextractor convert document.pdf --template tabular
```

**C. Automatic Selection (Default)**
- CSV files → `tabular` template
- Excel files → `tabular` template
- PDF/DOCX → `default` template (if no template specified)

### 2. Template Rendering Process

```python
# 1. Extract document content
extraction_result = extractor.extract(data, precision)
normalized_doc = extraction_result.normalized_doc

# 2. Prepare template context
context = TemplateContext.from_normalized_doc(
    ndoc=normalized_doc,
    original_doc=doc,
    quality_score=quality_score,
)

# 3. Render template
markdown_text = template_engine.render(
    template_name="default",
    context={
        "blocks": context.blocks,
        "metadata": context.metadata,
        "quality_score": context.quality_score,
        "has_tables": context.has_tables,
        "has_images": context.has_images,
        "page_count": context.page_count,
    }
)
```

### 3. Template Location

Templates are stored in: `src/pydocextractor/infra/templates/templates/`

```
templates/
├── simple.j2      # Minimal formatting
├── default.j2     # Enhanced formatting with metadata
└── tabular.j2     # Specialized for CSV/Excel
```

## Built-in Templates

### 1. Simple Template (`simple.j2`)

**Purpose**: Minimal formatting, just content.

**Best for**: Quick conversions, plain text extraction, when you don't need metadata.

**Output Example**:
```markdown
This is the first paragraph.

This is the second paragraph.

| Column 1 | Column 2 |
|----------|----------|
| Value A  | Value B  |
```

**Template Code**:
```jinja2
{#- Simple template with minimal formatting -#}
{% for block in blocks -%}
{{ block.content }}

{% endfor -%}
```

**Use case**: Maximum simplicity, no frontmatter, no metadata.

---

### 2. Default Template (`default.j2`)

**Purpose**: Enhanced formatting with YAML frontmatter and metadata.

**Best for**: PDF/DOCX documents, structured output, when you need document metadata.

**Output Example**:
```markdown
---
title: "Company Handbook"
source_file: "handbook.pdf"
created_utc: "2025-10-22T10:30:00"
media_type: "text_document"
mode: "fast"
extractor: "PyMuPDF4LLM"
quality_score: 0.87
---

# Document: Company Handbook

## File Information

- **Filename:** `handbook.pdf`
- **File Type:** PDF
- **Pages:** 42
- **Contains Tables:** Yes
- **Processing Mode:** Fast
- **Extractor Used:** PyMuPDF4LLM

---

## Document Content

First paragraph content here.

- Bullet point 1
- Bullet point 2

| Column | Data |
|--------|------|
| A      | 100  |

---

## Document Metadata

- **Content Hash:** `abc123...`
- **Processing Time:** 2.34s

---

*Generated by PyMuPDF4LLM*
*Quality Score: 0.87*
```

**Features**:
- YAML frontmatter with document metadata
- File information section
- Structured content rendering
- Different handling for text vs spreadsheet
- Long paragraphs converted to bullet points
- Processing metadata footer

---

### 3. Tabular Template (`tabular.j2`)

**Purpose**: Specialized for CSV/Excel files with rich statistical summaries.

**Best for**: Tabular data, spreadsheets, when you need column statistics.

**Output Example**:
```markdown
---
title: "Sales Data Q4"
source_file: "sales.xlsx"
created_utc: "2025-10-22T10:30:00"
media_type: "tabular_data"
mode: "accurate"
extractor: "PandasExcel"
quality_score: 0.95
---

# Handling Tabular Data

## File Information

- **Filename:** `sales.xlsx`
- **Title (if available):** "Sales Data Q4"
- **File Type:** XLSX
- **Sheets:** 2 (Summary, Details)

---

## Sheet Summary

### 🗂 Summary

| Column Name | Type        | Min      | Max      | Mean     | Mode (if categorical) |
|-------------|-------------|----------|----------|----------|-----------------------|
| `Region`    | Categorical | –        | –        | –        | North                 |
| `Sales`     | Numerical   | 1,000.00 | 50,000.00| 12,345.67| –                     |
| `Quantity`  | Numerical   | 10.00    | 500.00   | 125.50   | –                     |

**⚠️ Duplicate Rows:** 3

---

### 🗂 Details

...

---

## Summary

✅ **Categorical Columns:** `Region`, `Product`

✅ **Numerical Columns:** `Sales`, `Quantity`, `Profit`

✅ **Duplicate Rows:** 3

✅ **Total Sheets Processed:** 2

---

## Document Metadata

- **Content Hash:** `def456...`
- **Processing Time:** 1.234s
- **Total Rows:** 1,500
- **Total Columns:** 8

---

*Generated by PandasExcel*
*Quality Score: 0.95*
```

**Features**:
- Sheet-by-sheet breakdown
- Column type classification (numerical vs categorical)
- Statistical summaries (min/max/mean/mode)
- Duplicate row detection
- Multi-sheet support
- Tabular metadata

## Template Context Variables

Every template receives a context dictionary with the following variables:

### Core Variables

| Variable | Type | Description | Example |
|----------|------|-------------|---------|
| `blocks` | `tuple[dict]` | List of content blocks | `[{"type": "text", "content": "Hello", ...}]` |
| `metadata` | `dict` | Document metadata | `{"filename": "doc.pdf", "extractor": "PyMuPDF"}` |
| `quality_score` | `float` | Quality score (0.0-1.0) | `0.87` |
| `has_tables` | `bool` | Whether document has tables | `True` |
| `has_images` | `bool` | Whether document has images | `False` |
| `page_count` | `int \| None` | Number of pages (if applicable) | `42` |

**Note**: When LLM image description is enabled (see [main README](../README.md#llm-image-description)), image blocks automatically include AI-generated descriptions in their `content` field.

### Block Structure

Each block in `blocks` is a dictionary with:

```python
{
    "type": "text",           # BlockType: text, table, header, image, code, list
    "content": "...",         # The actual content
    "page": 1,                # Page number (optional)
    "confidence": 0.95,       # Confidence score (0.0-1.0)
    # ... additional metadata from extractor
}
```

**Image Blocks with LLM Descriptions** (when LLM is enabled):

When the LLM image description feature is enabled, image blocks contain AI-generated descriptions:

```python
{
    "type": "image",
    "content": "<!-- image -->\n\n**Image Description**: The image shows a diagram...",
    "page": 5,
    "metadata": {
        "description_generated": True,
        "context_lines": 100
    }
}
```

The description is appended to the image marker in the `content` field, making it immediately available in templates without special handling.

### Metadata Keys

Common `metadata` keys (extractor-dependent):

| Key | Type | Description | Available In |
|-----|------|-------------|--------------|
| `filename` | `str` | Original filename | All |
| `extractor` | `str` | Extractor name | All |
| `source_mime` | `str` | MIME type | All |
| `content_hash` | `str` | SHA256 hash | CSV, Excel |
| `created_utc` | `str` | Creation timestamp | All (optional) |
| `processing_time` | `float` | Processing seconds | All (optional) |
| `rows` | `int` | Row count | CSV, Excel |
| `columns` | `int` | Column count | CSV, Excel |
| `sheet_names` | `list[str]` | Sheet names | Excel, CSV |
| `numerical_columns` | `list \| dict` | Numerical columns | CSV, Excel |
| `categorical_columns` | `list \| dict` | Categorical columns | CSV, Excel |
| `numerical_stats` | `dict` | Column statistics | CSV, Excel |
| `categorical_stats` | `dict` | Category stats | CSV, Excel |
| `duplicate_count` | `int` | Duplicate rows | CSV |
| `duplicate_counts` | `dict` | Per-sheet duplicates | Excel |
| `title` | `str` | Document title | All (optional) |
| `author` | `str` | Document author | PDF, DOCX (optional) |

### Special Variables in Tabular Template

For CSV/Excel, metadata includes:

```python
{
    # Single sheet (CSV)
    "numerical_columns": ["Sales", "Quantity"],
    "categorical_columns": ["Region", "Product"],
    "numerical_stats": {
        "Sales": {"min": 1000, "max": 50000, "mean": 12345},
        "Quantity": {"min": 10, "max": 500, "mean": 125}
    },
    "categorical_stats": {
        "Region": {"mode": "North"},
        "Product": {"mode": "Widget A"}
    },
    "duplicate_count": 3,

    # Multi-sheet (Excel)
    "numerical_columns": {
        "Sheet1": ["Sales"],
        "Sheet2": ["Revenue"]
    },
    "duplicate_counts": {
        "Sheet1": 2,
        "Sheet2": 1
    }
}
```

## Creating Custom Templates

### Step 1: Create Template File

Create a `.j2` file in `src/pydocextractor/infra/templates/templates/`:

**Example**: `custom_report.j2`

```jinja2
{#- Custom Report Template -#}
{#- This template creates a detailed report format -#}

# 📄 Document Report

**File**: {{ metadata.get('filename', 'Untitled') }}
**Processed**: {{ metadata.get('created_utc', 'Unknown') }}
**Quality**: {{ "%.1f" | format(quality_score * 100) }}%

---

## 📊 Content Overview

{% if has_tables -%}
✅ This document contains tables.
{% endif -%}
{% if has_images -%}
✅ This document contains images.
{% endif -%}
{% if page_count -%}
📖 Total pages: {{ page_count }}
{% endif %}

---

## 📝 Extracted Content

{% for block in blocks %}
{% if block.type == 'header' %}
### {{ block.content }}

{% elif block.type == 'text' %}
{{ block.content }}

{% elif block.type == 'table' %}
#### Table{% if block.page %} (Page {{ block.page }}){% endif %}

{{ block.content }}

{% elif block.type == 'image' %}
{{ block.content }}

{% elif block.type == 'code' %}
```
{{ block.content }}
```

{% endif %}
{% endfor %}

---

## ⚙️ Processing Details

- **Extractor**: {{ metadata.get('extractor', 'Unknown') }}
- **Quality Score**: {{ quality_score }}
- **Processing Time**: {{ metadata.get('processing_time', 'N/A') }}s

---

*Generated by pyDocExtractor*
```

### Step 2: Use Your Template

**In Python Code**:
```python
from pydocextractor import create_converter_service

service = create_converter_service()
result = service.convert_to_markdown(doc, template_name="custom_report")
print(result.text)
```

**In CLI**:
```bash
pydocextractor convert document.pdf --template custom_report
```

### Step 3: Custom Template Directory (Optional)

If you want to use templates from a custom directory:

```python
from pathlib import Path
from pydocextractor import create_converter_service

custom_templates = Path("my_templates/")
service = create_converter_service(template_dir=custom_templates)

result = service.convert_to_markdown(doc, template_name="my_custom")
```

## Template Filters

pyDocExtractor provides custom Jinja2 filters for text processing.

### Built-in Filters

Located in `src/pydocextractor/infra/templates/engines.py`:

```python
self._env.filters["word_count"] = lambda s: len(str(s).split())
self._env.filters["char_count"] = lambda s: len(str(s))
```

**Usage in Templates**:

```jinja2
{#- Word count example -#}
Document contains {{ blocks | map(attribute='content') | join(' ') | word_count }} words.

{#- Character count example -#}
This block has {{ block.content | char_count }} characters.
```

### Jinja2 Built-in Filters

You can use all standard Jinja2 filters:

```jinja2
{#- String manipulation -#}
{{ filename | upper }}                    {#- DOCUMENT.PDF -#}
{{ filename | lower }}                    {#- document.pdf -#}
{{ title | replace(" ", "_") }}           {#- My_Document -#}
{{ content | truncate(100) }}             {#- First 100 chars... -#}

{#- Number formatting -#}
{{ quality_score | round(2) }}            {#- 0.87 -#}
{{ "%.2f" | format(quality_score) }}      {#- 0.87 -#}
{{ "{:,.0f}".format(row_count) }}         {#- 1,500 -#}

{#- List operations -#}
{{ columns | join(', ') }}                {#- col1, col2, col3 -#}
{{ columns | length }}                    {#- 3 -#}
{{ values | sum }}                        {#- Sum of numbers -#}

{#- Conditional -#}
{{ value | default('N/A') }}              {#- N/A if value is None -#}
```

### Adding Custom Filters

To add your own filters, modify `engines.py`:

```python
class Jinja2TemplateEngine:
    def __init__(self, template_dir: str | Path | None = None) -> None:
        # ... existing code ...

        # Add custom filters
        self._env.filters["word_count"] = lambda s: len(str(s).split())
        self._env.filters["char_count"] = lambda s: len(str(s))

        # Your custom filters
        self._env.filters["markdown_escape"] = self._markdown_escape
        self._env.filters["abbreviate"] = lambda s, n: s[:n] + "..." if len(s) > n else s

    @staticmethod
    def _markdown_escape(text: str) -> str:
        """Escape markdown special characters."""
        escape_chars = ['\\', '`', '*', '_', '{', '}', '[', ']', '(', ')', '#', '+', '-', '.', '!']
        for char in escape_chars:
            text = text.replace(char, f'\\{char}')
        return text
```

**Usage**:
```jinja2
{{ filename | markdown_escape }}
{{ long_text | abbreviate(50) }}
```

## Advanced Template Techniques

### 1. Conditional Rendering

```jinja2
{% if has_tables %}
## Tables Found

This document contains {{ blocks | selectattr('type', 'equalto', 'table') | list | length }} tables.
{% endif %}

{% if quality_score > 0.8 %}
✅ High quality extraction
{% elif quality_score > 0.5 %}
⚠️ Medium quality extraction
{% else %}
❌ Low quality extraction
{% endif %}
```

### 2. Loop with Counters

```jinja2
{% for block in blocks %}
### Block {{ loop.index }} of {{ loop.length }}

{{ block.content }}

{% if loop.first %}
*This is the first block*
{% endif %}

{% if loop.last %}
*This is the last block*
{% endif %}
{% endfor %}
```

### 3. Filtering Blocks

```jinja2
{#- Show only text blocks -#}
{% for block in blocks if block.type == 'text' %}
{{ block.content }}

{% endfor %}

{#- Show only tables -#}
{% for block in blocks | selectattr('type', 'equalto', 'table') %}
### Table {{ loop.index }}

{{ block.content }}
{% endfor %}
```

### 4. Namespaces for State

```jinja2
{% set ns = namespace(table_count=0, image_count=0) %}

{% for block in blocks %}
{% if block.type == 'table' %}
{% set ns.table_count = ns.table_count + 1 %}
{% elif block.type == 'image' %}
{% set ns.image_count = ns.image_count + 1 %}
{% endif %}
{% endfor %}

**Summary**: Found {{ ns.table_count }} tables and {{ ns.image_count }} images.
```

### 5. Macros for Reusability

```jinja2
{#- Define macro -#}
{% macro render_block(block) %}
{% if block.type == 'text' %}
{{ block.content }}
{% elif block.type == 'table' %}
#### Table

{{ block.content }}
{% endif %}
{% endmacro %}

{#- Use macro -#}
{% for block in blocks %}
{{ render_block(block) }}
{% endfor %}
```

### 6. Template Inheritance (Not Used Currently)

Jinja2 supports template inheritance, though pyDocExtractor doesn't use it by default:

**Base template** (`base.j2`):
```jinja2
# {{ title }}

{% block header %}
Default header
{% endblock %}

{% block content %}
{% endblock %}

{% block footer %}
*Generated by pyDocExtractor*
{% endblock %}
```

**Child template** (`report.j2`):
```jinja2
{% extends "base.j2" %}

{% block header %}
## Custom Report Header
{% endblock %}

{% block content %}
{% for block in blocks %}
{{ block.content }}
{% endfor %}
{% endblock %}
```

## Using Templates in Code

### Basic Usage

```python
from pathlib import Path
from pydocextractor import Document, PrecisionLevel, create_converter_service

# Create service
service = create_converter_service()

# Load document
doc = Document(
    bytes=Path("document.pdf").read_bytes(),
    mime="application/pdf",
    size_bytes=Path("document.pdf").stat().st_size,
    precision=PrecisionLevel.BALANCED,
    filename="document.pdf",
)

# Convert with default template
result = service.convert_to_markdown(doc)

# Convert with specific template
result = service.convert_to_markdown(doc, template_name="simple")

# Convert with tabular template
result = service.convert_to_markdown(doc, template_name="tabular")
```

### List Available Templates

```python
from pydocextractor import create_converter_service

service = create_converter_service()
templates = service.list_available_templates()

print("Available templates:")
for template in templates:
    print(f"  - {template}")
```

### Custom Template Directory

```python
from pathlib import Path
from pydocextractor import create_converter_service

# Use custom template directory
custom_dir = Path("my_templates/")
service = create_converter_service(template_dir=custom_dir)

result = service.convert_to_markdown(doc, template_name="my_custom")
```

### Programmatic Template Rendering

For advanced use cases, you can render templates directly:

```python
from pydocextractor.infra.templates.engines import Jinja2TemplateEngine
from pydocextractor.domain.models import TemplateContext

# Create template engine
engine = Jinja2TemplateEngine()

# Prepare context
context = TemplateContext.from_normalized_doc(
    ndoc=normalized_doc,
    quality_score=0.85,
)

# Render template
markdown = engine.render(
    template_name="default",
    context={
        "blocks": context.blocks,
        "metadata": context.metadata,
        "quality_score": context.quality_score,
        "has_tables": context.has_tables,
        "has_images": context.has_images,
        "page_count": context.page_count,
    }
)
```

### Render from String Template

```python
from pydocextractor.infra.templates.engines import Jinja2TemplateEngine

engine = Jinja2TemplateEngine()

template_string = """
# {{ metadata.filename }}

{% for block in blocks %}
{{ block.content }}
{% endfor %}
"""

markdown = engine.render_string(template_string, context)
```

## Template Best Practices

### 1. Use Comments

```jinja2
{#- This is a Jinja2 comment, won't appear in output -#}
{#- Explain complex logic -#}
{% set is_spreadsheet = 'sheet' in source_mime.lower() %}
```

### 2. Handle Missing Values

```jinja2
{#- Safe access with default -#}
{{ metadata.get('filename', 'Untitled') }}

{#- Conditional rendering -#}
{% if metadata.get('author') %}
**Author**: {{ metadata.author }}
{% endif %}
```

### 3. Control Whitespace

```jinja2
{#- Use hyphens to strip whitespace -#}
{% for block in blocks -%}
  {{ block.content }}
{%- endfor %}

{#- This prevents extra blank lines -#}
```

The template engine is configured with:
```python
Environment(
    trim_blocks=True,      # Remove first newline after block
    lstrip_blocks=True,    # Strip leading spaces from line start
)
```

### 4. Validate Block Types

```jinja2
{#- Use specific block type checks -#}
{% if block.type == 'text' %}
  {#- Text rendering -#}
{% elif block.type == 'table' %}
  {#- Table rendering -#}
{% elif block.type == 'image' %}
  {#- Image rendering -#}
{% endif %}
```

### 5. Keep Templates Simple

- Avoid complex logic in templates
- Move business logic to extractors or service layer
- Templates should focus on presentation

### 6. Test Templates

Create test cases for your templates:

```python
# tests/adapters/test_templates.py
from pydocextractor.infra.templates.engines import Jinja2TemplateEngine
from pydocextractor.domain.models import Block, BlockType, NormalizedDoc

def test_custom_template():
    engine = Jinja2TemplateEngine()

    ndoc = NormalizedDoc(
        blocks=(Block(type=BlockType.TEXT, content="Test content"),),
        source_mime="application/pdf",
    )

    context = {
        "blocks": [{"type": "text", "content": "Test"}],
        "metadata": {"filename": "test.pdf"},
        "quality_score": 0.9,
    }

    result = engine.render("custom_report", context)

    assert "Test content" in result
    assert "test.pdf" in result
```

### 7. Document Your Templates

Add header comments explaining:
- Purpose of the template
- Best use cases
- Required context variables
- Expected output format

```jinja2
{#-
Template: custom_report.j2
Purpose: Detailed document report with statistics
Best for: PDF/DOCX documents requiring metadata
Required: blocks, metadata, quality_score
Output: Markdown with YAML frontmatter
-#}
```

## Example: Creating a Minimal Template

Let's create a minimal template for quick text extraction:

**File**: `src/pydocextractor/infra/templates/templates/minimal.j2`

```jinja2
{#- Minimal template - just the text content -#}
{% for block in blocks if block.type == 'text' -%}
{{ block.content }}

{% endfor -%}
```

**Usage**:
```python
result = service.convert_to_markdown(doc, template_name="minimal")
```

**Output**:
```
This is paragraph one.

This is paragraph two.

This is paragraph three.
```

## Example: Creating a Detailed Template

**File**: `src/pydocextractor/infra/templates/templates/detailed.j2`

```jinja2
{#- Detailed template with full metadata -#}
---
title: {{ metadata.get('filename', 'Document') }}
extractor: {{ metadata.get('extractor', 'Unknown') }}
quality: {{ quality_score }}
pages: {{ page_count or 'N/A' }}
tables: {{ has_tables }}
images: {{ has_images }}
---

# 📄 {{ metadata.get('filename', 'Document') }}

## Document Statistics

- **Quality Score**: {{ "%.1f" | format(quality_score * 100) }}%
- **Total Blocks**: {{ blocks | length }}
- **Text Blocks**: {{ blocks | selectattr('type', 'equalto', 'text') | list | length }}
- **Table Blocks**: {{ blocks | selectattr('type', 'equalto', 'table') | list | length }}
- **Image Blocks**: {{ blocks | selectattr('type', 'equalto', 'image') | list | length }}

---

{% for block in blocks %}
{% if block.type == 'text' %}
{{ block.content }}

{% elif block.type == 'table' %}
### Table {% if block.page %}(Page {{ block.page }}){% endif %}

{{ block.content }}

{% elif block.type == 'image' %}
{{ block.content }}

{% endif %}
{% endfor %}

---

**Extracted by**: {{ metadata.get('extractor', 'pyDocExtractor') }}
**Quality**: {{ quality_score }}
```

## Troubleshooting

### Template Not Found

```python
# Error: Template 'mytemplate' not found

# Solution 1: Add .j2 extension
result = service.convert_to_markdown(doc, template_name="mytemplate.j2")

# Solution 2: Check template directory
from pydocextractor import create_converter_service
service = create_converter_service()
print(service.list_available_templates())

# Solution 3: Verify file exists
# Should be at: src/pydocextractor/infra/templates/templates/mytemplate.j2
```

### Variable Not Defined

```jinja2
{#- Wrong: KeyError if 'author' missing -#}
{{ metadata['author'] }}

{#- Correct: Safe access with default -#}
{{ metadata.get('author', 'Unknown') }}

{#- Correct: Conditional check -#}
{% if metadata.get('author') %}
Author: {{ metadata.author }}
{% endif %}
```

### Whitespace Issues

```jinja2
{#- Problem: Extra blank lines -#}
{% for block in blocks %}
{{ block.content }}
{% endfor %}

{#- Solution: Use hyphens to strip whitespace -#}
{% for block in blocks -%}
{{ block.content }}
{% endfor -%}
```

## Summary

- **Templates control Markdown output formatting**
- **Three built-in templates**: `simple`, `default`, `tabular`
- **Jinja2 syntax** with full template feature support
- **Context variables**: `blocks`, `metadata`, `quality_score`, etc.
- **Custom templates**: Place `.j2` files in templates directory
- **Filters**: Custom filters for text processing
- **Best practices**: Handle missing values, control whitespace, keep logic simple

## Next Steps

1. **Explore built-in templates**: Read `simple.j2`, `default.j2`, `tabular.j2`
2. **Create custom template**: Follow examples in this guide
3. **Test your template**: Use with various document types
4. **Share your template**: Contribute to the project!

## Related Documentation

- **[CONTRIBUTING.md](CONTRIBUTING.md#how-to-add-a-custom-template)** - How to contribute templates
- **[CONTRIBUTING_GUIDE.md](CONTRIBUTING_GUIDE.md#task-2-add-a-custom-template)** - Detailed template creation guide
- **[Jinja2 Documentation](https://jinja.palletsprojects.com/)** - Official Jinja2 docs

---

**Need help?** Open an issue at [GitHub Issues](https://github.com/AminiTech/pyDocExtractor/issues)
