Skip to content

Commit 2b16a44

Browse files
committed
Simplify markdown codeblock tests, glamour helper
This commit is focused on PR feedback around codeblock testing and simplifying related code: 1. Use of new `WithOptions(...TermRendererOption) TermRendererOption` to clean up `WithTheme()` The `glamour` TermRendererOption pattern has a limitation that users cannot compose multiple options without duplicating code or building one-off anonymous functions. However, this commit takes advantage of an enhancement in cli/glamour#3 that allows users to leverage a helper to avoid building one-offs. 1. Use of new `leaanthony/go-ansi-parser` dependency for parsing ANSI escape sequences and display attributes In v1 of `markdown_test.go`, the codeblock tests were very simple, testing the result of output of markdown rendering against a string of ANSI escape sequences. The concern raised is that this was testing the result rather than behavior wanted. In v2 of `markdown_test.go`, the codeblock tests were refactored to use regex to extract and analyze ANSI escape sequences and display attributes. The concern raised was that this was a lot of logic to build and maintain and might benefit from a dependency that could do it. In v3 of `markdown_test.go`, a combination of v1 and v2 approaches for 1) testing that theme appropriate colors are used and 2) testing that ensures accessible display options are used when accessible experience is enabled
1 parent c5336a5 commit 2b16a44

File tree

4 files changed

+61
-94
lines changed

4 files changed

+61
-94
lines changed

go.mod

