mirror of
https://github.com/PerpetualSoftware/pad.git
synced 2026-09-10 23:15:40 +00:00
b9381bf5f1
Completes #898 and fixes #1076. Markdown on the seven surfaces left out of #1070, so `--format markdown` is now honestly global and the flag help collapses to "table, json, markdown": - `item comments`, `item deps`, `project activity`, `attachment list`, `library list`, `role list`, `workspace members`. Two of those are not tabular, and markdown follows the terminal shape rather than forcing a table onto them: - `item comments` keeps the attribution-line-then-body form, and the body is emitted VERBATIM. A comment body is authored as markdown; escaping it would turn its lists and code fences into literal text. Only the attribution line, which we construct, is sanitized. - `item deps` keeps its two sections as `## Blocks` / `## Blocked by` lists. Colour carried the direction in the terminal (yellow out, red in); headings carry it here. New shared spine: `cli.RenderMarkdownTable(w, headers, rows)`. Every cell is escaped, and ragged rows are padded or truncated to the header width so a short or long row can't shift the column count and break the table. Wiring a surface is now naming columns and mapping rows. #1076 — ANSI stripping covered only SGR (`ESC[…m`), so non-SGR CSI sequences, OSC-8 hyperlinks, and stray C0 controls survived, both in the table width maths and in markdown output whose doc comment promised escape-free text. Replaced `sgrPattern` with `ansiPattern` + `stripANSI` covering OSC, CSI, two-character Fe escapes, and stray C0/DEL, with TAB/LF/CR deliberately preserved for callers that normalize them. `displayWidth` now uses it too: a control sequence is zero-width, so counting it was a column-alignment bug of the same family. Tests: 12 stripping cases, 4 table-helper cases (including ragged rows), 4 renderer cases for the two non-tabular surfaces, and the routing test extended to 8 subtests — one per surface, driven through cobra against an httptest server. Also covers the two gaps named in #1076: `item starred` and the scoped `item list <collection>` path. Each new guard was proven by mutating the source and watching it fail, not just by passing. Gates: go build ./... PASS; go vet PASS; gofmt clean; golangci-lint 0 issues. Both touched packages show the same 6+2 pre-existing Windows failures as clean main under an identical sandboxed run.
76 lines
2.0 KiB
Go
76 lines
2.0 KiB
Go
package cli
|
|
|
|
import (
|
|
"bytes"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestRenderMarkdownTable_Shape(t *testing.T) {
|
|
var buf bytes.Buffer
|
|
RenderMarkdownTable(&buf, []string{"A", "B"}, [][]string{{"1", "2"}, {"3", "4"}})
|
|
|
|
want := strings.Join([]string{
|
|
"| A | B |",
|
|
"| --- | --- |",
|
|
"| 1 | 2 |",
|
|
"| 3 | 4 |",
|
|
"",
|
|
}, "\n")
|
|
if got := buf.String(); got != want {
|
|
t.Errorf("table =\n%q\nwant\n%q", got, want)
|
|
}
|
|
}
|
|
|
|
func TestRenderMarkdownTable_EscapesCells(t *testing.T) {
|
|
var buf bytes.Buffer
|
|
RenderMarkdownTable(&buf, []string{"A"}, [][]string{
|
|
{"pipe | here"},
|
|
{"line\nbreak"},
|
|
{"ansi \x1b[2K here"},
|
|
})
|
|
out := buf.String()
|
|
|
|
if !strings.Contains(out, `pipe \| here`) {
|
|
t.Errorf("pipe not escaped:\n%s", out)
|
|
}
|
|
if strings.Contains(out, "line\nbreak") {
|
|
t.Errorf("newline not collapsed:\n%q", out)
|
|
}
|
|
if strings.ContainsRune(out, 0x1b) {
|
|
t.Errorf("escape byte survived:\n%q", out)
|
|
}
|
|
// Header + separator + three rows, and nothing extra from the newline cell.
|
|
if n := len(strings.Split(strings.TrimRight(out, "\n"), "\n")); n != 5 {
|
|
t.Errorf("got %d lines, want 5:\n%s", n, out)
|
|
}
|
|
}
|
|
|
|
// A row with the wrong number of cells must not change the column count, or the
|
|
// whole table stops parsing as a table.
|
|
func TestRenderMarkdownTable_NormalizesRaggedRows(t *testing.T) {
|
|
var buf bytes.Buffer
|
|
RenderMarkdownTable(&buf, []string{"A", "B", "C"}, [][]string{
|
|
{"only-one"},
|
|
{"one", "two", "three", "four-is-too-many"},
|
|
})
|
|
|
|
lines := strings.Split(strings.TrimRight(buf.String(), "\n"), "\n")
|
|
for i, line := range lines {
|
|
if n := strings.Count(line, "|"); n != 4 {
|
|
t.Errorf("line %d has %d pipes, want 4 (3 cells): %q", i, n, line)
|
|
}
|
|
}
|
|
if strings.Contains(buf.String(), "four-is-too-many") {
|
|
t.Errorf("overflow cell was not dropped:\n%s", buf.String())
|
|
}
|
|
}
|
|
|
|
func TestRenderMarkdownTable_NoHeadersWritesNothing(t *testing.T) {
|
|
var buf bytes.Buffer
|
|
RenderMarkdownTable(&buf, nil, [][]string{{"x"}})
|
|
if buf.Len() != 0 {
|
|
t.Errorf("expected no output without headers, got %q", buf.String())
|
|
}
|
|
}
|