first commit
This commit is contained in:
@@ -0,0 +1,182 @@
|
||||
// This file implements the prompt-template expansion engine (US-002, #332): it
|
||||
// turns a template body plus tokenized invocation args into the final prompt
|
||||
// text, supporting pi's positional/default/slice syntax.
|
||||
//
|
||||
// Supported placeholders (mirrors https://pi.dev/docs/latest/prompt-templates):
|
||||
// - $1, $2, ... $N : Nth positional arg (1-indexed; out-of-range -> "")
|
||||
// - $@, $ARGUMENTS : all args joined by a single space
|
||||
// - ${1:-default} : arg 1 when present and non-empty, else `default`
|
||||
// - ${@:-default}, ${ARGUMENTS:-default} : all args when non-empty, else default
|
||||
// - ${@:N} : args from the Nth onward (1-indexed), joined
|
||||
// - ${@:N:L} : L args starting at N, joined
|
||||
//
|
||||
// A single left-to-right pass expands both braced ${...} and bare $N/$@ forms.
|
||||
// Because the pass consumes a ${...} as one unit, a bare $1 never matches inside
|
||||
// ${1:-...}, and a default literal containing $ is not re-expanded (the
|
||||
// substituted text is appended verbatim and the scan advances past it).
|
||||
package runtime
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ExpandTemplate expands template against the tokenized args. A template with no
|
||||
// placeholder preserves ParseUserCommand's behavior: with no args it is returned
|
||||
// verbatim, with args they are appended after a blank line (joined by spaces).
|
||||
func ExpandTemplate(template string, args []string) string {
|
||||
if !hasPlaceholder(template) {
|
||||
if len(args) == 0 {
|
||||
return template
|
||||
}
|
||||
return template + "\n\n" + strings.Join(args, " ")
|
||||
}
|
||||
var b strings.Builder
|
||||
i := 0
|
||||
n := len(template)
|
||||
for i < n {
|
||||
c := template[i]
|
||||
if c == '$' && i+1 < n {
|
||||
next := template[i+1]
|
||||
if next == '{' {
|
||||
end := strings.IndexByte(template[i+2:], '}')
|
||||
if end < 0 {
|
||||
// No closing brace: emit the '$' literally and continue.
|
||||
b.WriteByte(c)
|
||||
i++
|
||||
continue
|
||||
}
|
||||
inner := template[i+2 : i+2+end]
|
||||
b.WriteString(expandBraced(inner, args))
|
||||
i += 2 + end + 1
|
||||
continue
|
||||
}
|
||||
if next == '@' {
|
||||
b.WriteString(strings.Join(args, " "))
|
||||
i += 2
|
||||
continue
|
||||
}
|
||||
if isDigitByte(next) {
|
||||
j := i + 1
|
||||
for j < n && isDigitByte(template[j]) {
|
||||
j++
|
||||
}
|
||||
idx, _ := strconv.Atoi(template[i+1 : j])
|
||||
if idx >= 1 && idx <= len(args) {
|
||||
b.WriteString(args[idx-1])
|
||||
}
|
||||
i = j
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(template[i+1:], "ARGUMENTS") {
|
||||
b.WriteString(strings.Join(args, " "))
|
||||
i += 1 + len("ARGUMENTS")
|
||||
continue
|
||||
}
|
||||
}
|
||||
b.WriteByte(c)
|
||||
i++
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// hasPlaceholder reports whether s contains any template placeholder: ${...},
|
||||
// $@, $<digits>, or $ARGUMENTS. A bare '$' not followed by one of these is not a
|
||||
// placeholder (emitted literally), so a template like "price $5" still counts.
|
||||
func hasPlaceholder(s string) bool {
|
||||
for i := 0; i < len(s); i++ {
|
||||
if s[i] != '$' {
|
||||
continue
|
||||
}
|
||||
if i+1 >= len(s) {
|
||||
return false
|
||||
}
|
||||
next := s[i+1]
|
||||
if next == '{' || next == '@' || isDigitByte(next) {
|
||||
return true
|
||||
}
|
||||
if strings.HasPrefix(s[i+1:], "ARGUMENTS") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// expandBraced expands the content inside ${...} (without the surrounding
|
||||
// braces). It dispatches on the three forms: name:-default, @:N[:L], or plain.
|
||||
func expandBraced(inner string, args []string) string {
|
||||
// default form: name:-default
|
||||
if idx := strings.Index(inner, ":-"); idx >= 0 {
|
||||
return expandDefaulted(inner[:idx], inner[idx+2:], args)
|
||||
}
|
||||
// slice form: @:N or @:N:L
|
||||
if idx := strings.Index(inner, ":"); idx >= 0 {
|
||||
if name := inner[:idx]; name != "@" && name != "ARGUMENTS" {
|
||||
return "" // slicing only applies to all-args
|
||||
}
|
||||
return expandSlice(inner[idx+1:], args)
|
||||
}
|
||||
// plain: N, @, or ARGUMENTS
|
||||
return expandPlain(inner, args)
|
||||
}
|
||||
|
||||
// expandPlain expands a bare braced name (no :- or :): a positional index, @,
|
||||
// or ARGUMENTS. An unknown name (e.g. ${foo}) expands to "".
|
||||
func expandPlain(name string, args []string) string {
|
||||
if name == "@" || name == "ARGUMENTS" {
|
||||
return strings.Join(args, " ")
|
||||
}
|
||||
if idx, err := strconv.Atoi(name); err == nil {
|
||||
if idx >= 1 && idx <= len(args) {
|
||||
return args[idx-1]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// expandDefaulted expands name:-default: the named positional (when in range and
|
||||
// non-empty) or all-args (when the join is non-empty), otherwise the literal
|
||||
// default. The default is not re-expanded.
|
||||
func expandDefaulted(name, def string, args []string) string {
|
||||
if name == "@" || name == "ARGUMENTS" {
|
||||
if joined := strings.Join(args, " "); joined != "" {
|
||||
return joined
|
||||
}
|
||||
return def
|
||||
}
|
||||
if idx, err := strconv.Atoi(name); err == nil {
|
||||
if idx >= 1 && idx <= len(args) && args[idx-1] != "" {
|
||||
return args[idx-1]
|
||||
}
|
||||
return def
|
||||
}
|
||||
return def
|
||||
}
|
||||
|
||||
// expandSlice expands the N (or N:L) part of ${@:N} / ${@:N:L}: args from the Nth
|
||||
// onward (1-indexed), optionally limited to L, joined by spaces. N<1 or beyond
|
||||
// the arg list yields "".
|
||||
func expandSlice(rest string, args []string) string {
|
||||
parts := strings.SplitN(rest, ":", 2)
|
||||
start, _ := strconv.Atoi(parts[0])
|
||||
if start < 1 {
|
||||
return ""
|
||||
}
|
||||
begin := start - 1
|
||||
if begin >= len(args) {
|
||||
return ""
|
||||
}
|
||||
end := len(args)
|
||||
if len(parts) == 2 {
|
||||
if l, err := strconv.Atoi(parts[1]); err == nil && l >= 0 {
|
||||
end = begin + l
|
||||
if end > len(args) {
|
||||
end = len(args)
|
||||
}
|
||||
}
|
||||
}
|
||||
return strings.Join(args[begin:end], " ")
|
||||
}
|
||||
|
||||
func isDigitByte(b byte) bool { return b >= '0' && b <= '9' }
|
||||
Reference in New Issue
Block a user