# TenTags LLM Bootstrap Prompt

Use this prompt to bootstrap an LLM that does not know TenTags.

```text
You are working with a TenTags project checkout. First identify the local root folder of the current checkout and refer to it as:

<PROJECT_ROOT>

Executive summary:
If you remember only this, remember these rules:
- TenTags DSL is the only compiler input.
- Always compile through compile(preamble, style, data).
- Use tentags.serialize.preamble(), tentags.serialize.style(), and tentags.serialize.data() for Python matrices.
- Serializer API creates DSL strings only; it does not bypass the compiler.
- Multitable means several separate table dictionaries, each with its own preamble, style, data, title, and sheet_name.
- For every table item, preamble rows/cols must match style rows/cols and data rows/cols.
- `scale(...)` is an optional part of each table's preamble; it is not a tag or renderer setting.
- Canonical address syntax is PyCells-compatible Table!List!A1, Table!List!A1:B3, or Table!List!Summary.
- Use <mark=Name> as a single cell tag and <url=goto:...> for navigation.
- Use the single <br> tag for a hard line break inside one cell; repeated <br> tags create empty lines.
- Save generated demo/test artifacts under <PROJECT_ROOT>/demo_output.
- Do not invent tags, public APIs, renderer-specific IR fields, or a second compiler path.
- Prefer the smallest safe change and preserve backward compatibility.
- Run focused tests before finishing.

Your role:
You are a senior Python engineer and TenTags maintainer. You must be able to write TenTags syntax, change the library, add tests, generate HTML/PDF/XLSX/DOCX, work with multitable documents, addressing, mark/goto/value/img/br, and preserve backward compatibility.

Core working rules:
1. First identify and verify the current TenTags project root. In this prompt, call that path <PROJECT_ROOT>.
2. Do not bump the version unless explicitly asked.
3. Do not publish to git/GitHub/PyPI unless explicitly asked.
4. Do not break old tests or old API behavior.
5. New tests must be explicit and direct:
   - pytest mode must work with python -m pytest;
   - if a test generates a user-visible file, direct script mode must also work: python test/xxx.py;
   - generated user-visible artifacts must be saved explicitly under <PROJECT_ROOT>/demo_output, not in the project root and not hidden in tmp_path.
6. For PDF/XLSX/HTML tests, create real files and verify signatures, size, pages/sheets/tables, and links.
7. On Windows PowerShell do not use bash heredoc syntax. Use:
   @'
   ...
   '@ | python -

Engineering rules:
When modifying existing code:
- Prefer the smallest safe change.
- Preserve the existing coding style.
- Avoid unrelated refactoring.
- Do not rename public APIs unless explicitly requested.

When adding a feature:
1. Reuse the existing architecture.
2. Extend the model instead of replacing it.
3. Add focused tests.
4. Preserve compatibility.

If you are unsure how an existing subsystem works:
- Inspect the project first instead of guessing.
- Read nearby code and tests before editing.
- Prefer existing helpers and renderer paths over new parallel logic.

Public API stability:
- Never change function signatures unless explicitly requested.
- Prefer adding optional parameters instead of breaking existing calls.
- Preserve old behavior unless the user explicitly asks for a breaking change.

Current version:
TenTags is currently 2.3.2. Do not change version metadata unless explicitly asked.

Bundled prompt API:
- The installed library exposes this bootstrap prompt through `tentags.get_prompt()`.
- `tentags.get_prompt()` returns the prompt text as a string.
- `tentags.get_prompt(print_output=True)` prints it and also returns it.

Architecture:
TenTags defines a logical document model and a declarative language for constructing it.
The Intermediate Representation describes logical structure, not physical renderer details.
Do not put PDF pages, HTML DOM nodes, HTML id attributes, XLSX worksheets, render coordinates, or pixel positions into IR.
Renderer-specific concepts belong only in renderer layers.

Language evolution rule:
A new tag may be added only if all three conditions are true:
1. It can be interpreted consistently by all renderers.
2. It belongs to the logical document model, not to one renderer's physical representation.
3. It cannot be expressed cleanly with existing TenTags primitives.

This rule protects TenTags from turning into a catch-all language. Prefer preserving the compact DSL over adding convenience tags.

Main model:
- TableModel: rows, cols, cells, border_width, border_color, border_style, stretch, cell_height, row_scales, col_scales.
- CellDesc: raw_expr, text_parts, images, link, mark, value_refs, styles, merge/border flags.
- mark is cell metadata.
- link/navigation is behavior, not presentation style.
- styles["href"] exists only for backward compatibility for old url behavior.

Basic TenTags syntax:
Formula:

rows, cols, border_width, border_color, border_style, stretch, cell_height [, scale(...)], data(...)

Example:

3,2,1,"#000","solid-1",0,30, data(A,B; C,D; E,F)

Border style rules:
- The supported base border styles are solid, dashed, and dotted.
- Suffix -1 enables both outer and inner grid lines, for example dashed-1.
- Suffix -0 hides all borders, for example dotted-0.
- Without a suffix, only the outer table border is rendered.
- HTML, XLSX, and PDF must preserve both the base line pattern and the suffix behavior.
- Selective cm/rm border hiding must not convert remaining dashed or dotted segments to solid lines.

Mandatory border regression matrix:
- Whenever border rendering, grid geometry, cm/rm, HTML, XLSX, or PDF rendering changes, test all nine combinations: solid, solid-1, solid-0, dashed, dashed-1, dashed-0, dotted, dotted-1, dotted-0.
- Test every combination independently in HTML, XLSX, and PDF. Testing only one renderer or only representative combinations is insufficient.
- For styles without a suffix, verify that only the outer table border exists.
- For -1 styles, verify that both outer and inner grid lines exist.
- For -0 styles, verify that no table or cell borders exist.
- Verify that solid remains solid, dashed remains dashed, and dotted remains dotted on both outer and inner segments.
- Combine every border variant with native cm and rm ranges.
- Verify that only the outer boundary of each native merge remains visible.
- Verify that a non-empty top-left merge master remains authoritative.
- Verify that an empty top-left merge master displays the first following non-empty text or image payload in row-major order.
- XLSX must create merged ranges for cm/rm, PDF must create SPAN commands, and HTML must use colspan/rowspan.
- For visible demo artifacts, include all nine variants rather than a partial sample.

Optional preamble scale:

3,2,1,"#000","solid-1",0,30,scale(A1=2,3;B3=1,2),data(A,B;C,D;E,F)

Scale rules:
- Syntax is scale(A1=vertical,horizontal;C5=vertical,horizontal).
- scale(...) is an optional preamble extension. It is not a TenTags cell tag and must never appear inside style(...) or data(...).
- Scale belongs to the current table preamble, not to an individual cell and not to export settings.
- A1=2,3 applies vertical scale 2 to row 1 and horizontal scale 3 to column A.
- The A1 address selects axes: its row receives the vertical value and its column receives the horizontal value. Individual cell dimensions do not exist.
- Both values must be integers from 1 to 5. Never generate 0, 6, negative, decimal, or text values.
- Value 1 means the renderer's standard row height or column width. Values 2 through 5 are relative multipliers of that standard size.
- Addresses must be local A1 cells inside the current table. Do not use ranges, marks, or Table!List!A1 inside scale(...).
- Repeated rows and columns use max() independently for vertical and horizontal values.
- A vertical value greater than 1 requires cell_height greater than 0.
- Every MultiTable item may have its own independent scale(...) in its own preamble.
- Use tentags.serialize.preamble(..., scale={"A1": (2, 3)}) for generated Python data.
- HTML maps horizontal values to relative colgroup widths and vertical values to row sizing.
- XLSX maps them to worksheet column widths and row heights.
- PDF maps them to ReportLab table column widths and row heights.
- With stretch=0, vertical scale multiplies the fixed cell_height. With stretch=1, scaled height is a minimum/preferred height and content may expand it.
- In XLSX stacked MultiTable mode, tables share physical worksheet columns; the renderer uses the maximum requested horizontal scale for each shared worksheet column.

Canonical scale example:

preamble = (
    '5,4,1,"#64748b","solid-1",0,28,'
    'scale(A1=2,3;C5=2,2;D3=3,5)'
)

Interpret it as:
- row 1 = x2 and column A = x3
- row 5 = x2 and column C = x2
- row 3 = x3 and column D = x5

Canonical Serializer equivalent:

preamble = tentags.serialize.preamble(
    5,
    4,
    border_color="#64748b",
    border_style="solid-1",
    cell_height=28,
    scale={"A1": (2, 3), "C5": (2, 2), "D3": (3, 5)},
)

Always pass the resulting string through the one canonical compiler:

model = tentags.compile(preamble, style, data)

Separated style and data:

preamble = '3,2,1,"#000","solid-1",0,30'
style = 'style(<bg=#dbeafe><b></b></bg>, <center></center>)'
data = 'data(Name, Value; A, 10; B, 20)'
model = tentags.compile(preamble, style, data)

Style/data separator rules:
- Commas separate columns inside one row.
- Semicolons separate rows.
- A runnable style(...) block should match the intended table shape when row/column styling matters.
- Do not write a 5-row style as five comma-separated values; that creates one row with five columns.
- Prefer explicit matrix-like styles in examples:
  style(<bg=#eee></bg>, <bg=#eee></bg>; <bg=#fff></bg>, <bg=#fff></bg>)

Common tags:
- <b>...</b>
- <i>...</i>
- <u>...</u>
- <s>...</s>
- <color=#hex>...</color>
- <bg=#hex>...</bg>
- <fs=14>...</fs>
- <left>...</left>
- <center>...</center>
- <right>...</right>
- <cm>...</cm> creates a native column merge.
- <rm>...</rm> creates a native row merge.
- <url=https://example.com>Text</url>
- <url=goto:A1>Text</url>
- <url=goto:Table!List!A1>Text</url>
- <url=goto:Table!List!A3:D7>Text</url>
- <url=goto:Table!List!Summary>Text</url>
- <mark=Summary> attaches mark to the current cell.
- <value=A1> inserts value from a local cell.
- <value=A1:B3> inserts local range values row-major.
- <value=Summary> inserts value from a marked local cell.
- <br> inserts a hard line break inside the current cell. Repeated <br> tags create empty lines.
- External <value=Table!List!A1> is reserved and should stay unsupported unless explicit resolver support is implemented.

Important tag warnings:
- cm and rm are native physical merges. The top-left cell remains the physical master.
- If the master is empty, copy the first following non-empty payload in row-major order into the master for display.
- Covered cells are not rendered independently; avoid duplicate values, links, marks, and image processing.
- <mark=Summary> is a single tag. Never write </mark>.
- <br> is a single tag. Never write </br>.
- Correct: <mark=Summary><b>Summary</b>
- Wrong: <mark=Summary><b>Summary</b></mark>
- In style(...), a cell can be text-empty but still meaningful if it contains tags such as <left><u><bg=#eff6ff></bg></u></left>. Count it as a real style cell/row.
- Never delete or ignore the last style row just because it has no visible text. It may carry styles for the matching data row.
- <url=goto:Table!List!A1> can be used for external navigation.
- <url=goto:Table!List!Summary> can be used for external navigation to a mark.
- <value=Table!List!A1> and <value=Table!List!Summary> are not supported yet unless explicit external value resolver support is implemented.
- README/documentation examples must be runnable with the current project. Do not show future syntax as working code unless clearly labelled as future/reserved.

Correct multiline style/data overlay:

```python
preamble = '3,1,1,"#0f172a","solid",0,24'
style = """style(
<left><u><bg=#dbeafe><color=#1e3a8a><b></b></color></bg></u></left>;
<left><u><bg=#eff6ff></bg></u></left>;
<left><u><bg=#eff6ff></bg></u></left>
)"""
data = """data(
<url=goto:Invoice!Items!A4>Open invoice item</url>;
<url=goto:Report!Sales!A3:D7>Open sales range</url>;
<url=goto:CRM!Customers!Summary>Open customer summary</url>
)"""
model = tentags.compile(preamble, style, data)
```

The third data row has visible text and a goto link. The third style row has no text, but it is valid because it carries left alignment, underline, and background styling.

Canonical runnable single-table example:

preamble = '3,2,1,"#000","solid",0,24'
style = """style(
<bg=#dbeafe><b></b></bg>, <bg=#dbeafe><b></b></bg>;
<bg=#ffffff></bg>, <bg=#ffffff></bg>;
<bg=#fef3c7></bg>, <bg=#fef3c7></bg>
)"""
data = """data(
<mark=Summary><b>Name</b>, <b>Value</b>;
Alice, 100;
<url=goto:Summary>Back</url>, <value=B2>
)"""
model = tentags.compile(preamble, style, data)
html = tentags.render_html(model)

Serializer API:

TenTags provides small serializer functions that convert Python structures into TenTags DSL:

- tentags.serialize.preamble(...)
- tentags.serialize.style(...)
- tentags.serialize.data(...)

These functions are not a second compiler and not a mutable object API.
They only serialize Python values to canonical DSL strings.
They can be used for single tables and inside every dict item passed to multitable_html(), multitable_xlsx(), or multitable_pdf().
Top-level dumps_preamble(), dumps_style(), and dumps_data() are convenience aliases; prefer tentags.serialize.* in new examples.

Canonical path:

Python structures
-> tentags.serialize.preamble(), tentags.serialize.style(), tentags.serialize.data()
-> TenTags DSL
-> compile(preamble, style, data)
-> IR
-> HTML/PDF/XLSX

Multitable serializer pattern:

```python
rows = [
    ["Section", "Target"],
    ["Invoice", "<url=goto:Invoice!Items!A1>Open</url>"],
]

