mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-14 18:18:37 +00:00
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:
2 files changed
+68
-33
No files matched your search
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user