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>
This commit is contained in:
authored and GitHub committed 2026-03-21 08:58:21 +07:00
1 parent f3b344d731
commit 394ded7836
2 files changed
+68 -33

No files matched your search

+4 -24
View File
@@ -21,30 +21,10 @@ func ParseSkillFrontmatter(content string) (name, description, slug string, allF
return "", "", "", allFields
}
fm := content[3 : 3+end]
for _, line := range strings.Split(fm, "\n") {
line = strings.TrimSpace(line)
if line == "" || strings.HasPrefix(line, "#") {
continue
}
parts := strings.SplitN(line, ":", 2)
if len(parts) != 2 {
continue
}
key := strings.TrimSpace(parts[0])
val := strings.TrimSpace(parts[1])
val = strings.Trim(val, `"'`)
allFields[key] = val
switch key {
case "name":
name = val
case "description":
description = val
case "slug":
slug = val
}
}
allFields = parseSimpleYAML(fm)
name = allFields["name"]
description = allFields["description"]
slug = allFields["slug"]
return
}
+64 -9
View File
@@ -504,21 +504,76 @@ func stripFrontmatter(content string) string {
return frontmatterRe.ReplaceAllString(normalizeLineEndings(content), "")
}
// parseSimpleYAML parses a subset of YAML: simple key: value pairs,
// multiline block scalars (| and >), and list values (- item).
func parseSimpleYAML(content string) map[string]string {
result := make(map[string]string)
for line := range strings.SplitSeq(content, "\n") {
line = strings.TrimSpace(line)
if line == "" || strings.HasPrefix(line, "#") {
lines := strings.Split(content, "\n")
var currentKey string
var blockLines []string
var inBlock bool
flushBlock := func() {
if currentKey != "" {
if len(blockLines) > 0 {
result[currentKey] = strings.Join(blockLines, " ")
} else {
// Empty value (e.g. "slug:" with no indented continuation).
result[currentKey] = ""
}
}
currentKey = ""
blockLines = nil
inBlock = false
}
for _, line := range lines {
// Indented continuation line (block scalar or list item)
if inBlock && len(line) > 0 && (line[0] == ' ' || line[0] == '\t') {
trimmed := strings.TrimSpace(line)
if trimmed == "" {
continue
}
// List item: " - value"
if strings.HasPrefix(trimmed, "- ") {
blockLines = append(blockLines, strings.TrimSpace(trimmed[2:]))
} else if trimmed != "-" {
blockLines = append(blockLines, trimmed)
}
continue
}
parts := strings.SplitN(line, ":", 2)
if len(parts) == 2 {
key := strings.TrimSpace(parts[0])
val := strings.TrimSpace(parts[1])
val = strings.Trim(val, "\"'")
result[key] = val
// Not indented — flush any pending block and parse as top-level key
flushBlock()
trimmed := strings.TrimSpace(line)
if trimmed == "" || strings.HasPrefix(trimmed, "#") {
continue
}
parts := strings.SplitN(trimmed, ":", 2)
if len(parts) != 2 {
continue
}
key := strings.TrimSpace(parts[0])
val := strings.TrimSpace(parts[1])
val = strings.Trim(val, "\"'")
if val == "|" || val == ">" || val == "|-" || val == ">-" {
// Start of a multiline block — collect subsequent indented lines
currentKey = key
inBlock = true
continue
}
if val == "" {
// Could be start of a list block (e.g. "allowed-tools:\n - Bash")
currentKey = key
inBlock = true
continue
}
result[key] = val
}
flushBlock()
return result
}