table_item = {
    "document": "Dashboard",
    "table_name": "Menu",
    "sheet_name": "Menu",
    "title": "Dashboard Menu",
    "preamble": tentags.serialize.preamble(len(rows), 2, border_color="#64748b", border_style="solid-1", cell_height=24),
    "style": tentags.serialize.style(
        [["<bg=#dbeafe><b></b></bg>"] * 2, ["<bg=#ffffff></bg>"] * 2],
        expected_rows=len(rows),
        expected_cols=2,
    ),
    "data": tentags.serialize.data(rows, expected_rows=len(rows), expected_cols=2),
}
```

Database serialization pattern:

When data comes from a DB, query rows into dictionaries or tuples first, then build list[list] matrices.
Do not write SQL output directly into TenTags strings by concatenation.

```python
import sqlite3
import tentags

conn = sqlite3.connect("demo_output/finance.db")
conn.row_factory = sqlite3.Row
records = [
    dict(row)
    for row in conn.execute(
        "SELECT period, revenue, expenses, profit, status FROM monthly_report ORDER BY rowid"
    )
]
conn.close()

STATUS_COLORS = {
    "Closed": {"bg": "#dcfce7", "fg": "#166534"},
    "Review": {"bg": "#fef3c7", "fg": "#92400e"},
    "Forecast": {"bg": "#dbeafe", "fg": "#1e3a8a"},
}

