Files
394ded7836 fix(skills): parse YAML multiline blocks and lists in skill frontmatter (#325)
* fix(skills): parse YAML multiline blocks and lists in skill frontmatter

parseSimpleYAML and ParseSkillFrontmatter only handled single-line
key: value pairs. YAML multiline block scalars (|, >) and list values
(- item) were parsed incorrectly:

  description: |
    Multi-line text here...

was stored as description="|" (literal pipe character), and indented
continuation lines were misinterpreted as new key-value pairs.

Fix: parseSimpleYAML now tracks indented continuation lines after
block scalar indicators (|, >, |-, >-) and empty-value keys (for
list blocks). Continuation lines are joined with spaces. List items
(- value) have the prefix stripped.

ParseSkillFrontmatter now delegates to parseSimpleYAML instead of
duplicating the parsing logic.

* fix(skills): preserve empty-value keys in parseSimpleYAML

When a key has no value and no indented continuation (e.g. "slug:"),
flushBlock now stores "" instead of silently dropping the key.
Maintains backward compatibility with the previous parser behavior.

---------

Co-authored-by: Luvu182 <208665161+Luvu182@users.noreply.github.com>
Co-authored-by: viettranx <viettranx@gmail.com>
2026-03-21 08:58:21 +07:00

72 lines
2.0 KiB
Go

package skills
import (
"path/filepath"
"regexp"
"strings"
)
// SlugRegexp validates skill slugs: lowercase alphanumeric with hyphens, no leading/trailing hyphen.
var SlugRegexp = regexp.MustCompile(`^[a-z0-9][a-z0-9-]*[a-z0-9]$`)
// ParseSkillFrontmatter extracts name, description, and slug from SKILL.md YAML frontmatter.
// Also returns the full parsed frontmatter as a map for DB storage.
func ParseSkillFrontmatter(content string) (name, description, slug string, allFields map[string]string) {
allFields = make(map[string]string)
if !strings.HasPrefix(content, "---") {
return "", "", "", allFields
}
end := strings.Index(content[3:], "---")
if end < 0 {
return "", "", "", allFields
}
fm := content[3 : 3+end]
allFields = parseSimpleYAML(fm)
name = allFields["name"]
description = allFields["description"]
slug = allFields["slug"]
return
}
// Slugify converts a skill name into a valid slug (lowercase, alphanumeric + hyphens).
func Slugify(name string) string {
s := strings.ToLower(name)
s = strings.Map(func(r rune) rune {
if (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') {
return r
}
return '-'
}, s)
// Collapse multiple dashes
for strings.Contains(s, "--") {
s = strings.ReplaceAll(s, "--", "-")
}
s = strings.Trim(s, "-")
if s == "" {
s = "skill"
}
return s
}
// IsSystemArtifact returns true for OS-generated junk that should be skipped
// during file extraction and listing (e.g. __MACOSX, .DS_Store, Thumbs.db).
func IsSystemArtifact(name string) bool {
base := filepath.Base(name)
// macOS resource fork / metadata folders and files
if base == "__MACOSX" || strings.HasPrefix(base, "._") {
return true
}
// Check if any path component is __MACOSX
for _, part := range strings.Split(filepath.ToSlash(name), "/") {
if part == "__MACOSX" {
return true
}
}
// Common OS junk files
switch base {
case ".DS_Store", "Thumbs.db", "desktop.ini", ".Spotlight-V100", ".Trashes", ".fseventsd":
return true
}
return false
}