Skip to content

Commit a53e36c

Browse files
authored
Merge pull request #14 from choria-io/13
(#13) Add color markup support for form descriptions
2 parents 8312afb + e6b3664 commit a53e36c

File tree

6 files changed

+216
-5
lines changed

6 files changed

+216
-5
lines changed

forms/forms.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ func (p *Property) RenderedDescription(env map[string]any) (string, error) {
6363
return "", err
6464
}
6565

66-
return buffer.String(), nil
66+
return colorMarkup(buffer.String()), nil
6767
}
6868

6969
type processor struct {

forms/util.go

Lines changed: 95 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,13 @@ package forms
66

77
import (
88
"bytes"
9-
"github.com/choria-io/scaffold/internal/sprig"
109
"os"
10+
"strings"
1111
"text/template"
1212

13+
"github.com/choria-io/scaffold/internal/sprig"
14+
"github.com/jedib0t/go-pretty/v6/text"
15+
1316
"github.com/AlecAivazis/survey/v2"
1417
terminal "golang.org/x/term"
1518
)
@@ -61,5 +64,95 @@ func renderTemplate(tmpl string, env map[string]any) (string, error) {
6164
return "", err
6265
}
6366

64-
return out.String(), nil
67+
return colorMarkup(out.String()), nil
68+
}
69+
70+
// colorMarkup parses a string with color markup tags and returns a colorized string.
71+
// Supports tags like {red}text{/red}, {blue}text{/blue}, etc.
72+
// Supports all colors available in the go-pretty/text package.
73+
// Supports nested and multiple color tags in a single string.
74+
func colorMarkup(input string) string {
75+
colorMap := map[string]text.Color{
76+
"bold": text.Bold,
77+
"black": text.FgBlack,
78+
"red": text.FgRed,
79+
"green": text.FgGreen,
80+
"yellow": text.FgYellow,
81+
"blue": text.FgBlue,
82+
"magenta": text.FgMagenta,
83+
"cyan": text.FgCyan,
84+
"white": text.FgWhite,
85+
"hiblack": text.FgHiBlack,
86+
"hired": text.FgHiRed,
87+
"higreen": text.FgHiGreen,
88+
"hiyellow": text.FgHiYellow,
89+
"hiblue": text.FgHiBlue,
90+
"himagenta": text.FgHiMagenta,
91+
"hicyan": text.FgHiCyan,
92+
"hiwhite": text.FgHiWhite,
93+
}
94+
95+
// Process innermost tags first to handle nesting properly
96+
result := input
97+
for {
98+
changed := false
99+
100+
// Find innermost color tag (one that doesn't contain other opening tags)
101+
for i := 0; i < len(result); i++ {
102+
if result[i] != '{' {
103+
continue
104+
}
105+
106+
// Find the end of this opening tag
107+
closePos := strings.Index(result[i:], "}")
108+
if closePos == -1 {
109+
continue
110+
}
111+
closePos += i
112+
113+
colorName := result[i+1 : closePos]
114+
115+
// Skip if this contains a slash (it's a closing tag)
116+
if strings.Contains(colorName, "/") {
117+
continue
118+
}
119+
120+
// Find the corresponding closing tag
121+
closeTag := "{/" + colorName + "}"
122+
closeStart := strings.Index(result[closePos+1:], closeTag)
123+
if closeStart == -1 {
124+
continue
125+
}
126+
closeStart += closePos + 1
127+
128+
// Check if this is innermost (no opening tags in between)
129+
content := result[closePos+1 : closeStart]
130+
if strings.Contains(content, "{") && !strings.HasPrefix(strings.TrimSpace(content[strings.Index(content, "{"):]), "/") {
131+
// This contains other opening tags, skip for now
132+
continue
133+
}
134+
135+
// This is an innermost tag, process it
136+
fullMatch := result[i : closeStart+len(closeTag)]
137+
138+
colorNameLower := strings.ToLower(colorName)
139+
if color, exists := colorMap[colorNameLower]; exists {
140+
coloredText := text.Colors{color}.Sprint(content)
141+
result = strings.Replace(result, fullMatch, coloredText, 1)
142+
changed = true
143+
break
144+
} else {
145+
// If color doesn't exist, just remove the tags
146+
result = strings.Replace(result, fullMatch, content, 1)
147+
changed = true
148+
break
149+
}
150+
}
151+
152+
if !changed {
153+
break
154+
}
155+
}
156+
157+
return result
65158
}