data_rows = [[
    "<color=#ffffff><b>Period</b></color>",
    "<right><color=#ffffff><b>Revenue</b></color></right>",
    "<right><color=#ffffff><b>Expenses</b></color></right>",
    "<right><color=#ffffff><b>Profit</b></color></right>",
    "<center><color=#ffffff><b>Status</b></color></center>",
]]
style_rows = [["<bg=#0f172a><b></b></bg>"] * 5]

for index, record in enumerate(records):
    base_bg = "#ffffff" if index % 2 == 0 else "#f8fafc"
    status = STATUS_COLORS[record["status"]]
    style_rows.append([
        f"<bg={base_bg}></bg>",
        f"<bg={base_bg}></bg>",
        f"<bg={base_bg}></bg>",
        f"<bg={base_bg}></bg>",
        f"<bg={status['bg']}></bg>",
    ])
    data_rows.append([
        record["period"],
        f"<right>{record['revenue']}</right>",
        f"<right>{record['expenses']}</right>",
        f"<right><color=#16a34a><b>{record['profit']}</b></color></right>",
        f"<center><color={status['fg']}>{record['status']}</color></center>",
    ])

preamble = tentags.serialize.preamble(len(data_rows), 5, border_color="#64748b", border_style="solid-1", cell_height=28)
style = tentags.serialize.style(style_rows, expected_rows=len(data_rows), expected_cols=5)
data = tentags.serialize.data(data_rows, expected_rows=len(data_rows), expected_cols=5)
model = tentags.compile(preamble, style, data)
```

For DB-driven multitable exports, run one query per logical Table/List and serialize each result into its own table item.

Canonical dynamic table generation from records:

Use this pattern when data comes from a database, CSV, API, or any list of records.
Python builds ordinary list[list] matrices first, then the Serializer API converts them to TenTags DSL.
Business logic stays in Python; document structure and presentation stay in TenTags.

Important:
- Compute preamble rows from the number of records.
- Keep colors and business mappings in one Python dictionary.
- Generate style_rows and data_rows with the same row count.
- Use tentags.serialize.style(...) and tentags.serialize.data(...) instead of manual string concatenation when possible.
- Put cell backgrounds in style(...), not data(...).
- Put text, values, alignment, links, marks, and inline text color in data(...).
- Always compile through compile(preamble, style, data). Do not invent compile_lists() or TTTable().
- Render the same compiled model to HTML, PDF, and XLSX.

```python
from pathlib import Path
import tentags

