feat(skill): template rendering with Go text/template
This commit is contained in:
43
internal/skill/template.go
Normal file
43
internal/skill/template.go
Normal file
@@ -0,0 +1,43 @@
|
||||
package skill
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"strings"
|
||||
"text/template"
|
||||
)
|
||||
|
||||
// TemplateData holds the variables available in skill body templates.
|
||||
type TemplateData struct {
|
||||
Args string // raw user arguments after the skill name
|
||||
Cwd string // current working directory
|
||||
ProjectRoot string // detected project root
|
||||
}
|
||||
|
||||
// Render executes the skill body as a Go text/template with data.
|
||||
// If the body contains no template directives and Args is non-empty,
|
||||
// args are appended after the body with a blank line separator.
|
||||
func (s *Skill) Render(data TemplateData) (string, error) {
|
||||
t, err := template.New(s.Frontmatter.Name).Parse(s.Body)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("skill %q: template parse error: %w", s.Frontmatter.Name, err)
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
if err := t.Execute(&buf, data); err != nil {
|
||||
return "", fmt.Errorf("skill %q: template execute error: %w", s.Frontmatter.Name, err)
|
||||
}
|
||||
|
||||
rendered := buf.String()
|
||||
|
||||
// If the body contained no template directives, the rendered output equals
|
||||
// the original body. In that case, append args (if any) after a blank line.
|
||||
if !strings.Contains(s.Body, "{{") && data.Args != "" {
|
||||
if rendered == "" {
|
||||
return data.Args, nil
|
||||
}
|
||||
return rendered + "\n" + data.Args, nil
|
||||
}
|
||||
|
||||
return rendered, nil
|
||||
}
|
||||
86
internal/skill/template_test.go
Normal file
86
internal/skill/template_test.go
Normal file
@@ -0,0 +1,86 @@
|
||||
package skill
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func skillWithBody(body string) *Skill {
|
||||
return &Skill{
|
||||
Frontmatter: Frontmatter{Name: "test"},
|
||||
Body: body,
|
||||
}
|
||||
}
|
||||
|
||||
func TestRender_ArgsSubstituted(t *testing.T) {
|
||||
s := skillWithBody("Please do: {{.Args}}")
|
||||
out, err := s.Render(TemplateData{Args: "fix the tests"})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if out != "Please do: fix the tests" {
|
||||
t.Errorf("output = %q", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRender_AllVariables(t *testing.T) {
|
||||
s := skillWithBody("Cwd={{.Cwd}} Root={{.ProjectRoot}} Args={{.Args}}")
|
||||
out, err := s.Render(TemplateData{Args: "a", Cwd: "/tmp", ProjectRoot: "/proj"})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if out != "Cwd=/tmp Root=/proj Args=a" {
|
||||
t.Errorf("output = %q", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRender_NoDirectives_ArgsAppended(t *testing.T) {
|
||||
s := skillWithBody("Refactor all the things.\n")
|
||||
out, err := s.Render(TemplateData{Args: "focus on error handling"})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if !strings.Contains(out, "Refactor all the things.") {
|
||||
t.Errorf("body missing from output: %q", out)
|
||||
}
|
||||
if !strings.Contains(out, "focus on error handling") {
|
||||
t.Errorf("args missing from output: %q", out)
|
||||
}
|
||||
// args must follow body with blank line separator
|
||||
if !strings.Contains(out, "Refactor all the things.\n\n\nfocus on error handling") &&
|
||||
!strings.Contains(out, "Refactor all the things.\n\nfocus on error handling") {
|
||||
t.Errorf("unexpected separator in output: %q", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRender_NoDirectives_NoArgs(t *testing.T) {
|
||||
body := "Just the body.\n"
|
||||
s := skillWithBody(body)
|
||||
out, err := s.Render(TemplateData{})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if out != body {
|
||||
t.Errorf("output = %q, want %q", out, body)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRender_InvalidTemplate(t *testing.T) {
|
||||
s := skillWithBody("{{.Unclosed")
|
||||
_, err := s.Render(TemplateData{})
|
||||
if err == nil {
|
||||
t.Error("expected error for invalid template syntax")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRender_EmptyBody_WithArgs(t *testing.T) {
|
||||
s := skillWithBody("")
|
||||
out, err := s.Render(TemplateData{Args: "do something"})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
// Empty body + args → just the args
|
||||
if out != "do something" {
|
||||
t.Errorf("output = %q, want %q", out, "do something")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user