From 5a57d6909dcbef313b8cddfdc0b3b95d184d424d Mon Sep 17 00:00:00 2001 From: Jan De Dobbeleer Date: Wed, 12 Oct 2022 20:02:58 +0200 Subject: [PATCH] fix(template): parse temnplates correctly resolves #2928 --- src/template/text.go | 90 ++++++++++++++++++++++++++++----------- src/template/text_test.go | 72 ++++++++++++++++++++++++++++--- 2 files changed, 133 insertions(+), 29 deletions(-) diff --git a/src/template/text.go b/src/template/text.go index 61a6e615..4c03f761 100644 --- a/src/template/text.go +++ b/src/template/text.go @@ -3,7 +3,6 @@ package template import ( "bytes" "errors" - "fmt" "oh-my-posh/environment" "oh-my-posh/regex" "strings" @@ -66,21 +65,6 @@ func (t *Text) Render() (string, error) { } func (t *Text) cleanTemplate() { - unknownVariable := func(variable string, knownVariables *[]string) (string, bool) { - variable = strings.TrimPrefix(variable, ".") - splitted := strings.Split(variable, ".") - if len(splitted) == 0 { - return "", false - } - for _, b := range *knownVariables { - if b == splitted[0] { - return "", false - } - } - *knownVariables = append(*knownVariables, splitted[0]) - return splitted[0], true - } - knownVariables := []string{ "Root", "PWD", @@ -97,14 +81,72 @@ func (t *Text) cleanTemplate() { "Segments", "Templates", } - matches := regex.FindAllNamedRegexMatch(`(?: |{|\()(?P(\.[a-zA-Z_][a-zA-Z0-9]*)+)`, t.Template) - for _, match := range matches { - if variable, OK := unknownVariable(match["VAR"], &knownVariables); OK { - pattern := fmt.Sprintf(`\.%s\b`, variable) - dataVar := fmt.Sprintf(".Data.%s", variable) - t.Template = regex.ReplaceAllString(pattern, t.Template, dataVar) + + knownVariable := func(variable string) bool { + variable = strings.TrimPrefix(variable, ".") + splitted := strings.Split(variable, ".") + if len(splitted) == 0 { + return false } + variable = splitted[0] + for _, b := range knownVariables { + if variable == b { + return true + } + } + return false + } + + walkAndReplace := func(node string) string { + var result string + var property string + var inProperty bool + // var literal bool + for _, char := range node { + switch char { + case '.': + var lastChar rune + if len(result) > 0 { + lastChar = rune(result[len(result)-1]) + } + // only replace if we're in a valid property start + // with a space, { or ( character + switch lastChar { + case ' ', '{', '(': + property += string(char) + inProperty = true + default: + result += string(char) + } + case ' ', '}', ')': // space or } + if !inProperty { + result += string(char) + continue + } + // end of a variable, needs to be appended + if !knownVariable(property) { + result += ".Data" + property + } else { + result += property + } + property = "" + result += string(char) + inProperty = false + default: + if inProperty { + property += string(char) + continue + } + result += string(char) + } + } + return result + } + + // matches := regex.FindAllNamedRegexMatch(`(?: |{|\()(?P(\.[a-zA-Z_][a-zA-Z0-9]*)+)`, t.Template) + matches := regex.FindAllNamedRegexMatch(`(?P{{[^{]+}})`, t.Template) + for _, match := range matches { + node := walkAndReplace(match["NODE"]) + t.Template = strings.Replace(t.Template, match["NODE"], node, 1) } - // allow literal dots in template - t.Template = strings.ReplaceAll(t.Template, `\.`, ".") } diff --git a/src/template/text_test.go b/src/template/text_test.go index c5dd1744..55bc8026 100644 --- a/src/template/text_test.go +++ b/src/template/text_test.go @@ -10,6 +10,9 @@ import ( ) func TestRenderTemplate(t *testing.T) { + type Me struct { + Name string + } cases := []struct { Case string Expected string @@ -17,11 +20,68 @@ func TestRenderTemplate(t *testing.T) { ShouldError bool Context interface{} }{ - {Case: "single property", Expected: "hello world", Template: "{{.Text}} world", Context: struct{ Text string }{Text: "hello"}}, - {Case: "invalid property", ShouldError: true, Template: "{{.Durp}} world", Context: struct{ Text string }{Text: "hello"}}, - {Case: "invalid template", ShouldError: true, Template: "{{ if .Text }} world", Context: struct{ Text string }{Text: "hello"}}, - {Case: "if statement true", Expected: "hello world", Template: "{{ if .Text }}{{.Text}} world{{end}}", Context: struct{ Text string }{Text: "hello"}}, - {Case: "if statement false", Expected: "world", Template: "{{ if .Text }}{{.Text}} {{end}}world", Context: struct{ Text string }{Text: ""}}, + { + Case: "Env like property name", + Expected: "hello world", + Template: "{{.EnvLike}} {{.Text2}}", + Context: struct { + EnvLike string + Text2 string + }{ + EnvLike: "hello", + Text2: "world", + }, + }, + { + Case: "single property with a dot literal", + Expected: "hello world", + Template: "{{ if eq .Text \".Net\" }}hello world{{ end }}", + Context: struct{ Text string }{Text: ".Net"}, + }, + { + Case: "single property", + Expected: "hello world", + Template: "{{.Text}} world", + Context: struct{ Text string }{Text: "hello"}, + }, + { + Case: "duplicate property", + Expected: "hello jan posh", + Template: "hello {{ .Me.Name }} {{ .Name }}", + Context: struct { + Name string + Me Me + }{ + Name: "posh", + Me: Me{ + Name: "jan", + }, + }, + }, + { + Case: "invalid property", + ShouldError: true, + Template: "{{.Durp}} world", + Context: struct{ Text string }{Text: "hello"}, + }, + { + Case: "invalid template", + ShouldError: true, + Template: "{{ if .Text }} world", + Context: struct{ Text string }{Text: "hello"}, + }, + { + Case: "if statement true", + Expected: "hello world", + Template: "{{ if .Text }}{{.Text}} world{{end}}", + Context: struct{ Text string }{Text: "hello"}, + }, + { + Case: "if statement false", + Expected: "world", + Template: "{{ if .Text }}{{.Text}} {{end}}world", + Context: struct{ Text string }{Text: ""}, + }, { Case: "if statement true with 2 properties", Expected: "hello world", @@ -86,6 +146,8 @@ func TestRenderTemplate(t *testing.T) { if tc.ShouldError { assert.Error(t, err) continue + } else { + assert.NoError(t, err) } assert.Equal(t, tc.Expected, text, tc.Case) }