diff --git a/shortcuts/doc/docs_update.go b/shortcuts/doc/docs_update.go index 5c64b7cc..64298721 100644 --- a/shortcuts/doc/docs_update.go +++ b/shortcuts/doc/docs_update.go @@ -62,6 +62,9 @@ var DocsUpdate = common.Shortcut{ if needsSelection[mode] && selEllipsis == "" && selTitle == "" { return common.FlagErrorf("--%s mode requires --selection-with-ellipsis or --selection-by-title", mode) } + if err := validateSelectionByTitle(selTitle); err != nil { + return err + } return nil }, @@ -156,3 +159,17 @@ func normalizeBoardTokens(raw interface{}) []string { return []string{} } } + +func validateSelectionByTitle(title string) error { + if title == "" { + return nil + } + trimmed := strings.TrimSpace(title) + if strings.Contains(trimmed, "\n") || strings.Contains(trimmed, "\r") { + return common.FlagErrorf("--selection-by-title must be a single heading line (for example: '## Section')") + } + if strings.HasPrefix(trimmed, "#") { + return nil + } + return common.FlagErrorf("--selection-by-title must include markdown heading prefix '#'. Example: --selection-by-title '## Section'") +} diff --git a/shortcuts/doc/docs_update_test.go b/shortcuts/doc/docs_update_test.go index d0845e36..b4270def 100644 --- a/shortcuts/doc/docs_update_test.go +++ b/shortcuts/doc/docs_update_test.go @@ -4,6 +4,7 @@ package doc import ( "reflect" + "strings" "testing" ) @@ -76,3 +77,56 @@ func TestNormalizeDocsUpdateResult(t *testing.T) { } }) } + +func TestValidateSelectionByTitle(t *testing.T) { + t.Run("empty title passes", func(t *testing.T) { + if err := validateSelectionByTitle(""); err != nil { + t.Fatalf("expected nil error, got %v", err) + } + }) + + t.Run("heading style title passes", func(t *testing.T) { + if err := validateSelectionByTitle("## 第二章"); err != nil { + t.Fatalf("expected nil error, got %v", err) + } + }) + + t.Run("plain text title fails with guidance", func(t *testing.T) { + err := validateSelectionByTitle("第二章") + if err == nil { + t.Fatalf("expected validation error") + } + if got := err.Error(); got == "" || !containsAll(got, "selection-by-title", "heading prefix") { + t.Fatalf("unexpected error: %v", err) + } + }) + + t.Run("multi-line heading still fails", func(t *testing.T) { + err := validateSelectionByTitle("## 第二章\n## 第三章") + if err == nil { + t.Fatalf("expected validation error") + } + if got := err.Error(); got == "" || !containsAll(got, "single heading line") { + t.Fatalf("unexpected error: %v", err) + } + }) + + t.Run("multi-line title fails", func(t *testing.T) { + err := validateSelectionByTitle("第二章\n第三章") + if err == nil { + t.Fatalf("expected validation error") + } + if got := err.Error(); got == "" || !containsAll(got, "single heading line") { + t.Fatalf("unexpected error: %v", err) + } + }) +} + +func containsAll(s string, tokens ...string) bool { + for _, token := range tokens { + if !strings.Contains(s, token) { + return false + } + } + return true +}