package preproc import ( "strings" "unicode/utf8" "github.com/armon/go-radix" ) type DefineList struct { tree *radix.Tree } func NewDefineList() *DefineList { return &DefineList{tree: radix.New()} } // Add inserts or updates a define->value mapping. func (d *DefineList) Add(define, value string) { d.tree.Insert(define, expandDollarEscapes(value)) } // Delete removes a define. Returns true if it existed. func (d *DefineList) Delete(define string) bool { _, ok := d.tree.Delete(define) return ok } // Defined reports whether a define exists. func (d *DefineList) Defined(s string) bool { _, ok := d.tree.Get(s) return ok } // typed wrapper to avoid type assertions sprinkled in hot code func (d *DefineList) longestPrefixString(s string) (key string, val string, ok bool) { k, v, ok := d.tree.LongestPrefix(s) if !ok { return "", "", false } return k, v.(string), true } // ReplaceDefines performs a single-pass, longest-prefix replacement. // Case-sensitive. Matches anywhere. No recursive re-expansion. func (d *DefineList) ReplaceDefines(s string) string { var b strings.Builder b.Grow(len(s)) // baseline; grows if expansions occur for i := 0; i < len(s); { if key, val, ok := d.longestPrefixString(s[i:]); ok && key != "" { b.WriteString(val) i += len(key) // advance by matched bytes (UTF-8-safe) continue } _, size := utf8.DecodeRuneInString(s[i:]) b.WriteString(s[i : i+size]) i += size } return b.String() } func isHex(c byte) bool { return (c >= '0' && c <= '9') || (c >= 'A' && c <= 'F') || (c >= 'a' && c <= 'f') } func hexToByte(hi, lo byte) byte { return hexDigit(hi)<<4 | hexDigit(lo) } func hexDigit(c byte) byte { if c >= '0' && c <= '9' { return c - '0' } if c >= 'A' && c <= 'F' { return c - 'A' + 10 } if c >= 'a' && c <= 'f' { return c - 'a' + 10 } return 0 } func expandDollarEscapes(s string) string { var b strings.Builder for i := 0; i < len(s); i++ { if s[i] == '$' { if i+1 < len(s) && s[i+1] == '$' { // $$ -> $ b.WriteByte('$') i++ // skip second $ } else if i+2 < len(s) && isHex(s[i+1]) && isHex(s[i+2]) { // $XX -> character val := hexToByte(s[i+1], s[i+2]) b.WriteByte(val) i += 2 // skip hex digits } else { b.WriteByte('$') } } else { b.WriteByte(s[i]) } } return b.String() }