Skip to content

:::table container directive for writer-friendly tables #289

@yumike

Description

@yumike

Problem

GFM pipe tables are painful for the table-heavy docs this engine is built for:

  1. Adding a column rewrites every row. Prettier (and GitHub's own diff view) pads cells to the widest value in each column, so a single long cell reformats dozens of surrounding rows into noisy diffs.
  2. Cells can't hold block content. No lists, no code blocks, no multiple paragraphs. The universal workarounds (<br>, escaped \|, raw <table>) render inconsistently and fight the rest of the pipeline.
  3. Long cells force long source lines. Unreadable in narrow editors and in GitHub's diff view.

Every mature docs ecosystem solves this by moving rows into bullet lists: RST's list-table, MyST's {list-table}, AsciiDoc's cell-per-line, Markdoc's {% table %}. Pipe tables should stay valid — this is an escape hatch for the painful 20%.

Proposed solution

A :::table container directive where rows are bullet lists separated by ---:

:::table {caption=\"HTTP endpoints\" widths=\"1,2,4\" align=\"left,center,left\"}
- Method
- Path
- Description
---
- GET
- /users
- List users
---
- POST
- /users
- Create a user
:::
  • First row group is the header by default (header=N to extend; footer=N for trailing footer rows).
  • :cell{colspan=2 rowspan=2 align=\"right\" scope=\"col\"} as an inline directive for per-cell attributes.
  • ::skip as a leaf directive for grid positions consumed by a surrounding span — self-checking, no silent inference.
  • Block attributes on :::table: caption, widths, align, header, footer, label, class.
  • Rich cell content: any block content valid in a list item is valid in a cell — paragraphs, nested lists, code blocks, blockquotes, images, inline formatting.

Diff behavior (the core win)

Adding a column is N+1 line additions with zero reformatting:

 :::table {label=\"http-methods\"}
 - Method
 - Idempotent
 - Safe
+- Rate limit
 ---
 - GET
 - Yes
 - Yes
+- 100/s
 ---
 - POST
 - No
 - No
+- 10/s
 :::

The pipe-table equivalent repads every cell in every row.

Implementation notes

The directive infrastructure already exists in crates/rw-renderer/src/directive/:

  • ContainerDirective trait (:::name) — implement this for tables. TabsDirective at crates/rw-renderer/src/tabs/directive.rs is the reference pattern.
  • InlineDirective trait (:name) — implement for :cell{…}.
  • LeafDirective trait (::name) — implement for ::skip.
  • DirectiveArgs::parse() already handles [content]{#id .class key=\"value\"} parsing.
  • DirectiveProcessor registers handlers with .with_container(...) / .with_inline(...) / .with_leaf(...).
  • Registration goes in rw-site::page::PageRenderer::configure_renderer alongside TabsDirective.

Open question: rich cell content through the existing pipeline

The existing pipeline is preprocess → pulldown-cmark → post-process replacements (see DirectiveProcessor::process in directive/processor.rs). Tabs work with this because tab bodies are plain block content that pulldown-cmark parses unchanged between the intermediate <rw-tabs> / <rw-tab> tags.

Tables are harder: CommonMark treats a raw <table> block as an HTML block and stops block-level markdown parsing inside it, which breaks the rich-cell-content goal. Options:

  1. Preprocess each :::table into an intermediate HTML skeleton with placeholder tokens for each cell body, stash the cell markdown in the directive, parse each cell body independently, and substitute rendered cell HTML in post_process (Replacements). Cells stay markdown until render time.
  2. Render the whole table to HTML in start()/end(), invoking an inner MarkdownRenderer pass per cell. Simpler, but allocates a sub-renderer per cell.

Either works; option 1 is closer to how TabsDirective already leans on Replacements. Worth prototyping before committing.

Validation

Errors (fail the render with line-accurate diagnostics):

  • Row groups with unequal column counts (after span adjustment).
  • colspan/rowspan extending past the table edge.
  • Span collisions — two cells claiming the same grid position.
  • ::skip at a position not consumed by a span; missing ::skip at a consumed position.
  • Nested :::table inside a cell.

Warnings (log, keep rendering):

  • Unknown block or cell attributes.
  • widths / align count mismatch with column count.
  • Empty table.

Non-goals

  • Replacing pipe tables. They stay valid and remain the right choice for small tables that need to render on GitHub.
  • Normalizing pipe tables and :::table into a shared internal model. Separate concern; parking it.
  • Nested tables. Explicit error.
  • Data-file-backed tables (:::csv-table etc.). Out of scope.
  • Sortable/filterable/interactive tables. Renderer concern, orthogonal to authoring.

Prior art

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions