diff --git a/replacer.go b/replacer.go index aad13e2a..88234046 100644 --- a/replacer.go +++ b/replacer.go @@ -125,6 +125,14 @@ func (r *Replacer) replace(input, empty string, // iterate the input to find each placeholder var lastWriteCursor int for i := 0; i < len(input); i++ { + + // check for escaped braces + if i > 0 && input[i-1] == phEscape && (input[i] == phClose || input[i] == phOpen) { + sb.WriteString(input[lastWriteCursor : i-1]) + lastWriteCursor = i + continue + } + if input[i] != phOpen { continue } @@ -135,6 +143,11 @@ func (r *Replacer) replace(input, empty string, continue } + // if necessary look for the first closing brace that is not escaped + for end > 0 && end < len(input)-1 && input[end-1] == phEscape { + end = strings.Index(input[end+1:], string(phClose)) + end + 1 + } + // write the substring from the last cursor to this point sb.WriteString(input[lastWriteCursor:i]) @@ -237,4 +250,4 @@ var nowFunc = time.Now // ReplacerCtxKey is the context key for a replacer. const ReplacerCtxKey CtxKey = "replacer" -const phOpen, phClose = '{', '}' +const phOpen, phClose, phEscape = '{', '}', '\\' diff --git a/replacer_test.go b/replacer_test.go index 4b561945..66bb5379 100644 --- a/replacer_test.go +++ b/replacer_test.go @@ -35,30 +35,86 @@ func TestReplacer(t *testing.T) { input: "{", expect: "{", }, + { + input: `\{`, + expect: `{`, + }, { input: "foo{", expect: "foo{", }, + { + input: `foo\{`, + expect: `foo{`, + }, { input: "foo{bar", expect: "foo{bar", }, + { + input: `foo\{bar`, + expect: `foo{bar`, + }, { input: "foo{bar}", expect: "foo", }, + { + input: `foo\{bar\}`, + expect: `foo{bar}`, + }, { input: "}", expect: "}", }, + { + input: `\}`, + expect: `\}`, + }, { input: "{}", expect: "", }, + { + input: `\{\}`, + expect: `{}`, + }, { input: `{"json": "object"}`, expect: "", }, + { + input: `\{"json": "object"}`, + expect: `{"json": "object"}`, + }, + { + input: `\{"json": "object"\}`, + expect: `{"json": "object"}`, + }, + { + input: `\{"json": "object{bar}"\}`, + expect: `{"json": "object"}`, + }, + { + input: `\{"json": \{"nested": "object"\}\}`, + expect: `{"json": {"nested": "object"}}`, + }, + { + input: `\{"json": \{"nested": "{bar}"\}\}`, + expect: `{"json": {"nested": ""}}`, + }, + { + input: `pre \{"json": \{"nested": "{bar}"\}\}`, + expect: `pre {"json": {"nested": ""}}`, + }, + { + input: `\{"json": \{"nested": "{bar}"\}\} post`, + expect: `{"json": {"nested": ""}} post`, + }, + { + input: `pre \{"json": \{"nested": "{bar}"\}\} post`, + expect: `pre {"json": {"nested": ""}} post`, + }, { input: `{{`, expect: "{{", @@ -67,11 +123,39 @@ func TestReplacer(t *testing.T) { input: `{{}`, expect: "", }, + { + input: `{"json": "object"\}`, + expect: "", + }, { input: `{unknown}`, empty: "-", expect: "-", }, + { + input: `back\slashes`, + expect: `back\slashes`, + }, + { + input: `double back\\slashes`, + expect: `double back\\slashes`, + }, + { + input: `placeholder {with \{ brace} in name`, + expect: `placeholder in name`, + }, + { + input: `placeholder {with \} brace} in name`, + expect: `placeholder in name`, + }, + { + input: `placeholder {with \} \} braces} in name`, + expect: `placeholder in name`, + }, + { + input: `\{'group':'default','max_age':3600,'endpoints':[\{'url':'https://some.domain.local/a/d/g'\}],'include_subdomains':true\}`, + expect: `{'group':'default','max_age':3600,'endpoints':[{'url':'https://some.domain.local/a/d/g'}],'include_subdomains':true}`, + }, } { actual := rep.ReplaceAll(tc.input, tc.empty) if actual != tc.expect { @@ -81,6 +165,35 @@ func TestReplacer(t *testing.T) { } } +func BenchmarkReplacer(b *testing.B) { + type testCase struct { + name, input, empty string + } + + rep := testReplacer() + + for _, bm := range []testCase{ + { + name: "no placeholder", + input: `simple string`, + }, + { + name: "placeholder", + input: `{"json": "object"}`, + }, + { + name: "escaped placeholder", + input: `\{"json": \{"nested": "{bar}"\}\}`, + }, + } { + b.Run(bm.name, func(b *testing.B) { + for i := 0; i < b.N; i++ { + rep.ReplaceAll(bm.input, bm.empty) + } + }) + } +} + func TestReplacerSet(t *testing.T) { rep := testReplacer()