-
Notifications
You must be signed in to change notification settings - Fork 0
claude/add-table-rendering-41A3r #1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -33,12 +33,21 @@ func Note(file, text string) error { | |
|
|
||
| // Exec appends a code block, executes it, and appends the output. | ||
| // It returns the captured output, the process exit code, and any error. | ||
| // If lang has a " {table}" suffix, the output is parsed as TSV and rendered | ||
| // as a markdown table instead of a plain output fence. | ||
| func Exec(file, lang, code, workdir string) (string, int, error) { | ||
| if _, err := os.Stat(file); err != nil { | ||
| return "", 1, fmt.Errorf("file not found: %s", file) | ||
| } | ||
|
|
||
| output, exitCode, err := execpkg.Run(lang, code, workdir) | ||
| // Detect {table} annotation and strip it for execution. | ||
| isTable := strings.HasSuffix(lang, " {table}") | ||
| runLang := lang | ||
| if isTable { | ||
| runLang = strings.TrimSuffix(lang, " {table}") | ||
| } | ||
|
|
||
| output, exitCode, err := execpkg.Run(runLang, code, workdir) | ||
| if err != nil { | ||
| return "", exitCode, fmt.Errorf("running code: %w", err) | ||
| } | ||
|
|
@@ -48,17 +57,26 @@ func Exec(file, lang, code, workdir string) (string, int, error) { | |
| return "", exitCode, err | ||
| } | ||
|
|
||
| codeBlock := markdown.CodeBlock{Lang: lang, Code: code} | ||
| outputBlock := markdown.OutputBlock{Content: output} | ||
| blocks = append(blocks, codeBlock, outputBlock) | ||
| 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) | ||
|
|
||
| if err := writeBlocks(file, blocks); err != nil { | ||
| return output, exitCode, err | ||
| } | ||
|
|
||
| docID := documentID(blocks) | ||
| if docID != "" { | ||
| postSection(docID, "exec", []markdown.Block{codeBlock, outputBlock}) | ||
| postSection(docID, "exec", []markdown.Block{codeBlock, outputBlk}) | ||
| } | ||
|
Comment on lines
77
to
80
|
||
|
|
||
| return output, exitCode, nil | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,26 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| package exec | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "fmt" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "strings" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // ParseTSV parses tab-separated output into headers and rows. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // The first line is treated as the header row. Empty trailing lines are ignored. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| func ParseTSV(output string) ([]string, [][]string, error) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| lines := strings.Split(output, "\n") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // 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")) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
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")) | |
| 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) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,74 @@ | ||
| package exec | ||
|
|
||
| import ( | ||
| "testing" | ||
| ) | ||
|
|
||
| func TestParseTSV(t *testing.T) { | ||
| input := "name\tage\nAlice\t30\nBob\t25\n" | ||
| headers, rows, err := ParseTSV(input) | ||
| if err != nil { | ||
| t.Fatal(err) | ||
| } | ||
| if len(headers) != 2 || headers[0] != "name" || headers[1] != "age" { | ||
| t.Errorf("unexpected headers: %v", headers) | ||
| } | ||
| if len(rows) != 2 { | ||
| t.Fatalf("expected 2 rows, got %d", len(rows)) | ||
| } | ||
| if rows[0][0] != "Alice" || rows[0][1] != "30" { | ||
| t.Errorf("unexpected row 0: %v", rows[0]) | ||
| } | ||
| if rows[1][0] != "Bob" || rows[1][1] != "25" { | ||
| t.Errorf("unexpected row 1: %v", rows[1]) | ||
| } | ||
| } | ||
|
|
||
| func TestParseTSVSingleColumn(t *testing.T) { | ||
| input := "count\n42\n" | ||
| headers, rows, err := ParseTSV(input) | ||
| if err != nil { | ||
| t.Fatal(err) | ||
| } | ||
| if len(headers) != 1 || headers[0] != "count" { | ||
| t.Errorf("unexpected headers: %v", headers) | ||
| } | ||
| if len(rows) != 1 || rows[0][0] != "42" { | ||
| t.Errorf("unexpected rows: %v", rows) | ||
| } | ||
| } | ||
|
|
||
| func TestParseTSVHeaderOnly(t *testing.T) { | ||
| input := "name\tage\n" | ||
| headers, rows, err := ParseTSV(input) | ||
| if err != nil { | ||
| t.Fatal(err) | ||
| } | ||
| if len(headers) != 2 { | ||
| t.Errorf("expected 2 headers, got %d", len(headers)) | ||
| } | ||
| if len(rows) != 0 { | ||
| t.Errorf("expected 0 rows, got %d", len(rows)) | ||
| } | ||
| } | ||
|
|
||
| func TestParseTSVEmpty(t *testing.T) { | ||
| _, _, err := ParseTSV("") | ||
| if err == nil { | ||
| t.Error("expected error for empty input") | ||
| } | ||
| } | ||
|
|
||
| func TestParseTSVTrailingNewlines(t *testing.T) { | ||
| input := "a\tb\n1\t2\n\n" | ||
| headers, rows, err := ParseTSV(input) | ||
| if err != nil { | ||
| t.Fatal(err) | ||
| } | ||
| if len(headers) != 2 { | ||
| t.Errorf("expected 2 headers, got %d", len(headers)) | ||
| } | ||
| if len(rows) != 1 { | ||
| t.Errorf("expected 1 row, got %d", len(rows)) | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This introduces
CodeBlock.IsTableandTableOutputBlock, but other commands still assume exec output is always anOutputBlock. In particular,showboat popwill only remove the table output block (leaving the preceding code block), andshowboat verifywill neither compare nor update table outputs because it only checks for a followingOutputBlock. Those commands should be updated to treatTableOutputBlockthe same way asOutputBlockfor grouping and verification.