From cb9c05a57b761919985193b5d53c9712f430ceac Mon Sep 17 00:00:00 2001 From: Leon Mika Date: Fri, 7 Aug 2026 05:10:16 +0000 Subject: [PATCH] Added CSV to ASCII table --- textfilters.go | 60 +++++++++++++++++++++++++++++ textfilters_test.go | 92 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 152 insertions(+) create mode 100644 textfilters_test.go diff --git a/textfilters.go b/textfilters.go index e3ef6d7..ed8af7b 100644 --- a/textfilters.go +++ b/textfilters.go @@ -5,6 +5,7 @@ import ( "bytes" "context" "encoding/base64" + "encoding/csv" "encoding/json" "errors" "fmt" @@ -12,6 +13,7 @@ import ( "sort" "strconv" "strings" + "unicode/utf8" "text/template" @@ -184,6 +186,64 @@ var TextFilters = map[string]TextProcessor{ return TextFilterResponse{Output: dst.String()}, nil }, }, + "csv-to-ascii-table": { + Label: "Convert: CSV to ASCII Table", + Filter: func(ctx context.Context, input string) (resp TextFilterResponse, err error) { + endNL := "" + if strings.HasSuffix(input, "\n") { + endNL = "\n" + } + + reader := csv.NewReader(strings.NewReader(input)) + reader.FieldsPerRecord = -1 + records, err := reader.ReadAll() + if err != nil { + return TextFilterResponse{}, err + } + if len(records) == 0 { + return TextFilterResponse{Output: input}, nil + } + + var colWidths []int + for _, record := range records { + for i, cell := range record { + if i >= len(colWidths) { + colWidths = append(colWidths, 0) + } + colWidths[i] = max(colWidths[i], utf8.RuneCountInString(cell)) + } + } + + writeRow := func(dst *strings.Builder, cells []string) { + line := make([]string, len(colWidths)) + for i, width := range colWidths { + cell := "" + if i < len(cells) { + cell = cells[i] + } + line[i] = cell + strings.Repeat(" ", width-utf8.RuneCountInString(cell)) + } + dst.WriteString(strings.TrimRight(strings.Join(line, " "), " ")) + dst.WriteString("\n") + } + + dashes := make([]string, len(colWidths)) + for i, width := range colWidths { + dashes[i] = strings.Repeat("-", width) + } + dashLine := strings.Join(dashes, " ") + "\n" + + var dst strings.Builder + writeRow(&dst, records[0]) + dst.WriteString(dashLine) + for _, record := range records[1:] { + writeRow(&dst, record) + } + dst.WriteString(dashLine) + + return TextFilterResponse{Output: strings.TrimSuffix(dst.String(), "\n") + endNL}, nil + }, + }, "convert-yaml-to-json": { Label: "Convert: YAML to JSON", Filter: func(ctx context.Context, input string) (resp TextFilterResponse, err error) { diff --git a/textfilters_test.go b/textfilters_test.go new file mode 100644 index 0000000..fde1603 --- /dev/null +++ b/textfilters_test.go @@ -0,0 +1,92 @@ +package main + +import ( + "context" + "testing" +) + +func TestCSVToASCIITable(t *testing.T) { + filter := TextFilters["csv-to-ascii-table"].Filter + + tests := []struct { + name string + input string + want string + wantErr bool + }{ + { + name: "basic table with quoted value", + input: "alpha,bravo\n" + + "1,b\n" + + "\"long value\",something\n", + want: "alpha bravo\n" + + "---------- ---------\n" + + "1 b\n" + + "long value something\n" + + "---------- ---------\n", + }, + { + name: "no trailing newline", + input: "a,b\n1,2", + want: "a b\n" + + "- -\n" + + "1 2\n" + + "- -", + }, + { + name: "quoted field containing comma", + input: "name,desc\n\"x, y\",z\n", + want: "name desc\n" + + "---- ----\n" + + "x, y z\n" + + "---- ----\n", + }, + { + name: "ragged rows padded with empty cells", + input: "a,b,c\n1\n2,3\n", + want: "a b c\n" + + "- - -\n" + + "1\n" + + "2 3\n" + + "- - -\n", + }, + { + name: "header only", + input: "one,two\n", + want: "one two\n" + + "--- ---\n" + + "--- ---\n", + }, + { + name: "empty input unchanged", + input: "", + want: "", + }, + { + name: "invalid CSV returns error", + input: "a,\"b\nc", + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + resp, err := filter(context.Background(), tt.input) + if tt.wantErr { + if err == nil { + t.Fatalf("expected error, got output %q", resp.Output) + } + return + } + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if resp.Output != tt.want { + t.Errorf("output mismatch\ngot:\n%s\nwant:\n%s", resp.Output, tt.want) + } + if resp.Append { + t.Errorf("Append should be false") + } + }) + } +}