File size: 3,770 Bytes
d2507b5 a2a3348 d2507b5 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 | package parser
import (
"fmt"
"os"
"strings"
"huggingface.co/turenlabs/Vigil/source/pkg/types"
)
// Parse reads a Claude Code skill markdown file and extracts frontmatter + body.
func Parse(filePath string) (*types.SkillFile, error) {
raw, err := os.ReadFile(filePath)
if err != nil {
return nil, fmt.Errorf("reading skill file: %w", err)
}
return ParseBytes(raw, filePath)
}
// ParseBytes parses skill file content from bytes.
func ParseBytes(raw []byte, filePath string) (*types.SkillFile, error) {
content := string(raw)
frontmatter, body, bodyStartLine, err := splitFrontmatter(content)
if err != nil {
return nil, fmt.Errorf("parsing frontmatter: %w", err)
}
sf := &types.SkillFile{
RawContent: content,
FilePath: filePath,
Body: body,
BodyStartLine: bodyStartLine,
}
if frontmatter != "" {
parseFrontmatterFields(frontmatter, sf)
}
return sf, nil
}
// splitFrontmatter splits a markdown file into YAML frontmatter and body.
// Frontmatter is delimited by --- on the first line and a closing ---.
// bodyStartLine is the 1-based line number where the body begins in the original content.
func splitFrontmatter(content string) (frontmatter, body string, bodyStartLine int, err error) {
trimmed := strings.TrimSpace(content)
if !strings.HasPrefix(trimmed, "---") {
return "", content, 1, nil
}
// Count leading blank lines that TrimSpace removed
leadingLines := 0
for i, ch := range content {
if ch == '\n' {
leadingLines++
} else if ch != ' ' && ch != '\t' && ch != '\r' {
_ = i
break
}
}
rest := trimmed[3:]
rest = strings.TrimLeft(rest, " \t")
if len(rest) > 0 && rest[0] == '\n' {
rest = rest[1:]
} else if len(rest) > 1 && rest[0] == '\r' && rest[1] == '\n' {
rest = rest[2:]
}
// Handle empty frontmatter: starts with closing ---
if strings.HasPrefix(rest, "---") {
body = strings.TrimLeft(rest[3:], " \t\r\n")
// Opening --- (1) + closing --- (1) + leadingLines
bodyStartLine = leadingLines + 3
return "", body, bodyStartLine, nil
}
idx := strings.Index(rest, "\n---")
if idx == -1 {
return "", content, 1, nil
}
frontmatter = rest[:idx]
body = strings.TrimLeft(rest[idx+4:], " \t\r\n")
// Count lines: leading blank + opening --- line + frontmatter lines + closing --- line
fmLines := strings.Count(frontmatter, "\n") + 1
bodyStartLine = leadingLines + 1 + fmLines + 1 // opening--- + fm lines + closing---
// Account for blank lines trimmed between closing --- and body start
afterClosing := rest[idx+4:]
for _, ch := range afterClosing {
if ch == '\n' {
bodyStartLine++
} else if ch != ' ' && ch != '\t' && ch != '\r' {
break
}
}
return frontmatter, body, bodyStartLine, nil
}
// parseFrontmatterFields extracts known fields from simple YAML frontmatter.
// Handles flat key: value pairs and simple list items (- value).
func parseFrontmatterFields(fm string, sf *types.SkillFile) {
lines := strings.Split(fm, "\n")
var currentKey string
for _, line := range lines {
trimmed := strings.TrimRight(line, " \t\r")
// List item under current key
if strings.HasPrefix(trimmed, " - ") || strings.HasPrefix(trimmed, " - ") {
val := strings.TrimSpace(trimmed[strings.Index(trimmed, "- ")+2:])
if currentKey == "triggers" {
sf.Triggers = append(sf.Triggers, val)
}
continue
}
// Key: value pair
colonIdx := strings.Index(trimmed, ":")
if colonIdx > 0 {
key := strings.TrimSpace(trimmed[:colonIdx])
val := strings.TrimSpace(trimmed[colonIdx+1:])
currentKey = key
switch key {
case "name":
sf.Name = val
case "description":
sf.Description = val
case "type":
sf.Type = val
case "triggers":
// Value comes on subsequent lines as list items
}
}
}
}
|