Skip to content

claude/add-table-rendering-41A3r#1

Open
dev-ankit wants to merge 3 commits into
mainfrom
claude/add-table-rendering-41A3r
Open

claude/add-table-rendering-41A3r#1
dev-ankit wants to merge 3 commits into
mainfrom
claude/add-table-rendering-41A3r

Conversation

@dev-ankit
Copy link
Copy Markdown
Owner

Adds a new {table} code block annotation (mirroring {image}) that
parses TSV output into a markdown pipe table. Scripts produce
tab-separated output and Showboat renders it as a proper table instead
of a raw output fence. Supports full round-trip parsing/writing and
integrates with extract.

https://claude.ai/code/session_01HMef5o6Fq7kFAbhurXGU3T

Adds a new `{table}` code block annotation (mirroring `{image}`) that
parses TSV output into a markdown pipe table. Scripts produce
tab-separated output and Showboat renders it as a proper table instead
of a raw output fence. Supports full round-trip parsing/writing and
integrates with extract.

https://claude.ai/code/session_01HMef5o6Fq7kFAbhurXGU3T
Covers usage line, table output section with common DB patterns
(SQLite, Postgres, Python, PySpark), example command, and resulting
markdown format.

https://claude.ai/code/session_01HMef5o6Fq7kFAbhurXGU3T
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds support for {table}-annotated code blocks so tab-separated command output can be rendered as a Markdown pipe table, with parsing/writing round-trips and extract integration.

Changes:

  • Introduces CodeBlock.IsTable + TableOutputBlock and updates Markdown parsing/writing to support {table} and pipe-table output blocks.
  • Adds TSV parsing (exec.ParseTSV) and updates cmd.Exec to render {table} output as a Markdown table (instead of an output fence).
  • Updates extract to preserve {table} on reconstructed showboat exec commands; adds unit tests across packages.

Reviewed changes

Copilot reviewed 13 out of 13 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
markdown/blocks.go Adds IsTable and new TableOutputBlock type.
markdown/blocks_test.go Tests TableOutputBlock typing/shape.
markdown/parser.go Parses {table} annotation and pipe tables into TableOutputBlock.
markdown/parser_test.go Adds tests for parsing/round-tripping {table} blocks and table output.
markdown/writer.go Emits {table} code fences and renders TableOutputBlock as pipe tables.
markdown/writer_test.go Adds writer tests for {table} and table output formatting.
exec/table.go Adds TSV-to-(headers, rows) parser for table rendering.
exec/table_test.go Tests TSV parsing edge cases (empty, single column, trailing newlines).
cmd/build.go Updates exec to detect {table}, parse TSV, and write table output blocks.
cmd/build_test.go Adds end-to-end test asserting {table} output is a table (no output fence).
cmd/extract.go Ensures extracted exec commands preserve {table} and skips table output blocks.
cmd/extract_test.go Adds extract test ensuring {table} is preserved in emitted commands.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread cmd/build.go
Comment on lines 77 to 80
docID := documentID(blocks)
if docID != "" {
postSection(docID, "exec", []markdown.Block{codeBlock, outputBlock})
postSection(docID, "exec", []markdown.Block{codeBlock, outputBlk})
}
Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

postSection(..., "exec", ...) currently only serializes OutputBlock outputs. With {table} execution, outputBlk will be a TableOutputBlock, so remote exec posts will omit the output entirely. Update postSection to handle TableOutputBlock (e.g., by sending rendered markdown via markdown.Write, or adding a dedicated form field for table output).

Copilot uses AI. Check for mistakes.
Comment thread exec/table.go
Comment on lines +12 to +23
// Remove empty trailing lines
for len(lines) > 0 && lines[len(lines)-1] == "" {
lines = lines[:len(lines)-1]
}
if len(lines) == 0 {
return nil, nil, fmt.Errorf("empty output: no header row found")
}