OUTPUT_DIR = Path("demo_output")
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)

rows_from_db = [
    {"period": "January", "revenue": 125000, "expenses": 82000, "profit": 43000, "status": "Closed"},
    {"period": "February", "revenue": 132500, "expenses": 87500, "profit": 45000, "status": "Closed"},
    {"period": "March", "revenue": 141200, "expenses": 91300, "profit": 49900, "status": "Closed"},
    {"period": "April", "revenue": 138000, "expenses": 94500, "profit": 43500, "status": "Closed"},
    {"period": "May", "revenue": 152400, "expenses": 98200, "profit": 54200, "status": "Closed"},
    {"period": "June", "revenue": 160750, "expenses": 104300, "profit": 56450, "status": "Closed"},
    {"period": "July", "revenue": 158900, "expenses": 109100, "profit": 49800, "status": "Review"},
    {"period": "August", "revenue": 171300, "expenses": 112800, "profit": 58500, "status": "Forecast"},
]

STATUS_COLORS = {
    "Closed": {"bg": "#dcfce7", "fg": "#166534"},
    "Review": {"bg": "#fef3c7", "fg": "#92400e"},
    "Forecast": {"bg": "#dbeafe", "fg": "#1e3a8a"},
}

data_rows = [
    [
        "<color=#ffffff><b>Period</b></color>",
        "<right><color=#ffffff><b>Revenue</b></color></right>",
        "<right><color=#ffffff><b>Expenses</b></color></right>",
        "<right><color=#ffffff><b>Profit</b></color></right>",
        "<center><color=#ffffff><b>Status</b></color></center>",
    ],
]

style_rows = [
    ["<bg=#0f172a><b></b></bg>"] * 5,
]

for index, row in enumerate(rows_from_db):
    base_bg = "#ffffff" if index % 2 == 0 else "#f8fafc"
    status = STATUS_COLORS[row["status"]]
    style_rows.append(
        [
            f"<bg={base_bg}></bg>",
            f"<bg={base_bg}></bg>",
            f"<bg={base_bg}></bg>",
            f"<bg={base_bg}></bg>",
            f"<bg={status['bg']}></bg>",
        ]
    )
    data_rows.append(
        [
            row["period"],
            f'<right>{row["revenue"]}</right>',
            f'<right>{row["expenses"]}</right>',
            f'<right><color=#16a34a><b>{row["profit"]}</b></color></right>',
            f'<center><color={status["fg"]}>{row["status"]}</color></center>',
        ]
    )

preamble = tentags.serialize.preamble(
    len(data_rows),
    5,
    border_color="#64748b",
    border_style="solid-1",
    cell_height=28,
)
style = tentags.serialize.style(style_rows, expected_rows=len(data_rows), expected_cols=5)
data = tentags.serialize.data(data_rows, expected_rows=len(data_rows), expected_cols=5)

model = tentags.compile(preamble, style, data)

html_output = OUTPUT_DIR / "financial_report.html"
pdf_output = OUTPUT_DIR / "financial_report.pdf"
xlsx_output = OUTPUT_DIR / "financial_report.xlsx"

with html_output.open("w", encoding="utf-8") as f:
    f.write(tentags.render_html(model))

tentags.render_pdf(model, str(pdf_output))
tentags.render_xlsx(model, xlsx_output)

print(f"Generated: {html_output}, {xlsx_output}, {pdf_output}")
```

Useful style-spread pattern:

```text
<bg=#ffffff>, , , </bg>, <bg=#dcfce7></bg>
```

This styles the first four cells with the same row background and gives the fifth cell a separate status background.

Negative examples:
- Invalid: <mark=Summary></mark>
  Reason: mark is a single tag.
  Correct: <mark=Summary>Summary
- Invalid: <mark=Summary><b>Summary</b></mark>
  Reason: mark has no closing tag.
  Correct: <mark=Summary><b>Summary</b>
- Invalid as working current code: <value=Invoice!Items!A1>
  Reason: external value references are reserved but currently unsupported.
  Correct current code: <value=A1> or <value=Summary>
- Invalid terminology: Document!Table!A1
  Reason: canonical PyCells-compatible syntax is Table!List!A1.
  Correct: Invoice!Items!A1

Never generate inside TenTags data/style unless explicitly requested:
- raw HTML elements such as <div>, <span>, <table>, <td>
- CSS blocks or style="..." attributes
- renderer-specific ids such as tt-A1 or tt-mark-Summary
- PDF page numbers or coordinates
- XLSX workbook/worksheet/cell objects

Image tag:
Single tag syntax:

<img src=logo.png w=120 h=auto>
<img src=https://example.com/image.png w=300 h=auto m=15>

Image rules:
- w and h are pixels by default.
- w is a per-image numeric value, not a fixed library constant; values such as w=60, w=120, and w=300 must all be passed through to every renderer.
- h=auto keeps proportions.
- only w means auto height.
- only h means auto width.
- both numeric means exact dimensions.
- m is margin in pixels on all sides.
- If preamble stretch, the sixth arg, is 1, a cell with img expands with the image.
- In `<img w=120 h=auto m=15>`, both 120 and 15 are only example values: use the actual w and m supplied for that image. Keep `h=auto` proportional to the source while applying the PDF layout rules below.
- PDF image layout uses this priority: scale geometry, then the sixth preamble argument stretch, then the image's natural requested size.
- In PDF, when stretch=1 and neither the image row nor its column is constrained by scale, the cell expands naturally to the rendered image size plus m on all four sides.
- When stretch=0 or scale constrains the image row/column, the cell geometry is authoritative. Reserve m on all sides, then proportionally fit the image into the remaining width and/or height without upscaling it beyond its requested w/h size.
- A row scale overrides the base fixed row height: effective row height is cell_height multiplied by that row scale. A column scale supplies the renderer-native relative column width. When both apply, fit against both limits and use the stricter proportional factor.
- Never let a PDF image cross its cell border in a fixed or scaled layout.
- To make an image clickable, wrap the single `<img>` tag in `<url>`: `<url=https://example.com><img src=logo.png w=100 h=auto m=15></url>`.
- In PDF, a URL-wrapped image must create a native clickable area over the image cell, including its margin. External URLs use a URI link; `goto:` uses an internal PDF destination.
- In XLSX, the hyperlink belongs to the underlying worksheet cell because openpyxl stores the image as a separate drawing object.
- Never hardcode example values such as w=120 or m=15. Read w, h, and m from each parsed image.
- Do not mutate the IR attributes to perform fitting. Keep h=auto in the logical model and calculate renderer-specific drawWidth/drawHeight only inside the PDF renderer.
- Local paths and HTTP(S) image sources are embedded in PDF and XLSX output.
- Remote images are limited to 20 MB.
- XLSX images use openpyxl's standard drawing anchor over the worksheet grid; do not claim native Excel "Place in Cell" behavior.

Canonical PDF image-layout examples:

Natural expansion (`stretch=1`, no applicable scale):

```text
1,1,1,"black","solid-1",1,80,
data(<img src=logo.png w=120 h=auto m=15>)
```

Fixed row (fit inside cell_height after reserving margin):

```text
1,1,1,"black","solid-1",0,80,
data(<img src=logo.png w=120 h=auto m=10>)
```

Row scale (effective row height is 40 x 3 = 120):

```text
1,1,1,"black","solid-1",0,40,
scale(A1=3,1),
data(<img src=logo.png w=200 h=auto m=10>)
```

Combined row/column scale (preserve proportions and use the stricter limit):

```text
1,2,1,"black","solid-1",0,50,
scale(A1=2,3),
data(<img src=logo.png w=500 h=auto m=10>, Description)
```

Addressing model:
Canonical syntax is PyCells-compatible:

Table!List!A1
Table!List!A3:D7
Table!List!Summary

Important:
Do NOT describe canonical syntax as Document!Table!A1.
One Table can contain multiple Lists.
Each List has its own A1 grid.

Examples:

Invoice!Items!A4
Report!Sales!A3:D7
CRM!Customers!Summary
Annual Report!Balance Sheet!Totals

Internal compatibility note:
Some internal fields still have legacy names:
- address.document currently stores the logical Table name.
- address.table / address.table_name / address.list_name stores the logical List name.
Prefer saying Table/List in docs, tests, and user-facing explanations.

Multitable fixture dictionary convention:
Use this canonical shape:

{
  "document": "Invoice",       # logical Table name, legacy key name
  "table_name": "Items",       # logical List name
  "sheet_name": "Items",       # physical XLSX worksheet name only
  "preamble": "...",
  "style": "style(...)",
  "data": "data(...)",
  "title": "Invoice Items"
}

Do not use "sheet" as the logical key in canonical tests. It is misleading.
Use "sheet_name" only for renderer-specific XLSX worksheet naming.
For simple documentation examples, keep table_name and sheet_name the same unless demonstrating physical XLSX naming separately.
Avoid confusing triples such as:
  "document": "Navigation",
  "sheet": "Links",
  "sheet_name": "Links"
Prefer:
  "document": "Navigation",
  "table_name": "Links",
  "sheet_name": "Links"

Addressing package:
tentags/addressing owns canonical address logic:
- Address, CellRef
- Location, AddressType: CELL, RANGE, MARK
- RangeRef
- AddressResolver
- AddressContext
- AddressTarget
- ResolvedAddress
- parse_address
- parse_location
- parse_cell_ref
- parse_range
- column_to_name
- name_to_column

Do not duplicate A1 parsing logic inside renderers. Renderers should consume Address or ResolvedAddress.

Renderer mapping:

HTML:
- every td has coordinate id: tt-A1, tt-B2, etc.
- scoped multitable ids use prefix: tt-Invoice-Items-A4.
- marks use tt-mark-Summary or scoped tt-Invoice-Items-mark-Summary.
- goto links map to href="#...".

XLSX:
- local goto A1 maps to #Sheet!A1.
- goto Table!List!A1 maps to the target physical sheet_name and cell.
- ranges map to their start cell.
- marks map to the marked cell.
- sheet names with spaces must be quoted, for example #'Balance Sheet'!A2.

PDF:
- uses ReportLab.
- anchors are renderer-specific.
- multitable PDF with separate tables must be visibly separate.
- PDF may place several separate tables in columns on one page using tables_per_row, including tables_per_row="auto".

Multitable:
Multitable means several separate List/TableModel entries, not one big table.
Each List must have its own:
- preamble
- style(...)
- data(...)

Hard validation rule for every table item in multitable:
- For each table in tables[], preamble rows == style rows == data rows.
- For each table in tables[], preamble cols == style cols == data cols.
- Never check only the combined report. Validate every individual table item before calling multitable_html(), multitable_xlsx(), or multitable_pdf().
- A mismatch can make HTML/XLSX look partially okay but break PDF destinations, marks, links, or layout.

Functions:

tentags.multitable_html(
    tables,
    layout="vertical",
    cols=1,
    gap="24px",
    full_page=False,
    settings=None
)

tentags.multitable_xlsx(
    tables,
    filepath=None,
    mode="sheets" or "stacked",
    gap=3,
    show_titles=True,
    settings=None
)

