From e2f92b951528421bc3737f84bbaada8918e499a0 Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Thu, 11 Jun 2026 14:51:52 +0300 Subject: [PATCH] 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. --- internal/skills/templates.go | 19 +++++++++++------- pkg/kit/template_bridge.go | 37 +++++++++++++----------------------- 2 files changed, 25 insertions(+), 31 deletions(-) diff --git a/internal/skills/templates.go b/internal/skills/templates.go index 7902d662..b3c5c963 100644 --- a/internal/skills/templates.go +++ b/internal/skills/templates.go @@ -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 diff --git a/pkg/kit/template_bridge.go b/pkg/kit/template_bridge.go index 9373dcf3..048c1a15 100644 --- a/pkg/kit/template_bridge.go +++ b/pkg/kit/template_bridge.go @@ -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.