forms/util_test.go

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
// Copyright (c) 2023-2024, R.I. Pienaar and the Choria Project contributors
2+
//
3+
// SPDX-License-Identifier: Apache-2.0
4+
5+
package forms
6+
7+
import (
8+
"github.com/jedib0t/go-pretty/v6/text"
9+
. "github.com/onsi/ginkgo/v2"
10+
. "github.com/onsi/gomega"
11+
)
12+
13+
var _ = Describe("ColorMarkup", func() {
14+
Describe("colorMarkup function", func() {
15+
It("should handle no color markup", func() {
16+
input := "Hello World"
17+
expected := "Hello World"
18+
result := colorMarkup(input)
19+
Expect(result).To(Equal(expected))
20+
})
21+
22+
It("should handle single color tag", func() {
23+
input := "{red}Hello{/red} World"
24+
expected := text.Colors{text.FgRed}.Sprint("Hello") + " World"
25+
result := colorMarkup(input)
26+
Expect(result).To(Equal(expected))
27+
})
28+
29+
It("should handle multiple color tags", func() {
30+
input := "{red}Hello{/red} {blue}World{/blue}"
31+
expected := text.Colors{text.FgRed}.Sprint("Hello") + " " + text.Colors{text.FgBlue}.Sprint("World")
32+
result := colorMarkup(input)
33+
Expect(result).To(Equal(expected))
34+
})
35+
36+
It("should handle nested color tags", func() {
37+
input := "{red}Outer {green}Inner{/green} Text{/red}"
38+
expected := text.Colors{text.FgRed}.Sprint("Outer " + text.Colors{text.FgGreen}.Sprint("Inner") + " Text")
39+
result := colorMarkup(input)
40+
Expect(result).To(Equal(expected))
41+
})
42+
43+
It("should handle case insensitive colors", func() {
44+
input := "{RED}Hello{/RED} {Blue}World{/Blue}"
45+
expected := text.Colors{text.FgRed}.Sprint("Hello") + " " + text.Colors{text.FgBlue}.Sprint("World")
46+
result := colorMarkup(input)
47+
Expect(result).To(Equal(expected))
48+
})
49+
50+
It("should handle high intensity colors", func() {
51+
input := "{hired}Error{/hired} {higreen}Success{/higreen}"
52+
expected := text.Colors{text.FgHiRed}.Sprint("Error") + " " + text.Colors{text.FgHiGreen}.Sprint("Success")
53+
result := colorMarkup(input)
54+
Expect(result).To(Equal(expected))
55+
})
56+
57+
It("should remove invalid color tags", func() {
58+
input := "{invalid}Text{/invalid}"
59+
expected := "Text"
60+
result := colorMarkup(input)
61+
Expect(result).To(Equal(expected))
62+
})
63+
64+
It("should handle mixed valid and invalid colors", func() {
65+
input := "{red}Valid{/red} {invalid}Invalid{/invalid} {blue}Another{/blue}"
66+
expected := text.Colors{text.FgRed}.Sprint("Valid") + " Invalid " + text.Colors{text.FgBlue}.Sprint("Another")
67+
result := colorMarkup(input)
68+
Expect(result).To(Equal(expected))
69+
})
70+
71+
It("should handle empty color tag", func() {
72+
input := "{red}{/red}"
73+
expected := text.Colors{text.FgRed}.Sprint("")
74+
result := colorMarkup(input)
75+
Expect(result).To(Equal(expected))
76+
})
77+
78+
It("should handle all standard colors", func() {
79+
input := "{black}black{/black} {red}red{/red} {green}green{/green} {yellow}yellow{/yellow} {blue}blue{/blue} {magenta}magenta{/magenta} {cyan}cyan{/cyan} {white}white{/white}"
80+
expected := text.Colors{text.FgBlack}.Sprint("black") + " " +
81+
text.Colors{text.FgRed}.Sprint("red") + " " +
82+
text.Colors{text.FgGreen}.Sprint("green") + " " +
83+
text.Colors{text.FgYellow}.Sprint("yellow") + " " +
84+
text.Colors{text.FgBlue}.Sprint("blue") + " " +
85+
text.Colors{text.FgMagenta}.Sprint("magenta") + " " +
86+
text.Colors{text.FgCyan}.Sprint("cyan") + " " +
87+
text.Colors{text.FgWhite}.Sprint("white")
88+
result := colorMarkup(input)
89+
Expect(result).To(Equal(expected))
90+
})
91+
92+
It("should handle complex nesting and preserve all text content", func() {
93+
input := "{red}Start {blue}Middle {green}End{/green} More{/blue} Final{/red}"
94+
result := colorMarkup(input)
95+
96+
// The function should process innermost tags first
97+
// This is a complex case that tests the iterative processing
98+
Expect(result).To(ContainSubstring("Start"))
99+
Expect(result).To(ContainSubstring("Middle"))
100+
Expect(result).To(ContainSubstring("End"))
101+
Expect(result).To(ContainSubstring("More"))
102+
Expect(result).To(ContainSubstring("Final"))
103+
})
104+
})
105+
})

