diff --git a/pkg/parser/footer_test.go b/pkg/parser/footer_test.go new file mode 100644 index 0000000..1f86507 --- /dev/null +++ b/pkg/parser/footer_test.go @@ -0,0 +1,68 @@ +package parser + +import ( + "os" + "path/filepath" + "reflect" + "slices" + "testing" +) + +func TestFooterPreservesOrdinaryMarkdown(t *testing.T) { + for _, block := range []string{ + "## Greeting\n```sh\necho hello\n```", + "Ordinary prose mentioning tags: [greeting]", + "description: ordinary metadata", + "description: |\n tags: [greeting]", + "metadata:\n tags: [greeting]", + "- tags: [greeting]", + "tags: [unterminated", + } { + t.Run(block, func(t *testing.T) { + input := "# Documentation\nIntro\n---\n" + block + "\n---\n" + body, tags := extractFooterTags([]byte(input)) + if string(body) != input || len(tags) != 0 { + t.Fatalf("extractFooterTags() = (%q, %v), want (%q, [])", body, tags, input) + } + }) + } +} + +func TestFooterTagsPreserveCommandBetweenRules(t *testing.T) { + for _, tc := range []struct { + name, footer string + wantTags []string + }{ + {"ordinary closing rule", "---\n", nil}, + {"inline tags", "---\ntags: [greeting, sample]\n---\n", []string{"greeting", "sample"}}, + {"case insensitive tags", "---\nTaGs: Greeting, Sample\n---\n", []string{"Greeting", "Sample"}}, + {"list tags", "---\ntags:\n - greeting\n - sample\n---\n", []string{"greeting", "sample"}}, + } { + t.Run(tc.name, func(t *testing.T) { + commandBody := "# Documentation\nIntro\n---\n## Greeting\n```sh\necho hello\n```\n" + input := commandBody + tc.footer + body, tags := extractFooterTags([]byte(input)) + wantBody := commandBody + if tc.wantTags == nil { + wantBody = input + } + if string(body) != wantBody || !reflect.DeepEqual(tags, tc.wantTags) { + t.Fatalf("extractFooterTags() = (%q, %v), want (%q, %v)", body, tags, wantBody, tc.wantTags) + } + path := filepath.Join(t.TempDir(), "rules.md") + if err := os.WriteFile(path, []byte(input), 0600); err != nil { + t.Fatal(err) + } + index, err := NewParser().ParseSingleFile(path) + if err != nil { + t.Fatal(err) + } + if len(index.Cheats) != 1 || index.Cheats[0].Command != "echo hello" { + t.Fatalf("parsed cheats = %+v, want one echo hello command", index.Cheats) + } + if tc.wantTags != nil && (!slices.Contains(index.Cheats[0].Tags, "greeting") || !slices.Contains(index.Cheats[0].Tags, "sample")) { + t.Fatalf("footer tags not applied: %v", index.Cheats[0].Tags) + } + }) + } +} diff --git a/pkg/parser/tags.go b/pkg/parser/tags.go index 4011991..b51c602 100644 --- a/pkg/parser/tags.go +++ b/pkg/parser/tags.go @@ -4,6 +4,8 @@ import ( "bytes" "path/filepath" "strings" + + yaml "go.yaml.in/yaml/v3" ) // ============================================================================ @@ -177,7 +179,11 @@ func extractYAMLFooter(data []byte, end int) ([]byte, []string, bool) { lineStart := findLineStart(data, lineEnd) line := bytes.TrimRight(data[lineStart:lineEnd], " \t\r") if bytes.Equal(line, []byte("---")) && lineStart != openEnd-3 { - tags := parseYAMLTags(data[lineEnd+1 : openEnd]) + block := data[lineEnd+1 : openEnd] + if !isTagMetadata(block) { + return nil, nil, false + } + tags := parseYAMLTags(block) return data[:lineStart], tags, true } if lineStart == 0 { @@ -188,6 +194,19 @@ func extractYAMLFooter(data []byte, end int) ([]byte, []string, bool) { return nil, nil, false } +func isTagMetadata(block []byte) bool { + var metadata map[string]any + if err := yaml.Unmarshal(block, &metadata); err != nil { + return false + } + for key := range metadata { + if strings.EqualFold(key, "tags") { + return true + } + } + return false +} + func findLineStart(data []byte, end int) int { start := end for start > 0 && data[start-1] != '\n' {