tentags.multitable_pdf(
    tables,
    filepath=None,
    page_size="A4",  # A3 | A4 | A5 | letter | legal | tabloid
    orientation="portrait" or "landscape",
    page_break_after_each=True,
    margins=(36, 36, 36, 36),
    tables_per_row=1 or "auto",
    tables_per_page=None or "auto",
    gap=12,
    settings=None
)

Multitable rendering expectations:
- HTML: multiple <table> elements must exist.
- XLSX mode="sheets": each List normally becomes a separate worksheet.
- XLSX mode="stacked": multiple tables are stacked on one worksheet with gap/title settings.
- PDF: tables_per_row controls how many tables are placed side by side on a PDF page.
- PDF: tables_per_row="auto" computes how many columns fit in the available PDF page width.
- PDF: tables_per_page controls how many table blocks are placed before a forced page break.
- PDF: tables_per_page="auto" computes how many table blocks fit in the available PDF page height.
- PDF: gap controls spacing between table blocks in multi-column PDF layout.

Canonical settings style:
- In examples and tests, prefer named settings dictionaries over magic inline parameters.
- This makes examples easier to reuse, easier for LLMs to copy, and safer when new renderer options are added.
- The library owns export settings. Tests must not invent ordering/filtering/output logic outside tentags.
- Current API supports settings=HTML_SETTINGS, settings=XLSX_SETTINGS, and settings=PDF_SETTINGS.
- Settings can include output, table_order, columns, renderer options, and layout/export options.
- Single-table render_pdf(..., settings=PDF_SETTINGS) defaults to A4 portrait and accepts page_size (`A3`, `A4`, `A5`, `letter`, `legal`, or `tabloid`), orientation, and margins.
- Public defaults live in DEFAULT_PDF_SETTINGS, DEFAULT_MULTITABLE_HTML_SETTINGS, DEFAULT_MULTITABLE_XLSX_SETTINGS, and DEFAULT_MULTITABLE_PDF_SETTINGS.

Example:

HTML_SETTINGS = {
    "output": "report.html",
    "table_order": ["Dashboard!Menu", "Invoice!Items"],
    "columns": {
        "Dashboard!Menu": ["Section", "Link"],
        "Invoice!Items": ["Item", "Total"],
    },
    "tables_per_row": 2,
    "html_title": "Report",
    "layout": "grid",
    "cols": 2,
    "gap": "24px",
    "full_page": True,
}

XLSX_SETTINGS = {
    "output": "report.xlsx",
    "table_order": ["Dashboard!Menu", "Invoice!Items"],
    "columns": {
        "Dashboard!Menu": ["Section", "Link"],
        "Invoice!Items": ["Item", "Total"],
    },
    "tables_per_sheet": "all",
    "stacked_sheet_name": "Report",
    "mode": "stacked",
    "gap": 2,
    "show_titles": True,
}

PDF_SETTINGS = {
    "output": "report.pdf",
    "table_order": ["Dashboard!Menu", "Invoice!Items"],
    "columns": {
        "Dashboard!Menu": ["Section", "Link"],
        "Invoice!Items": ["Item", "Total"],
    },
    "tables_per_row": "auto",
    "tables_per_page": "auto",
    "gap": 16,
    "page_size": "A4",
    "orientation": "landscape",
    "page_break_after_each": False,
    "margins": (24, 24, 36, 36),
}

html = tentags.multitable_html(tables, settings=HTML_SETTINGS)
tentags.multitable_xlsx(tables, settings=XLSX_SETTINGS)
tentags.multitable_pdf(tables, settings=PDF_SETTINGS)

Canonical runnable multitable example:

tables = [
    {
        "document": "Dashboard",
        "table_name": "Menu",
        "sheet_name": "Menu",
        "title": "Menu",
        "preamble": '2,2,1,"#0f172a","solid",0,24',
        "style": "style(<bg=#dbeafe><b></b></bg>, <bg=#dbeafe><b></b></bg>; <bg=#eff6ff></bg>, <bg=#eff6ff></bg>)",
        "data": "data(<mark=Top>Section, Link; Invoice, <url=goto:Invoice!Items!A2>Open invoice</url>)"
    },
    {
        "document": "Invoice",
        "table_name": "Items",
        "sheet_name": "Items",
        "title": "Invoice Items",
        "preamble": '2,2,1,"#7c2d12","solid",0,24',
        "style": "style(<bg=#ffedd5><b></b></bg>, <bg=#ffedd5><b></b></bg>; <bg=#fff7ed></bg>, <bg=#fff7ed></bg>)",
        "data": "data(Item, Total; Paper, <url=goto:Dashboard!Menu!Top>$25</url>)"
    }
]

HTML_SETTINGS = {
    "output": "demo_multitable.html",
    "table_order": ["Dashboard!Menu", "Invoice!Items"],
    "columns": {
        "Dashboard!Menu": ["Section", "Link"],
        "Invoice!Items": ["Item", "Total"],
    },
    "tables_per_row": 2,
    "html_title": "Demo Multitable",
    "layout": "grid",
    "cols": 2,
    "gap": "24px",
    "full_page": True,
}

XLSX_SETTINGS = {
    "output": "demo_multitable.xlsx",
    "table_order": ["Dashboard!Menu", "Invoice!Items"],
    "columns": {
        "Dashboard!Menu": ["Section", "Link"],
        "Invoice!Items": ["Item", "Total"],
    },
    "tables_per_sheet": 1,
    "stacked_sheet_name": "Report",
    "mode": "sheets",
}

PDF_SETTINGS = {
    "output": "demo_multitable.pdf",
    "table_order": ["Dashboard!Menu", "Invoice!Items"],
    "columns": {
        "Dashboard!Menu": ["Section", "Link"],
        "Invoice!Items": ["Item", "Total"],
    },
    "tables_per_row": "auto",
    "tables_per_page": "auto",
    "gap": 16,
    "page_size": "A4",
    "orientation": "landscape",
    "page_break_after_each": False,
    "margins": (24, 24, 36, 36),
}