go.mod

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,13 @@ require (
2727
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
2828
github.com/google/go-cmp v0.7.0 // indirect
2929
github.com/google/pprof v0.0.0-20250501235452-c0086092b71a // indirect
30+
github.com/jedib0t/go-pretty/v6 v6.6.7 // indirect
3031
github.com/mattn/go-colorable v0.1.14 // indirect
3132
github.com/mattn/go-isatty v0.0.20 // indirect
33+
github.com/mattn/go-runewidth v0.0.16 // indirect
3234
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
3335
github.com/mitchellh/reflectwalk v1.0.2 // indirect
36+
github.com/rivo/uniseg v0.4.7 // indirect
3437
go.uber.org/automaxprocs v1.6.0 // indirect
3538
golang.org/x/net v0.40.0 // indirect
3639
golang.org/x/sys v0.33.0 // indirect

go.sum

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u
3333
github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68=
3434
github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI=
3535
github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
36+
github.com/jedib0t/go-pretty/v6 v6.6.7 h1:m+LbHpm0aIAPLzLbMfn8dc3Ht8MW7lsSO4MPItz/Uuo=
37+
github.com/jedib0t/go-pretty/v6 v6.6.7/go.mod h1:YwC5CE4fJ1HFUDeivSV1r//AmANFHyqczZk+U6BDALU=
3638
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
3739
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
3840
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
@@ -45,6 +47,8 @@ github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stg
4547
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
4648
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
4749
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
50+
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
51+
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
4852
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
4953
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI=
5054
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
@@ -60,6 +64,9 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
6064
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
6165
github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
6266
github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=
67+
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
68+
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
69+
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
6370
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
6471
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
6572
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=

scaffold.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,15 @@ import (
88
"bytes"
99
"errors"
1010
"fmt"
11-
"github.com/choria-io/scaffold/internal/sprig"
12-
"github.com/kballard/go-shellquote"
1311
"io/fs"
1412
"os"
1513
"os/exec"
1614
"path/filepath"
1715
"strings"
1816
"text/template"
17+
18+
"github.com/choria-io/scaffold/internal/sprig"
19+
"github.com/kballard/go-shellquote"
1920
)
2021

2122
// Config configures a scaffolding operation
@@ -24,6 +25,8 @@ type Config struct {
2425
TargetDirectory string `yaml:"target"`
2526
// SourceDirectory reads templates from a directory, mutually exclusive with Source
2627
SourceDirectory string `yaml:"source_directory"`
28+
// MergeTargetDirectory writes into existing target directories
29+
MergeTargetDirectory bool `yaml:"merge_target_directory"`
2730
// Source reads templates from in-process memory
2831
Source map[string]any `yaml:"source"`
2932
// Post configures post-processing of files using filepath globs

0 commit comments

Comments
 (0)