Skip to content

render: correct output with post-render width - #83

Merged
llimllib merged 3 commits into
mainfrom
better-tables
Aug 19, 2026
Merged

render: correct output with post-render width#83
llimllib merged 3 commits into
mainfrom
better-tables

Conversation

@llimllib

@llimllib llimllib commented Aug 19, 2026

Copy link
Copy Markdown
Owner

Two places sized layout from pre-render text rather than the visible width of what actually gets printed.

1. Table columns counted the markdown source

format_table measured column widths on the raw cell:

col_widths[i] = self.strip_ansi(cell).width();

strip_ansi only removes escape sequences — it does not render markdown. So a link like [ENG-396](https://linear.app/company/issue/ENG-396) counted as 55 columns even though it renders as just ENG-396 (7). The same overcount applied to **bold** and `code`.

Reported case, a table whose Ticket column is all links:

Before — Ticket column padded to 55:

┌─────────────────────────────────────────────────────────┬──────────────
│ Ticket                                                  │ Title

After — 7:

┌─────────┬────────────────────────────────────────────────────────────
│ Ticket  │ Title

The header and rows now go through format_inline once up front, widths are measured on those rendered strings with display_width, and the formatted cells are reused when rendering (which also stops every cell being formatted twice).

2. wrap_text measured its indents with raw .width()

let first_indent_width = first_indent.width();

Most callers pass plain-space indents, but format_callout passes a colored bar:

let bar = format!("{} │ {}", color, reset);   // "\e[34m │ \e[0m"

The escape bytes were counted, so callouts believed their prefix was ~11 columns instead of 3 and wrapped ~8 columns early. At --width 70 (ANSI stripped, lengths shown):

before:  58: [ │ This is a fairly long callout body that should wrap]
after:   71: [ │ This is a fairly long callout body that should wrap right at the]

Both indents now use display_width. As a side effect UnicodeWidthStr::width is called from exactly one place (display_width), so all text measurement funnels through the ANSI-stripping path.

Sites audited and found correct

Site Status
format_paragraph format_inline before wrap_text
format_list — bullet / ordered / task ✅ formats first; indents are plain spaces
format_blockquote ✅ formats first; " │ " prefix has no ANSI
format_callout ✅ formats first — the bug was the indent, now fixed
wrap_text token widths ✅ already display_width
align_cell ✅ already display_width; now always receives rendered text
try_render_mermaid_ascii display_width; ASCII art is pre-rendered anyway
format_heading, format_horizontal_rule, format_code_block ✅ no width math on content
process_image img.width() is image pixels, unrelated
apply_padding (main.rs) ✅ prepends spaces, measures nothing

Tests

  • tests/fixtures/blocks/table_inline_formatting.toml — this fixture encoded the buggy widths (**Alice** measured as 9), updated to the correct rendered widths.
  • tests/fixtures/blocks/table_link_width.toml — new, covers the link case.
  • tests/fixtures/wrapping/callout_wrap.toml — new, covers the callout bar prefix.

cargo fmt -- --check, cargo clippy --all-targets --all-features -- -D warnings, and all 171 tests pass.

Docs

CLAUDE.md told agents to publish the transcript as a public gist, but every recent PR commits an HTML export to transcripts/ via the pr-transcript skill. Updated it to describe what the repo actually does, including how to guess the PR number for the filename before the PR exists.

Also documented the commit-signing failure mode: commit.gpgsign is on with an SSH key, and sandboxed agents usually cannot read ~/.ssh, so git commit dies with Couldn't load public key until --no-gpg-sign is passed.

Note

The commits here are unsigned — the sandbox could not read ~/.ssh to load the signing key. Amend with git commit --amend -S --no-edit if you want it signed.

Session transcript

Pi session transcript (transcripts/83-fix-table-and-wrap-widths.html)

Two places sized layout from pre-render text instead of the visible
width of what actually gets printed.

format_table measured column widths on the raw markdown cell, so a
link like `[ENG-396](https://linear.app/jellyfishco/issue/ENG-396)`
counted as 55 columns even though it renders as just `ENG-396` (7).
strip_ansi only removes escape sequences; it does not render markdown.
The same overcount applied to `**bold**` and `` `code` ``. Now the
header and rows are run through format_inline once up front, widths
are measured on those rendered strings with display_width, and the
formatted cells are reused when rendering (also avoids formatting
every cell twice).

wrap_text measured its indents with raw .width(). Most callers pass
plain spaces, but format_callout passes a colored bar
("\e[34m | \e[0m"), so callouts believed their prefix was ~11 columns
instead of 3 and wrapped ~8 columns early. Both indents now use
display_width, which leaves display_width as the only caller of
UnicodeWidthStr::width.

table_inline_formatting.toml encoded the buggy widths (`**Alice**`
as 9), so it is updated to the correct rendered widths.
The documented workflow said to publish the transcript as a public gist,
but every recent PR commits an HTML export to transcripts/ instead, via
the pr-transcript skill. Document what the repo actually does.

Also note the commit-signing failure mode: commit.gpgsign is on with an
SSH key, and sandboxed agents usually cannot read ~/.ssh, so git commit
fails with 'Couldn't load public key' until you pass --no-gpg-sign.
@llimllib llimllib changed the title tables/wrapping: measure widths on rendered output, not markdown source render: correct output with post-render width Aug 19, 2026
@llimllib
llimllib merged commit 55a3284 into main Aug 19, 2026
3 checks passed
@llimllib
llimllib deleted the better-tables branch August 19, 2026 13:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant