mirror of
https://github.com/PerpetualSoftware/pad.git
synced 2026-09-11 21:39:01 +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.
83 lines
2.4 KiB
Go
83 lines
2.4 KiB
Go
package cli
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// #1076: "ANSI stripping" covered only SGR (ESC[…m), so non-SGR CSI sequences,
|
|
// OSC-8 hyperlinks, and stray C0 controls survived — including through the
|
|
// markdown renderers, whose doc comments promise ANSI is stripped.
|
|
|
|
func TestStripANSI_NonSGRSequences(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
in string
|
|
want string
|
|
}{
|
|
{"SGR colour", "\x1b[1;31mred\x1b[0m", "red"},
|
|
{"CSI erase line", "before\x1b[2Kafter", "beforeafter"},
|
|
{"CSI cursor move", "a\x1b[10;20Hb", "ab"},
|
|
{"CSI private mode", "a\x1b[?25lb", "ab"},
|
|
{"OSC-8 hyperlink", "\x1b]8;;http://example.com\x07label\x1b]8;;\x07", "label"},
|
|
{"OSC window title with ST", "a\x1b]0;title\x1b\\b", "ab"},
|
|
{"stray C0 control", "a\x01b\x7fc", "abc"},
|
|
{"vertical tab and form feed", "a\x0bb\x0cc", "abc"},
|
|
{"bare ESC", "a\x1bb", "ab"},
|
|
{"plain text untouched", "nothing to strip", "nothing to strip"},
|
|
{"newlines preserved for the caller to handle", "a\nb", "a\nb"},
|
|
{"tabs preserved", "a\tb", "a\tb"},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
if got := stripANSI(tc.in); got != tc.want {
|
|
t.Errorf("stripANSI(%q) = %q, want %q", tc.in, got, tc.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// displayWidth drives the table column maths, so a zero-width control sequence
|
|
// must not be counted as visible columns.
|
|
func TestDisplayWidth_IgnoresNonSGRSequences(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
in string
|
|
want int
|
|
}{
|
|
{"CSI erase line", "abc\x1b[2K", 3},
|
|
{"OSC-8 hyperlink", "\x1b]8;;http://example.com\x07abc\x1b]8;;\x07", 3},
|
|
{"stray C0", "ab\x01c", 3},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
if got := displayWidth(tc.in); got != tc.want {
|
|
t.Errorf("displayWidth(%q) = %d, want %d", tc.in, got, tc.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// The markdown path promises no ANSI in its output; that promise has to hold for
|
|
// non-SGR sequences too.
|
|
func TestSanitizeMarkdownText_StripsNonSGRSequences(t *testing.T) {
|
|
in := "Tasks\x1b[2K\x1b]8;;http://example.com\x07"
|
|
got := SanitizeMarkdownText(in)
|
|
|
|
if strings.ContainsRune(got, 0x1b) {
|
|
t.Errorf("escape byte survived: %q", got)
|
|
}
|
|
if got != "Tasks" {
|
|
t.Errorf("SanitizeMarkdownText(%q) = %q, want %q", in, got, "Tasks")
|
|
}
|
|
}
|
|
|
|
func TestEscapeMarkdownCell_StripsNonSGRSequences(t *testing.T) {
|
|
got := escapeMarkdownCell("a\x1b[2Kb")
|
|
if got != "ab" {
|
|
t.Errorf("escapeMarkdownCell = %q, want %q", got, "ab")
|
|
}
|
|
}
|