+3-2
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,16 @@ go 1.21
55
require (
66
github.com/AlecAivazis/survey/v2 v2.3.7
77
github.com/MakeNowJust/heredoc v1.0.0
8+
github.com/alecthomas/chroma/v2 v2.14.0
89
github.com/charmbracelet/lipgloss v1.0.0
910
github.com/cli/browser v1.3.0
10-
github.com/cli/glamour v0.0.0-20250220192152-8544502ccff9
11+
github.com/cli/glamour v0.0.0-20250225134531-b1d96ed4a7e1
1112
github.com/cli/safeexec v1.0.0
1213
github.com/cli/shurcooL-graphql v0.0.4
1314
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
1415
github.com/henvic/httpretty v0.0.6
1516
github.com/itchyny/gojq v0.12.15
17+
github.com/leaanthony/go-ansi-parser v1.6.1
1618
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d
1719
github.com/muesli/reflow v0.3.0
1820
github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a
@@ -26,7 +28,6 @@ require (
2628
)
2729

2830
require (
29-
github.com/alecthomas/chroma/v2 v2.14.0 // indirect
3031
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
3132
github.com/aymerick/douceur v0.2.0 // indirect
3233
github.com/charmbracelet/x/ansi v0.8.0 // indirect

go.sum

+6
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ github.com/cli/browser v1.3.0 h1:LejqCrpWr+1pRqmEPDGnTZOjsMe7sehifLynZJuqJpo=
2626
github.com/cli/browser v1.3.0/go.mod h1:HH8s+fOAxjhQoBUAsKuPCbqUuxZDhQ2/aD+SzsEfBTk=
2727
github.com/cli/glamour v0.0.0-20250220192152-8544502ccff9 h1:fPJNUzG+Au+pIfYx2c5QNngZ3KLj7xAzjutL6efy1x8=
2828
github.com/cli/glamour v0.0.0-20250220192152-8544502ccff9/go.mod h1:bB4uNJ5F0+nzpGqwlhKEy7tD0PPL0SxNWEwZzG77vMg=
29+
github.com/cli/glamour v0.0.0-20250225134531-b1d96ed4a7e1 h1:TrV+Bs2RKR/CeodJDRO6GXzIxT7PAOwbuxth8ACcucg=
30+
github.com/cli/glamour v0.0.0-20250225134531-b1d96ed4a7e1/go.mod h1:bB4uNJ5F0+nzpGqwlhKEy7tD0PPL0SxNWEwZzG77vMg=
2931
github.com/cli/safeexec v1.0.0 h1:0VngyaIyqACHdcMNWfo6+KdUYnqEr2Sg+bSP1pdF+dI=
3032
github.com/cli/safeexec v1.0.0/go.mod h1:Z/D4tTN8Vs5gXYHDCbaM1S/anmEDnJb1iW0+EJ5zx3Q=
3133
github.com/cli/shurcooL-graphql v0.0.4 h1:6MogPnQJLjKkaXPyGqPRXOI2qCsQdqNfUY1QSJu2GuY=
@@ -60,8 +62,12 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN
6062
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
6163
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
6264
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
65+
github.com/leaanthony/go-ansi-parser v1.6.1 h1:xd8bzARK3dErqkPFtoF9F3/HgN8UQk0ed1YDKpEz01A=
66+
github.com/leaanthony/go-ansi-parser v1.6.1/go.mod h1:+vva/2y4alzVmmIEpk9QDhA7vLC5zKDTRwfZGOp3IWU=
6367
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
6468
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
69+
github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE=
70+
github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
6571
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
6672
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
6773
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=

pkg/markdown/markdown.go

+4-14
Original file line numberDiff line numberDiff line change
@@ -39,20 +39,10 @@ func WithTheme(theme string) glamour.TermRendererOption {
3939
switch theme {
4040
case "light", "dark":
4141
if accessible {
42-
// Applying multiple glamour.TermRendererOption here requires a wrapper that applies each
43-
// within glamour.NewTermRenderer() in Render() below.
44-
stylesOption := glamour.WithStyles(AccessibleStyleConfig(theme))
45-
chromaOption := glamour.WithChromaFormatter("terminal16")
46-
47-
return func(tr *glamour.TermRenderer) error {
48-
if err := stylesOption(tr); err != nil {
49-
return err
50-
}
51-
if err := chromaOption(tr); err != nil {
52-
return err
53-
}
54-
return nil
55-
}
42+
return glamour.WithOptions(
43+
glamour.WithStyles(AccessibleStyleConfig(theme)),
44+
glamour.WithChromaFormatter("terminal16"),
45+
)
5646
}
5747
style = theme
5848
default:

pkg/markdown/markdown_test.go

+48-78
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,13 @@ import (
44
"fmt"
55
"os"
66
"path/filepath"
7-
"regexp"
8-
"strconv"
97
"strings"
108
"testing"
119

1210
"github.com/MakeNowJust/heredoc"
11+
"github.com/alecthomas/chroma/v2/styles"
1312
"github.com/cli/go-gh/v2/pkg/accessibility"
13+
ansi "github.com/leaanthony/go-ansi-parser"
1414
"github.com/stretchr/testify/assert"
1515
"github.com/stretchr/testify/require"
1616
)
@@ -22,25 +22,11 @@ const (
2222
customH2_8bitColorSeq = "\x1b[38;5;61;"
2323
magenta_4bitColorSeq = "\x1b[35;"
2424
brightMagenta_4bitColorSeq = "\x1b[95;"
25-
26-
// TODO: Include a little more context on SGR including link to https://en.wikipedia.org/wiki/ANSI_escape_code#Select_Graphic_Rendition_parameters for more info
27-
// sgrSequencePattern identifies ANSI escape sequences containing display attributes
28-
// that affect the color, emphasis, and other aspects of displaying text.
29-
sgrSequencePattern = `\x1b\[(.+?)m`
30-
31-
// sgrAttributePattern analyzes separate display attributes within an ANSI escape sequence
32-
// for detecting color depth (3-bit, 4-bit, 8-bit, 24-bit, etc) or other effects.
33-
//
34-
// This is a separate regex from sgrSequencePattern as `FindAllStringSubmatch()` does not
35-
// handle repeating capture groups well.
36-
sgrAttributePattern = `;?(?P<sequence>\d+)` // TODO: change the `sequence` note; if we aren't actually using the name group, remove it
3725
)
3826

3927
func Test_Render_Codeblocks(t *testing.T) {
4028
t.Setenv("GLAMOUR_STYLE", "")
4129

42-
sequencesRegex := regexp.MustCompile(sgrSequencePattern)
43-
attributesRegex := regexp.MustCompile(sgrAttributePattern)
4430
text := heredoc.Docf(`
4531
%[1]s%[1]s%[1]sgo
4632
package main
@@ -92,64 +78,14 @@ func Test_Render_Codeblocks(t *testing.T) {
9278

9379
out, err := Render(tt.text, WithTheme(tt.theme))
9480
require.NoError(t, err)
95-
sequences := sequencesRegex.FindAllStringSubmatch(out, -1)
96-
require.NotEmpty(t, sequences, "Failed to find expected SGR sequences in rendered output")
97-
98-
// TODO: Review use of a module like https://github.com/leaanthony/go-ansi-parser/blob/main/ansi.go#L264 to do the sequence and attribute parsing such that we just have to iterate over the results
99-
for _, sequence := range sequences {
100-
attributes := attributesRegex.FindAllStringSubmatch(sequence[1], -1)
101-
require.NotEmpty(t, attributes, "Failed to extract SGR attributes for testing")
102-
103-
// Analysis loop handles index incrementing due to unique display attribute situations like color depth
104-
for i := 0; i < len(attributes); {
105-
// TODO: Use constants for 38,48 and maybe remove the int conversion
106-
attribute, err := strconv.Atoi(attributes[i][1])
107-
require.NoError(t, err, "Failed to convert SGR attribute for testing")
108-
109-
switch attribute {
110-
case 38, 48:
111-
// Display attributes for setting 8-bit and 24-bit foreground and background colors
112-
colorDepth, err := strconv.Atoi(attributes[i+1][1])
113-
require.NoError(t, err, "Failed to convert SGR color depth attribute for testing")
114-
115-
switch colorDepth {
116-
case 2:
117-
// 24-bit color display attribute form
118-
// - ESC[38;2;⟨r⟩;⟨g⟩;⟨b⟩m for foreground colors
119-
// - ESC[48;2;⟨r⟩;⟨g⟩;⟨b⟩m for background colors
120-
require.False(t, tt.accessible, "24-bit color is not accessible, customizable")
121-
122-
color24bitRed, err := strconv.Atoi(attributes[i+2][1])
123-
require.NoError(t, err, "Failed to convert 24-bit red color value for testing")
124-
require.True(t, color24bitRed >= 0 && color24bitRed <= 255, "24-bit red color value out of 0-255 range")
125-
126-
color24bitGreen, err := strconv.Atoi(attributes[i+3][1])
127-
require.NoError(t, err, "Failed to convert 24-bit green color value for testing")
128-
require.True(t, color24bitGreen >= 0 && color24bitGreen <= 255, "24-bit green color value out of 0-255 range")
129-
130-
color24bitBlue, err := strconv.Atoi(attributes[i+4][1])
131-
require.NoError(t, err, "Failed to convert 24-bit blue color value for testing")
132-
require.True(t, color24bitBlue >= 0 && color24bitBlue <= 255, "24-bit blue color value out of 0-255 range")
133-
134-
i += 5
135-
case 5:
136-
// 8-bit color display attributes form:
137-
// - ESC[38;5;⟨n⟩m for foreground colors
138-
// - ESC[48;5;⟨n⟩m for background colors
139-
require.False(t, tt.accessible, "8-bit color is not accessible, customizable")
140-
141-
color8bit, err := strconv.Atoi(attributes[i+2][1])
142-
require.NoError(t, err, "Failed to convert 8-bit color value for testing")
143-
require.True(t, color8bit >= 0 && color8bit <= 255, "8-bit color value out of 0-255 range")
144-
145-
i += 3
146-
default:
147-
require.Fail(t, "Unexpected color depth in attribute")
148-
}
149-
default:
150-
// Increment index as this attribute does not affect accessibility currently
151-
i += 1
152-
}
81+
82+
styledText, err := ansi.Parse(out)
83+
require.NoError(t, err)
84+
85+
for _, st := range styledText {
86+
if tt.accessible {
87+
require.Equalf(t, st.ColourMode, ansi.Default, "Inaccessible color found in '%s' at %d", st, st.Offset)
88+
require.Falsef(t, st.Faint(), "Inaccessible style found in '%s' at %d", st, st.Offset)
15389
}
15490
}
15591
})
@@ -161,7 +97,12 @@ func Test_Render_Codeblocks(t *testing.T) {
16197
// match. For more information on ANSI color codes, see
16298
// https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit
16399
func Test_Render(t *testing.T) {
164-
t.Setenv("GLAMOUR_STYLE", "")
100+
codeBlock := heredoc.Docf(`
101+
%[1]s%[1]s%[1]sgo
102+
fmt.Println("Hello, world!")
103+
%[1]s%[1]s%[1]s
104+
`, "`")
105+
165106
tests := []struct {
166107
name string
167108
text string
@@ -209,14 +150,43 @@ func Test_Render(t *testing.T) {
209150
accessibleEnvVar: "true",
210151
wantOut: fmt.Sprintf("%s1mh2", brightMagenta_4bitColorSeq),
211152
},
153+
{
154+
name: "when the light theme is selected, the codeblock renders using 8-bit colors",
155+
text: codeBlock,
156+
theme: "light",
157+
wantOut: "\x1b[0m\x1b[38;5;235mfmt\x1b[0m\x1b[38;5;210m.\x1b[0m\x1b[38;5;35mPrintln\x1b[0m\x1b[38;5;210m(\x1b[0m\x1b[38;5;95m\"Hello, world!\"\x1b[0m\x1b[38;5;210m)\x1b[0m",
158+
},
159+
{
160+
name: "when the dark theme is selected, the codeblock renders using 8-bit colors",
161+
text: codeBlock,
162+
theme: "dark",
163+
wantOut: "\x1b[0m\x1b[38;5;251mfmt\x1b[0m\x1b[38;5;187m.\x1b[0m\x1b[38;5;42mPrintln\x1b[0m\x1b[38;5;187m(\x1b[0m\x1b[38;5;173m\"Hello, world!\"\x1b[0m\x1b[38;5;187m)\x1b[0m",
164+
},
165+
{
166+
name: "when the accessible env var is set and the light theme is selected, the codeblock renders using 4-bit colors",
167+
text: codeBlock,
168+
theme: "light",
169+
accessibleEnvVar: "true",
170+
wantOut: "\x1b[0m\x1b[30mfmt\x1b[0m\x1b[33m.\x1b[0m\x1b[36mPrintln\x1b[0m\x1b[33m(\x1b[0m\x1b[90m\"Hello, world!\"\x1b[0m\x1b[33m)\x1b[0m",
171+
},
172+
{
173+
name: "when the accessible env var is set and the dark theme is selected, the codeblock renders using 4-bit colors",
174+
text: codeBlock,
175+
theme: "dark",
176+
accessibleEnvVar: "true",
177+
wantOut: "\x1b[0m\x1b[37mfmt\x1b[0m\x1b[37m.\x1b[0m\x1b[36mPrintln\x1b[0m\x1b[37m(\x1b[0m\x1b[33m\"Hello, world!\"\x1b[0m\x1b[37m)\x1b[0m",
178+
},
212179
}
213180
for _, tt := range tests {
214181
t.Run(tt.name, func(t *testing.T) {
182+
// Unregister cached chroma style causing codeblock tests to fail based on previous theme
183+
delete(styles.Registry, "charm")
215184
t.Setenv(accessibility.ACCESSIBILITY_ENV, tt.accessibleEnvVar)
216185

217-
if tt.styleEnvVar != "" {
218-
tmpDir := t.TempDir()
219-
path := filepath.Join(tmpDir, fmt.Sprintf("%s.json", tt.styleEnvVar))
186+
if tt.styleEnvVar == "" {
187+
t.Setenv("GLAMOUR_STYLE", "")
188+
} else {
189+
path := filepath.Join(t.TempDir(), fmt.Sprintf("%s.json", tt.styleEnvVar))
220190
err := os.WriteFile(path, []byte(customGlamourStyle(t)), 0644)
221191
if err != nil {
222192
t.Fatal(err)

0 commit comments

Comments
 (0)