Unify the two {{variable}} template engines

internal/skills and pkg/kit/template_bridge each had their own grammar:
skills rejected '{{ name }}' (whitespace) but allowed digit-first names;
the bridge was the opposite. A template behaved differently depending on
whether it was loaded as a skill prompt or via the extension API.

internal/skills is now the single engine using the superset grammar
(\{\{\s*(\w+)\s*\}\}); pkg/kit ParseTemplate/RenderTemplate are thin
adapters over it. Expand is now regex-based so whitespace placeholders
expand consistently; missing variables are still left as-is.
This commit is contained in:
Ed Zynda
2026-06-11 14:51:52 +03:00
parent dec04954e1
commit e2f92b9515
2 changed files with 25 additions and 31 deletions
+12 -7
View File
@@ -18,8 +18,11 @@ type PromptTemplate struct {
Variables []string
}
// variableRe matches {{variable_name}} placeholders.
var variableRe = regexp.MustCompile(`\{\{(\w+)\}\}`)
// variableRe matches {{variable_name}} placeholders, tolerating surrounding
// whitespace inside the braces (e.g. {{ name }}). This is the canonical
// template grammar shared by skill prompts and the extension template API
// (pkg/kit ParseTemplate/RenderTemplate delegate here).
var variableRe = regexp.MustCompile(`\{\{\s*(\w+)\s*\}\}`)
// NewPromptTemplate creates a PromptTemplate, automatically extracting
// variable names from {{...}} placeholders in content.
@@ -50,11 +53,13 @@ func LoadPromptTemplate(path string) (*PromptTemplate, error) {
// Expand replaces all {{variable}} placeholders with values from the
// provided map. Missing variables are left as-is (no error).
func (t *PromptTemplate) Expand(values map[string]string) string {
result := t.Content
for k, v := range values {
result = strings.ReplaceAll(result, "{{"+k+"}}", v)
}
return result
return variableRe.ReplaceAllStringFunc(t.Content, func(m string) string {
name := variableRe.FindStringSubmatch(m)[1]
if v, ok := values[name]; ok {
return v
}
return m
})
}
// ExpandStrict replaces all {{variable}} placeholders and returns an error
+13 -24
View File
@@ -8,45 +8,34 @@ import (
"github.com/mark3labs/kit/internal/extensions"
"github.com/mark3labs/kit/internal/models"
"github.com/mark3labs/kit/internal/prompts"
"github.com/mark3labs/kit/internal/skills"
)
// ---------------------------------------------------------------------------
// Template Parsing Bridge for Extensions (Phase 3)
// ---------------------------------------------------------------------------
// varRegex matches {{variable}} placeholders in templates.
var varRegex = regexp.MustCompile(`\{\{\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*\}\}`)
// ParseTemplate extracts {{variables}} from template content.
// ParseTemplate extracts {{variables}} from template content. The template
// grammar is shared with skill prompt templates (see internal/skills).
func ParseTemplate(name, content string) extensions.PromptTemplate {
matches := varRegex.FindAllStringSubmatch(content, -1)
vars := make([]string, 0, len(matches))
seen := make(map[string]bool)
for _, m := range matches {
if len(m) > 1 && !seen[m[1]] {
seen[m[1]] = true
vars = append(vars, m[1])
}
tpl := skills.NewPromptTemplate(name, content)
vars := tpl.Variables
if vars == nil {
vars = []string{}
}
return extensions.PromptTemplate{
Name: name,
Content: content,
Name: tpl.Name,
Content: tpl.Content,
Variables: vars,
}
}
// RenderTemplate substitutes variables into template content.
// Handles {{name}} and {{ name }} (any whitespace) placeholders.
// Handles {{name}} and {{ name }} (any whitespace) placeholders; missing
// variables are left as-is.
func RenderTemplate(tpl extensions.PromptTemplate, vars map[string]string) string {
return varRegex.ReplaceAllStringFunc(tpl.Content, func(m string) string {
sub := varRegex.FindStringSubmatch(m)
if len(sub) > 1 {
if v, ok := vars[sub[1]]; ok {
return v
}
}
return m
})
t := skills.PromptTemplate{Content: tpl.Content}
return t.Expand(vars)
}
// ParseArguments parses command-line style arguments.