diff --git a/CHANGELOG.md b/CHANGELOG.md index 7e416e57..fb019569 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ - Release: update the Homebrew handoff to publish through `openclaw/tap`. - Version: `gog --version` now reports an informative fallback (for example, `v0.17.0-dev`) when built from source with plain `go build` instead of returning `dev`. - Docs: `gog docs insert` now defaults to end-of-doc when `--index` is omitted, instead of always inserting at position 1 (which silently reversed iterative inserts across multiple calls). Pass `--index 1` explicitly to keep the previous behaviour. (#606) +- Docs: markdown empty-header table rows (e.g. `| | |`) no longer collide with the separator detection — previously `docs write --append --markdown` swallowed both the empty header and the real `|---|---|` separator, leaving the last data row re-parsed as a literal pipe paragraph after the table. (#609) ## 0.17.0 - 2026-05-15 diff --git a/internal/cmd/docs_markdown.go b/internal/cmd/docs_markdown.go index 89e1e839..39a26602 100644 --- a/internal/cmd/docs_markdown.go +++ b/internal/cmd/docs_markdown.go @@ -218,6 +218,12 @@ func isTableSeparator(line string) bool { inner := strings.Trim(trimmed, "|") // Split by | and check each segment segments := strings.Split(inner, "|") + // A genuine separator must have at least one segment that actually contains + // dashes. Without this guard a row of empty pipe cells (e.g. an empty + // markdown table header `| | |`) would be misclassified as a + // separator because every segment hits the `continue` and never trips the + // dash check — see #609. + sawDashSegment := false for _, seg := range segments { seg = strings.TrimSpace(seg) if seg == "" { @@ -237,8 +243,9 @@ func isTableSeparator(line string) bool { if strings.Count(seg, "-") == 0 { return false } + sawDashSegment = true } - return len(segments) > 1 + return sawDashSegment && len(segments) > 1 } // parseMarkdownTable parses a markdown table into rows of cells diff --git a/internal/cmd/docs_markdown_test.go b/internal/cmd/docs_markdown_test.go index 99213f98..7455da17 100644 --- a/internal/cmd/docs_markdown_test.go +++ b/internal/cmd/docs_markdown_test.go @@ -335,3 +335,52 @@ func TestParseMarkdown_TableDoesNotSkipFollowingLine(t *testing.T) { t.Fatalf("second element = %#v, want paragraph 'After table'", got[1]) } } + +func TestIsTableSeparator_EmptyPipeRowRejected(t *testing.T) { + // Regression for #609: a row of empty pipe cells (e.g. an empty markdown + // table header) must not be classified as a separator line. Otherwise the + // outer parser drops a row from the table and re-parses the next data line + // as a literal pipe paragraph. + tests := []struct { + name string + line string + want bool + }{ + {"empty cells", "| | |", false}, + {"empty cells tight", "||", false}, + {"empty cells three cols", "| | | |", false}, + {"normal separator", "|---|---|", true}, + {"spaced separator", "| --- | --- |", true}, + {"left align", "|:---|---|", true}, + {"center align", "|:---:|---:|", true}, + {"mixed empty+dashes still valid", "|---| |", true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := isTableSeparator(tt.line); got != tt.want { + t.Errorf("isTableSeparator(%q) = %v, want %v", tt.line, got, tt.want) + } + }) + } +} + +func TestParseMarkdown_EmptyHeaderTableKeepsAllDataRows(t *testing.T) { + // Regression for #609: an empty-header table previously had its last data + // row re-parsed as a literal pipe paragraph (because the empty pipe row + // matched isTableSeparator and the outer loop advanced too far). + input := "| | |\n|-----|-----|\n| Label A | Value A |\n| Label B | Value B |" + got := ParseMarkdown(input) + if len(got) != 1 { + t.Fatalf("expected 1 element (table only), got %d: %#v", len(got), got) + } + if got[0].Type != MDTable { + t.Fatalf("element type = %v, want MDTable", got[0].Type) + } + if len(got[0].TableCells) != 3 { + t.Fatalf("expected 3 rows (empty header + 2 data), got %d: %#v", len(got[0].TableCells), got[0].TableCells) + } + last := got[0].TableCells[2] + if len(last) != 2 || last[0] != "Label B" || last[1] != "Value B" { + t.Fatalf("last row = %#v, want [Label B, Value B]", last) + } +}