fix(template): parse temnplates correctly

resolves #2928
This commit is contained in:
Jan De Dobbeleer 2022-10-12 20:02:58 +02:00 committed by Jan De Dobbeleer
parent 6980cb55e7
commit 5a57d6909d
2 changed files with 133 additions and 29 deletions

View file

@ -3,7 +3,6 @@ package template
import ( import (
"bytes" "bytes"
"errors" "errors"
"fmt"
"oh-my-posh/environment" "oh-my-posh/environment"
"oh-my-posh/regex" "oh-my-posh/regex"
"strings" "strings"
@ -66,21 +65,6 @@ func (t *Text) Render() (string, error) {
} }
func (t *Text) cleanTemplate() { 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{ knownVariables := []string{
"Root", "Root",
"PWD", "PWD",
@ -97,14 +81,72 @@ func (t *Text) cleanTemplate() {
"Segments", "Segments",
"Templates", "Templates",
} }
matches := regex.FindAllNamedRegexMatch(`(?: |{|\()(?P<VAR>(\.[a-zA-Z_][a-zA-Z0-9]*)+)`, t.Template)
for _, match := range matches { knownVariable := func(variable string) bool {
if variable, OK := unknownVariable(match["VAR"], &knownVariables); OK { variable = strings.TrimPrefix(variable, ".")
pattern := fmt.Sprintf(`\.%s\b`, variable) splitted := strings.Split(variable, ".")
dataVar := fmt.Sprintf(".Data.%s", variable) if len(splitted) == 0 {
t.Template = regex.ReplaceAllString(pattern, t.Template, dataVar) 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<VAR>(\.[a-zA-Z_][a-zA-Z0-9]*)+)`, t.Template)
matches := regex.FindAllNamedRegexMatch(`(?P<NODE>{{[^{]+}})`, 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, `\.`, ".")
} }

View file

@ -10,6 +10,9 @@ import (
) )
func TestRenderTemplate(t *testing.T) { func TestRenderTemplate(t *testing.T) {
type Me struct {
Name string
}
cases := []struct { cases := []struct {
Case string Case string
Expected string Expected string
@ -17,11 +20,68 @@ func TestRenderTemplate(t *testing.T) {
ShouldError bool ShouldError bool
Context interface{} 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: "Env like property name",
{Case: "invalid template", ShouldError: true, Template: "{{ if .Text }} world", Context: struct{ Text string }{Text: "hello"}}, Expected: "hello world",
{Case: "if statement true", Expected: "hello world", Template: "{{ if .Text }}{{.Text}} world{{end}}", Context: struct{ Text string }{Text: "hello"}}, Template: "{{.EnvLike}} {{.Text2}}",
{Case: "if statement false", Expected: "world", Template: "{{ if .Text }}{{.Text}} {{end}}world", Context: struct{ Text string }{Text: ""}}, 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", Case: "if statement true with 2 properties",
Expected: "hello world", Expected: "hello world",
@ -86,6 +146,8 @@ func TestRenderTemplate(t *testing.T) {
if tc.ShouldError { if tc.ShouldError {
assert.Error(t, err) assert.Error(t, err)
continue continue
} else {
assert.NoError(t, err)
} }
assert.Equal(t, tc.Expected, text, tc.Case) assert.Equal(t, tc.Expected, text, tc.Case)
} }