headers := strings.Split(lines[0], "\t")
var rows [][]string
for _, line := range lines[1:] {
rows = append(rows, strings.Split(line, "\t"))
Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ParseTSV should validate row widths and normalize line endings. As written, CRLF output will leave trailing \r in the last header/cell, and rows with a different number of columns than the header will silently produce malformed markdown tables later. Consider trimming \r from each line, rejecting empty/blank header rows, and returning an error when any row has a column count != len(headers) (or explicitly pad/truncate).

Suggested change
// Remove empty trailing lines
for len(lines) > 0 && lines[len(lines)-1] == "" {
lines = lines[:len(lines)-1]
}
if len(lines) == 0 {
return nil, nil, fmt.Errorf("empty output: no header row found")
}
headers := strings.Split(lines[0], "\t")
var rows [][]string
for _, line := range lines[1:] {
rows = append(rows, strings.Split(line, "\t"))
for i, line := range lines {
lines[i] = strings.TrimSuffix(line, "\r")
}
// Remove empty trailing lines
for len(lines) > 0 && strings.TrimSpace(lines[len(lines)-1]) == "" {
lines = lines[:len(lines)-1]
}
if len(lines) == 0 {
return nil, nil, fmt.Errorf("empty output: no header row found")
}
if strings.TrimSpace(lines[0]) == "" {
return nil, nil, fmt.Errorf("empty output: no header row found")
}
headers := strings.Split(lines[0], "\t")
var rows [][]string
for i, line := range lines[1:] {
row := strings.Split(line, "\t")
if len(row) != len(headers) {
return nil, nil, fmt.Errorf("invalid TSV row %d: expected %d columns, got %d", i+2, len(headers), len(row))
}
rows = append(rows, row)

Copilot uses AI. Check for mistakes.
Comment thread markdown/writer.go
Comment on lines +59 to +74
case TableOutputBlock:
if _, err := fmt.Fprintf(w, "| %s |\n", strings.Join(b.Headers, " | ")); err != nil {
return err
}
seps := make([]string, len(b.Headers))
for i := range seps {
seps[i] = "---"
}
if _, err := fmt.Fprintf(w, "| %s |\n", strings.Join(seps, " | ")); err != nil {
return err
}
for _, row := range b.Rows {
if _, err := fmt.Fprintf(w, "| %s |\n", strings.Join(row, " | ")); err != nil {
return err
}
}
Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TableOutputBlock writing doesn't enforce that each row has the same number of cells as Headers. If a row is short/long, the emitted markdown table will be structurally invalid or render unexpectedly. Consider validating len(row) == len(b.Headers) and returning an error (or padding/truncating deterministically) before writing.

Copilot uses AI. Check for mistakes.
Comment thread cmd/build.go
Comment on lines +60 to +71
codeBlock := markdown.CodeBlock{Lang: runLang, Code: code, IsTable: isTable}
var outputBlk markdown.Block
if isTable {
headers, rows, parseErr := execpkg.ParseTSV(output)
if parseErr != nil {
return output, exitCode, fmt.Errorf("parsing table output: %w", parseErr)
}
outputBlk = markdown.TableOutputBlock{Headers: headers, Rows: rows}
} else {
outputBlk = markdown.OutputBlock{Content: output}
}
blocks = append(blocks, codeBlock, outputBlk)
Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This introduces CodeBlock.IsTable and TableOutputBlock, but other commands still assume exec output is always an OutputBlock. In particular, showboat pop will only remove the table output block (leaving the preceding code block), and showboat verify will neither compare nor update table outputs because it only checks for a following OutputBlock. Those commands should be updated to treat TableOutputBlock the same way as OutputBlock for grouping and verification.

Copilot uses AI. Check for mistakes.
Comment thread markdown/parser.go
Comment on lines +132 to +143
// Table output: a pipe table (| header | ... | followed by | --- | ... |).
if isTableRow(lines[i]) && i+1 < len(lines) && isTableSeparator(lines[i+1]) {
headers := parseTableRow(lines[i])
i += 2 // past header and separator lines
var rows [][]string
for i < len(lines) && isTableRow(lines[i]) {
rows = append(rows, parseTableRow(lines[i]))
i++
}
blocks = append(blocks, TableOutputBlock{Headers: headers, Rows: rows})
skipSeparator()
continue
Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The parser currently treats any markdown pipe table (header row + separator row) as a TableOutputBlock. That will reclassify user-authored tables inside commentary as generated output, which then gets skipped by showboat extract (and can affect other behaviors). Consider only parsing a table as TableOutputBlock when it immediately follows a CodeBlock with IsTable=true (i.e., the preceding fenced block had {table}), otherwise leave it in a CommentaryBlock.

Suggested change
// Table output: a pipe table (| header | ... | followed by | --- | ... |).
if isTableRow(lines[i]) && i+1 < len(lines) && isTableSeparator(lines[i+1]) {
headers := parseTableRow(lines[i])
i += 2 // past header and separator lines
var rows [][]string
for i < len(lines) && isTableRow(lines[i]) {
rows = append(rows, parseTableRow(lines[i]))
i++
}
blocks = append(blocks, TableOutputBlock{Headers: headers, Rows: rows})
skipSeparator()
continue
// Table output: only treat a pipe table as generated output when it
// immediately follows a CodeBlock marked as a table source.
if isTableRow(lines[i]) && i+1 < len(lines) && isTableSeparator(lines[i+1]) && len(blocks) > 0 {
if codeBlock, ok := blocks[len(blocks)-1].(CodeBlock); ok && codeBlock.IsTable {
headers := parseTableRow(lines[i])
i += 2 // past header and separator lines
var rows [][]string
for i < len(lines) && isTableRow(lines[i]) {
rows = append(rows, parseTableRow(lines[i]))
i++
}
blocks = append(blocks, TableOutputBlock{Headers: headers, Rows: rows})
skipSeparator()
continue
}

Copilot uses AI. Check for mistakes.
Comment thread markdown/parser_test.go
Comment on lines +328 to +337
tbl, ok := blocks[0].(TableOutputBlock)
if !ok {
t.Fatalf("expected TableOutputBlock, got %T", blocks[0])
}
if len(tbl.Headers) != 2 {
t.Errorf("expected 2 headers, got %d", len(tbl.Headers))
}
if len(tbl.Rows) != 0 {
t.Errorf("expected 0 rows, got %d", len(tbl.Rows))
}
Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test asserts that a standalone pipe table parses as TableOutputBlock. If table parsing is unconditional, user-authored tables will be treated as generated output and omitted by showboat extract. Adjust the expected behavior so pipe tables are only parsed as TableOutputBlock when they are the output of a preceding {table} code block; otherwise they should remain part of a CommentaryBlock.

Suggested change
tbl, ok := blocks[0].(TableOutputBlock)
if !ok {
t.Fatalf("expected TableOutputBlock, got %T", blocks[0])
}
if len(tbl.Headers) != 2 {
t.Errorf("expected 2 headers, got %d", len(tbl.Headers))
}
if len(tbl.Rows) != 0 {
t.Errorf("expected 0 rows, got %d", len(tbl.Rows))
}
cb, ok := blocks[0].(CommentaryBlock)
if !ok {
t.Fatalf("expected CommentaryBlock, got %T", blocks[0])
}
if cb.Text != input {
t.Errorf("expected commentary text %q, got %q", input, cb.Text)
}

Copilot uses AI. Check for mistakes.
Pop only knew about OutputBlock and ImageOutputBlock. When popping a
table entry it removed the TableOutputBlock but left the CodeBlock
orphaned. Found by dogfooding the new {table} feature.

https://claude.ai/code/session_01HMef5o6Fq7kFAbhurXGU3T
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.

3 participants