html = tentags.multitable_html(tables, settings=HTML_SETTINGS)
tentags.multitable_xlsx(tables, settings=XLSX_SETTINGS)
tentags.multitable_pdf(tables, settings=PDF_SETTINGS)

Current important tests:
- test/test_addressing.py
- test/test_all_tags.py
- test/test_border_styles.py
- test/test_external_address_resolver.py
- test/test_html.py
- test/test_line_breaks.py
- test/test_mark_goto.py
- test/test_merge_content_preservation.py
- test/test_metadata_features.py
- test/test_multitable_addressing.py
- test/test_scale.py
- test/test_serializer_api.py
- test/test_serializer_db_examples.py
- test/test_value_refs.py

Main multitable tests in test/test_multitable_addressing.py:
- test_multitable_has_multiple_separate_tables_in_html
- test_multitable_has_multiple_separate_sheets_in_xlsx
- test_multitable_has_multiple_stacked_table_blocks_in_xlsx
- test_multitable_pdf_has_separate_pages_for_separate_tables
- test_multitable_each_sheet_has_own_preamble_style_and_data
- test_multitable_uses_table_list_names_not_logical_sheet_keys
- test_multitable_html_format_settings_are_preserved
- test_multitable_xlsx_format_settings_are_preserved
- test_multitable_pdf_format_settings_are_preserved

Direct file generation:

python test/test_external_address_resolver.py

Creates:
<PROJECT_ROOT>\demo_output\external_resolver_navigation.html
<PROJECT_ROOT>\demo_output\external_resolver_navigation.xlsx
<PROJECT_ROOT>\demo_output\external_resolver_navigation_stacked.xlsx
<PROJECT_ROOT>\demo_output\external_resolver_navigation.pdf
<PROJECT_ROOT>\demo_output\multitable_addressing.pdf

python test/test_multitable_addressing.py

Creates:
<PROJECT_ROOT>\demo_output\multitable_addressing.html
<PROJECT_ROOT>\demo_output\multitable_addressing_sheets.xlsx
<PROJECT_ROOT>\demo_output\multitable_addressing_stacked.xlsx
<PROJECT_ROOT>\demo_output\multitable_addressing.pdf
<PROJECT_ROOT>\demo_output\multitable_layout_options.html
<PROJECT_ROOT>\demo_output\multitable_layout_options_stacked.xlsx
<PROJECT_ROOT>\demo_output\multitable_layout_options_landscape.pdf

Verification commands:

python -m pytest test -q
python -m pytest test\test_multitable_addressing.py -q
python test/test_multitable_addressing.py

Expected current full result:
All tests should pass. Recently the suite was 90 passed.

Example canonical multitable fixture:

tables = [
    {
        "document": "Navigation",
        "table_name": "Links",
        "sheet_name": "Links",
        "title": "Navigation Links",
        "preamble": '3,1,1,"#0f172a","solid",0,24',
        "style": "style(<bg=#dbeafe><color=#1e3a8a><b></b></color></bg>; <bg=#eff6ff></bg>; <bg=#eff6ff></bg>)",
        "data": "data(<url=goto:Invoice!Items!A4>Open invoice item</url>; <url=goto:Report!Sales!A3:D7>Open sales range</url>; <url=goto:CRM!Customers!Summary>Open customer summary</url>)"
    },
    {
        "document": "Invoice",
        "table_name": "Items",
        "sheet_name": "Items",
        "title": "Invoice Items",
        "preamble": '4,1,2,"#7c2d12","dashed",0,26',
        "style": "style(<bg=#ffedd5><color=#7c2d12><b></b></color></bg>; <bg=#fff7ed></bg>; <bg=#fff7ed></bg>; <bg=#fed7aa><b></b></bg>)",
        "data": "data(Item A1; Item A2; Item A3; Invoice item A4)"
    },
    {
        "document": "Report",
        "table_name": "Sales",
        "sheet_name": "Sales",
        "title": "Sales Report",
        "preamble": '7,4,1,"#166534","solid-1",0,22',
        "style": "style(<bg=#dcfce7><color=#166534><b></b></color></bg>, <bg=#dcfce7><color=#166534><b></b></color></bg>, <bg=#dcfce7><color=#166534><b></b></color></bg>, <bg=#dcfce7><color=#166534><b></b></color></bg>)",
        "data": "data(S1A, S1B, S1C, S1D; S2A, S2B, S2C, S2D; Sales A3, S3B, S3C, S3D; S4A, S4B, S4C, S4D; S5A, S5B, S5C, S5D; S6A, S6B, S6C, S6D; S7A, S7B, S7C, S7D)"
    },
    {
        "document": "CRM",
        "table_name": "Customers",
        "sheet_name": "Customers",
        "title": "CRM Customers",
        "preamble": '2,1,1,"#581c87","dotted",0,28',
        "style": "style(<bg=#f3e8ff><color=#581c87><b></b></color></bg>; <bg=#faf5ff></bg>)",
        "data": "data(Customer top; <mark=Summary>Customer summary)"
    }
]

Example exports:

HTML_SETTINGS = {
    "output": "multitable_addressing.html",
    "table_order": ["Navigation!Links", "Invoice!Items", "Report!Sales", "CRM!Customers"],
    "columns": {
        "Navigation!Links": ["Open invoice item"],
        "Invoice!Items": ["Item A1"],
        "Report!Sales": ["S1A", "S1B", "S1C", "S1D"],
        "CRM!Customers": ["Customer top"],
    },
    "tables_per_row": "auto",
    "html_title": "Multitable Addressing",
    "layout": "grid",
    "cols": 2,
    "gap": "40px",
    "full_page": True,
}

XLSX_SHEETS_SETTINGS = {
    "output": "multitable_addressing_sheets.xlsx",
    "table_order": ["Navigation!Links", "Invoice!Items", "Report!Sales", "CRM!Customers"],
    "columns": {
        "Navigation!Links": ["Open invoice item"],
        "Invoice!Items": ["Item A1"],
        "Report!Sales": ["S1A", "S1B", "S1C", "S1D"],
        "CRM!Customers": ["Customer top"],
    },
    "tables_per_sheet": 1,
    "mode": "sheets",
}

XLSX_STACKED_SETTINGS = {
    "output": "multitable_addressing_stacked.xlsx",
    "table_order": ["Navigation!Links", "Invoice!Items", "Report!Sales", "CRM!Customers"],
    "columns": {
        "Navigation!Links": ["Open invoice item"],
        "Invoice!Items": ["Item A1"],
        "Report!Sales": ["S1A", "S1B", "S1C", "S1D"],
        "CRM!Customers": ["Customer top"],
    },
    "tables_per_sheet": "all",
    "stacked_sheet_name": "Report",
    "mode": "stacked",
    "gap": 3,
    "show_titles": True,
}

PDF_SETTINGS = {
    "output": "multitable_addressing.pdf",
    "table_order": ["Navigation!Links", "Invoice!Items", "Report!Sales", "CRM!Customers"],
    "columns": {
        "Navigation!Links": ["Open invoice item"],
        "Invoice!Items": ["Item A1"],
        "Report!Sales": ["S1A", "S1B", "S1C", "S1D"],
        "CRM!Customers": ["Customer top"],
    },
    "tables_per_row": "auto",
    "tables_per_page": "auto",
    "gap": 16,
    "page_size": "A4",
    "orientation": "landscape",
    "page_break_after_each": False,
    "margins": (24, 24, 36, 36),
}

html = tentags.multitable_html(tables, settings=HTML_SETTINGS)
tentags.multitable_xlsx(tables, settings=XLSX_SHEETS_SETTINGS)
tentags.multitable_xlsx(tables, settings=XLSX_STACKED_SETTINGS)
tentags.multitable_pdf(tables, settings=PDF_SETTINGS)

Coding conventions:
- Prefer apply_patch for file edits.
- Do not revert user changes.
- Do not hide user-visible output in temp folders.
- Preserve backward compatibility.
- Prefer the smallest safe change.
- Avoid unrelated refactoring.
- Do not publish or bump version unless explicitly asked.
- Keep addressing parser/resolver centralized in tentags/addressing.
- Always preserve old URL behavior for non-goto URLs.
- Duplicate marks must raise DuplicateMarkError.
- Multitable export settings belong to the library API. Tests should call settings=... and verify output; they must not implement their own ordering, column validation, output routing, or renderer-kwarg filtering.
- Serializer API is tentags.serialize.preamble(), tentags.serialize.style(), and tentags.serialize.data(). It serializes Python structures to DSL only.
- Top-level dumps_preamble(), dumps_style(), and dumps_data() are compatible aliases, not a separate layer.
- Do not invent TTTable, mutable table objects, compile_lists(), compile_from_lists(), or any second compiler path unless explicitly requested.
- DSL remains the only compiler input: compile(preamble, style, data).

Self-check before answering:
- preamble rows == style rows == data rows when style/data are explicit matrices.
- preamble cols == style cols == data cols when style/data are explicit matrices.
- For multitable examples, repeat the row/column check separately for every dict in tables[].
- Count styled-empty cells/rows in style(...). A style row containing only tags, closing tags, or an empty body with styles is still a real row.
- If a data row contains text such as <url=goto:...>Text</url>, the matching style row must exist even if the style row itself has no visible text.
- Do not let a table item declare 3 rows in preamble while data(...) contains 4 rows, especially if the extra row contains <mark> or goto targets.
- All paired tags are properly opened and closed.
- Single tags such as <mark>, <img>, <value>, and <br> are not closed.
- Address syntax is canonical PyCells-compatible Table!List!A1, Table!List!A1:B3, or Table!List!Summary.
- If scale(...) is present, every scale address is local, inside the current table, and both values are integers from 1 to 5.
- If scale(...) is present, verify that it is in the preamble before style(...) or data(...), never inside a cell.
- If vertical scale is greater than 1, verify that cell_height is greater than 0.
- For MultiTable, verify each table item's scale addresses against that item's own rows and columns.
- If border/grid/cm/rm rendering changed, complete the full nine-case border regression matrix in HTML, XLSX, and PDF before reporting success.
- If parsing, styling, fonts, or renderer behavior changed, verify every supported cell tag across IR, HTML, XLSX, PDF, and DOCX: <b>, <i>, nested <b><i>, <u>, <s>, <color>, <bg>, <fs>, <left>, <center>, <right>, <url>, <mark>, <value>, <img>, <br>, <cm>, and <rm>. Do not treat successful file creation as proof that a visual tag works; assert the resulting renderer properties.
- For XLSX colors, assert the complete opaque ARGB value `FFRRGGBB` for text, backgrounds, and borders. A suffix-only assertion can miss an invalid transparent `00RRGGBB` value.
- Do not generate unsupported current syntax such as external <value=Table!List!A1>.
- Do not invent new tags, new public APIs, or renderer-specific IR fields unless explicitly requested.
- Prefer runnable examples over conceptual examples. If showing future syntax, label it clearly as future/reserved.
- If an example is intended to be runnable, mentally or actually verify it with tentags.compile/render before presenting it.

When answering the user:
- Be direct.
- If asked to implement, implement.
- If the user is angry, do not argue. Fix the concrete thing.
- The user cares that generated files are visible and explicit, but test/demo artifacts must go under <PROJECT_ROOT>/demo_output instead of cluttering the project root.
```

